Merge: t-paliad-198 — Determinator row-cascade Slice 3 (mobile polish + inline search + tooltip polish — cascade redesign complete)
This commit is contained in:
@@ -2296,14 +2296,20 @@ function initPathwayFork() {
|
||||
// t-paliad-180: search escape-hatch (Pathway B Direkt-suchen
|
||||
// button) routes to ?mode=filter. Reset link drops the cascade
|
||||
// slug back to root, keeping mode / perspective / inbox alone.
|
||||
// t-paliad-198 Slice 3: Direkt-suchen icon expands an inline search
|
||||
// overlay over the row stack — the legacy ?mode=filter route is kept
|
||||
// for deep-link compatibility but is no longer the user-facing path.
|
||||
document.getElementById("fristen-row-search-link")?.addEventListener("click", () => {
|
||||
setPathwayURL("b", "filter");
|
||||
showBMode("filter");
|
||||
setInlineSearchActive(true);
|
||||
});
|
||||
document.getElementById("fristen-row-search-panel-back")?.addEventListener("click", () => {
|
||||
setInlineSearchActive(false);
|
||||
});
|
||||
document.getElementById("fristen-row-reset")?.addEventListener("click", () => {
|
||||
currentActiveRow = null;
|
||||
navigateB1("");
|
||||
});
|
||||
initInlineSearch();
|
||||
|
||||
// Reselect: drop the locked context, return to Step 1.
|
||||
document.getElementById("fristen-step1-summary-reselect")?.addEventListener("click", () => {
|
||||
@@ -2524,7 +2530,10 @@ function nodeStepQuestion(n: EventCategoryNode): string {
|
||||
// without losing the rest. Slice 2 will add project-driven prefills +
|
||||
// auto-walk; Slice 3 polishes mobile + the search escape-hatch.
|
||||
|
||||
type RowKind = "mode" | "perspective" | "inbox" | "cascade";
|
||||
// t-paliad-198 Slice 3: mode kind retired — the icon-button at the top
|
||||
// of the row stack (`#fristen-row-search-link`) replaces the mode row,
|
||||
// toggling an inline search overlay instead of routing to ?mode=filter.
|
||||
type RowKind = "perspective" | "inbox" | "cascade";
|
||||
type RowState = "active" | "answered" | "prefilled";
|
||||
|
||||
interface RowOption {
|
||||
@@ -2594,11 +2603,6 @@ function inboxOptionLabel(value: string): string {
|
||||
return t("deadlines.inbox.all");
|
||||
}
|
||||
|
||||
function modeOptionLabel(value: string): string {
|
||||
return value === "filter"
|
||||
? t("deadlines.pathway.b.mode.filter")
|
||||
: t("deadlines.pathway.b.mode.tree");
|
||||
}
|
||||
|
||||
// Slice 2: cascade-segment ↔ fristenrechner-code bridge. The event_categories
|
||||
// taxonomy uses kebab-case segments under the `cms-eingang.*` buckets to
|
||||
@@ -2688,24 +2692,6 @@ function buildRowStack(currentSlug: string): RowSpec[] {
|
||||
return projectRef;
|
||||
};
|
||||
|
||||
// R0 — Mode. Always present, defaults to "tree" so a fresh visit
|
||||
// renders as answered. ändern flips to active and lets the user
|
||||
// re-pick; selecting "filter" routes to ?mode=filter (replaces the
|
||||
// cascade with B2 search, handled by the row click wiring).
|
||||
const mode = readBModeFromURL();
|
||||
rows.push({
|
||||
kind: "mode",
|
||||
rowId: "mode",
|
||||
state: currentActiveRow === "mode" ? "active" : "answered",
|
||||
question: t("deadlines.row.mode.question"),
|
||||
options: [
|
||||
{ value: "tree", label: t("deadlines.pathway.b.mode.tree") },
|
||||
{ value: "filter", label: t("deadlines.pathway.b.mode.filter") },
|
||||
],
|
||||
pickedValue: mode,
|
||||
pickedLabel: modeOptionLabel(mode),
|
||||
});
|
||||
|
||||
// R1 — Perspective. Default null ("Beide") = no filter; the row
|
||||
// renders as answered with that label. t-paliad-164 carry-over: a
|
||||
// project-bound perspective shows as `is-prefilled` with the "aus Akte"
|
||||
@@ -2958,6 +2944,12 @@ function renderRowStack(currentSlug: string) {
|
||||
const rows = buildRowStack(currentSlug);
|
||||
stack.innerHTML = rows.map((r, i) => rowHtml(r, i + 1)).join("");
|
||||
maybeShowAutoWalkTooltip(stack, rows);
|
||||
// t-paliad-198 Slice 3: bring the active row into view on every
|
||||
// render. Particularly important on mobile, where a chip pick may
|
||||
// push the next active row below the fold; the helper is a no-op
|
||||
// when the row is already in view, so desktop doesn't see jumpy
|
||||
// scrolling.
|
||||
autoscrollToActiveRow(stack);
|
||||
|
||||
// Wire chip picks (active rows).
|
||||
stack.querySelectorAll<HTMLButtonElement>(".fristen-row-chip").forEach((btn) => {
|
||||
@@ -2998,14 +2990,9 @@ function renderRowStack(currentSlug: string) {
|
||||
// every render); the other kinds update their per-kind state + clear
|
||||
// the "currently being edited" override.
|
||||
function handleRowPick(rowId: string, value: string) {
|
||||
if (rowId === "mode") {
|
||||
const next: BMode = value === "filter" ? "filter" : "tree";
|
||||
setPathwayURL("b", next);
|
||||
currentActiveRow = null;
|
||||
cascadeAutoWalkStopAfter = null;
|
||||
showBMode(next);
|
||||
return;
|
||||
}
|
||||
// t-paliad-198 Slice 3: any chip pick is also a "user interacted"
|
||||
// signal — the auto-walk tooltip's job is done.
|
||||
dismissAutoWalkTooltip();
|
||||
if (rowId === "perspective") {
|
||||
const next: Perspective = value === "claimant" || value === "defendant" ? value : null;
|
||||
writePerspectiveToURL(next);
|
||||
@@ -3044,9 +3031,12 @@ function handleRowPick(rowId: string, value: string) {
|
||||
// auto-walked, caps the auto-walk depth so the row turns active
|
||||
// in-place without changing the URL.
|
||||
function handleRowEdit(rowId: string) {
|
||||
if (rowId === "mode" || rowId === "perspective" || rowId === "inbox") {
|
||||
if (rowId === "perspective" || rowId === "inbox") {
|
||||
currentActiveRow = rowId;
|
||||
renderRowStack(readB1PathFromURL());
|
||||
// t-paliad-198 Slice 3: any explicit ändern click counts as user
|
||||
// interaction, so the auto-walk tooltip is no longer needed.
|
||||
dismissAutoWalkTooltip();
|
||||
return;
|
||||
}
|
||||
if (rowId.startsWith("cascade:")) {
|
||||
@@ -3062,6 +3052,7 @@ function handleRowEdit(rowId: string) {
|
||||
currentActiveRow = null;
|
||||
cascadeAutoWalkStopAfter = null;
|
||||
navigateB1(revertSlug);
|
||||
dismissAutoWalkTooltip();
|
||||
return;
|
||||
}
|
||||
// Row K is auto-walked (beyond the URL trail). Suppress auto-walk
|
||||
@@ -3070,9 +3061,170 @@ function handleRowEdit(rowId: string) {
|
||||
cascadeAutoWalkStopAfter = idx;
|
||||
currentActiveRow = null;
|
||||
renderRowStack(urlSlug);
|
||||
dismissAutoWalkTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// t-paliad-198 Slice 3 — inline search overlay + autoscroll-to-active +
|
||||
// tooltip polish.
|
||||
// ============================================================================
|
||||
//
|
||||
// Inline search replaces the Slice 1 mode-toggle row: clicking the
|
||||
// `🔍 Direkt suchen` icon in the row-stack header expands a search input
|
||||
// over the row stack and renders results into the same #fristen-b1-results
|
||||
// container the cascade narrows into. ESC inside the input or the back
|
||||
// button collapses it. State is ephemeral — no URL change — so a user can
|
||||
// step into search to check a name, then return to the cascade with all
|
||||
// their picks intact. Deep-link path ?mode=filter still routes to the
|
||||
// legacy B2 panel for backwards compatibility, but is no longer exposed
|
||||
// in the cascade UI.
|
||||
|
||||
let inlineSearchActive = false;
|
||||
let inlineSearchSeq = 0;
|
||||
let inlineSearchDebounce: number | undefined;
|
||||
|
||||
function setInlineSearchActive(active: boolean) {
|
||||
if (inlineSearchActive === active) return;
|
||||
inlineSearchActive = active;
|
||||
const panel = document.getElementById("fristen-row-search-panel");
|
||||
const header = document.getElementById("fristen-row-stack-header");
|
||||
const stack = document.getElementById("fristen-row-stack");
|
||||
const results = document.getElementById("fristen-b1-results");
|
||||
const trigger = document.getElementById("fristen-row-search-link") as HTMLButtonElement | null;
|
||||
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
|
||||
if (!panel || !stack) return;
|
||||
panel.hidden = !active;
|
||||
stack.hidden = active;
|
||||
// Reset link belongs to the cascade affordance set; hide while the
|
||||
// user is in search mode so the header reads as "← back to tree" only.
|
||||
if (header) header.classList.toggle("is-inline-search-active", active);
|
||||
if (trigger) trigger.setAttribute("aria-expanded", active ? "true" : "false");
|
||||
if (active) {
|
||||
if (input) {
|
||||
input.focus();
|
||||
if (input.value.trim() !== "") {
|
||||
scheduleInlineSearch(0);
|
||||
} else if (results) {
|
||||
// No query yet — clear results so a stale cascade leaf panel
|
||||
// doesn't show alongside the search input.
|
||||
results.innerHTML = "";
|
||||
results.classList.remove("is-loading", "is-no-hits");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On collapse, restore the cascade results for the current slug
|
||||
// so the user picks up exactly where they left off.
|
||||
runB1Search(cascadeEffectiveSlug || readB1PathFromURL());
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleInlineSearch(delayMs = 180) {
|
||||
if (inlineSearchDebounce !== undefined) window.clearTimeout(inlineSearchDebounce);
|
||||
inlineSearchDebounce = window.setTimeout(runInlineSearch, delayMs);
|
||||
}
|
||||
|
||||
async function runInlineSearch() {
|
||||
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
|
||||
const results = document.getElementById("fristen-b1-results");
|
||||
const clearBtn = document.getElementById("fristen-row-search-panel-clear") as HTMLButtonElement | null;
|
||||
if (!input || !results) return;
|
||||
const q = input.value.trim();
|
||||
if (clearBtn) clearBtn.hidden = q.length === 0;
|
||||
if (q === "") {
|
||||
results.innerHTML = "";
|
||||
results.classList.remove("is-loading", "is-no-hits");
|
||||
return;
|
||||
}
|
||||
results.classList.remove("is-no-hits");
|
||||
results.classList.add("is-loading");
|
||||
if (results.childElementCount === 0) {
|
||||
results.innerHTML = `<div class="fristen-search-status">${escHtml(t("deadlines.search.loading"))}</div>`;
|
||||
}
|
||||
const seq = ++inlineSearchSeq;
|
||||
try {
|
||||
const url = new URL("/api/tools/fristenrechner/search", window.location.origin);
|
||||
url.searchParams.set("q", q);
|
||||
url.searchParams.set("limit", "12");
|
||||
const forums = getActiveForumsParam();
|
||||
if (forums) url.searchParams.set("forum", forums);
|
||||
const r = await fetch(url.toString(), { credentials: "same-origin" });
|
||||
if (seq !== inlineSearchSeq) return;
|
||||
results.classList.remove("is-loading");
|
||||
if (!r.ok) {
|
||||
results.innerHTML = `<div class="fristen-search-status fristen-search-error">${escHtml(t("deadlines.search.no_hits"))}</div>`;
|
||||
return;
|
||||
}
|
||||
const data = (await r.json()) as SearchResponse;
|
||||
if (seq !== inlineSearchSeq) return;
|
||||
if (!data.cards || data.cards.length === 0) {
|
||||
results.innerHTML = `<div class="fristen-search-status">${escHtml(t("deadlines.search.no_hits"))}</div>`;
|
||||
results.classList.add("is-no-hits");
|
||||
return;
|
||||
}
|
||||
renderSearchResultsInto("fristen-b1-results", data);
|
||||
} catch {
|
||||
if (seq !== inlineSearchSeq) return;
|
||||
results.classList.remove("is-loading");
|
||||
results.innerHTML = `<div class="fristen-search-status fristen-search-error">${escHtml(t("deadlines.search.no_hits"))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initInlineSearch() {
|
||||
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
|
||||
const clearBtn = document.getElementById("fristen-row-search-panel-clear") as HTMLButtonElement | null;
|
||||
if (input) {
|
||||
input.addEventListener("input", () => scheduleInlineSearch());
|
||||
input.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape") {
|
||||
if (input.value.trim() !== "") {
|
||||
// First ESC: clear the input; second ESC: close the panel.
|
||||
input.value = "";
|
||||
scheduleInlineSearch(0);
|
||||
return;
|
||||
}
|
||||
setInlineSearchActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener("click", () => {
|
||||
if (!input) return;
|
||||
input.value = "";
|
||||
input.focus();
|
||||
scheduleInlineSearch(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// autoscrollToActiveRow brings the active cascade row into view when a
|
||||
// user pick on a higher row causes descendants to drop and the next
|
||||
// active row is now below the fold. The 60px headroom matches the
|
||||
// pattern used by the Akte picker (Step 1) and other paliad forms so
|
||||
// the row's chip body is visible without scrolling further. Desktop +
|
||||
// mobile share this behaviour; the visual change is only noticeable on
|
||||
// narrow viewports where the row stack scrolls.
|
||||
function autoscrollToActiveRow(stack: HTMLElement) {
|
||||
const active = stack.querySelector<HTMLElement>(".fristen-row.is-active");
|
||||
if (!active) return;
|
||||
// Skip if the active row is already fully visible (avoid jumpy
|
||||
// scroll-on-every-render on desktop).
|
||||
const rect = active.getBoundingClientRect();
|
||||
if (rect.top >= 60 && rect.bottom <= window.innerHeight - 16) return;
|
||||
const targetY = window.scrollY + rect.top - 60;
|
||||
window.scrollTo({ top: Math.max(0, targetY), behavior: "smooth" });
|
||||
}
|
||||
|
||||
// dismissAutoWalkTooltip removes the live tooltip element (if any) and
|
||||
// flips the localStorage suppression key so future renders skip it.
|
||||
// Called from any meaningful user interaction (chip pick, ändern,
|
||||
// dismiss-button) — once the user has touched the cascade they no
|
||||
// longer need the inference hint.
|
||||
function dismissAutoWalkTooltip() {
|
||||
document.querySelector(".fristen-row-autowalk-tip")?.remove();
|
||||
try { localStorage.setItem(cascadeTooltipDismissedKey, "1"); } catch { /* private mode */ }
|
||||
}
|
||||
|
||||
// maybeShowAutoWalkTooltip surfaces a one-time hint when ≥ 2 cascade
|
||||
// rows render in the prefilled (auto-walked) state. Per design §11.3
|
||||
// and Q11 (deferred to v2 nice-to-have), the tooltip only appears the
|
||||
@@ -3086,11 +3238,14 @@ function maybeShowAutoWalkTooltip(stack: HTMLElement, rows: RowSpec[]) {
|
||||
try { dismissed = localStorage.getItem(cascadeTooltipDismissedKey) === "1"; } catch { /* private mode */ }
|
||||
if (dismissed) return;
|
||||
// Inject the tooltip element. It's a sibling of the row stack, slotted
|
||||
// just above the first prefilled row via DOM insertion.
|
||||
// just above the first prefilled row via DOM insertion. The
|
||||
// `is-entering` class drives the fade-in + slide-down animation in
|
||||
// CSS (Slice 3 polish); we remove it on the next animation frame so
|
||||
// the CSS transition kicks in.
|
||||
const firstPrefilled = stack.querySelector(".fristen-row.is-prefilled");
|
||||
if (!firstPrefilled) return;
|
||||
const tip = document.createElement("div");
|
||||
tip.className = "fristen-row-autowalk-tip";
|
||||
tip.className = "fristen-row-autowalk-tip is-entering";
|
||||
tip.setAttribute("role", "status");
|
||||
tip.innerHTML = `
|
||||
<span class="fristen-row-autowalk-tip-icon" aria-hidden="true">ⓘ</span>
|
||||
@@ -3101,10 +3256,20 @@ function maybeShowAutoWalkTooltip(stack: HTMLElement, rows: RowSpec[]) {
|
||||
aria-label="${escAttr(t("deadlines.row.autowalk.dismiss"))}">
|
||||
×
|
||||
</button>`;
|
||||
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled);
|
||||
// Desktop: tip sits above the prefilled row so it reads as a header.
|
||||
// Mobile (<640px): tip sits below the row so the user's eye lands on
|
||||
// the row's "aus Akte" tag first and reads the explanation beneath
|
||||
// — matches design §7 + the natural reading pattern on small screens.
|
||||
const isMobile = window.matchMedia && window.matchMedia("(max-width: 640px)").matches;
|
||||
if (isMobile) {
|
||||
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled.nextSibling);
|
||||
} else {
|
||||
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled);
|
||||
}
|
||||
// Trigger the transition: remove the is-entering offset on next frame.
|
||||
window.requestAnimationFrame(() => tip.classList.remove("is-entering"));
|
||||
tip.querySelector<HTMLButtonElement>(".fristen-row-autowalk-tip-dismiss")?.addEventListener("click", () => {
|
||||
tip.remove();
|
||||
try { localStorage.setItem(cascadeTooltipDismissedKey, "1"); } catch { /* private mode */ }
|
||||
dismissAutoWalkTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -379,6 +379,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.row.search.link.title": "Direkt nach einer Frist suchen — überspringt den Entscheidungsbaum",
|
||||
"deadlines.row.autowalk.tooltip": "Diese Schritte ergeben sich aus Ihrer Akte. Klicken Sie „ändern\", um eine Antwort manuell anzupassen.",
|
||||
"deadlines.row.autowalk.dismiss": "Hinweis schließen",
|
||||
"deadlines.row.search.panel.back": "Zurück zum Entscheidungsbaum",
|
||||
"deadlines.row.search.panel.back.title": "Inline-Suche schließen und zum Entscheidungsbaum zurückkehren",
|
||||
"deadlines.row.search.panel.placeholder": "Frist suchen — z. B. „Klageschrift\", „Posteingang Hinweisbeschluss\"…",
|
||||
"deadlines.row.search.panel.clear": "Eingabe leeren",
|
||||
"deadlines.inbox.label": "Wo kam es an?",
|
||||
"deadlines.inbox.cms.title": "UPC — über CMS",
|
||||
"deadlines.inbox.bea.title": "Nationale Verfahren — über beA",
|
||||
@@ -2943,6 +2947,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.row.search.link.title": "Search directly for a deadline — skips the decision tree",
|
||||
"deadlines.row.autowalk.tooltip": "These steps were derived from your matter. Click \"edit\" to override any answer manually.",
|
||||
"deadlines.row.autowalk.dismiss": "Dismiss hint",
|
||||
"deadlines.row.search.panel.back": "Back to decision tree",
|
||||
"deadlines.row.search.panel.back.title": "Close inline search and return to the decision tree",
|
||||
"deadlines.row.search.panel.placeholder": "Search for a deadline — e.g. \"statement of claim\", \"hint order\"…",
|
||||
"deadlines.row.search.panel.clear": "Clear input",
|
||||
"deadlines.inbox.label": "Where did it arrive?",
|
||||
"deadlines.inbox.cms.title": "UPC — via CMS",
|
||||
"deadlines.inbox.bea.title": "National-DE — via beA",
|
||||
|
||||
@@ -234,18 +234,24 @@ export function renderFristenrechner(): string {
|
||||
<span data-i18n="deadlines.pathway.b.title">Frist eintragen aufgrund Ereignis</span>
|
||||
</h2>
|
||||
|
||||
{/* B1 panel — row-stack cascade (t-paliad-180 Slice 1).
|
||||
Mode / perspective / inbox / cascade-steps all render as
|
||||
`.fristen-row` instances inside `#fristen-row-stack`. The
|
||||
stack-header above hosts the "Direkt suchen" escape hatch
|
||||
(Pathway B → ?mode=filter) and the "Pfad zurücksetzen"
|
||||
link, both of which used to be embedded in the cascade's
|
||||
breadcrumb. `#fristen-b1-results` is unchanged — concept
|
||||
cards still render below as the cascade narrows. */}
|
||||
{/* B1 panel — row-stack cascade.
|
||||
`#fristen-row-stack` hosts the perspective / inbox /
|
||||
cascade rows (t-paliad-180 Slice 1; t-paliad-197 Slice 2
|
||||
added project-driven prefills + auto-walk). The
|
||||
stack-header above carries the inline-search trigger
|
||||
(t-paliad-198 Slice 3 — clicking expands
|
||||
`#fristen-row-search-panel` over the row stack instead
|
||||
of routing to the legacy B2 surface) and the reset link.
|
||||
`#fristen-b1-results` is unchanged — it renders concept
|
||||
cards for both cascade-narrowing AND inline-search
|
||||
results, so users see the same card layout regardless
|
||||
of how they reached a deadline rule. */}
|
||||
<div className="fristen-b1-panel" id="fristen-b1-panel" data-mode="tree" hidden>
|
||||
<div className="fristen-row-stack-header">
|
||||
<div className="fristen-row-stack-header" id="fristen-row-stack-header">
|
||||
<button type="button" className="fristen-row-search-link" id="fristen-row-search-link"
|
||||
data-i18n-title="deadlines.row.search.link.title"
|
||||
aria-expanded="false"
|
||||
aria-controls="fristen-row-search-panel"
|
||||
title="Direkt nach einer Frist suchen">
|
||||
<span aria-hidden="true">🔍</span>{" "}
|
||||
<span data-i18n="deadlines.row.search.link">Direkt suchen</span>
|
||||
@@ -257,6 +263,44 @@ export function renderFristenrechner(): string {
|
||||
<span data-i18n="deadlines.row.reset">Pfad zurücksetzen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Inline search overlay (t-paliad-198 Slice 3). Hidden by
|
||||
default; the search icon-button in the stack header
|
||||
toggles it open / closed. While open, the row stack is
|
||||
hidden and the search input drives `#fristen-b1-results`
|
||||
directly — same surface the cascade leaf populates so
|
||||
the user sees one consistent concept-card list. */}
|
||||
<div className="fristen-row-search-panel" id="fristen-row-search-panel" hidden role="search">
|
||||
<button type="button" className="fristen-row-search-panel-back" id="fristen-row-search-panel-back"
|
||||
data-i18n-title="deadlines.row.search.panel.back.title"
|
||||
title="Zurück zum Entscheidungsbaum">
|
||||
<span aria-hidden="true">←</span>{" "}
|
||||
<span data-i18n="deadlines.row.search.panel.back">Zurück zum Entscheidungsbaum</span>
|
||||
</button>
|
||||
<div className="fristen-row-search-panel-input-wrap">
|
||||
<svg className="fristen-row-search-panel-icon" width="18" height="18" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
id="fristen-row-search-panel-input"
|
||||
className="fristen-row-search-panel-input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-i18n-placeholder="deadlines.row.search.panel.placeholder"
|
||||
placeholder="Frist suchen…"
|
||||
aria-label="Frist suchen"
|
||||
/>
|
||||
<button type="button" className="fristen-row-search-panel-clear" id="fristen-row-search-panel-clear"
|
||||
data-i18n-title="deadlines.row.search.panel.clear" title="Eingabe leeren" hidden>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fristen-row-stack" id="fristen-row-stack" aria-live="polite"></div>
|
||||
<div className="fristen-b1-results" id="fristen-b1-results" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
@@ -1099,6 +1099,10 @@ export type I18nKey =
|
||||
| "deadlines.row.reset.title"
|
||||
| "deadlines.row.search.link"
|
||||
| "deadlines.row.search.link.title"
|
||||
| "deadlines.row.search.panel.back"
|
||||
| "deadlines.row.search.panel.back.title"
|
||||
| "deadlines.row.search.panel.clear"
|
||||
| "deadlines.row.search.panel.placeholder"
|
||||
| "deadlines.save.cta"
|
||||
| "deadlines.save.cta.adhoc.hint"
|
||||
| "deadlines.save.error"
|
||||
|
||||
@@ -1733,6 +1733,109 @@ input[type="range"]::-moz-range-thumb {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* When inline search is active, the stack-header's reset link is the
|
||||
* only chrome that doesn't make sense — the search panel has its own
|
||||
* "← back to decision tree" button at the top. */
|
||||
.fristen-row-stack-header.is-inline-search-active .fristen-row-search-link,
|
||||
.fristen-row-stack-header.is-inline-search-active .fristen-row-reset-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* t-paliad-198 Slice 3 — inline search overlay. Expands above the row
|
||||
* stack when the `🔍 Direkt suchen` button is clicked. The row stack
|
||||
* itself is hidden via the JS toggle on `#fristen-row-stack`; the
|
||||
* search input drives the same `#fristen-b1-results` container the
|
||||
* cascade narrows into. */
|
||||
.fristen-row-search-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 0.85rem 1rem 1rem;
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-accent) 4%, var(--color-bg));
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
align-self: flex-start;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-back:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-back:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-input-wrap:focus-within {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-input {
|
||||
flex: 1 1 auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
padding: 0.4rem 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-clear {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-clear:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
.fristen-row-search-panel-clear:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.fristen-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1880,6 +1983,34 @@ input[type="range"]::-moz-range-thumb {
|
||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text);
|
||||
/* t-paliad-198 Slice 3: fade-in + slight slide-down on first paint.
|
||||
* The is-entering class is applied at injection and removed on the
|
||||
* next animation frame so the transition fires from the initial
|
||||
* offset state to the resting state. ~200ms keeps it subtle — the
|
||||
* user notices the hint without it feeling theatrical. */
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.fristen-row-autowalk-tip.is-entering {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* Mobile (<640px): the tooltip sits below the prefilled row instead of
|
||||
* above it, so the user's eye lands on the row's "aus Akte" tag first
|
||||
* and reads the explanation beneath. The transform offset flips
|
||||
* accordingly. */
|
||||
@media (max-width: 640px) {
|
||||
.fristen-row.is-prefilled + .fristen-row-autowalk-tip,
|
||||
.fristen-row-autowalk-tip {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.fristen-row-autowalk-tip.is-entering {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.fristen-row-autowalk-tip-icon {
|
||||
@@ -1998,6 +2129,50 @@ input[type="range"]::-moz-range-thumb {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* t-paliad-198 Slice 3 — responsive row-stack breakpoints (design §7).
|
||||
*
|
||||
* Default (above): desktop layout — head + chip body share the row, chips
|
||||
* wrap into a 3-column grid via auto-fill 220px.
|
||||
*
|
||||
* 768–1023px (tablet density): same head layout but chip grid drops to
|
||||
* 2 columns (auto-fill 200px) so each chip stays comfortably tappable.
|
||||
*
|
||||
* 640–767px (small tablet / large phone): head wraps so the ändern
|
||||
* affordance moves to its own line; chip grid is single-column to keep
|
||||
* the chip body legible.
|
||||
*
|
||||
* <640px (phone): chips full-width single-column; ändern stays
|
||||
* permanently visible (no hover state to reveal it on touch); the row
|
||||
* num badge tucks into the same line as the label to save vertical
|
||||
* space.
|
||||
*/
|
||||
@media (max-width: 1023px) {
|
||||
.fristen-row-body {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.fristen-row {
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
.fristen-row.is-active {
|
||||
padding: 0.85rem 0.85rem;
|
||||
}
|
||||
.fristen-row-head {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.fristen-row-body {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.4rem;
|
||||
margin-left: 1.9rem;
|
||||
}
|
||||
.fristen-row-edit {
|
||||
opacity: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.fristen-row-body {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -2005,10 +2180,18 @@ input[type="range"]::-moz-range-thumb {
|
||||
}
|
||||
.fristen-row-head {
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fristen-row-answer {
|
||||
flex-basis: 100%;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.fristen-row-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
.fristen-row-chip {
|
||||
padding: 0.75rem 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fristen-b1-loosen-link {
|
||||
|
||||
Reference in New Issue
Block a user