Revert "Merge: t-paliad-338 T1 — workflow-tracker shell replaces catalog (m/paliad#152)"
This reverts commit2e6427dca6, reversing changes made to9fe06094a8.
This commit is contained in:
@@ -212,33 +212,6 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"procedures.tab.wizard": "Gef\u00fchrt",
|
"procedures.tab.wizard": "Gef\u00fchrt",
|
||||||
"procedures.tab.akte": "Aus Akte",
|
"procedures.tab.akte": "Aus Akte",
|
||||||
"procedures.panel.akte.placeholder": "Akten-Einstieg folgt in einem sp\u00e4teren Slice.",
|
"procedures.panel.akte.placeholder": "Akten-Einstieg folgt in einem sp\u00e4teren Slice.",
|
||||||
|
|
||||||
// Workflow-tracker shell (m/paliad#152 T1+) \u2014 keys for the new
|
|
||||||
// /tools/procedures shape (find header + per-proceeding cards).
|
|
||||||
"procedures.filter.axis.date": "Stichtag:",
|
|
||||||
"procedures.filter.forum.all": "Alle",
|
|
||||||
"procedures.filter.party.all": "Alle",
|
|
||||||
"procedures.timelines.loading": "Verfahren werden geladen\u2026",
|
|
||||||
"procedures.timelines.empty": "Keine Verfahren passen. Filter zur\u00fccksetzen.",
|
|
||||||
"procedures.timelines.error": "Fehler beim Laden dieses Verfahrens.",
|
|
||||||
"procedures.timelines.options": "Optionen:",
|
|
||||||
"procedures.timelines.court_set": "vom Gericht bestimmt",
|
|
||||||
"procedures.cold_open.hint": "Suchen oder filtern, um andere Verfahren einzublenden.",
|
|
||||||
"procedures.find.summary.empty": "Keine Treffer.",
|
|
||||||
"procedures.find.summary.one": "{n} Verfahren",
|
|
||||||
"procedures.find.summary.many": "{n} Verfahren",
|
|
||||||
"procedures.find.summary.anchor": "Anker: {name}",
|
|
||||||
"procedures.node.pin": "An dieses Ereignis anheften",
|
|
||||||
"procedures.node.fokus": "Fokus \u2014 andere Zweige ausblenden",
|
|
||||||
"procedures.node.here": "\u2500\u2500 DU BIST HIER \u2500\u2500",
|
|
||||||
"procedures.zoom.breadcrumb": "Pfad",
|
|
||||||
"procedures.zoom.hidden": "{n} weitere Schritte verborgen \u2014 Fokus aufheben f\u00fcr volle Ansicht",
|
|
||||||
"procedures.proceeding.toggle": "Verfahren ein-/ausblenden",
|
|
||||||
"procedures.proceeding.show": "zeigen",
|
|
||||||
"procedures.proceeding.hide": "ausblenden",
|
|
||||||
"deadlines.flag.amend": "Mit Antrag auf Patent\u00e4nderung",
|
|
||||||
"deadlines.flag.cci": "Mit Verletzungswiderklage",
|
|
||||||
|
|
||||||
"nav.procedures": "Verfahren & Fristen",
|
"nav.procedures": "Verfahren & Fristen",
|
||||||
|
|
||||||
"deadlines.step1": "Verfahrensart w\u00e4hlen",
|
"deadlines.step1": "Verfahrensart w\u00e4hlen",
|
||||||
@@ -3443,32 +3416,6 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"procedures.tab.wizard": "Guided",
|
"procedures.tab.wizard": "Guided",
|
||||||
"procedures.tab.akte": "From matter",
|
"procedures.tab.akte": "From matter",
|
||||||
"procedures.panel.akte.placeholder": "Matter entry ships in a later slice.",
|
"procedures.panel.akte.placeholder": "Matter entry ships in a later slice.",
|
||||||
|
|
||||||
// Workflow-tracker shell (m/paliad#152 T1+).
|
|
||||||
"procedures.filter.axis.date": "As of:",
|
|
||||||
"procedures.filter.forum.all": "All",
|
|
||||||
"procedures.filter.party.all": "All",
|
|
||||||
"procedures.timelines.loading": "Loading proceedings…",
|
|
||||||
"procedures.timelines.empty": "No proceedings match. Reset filters.",
|
|
||||||
"procedures.timelines.error": "Failed to load this proceeding.",
|
|
||||||
"procedures.timelines.options": "Options:",
|
|
||||||
"procedures.timelines.court_set": "court-set",
|
|
||||||
"procedures.cold_open.hint": "Search or filter to surface other proceedings.",
|
|
||||||
"procedures.find.summary.empty": "No matches.",
|
|
||||||
"procedures.find.summary.one": "{n} proceeding",
|
|
||||||
"procedures.find.summary.many": "{n} proceedings",
|
|
||||||
"procedures.find.summary.anchor": "Anchor: {name}",
|
|
||||||
"procedures.node.pin": "Pin this event as the anchor",
|
|
||||||
"procedures.node.fokus": "Focus — hide sibling branches",
|
|
||||||
"procedures.node.here": "── YOU ARE HERE ──",
|
|
||||||
"procedures.zoom.breadcrumb": "Path",
|
|
||||||
"procedures.zoom.hidden": "{n} more steps hidden — unfocus to see all",
|
|
||||||
"procedures.proceeding.toggle": "Toggle proceeding",
|
|
||||||
"procedures.proceeding.show": "show",
|
|
||||||
"procedures.proceeding.hide": "hide",
|
|
||||||
"deadlines.flag.amend": "With patent amendment request",
|
|
||||||
"deadlines.flag.cci": "With infringement counterclaim",
|
|
||||||
|
|
||||||
"nav.procedures": "Procedures & Deadlines",
|
"nav.procedures": "Procedures & Deadlines",
|
||||||
|
|
||||||
"deadlines.step1": "Select Proceeding Type",
|
"deadlines.step1": "Select Proceeding Type",
|
||||||
|
|||||||
@@ -1,514 +0,0 @@
|
|||||||
// procedures-tracker — render module for /tools/procedures (m/paliad#152
|
|
||||||
// T1 + onwards, docs/design-procedures-workflow-tracker-2026-05-27.md).
|
|
||||||
//
|
|
||||||
// Responsibilities:
|
|
||||||
// - Per-proceeding card render: header + chained tree by parent_id +
|
|
||||||
// priority-styled bullets + scenario-flag fork checkboxes.
|
|
||||||
// - Tree layout: children grouped under their parentRuleCode in
|
|
||||||
// deadlines-list order, root rules surface at depth 0.
|
|
||||||
// - Default detail mode = "selected" (mandatory + recommended +
|
|
||||||
// scenario-flag-enabled). Conditional rules whose gate is OFF are
|
|
||||||
// filtered out by the calculator and don't surface; the
|
|
||||||
// corresponding fork checkbox on the gating node reveals them when
|
|
||||||
// toggled ON.
|
|
||||||
//
|
|
||||||
// T1 floor: card-level "Optionen" strip carries the scenario-flag
|
|
||||||
// forks at the top of each proceeding card. Per-node inline placement
|
|
||||||
// (the design's stated final shape — fork checkbox on the actual
|
|
||||||
// gating node) is a T2 refinement; T1 keeps forks discoverable but
|
|
||||||
// scoped per proceeding so they're not the global-page strip m's bug
|
|
||||||
// #5 flagged.
|
|
||||||
|
|
||||||
import { t, tDyn, getLang } from "./i18n";
|
|
||||||
import {
|
|
||||||
type CalculatedDeadline,
|
|
||||||
type DeadlineResponse,
|
|
||||||
calculateDeadlines,
|
|
||||||
escHtml,
|
|
||||||
formatDate,
|
|
||||||
} from "./views/verfahrensablauf-core";
|
|
||||||
import { filterByDetailMode } from "./verfahrensablauf-detail-mode";
|
|
||||||
|
|
||||||
// ProceedingDef — the catalog of proceedings the find header pills and
|
|
||||||
// the cold-open default surface against. Kept in sync with paliad's
|
|
||||||
// proceeding_types catalog as of 2026-05-27 (matches
|
|
||||||
// VerfahrensablaufBody.tsx's listing).
|
|
||||||
export interface ProceedingDef {
|
|
||||||
code: string;
|
|
||||||
forum: "upc" | "de" | "epa" | "dpma";
|
|
||||||
i18nKey: string;
|
|
||||||
nameDE: string;
|
|
||||||
nameEN: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PROCEEDINGS: ProceedingDef[] = [
|
|
||||||
{ code: "upc.inf.cfi", forum: "upc", i18nKey: "deadlines.upc.inf.cfi", nameDE: "Verletzungsverfahren", nameEN: "Infringement (CFI)" },
|
|
||||||
{ code: "upc.rev.cfi", forum: "upc", i18nKey: "deadlines.upc.rev.cfi", nameDE: "Nichtigkeitsklage", nameEN: "Revocation (CFI)" },
|
|
||||||
{ code: "upc.ccr.cfi", forum: "upc", i18nKey: "deadlines.upc.ccr.cfi", nameDE: "Widerklage auf Nichtigkeit", nameEN: "Counterclaim for revocation" },
|
|
||||||
{ code: "upc.pi.cfi", forum: "upc", i18nKey: "deadlines.upc.pi.cfi", nameDE: "Einstw. Maßnahmen", nameEN: "Provisional measures" },
|
|
||||||
{ code: "upc.apl.unified", forum: "upc", i18nKey: "deadlines.upc.apl.unified", nameDE: "Berufung UPC", nameEN: "Appeal UPC" },
|
|
||||||
{ code: "upc.dmgs.cfi", forum: "upc", i18nKey: "deadlines.upc.dmgs.cfi", nameDE: "Schadensbemessung", nameEN: "Damages" },
|
|
||||||
{ code: "upc.disc.cfi", forum: "upc", i18nKey: "deadlines.upc.disc.cfi", nameDE: "Bucheinsicht", nameEN: "Inspection of accounts" },
|
|
||||||
{ code: "de.inf.lg", forum: "de", i18nKey: "deadlines.de.inf.lg", nameDE: "LG Verletzungsklage", nameEN: "LG infringement (DE 1st inst.)" },
|
|
||||||
{ code: "de.inf.olg", forum: "de", i18nKey: "deadlines.de.inf.olg", nameDE: "OLG Berufung", nameEN: "OLG appeal" },
|
|
||||||
{ code: "de.inf.bgh", forum: "de", i18nKey: "deadlines.de.inf.bgh", nameDE: "BGH Revision / NZB", nameEN: "BGH revision / NZB" },
|
|
||||||
{ code: "de.null.bpatg", forum: "de", i18nKey: "deadlines.de.null.bpatg", nameDE: "BPatG Nichtigkeit", nameEN: "BPatG revocation" },
|
|
||||||
{ code: "de.null.bgh", forum: "de", i18nKey: "deadlines.de.null.bgh", nameDE: "BGH Berufung (Nichtigkeit)", nameEN: "BGH revocation appeal" },
|
|
||||||
{ code: "epa.opp.opd", forum: "epa", i18nKey: "deadlines.epa.opp.opd", nameDE: "Einspruchsverfahren EPA", nameEN: "EPO opposition" },
|
|
||||||
{ code: "epa.opp.boa", forum: "epa", i18nKey: "deadlines.epa.opp.boa", nameDE: "Beschwerdeverfahren EPA", nameEN: "EPO appeal" },
|
|
||||||
{ code: "epa.grant.exa", forum: "epa", i18nKey: "deadlines.epa.grant.exa", nameDE: "EP-Erteilungsverfahren", nameEN: "EP grant" },
|
|
||||||
{ code: "dpma.opp.dpma", forum: "dpma", i18nKey: "deadlines.dpma.opp.dpma", nameDE: "Einspruch DPMA", nameEN: "DPMA opposition" },
|
|
||||||
{ code: "dpma.appeal.bpatg", forum: "dpma", i18nKey: "deadlines.dpma.appeal.bpatg", nameDE: "Beschwerde BPatG (DPMA)", nameEN: "BPatG appeal (DPMA)" },
|
|
||||||
{ code: "dpma.appeal.bgh", forum: "dpma", i18nKey: "deadlines.dpma.appeal.bgh", nameDE: "Rechtsbeschwerde BGH", nameEN: "BGH legal appeal" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// COLD_OPEN_DEFAULTS — design §8 / §11.Q4. When no URL params and no
|
|
||||||
// Akte context, the page renders these 6 proceedings stacked. Hint
|
|
||||||
// text above the timelines invites the user to filter for more.
|
|
||||||
export const COLD_OPEN_DEFAULTS: string[] = [
|
|
||||||
"upc.inf.cfi",
|
|
||||||
"upc.rev.cfi",
|
|
||||||
"upc.apl.unified",
|
|
||||||
"de.inf.lg",
|
|
||||||
"epa.opp.opd",
|
|
||||||
"dpma.opp.dpma",
|
|
||||||
];
|
|
||||||
|
|
||||||
// FORUM_LABEL mirrors the forum-pill label and the proceeding card
|
|
||||||
// header jurisdiction prefix. Same slugs the proceeding-types catalog
|
|
||||||
// carries.
|
|
||||||
const FORUM_LABEL: Record<string, string> = {
|
|
||||||
upc: "UPC",
|
|
||||||
de: "DE",
|
|
||||||
epa: "EPA",
|
|
||||||
dpma: "DPMA",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function lookupProceeding(code: string): ProceedingDef | undefined {
|
|
||||||
return PROCEEDINGS.find((p) => p.code === code);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function proceedingDisplayName(code: string): string {
|
|
||||||
const def = lookupProceeding(code);
|
|
||||||
if (!def) return code;
|
|
||||||
const lang = getLang();
|
|
||||||
return lang === "en" ? def.nameEN : def.nameDE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── condition_expr flag extraction ─────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Walks the jsonb tree shape documented in pkg/litigationplanner/expr.go:
|
|
||||||
// {"flag": "<name>"} — leaf
|
|
||||||
// {"op": "and|or|not", "args":[…]} — composite
|
|
||||||
// Returns the set of flag names mentioned in the expression. The page
|
|
||||||
// uses this to decide which scenario_flag forks apply to a given
|
|
||||||
// proceeding (the union over all conditional rules' expressions).
|
|
||||||
|
|
||||||
function collectFlagsFromExpr(node: unknown, out: Set<string>): void {
|
|
||||||
if (!node || typeof node !== "object") return;
|
|
||||||
const n = node as Record<string, unknown>;
|
|
||||||
if (typeof n.flag === "string" && n.flag) {
|
|
||||||
out.add(n.flag);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(n.args)) {
|
|
||||||
for (const arg of n.args) collectFlagsFromExpr(arg, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gatingFlagsForProceeding(deadlines: CalculatedDeadline[]): string[] {
|
|
||||||
const set = new Set<string>();
|
|
||||||
for (const dl of deadlines) {
|
|
||||||
if (!dl.conditionExpr) continue;
|
|
||||||
collectFlagsFromExpr(dl.conditionExpr, set);
|
|
||||||
}
|
|
||||||
return Array.from(set).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hard-coded fallback set: when the calc was run without a flag, the
|
|
||||||
// conditional rules gated by that flag are filtered out server-side
|
|
||||||
// (selected mode doesn't surface their condition_expr). The fallback
|
|
||||||
// lists the flags each proceeding *can* gate so the per-card Optionen
|
|
||||||
// strip surfaces them even on first render with the flag off.
|
|
||||||
//
|
|
||||||
// Mirrors mig 084's backfill: each scenario_flag → the proceedings
|
|
||||||
// where it's referenced by at least one rule. Today's catalog: 18
|
|
||||||
// conditional rules across upc.inf.cfi (with_ccr / with_amend) and
|
|
||||||
// upc.rev.cfi (with_amend / with_cci). Keep in sync with
|
|
||||||
// paliad.sequencing_rules.condition_expr.
|
|
||||||
const FALLBACK_FLAGS: Record<string, string[]> = {
|
|
||||||
"upc.inf.cfi": ["with_ccr", "with_amend"],
|
|
||||||
"upc.rev.cfi": ["with_amend", "with_cci"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function applicableFlagsForProceeding(
|
|
||||||
code: string,
|
|
||||||
deadlines: CalculatedDeadline[],
|
|
||||||
): string[] {
|
|
||||||
const fromExpr = gatingFlagsForProceeding(deadlines);
|
|
||||||
if (fromExpr.length > 0) return fromExpr;
|
|
||||||
return FALLBACK_FLAGS[code] || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// flagLabel maps scenario_flag keys to i18n keys (matches the
|
|
||||||
// existing deadlines.flag.* keys used by VerfahrensablaufBody).
|
|
||||||
const FLAG_I18N: Record<string, string> = {
|
|
||||||
with_ccr: "deadlines.flag.ccr",
|
|
||||||
with_amend: "deadlines.flag.amend",
|
|
||||||
with_cci: "deadlines.flag.cci",
|
|
||||||
};
|
|
||||||
|
|
||||||
function labelForFlag(flagKey: string, proceeding: string): string {
|
|
||||||
// upc.inf.cfi with_amend = R.30 amendment request; upc.rev.cfi
|
|
||||||
// with_amend = R.49.2(a) amendment. Different labels even though the
|
|
||||||
// flag key is the same. Honour the inf vs rev distinction.
|
|
||||||
if (flagKey === "with_amend" && proceeding === "upc.inf.cfi") return t("deadlines.flag.inf_amend");
|
|
||||||
if (flagKey === "with_amend" && proceeding === "upc.rev.cfi") return t("deadlines.flag.rev_amend");
|
|
||||||
if (flagKey === "with_cci" && proceeding === "upc.rev.cfi") return t("deadlines.flag.rev_cci");
|
|
||||||
const key = FLAG_I18N[flagKey];
|
|
||||||
return key ? t(key) : flagKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── per-proceeding render ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface TimelineRenderParams {
|
|
||||||
proceedingType: string;
|
|
||||||
triggerDate: string;
|
|
||||||
flags: string[];
|
|
||||||
anchorRuleId?: string;
|
|
||||||
zoom?: boolean;
|
|
||||||
// collapsed=true renders the card as a one-line header only with a
|
|
||||||
// [zeigen] link. Used by §6.5: when an anchor is pinned + >1
|
|
||||||
// proceeding visible, non-anchored proceedings collapse so the
|
|
||||||
// anchor's full context owns the page.
|
|
||||||
collapsed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RenderedTimeline {
|
|
||||||
card: HTMLElement;
|
|
||||||
data: DeadlineResponse | null;
|
|
||||||
// hasAnchor true iff the rendered card contains the active
|
|
||||||
// anchorRuleId. Drives the multi-proceeding auto-collapse decision
|
|
||||||
// back in procedures.ts.
|
|
||||||
hasAnchor: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderCard takes a proceeding code + flag set + trigger date and
|
|
||||||
// returns a fully-wired card element ready to mount. Re-running with a
|
|
||||||
// new flag set requires a fresh fetch (calc applies the flag set
|
|
||||||
// server-side).
|
|
||||||
export async function renderCard(params: TimelineRenderParams): Promise<RenderedTimeline> {
|
|
||||||
const card = document.createElement("article");
|
|
||||||
card.className = "tracker-proceeding";
|
|
||||||
card.dataset.proceeding = params.proceedingType;
|
|
||||||
|
|
||||||
// Header — proceeding name + jurisdiction badge + flag strip host.
|
|
||||||
// Flag strip hydrates from the calc response below.
|
|
||||||
const def = lookupProceeding(params.proceedingType);
|
|
||||||
const jur = def ? (FORUM_LABEL[def.forum] || "") : "";
|
|
||||||
const procName = proceedingDisplayName(params.proceedingType);
|
|
||||||
const header = document.createElement("header");
|
|
||||||
header.className = "tracker-proceeding-header";
|
|
||||||
header.innerHTML = `
|
|
||||||
<span class="tracker-proceeding-jur">${escHtml(jur)}</span>
|
|
||||||
<h3 class="tracker-proceeding-name">${escHtml(procName)}</h3>
|
|
||||||
<span class="tracker-proceeding-code" title="${escHtml(params.proceedingType)}">${escHtml(params.proceedingType)}</span>
|
|
||||||
<button type="button" class="tracker-proceeding-toggle" data-action="proc-toggle"
|
|
||||||
data-code="${escHtml(params.proceedingType)}"
|
|
||||||
aria-label="${escHtml(t("procedures.proceeding.toggle"))}">
|
|
||||||
${escHtml(params.collapsed ? t("procedures.proceeding.show") : t("procedures.proceeding.hide"))}
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
card.appendChild(header);
|
|
||||||
|
|
||||||
// Collapsed state — header-only render (§6.5). Bail before issuing
|
|
||||||
// the calc fetch; the card has no body. hasAnchor stays false here
|
|
||||||
// because we never resolved the data.
|
|
||||||
if (params.collapsed) {
|
|
||||||
card.classList.add("tracker-proceeding--collapsed");
|
|
||||||
return { card, data: null, hasAnchor: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionen strip — scenario flag checkboxes scoped to this card.
|
|
||||||
// Hydrated after the calc response so the applicable flag set is
|
|
||||||
// known. T1 floor: card-level placement; T2+ may move these to
|
|
||||||
// inline on the actual gating node per the design.
|
|
||||||
const optionsStrip = document.createElement("div");
|
|
||||||
optionsStrip.className = "tracker-proceeding-options";
|
|
||||||
optionsStrip.hidden = true;
|
|
||||||
card.appendChild(optionsStrip);
|
|
||||||
|
|
||||||
// Body — chained tree mounts here once the calc returns.
|
|
||||||
const body = document.createElement("div");
|
|
||||||
body.className = "tracker-proceeding-body";
|
|
||||||
body.innerHTML = `<div class="tracker-proceeding-loading">${escHtml(t("procedures.timelines.loading"))}</div>`;
|
|
||||||
card.appendChild(body);
|
|
||||||
|
|
||||||
// Fetch + render.
|
|
||||||
const data = await calculateDeadlines({
|
|
||||||
proceedingType: params.proceedingType,
|
|
||||||
triggerDate: params.triggerDate,
|
|
||||||
flags: params.flags,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
body.innerHTML = `<div class="tracker-proceeding-error">${escHtml(t("procedures.timelines.error"))}</div>`;
|
|
||||||
return { card, data: null, hasAnchor: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hydrate the Optionen strip with the applicable scenario flags.
|
|
||||||
const applicable = applicableFlagsForProceeding(params.proceedingType, data.deadlines);
|
|
||||||
if (applicable.length > 0) {
|
|
||||||
const labelSpan = document.createElement("span");
|
|
||||||
labelSpan.className = "tracker-proceeding-options-label";
|
|
||||||
labelSpan.textContent = t("procedures.timelines.options");
|
|
||||||
optionsStrip.appendChild(labelSpan);
|
|
||||||
|
|
||||||
for (const flag of applicable) {
|
|
||||||
const label = document.createElement("label");
|
|
||||||
label.className = "tracker-proceeding-option";
|
|
||||||
const cb = document.createElement("input");
|
|
||||||
cb.type = "checkbox";
|
|
||||||
cb.value = flag;
|
|
||||||
cb.checked = params.flags.includes(flag);
|
|
||||||
cb.dataset.flag = flag;
|
|
||||||
label.appendChild(cb);
|
|
||||||
const text = document.createElement("span");
|
|
||||||
text.textContent = labelForFlag(flag, params.proceedingType);
|
|
||||||
label.appendChild(text);
|
|
||||||
optionsStrip.appendChild(label);
|
|
||||||
}
|
|
||||||
optionsStrip.hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to selected detail mode (mandatory + recommended +
|
|
||||||
// active-flag-gated). Conditional rules whose gate is OFF were
|
|
||||||
// already dropped server-side — the filter just removes optionals
|
|
||||||
// not explicitly selected.
|
|
||||||
const filtered = filterByDetailMode(data.deadlines, "selected", null);
|
|
||||||
|
|
||||||
// Anchor-present detection: does this card's rule set contain the
|
|
||||||
// active anchor rule id? Drives the multi-proceeding scope logic
|
|
||||||
// and the zoom branch below.
|
|
||||||
const hasAnchor = !!(params.anchorRuleId && filtered.some((d) => d.ruleId === params.anchorRuleId));
|
|
||||||
|
|
||||||
if (params.zoom && hasAnchor && params.anchorRuleId) {
|
|
||||||
body.innerHTML = renderZoomedBody(filtered, params.anchorRuleId);
|
|
||||||
} else {
|
|
||||||
body.innerHTML = renderTreeBody(filtered, params.anchorRuleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAnchor) card.classList.add("tracker-proceeding--anchored");
|
|
||||||
|
|
||||||
return { card, data, hasAnchor };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── tree builder ──────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Builds a `parentRuleCode → child[]` map, then walks from each root
|
|
||||||
// (no parent) emitting nested <ul class="tracker-tree-…"> nodes. The
|
|
||||||
// calculator already sorts the deadlines into a sensible chain (root
|
|
||||||
// → linear-deepest-first); the tree builder preserves that order.
|
|
||||||
|
|
||||||
function renderTreeBody(deadlines: CalculatedDeadline[], anchorRuleId?: string): string {
|
|
||||||
if (deadlines.length === 0) {
|
|
||||||
return `<div class="tracker-proceeding-empty">${escHtml(t("procedures.timelines.empty"))}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index by code so children can find their parent visually. Track
|
|
||||||
// every code that appears in the filtered set — children whose
|
|
||||||
// parent isn't in the set surface as orphan roots.
|
|
||||||
const present = new Set(deadlines.map((d) => d.code));
|
|
||||||
const childrenOf: Record<string, CalculatedDeadline[]> = {};
|
|
||||||
const roots: CalculatedDeadline[] = [];
|
|
||||||
|
|
||||||
for (const dl of deadlines) {
|
|
||||||
const parent = dl.parentRuleCode || "";
|
|
||||||
if (parent && present.has(parent)) {
|
|
||||||
(childrenOf[parent] ||= []).push(dl);
|
|
||||||
} else {
|
|
||||||
roots.push(dl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: string[] = [`<ul class="tracker-tree tracker-tree-root">`];
|
|
||||||
for (const root of roots) {
|
|
||||||
parts.push(renderTreeNode(root, childrenOf, 0, anchorRuleId));
|
|
||||||
}
|
|
||||||
parts.push(`</ul>`);
|
|
||||||
return parts.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTreeNode(
|
|
||||||
dl: CalculatedDeadline,
|
|
||||||
childrenOf: Record<string, CalculatedDeadline[]>,
|
|
||||||
depth: number,
|
|
||||||
anchorRuleId?: string,
|
|
||||||
): string {
|
|
||||||
const children = childrenOf[dl.code] || [];
|
|
||||||
const isAnchored = !!(anchorRuleId && dl.ruleId === anchorRuleId);
|
|
||||||
const lang = getLang();
|
|
||||||
|
|
||||||
// Priority-driven bullet style.
|
|
||||||
const priorityClass = `tracker-node--${dl.priority || "mandatory"}`;
|
|
||||||
const anchorClass = isAnchored ? " tracker-node--anchored" : "";
|
|
||||||
const courtClass = dl.isCourtSet ? " tracker-node--court" : "";
|
|
||||||
|
|
||||||
const name = lang === "en" ? (dl.nameEN || dl.name) : (dl.name || dl.nameEN);
|
|
||||||
const ref = dl.legalSourceDisplay || dl.ruleRef || "";
|
|
||||||
const dateLabel = dl.isCourtSet
|
|
||||||
? t("procedures.timelines.court_set")
|
|
||||||
: (dl.dueDate ? formatDate(dl.dueDate) : "");
|
|
||||||
|
|
||||||
// Party badge — one-letter affordance to the right.
|
|
||||||
const partyBadge = dl.party === "court"
|
|
||||||
? "G"
|
|
||||||
: dl.party === "claimant"
|
|
||||||
? "K"
|
|
||||||
: dl.party === "defendant"
|
|
||||||
? "B"
|
|
||||||
: dl.party === "both" ? "K+B" : "";
|
|
||||||
|
|
||||||
const ruleIdAttr = dl.ruleId ? ` data-rule-id="${escHtml(dl.ruleId)}"` : "";
|
|
||||||
const codeAttr = ` data-code="${escHtml(dl.code)}"`;
|
|
||||||
|
|
||||||
// Pin + Fokus affordances. Pin is always available on nodes with a
|
|
||||||
// real ruleId (synthetic appeal-trigger markers carry no id). Fokus
|
|
||||||
// only on the currently anchored node.
|
|
||||||
const pinBtn = dl.ruleId
|
|
||||||
? `<button type="button" class="tracker-node-pin" data-action="pin"
|
|
||||||
data-rule-id="${escHtml(dl.ruleId)}"
|
|
||||||
aria-label="${escHtml(t("procedures.node.pin"))}"
|
|
||||||
title="${escHtml(t("procedures.node.pin"))}">📌</button>`
|
|
||||||
: "";
|
|
||||||
const fokusBtn = isAnchored
|
|
||||||
? `<button type="button" class="tracker-node-fokus" data-action="fokus"
|
|
||||||
aria-label="${escHtml(t("procedures.node.fokus"))}"
|
|
||||||
title="${escHtml(t("procedures.node.fokus"))}">🔍</button>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const meta = `
|
|
||||||
<div class="tracker-node-line">
|
|
||||||
<span class="tracker-node-bullet" aria-hidden="true"></span>
|
|
||||||
<span class="tracker-node-name">${escHtml(name)}</span>
|
|
||||||
${ref ? `<span class="tracker-node-ref">${escHtml(ref)}</span>` : ""}
|
|
||||||
${dateLabel ? `<span class="tracker-node-date">${escHtml(dateLabel)}</span>` : ""}
|
|
||||||
${partyBadge ? `<span class="tracker-node-party tracker-node-party--${escHtml(dl.party || "")}">${escHtml(partyBadge)}</span>` : ""}
|
|
||||||
${pinBtn}
|
|
||||||
${fokusBtn}
|
|
||||||
</div>
|
|
||||||
${isAnchored ? `<div class="tracker-node-here" role="note">${escHtml(t("procedures.node.here"))}</div>` : ""}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let inner = meta;
|
|
||||||
if (children.length > 0) {
|
|
||||||
const kids = children
|
|
||||||
.map((c) => renderTreeNode(c, childrenOf, depth + 1, anchorRuleId))
|
|
||||||
.join("");
|
|
||||||
inner += `<ul class="tracker-tree tracker-tree--depth-${depth + 1}">${kids}</ul>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<li class="tracker-node ${priorityClass}${anchorClass}${courtClass}"${ruleIdAttr}${codeAttr}>${inner}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── zoom mode (§6.2) ──────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// When the user clicks [🔍] on the anchored node, the proceeding card
|
|
||||||
// re-renders into zoom mode:
|
|
||||||
// - Ancestors of the anchor collapse to a single breadcrumb at the
|
|
||||||
// top of the card (proceeding-code ▸ root ▸ … ▸ anchor).
|
|
||||||
// - Sibling branches at each ancestor depth fold to a one-line
|
|
||||||
// "… N weitere verborgen — [zeigen]" summary.
|
|
||||||
// - The anchored node renders full with all its successors.
|
|
||||||
//
|
|
||||||
// Sibling-expand-on-demand: when the user clicks [zeigen] on a fold
|
|
||||||
// summary, the corresponding sibling subtree expands inline. State is
|
|
||||||
// per-card in sessionStorage so a reload keeps it.
|
|
||||||
|
|
||||||
function renderZoomedBody(deadlines: CalculatedDeadline[], anchorRuleId: string): string {
|
|
||||||
const anchor = deadlines.find((d) => d.ruleId === anchorRuleId);
|
|
||||||
if (!anchor) {
|
|
||||||
// The anchor is no longer in the filtered set (e.g. the user
|
|
||||||
// toggled a flag that hid it). Fall back to the full tree so the
|
|
||||||
// user can re-pin.
|
|
||||||
return renderTreeBody(deadlines, anchorRuleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the parent chain (anchor → root). The chain is walked via
|
|
||||||
// parentRuleCode; bail at the first missing parent or at >20 hops
|
|
||||||
// (defensive — the deepest tree today is ~5).
|
|
||||||
const byCode: Record<string, CalculatedDeadline> = {};
|
|
||||||
for (const dl of deadlines) byCode[dl.code] = dl;
|
|
||||||
const ancestors: CalculatedDeadline[] = [];
|
|
||||||
let cursor: CalculatedDeadline | undefined = anchor;
|
|
||||||
let safety = 20;
|
|
||||||
while (cursor && safety-- > 0) {
|
|
||||||
const parentCode = cursor.parentRuleCode || "";
|
|
||||||
if (!parentCode) break;
|
|
||||||
const parent = byCode[parentCode];
|
|
||||||
if (!parent) break;
|
|
||||||
ancestors.unshift(parent);
|
|
||||||
cursor = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lang = getLang();
|
|
||||||
const breadcrumbParts: string[] = [];
|
|
||||||
for (const a of ancestors) {
|
|
||||||
const name = lang === "en" ? (a.nameEN || a.name) : (a.name || a.nameEN);
|
|
||||||
breadcrumbParts.push(`<span class="tracker-zoom-crumb">${escHtml(name)}</span>`);
|
|
||||||
}
|
|
||||||
const anchorName = lang === "en" ? (anchor.nameEN || anchor.name) : (anchor.name || anchor.nameEN);
|
|
||||||
breadcrumbParts.push(`<span class="tracker-zoom-crumb tracker-zoom-crumb--anchor">${escHtml(anchorName)}</span>`);
|
|
||||||
|
|
||||||
const breadcrumb = `<nav class="tracker-zoom-breadcrumb" aria-label="${escHtml(t("procedures.zoom.breadcrumb"))}">
|
|
||||||
${breadcrumbParts.join('<span class="tracker-zoom-crumb-sep">▸</span>')}
|
|
||||||
</nav>`;
|
|
||||||
|
|
||||||
// Subtree: anchor + all descendants (parentRuleCode chain rooted
|
|
||||||
// at the anchor).
|
|
||||||
const descendants = new Set<string>([anchor.code]);
|
|
||||||
const queue = [anchor.code];
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const code = queue.shift()!;
|
|
||||||
for (const dl of deadlines) {
|
|
||||||
if (dl.parentRuleCode === code && !descendants.has(dl.code)) {
|
|
||||||
descendants.add(dl.code);
|
|
||||||
queue.push(dl.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const subtree = deadlines.filter((d) => descendants.has(d.code));
|
|
||||||
const subtreeBody = renderTreeBody(subtree, anchorRuleId);
|
|
||||||
|
|
||||||
// Sibling count summary — descendants ignored. Stays terse so the
|
|
||||||
// page tells the user how much is hidden without listing it.
|
|
||||||
const totalCount = deadlines.length;
|
|
||||||
const hiddenCount = totalCount - subtree.length;
|
|
||||||
const hiddenLine = hiddenCount > 0
|
|
||||||
? `<div class="tracker-zoom-hidden">${escHtml(tDyn("procedures.zoom.hidden").replace("{n}", String(hiddenCount)))}</div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return breadcrumb + subtreeBody + hiddenLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── search hit highlight (URL `?event=`) ──────────────────────────────────
|
|
||||||
//
|
|
||||||
// T1 affordance: when `?event=<rule_id>` is set, scroll the matching
|
|
||||||
// node into view and apply a transient highlight. Anchor pin + zoom is
|
|
||||||
// a T2 layering on top of this.
|
|
||||||
|
|
||||||
export function scrollAnchorIntoView(card: HTMLElement, anchorRuleId: string): void {
|
|
||||||
const node = card.querySelector<HTMLElement>(`[data-rule-id="${CSS.escape(anchorRuleId)}"]`);
|
|
||||||
if (!node) return;
|
|
||||||
node.classList.add("tracker-node--highlight");
|
|
||||||
node.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
||||||
setTimeout(() => node.classList.remove("tracker-node--highlight"), 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// summarise — short status line for the find-header summary.
|
|
||||||
export function summariseRender(rendered: RenderedTimeline[]): string {
|
|
||||||
const proc = rendered.length;
|
|
||||||
if (proc === 0) return t("procedures.find.summary.empty");
|
|
||||||
if (proc === 1) return tDyn("procedures.find.summary.one").replace("{n}", "1");
|
|
||||||
return tDyn("procedures.find.summary.many").replace("{n}", String(proc));
|
|
||||||
}
|
|
||||||
@@ -1,520 +1,150 @@
|
|||||||
// /tools/procedures client (m/paliad#152 T1,
|
// /tools/procedures client (m/paliad#151,
|
||||||
// docs/design-procedures-workflow-tracker-2026-05-27.md §9 T1).
|
// docs/design-unified-procedural-events-tool-2026-05-27.md).
|
||||||
//
|
//
|
||||||
// Workflow-tracker shell — replaces the 4-tab catalog (U0-U4) shipped
|
// Boot logic + tab switching for the unified procedural-events tool.
|
||||||
// earlier today with a single canonical shape:
|
// Each entry tab mounts its own module; the search box and chip
|
||||||
|
// filters in the top filter strip are wired in U1+ as each slice adds
|
||||||
|
// its dimension-aware behaviour.
|
||||||
//
|
//
|
||||||
// 1. Sticky find header — search input + forum / Verfahren / Partei
|
// U0 — Skeleton + tab toggling.
|
||||||
// pill rows + global Stichtag. The header narrows the timeline
|
// U1 — Direkt suchen mounts Mode A.
|
||||||
// set rendered below.
|
// U2 — Geführt mounts Mode B wizard.
|
||||||
// 2. Timeline body — one card per matched proceeding, rendered as
|
// U3 — Verfahren wählen wires the Verfahrensablauf wizard + detail-mode toggle.
|
||||||
// a chained tree by parent_id with priority-styled bullets.
|
|
||||||
//
|
//
|
||||||
// URL state (T1):
|
// Mode A renders its shell into #fristen-overhaul-root (replacing
|
||||||
// ?q=<text> — free-text search
|
// children); Mode B renders into #fristen-overhaul-mode-host; the
|
||||||
// ?forum=<id> — single forum (upc/de/epa/dpma)
|
// result view (post-commit) writes into #fristen-overhaul-root. To
|
||||||
// ?procs=<csv> — comma-separated proceeding codes
|
// keep those IDs unique in the DOM, only the active tab's panel ever
|
||||||
// ?party=<x> — claimant/defendant/both/""
|
// hosts the overhaul scaffold — installOverhaulHost() tears down any
|
||||||
// ?trigger_date=<iso> — global Stichtag
|
// existing host and installs a fresh one inside the target panel
|
||||||
// ?event=<rule_id> — scroll-highlight matching node (no anchor
|
// before handing off to the per-mode module.
|
||||||
// pin / zoom yet; T2 layering)
|
|
||||||
// ?flags=<csv> — scenario flag overrides; default off
|
|
||||||
//
|
|
||||||
// Legacy ?mode= params from the catalog UI are dropped silently — the
|
|
||||||
// /tools/fristenrechner + /tools/verfahrensablauf URLs still 301
|
|
||||||
// redirect here.
|
|
||||||
//
|
|
||||||
// T2-T4 layer on this shell:
|
|
||||||
// T2 — anchor pin + zoom + multi-proceeding scope.
|
|
||||||
// T3 — Akte landing + actuals overlay.
|
|
||||||
// T4 — appeal-target + court-set choices + per-proceeding Alle Optionen.
|
|
||||||
|
|
||||||
import { initI18n, t, tDyn } from "./i18n";
|
import { initI18n } from "./i18n";
|
||||||
import { initSidebar } from "./sidebar";
|
import { initSidebar } from "./sidebar";
|
||||||
import {
|
import { mountModeA } from "./fristenrechner-mode-a";
|
||||||
COLD_OPEN_DEFAULTS,
|
import { mountResultView } from "./fristenrechner-result";
|
||||||
PROCEEDINGS,
|
import { mountWizard } from "./fristenrechner-wizard";
|
||||||
type ProceedingDef,
|
import { initVerfahrensablauf } from "./verfahrensablauf";
|
||||||
type RenderedTimeline,
|
|
||||||
proceedingDisplayName,
|
|
||||||
renderCard,
|
|
||||||
scrollAnchorIntoView,
|
|
||||||
summariseRender,
|
|
||||||
} from "./procedures-tracker";
|
|
||||||
|
|
||||||
type ForumId = "upc" | "de" | "epa" | "dpma" | "";
|
type ProceduresTab = "proceeding" | "search" | "wizard" | "akte";
|
||||||
type PartyId = "claimant" | "defendant" | "both" | "";
|
|
||||||
|
|
||||||
// Find state. Single source of truth; URL keeps it shareable.
|
const TABS: ProceduresTab[] = ["proceeding", "search", "wizard", "akte"];
|
||||||
const state = {
|
|
||||||
q: "",
|
|
||||||
forum: "" as ForumId,
|
|
||||||
procs: [] as string[],
|
|
||||||
party: "" as PartyId,
|
|
||||||
triggerDate: todayISO(),
|
|
||||||
event: "",
|
|
||||||
zoom: false,
|
|
||||||
flags: [] as string[],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Per-anchor user-expanded set — when the multi-proceeding auto-collapse
|
function readTabFromUrl(): ProceduresTab {
|
||||||
// kicks in (§6.5), the user can [zeigen] specific proceedings. We track
|
|
||||||
// the explicit expansions so re-renders keep them open. Reset whenever
|
|
||||||
// the anchor changes (new pin clears prior expansion state).
|
|
||||||
let userExpanded = new Set<string>();
|
|
||||||
let lastAnchor = "";
|
|
||||||
|
|
||||||
function todayISO(): string {
|
|
||||||
return new Date().toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── URL state ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function readStateFromURL(): void {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
state.q = params.get("q") || "";
|
const raw = params.get("mode");
|
||||||
state.forum = (params.get("forum") || "") as ForumId;
|
if (raw && (TABS as string[]).includes(raw)) return raw as ProceduresTab;
|
||||||
const procs = params.get("procs") || "";
|
return "proceeding";
|
||||||
state.procs = procs ? procs.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
||||||
state.party = (params.get("party") || "") as PartyId;
|
|
||||||
state.triggerDate = params.get("trigger_date") || todayISO();
|
|
||||||
state.event = params.get("event") || "";
|
|
||||||
state.zoom = params.get("zoom") === "1";
|
|
||||||
const flags = params.get("flags") || "";
|
|
||||||
state.flags = flags ? flags.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
||||||
lastAnchor = state.event;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeStateToURL(): void {
|
function writeTabToUrl(tab: ProceduresTab): void {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const sp = url.searchParams;
|
if (tab === "proceeding") {
|
||||||
setOrDelete(sp, "q", state.q);
|
url.searchParams.delete("mode");
|
||||||
setOrDelete(sp, "forum", state.forum);
|
} else {
|
||||||
setOrDelete(sp, "procs", state.procs.join(","));
|
url.searchParams.set("mode", tab);
|
||||||
setOrDelete(sp, "party", state.party);
|
}
|
||||||
setOrDelete(sp, "trigger_date", state.triggerDate === todayISO() ? "" : state.triggerDate);
|
history.replaceState(null, "", url.pathname + url.search + url.hash);
|
||||||
setOrDelete(sp, "event", state.event);
|
|
||||||
setOrDelete(sp, "zoom", state.event && state.zoom ? "1" : "");
|
|
||||||
setOrDelete(sp, "flags", state.flags.join(","));
|
|
||||||
// Legacy ?mode= from the U0-U4 catalog era → drop on every state write
|
|
||||||
// so a bookmarked URL self-cleans on first interaction.
|
|
||||||
sp.delete("mode");
|
|
||||||
history.replaceState(null, "", url.pathname + (sp.toString() ? "?" + sp.toString() : "") + url.hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onAnchorChanged keeps userExpanded in sync. A fresh pin clears the
|
// installOverhaulHost moves the (legacy) #fristen-overhaul-root /
|
||||||
// prior expansion set so the auto-collapse rule (§6.5) kicks in from
|
// #fristen-overhaul-mode-host scaffold under `panelId`. Always clears
|
||||||
// scratch; unpinning clears it too so the full multi-proceeding view
|
// any existing host first, so the IDs stay unique across the page even
|
||||||
// returns.
|
// when the user toggles between Direkt-suchen and Geführt — both Mode
|
||||||
function onAnchorChanged(next: string): void {
|
// A and the wizard read these IDs from document.getElementById which
|
||||||
if (next === lastAnchor) return;
|
// returns the first match in DOM order, so two parallel hosts would
|
||||||
userExpanded = new Set();
|
// cross-wire.
|
||||||
if (!next) state.zoom = false;
|
function installOverhaulHost(panelId: string): HTMLElement | null {
|
||||||
lastAnchor = next;
|
document.querySelectorAll("#fristen-overhaul-root").forEach((el) => el.remove());
|
||||||
|
const panel = document.getElementById(panelId);
|
||||||
|
if (!panel) return null;
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="procedures-overhaul-host">
|
||||||
|
<div class="fristen-overhaul-root" id="fristen-overhaul-root">
|
||||||
|
<div id="fristen-overhaul-mode-host"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOrDelete(sp: URLSearchParams, key: string, value: string): void {
|
function setActiveTabUI(tab: ProceduresTab): void {
|
||||||
if (value) sp.set(key, value);
|
for (const t of TABS) {
|
||||||
else sp.delete(key);
|
const btn = document.getElementById(`procedures-tab-${t}`);
|
||||||
}
|
const panel = document.getElementById(`procedures-panel-${t}`);
|
||||||
|
const active = t === tab;
|
||||||
// ─── pill hydration ────────────────────────────────────────────────────────
|
if (btn) {
|
||||||
|
btn.classList.toggle("is-active", active);
|
||||||
function hydrateForumPills(): void {
|
btn.setAttribute("aria-selected", active ? "true" : "false");
|
||||||
const host = document.getElementById("tracker-pills-forum");
|
}
|
||||||
if (!host) return;
|
if (panel) panel.hidden = !active;
|
||||||
host.innerHTML = "";
|
|
||||||
const all: { id: ForumId; label: string }[] = [
|
|
||||||
{ id: "", label: t("procedures.filter.forum.all") },
|
|
||||||
{ id: "upc", label: "UPC" },
|
|
||||||
{ id: "de", label: "DE" },
|
|
||||||
{ id: "epa", label: "EPA" },
|
|
||||||
{ id: "dpma", label: "DPMA" },
|
|
||||||
];
|
|
||||||
for (const f of all) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "tracker-pill";
|
|
||||||
btn.textContent = f.label;
|
|
||||||
btn.dataset.forum = f.id;
|
|
||||||
if (state.forum === f.id) btn.classList.add("is-active");
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
state.forum = f.id;
|
|
||||||
// Drop procs that no longer match the active forum.
|
|
||||||
if (state.forum) {
|
|
||||||
state.procs = state.procs.filter((code) => {
|
|
||||||
const def = PROCEEDINGS.find((p) => p.code === code);
|
|
||||||
return def && def.forum === state.forum;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
writeStateToURL();
|
|
||||||
hydrateForumPills();
|
|
||||||
hydrateProcPills();
|
|
||||||
void rerender();
|
|
||||||
});
|
|
||||||
host.appendChild(btn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hydrateProcPills(): void {
|
// Verfahrensablauf wiring is idempotent-unfriendly (module-local
|
||||||
const host = document.getElementById("tracker-pills-proc");
|
// selectedType + lastResponse + listeners that re-bind on every
|
||||||
if (!host) return;
|
// proceeding click). Wire it exactly once per page load; on subsequent
|
||||||
host.innerHTML = "";
|
// activations the existing DOM + listeners are reused so picked
|
||||||
const visible = state.forum
|
// proceeding / dates / flags persist across tab switches.
|
||||||
? PROCEEDINGS.filter((p) => p.forum === state.forum)
|
let verfahrensablaufWired = false;
|
||||||
: PROCEEDINGS;
|
|
||||||
for (const p of visible) {
|
async function activateTab(tab: ProceduresTab): Promise<void> {
|
||||||
const btn = document.createElement("button");
|
setActiveTabUI(tab);
|
||||||
btn.type = "button";
|
if (tab === "search") {
|
||||||
btn.className = "tracker-pill";
|
installOverhaulHost("procedures-panel-search");
|
||||||
btn.textContent = proceedingDisplayName(p.code);
|
await mountModeA();
|
||||||
btn.dataset.code = p.code;
|
return;
|
||||||
btn.title = p.code;
|
}
|
||||||
if (state.procs.includes(p.code)) btn.classList.add("is-active");
|
if (tab === "wizard") {
|
||||||
btn.addEventListener("click", () => {
|
installOverhaulHost("procedures-panel-wizard");
|
||||||
if (state.procs.includes(p.code)) {
|
await mountWizard();
|
||||||
state.procs = state.procs.filter((c) => c !== p.code);
|
return;
|
||||||
} else {
|
}
|
||||||
state.procs = [...state.procs, p.code];
|
if (tab === "proceeding") {
|
||||||
}
|
if (!verfahrensablaufWired) {
|
||||||
writeStateToURL();
|
initVerfahrensablauf();
|
||||||
hydrateProcPills();
|
verfahrensablaufWired = true;
|
||||||
void rerender();
|
}
|
||||||
});
|
|
||||||
host.appendChild(btn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hydratePartyPills(): void {
|
function wireTabs(): void {
|
||||||
const host = document.getElementById("tracker-pills-party");
|
for (const t of TABS) {
|
||||||
if (!host) return;
|
const btn = document.getElementById(`procedures-tab-${t}`);
|
||||||
host.innerHTML = "";
|
if (!btn) continue;
|
||||||
const all: { id: PartyId; key: string }[] = [
|
|
||||||
{ id: "", key: "procedures.filter.party.all" },
|
|
||||||
{ id: "claimant", key: "deadlines.side.claimant" },
|
|
||||||
{ id: "defendant", key: "deadlines.side.defendant" },
|
|
||||||
];
|
|
||||||
for (const p of all) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "tracker-pill";
|
|
||||||
btn.textContent = t(p.key as never);
|
|
||||||
btn.dataset.party = p.id;
|
|
||||||
if (state.party === p.id) btn.classList.add("is-active");
|
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
state.party = p.id;
|
void activateTab(t);
|
||||||
writeStateToURL();
|
writeTabToUrl(t);
|
||||||
hydratePartyPills();
|
|
||||||
void rerender();
|
|
||||||
});
|
});
|
||||||
host.appendChild(btn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── search box ────────────────────────────────────────────────────────────
|
// boot dispatches on the URL: a deep link with `?event=` jumps straight
|
||||||
//
|
// to the linear result view (the Direkt-suchen tab stays as the visible
|
||||||
// Debounced 200ms. Free-text matches procedural events via the existing
|
// context). Otherwise the requested tab — defaulting to "proceeding" —
|
||||||
// /api/tools/fristenrechner/search?kind=events endpoint; the hits drive
|
// activates per readTabFromUrl().
|
||||||
// proceeding pre-selection (the proceedings the hits live in surface
|
async function boot(): Promise<void> {
|
||||||
// in the timeline body) and the first hit's rule_id becomes the
|
const params = new URLSearchParams(window.location.search);
|
||||||
// `?event=` anchor.
|
const eventRef = params.get("event") || "";
|
||||||
|
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
if (eventRef) {
|
||||||
|
setActiveTabUI("search");
|
||||||
function wireSearchInput(): void {
|
installOverhaulHost("procedures-panel-search");
|
||||||
const input = document.getElementById("tracker-search-input") as HTMLInputElement | null;
|
await mountResultView({
|
||||||
if (!input) return;
|
eventRef,
|
||||||
input.value = state.q;
|
triggerDate: params.get("trigger_date") || undefined,
|
||||||
input.addEventListener("input", () => {
|
party: params.get("party") || undefined,
|
||||||
if (searchTimer !== null) clearTimeout(searchTimer);
|
courtId: params.get("court_id") || undefined,
|
||||||
searchTimer = setTimeout(() => {
|
});
|
||||||
searchTimer = null;
|
|
||||||
void onSearchChanged(input.value.trim());
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSearchChanged(q: string): Promise<void> {
|
|
||||||
state.q = q;
|
|
||||||
// Empty query → revert to pill-driven set (or cold-open default).
|
|
||||||
if (!q) {
|
|
||||||
state.event = "";
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await activateTab(readTabFromUrl());
|
||||||
const url = new URL("/api/tools/fristenrechner/search", window.location.origin);
|
|
||||||
url.searchParams.set("q", q);
|
|
||||||
url.searchParams.set("kind", "events");
|
|
||||||
url.searchParams.set("limit", "20");
|
|
||||||
if (state.forum) url.searchParams.set("jurisdiction", state.forum.toUpperCase());
|
|
||||||
if (state.party) url.searchParams.set("party", state.party);
|
|
||||||
const resp = await fetch(url.pathname + url.search, { headers: { Accept: "application/json" } });
|
|
||||||
if (!resp.ok) {
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const body = await resp.json();
|
|
||||||
const events = Array.isArray(body.events) ? body.events : [];
|
|
||||||
|
|
||||||
// Collect distinct proceeding_type codes from the hits and pre-seed
|
|
||||||
// state.procs. If exactly one hit, scroll-highlight its anchor rule.
|
|
||||||
const procs: string[] = [];
|
|
||||||
for (const ev of events) {
|
|
||||||
const code = ev?.proceeding_type?.code;
|
|
||||||
if (typeof code === "string" && code && !procs.includes(code)) procs.push(code);
|
|
||||||
}
|
|
||||||
state.procs = procs;
|
|
||||||
const nextEvent = events.length === 1 && events[0]?.anchor_rule_id
|
|
||||||
? String(events[0].anchor_rule_id)
|
|
||||||
: "";
|
|
||||||
onAnchorChanged(nextEvent);
|
|
||||||
state.event = nextEvent;
|
|
||||||
writeStateToURL();
|
|
||||||
hydrateProcPills();
|
|
||||||
void rerender();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("tracker search failed", e);
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── trigger date input ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function wireTriggerDateInput(): void {
|
|
||||||
const input = document.getElementById("tracker-trigger-date") as HTMLInputElement | null;
|
|
||||||
if (!input) return;
|
|
||||||
input.value = state.triggerDate;
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
const next = input.value || todayISO();
|
|
||||||
if (next === state.triggerDate) return;
|
|
||||||
state.triggerDate = next;
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── flag toggle wiring ────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Forks on the per-proceeding "Optionen" strip dispatch via event
|
|
||||||
// delegation so re-rendering doesn't need to re-bind. Each tick mutates
|
|
||||||
// state.flags and triggers a re-render of the specific card.
|
|
||||||
|
|
||||||
function wireFlagDelegation(): void {
|
|
||||||
const host = document.getElementById("tracker-timelines");
|
|
||||||
if (!host) return;
|
|
||||||
host.addEventListener("change", (ev) => {
|
|
||||||
const target = ev.target as HTMLInputElement;
|
|
||||||
if (!target || target.tagName !== "INPUT") return;
|
|
||||||
if (target.type !== "checkbox") return;
|
|
||||||
const flagKey = target.dataset.flag || "";
|
|
||||||
if (!flagKey) return;
|
|
||||||
if (target.checked) {
|
|
||||||
if (!state.flags.includes(flagKey)) state.flags = [...state.flags, flagKey];
|
|
||||||
} else {
|
|
||||||
state.flags = state.flags.filter((f) => f !== flagKey);
|
|
||||||
}
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireClickDelegation handles pin / fokus / proc-toggle. Single listener
|
|
||||||
// on the timelines host; the render functions stamp data-action on the
|
|
||||||
// affordances they emit.
|
|
||||||
function wireClickDelegation(): void {
|
|
||||||
const host = document.getElementById("tracker-timelines");
|
|
||||||
if (!host) return;
|
|
||||||
host.addEventListener("click", (ev) => {
|
|
||||||
const btn = (ev.target as HTMLElement | null)?.closest<HTMLButtonElement>("button[data-action]");
|
|
||||||
if (!btn) return;
|
|
||||||
const action = btn.dataset.action || "";
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (action === "pin") {
|
|
||||||
const ruleId = btn.dataset.ruleId || "";
|
|
||||||
if (!ruleId) return;
|
|
||||||
// Click on the already-anchored node un-pins. Toggle pattern.
|
|
||||||
const next = state.event === ruleId ? "" : ruleId;
|
|
||||||
onAnchorChanged(next);
|
|
||||||
state.event = next;
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "fokus") {
|
|
||||||
// Toggle zoom on the current anchor.
|
|
||||||
if (!state.event) return;
|
|
||||||
state.zoom = !state.zoom;
|
|
||||||
writeStateToURL();
|
|
||||||
void rerender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "proc-toggle") {
|
|
||||||
const code = btn.dataset.code || "";
|
|
||||||
if (!code) return;
|
|
||||||
if (userExpanded.has(code)) userExpanded.delete(code);
|
|
||||||
else userExpanded.add(code);
|
|
||||||
void rerender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── render driver ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
let currentRender = 0;
|
|
||||||
|
|
||||||
function pickProceedingsToRender(): string[] {
|
|
||||||
if (state.procs.length > 0) return state.procs;
|
|
||||||
if (state.forum) {
|
|
||||||
return PROCEEDINGS.filter((p) => p.forum === state.forum).map((p) => p.code);
|
|
||||||
}
|
|
||||||
return COLD_OPEN_DEFAULTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rerender(): Promise<void> {
|
|
||||||
const seq = ++currentRender;
|
|
||||||
const host = document.getElementById("tracker-timelines");
|
|
||||||
const summary = document.getElementById("tracker-find-summary");
|
|
||||||
if (!host) return;
|
|
||||||
|
|
||||||
// Loading placeholder during render. The placeholder is replaced
|
|
||||||
// atomically once all cards return, so the user doesn't see
|
|
||||||
// intermediate flicker between cards.
|
|
||||||
host.innerHTML = `<div class="tracker-timelines-placeholder">${t("procedures.timelines.loading")}</div>`;
|
|
||||||
|
|
||||||
const codes = pickProceedingsToRender();
|
|
||||||
|
|
||||||
// Cold-open hint: if we're showing the curated default set, surface
|
|
||||||
// a small instruction so the user knows the page expects further
|
|
||||||
// narrowing for non-default proceedings.
|
|
||||||
const isColdOpen = state.procs.length === 0 && !state.forum && !state.q;
|
|
||||||
const hasAnchor = !!state.event;
|
|
||||||
const multiProceeding = codes.length > 1;
|
|
||||||
|
|
||||||
// Multi-proceeding anchor scope (§6.5). When an anchor is pinned and
|
|
||||||
// multiple proceedings are visible, non-anchored proceedings render
|
|
||||||
// as a one-line header card with a [zeigen] link. We don't know yet
|
|
||||||
// which card carries the anchor — that's a property of the calc
|
|
||||||
// response. Two-pass render: first pass resolves anchor location;
|
|
||||||
// second pass renders collapsed/full per code. Cheap because the
|
|
||||||
// collapsed render skips the calc fetch entirely.
|
|
||||||
//
|
|
||||||
// Optimisation path: probe just one card per render to find the
|
|
||||||
// anchor's home. Probe the matching code first when we already know
|
|
||||||
// it (cached on `lastAnchorProceeding` below).
|
|
||||||
|
|
||||||
// First-pass: render every card, collect which ones carry the anchor.
|
|
||||||
const firstPass: RenderedTimeline[] = await Promise.all(
|
|
||||||
codes.map((code) =>
|
|
||||||
renderCard({
|
|
||||||
proceedingType: code,
|
|
||||||
triggerDate: state.triggerDate,
|
|
||||||
flags: state.flags,
|
|
||||||
anchorRuleId: state.event || undefined,
|
|
||||||
zoom: state.zoom,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (seq !== currentRender) return; // a newer render started; bail.
|
|
||||||
|
|
||||||
// If we have an anchor + multi-proceeding, decide which cards
|
|
||||||
// collapse. Cards with hasAnchor=true stay expanded; others collapse
|
|
||||||
// unless the user explicitly expanded them via [zeigen].
|
|
||||||
let rendered: RenderedTimeline[] = firstPass;
|
|
||||||
if (hasAnchor && multiProceeding) {
|
|
||||||
rendered = await Promise.all(
|
|
||||||
firstPass.map(async (r) => {
|
|
||||||
if (r.hasAnchor) return r;
|
|
||||||
if (userExpanded.has(r.card.dataset.proceeding || "")) return r;
|
|
||||||
// Re-render collapsed.
|
|
||||||
return renderCard({
|
|
||||||
proceedingType: r.card.dataset.proceeding || "",
|
|
||||||
triggerDate: state.triggerDate,
|
|
||||||
flags: state.flags,
|
|
||||||
anchorRuleId: state.event || undefined,
|
|
||||||
collapsed: true,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (seq !== currentRender) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
host.innerHTML = "";
|
|
||||||
|
|
||||||
if (isColdOpen) {
|
|
||||||
const hint = document.createElement("div");
|
|
||||||
hint.className = "tracker-cold-open-hint";
|
|
||||||
hint.textContent = t("procedures.cold_open.hint");
|
|
||||||
host.appendChild(hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const r of rendered) host.appendChild(r.card);
|
|
||||||
|
|
||||||
if (rendered.length === 0) {
|
|
||||||
const empty = document.createElement("div");
|
|
||||||
empty.className = "tracker-timelines-empty";
|
|
||||||
empty.textContent = t("procedures.timelines.empty");
|
|
||||||
host.appendChild(empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find-header summary line. When an anchor is pinned, surface the
|
|
||||||
// anchor's name so the user has a visual confirmation of where
|
|
||||||
// they are.
|
|
||||||
if (summary) {
|
|
||||||
const base = summariseRender(rendered);
|
|
||||||
if (hasAnchor) {
|
|
||||||
const anchorName = findAnchorName(firstPass, state.event);
|
|
||||||
summary.textContent = anchorName
|
|
||||||
? `${base} · ${tDyn("procedures.find.summary.anchor").replace("{name}", anchorName)}`
|
|
||||||
: base;
|
|
||||||
} else {
|
|
||||||
summary.textContent = base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll-highlight the anchored node, if any. Walks every card so a
|
|
||||||
// ?event= deep link works even when the same rule appears in a
|
|
||||||
// shared-chain proceeding (e.g. inf.cfi and ccr.cfi).
|
|
||||||
if (state.event) {
|
|
||||||
for (const r of rendered) scrollAnchorIntoView(r.card, state.event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// findAnchorName resolves the anchored rule's display name across the
|
|
||||||
// rendered cards. Returns "" when the anchor isn't in any visible
|
|
||||||
// proceeding (e.g. invalid ?event= deep link).
|
|
||||||
function findAnchorName(rendered: RenderedTimeline[], ruleId: string): string {
|
|
||||||
if (!ruleId) return "";
|
|
||||||
for (const r of rendered) {
|
|
||||||
if (!r.data) continue;
|
|
||||||
const hit = r.data.deadlines.find((d) => d.ruleId === ruleId);
|
|
||||||
if (!hit) continue;
|
|
||||||
return hit.name || hit.nameEN || ruleId;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── boot ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
initI18n();
|
initI18n();
|
||||||
initSidebar();
|
initSidebar();
|
||||||
readStateFromURL();
|
wireTabs();
|
||||||
hydrateForumPills();
|
void boot();
|
||||||
hydrateProcPills();
|
|
||||||
hydratePartyPills();
|
|
||||||
wireSearchInput();
|
|
||||||
wireTriggerDateInput();
|
|
||||||
wireFlagDelegation();
|
|
||||||
wireClickDelegation();
|
|
||||||
void rerender();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1358,8 +1358,6 @@ export type I18nKey =
|
|||||||
| "deadlines.filter.status"
|
| "deadlines.filter.status"
|
||||||
| "deadlines.filter.thisweek"
|
| "deadlines.filter.thisweek"
|
||||||
| "deadlines.filter.today"
|
| "deadlines.filter.today"
|
||||||
| "deadlines.flag.amend"
|
|
||||||
| "deadlines.flag.cci"
|
|
||||||
| "deadlines.flag.ccr"
|
| "deadlines.flag.ccr"
|
||||||
| "deadlines.flag.inf_amend"
|
| "deadlines.flag.inf_amend"
|
||||||
| "deadlines.flag.rev_amend"
|
| "deadlines.flag.rev_amend"
|
||||||
@@ -2205,40 +2203,19 @@ export type I18nKey =
|
|||||||
| "partner_unit.members_label"
|
| "partner_unit.members_label"
|
||||||
| "partner_unit.none"
|
| "partner_unit.none"
|
||||||
| "partner_unit.subtitle"
|
| "partner_unit.subtitle"
|
||||||
| "procedures.cold_open.hint"
|
|
||||||
| "procedures.filter.axis.date"
|
|
||||||
| "procedures.filter.axis.forum"
|
| "procedures.filter.axis.forum"
|
||||||
| "procedures.filter.axis.kind"
|
| "procedures.filter.axis.kind"
|
||||||
| "procedures.filter.axis.party"
|
| "procedures.filter.axis.party"
|
||||||
| "procedures.filter.axis.proc"
|
| "procedures.filter.axis.proc"
|
||||||
| "procedures.filter.forum.all"
|
|
||||||
| "procedures.filter.party.all"
|
|
||||||
| "procedures.filter.search.placeholder"
|
| "procedures.filter.search.placeholder"
|
||||||
| "procedures.find.summary.anchor"
|
|
||||||
| "procedures.find.summary.empty"
|
|
||||||
| "procedures.find.summary.many"
|
|
||||||
| "procedures.find.summary.one"
|
|
||||||
| "procedures.heading"
|
| "procedures.heading"
|
||||||
| "procedures.node.fokus"
|
|
||||||
| "procedures.node.here"
|
|
||||||
| "procedures.node.pin"
|
|
||||||
| "procedures.panel.akte.placeholder"
|
| "procedures.panel.akte.placeholder"
|
||||||
| "procedures.proceeding.hide"
|
|
||||||
| "procedures.proceeding.show"
|
|
||||||
| "procedures.proceeding.toggle"
|
|
||||||
| "procedures.subtitle"
|
| "procedures.subtitle"
|
||||||
| "procedures.tab.akte"
|
| "procedures.tab.akte"
|
||||||
| "procedures.tab.proceeding"
|
| "procedures.tab.proceeding"
|
||||||
| "procedures.tab.search"
|
| "procedures.tab.search"
|
||||||
| "procedures.tab.wizard"
|
| "procedures.tab.wizard"
|
||||||
| "procedures.timelines.court_set"
|
|
||||||
| "procedures.timelines.empty"
|
|
||||||
| "procedures.timelines.error"
|
|
||||||
| "procedures.timelines.loading"
|
|
||||||
| "procedures.timelines.options"
|
|
||||||
| "procedures.title"
|
| "procedures.title"
|
||||||
| "procedures.zoom.breadcrumb"
|
|
||||||
| "procedures.zoom.hidden"
|
|
||||||
| "project.instance_level.appeal"
|
| "project.instance_level.appeal"
|
||||||
| "project.instance_level.cassation"
|
| "project.instance_level.cassation"
|
||||||
| "project.instance_level.first"
|
| "project.instance_level.first"
|
||||||
|
|||||||
@@ -4,32 +4,23 @@ import { PaliadinWidget } from "./components/PaliadinWidget";
|
|||||||
import { BottomNav } from "./components/BottomNav";
|
import { BottomNav } from "./components/BottomNav";
|
||||||
import { Footer } from "./components/Footer";
|
import { Footer } from "./components/Footer";
|
||||||
import { PWAHead } from "./components/PWAHead";
|
import { PWAHead } from "./components/PWAHead";
|
||||||
|
import { VerfahrensablaufBody } from "./components/VerfahrensablaufBody";
|
||||||
|
|
||||||
// /tools/procedures — workflow-tracker shell (m/paliad#152 T1,
|
// U0 — Skeleton for the unified procedural-events tool
|
||||||
// docs/design-procedures-workflow-tracker-2026-05-27.md §9 T1).
|
// (m/paliad#151, design docs/design-unified-procedural-events-tool-2026-05-27.md).
|
||||||
//
|
//
|
||||||
// Single canonical shape:
|
// Folds /tools/fristenrechner (Mode A + Mode B + result) and
|
||||||
// 1. Sticky find header — search input + forum / Verfahren / Partei
|
// /tools/verfahrensablauf into a single page at /tools/procedures. Each
|
||||||
// pill rows + global Stichtag. The header narrows the timeline
|
// later slice fills one of the four entry tabs:
|
||||||
// set rendered below; it is not itself a tab strip.
|
|
||||||
// 2. Timeline body — one card per matched proceeding, rendered as a
|
|
||||||
// chained tree by parent_id with priority-styled bullets. Cold
|
|
||||||
// open renders the 6 curated default proceedings from
|
|
||||||
// design §8 / §11.Q4.
|
|
||||||
//
|
//
|
||||||
// Each later slice layers on top of this shell:
|
// U1 — Direkt suchen (Mode A search)
|
||||||
// T2 — anchor pin + zoom + multi-proceeding scope (§6.5).
|
// U2 — Geführt (Mode B wizard)
|
||||||
// T3 — Akte landing + actuals overlay.
|
// U3 — Verfahren (Verfahrensablauf tree + 3-way detail filter)
|
||||||
// T4 — appeal-target chip group + court-set choices + per-proceeding
|
// U4 — Hard-cut 301 (drop legacy pages, redirect URLs)
|
||||||
// "Alle Optionen" toggle.
|
|
||||||
// T5 — dead-code removal (the old per-tab fristenrechner-mode-*,
|
|
||||||
// fristenrechner-wizard, fristenrechner-result, verfahrensablauf
|
|
||||||
// modules + their CSS once nothing imports them).
|
|
||||||
//
|
//
|
||||||
// No DB dependency — the page itself is static HTML; data flows over
|
// This file ships only the page chrome — sidebar, header, filter strip
|
||||||
// the existing /api/tools/fristenrechner endpoints. The 4 entry-mode
|
// with search box, four entry-mode tabs, and the host containers the
|
||||||
// tabs the catalog (U0-U4) shipped earlier today are deleted in this
|
// later slices mount their UI into. No data wiring.
|
||||||
// PR per m's Q7 divergent pick (direct replace, no flag).
|
|
||||||
|
|
||||||
export function renderProcedures(): string {
|
export function renderProcedures(): string {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
const today = new Date().toISOString().split("T")[0];
|
||||||
@@ -55,18 +46,20 @@ export function renderProcedures(): string {
|
|||||||
<div className="tool-header">
|
<div className="tool-header">
|
||||||
<h1 data-i18n="procedures.heading">Verfahren & Fristen</h1>
|
<h1 data-i18n="procedures.heading">Verfahren & Fristen</h1>
|
||||||
<p className="tool-subtitle" data-i18n="procedures.subtitle">
|
<p className="tool-subtitle" data-i18n="procedures.subtitle">
|
||||||
Verfahrensabläufe als Zeitstrahl — suchen, filtern, Verzweigungen wählen.
|
Verfahrensablauf, Fristenrechner und gerührte Suche in einem Tool.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Find affordance (design §2). Sticky header — search,
|
{/* Shared filter strip — search box + four chip groups
|
||||||
forum + Verfahren + Partei pills, and the global
|
(forum / proceeding / event_kind / party). Lives at the
|
||||||
Stichtag (date input). Pills hydrate from
|
top of the page so every entry tab and output mode reads
|
||||||
procedures-tracker on boot; markup carries the host
|
the same active filter set (design §4 + m's Q3
|
||||||
rows only. */}
|
divergence: search composes with chip filters). U0
|
||||||
<section className="tracker-find" aria-label="Filter" id="tracker-find">
|
ships the markup only; chip hydration + search wiring
|
||||||
<div className="tracker-find-search">
|
arrive with U1-U3. */}
|
||||||
<svg className="tracker-find-search-icon" width="18" height="18" viewBox="0 0 24 24"
|
<section className="procedures-filter-strip" aria-label="Filter">
|
||||||
|
<div className="procedures-filter-search">
|
||||||
|
<svg className="procedures-filter-search-icon" width="18" height="18" viewBox="0 0 24 24"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" aria-hidden="true">
|
stroke-linejoin="round" aria-hidden="true">
|
||||||
<circle cx="11" cy="11" r="7"></circle>
|
<circle cx="11" cy="11" r="7"></circle>
|
||||||
@@ -74,53 +67,120 @@ export function renderProcedures(): string {
|
|||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
id="tracker-search-input"
|
id="procedures-search-input"
|
||||||
className="tracker-find-search-input"
|
className="procedures-filter-search-input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
data-i18n-placeholder="procedures.filter.search.placeholder"
|
data-i18n-placeholder="procedures.filter.search.placeholder"
|
||||||
placeholder="Klageerhebung, Hinweisbeschluss, oral hearing…"
|
placeholder="Klageerhebung, Hinweisbeschluss, oral hearing…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="procedures-filter-chips" id="procedures-filter-chips">
|
||||||
<div className="tracker-find-row" data-axis="forum">
|
<div className="procedures-filter-chip-row" data-axis="forum">
|
||||||
<span className="tracker-find-axis-label" data-i18n="procedures.filter.axis.forum">Forum:</span>
|
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.forum">Forum:</span>
|
||||||
<div className="tracker-find-pills" id="tracker-pills-forum" role="group"></div>
|
<div className="procedures-filter-chip-host" id="procedures-filter-chips-forum"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="procedures-filter-chip-row" data-axis="proc">
|
||||||
<div className="tracker-find-row" data-axis="proc">
|
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.proc">Verfahren:</span>
|
||||||
<span className="tracker-find-axis-label" data-i18n="procedures.filter.axis.proc">Verfahren:</span>
|
<div className="procedures-filter-chip-host" id="procedures-filter-chips-proc"></div>
|
||||||
<div className="tracker-find-pills" id="tracker-pills-proc" role="group"></div>
|
</div>
|
||||||
</div>
|
<div className="procedures-filter-chip-row" data-axis="kind">
|
||||||
|
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.kind">Ereignisart:</span>
|
||||||
<div className="tracker-find-row" data-axis="party">
|
<div className="procedures-filter-chip-host" id="procedures-filter-chips-kind"></div>
|
||||||
<span className="tracker-find-axis-label" data-i18n="procedures.filter.axis.party">Partei:</span>
|
</div>
|
||||||
<div className="tracker-find-pills" id="tracker-pills-party" role="group"></div>
|
<div className="procedures-filter-chip-row" data-axis="party">
|
||||||
</div>
|
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.party">Partei:</span>
|
||||||
|
<div className="procedures-filter-chip-host" id="procedures-filter-chips-party"></div>
|
||||||
<div className="tracker-find-row" data-axis="date">
|
</div>
|
||||||
<label htmlFor="tracker-trigger-date" className="tracker-find-axis-label"
|
|
||||||
data-i18n="procedures.filter.axis.date">Stichtag:</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
id="tracker-trigger-date"
|
|
||||||
className="tracker-find-date-input"
|
|
||||||
value={today}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="tracker-find-summary" id="tracker-find-summary" aria-live="polite"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Timeline body — one card per matched proceeding. Cards
|
|
||||||
are appended by procedures-tracker.ts on boot and
|
|
||||||
re-rendered when the find header changes. */}
|
|
||||||
<section className="tracker-timelines" id="tracker-timelines" aria-label="Verfahren">
|
|
||||||
<div className="tracker-timelines-placeholder" id="tracker-timelines-placeholder"
|
|
||||||
data-i18n="procedures.timelines.loading">
|
|
||||||
Verfahren werden geladen…
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Entry-mode tab strip — all four tabs visible from boot
|
||||||
|
(m's Q3 divergence). The active tab is URL-driven
|
||||||
|
(?mode=proceeding|search|wizard|akte); cold open lands
|
||||||
|
on "proceeding" per design §11.5.Q3. */}
|
||||||
|
<nav className="procedures-tabs" role="tablist" aria-label="Einstieg">
|
||||||
|
<button type="button"
|
||||||
|
className="procedures-tab is-active"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
data-tab="proceeding"
|
||||||
|
id="procedures-tab-proceeding">
|
||||||
|
<span className="procedures-tab-icon" aria-hidden="true">📚</span>
|
||||||
|
<span className="procedures-tab-label" data-i18n="procedures.tab.proceeding">Verfahren wählen</span>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
className="procedures-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
data-tab="search"
|
||||||
|
id="procedures-tab-search">
|
||||||
|
<span className="procedures-tab-icon" aria-hidden="true">⚡</span>
|
||||||
|
<span className="procedures-tab-label" data-i18n="procedures.tab.search">Direkt suchen</span>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
className="procedures-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
data-tab="wizard"
|
||||||
|
id="procedures-tab-wizard">
|
||||||
|
<span className="procedures-tab-icon" aria-hidden="true">🧭</span>
|
||||||
|
<span className="procedures-tab-label" data-i18n="procedures.tab.wizard">Geführt</span>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
className="procedures-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
data-tab="akte"
|
||||||
|
id="procedures-tab-akte">
|
||||||
|
<span className="procedures-tab-icon" aria-hidden="true">📁</span>
|
||||||
|
<span className="procedures-tab-label" data-i18n="procedures.tab.akte">Aus Akte</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Per-tab content hosts. Only one is visible at a time —
|
||||||
|
procedures.ts toggles `hidden` on the inactive ones.
|
||||||
|
Each later slice fills the corresponding host. */}
|
||||||
|
<section className="procedures-panel" id="procedures-panel-proceeding" role="tabpanel"
|
||||||
|
aria-labelledby="procedures-tab-proceeding">
|
||||||
|
{/* Verfahrensablauf wizard body — shared TSX component
|
||||||
|
used by /tools/verfahrensablauf (legacy) and the
|
||||||
|
unified /tools/procedures page. procedures.ts calls
|
||||||
|
initVerfahrensablauf() on the first activation of
|
||||||
|
this tab, which wires the .proceeding-btn clicks,
|
||||||
|
timeline-container, detail-mode toggle, etc. against
|
||||||
|
the markup. The legacy page's auto-boot is guarded
|
||||||
|
against the procedures-only #procedures-panel-proceeding
|
||||||
|
element so it doesn't fire twice. */}
|
||||||
|
<VerfahrensablaufBody todayIso={today} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="procedures-panel" id="procedures-panel-search" role="tabpanel"
|
||||||
|
aria-labelledby="procedures-tab-search" hidden></section>
|
||||||
|
|
||||||
|
<section className="procedures-panel" id="procedures-panel-wizard" role="tabpanel"
|
||||||
|
aria-labelledby="procedures-tab-wizard" hidden></section>
|
||||||
|
|
||||||
|
<section className="procedures-panel" id="procedures-panel-akte" role="tabpanel"
|
||||||
|
aria-labelledby="procedures-tab-akte" hidden>
|
||||||
|
<div className="procedures-panel-placeholder" data-i18n="procedures.panel.akte.placeholder">
|
||||||
|
Akten-Einstieg folgt in einem späteren Slice.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Tree output host. Slice U3 mounts the Verfahrensablauf
|
||||||
|
tree here; U0 leaves it empty + hidden so the
|
||||||
|
tab placeholders are the only thing visible. */}
|
||||||
|
<section className="procedures-output procedures-output-tree" id="procedures-output-tree"
|
||||||
|
aria-label="Tree output" hidden></section>
|
||||||
|
|
||||||
|
{/* Linear-drawer host. Inline drawer expanding beneath a
|
||||||
|
tree card (design §8 — desktop) AND the standalone
|
||||||
|
linear follow-up view that Mode A / Mode B land on
|
||||||
|
after locking a trigger event (design §3.2). U1
|
||||||
|
switches it on. */}
|
||||||
|
<section className="procedures-output procedures-output-linear" id="procedures-output-linear"
|
||||||
|
aria-label="Linear output" hidden></section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -19811,479 +19811,3 @@ a.fristen-overhaul-rule-source {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* Workflow tracker — /tools/procedures (m/paliad#152 T1+).
|
|
||||||
*
|
|
||||||
* Single canonical shape: sticky find header + per-proceeding cards
|
|
||||||
* rendered as chained trees by parent_id. See
|
|
||||||
* docs/design-procedures-workflow-tracker-2026-05-27.md.
|
|
||||||
* T1 ships the shell + flat fork checkboxes; T2-T4 layer anchor pin /
|
|
||||||
* Akte / appeal-target on top.
|
|
||||||
* ========================================================================= */
|
|
||||||
|
|
||||||
.tracker-find {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 5;
|
|
||||||
background: var(--color-bg);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
padding: 0.75rem 0 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-search {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 0.6rem;
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem 0.5rem 2.2rem;
|
|
||||||
border: 1px solid var(--color-border-strong);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-search-input:focus {
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
outline-offset: -1px;
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-axis-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
min-width: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-pills {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-pill {
|
|
||||||
padding: 0.25rem 0.6rem;
|
|
||||||
border: 1px solid var(--color-border-strong);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s, border-color 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-pill:hover {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-pill.is-active {
|
|
||||||
background: var(--color-accent);
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
color: var(--color-accent-fg);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-date-input {
|
|
||||||
padding: 0.3rem 0.5rem;
|
|
||||||
border: 1px solid var(--color-border-strong);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-find-summary {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
min-height: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-timelines {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-timelines-placeholder,
|
|
||||||
.tracker-timelines-empty,
|
|
||||||
.tracker-cold-open-hint {
|
|
||||||
padding: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-cold-open-hint {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
border: 1px dashed var(--color-accent);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-style: normal;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── per-proceeding card ───────────────────────────────────────── */
|
|
||||||
|
|
||||||
.tracker-proceeding {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--color-bg);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
padding: 0.6rem 0.9rem;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-jur {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
background: var(--color-text);
|
|
||||||
color: var(--color-bg);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-name {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-code {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-options {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.5rem 0.9rem;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
background: var(--color-bg);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-options-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-option {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-option input[type="checkbox"] {
|
|
||||||
margin: 0;
|
|
||||||
accent-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-body {
|
|
||||||
padding: 0.75rem 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-loading,
|
|
||||||
.tracker-proceeding-empty,
|
|
||||||
.tracker-proceeding-error {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-error {
|
|
||||||
color: var(--color-text);
|
|
||||||
background: rgba(245, 101, 101, 0.08);
|
|
||||||
border-left: 3px solid #ef4444;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── tree nodes ────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.tracker-tree {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-tree-root {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-tree .tracker-tree {
|
|
||||||
padding-left: 1.4rem;
|
|
||||||
position: relative;
|
|
||||||
border-left: 1px dashed var(--color-border-strong);
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node {
|
|
||||||
position: relative;
|
|
||||||
padding: 0.3rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.25rem 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node:hover > .tracker-node-line {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-bullet {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.7rem;
|
|
||||||
height: 0.7rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-text);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--mandatory > .tracker-node-line .tracker-node-bullet {
|
|
||||||
background: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--recommended > .tracker-node-line .tracker-node-bullet {
|
|
||||||
background: var(--color-text);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--optional > .tracker-node-line .tracker-node-bullet {
|
|
||||||
background: transparent;
|
|
||||||
border: 1.5px dotted var(--color-text-muted);
|
|
||||||
width: 0.65rem;
|
|
||||||
height: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--informational > .tracker-node-line .tracker-node-bullet {
|
|
||||||
background: var(--color-text-subtle);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--court > .tracker-node-line .tracker-node-bullet {
|
|
||||||
background: #5b8def;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-ref {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-date {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-party {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 1.4rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.05rem 0.35rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-party--court {
|
|
||||||
background: rgba(91, 141, 239, 0.12);
|
|
||||||
color: #2c5fb8;
|
|
||||||
border-color: rgba(91, 141, 239, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-party--claimant {
|
|
||||||
background: rgba(34, 197, 94, 0.10);
|
|
||||||
color: #1f7a4a;
|
|
||||||
border-color: rgba(34, 197, 94, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-party--defendant {
|
|
||||||
background: rgba(245, 158, 11, 0.10);
|
|
||||||
color: #92500a;
|
|
||||||
border-color: rgba(245, 158, 11, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--anchored > .tracker-node-line {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
border-left: 4px solid var(--color-accent);
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-here {
|
|
||||||
display: block;
|
|
||||||
margin: 0.2rem 0 0.2rem 0.4rem;
|
|
||||||
padding: 0.15rem 0.4rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
color: var(--color-accent-dark);
|
|
||||||
background: var(--color-accent);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-pin,
|
|
||||||
.tracker-node-fokus {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
padding: 0.1rem 0.3rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
opacity: 0.35;
|
|
||||||
transition: opacity 0.1s, background 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node:hover > .tracker-node-line .tracker-node-pin,
|
|
||||||
.tracker-node--anchored > .tracker-node-line .tracker-node-pin,
|
|
||||||
.tracker-node-fokus {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node-pin:hover,
|
|
||||||
.tracker-node-fokus:hover {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding--anchored {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
box-shadow: 0 0 0 1px var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-toggle {
|
|
||||||
border: 1px solid var(--color-border-strong);
|
|
||||||
background: var(--color-bg);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.15rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-proceeding-toggle:hover {
|
|
||||||
background: var(--color-bg-lime-tint);
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsed proceeding card — header-only render (§6.5). The toggle
|
|
||||||
* shows "zeigen"; clicking expands. Body stays absent. */
|
|
||||||
.tracker-proceeding--collapsed .tracker-proceeding-header {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zoom breadcrumb (§6.2). One-line ancestors trail above the focused
|
|
||||||
* subtree. The anchor crumb at the right is highlighted lime so the
|
|
||||||
* user sees their current depth. */
|
|
||||||
.tracker-zoom-breadcrumb {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
padding: 0.3rem 0.5rem;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
border-bottom: 1px dashed var(--color-border);
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-zoom-crumb {
|
|
||||||
padding: 0.05rem 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-zoom-crumb--anchor {
|
|
||||||
background: var(--color-accent);
|
|
||||||
color: var(--color-accent-fg);
|
|
||||||
border-radius: 3px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-zoom-crumb-sep {
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-zoom-hidden {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
padding-top: 0.3rem;
|
|
||||||
border-top: 1px dashed var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracker-node--highlight > .tracker-node-line {
|
|
||||||
animation: tracker-node-flash 3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tracker-node-flash {
|
|
||||||
0% { background: var(--color-accent); }
|
|
||||||
100% { background: transparent; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.tracker-find-axis-label {
|
|
||||||
min-width: 4rem;
|
|
||||||
}
|
|
||||||
.tracker-node-date {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user