mAi: #82 - deadline form overhaul: type-modal filter chips, type→rule autofill, Auto mode, Standardtitel

t-paliad-251. Four bundled concerns from m's 2026-05-25 reports, one
worker, one branch.

Part 1 — Event-type browse modal (search + filters)
- Modal already had a search input; added court-type filter chips
  (UPC / EPA / DPMA / DE / Allgemein) under the search.
- Chips render only the jurisdictions actually present in the data;
  any future flavour lands at the end of the row.
- Active chip uses the lime-tint chip palette already established by
  the .event-type-collapsed* family (t-paliad-165).
- Search input keeps autofocus; chip + search filters intersect.

Part 2 — Type → Rule auto-fill + sort options
- Inverted the existing rule.concept_default_event_type_id mapping
  client-side: given a chosen event_type X, candidate rules are
  those with concept_default_event_type_id === X.
- Resolution picks (1) exact match on the project's
  proceeding_type_id, (2) jurisdiction match on the rule's
  proceeding (EPA→EPO canonicalised), (3) first candidate.
- Sort dropdown next to the Rule label: by proceeding sequence,
  by court (jurisdiction grouping with optgroup), alphabetical.
  Defaults to "by court"; localStorage-persisted per browser.
- All sorts are client-side over the existing /api/deadline-rules
  payload — no new endpoint.

Part 3 — Auto rule mode + clearer override warning
- Auto badge (.form-hint--auto, lime-tint pill + " — <rule name>")
  surfaces whenever the Rule was derived from the chosen Type.
  Disappears the moment the user manually picks a different rule.
- Override warning names BOTH sides + the actually-applied rule:
  "Typ ergibt Regel: X. Gewählte Regel: Y. Es wird Y angewendet."
- Symmetric `lastAutoFilledRuleID` sticky-replace flag mirrors the
  existing `lastAutoFilledEventTypeID` (t-paliad-165) so the auto-
  fill only replaces its own previous suggestion, never a manual
  pick.
- Collapsed Typ view (t-paliad-165) is suppressed when the rule was
  auto-derived from the type — the "vorgegeben durch Regel" copy
  reads backwards in that case; show picker + Auto badge instead.

Part 4 — Standardtitel button (create + edit)
- Button rendered next to the Title field on both /deadlines/new
  and /deadlines/{id} (edit mode only).
- Recipe (recipe-docs-here-so-future-templates-can-mirror-it):
    head =
      1. event_type label (if exactly one Typ chip is set)
      2. rule code+name (when a Rule is set — "RoP.023 — Klageerwiderung")
      3. proceeding type name from project (create form only)
      4. fallback: t("deadlines.field.title.default_fallback")
    suffix = " — <project.reference>" when ref is set and not
             already in head.
  Examples:
    Klageerwiderung — C-UPC-0042       (type known)
    RoP.023 — Klageerwiderung — REF    (rule known, no type)
    UPC — Verletzungsverfahren — REF   (only proceeding type)
    Neue Frist — REF                   (fallback)
- Click REPLACES current title; no destructive confirmation
  because the user invoked it explicitly. Focus moves into the
  title input afterwards so the user can fine-tune.

Build hygiene:
- go build + go vet + go test ./internal/... clean.
- frontend/build.ts clean (2786 keys, +10 new DE+EN, scan clean).
- All changes client-side / CSS / i18n + 2 small TSX edits; no
  schema, no service, no migration.

Files touched:
- frontend/src/client/event-types.ts (browse-modal chips)
- frontend/src/client/deadlines-new.ts (rewrite — Type→Rule, sort,
  Auto badge, override warn, Standardtitel)
- frontend/src/client/deadlines-detail.ts (edit-mode Standardtitel
  + show/hide on enter/exit edit)
- frontend/src/deadlines-new.tsx (label-row + sort dropdown + Auto
  badge slot + override-warn slot + Standardtitel button)
- frontend/src/deadlines-detail.tsx (Standardtitel button)
- frontend/src/styles/global.css (.event-type-browse-chip*,
  .form-hint--auto, .form-hint-badge, .form-field-label-row,
  .btn-link-action, .rule-sort-select)
- frontend/src/client/i18n.ts (+10 keys DE+EN)
This commit is contained in:
mAi
2026-05-25 14:03:04 +02:00
parent 898348a64a
commit 8caaf6a631
8 changed files with 696 additions and 29 deletions

View File

@@ -366,6 +366,7 @@ function initEdit() {
const etEdit = document.getElementById("deadline-event-types-edit"); const etEdit = document.getElementById("deadline-event-types-edit");
const projectLink = document.getElementById("deadline-project-link") as HTMLAnchorElement; const projectLink = document.getElementById("deadline-project-link") as HTMLAnchorElement;
const projectEdit = document.getElementById("deadline-project-edit") as HTMLSelectElement | null; const projectEdit = document.getElementById("deadline-project-edit") as HTMLSelectElement | null;
const titleDefaultBtn = document.getElementById("deadline-title-default-btn") as HTMLButtonElement | null;
function enterEdit() { function enterEdit() {
titleDisplay.style.display = "none"; titleDisplay.style.display = "none";
@@ -381,6 +382,7 @@ function initEdit() {
projectEdit.style.display = ""; projectEdit.style.display = "";
projectEdit.value = deadline.project_id; projectEdit.value = deadline.project_id;
} }
if (titleDefaultBtn) titleDefaultBtn.style.display = "";
saveBtn.style.display = ""; saveBtn.style.display = "";
editBtn.style.display = "none"; editBtn.style.display = "none";
titleEdit.focus(); titleEdit.focus();
@@ -399,12 +401,41 @@ function initEdit() {
projectEdit.style.display = "none"; projectEdit.style.display = "none";
projectLink.style.display = ""; projectLink.style.display = "";
} }
if (titleDefaultBtn) titleDefaultBtn.style.display = "none";
saveBtn.style.display = "none"; saveBtn.style.display = "none";
editBtn.style.display = ""; editBtn.style.display = "";
} }
editBtn.addEventListener("click", enterEdit); editBtn.addEventListener("click", enterEdit);
// t-paliad-251 Part 4 — Standardtitel button.
// Recipe (mirror of computeDefaultTitle in deadlines-new.ts):
// head = event_type label (if exactly one Typ chip is in edit)
// || rule code+name (when deadline carries a rule)
// || "Neue Frist" fallback
// suffix = " — <project.reference>" when not already in head
titleDefaultBtn?.addEventListener("click", () => {
if (!deadline) return;
let head = "";
const ids = eventTypePicker?.getIDs() ?? deadline.event_type_ids ?? [];
if (ids.length === 1) {
const et = eventTypeByID.get(ids[0]);
if (et) head = eventTypeLabel(et);
}
if (!head && rule) {
const code = rule.rule_code || rule.code || "";
head = code ? `${code}${rule.name}` : rule.name;
}
if (!head && deadline.rule_code) {
head = deadline.rule_code;
}
if (!head) head = t("deadlines.field.title.default_fallback");
const ref = project?.reference?.trim() || "";
if (ref && !head.includes(ref)) head = `${head}${ref}`;
titleEdit.value = head;
titleEdit.focus();
});
saveBtn.addEventListener("click", async () => { saveBtn.addEventListener("click", async () => {
if (!deadline) return; if (!deadline) return;
const newTitle = titleEdit.value.trim(); const newTitle = titleEdit.value.trim();

View File

@@ -1,4 +1,4 @@
import { initI18n, t, tDyn } from "./i18n"; import { initI18n, t, tDyn, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
import { import {
attachEventTypePicker, attachEventTypePicker,
@@ -24,6 +24,10 @@ interface Project {
reference?: string | null; reference?: string | null;
title: string; title: string;
path: string; path: string;
// t-paliad-251 — used by Type→Rule autofill to narrow rule candidates
// to the project's own proceeding. Optional because not every project
// is a case/proceeding (clients + matters carry no proceeding type).
proceeding_type_id?: number | null;
} }
interface DeadlineRule { interface DeadlineRule {
@@ -32,15 +36,32 @@ interface DeadlineRule {
name: string; name: string;
name_en: string; name_en: string;
rule_code?: string; rule_code?: string;
proceeding_type_id?: number | null;
sequence_order?: number;
// t-paliad-165 — canonical event_type for this rule's concept, // t-paliad-165 — canonical event_type for this rule's concept,
// hydrated server-side from paliad.deadline_concept_event_types. // hydrated server-side from paliad.deadline_concept_event_types.
// Drives auto-fill of the Typ chip when the user picks this rule. // Drives auto-fill of the Typ chip when the user picks this rule,
// AND is inverted to power Typ→Regel auto-fill (t-paliad-251 Part 2):
// given a chosen event_type X, candidate rules are those whose
// concept_default_event_type_id === X.
concept_default_event_type_id?: string | null; concept_default_event_type_id?: string | null;
} }
interface ProceedingType {
id: number;
code: string;
name: string;
name_en?: string;
jurisdiction: string;
sort_order?: number;
}
// Rules indexed by id so the Regel-change handler can look up the // Rules indexed by id so the Regel-change handler can look up the
// concept's canonical event_type without re-fetching. // concept's canonical event_type without re-fetching.
let rulesByID = new Map<string, DeadlineRule>(); let rulesByID = new Map<string, DeadlineRule>();
let allRules: DeadlineRule[] = [];
let proceedingTypesByID = new Map<number, ProceedingType>();
let projectsByID = new Map<string, Project>();
// Last event_type the rule auto-filled. Tracked so we can tell whether // Last event_type the rule auto-filled. Tracked so we can tell whether
// the picker still reflects the rule's suggestion (replace silently on // the picker still reflects the rule's suggestion (replace silently on
@@ -48,6 +69,17 @@ let rulesByID = new Map<string, DeadlineRule>();
// surface the mismatch warning instead). // surface the mismatch warning instead).
let lastAutoFilledEventTypeID: string | null = null; let lastAutoFilledEventTypeID: string | null = null;
// t-paliad-251 — symmetric flag for the inverse direction. Tracks the
// rule ID we most recently injected as the Auto-derived default for the
// chosen event_type, so we can replace it silently when the user picks
// a different type but leave manual rule picks alone.
let lastAutoFilledRuleID: string | null = null;
// Current sort mode for the Rule select. Persisted to localStorage so
// repeat-form users don't have to re-pick their preferred ordering.
type RuleSort = "by_proceeding" | "by_court" | "alpha";
const RULE_SORT_KEY = "paliad.deadline.rule.sort";
let preselectedProjectID = ""; let preselectedProjectID = "";
function esc(s: string): string { function esc(s: string): string {
@@ -62,6 +94,20 @@ function showError(msg: string) {
el.className = "form-msg form-msg-error"; el.className = "form-msg form-msg-error";
} }
function ruleLabel(r: DeadlineRule): string {
const lang = getLang();
const name = (lang === "en" && r.name_en) ? r.name_en : r.name;
const code = r.rule_code || r.code || "";
return code ? `${code}${name}` : name;
}
function proceedingLabel(pt: ProceedingType | undefined): string {
if (!pt) return "";
const lang = getLang();
const name = (lang === "en" && pt.name_en) ? pt.name_en : pt.name;
return `${pt.jurisdiction}${name}`;
}
async function loadProjects() { async function loadProjects() {
const sel = document.getElementById("deadline-project") as HTMLSelectElement; const sel = document.getElementById("deadline-project") as HTMLSelectElement;
const hint = document.getElementById("deadline-project-empty-hint")!; const hint = document.getElementById("deadline-project-empty-hint")!;
@@ -69,6 +115,7 @@ async function loadProjects() {
const resp = await fetch("/api/projects"); const resp = await fetch("/api/projects");
if (!resp.ok) return; if (!resp.ok) return;
const projects: Project[] = await resp.json(); const projects: Project[] = await resp.json();
projectsByID = new Map(projects.map((p) => [p.id, p]));
if (projects.length === 0) { if (projects.length === 0) {
hint.style.display = ""; hint.style.display = "";
hint.innerHTML = `${esc(t("deadlines.field.akte.empty"))} <a href="/projects/new">${esc(t("deadlines.field.akte.empty.link"))}</a>`; hint.innerHTML = `${esc(t("deadlines.field.akte.empty"))} <a href="/projects/new">${esc(t("deadlines.field.akte.empty.link"))}</a>`;
@@ -82,7 +129,7 @@ async function loadProjects() {
const ref = p.reference || ""; const ref = p.reference || "";
const indent = projectIndent(p.path); const indent = projectIndent(p.path);
options.push( options.push(
`<option value="${esc(p.id)}"${isSelected}>${indent}${esc(ref)} \u2014 ${esc(p.title)}</option>`, `<option value="${esc(p.id)}"${isSelected}>${indent}${esc(ref)} ${esc(p.title)}</option>`,
); );
} }
sel.innerHTML = options.join(""); sel.innerHTML = options.join("");
@@ -91,28 +138,186 @@ async function loadProjects() {
} }
} }
async function loadProceedingTypes() {
try {
const resp = await fetch("/api/proceeding-types-db");
if (!resp.ok) return;
const types: ProceedingType[] = await resp.json();
proceedingTypesByID = new Map(types.map((pt) => [pt.id, pt]));
} catch {
/* non-fatal — rule sort falls back to alpha when proceeding-type
metadata is missing */
}
}
async function loadRules() { async function loadRules() {
// Optional: load rules so user can attach. We pull all rules; small set. // Optional: load rules so user can attach. We pull all rules; small set.
const sel = document.getElementById("deadline-rule") as HTMLSelectElement;
try { try {
const resp = await fetch("/api/deadline-rules"); const resp = await fetch("/api/deadline-rules");
if (!resp.ok) return; if (!resp.ok) return;
const rules: DeadlineRule[] = await resp.json(); allRules = (await resp.json()) as DeadlineRule[];
rulesByID = new Map(rules.map((r) => [r.id, r])); rulesByID = new Map(allRules.map((r) => [r.id, r]));
const opts: string[] = [ renderRuleSelect();
`<option value="" data-i18n="deadlines.field.rule.none">${esc(t("deadlines.field.rule.none"))}</option>`,
];
for (const r of rules) {
const code = r.rule_code || r.code || "";
const label = code ? `${code} \u2014 ${r.name}` : r.name;
opts.push(`<option value="${esc(r.id)}">${esc(label)}</option>`);
}
sel.innerHTML = opts.join("");
} catch { } catch {
/* non-fatal — rule select stays at "no rule" */ /* non-fatal — rule select stays at "no rule" */
} }
} }
// renderRuleSelect rebuilds the Rule <select> from the current sort
// mode + the cached rule set. Called whenever the user changes the sort
// dropdown, when the language flips, or after rules + proceeding types
// finish loading. The "Keine Regel" sentinel always stays at the top.
function renderRuleSelect(): void {
const sel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
if (!sel) return;
const previous = sel.value;
const sort = readRuleSort();
const opts: string[] = [
`<option value="" data-i18n="deadlines.field.rule.none">${esc(t("deadlines.field.rule.none"))}</option>`,
];
if (sort === "alpha") {
const sorted = [...allRules].sort((a, b) => ruleLabel(a).localeCompare(ruleLabel(b)));
for (const r of sorted) {
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
}
} else if (sort === "by_court") {
// Group by proceeding_type.jurisdiction (UPC / EPA / DPMA / DE /
// other). Within each group, sort alpha by rule label so the user
// can scan a court's rules in stable order.
const byJurisdiction = new Map<string, DeadlineRule[]>();
for (const r of allRules) {
const pt = r.proceeding_type_id ? proceedingTypesByID.get(r.proceeding_type_id) : undefined;
const j = pt?.jurisdiction || t("event_types.browse.jurisdiction.none");
const list = byJurisdiction.get(j) ?? [];
list.push(r);
byJurisdiction.set(j, list);
}
const order = ["UPC", "EPA", "EPO", "DPMA", "DE"];
const keys = [...byJurisdiction.keys()].sort((a, b) => {
const ai = order.indexOf(a);
const bi = order.indexOf(b);
if (ai === -1 && bi === -1) return a.localeCompare(b);
if (ai === -1) return 1;
if (bi === -1) return -1;
return ai - bi;
});
for (const k of keys) {
const list = byJurisdiction.get(k)!.sort((a, b) => ruleLabel(a).localeCompare(ruleLabel(b)));
opts.push(`<optgroup label="${esc(k === "EPO" ? "EPA" : k)}">`);
for (const r of list) {
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
}
opts.push(`</optgroup>`);
}
} else {
// by_proceeding — group by proceeding_type, within each preserve the
// canonical sequence_order so the user reads "Klageerwiderung →
// Replik → Duplik → Verhandlung" in chronological order.
const byProceeding = new Map<number | string, DeadlineRule[]>();
const noProceedingKey = "__none__";
for (const r of allRules) {
const k: number | string = r.proceeding_type_id ?? noProceedingKey;
const list = byProceeding.get(k) ?? [];
list.push(r);
byProceeding.set(k, list);
}
const keys = [...byProceeding.keys()].sort((a, b) => {
if (a === noProceedingKey) return 1;
if (b === noProceedingKey) return -1;
const pa = proceedingTypesByID.get(a as number);
const pb = proceedingTypesByID.get(b as number);
const sa = pa?.sort_order ?? 9999;
const sb = pb?.sort_order ?? 9999;
if (sa !== sb) return sa - sb;
return (pa?.code ?? "").localeCompare(pb?.code ?? "");
});
for (const k of keys) {
const list = (byProceeding.get(k)!).slice().sort(
(a, b) => (a.sequence_order ?? 0) - (b.sequence_order ?? 0),
);
const pt = typeof k === "number" ? proceedingTypesByID.get(k) : undefined;
const groupLabel = pt ? proceedingLabel(pt) : t("deadlines.field.rule.sort.other_proceeding");
opts.push(`<optgroup label="${esc(groupLabel)}">`);
for (const r of list) {
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
}
opts.push(`</optgroup>`);
}
}
sel.innerHTML = opts.join("");
// Restore previous selection if it still exists in the new order.
if (previous && rulesByID.has(previous)) {
sel.value = previous;
}
}
function readRuleSort(): RuleSort {
try {
const raw = localStorage.getItem(RULE_SORT_KEY);
if (raw === "by_proceeding" || raw === "by_court" || raw === "alpha") return raw;
} catch {
/* non-fatal */
}
return "by_court";
}
function writeRuleSort(s: RuleSort): void {
try {
localStorage.setItem(RULE_SORT_KEY, s);
} catch {
/* non-fatal */
}
}
// resolveAutoRuleForType picks the best-match rule for the chosen event
// type, scoring by:
// 1. project's proceeding_type_id (if known) — exact match wins,
// 2. otherwise event_type.jurisdiction matches the rule's
// proceeding's jurisdiction (EPA→EPO canonicalised),
// 3. otherwise just the first candidate in the canonical ordering.
//
// Returns null when no rule maps to this event_type. The caller surfaces
// this as "no Auto rule available — pick one manually" rather than
// silently leaving the dropdown stuck on whatever the user picked before.
function resolveAutoRuleForType(eventTypeID: string, projectID: string): DeadlineRule | null {
const candidates = allRules.filter((r) => r.concept_default_event_type_id === eventTypeID);
if (candidates.length === 0) return null;
if (candidates.length === 1) return candidates[0];
const project = projectID ? projectsByID.get(projectID) : undefined;
if (project?.proceeding_type_id) {
const exact = candidates.find((r) => r.proceeding_type_id === project.proceeding_type_id);
if (exact) return exact;
}
const et = eventTypesByID.get(eventTypeID);
if (et?.jurisdiction && et.jurisdiction !== "any") {
const want = et.jurisdiction === "EPO" ? "EPA" : et.jurisdiction;
const jurMatch = candidates.find((r) => {
const pt = r.proceeding_type_id ? proceedingTypesByID.get(r.proceeding_type_id) : undefined;
return pt?.jurisdiction === want;
});
if (jurMatch) return jurMatch;
}
return candidates[0];
}
let preselectedProjectIDLocal = "";
function initBackLinks() {
if (preselectedProjectID) {
const back = document.getElementById("deadline-new-back") as HTMLAnchorElement;
const cancel = document.getElementById("deadline-new-cancel") as HTMLAnchorElement;
back.href = `/projects/${preselectedProjectID}/deadlines`;
cancel.href = `/projects/${preselectedProjectID}/deadlines`;
}
preselectedProjectIDLocal = preselectedProjectID;
}
// t-paliad-165 follow-up — drive the collapsed/expanded view of the Typ // t-paliad-165 follow-up — drive the collapsed/expanded view of the Typ
// picker. The two modes are mutually exclusive: // picker. The two modes are mutually exclusive:
// //
@@ -140,8 +345,14 @@ function refreshRuleView(): void {
const pickerMatchesDefault = const pickerMatchesDefault =
expected !== null && picked.length === 1 && picked[0] === expected; expected !== null && picked.length === 1 && picked[0] === expected;
// t-paliad-251 — when the rule was auto-derived from a user-picked
// type (Typ→Regel direction), the collapsed "vorgegeben durch Regel"
// copy reads backwards. Show the picker explicitly + surface the
// Auto badge on the Rule field instead.
const ruleWasAutoDerivedFromType =
lastAutoFilledRuleID !== null && ruleID === lastAutoFilledRuleID;
const wantsCollapsed = const wantsCollapsed =
!expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault; !expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault && !ruleWasAutoDerivedFromType;
if (wantsCollapsed) { if (wantsCollapsed) {
const et = eventTypesByID.get(expected!); const et = eventTypesByID.get(expected!);
@@ -164,6 +375,58 @@ function refreshRuleView(): void {
} }
} }
// refreshRuleAutoBadgeAndWarning surfaces the Auto badge whenever the
// Rule was derived from the Typ (i.e. lastAutoFilledRuleID is currently
// selected) AND the warning whenever the user has manually picked a
// non-Auto rule that contradicts the Type's derived rule. Both end up
// inert when there's no Type chosen.
function refreshRuleAutoBadgeAndWarning(): void {
const autoEl = document.getElementById("deadline-rule-auto-hint");
const autoTextEl = document.getElementById("deadline-rule-auto-hint-text");
const warnEl = document.getElementById("deadline-rule-override-warn");
if (!autoEl || !autoTextEl || !warnEl) return;
const ruleSel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
if (!ruleSel) return;
const picked = eventTypePicker?.getIDs() ?? [];
if (picked.length !== 1) {
autoEl.style.display = "none";
warnEl.style.display = "none";
return;
}
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
const derived = resolveAutoRuleForType(picked[0], projectID);
const currentRuleID = ruleSel.value || "";
if (currentRuleID && currentRuleID === lastAutoFilledRuleID) {
// The current rule was auto-derived (and the user hasn't touched it).
autoEl.style.display = "";
autoTextEl.textContent = derived ? `${ruleLabel(derived)}` : "";
warnEl.style.display = "none";
return;
}
autoEl.style.display = "none";
// Override warning: derived rule exists AND user has picked a
// different non-empty rule. The copy names BOTH so the user knows
// exactly what's happening — and which one will be applied.
if (derived && currentRuleID && currentRuleID !== derived.id) {
const current = rulesByID.get(currentRuleID);
if (current) {
const tmpl = t("deadlines.field.rule.override_warn");
const msg = tmpl
.replace("{derived}", ruleLabel(derived))
.replace("{selected}", ruleLabel(current));
warnEl.textContent = msg;
warnEl.style.display = "";
return;
}
}
warnEl.style.display = "none";
}
// applyRuleAutoFill replaces the picker silently when it still reflects // applyRuleAutoFill replaces the picker silently when it still reflects
// the previous rule's suggestion (or is empty); leaves a manually-edited // the previous rule's suggestion (or is empty); leaves a manually-edited
// picker alone. Called whenever the Regel select changes. // picker alone. Called whenever the Regel select changes.
@@ -200,13 +463,97 @@ function applyRuleAutoFill(): void {
refreshRuleView(); refreshRuleView();
} }
function initBackLinks() { // applyTypeAutoFillRule is the inverse direction (t-paliad-251 Part 2):
if (preselectedProjectID) { // when the user picks a single Typ chip, derive the canonical Rule and
const back = document.getElementById("deadline-new-back") as HTMLAnchorElement; // inject it into the Regel select. Like applyRuleAutoFill, it leaves
const cancel = document.getElementById("deadline-new-cancel") as HTMLAnchorElement; // manual rule picks alone — only replaces when the current rule is the
back.href = `/projects/${preselectedProjectID}/deadlines`; // previous auto-fill (sticky-replace pattern).
cancel.href = `/projects/${preselectedProjectID}/deadlines`; function applyTypeAutoFillRule(): void {
const ruleSel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
if (!ruleSel) return;
const picked = eventTypePicker?.getIDs() ?? [];
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
if (picked.length !== 1) {
// 0 or 2+ Typ chips → no canonical rule to derive. Clear the
// sticky auto-fill so a stale Auto suggestion doesn't linger.
if (lastAutoFilledRuleID && ruleSel.value === lastAutoFilledRuleID) {
ruleSel.value = "";
lastAutoFilledRuleID = null;
// Mirror to the Regel→Typ path so its mismatch warning recomputes.
applyRuleAutoFill();
}
refreshRuleAutoBadgeAndWarning();
return;
} }
const derived = resolveAutoRuleForType(picked[0], projectID);
const currentRuleID = ruleSel.value || "";
const ruleStillReflectsLastSuggestion =
lastAutoFilledRuleID !== null && currentRuleID === lastAutoFilledRuleID;
const ruleIsEmpty = currentRuleID === "";
if (derived) {
if (ruleIsEmpty || ruleStillReflectsLastSuggestion) {
ruleSel.value = derived.id;
lastAutoFilledRuleID = derived.id;
// Mirror to the Regel→Typ direction — the new rule's collapsed
// view + mismatch state needs to recompute now that we changed
// the selection programmatically.
applyRuleAutoFill();
}
} else if (ruleStillReflectsLastSuggestion) {
// No derived rule for the new type — drop the stale auto-fill.
ruleSel.value = "";
lastAutoFilledRuleID = null;
applyRuleAutoFill();
}
refreshRuleAutoBadgeAndWarning();
}
// computeDefaultTitle — t-paliad-251 Part 4. Recipe (documented also in
// the commit message so future title templates can mirror it):
//
// priority order picks the head of the title:
// 1. event_type label (when exactly one Typ chip is set)
// 2. rule name (when a Rule is set — uses ruleLabel = "code — name")
// 3. proceeding type name (when project carries a proceeding_type_id)
// 4. fallback: t("deadlines.field.title.default_fallback")
//
// suffix: " — <project-reference>" when the project has a reference
// string and the title doesn't already contain it.
//
// Returns "" only when even the fallback fails (i18n unavailable) —
// callers handle that by leaving the field untouched.
function computeDefaultTitle(): string {
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
const project = projectID ? projectsByID.get(projectID) : undefined;
const ruleID = (document.getElementById("deadline-rule") as HTMLSelectElement | null)?.value || "";
const rule = ruleID ? rulesByID.get(ruleID) : undefined;
const picked = eventTypePicker?.getIDs() ?? [];
let head = "";
if (picked.length === 1) {
const et = eventTypesByID.get(picked[0]);
if (et) head = eventTypeLabel(et);
}
if (!head && rule) {
head = ruleLabel(rule);
}
if (!head && project?.proceeding_type_id) {
const pt = proceedingTypesByID.get(project.proceeding_type_id);
if (pt) head = proceedingLabel(pt);
}
if (!head) {
head = t("deadlines.field.title.default_fallback");
}
const ref = project?.reference?.trim() || "";
if (ref && !head.includes(ref)) {
return `${head}${ref}`;
}
return head;
} }
async function submitForm(e: Event) { async function submitForm(e: Event) {
@@ -252,8 +599,8 @@ async function submitForm(e: Event) {
return; return;
} }
const created = await resp.json(); const created = await resp.json();
if (preselectedProjectID) { if (preselectedProjectIDLocal) {
window.location.href = `/projects/${preselectedProjectID}/deadlines`; window.location.href = `/projects/${preselectedProjectIDLocal}/deadlines`;
} else { } else {
window.location.href = `/deadlines/${created.id}`; window.location.href = `/deadlines/${created.id}`;
} }
@@ -343,12 +690,33 @@ document.addEventListener("DOMContentLoaded", async () => {
// Default due to today // Default due to today
const dueInput = document.getElementById("deadline-due") as HTMLInputElement; const dueInput = document.getElementById("deadline-due") as HTMLInputElement;
if (!dueInput.value) dueInput.value = new Date().toISOString().split("T")[0]; if (!dueInput.value) dueInput.value = new Date().toISOString().split("T")[0];
await Promise.all([loadProjects(), loadRules(), loadMe()]);
// Wire the sort dropdown to read its initial value from localStorage and
// persist user picks back.
const sortSel = document.getElementById("deadline-rule-sort") as HTMLSelectElement | null;
if (sortSel) {
sortSel.value = readRuleSort();
sortSel.addEventListener("change", () => {
writeRuleSort(sortSel.value as RuleSort);
renderRuleSelect();
});
}
await Promise.all([loadProjects(), loadProceedingTypes(), loadRules(), loadMe()]);
// After both rules + proceeding types are in, re-render with the
// chosen sort so groups carry proper labels.
renderRuleSelect();
const pickerHost = document.getElementById("deadline-event-types"); const pickerHost = document.getElementById("deadline-event-types");
if (pickerHost) { if (pickerHost) {
eventTypePicker = attachEventTypePicker(pickerHost, { eventTypePicker = attachEventTypePicker(pickerHost, {
currentUserAdmin, currentUserAdmin,
onChange: () => refreshRuleView(), onChange: () => {
// Both directions trigger off picker change: refresh the
// Regel→Typ collapsed/expanded state AND the Typ→Regel auto-fill.
refreshRuleView();
applyTypeAutoFillRule();
},
}); });
} }
// t-paliad-165 follow-up — preload event_types so the collapsed // t-paliad-165 follow-up — preload event_types so the collapsed
@@ -358,13 +726,18 @@ document.addEventListener("DOMContentLoaded", async () => {
.then((types) => { .then((types) => {
eventTypesByID = new Map(types.map((et) => [et.id, et])); eventTypesByID = new Map(types.map((et) => [et.id, et]));
refreshRuleView(); refreshRuleView();
refreshRuleAutoBadgeAndWarning();
}) })
.catch(() => {/* non-fatal — collapsed view falls back to empty label */}); .catch(() => {/* non-fatal — collapsed view falls back to empty label */});
// t-paliad-165 — Regel change auto-fills the Typ chip from the rule's // t-paliad-165 — Regel change auto-fills the Typ chip from the rule's
// concept's canonical event_type, when the picker hasn't been // concept's canonical event_type, when the picker hasn't been
// manually edited away from the previous rule's suggestion. // manually edited away from the previous rule's suggestion. ALSO
// resets the Typ→Regel auto-fill marker since the user just made a
// manual rule pick.
document.getElementById("deadline-rule")?.addEventListener("change", () => { document.getElementById("deadline-rule")?.addEventListener("change", () => {
lastAutoFilledRuleID = null;
applyRuleAutoFill(); applyRuleAutoFill();
refreshRuleAutoBadgeAndWarning();
}); });
// "Anderen Typ wählen" — sticky expanded mode so the picker stays // "Anderen Typ wählen" — sticky expanded mode so the picker stays
// visible even when the chip still matches the rule's default. // visible even when the chip still matches the rule's default.
@@ -381,6 +754,20 @@ document.addEventListener("DOMContentLoaded", async () => {
// Wire approval-hint refresh: on first render + on project change. // Wire approval-hint refresh: on first render + on project change.
void refreshApprovalHint(); void refreshApprovalHint();
document.getElementById("deadline-project")?.addEventListener("change", () => { document.getElementById("deadline-project")?.addEventListener("change", () => {
// Project change can shift which rule the Type maps to (via the
// project's proceeding_type_id), so re-run the auto-fill.
void refreshApprovalHint(); void refreshApprovalHint();
applyTypeAutoFillRule();
});
// t-paliad-251 Part 4 — Standardtitel button replaces the title with
// a derived default. No destructive confirmation because the user
// invoked it explicitly.
document.getElementById("deadline-title-default-btn")?.addEventListener("click", () => {
const titleInput = document.getElementById("deadline-title") as HTMLInputElement | null;
if (!titleInput) return;
const derived = computeDefaultTitle();
if (derived) titleInput.value = derived;
titleInput.focus();
}); });
}); });

View File

@@ -686,6 +686,33 @@ export function openBrowseEventTypesModal(
return new Promise<string[] | null>((resolve) => { return new Promise<string[] | null>((resolve) => {
let selected = new Set<string>(opts.initialIDs); let selected = new Set<string>(opts.initialIDs);
let searchQuery = ""; let searchQuery = "";
// t-paliad-251 — court-type filter chips. `null` = "Alle" (any
// jurisdiction). Any non-null value matches event_types.jurisdiction;
// "any" is mapped to NULL/missing rows via jurisdictionMatches().
let activeJurisdiction: string | null = null;
// Surface every jurisdiction present in the data — "any" stays bucketed
// separately so users still have a "show generic-only" chip. EPA is
// canonicalised to EPO in event_types (see mig 074); the chip label
// shows EPA to match the legal vocabulary the lawyers use.
const jurisdictionsPresent = new Set<string>();
for (const et of opts.types) {
const j = (et.jurisdiction ?? "").trim();
if (j) jurisdictionsPresent.add(j);
}
const JURISDICTION_ORDER = ["UPC", "EPO", "DPMA", "DE", "any"];
const chipJurisdictions = JURISDICTION_ORDER.filter((j) => jurisdictionsPresent.has(j));
// Any jurisdiction in the data that isn't in our ordered list lands at
// the end so the chip row never silently drops a court flavour.
for (const j of jurisdictionsPresent) {
if (!chipJurisdictions.includes(j)) chipJurisdictions.push(j);
}
function chipLabel(j: string): string {
if (j === "EPO") return "EPA";
if (j === "any") return t("event_types.browse.jurisdiction.none");
return j;
}
const overlay = document.createElement("div"); const overlay = document.createElement("div");
overlay.className = "modal-overlay event-type-browse-overlay"; overlay.className = "modal-overlay event-type-browse-overlay";
@@ -694,6 +721,15 @@ export function openBrowseEventTypesModal(
<div class="event-type-browse-header"> <div class="event-type-browse-header">
<h2 id="event-type-browse-title">${esc(t("event_types.browse.title"))}</h2> <h2 id="event-type-browse-title">${esc(t("event_types.browse.title"))}</h2>
<input type="text" class="event-type-browse-search" data-role="search" placeholder="${esc(t("event_types.browse.search"))}" autocomplete="off" /> <input type="text" class="event-type-browse-search" data-role="search" placeholder="${esc(t("event_types.browse.search"))}" autocomplete="off" />
<div class="event-type-browse-chips" data-role="chips" role="group" aria-label="${esc(t("event_types.browse.jurisdiction.filter_label"))}">
<button type="button" class="event-type-browse-chip event-type-browse-chip--active" data-jurisdiction="" data-role="chip-all">${esc(t("event_types.browse.jurisdiction.all"))}</button>
${chipJurisdictions
.map(
(j) =>
`<button type="button" class="event-type-browse-chip" data-jurisdiction="${esc(j)}">${esc(chipLabel(j))}</button>`,
)
.join("")}
</div>
</div> </div>
<div class="event-type-browse-list" data-role="list" tabindex="-1"></div> <div class="event-type-browse-list" data-role="list" tabindex="-1"></div>
<div class="event-type-browse-actions"> <div class="event-type-browse-actions">
@@ -711,6 +747,7 @@ export function openBrowseEventTypesModal(
const countEl = overlay.querySelector<HTMLElement>("[data-role=count]")!; const countEl = overlay.querySelector<HTMLElement>("[data-role=count]")!;
const cancelBtn = overlay.querySelector<HTMLButtonElement>("[data-role=cancel]")!; const cancelBtn = overlay.querySelector<HTMLButtonElement>("[data-role=cancel]")!;
const applyBtn = overlay.querySelector<HTMLButtonElement>("[data-role=apply]")!; const applyBtn = overlay.querySelector<HTMLButtonElement>("[data-role=apply]")!;
const chipButtons = overlay.querySelectorAll<HTMLButtonElement>(".event-type-browse-chip");
const groups = groupByCategory(opts.types); const groups = groupByCategory(opts.types);
@@ -721,6 +758,12 @@ export function openBrowseEventTypesModal(
return j; return j;
} }
function jurisdictionMatches(et: EventType): boolean {
if (activeJurisdiction === null) return true;
const j = (et.jurisdiction ?? "").trim();
return j === activeJurisdiction;
}
function updateCount() { function updateCount() {
countEl.textContent = t("event_types.browse.selected_count").replace( countEl.textContent = t("event_types.browse.selected_count").replace(
"{n}", "{n}",
@@ -731,6 +774,7 @@ export function openBrowseEventTypesModal(
function renderList() { function renderList() {
const q = searchQuery.trim().toLowerCase(); const q = searchQuery.trim().toLowerCase();
const matches = (et: EventType) => { const matches = (et: EventType) => {
if (!jurisdictionMatches(et)) return false;
if (!q) return true; if (!q) return true;
return ( return (
et.label_de.toLowerCase().includes(q) || et.label_de.toLowerCase().includes(q) ||
@@ -783,6 +827,16 @@ export function openBrowseEventTypesModal(
renderList(); renderList();
}); });
chipButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const raw = btn.dataset.jurisdiction ?? "";
activeJurisdiction = raw === "" ? null : raw;
chipButtons.forEach((b) => b.classList.remove("event-type-browse-chip--active"));
btn.classList.add("event-type-browse-chip--active");
renderList();
});
});
function close(value: string[] | null) { function close(value: string[] | null) {
document.removeEventListener("keydown", onKey); document.removeEventListener("keydown", onKey);
overlay.remove(); overlay.remove();

View File

@@ -879,6 +879,14 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.field.rule.autofill_inline": " (vorgegeben durch Regel)", "deadlines.field.rule.autofill_inline": " (vorgegeben durch Regel)",
"deadlines.field.rule.mismatch": "Hinweis: Typ widerspricht Regel — Sie haben den Typ überschrieben.", "deadlines.field.rule.mismatch": "Hinweis: Typ widerspricht Regel — Sie haben den Typ überschrieben.",
"deadlines.field.rule.override": "Anderen Typ wählen", "deadlines.field.rule.override": "Anderen Typ wählen",
"deadlines.field.rule.auto_badge": "Auto",
"deadlines.field.rule.override_warn": "Typ ergibt Regel: {derived}. Gewählte Regel: {selected}. Es wird {selected} angewendet.",
"deadlines.field.rule.sort.by_proceeding": "Nach Verfahrensablauf",
"deadlines.field.rule.sort.by_court": "Nach Gerichtsart",
"deadlines.field.rule.sort.alpha": "Alphabetisch",
"deadlines.field.rule.sort.other_proceeding": "Sonstige Regeln",
"deadlines.field.title.default_btn": "Standardtitel",
"deadlines.field.title.default_fallback": "Neue Frist",
"deadlines.field.notes": "Notizen (optional)", "deadlines.field.notes": "Notizen (optional)",
"deadlines.field.notes.placeholder": "Hinweise, Verweise, n\u00e4chste Schritte\u2026", "deadlines.field.notes.placeholder": "Hinweise, Verweise, n\u00e4chste Schritte\u2026",
"deadlines.error.required": "Akte, Titel und F\u00e4lligkeitsdatum sind Pflichtfelder.", "deadlines.error.required": "Akte, Titel und F\u00e4lligkeitsdatum sind Pflichtfelder.",
@@ -2437,6 +2445,8 @@ const translations: Record<Lang, Record<string, string>> = {
"event_types.browse.cancel": "Abbrechen", "event_types.browse.cancel": "Abbrechen",
"event_types.browse.selected_count": "{n} ausgewählt", "event_types.browse.selected_count": "{n} ausgewählt",
"event_types.browse.jurisdiction.none": "Allgemein", "event_types.browse.jurisdiction.none": "Allgemein",
"event_types.browse.jurisdiction.all": "Alle Gerichte",
"event_types.browse.jurisdiction.filter_label": "Nach Gerichtsart filtern",
"event_types.filter.all": "Alle Typen", "event_types.filter.all": "Alle Typen",
"event_types.filter.untyped": "— Ohne Typ —", "event_types.filter.untyped": "— Ohne Typ —",
"event_types.filter.search": "Typ suchen…", "event_types.filter.search": "Typ suchen…",
@@ -3825,6 +3835,14 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.field.rule.autofill_inline": " (set by rule)", "deadlines.field.rule.autofill_inline": " (set by rule)",
"deadlines.field.rule.mismatch": "Note: type contradicts rule — you have overridden the type.", "deadlines.field.rule.mismatch": "Note: type contradicts rule — you have overridden the type.",
"deadlines.field.rule.override": "Choose another type", "deadlines.field.rule.override": "Choose another type",
"deadlines.field.rule.auto_badge": "Auto",
"deadlines.field.rule.override_warn": "Type derives rule: {derived}. Selected rule: {selected}. {selected} will be applied.",
"deadlines.field.rule.sort.by_proceeding": "By proceeding sequence",
"deadlines.field.rule.sort.by_court": "By court type",
"deadlines.field.rule.sort.alpha": "Alphabetical",
"deadlines.field.rule.sort.other_proceeding": "Other rules",
"deadlines.field.title.default_btn": "Default title",
"deadlines.field.title.default_fallback": "New deadline",
"deadlines.field.notes": "Notes (optional)", "deadlines.field.notes": "Notes (optional)",
"deadlines.field.notes.placeholder": "References, hints, next steps\u2026", "deadlines.field.notes.placeholder": "References, hints, next steps\u2026",
"deadlines.error.required": "Matter, title and due date are required.", "deadlines.error.required": "Matter, title and due date are required.",
@@ -5355,6 +5373,8 @@ const translations: Record<Lang, Record<string, string>> = {
"event_types.browse.cancel": "Cancel", "event_types.browse.cancel": "Cancel",
"event_types.browse.selected_count": "{n} selected", "event_types.browse.selected_count": "{n} selected",
"event_types.browse.jurisdiction.none": "Any", "event_types.browse.jurisdiction.none": "Any",
"event_types.browse.jurisdiction.all": "All courts",
"event_types.browse.jurisdiction.filter_label": "Filter by court type",
"event_types.filter.all": "All types", "event_types.filter.all": "All types",
"event_types.filter.untyped": "— Untyped —", "event_types.filter.untyped": "— Untyped —",
"event_types.filter.search": "Search type…", "event_types.filter.search": "Search type…",

View File

@@ -41,6 +41,19 @@ export function renderDeadlinesDetail(): string {
<div className="entity-detail-title-col"> <div className="entity-detail-title-col">
<h1 id="deadline-title-display" /> <h1 id="deadline-title-display" />
<input type="text" id="deadline-title-edit" className="entity-title-input" style="display:none" /> <input type="text" id="deadline-title-edit" className="entity-title-input" style="display:none" />
{/* t-paliad-251 Part 4 — Standardtitel button only
visible in edit mode; clicking replaces the
title with a default derived from the project
and the deadline's event types / rule. */}
<button
type="button"
id="deadline-title-default-btn"
className="btn-link-action"
style="display:none"
data-i18n="deadlines.field.title.default_btn"
>
Standardtitel
</button>
<div className="entity-detail-meta"> <div className="entity-detail-meta">
<span id="deadline-due-chip" className="frist-due-chip" /> <span id="deadline-due-chip" className="frist-due-chip" />
<span id="deadline-status-chip" className="entity-status-chip" /> <span id="deadline-status-chip" className="entity-status-chip" />

View File

@@ -45,7 +45,22 @@ export function renderDeadlinesNew(): string {
</div> </div>
<div className="form-field"> <div className="form-field">
<label htmlFor="deadline-title" data-i18n="deadlines.field.title">Titel</label> <div className="form-field-label-row">
<label htmlFor="deadline-title" data-i18n="deadlines.field.title">Titel</label>
{/* t-paliad-251 Part 4 — derive a Standardtitel from the
currently-known context (event type → rule → proceeding
type → fallback) with the project reference as suffix.
Always replaces the title; no destructive confirmation
because the user invoked it explicitly. */}
<button
type="button"
id="deadline-title-default-btn"
className="btn-link-action"
data-i18n="deadlines.field.title.default_btn"
>
Standardtitel
</button>
</div>
<input <input
type="text" type="text"
id="deadline-title" id="deadline-title"
@@ -105,10 +120,44 @@ export function renderDeadlinesNew(): string {
picker so the parent/child relationship reads at a picker so the parent/child relationship reads at a
glance. Due date is its own row below. */} glance. Due date is its own row below. */}
<div className="form-field"> <div className="form-field">
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label> <div className="form-field-label-row">
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label>
{/* t-paliad-251 Part 2 — sort options for the Rule
select. Defaults to "by_court" so users in the
UPC bucket find UPC rules quickly. */}
<select id="deadline-rule-sort" className="rule-sort-select" aria-label="Sortierung">
<option value="by_proceeding" data-i18n="deadlines.field.rule.sort.by_proceeding">Nach Verfahrensablauf</option>
<option value="by_court" data-i18n="deadlines.field.rule.sort.by_court" selected>Nach Gerichtsart</option>
<option value="alpha" data-i18n="deadlines.field.rule.sort.alpha">Alphabetisch</option>
</select>
</div>
<select id="deadline-rule"> <select id="deadline-rule">
<option value="" data-i18n="deadlines.field.rule.none">Keine Regel</option> <option value="" data-i18n="deadlines.field.rule.none">Keine Regel</option>
</select> </select>
{/* t-paliad-251 Part 3 — explicit Auto badge surfaces
whenever the Rule was auto-derived from the Typ.
Hidden when the user has manually picked a rule. */}
<p
className="form-hint form-hint--auto"
id="deadline-rule-auto-hint"
style="display:none"
>
<span
className="form-hint-badge"
data-i18n="deadlines.field.rule.auto_badge"
>Auto</span>
<span id="deadline-rule-auto-hint-text" />
</p>
{/* t-paliad-251 Part 3 — clearer override warning that
names BOTH the type-derived rule and the actually-
applied rule. Replaces the older Regel→Typ-only
mismatch warning when the contradiction goes the
other direction. */}
<p
className="form-hint form-hint--warning"
id="deadline-rule-override-warn"
style="display:none"
/>
</div> </div>
<div className="form-field"> <div className="form-field">

View File

@@ -1227,12 +1227,20 @@ export type I18nKey =
| "deadlines.field.notes" | "deadlines.field.notes"
| "deadlines.field.notes.placeholder" | "deadlines.field.notes.placeholder"
| "deadlines.field.rule" | "deadlines.field.rule"
| "deadlines.field.rule.auto_badge"
| "deadlines.field.rule.autofill" | "deadlines.field.rule.autofill"
| "deadlines.field.rule.autofill_inline" | "deadlines.field.rule.autofill_inline"
| "deadlines.field.rule.mismatch" | "deadlines.field.rule.mismatch"
| "deadlines.field.rule.none" | "deadlines.field.rule.none"
| "deadlines.field.rule.override" | "deadlines.field.rule.override"
| "deadlines.field.rule.override_warn"
| "deadlines.field.rule.sort.alpha"
| "deadlines.field.rule.sort.by_court"
| "deadlines.field.rule.sort.by_proceeding"
| "deadlines.field.rule.sort.other_proceeding"
| "deadlines.field.title" | "deadlines.field.title"
| "deadlines.field.title.default_btn"
| "deadlines.field.title.default_fallback"
| "deadlines.field.title.placeholder" | "deadlines.field.title.placeholder"
| "deadlines.filter.akte" | "deadlines.filter.akte"
| "deadlines.filter.akte.all" | "deadlines.filter.akte.all"
@@ -1574,6 +1582,8 @@ export type I18nKey =
| "event_types.browse.apply" | "event_types.browse.apply"
| "event_types.browse.cancel" | "event_types.browse.cancel"
| "event_types.browse.empty" | "event_types.browse.empty"
| "event_types.browse.jurisdiction.all"
| "event_types.browse.jurisdiction.filter_label"
| "event_types.browse.jurisdiction.none" | "event_types.browse.jurisdiction.none"
| "event_types.browse.search" | "event_types.browse.search"
| "event_types.browse.selected_count" | "event_types.browse.selected_count"

View File

@@ -7520,6 +7520,78 @@ dialog.modal::backdrop {
border-left: 2px solid #b88800; border-left: 2px solid #b88800;
} }
/* t-paliad-251 — Auto-derived hint variant. Lime-tint, sibling of the
yellow warning variant. Carries a small pill-badge in front (the
"Auto" label) followed by the derived rule name. */
.form-hint--auto {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--color-bg-lime-tint);
color: var(--color-text);
padding: 0.3rem 0.5rem;
border-radius: var(--radius-sm, 4px);
border-left: 2px solid var(--color-accent);
}
.form-hint-badge {
display: inline-block;
padding: 0.05rem 0.45rem;
border-radius: 999px;
background: var(--color-accent);
color: var(--color-text);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* t-paliad-251 — label row that hosts both the form label and an
inline action (Standardtitel button, Rule-sort dropdown). The label
keeps growing to push the action to the right edge. */
.form-field-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.form-field-label-row > label {
margin: 0;
}
/* Inline action button rendered next to a form label (Standardtitel).
Text-link styling so it doesn't compete with the primary CTA. */
.btn-link-action {
background: transparent;
border: none;
color: var(--color-link, var(--color-text));
padding: 0;
font-family: var(--font-sans);
font-size: 0.82rem;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.btn-link-action:hover {
color: var(--color-accent);
}
/* Small dropdown rendered alongside the Rule label to switch the
ordering. Tone-down sizing so it doesn't look like a co-equal
form field. Specificity-bumped to win over `.form-field select`'s
width: 100% baseline. */
.form-field select.rule-sort-select,
select.rule-sort-select {
width: auto;
padding: 0.2rem 0.4rem;
font-size: 0.82rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
color: var(--color-text);
font-family: var(--font-sans);
}
/* Inline checkbox label inside the attach-unit form. */ /* Inline checkbox label inside the attach-unit form. */
.form-checkbox { .form-checkbox {
display: inline-flex; display: inline-flex;
@@ -12517,6 +12589,37 @@ dialog.quick-add-sheet::backdrop {
transition: border-color 0.15s ease; transition: border-color 0.15s ease;
} }
.event-type-browse-search:focus { border-color: var(--color-accent); } .event-type-browse-search:focus { border-color: var(--color-accent); }
/* t-paliad-251 — jurisdiction filter chips inside the browse modal
header. Sits below the search input, between the search and the
results list. Active chip uses the lime-tint chip palette already
established by .event-type-collapsed* (t-paliad-165). */
.event-type-browse-chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.event-type-browse-chip {
padding: 0.2rem 0.7rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-surface);
color: var(--color-text-muted);
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.event-type-browse-chip:hover {
background: var(--color-bg-subtle);
color: var(--color-text);
}
.event-type-browse-chip--active {
background: var(--color-bg-lime-tint);
border-color: var(--color-accent);
color: var(--color-text);
font-weight: 600;
}
.event-type-browse-list { .event-type-browse-list {
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: auto; overflow-y: auto;