Compare commits
4 Commits
mai/knuth/
...
mai/icarus
| Author | SHA1 | Date | |
|---|---|---|---|
| a6d0acbcb4 | |||
| 930771a898 | |||
| 7368e7012b | |||
| d4df81e374 |
@@ -207,6 +207,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
|
||||
"deadlines.step1": "Verfahrensart w\u00e4hlen",
|
||||
"deadlines.step2": "Ausgangsdatum eingeben",
|
||||
"deadlines.step2.perspective": "Perspektive und Datum",
|
||||
"deadlines.step3": "Ergebnis",
|
||||
"deadlines.upc": "UPC",
|
||||
"deadlines.de": "Deutsche Gerichte",
|
||||
@@ -421,6 +422,8 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.side.claimant": "Klägerseite",
|
||||
"deadlines.side.defendant": "Beklagtenseite",
|
||||
"deadlines.side.both": "Beide",
|
||||
"deadlines.side.from_project": "Aus Akte:",
|
||||
"deadlines.side.override": "Andere Seite wählen",
|
||||
"deadlines.appellant.label": "Berufung durch:",
|
||||
"deadlines.appellant.claimant": "Klägerseite",
|
||||
"deadlines.appellant.defendant": "Beklagtenseite",
|
||||
@@ -3274,6 +3277,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
|
||||
"deadlines.step1": "Select Proceeding Type",
|
||||
"deadlines.step2": "Enter Trigger Date",
|
||||
"deadlines.step2.perspective": "Perspective and Date",
|
||||
"deadlines.step3": "Result",
|
||||
"deadlines.upc": "UPC",
|
||||
"deadlines.de": "German Courts",
|
||||
@@ -3495,6 +3499,8 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.side.claimant": "Claimant",
|
||||
"deadlines.side.defendant": "Defendant",
|
||||
"deadlines.side.both": "Both",
|
||||
"deadlines.side.from_project": "From case:",
|
||||
"deadlines.side.override": "Choose other side",
|
||||
"deadlines.appellant.label": "Appeal filed by:",
|
||||
"deadlines.appellant.claimant": "Claimant",
|
||||
"deadlines.appellant.defendant": "Defendant",
|
||||
|
||||
@@ -610,10 +610,25 @@ function paintVariables(): void {
|
||||
|
||||
host.querySelectorAll<HTMLInputElement>(".submission-draft-var-input").forEach((inp) => {
|
||||
inp.addEventListener("input", () => onVarChange(inp));
|
||||
// t-paliad-274 (B) — focus into a sidebar field highlights every
|
||||
// matching .draft-var span in the preview (sticky while focused,
|
||||
// clears on blur). Survives autosave repaints because paintVariables
|
||||
// is called by flushAutosave and we re-bind every render.
|
||||
inp.addEventListener("focusin", () => onVarFocusEnter(inp.dataset.var ?? ""));
|
||||
inp.addEventListener("focusout", () => onVarFocusLeave(inp.dataset.var ?? ""));
|
||||
});
|
||||
host.querySelectorAll<HTMLButtonElement>(".submission-draft-var-reset").forEach((btn) => {
|
||||
btn.addEventListener("click", () => onVarReset(btn.dataset.resetKey ?? ""));
|
||||
});
|
||||
// After repaint, re-apply the active highlight if a field is still
|
||||
// focused (paintVariables runs after autosave; the same input regains
|
||||
// focus via restoreVarFocus and would otherwise emit focusin too
|
||||
// late for our handler — re-apply explicitly).
|
||||
const active = document.activeElement;
|
||||
if (isVarField(active)) {
|
||||
const key = active.dataset.var;
|
||||
if (key) applyPreviewActiveHighlight(key);
|
||||
}
|
||||
}
|
||||
|
||||
function paintPreview(): void {
|
||||
@@ -621,6 +636,16 @@ function paintPreview(): void {
|
||||
if (!host || !state.view) return;
|
||||
host.innerHTML = state.view.preview_html ?? "";
|
||||
wireDraftVars(host);
|
||||
// t-paliad-274 (B) — preview HTML was just blown away by innerHTML,
|
||||
// so any prior --active classes are gone. Re-apply for whichever
|
||||
// sidebar field is currently focused (typing in a field triggers an
|
||||
// autosave round-trip that ends in paintPreview, and the user should
|
||||
// see the highlight stay put across that cycle).
|
||||
const active = document.activeElement;
|
||||
if (isVarField(active)) {
|
||||
const key = active.dataset.var;
|
||||
if (key) applyPreviewActiveHighlight(key);
|
||||
}
|
||||
}
|
||||
|
||||
// t-paliad-261 (B) — click a substituted variable in the preview to
|
||||
@@ -695,6 +720,48 @@ function onDraftVarClick(key: string, ev: Event): void {
|
||||
flashVarRow(input);
|
||||
}
|
||||
|
||||
// t-paliad-274 (B) — sidebar-field-focus → preview-occurrence highlight.
|
||||
// Reverse direction of the click-to-jump from #92: when the user focuses
|
||||
// any .submission-draft-var-input, every matching .draft-var span in the
|
||||
// preview gets the --active modifier; on blur (or focus shift to a
|
||||
// different field), the previous key's highlights clear and the new
|
||||
// key's apply. Sticky-while-focused, not a one-shot flash — the lawyer
|
||||
// can scan the preview for "where does this variable land in my prose?"
|
||||
// while the field stays focused.
|
||||
function onVarFocusEnter(key: string): void {
|
||||
if (!key) return;
|
||||
// Clear any leftover highlight before applying the new one — covers
|
||||
// the focus-shift-without-blur case (Tab between fields).
|
||||
clearPreviewActiveHighlight();
|
||||
applyPreviewActiveHighlight(key);
|
||||
}
|
||||
|
||||
function onVarFocusLeave(_key: string): void {
|
||||
// We don't need the key here — if focus moves to a different sidebar
|
||||
// input, that input's focusin will re-call apply with the new key
|
||||
// (after our clearPreviewActiveHighlight). If focus leaves the sidebar
|
||||
// entirely, this clears.
|
||||
clearPreviewActiveHighlight();
|
||||
}
|
||||
|
||||
function applyPreviewActiveHighlight(key: string): void {
|
||||
const host = document.getElementById("submission-draft-preview");
|
||||
if (!host) return;
|
||||
host.querySelectorAll<HTMLElement>(
|
||||
`.draft-var[data-var="${cssEscape(key)}"]`,
|
||||
).forEach((el) => {
|
||||
el.classList.add("draft-var--active");
|
||||
});
|
||||
}
|
||||
|
||||
function clearPreviewActiveHighlight(): void {
|
||||
const host = document.getElementById("submission-draft-preview");
|
||||
if (!host) return;
|
||||
host.querySelectorAll<HTMLElement>(".draft-var--active").forEach((el) => {
|
||||
el.classList.remove("draft-var--active");
|
||||
});
|
||||
}
|
||||
|
||||
function flashVarRow(input: HTMLElement): void {
|
||||
const row = input.closest<HTMLElement>(".submission-draft-var-row");
|
||||
if (!row) return;
|
||||
|
||||
@@ -38,6 +38,13 @@ let lastResponse: DeadlineResponse | null = null;
|
||||
let currentSide: Side = null;
|
||||
let currentAppellant: Side = null;
|
||||
|
||||
// Project-driven auto-fill state (t-paliad-279 / m/paliad#111). When the
|
||||
// page is opened with ?project=<id> and that project has our_side set,
|
||||
// the side row renders as a read-only chip instead of the radio cluster.
|
||||
// The user can flip to free-pick via the "Andere Seite wählen" override
|
||||
// link, which clears this flag (radio cluster takes over again).
|
||||
let sidePrefilledFromProject = false;
|
||||
|
||||
// Proceedings where one party initiates and "both" rows are role-swap
|
||||
// (i.e. either party files depending on who acted at the lower
|
||||
// instance). For these proceedings the appellant selector is meaningful
|
||||
@@ -388,6 +395,125 @@ function syncRadioGroup(name: string, value: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// Project context (t-paliad-279 / m/paliad#111). When the page is opened
|
||||
// with ?project=<id> and the project carries an our_side value, the side
|
||||
// row renders as a read-only chip with an "Andere Seite wählen" override
|
||||
// link. The proceeding picker + appellant axis stay untouched — only the
|
||||
// side selector pre-fills.
|
||||
interface ProjectOurSide {
|
||||
id: string;
|
||||
our_side?:
|
||||
| "claimant"
|
||||
| "defendant"
|
||||
| "applicant"
|
||||
| "appellant"
|
||||
| "respondent"
|
||||
| "third_party"
|
||||
| "other"
|
||||
| null;
|
||||
}
|
||||
|
||||
function readProjectFromURL(): string {
|
||||
return new URLSearchParams(window.location.search).get("project") || "";
|
||||
}
|
||||
|
||||
// ourSideToSide maps the project-level our_side enum (t-paliad-222) onto
|
||||
// the side-selector's two-value axis. Active roles (claimant / applicant /
|
||||
// appellant) collapse to "claimant"; reactive roles (defendant /
|
||||
// respondent) collapse to "defendant"; everything else (third_party /
|
||||
// other / NULL) returns null = no pre-fill. Mirrors fristenrechner.ts
|
||||
// ourSideToPerspective() so projects render consistently across both
|
||||
// surfaces.
|
||||
function ourSideToSide(os: ProjectOurSide["our_side"] | undefined): Side {
|
||||
switch (os) {
|
||||
case "claimant":
|
||||
case "applicant":
|
||||
case "appellant":
|
||||
return "claimant";
|
||||
case "defendant":
|
||||
case "respondent":
|
||||
return "defendant";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProjectOurSide(projectID: string): Promise<ProjectOurSide | null> {
|
||||
try {
|
||||
const resp = await fetch(`/api/projects/${encodeURIComponent(projectID)}`, {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
return (await resp.json()) as ProjectOurSide;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sideLabelI18n(s: Side): string {
|
||||
if (s === "claimant") return t("deadlines.side.claimant");
|
||||
if (s === "defendant") return t("deadlines.side.defendant");
|
||||
return t("deadlines.side.both");
|
||||
}
|
||||
|
||||
// renderSideChip swaps the radio cluster for a read-only chip showing
|
||||
// the auto-filled side + an "Andere Seite wählen" override link. Called
|
||||
// after fetchProjectOurSide resolves to a side. The override link clears
|
||||
// the prefilled flag and swaps back to the radio cluster — the user can
|
||||
// then pick any side freely.
|
||||
function renderSideChip(side: Side) {
|
||||
const cluster = document.getElementById("side-radio-cluster");
|
||||
const chip = document.getElementById("side-chip");
|
||||
const value = document.getElementById("side-chip-value");
|
||||
if (!cluster || !chip || !value) return;
|
||||
cluster.style.display = "none";
|
||||
chip.style.display = "";
|
||||
value.textContent = sideLabelI18n(side);
|
||||
}
|
||||
|
||||
function showSideRadioCluster() {
|
||||
const cluster = document.getElementById("side-radio-cluster");
|
||||
const chip = document.getElementById("side-chip");
|
||||
if (!cluster || !chip) return;
|
||||
cluster.style.display = "";
|
||||
chip.style.display = "none";
|
||||
}
|
||||
|
||||
// applySidePrefill takes a project's our_side, maps it to the side axis,
|
||||
// and locks the side row to a read-only chip if a mapping exists. URL
|
||||
// wins — if ?side= is already explicit, the user (or shared link) has
|
||||
// already chosen and we never overwrite. When we do prefill, write the
|
||||
// derived side to the URL so reload + back/forward round-trip cleanly.
|
||||
function applySidePrefill(os: ProjectOurSide["our_side"] | undefined) {
|
||||
if (readSideFromURL() !== null) return;
|
||||
const next = ourSideToSide(os);
|
||||
if (next === null) return;
|
||||
currentSide = next;
|
||||
writeSideToURL(next);
|
||||
syncRadioGroup("side", next);
|
||||
sidePrefilledFromProject = true;
|
||||
renderSideChip(next);
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
}
|
||||
|
||||
function clearSidePrefill() {
|
||||
sidePrefilledFromProject = false;
|
||||
showSideRadioCluster();
|
||||
// Drop ?project= from the URL so a reload doesn't re-lock the side.
|
||||
// ?side= stays — that's the user's last pick at this point.
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("project");
|
||||
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
||||
}
|
||||
|
||||
async function initProjectAutofill() {
|
||||
const projectID = readProjectFromURL();
|
||||
if (!projectID) return;
|
||||
const project = await fetchProjectOurSide(projectID);
|
||||
if (!project) return;
|
||||
applySidePrefill(project.our_side);
|
||||
}
|
||||
|
||||
function applyVerfahrensablaufViewBodyClass(view: ProcedureView) {
|
||||
// Mirrors the events.ts pattern (body.events-view-*). The print
|
||||
// stylesheet keys `body.verfahrensablauf-view-timeline` to
|
||||
@@ -529,6 +655,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initViewToggle();
|
||||
initPerspectiveControls();
|
||||
|
||||
// Override link on the prefilled side chip — swaps back to the radio
|
||||
// cluster and clears ?project= from the URL.
|
||||
document.getElementById("side-chip-override")?.addEventListener("click", clearSidePrefill);
|
||||
|
||||
// Project autofill — runs after the radio cluster has its URL-driven
|
||||
// state so we never clobber an explicit ?side= pick. Fire-and-forget;
|
||||
// the chip swap happens once the project resolves.
|
||||
void initProjectAutofill();
|
||||
|
||||
onLangChange(() => {
|
||||
// Active-button name updates with language change (the data-i18n
|
||||
// pass swaps the inner <strong>'s text). Re-collapse the summary
|
||||
@@ -539,6 +674,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const summary = document.getElementById("proceeding-summary-name");
|
||||
if (summary) summary.textContent = proceedingDisplayName(activeBtn);
|
||||
}
|
||||
// Side-chip label tracks language so a DE/EN flip while the chip is
|
||||
// visible re-renders the inferred side in the active language.
|
||||
if (sidePrefilledFromProject) {
|
||||
const value = document.getElementById("side-chip-value");
|
||||
if (value) value.textContent = sideLabelI18n(currentSide);
|
||||
}
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
syncTriggerEventLabel();
|
||||
});
|
||||
|
||||
@@ -1446,7 +1446,9 @@ export type I18nKey =
|
||||
| "deadlines.side.both"
|
||||
| "deadlines.side.claimant"
|
||||
| "deadlines.side.defendant"
|
||||
| "deadlines.side.from_project"
|
||||
| "deadlines.side.label"
|
||||
| "deadlines.side.override"
|
||||
| "deadlines.source.caldav"
|
||||
| "deadlines.source.fristenrechner"
|
||||
| "deadlines.source.imported"
|
||||
@@ -1477,6 +1479,7 @@ export type I18nKey =
|
||||
| "deadlines.step2.happened.desc"
|
||||
| "deadlines.step2.happened.title"
|
||||
| "deadlines.step2.heading"
|
||||
| "deadlines.step2.perspective"
|
||||
| "deadlines.step3"
|
||||
| "deadlines.step3a.back"
|
||||
| "deadlines.step3a.draft.desc"
|
||||
|
||||
@@ -3572,6 +3572,59 @@ input[type="range"]::-moz-range-thumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Visual divider between the perspective block (side + appellant)
|
||||
and the date / court / flag knobs below. t-paliad-279 reorder put
|
||||
the most-defining inputs (side, appellant) at the top of step-2; the
|
||||
divider keeps the date block from reading as a continuation of the
|
||||
perspective rows. */
|
||||
.verfahrensablauf-step2-divider {
|
||||
height: 1px;
|
||||
margin: 1rem 0;
|
||||
background: var(--color-border, #e5e5e5);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Read-only auto-fill chip for #side-row. Renders when ?project=<id>
|
||||
resolves a project whose our_side is set: shows the inferred side
|
||||
with a small "Andere Seite wählen" override link that swaps the row
|
||||
back to the radio cluster. t-paliad-279 / m/paliad#111. */
|
||||
.side-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid var(--color-border, #e5e5e5);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-bg-subtle, #fafafa);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.side-chip-tag {
|
||||
color: var(--color-text-muted, #666);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.side-chip-value {
|
||||
color: var(--color-text, #222);
|
||||
}
|
||||
.side-chip-override {
|
||||
margin-left: 0.3rem;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 9999px;
|
||||
background: var(--color-bg, #fff);
|
||||
color: var(--color-text-muted, #555);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 120ms, border-color 120ms;
|
||||
}
|
||||
.side-chip-override:hover {
|
||||
background: var(--color-bg-subtle, #f4f4f4);
|
||||
border-color: var(--color-text-muted, #aaa);
|
||||
}
|
||||
.side-chip-override:focus-visible {
|
||||
outline: 2px solid var(--color-accent, #c6f41c);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Compact note hint — sits in the timeline-meta line when the notes
|
||||
toggle is off. Native browser tooltip via title= attribute carries
|
||||
the full text on hover; tabindex=0 + aria-label make it
|
||||
@@ -5880,16 +5933,27 @@ dialog.modal::backdrop {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* t-paliad-261 (B) — substituted variables in the preview are wrapped
|
||||
in <span class="draft-var" data-var="…"> by the Go HTML renderer.
|
||||
.draft-var by itself shows a subtle dotted underline so the lawyer
|
||||
can SEE which text was filled in from a variable. .draft-var--has-input
|
||||
(added client-side when a matching sidebar input exists) layers on
|
||||
the clickable affordance — pointer cursor + brighter hover background.
|
||||
Non-matching draft-vars (derived variables not exposed in the
|
||||
sidebar) stay visually distinct but non-interactive. */
|
||||
/* t-paliad-261 / t-paliad-274 — substituted variables in the preview
|
||||
are wrapped in <span class="draft-var" data-var="…"> by the Go HTML
|
||||
renderer for BOTH filled values and missing-marker text. The lawyer
|
||||
can click any wrapped span and jump to the matching sidebar input;
|
||||
conversely, focusing a sidebar input lights up every matching span in
|
||||
the preview via .draft-var--active.
|
||||
|
||||
Visual contract:
|
||||
.draft-var — invisible by default (prose stays clean
|
||||
per t-paliad-274 m's request).
|
||||
.draft-var--has-input — pointer cursor; dotted underline on
|
||||
hover so the click affordance reveals
|
||||
itself, plus a brighter lime tint.
|
||||
.draft-var--active — sticky lime highlight applied while the
|
||||
matching sidebar input is focused
|
||||
(t-paliad-274 reverse direction).
|
||||
[KEIN WERT: …] / [NO VALUE: …] markers carry their own warning
|
||||
style via .submission-draft-var-marker on the sidebar hint; in the
|
||||
preview they read as obvious gap text, so .draft-var itself doesn't
|
||||
need an always-on visual to flag them. */
|
||||
.draft-var {
|
||||
background-color: rgba(198, 244, 28, 0.12);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
box-decoration-break: clone;
|
||||
@@ -5904,9 +5968,21 @@ dialog.modal::backdrop {
|
||||
.draft-var--has-input:hover,
|
||||
.draft-var--has-input:focus-visible {
|
||||
background-color: rgba(198, 244, 28, 0.45);
|
||||
text-decoration: underline dotted rgba(198, 244, 28, 0.85);
|
||||
text-underline-offset: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* t-paliad-274 (B) — sticky highlight while the matching sidebar input
|
||||
is focused. Brighter than the hover tint so the user's eye lands on
|
||||
every occurrence at once when they click into a field. Applies to ALL
|
||||
.draft-var spans for that data-var, not just one. */
|
||||
.draft-var--active,
|
||||
.draft-var--has-input.draft-var--active {
|
||||
background-color: rgba(198, 244, 28, 0.55);
|
||||
box-shadow: 0 0 0 1px rgba(198, 244, 28, 0.85);
|
||||
}
|
||||
|
||||
/* t-paliad-261 (B) — brief lime flash on the sidebar row after a
|
||||
click-jump from the preview, so the user's eye lands on the right
|
||||
input even after the smooth-scroll motion. Animation restarts on
|
||||
|
||||
@@ -158,9 +158,79 @@ export function renderVerfahrensablauf(): string {
|
||||
<div className="wizard-step" id="step-2" style="display:none">
|
||||
<h3 className="wizard-step-label">
|
||||
<span className="step-number">2</span>
|
||||
<span data-i18n="deadlines.step2">Ausgangsdatum eingeben</span>
|
||||
<span data-i18n="deadlines.step2.perspective">Perspektive und Datum</span>
|
||||
</h3>
|
||||
|
||||
{/* Perspective strip (t-paliad-250 / m/paliad#81, reordered
|
||||
in t-paliad-279 / m/paliad#111). Side defines whose
|
||||
perspective the columns project; appellant collapses
|
||||
party=both rows for role-swap proceedings (Appeal etc.).
|
||||
Moved above .date-input-group because party-side is the
|
||||
most-defining input after proceeding-type — without
|
||||
side, the column labels can't pick "your filings". Both
|
||||
selectors are URL-driven (?side= + ?appellant=) so the
|
||||
perspective survives reload and is shareable.
|
||||
|
||||
When the page is opened with ?project=<id> and that
|
||||
project's our_side is set, side-row renders as a
|
||||
read-only chip with an "Andere Seite wählen" override
|
||||
link — see client/verfahrensablauf.ts. */}
|
||||
<div className="verfahrensablauf-perspective" id="verfahrensablauf-perspective">
|
||||
<div className="verfahrensablauf-perspective-row" id="side-row">
|
||||
<span className="date-label" data-i18n="deadlines.side.label">Seite:</span>
|
||||
<div className="side-radio-cluster" id="side-radio-cluster">
|
||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Side">
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="claimant" />
|
||||
<span data-i18n="deadlines.side.claimant">Klägerseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="defendant" />
|
||||
<span data-i18n="deadlines.side.defendant">Beklagtenseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="" checked />
|
||||
<span data-i18n="deadlines.side.both">Beide</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/* Auto-fill chip — populated by the client when a
|
||||
?project=<id> URL resolves a project with our_side
|
||||
set. Hidden by default; the radio cluster above is
|
||||
hidden whenever this chip is shown. */}
|
||||
<div className="side-chip" id="side-chip" style="display:none">
|
||||
<span className="side-chip-tag" data-i18n="deadlines.side.from_project">Aus Akte:</span>
|
||||
<strong className="side-chip-value" id="side-chip-value">—</strong>
|
||||
<button type="button" className="side-chip-override" id="side-chip-override"
|
||||
data-i18n="deadlines.side.override">
|
||||
Andere Seite wählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="verfahrensablauf-perspective-row" id="appellant-row" style="display:none">
|
||||
<span className="date-label" data-i18n="deadlines.appellant.label">Berufung durch:</span>
|
||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appellant">
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="claimant" />
|
||||
<span data-i18n="deadlines.appellant.claimant">Klägerseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="defendant" />
|
||||
<span data-i18n="deadlines.appellant.defendant">Beklagtenseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="" checked />
|
||||
<span data-i18n="deadlines.appellant.none">—</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual divider — keeps the perspective block (most-
|
||||
defining inputs after proceeding-type) optically
|
||||
separate from the date / court / flag knobs below. */}
|
||||
<div className="verfahrensablauf-step2-divider" aria-hidden="true"></div>
|
||||
|
||||
<div className="date-input-group">
|
||||
<div className="date-field-row">
|
||||
{/* Read-only caption labelling the value <span>. Not a
|
||||
@@ -210,53 +280,6 @@ export function renderVerfahrensablauf(): string {
|
||||
Fristen berechnen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Perspective strip (t-paliad-250 / m/paliad#81). Side
|
||||
swaps the column LABELS so the user's own side is
|
||||
proactive (= "your filings"). Appellant collapses
|
||||
party=both rows to a single column when set — only
|
||||
relevant for role-swap proceedings (Appeal etc.);
|
||||
the row hides itself when the picked proceeding has
|
||||
no appellant axis (see hasAppellantAxis() in the
|
||||
client). Both selectors are URL-driven (?side= +
|
||||
?appellant=) so the perspective survives reload
|
||||
and is shareable. */}
|
||||
<div className="verfahrensablauf-perspective" id="verfahrensablauf-perspective">
|
||||
<div className="verfahrensablauf-perspective-row" id="side-row">
|
||||
<span className="date-label" data-i18n="deadlines.side.label">Seite:</span>
|
||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Side">
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="claimant" />
|
||||
<span data-i18n="deadlines.side.claimant">Klägerseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="defendant" />
|
||||
<span data-i18n="deadlines.side.defendant">Beklagtenseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="side" value="" checked />
|
||||
<span data-i18n="deadlines.side.both">Beide</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="verfahrensablauf-perspective-row" id="appellant-row" style="display:none">
|
||||
<span className="date-label" data-i18n="deadlines.appellant.label">Berufung durch:</span>
|
||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appellant">
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="claimant" />
|
||||
<span data-i18n="deadlines.appellant.claimant">Klägerseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="defendant" />
|
||||
<span data-i18n="deadlines.appellant.defendant">Beklagtenseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="" checked />
|
||||
<span data-i18n="deadlines.appellant.none">—</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wizard-step" id="step-3" style="display:none">
|
||||
|
||||
@@ -327,6 +327,40 @@ func TestRenderHTML_WrapsMissingMarker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderHTML_WrapsOverriddenValueSameAsResolved is the t-paliad-274
|
||||
// regression: m's report on m/paliad#106 was that "When filled, the link
|
||||
// disappears". The preview HTML must wrap an override value with the
|
||||
// same <span class="draft-var"> as it would an unfilled placeholder, so
|
||||
// the click-jump from preview→sidebar persists after the user types a
|
||||
// value. There is no distinction at the renderer level between a value
|
||||
// that came from the resolved bag (project / parties / deadline lookups)
|
||||
// and a value the lawyer typed into the sidebar — both arrive in the
|
||||
// same PlaceholderMap and both must be wrapped.
|
||||
func TestRenderHTML_WrapsOverriddenValueSameAsResolved(t *testing.T) {
|
||||
doc := `<w:document><w:body>` +
|
||||
`<w:p><w:r><w:t>{{project.case_number}} / {{firm.name}}</w:t></w:r></w:p>` +
|
||||
`</w:body></w:document>`
|
||||
tmpl := minimalMergeDOCX(t, doc)
|
||||
r := NewSubmissionRenderer()
|
||||
// project.case_number is the typed-by-lawyer override.
|
||||
// firm.name is the always-resolved value from the firm bag.
|
||||
html, err := r.RenderHTML(tmpl, PlaceholderMap{
|
||||
"project.case_number": "UPC_CFI_42/2026",
|
||||
"firm.name": "HLC",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("render html: %v", err)
|
||||
}
|
||||
wantOverride := `<span class="draft-var" data-var="project.case_number">UPC_CFI_42/2026</span>`
|
||||
if !strings.Contains(html, wantOverride) {
|
||||
t.Errorf("expected overridden value wrapped in draft-var span (click-jump must persist after fill, t-paliad-274), got %q", html)
|
||||
}
|
||||
wantResolved := `<span class="draft-var" data-var="firm.name">HLC</span>`
|
||||
if !strings.Contains(html, wantResolved) {
|
||||
t.Errorf("expected resolved value still wrapped, got %q", html)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRender_DocxOutputUnchangedByPreviewWrap asserts the hard rule from
|
||||
// t-paliad-261: the .docx export path must NOT carry the preview-only
|
||||
// draft-var sentinels or any draft-var span markup. Renders the same
|
||||
|
||||
Reference in New Issue
Block a user