feat(t-paliad-171): SmartTimeline render shape — shape-timeline.ts + CSS + i18n keys
The vertical-timeline render component for the SmartTimeline (Verlauf tab redesign). Two-column layout (date / event card), past chronological → "Heute →" rule → future chronological, status icon + kind chip per row. Deep-link is wired via a row-level click handler that skips clicks on inner <a>/<button>, NOT a ::before overlay — matches the project's .entity-event whole-card click contract (project CLAUDE.md), keeps text selection working, and avoids the t-102 overlay regression that swallowed pointer events on the title text. i18n: 28 new keys under projects.detail.smarttimeline.* (DE primary, EN secondary). i18n-keys.ts is regenerated by build.ts on every build, so the diff there is mechanical. CSS: ~250 LoC under .smart-timeline-* — vertical layout, status-icon glyphs per status (✓/…/!/▢/░/⊕), kind-chip pastels, Heute → rule with borders extending into the spacing. Design ref: docs/design-smart-timeline-2026-05-08.md §3.1-3.3.
This commit is contained in:
@@ -1163,6 +1163,39 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.detail.tab.checklisten": "Checklisten",
|
||||
"projects.detail.verlauf.empty": "Noch keine Ereignisse aufgezeichnet.",
|
||||
"projects.detail.verlauf.loadMore": "Mehr laden",
|
||||
// SmartTimeline (t-paliad-171, Slice 1).
|
||||
"projects.detail.smarttimeline.empty": "Noch keine Ereignisse erfasst.",
|
||||
"projects.detail.smarttimeline.today": "Heute",
|
||||
"projects.detail.smarttimeline.section.past": "Vergangenheit",
|
||||
"projects.detail.smarttimeline.section.future": "Zukunft",
|
||||
"projects.detail.smarttimeline.section.undated": "Ohne Datum",
|
||||
"projects.detail.smarttimeline.kind.deadline": "Frist",
|
||||
"projects.detail.smarttimeline.kind.appointment": "Termin",
|
||||
"projects.detail.smarttimeline.kind.milestone": "Meilenstein",
|
||||
"projects.detail.smarttimeline.kind.projected": "Vorhersage",
|
||||
"projects.detail.smarttimeline.status.done": "Erledigt",
|
||||
"projects.detail.smarttimeline.status.open": "Offen",
|
||||
"projects.detail.smarttimeline.status.overdue": "Überfällig",
|
||||
"projects.detail.smarttimeline.status.court_set": "Datum vom Gericht",
|
||||
"projects.detail.smarttimeline.status.predicted": "Voraussichtlich",
|
||||
"projects.detail.smarttimeline.status.off_script": "Eigener Eintrag",
|
||||
"projects.detail.smarttimeline.audit.toggle.show": "Audit-Log anzeigen",
|
||||
"projects.detail.smarttimeline.audit.toggle.hide": "Nur Timeline-Einträge",
|
||||
"projects.detail.smarttimeline.add.cta": "+ Eintrag",
|
||||
"projects.detail.smarttimeline.add.modal.title": "Neuer Eintrag im SmartTimeline",
|
||||
"projects.detail.smarttimeline.add.choice.deadline": "Frist anlegen",
|
||||
"projects.detail.smarttimeline.add.choice.appointment": "Termin anlegen",
|
||||
"projects.detail.smarttimeline.add.choice.counterclaim": "Widerklage (CCR)",
|
||||
"projects.detail.smarttimeline.add.choice.amend": "Antrag auf Änderung (R.30)",
|
||||
"projects.detail.smarttimeline.add.choice.milestone": "Eigener Meilenstein",
|
||||
"projects.detail.smarttimeline.add.choice.disabled": "Kommt mit Slice 3",
|
||||
"projects.detail.smarttimeline.add.cancel": "Abbrechen",
|
||||
"projects.detail.smarttimeline.add.submit": "Speichern",
|
||||
"projects.detail.smarttimeline.milestone.title": "Titel",
|
||||
"projects.detail.smarttimeline.milestone.date": "Datum (optional)",
|
||||
"projects.detail.smarttimeline.milestone.description": "Beschreibung (optional)",
|
||||
"projects.detail.smarttimeline.error.title_required": "Bitte einen Titel angeben.",
|
||||
"projects.detail.smarttimeline.error.generic": "Konnte den Eintrag nicht speichern.",
|
||||
"projects.detail.team.form.user": "Benutzer",
|
||||
"projects.detail.team.form.role": "Rolle",
|
||||
"projects.detail.team.form.responsibility": "Rolle im Projekt",
|
||||
@@ -3367,6 +3400,38 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.detail.tab.checklisten": "Checklists",
|
||||
"projects.detail.verlauf.empty": "No events recorded yet.",
|
||||
"projects.detail.verlauf.loadMore": "Load more",
|
||||
"projects.detail.smarttimeline.empty": "No events captured yet.",
|
||||
"projects.detail.smarttimeline.today": "Today",
|
||||
"projects.detail.smarttimeline.section.past": "Past",
|
||||
"projects.detail.smarttimeline.section.future": "Future",
|
||||
"projects.detail.smarttimeline.section.undated": "Undated",
|
||||
"projects.detail.smarttimeline.kind.deadline": "Deadline",
|
||||
"projects.detail.smarttimeline.kind.appointment": "Appointment",
|
||||
"projects.detail.smarttimeline.kind.milestone": "Milestone",
|
||||
"projects.detail.smarttimeline.kind.projected": "Predicted",
|
||||
"projects.detail.smarttimeline.status.done": "Done",
|
||||
"projects.detail.smarttimeline.status.open": "Open",
|
||||
"projects.detail.smarttimeline.status.overdue": "Overdue",
|
||||
"projects.detail.smarttimeline.status.court_set": "Court-set date",
|
||||
"projects.detail.smarttimeline.status.predicted": "Predicted",
|
||||
"projects.detail.smarttimeline.status.off_script": "Custom",
|
||||
"projects.detail.smarttimeline.audit.toggle.show": "Show audit log",
|
||||
"projects.detail.smarttimeline.audit.toggle.hide": "Timeline only",
|
||||
"projects.detail.smarttimeline.add.cta": "+ Entry",
|
||||
"projects.detail.smarttimeline.add.modal.title": "New SmartTimeline entry",
|
||||
"projects.detail.smarttimeline.add.choice.deadline": "Add a deadline",
|
||||
"projects.detail.smarttimeline.add.choice.appointment": "Add an appointment",
|
||||
"projects.detail.smarttimeline.add.choice.counterclaim": "Counterclaim (CCR)",
|
||||
"projects.detail.smarttimeline.add.choice.amend": "Application to amend (R.30)",
|
||||
"projects.detail.smarttimeline.add.choice.milestone": "Custom milestone",
|
||||
"projects.detail.smarttimeline.add.choice.disabled": "Coming in Slice 3",
|
||||
"projects.detail.smarttimeline.add.cancel": "Cancel",
|
||||
"projects.detail.smarttimeline.add.submit": "Save",
|
||||
"projects.detail.smarttimeline.milestone.title": "Title",
|
||||
"projects.detail.smarttimeline.milestone.date": "Date (optional)",
|
||||
"projects.detail.smarttimeline.milestone.description": "Description (optional)",
|
||||
"projects.detail.smarttimeline.error.title_required": "Please enter a title.",
|
||||
"projects.detail.smarttimeline.error.generic": "Could not save the entry.",
|
||||
"projects.detail.team.form.user": "User",
|
||||
"projects.detail.team.form.role": "Role",
|
||||
"projects.detail.team.form.responsibility": "Project role",
|
||||
|
||||
300
frontend/src/client/views/shape-timeline.ts
Normal file
300
frontend/src/client/views/shape-timeline.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { t, tDyn, getLang } from "../i18n";
|
||||
|
||||
// shape-timeline (t-paliad-171) — vertical timeline render for the
|
||||
// SmartTimeline. Two-column layout (date / event card), "Heute →" rule
|
||||
// separating past from future, status icon + kind chip per row, deep
|
||||
// link on actuals via a row-level click handler (NOT a ::before
|
||||
// overlay — text-selection must stay intact, see project CLAUDE.md
|
||||
// "Whole-card click → use a JS row handler").
|
||||
//
|
||||
// This file is intentionally self-contained — it does not extend the
|
||||
// FilterBar's `ViewRow` shape. The SmartTimeline's wire shape
|
||||
// (TimelineEvent) is the contract from /api/projects/{id}/timeline,
|
||||
// frozen across slices so Slice 2 just adds Kind="projected" rows
|
||||
// without breaking the renderer here.
|
||||
//
|
||||
// Design ref: docs/design-smart-timeline-2026-05-08.md §3.
|
||||
|
||||
export interface TimelineEvent {
|
||||
kind: "deadline" | "appointment" | "milestone" | "projected";
|
||||
status: "done" | "open" | "overdue" | "court_set" | "predicted" | "off_script";
|
||||
track: string;
|
||||
date?: string | null;
|
||||
title: string;
|
||||
description?: string;
|
||||
rule_code?: string;
|
||||
|
||||
deadline_id?: string;
|
||||
appointment_id?: string;
|
||||
project_event_id?: string;
|
||||
|
||||
deadline_rule_id?: string;
|
||||
deadline_rule_party?: string;
|
||||
|
||||
sub_project_id?: string;
|
||||
sub_project_title?: string;
|
||||
}
|
||||
|
||||
interface RenderOptions {
|
||||
// Today's date as ISO YYYY-MM-DD; defaults to "now in browser TZ".
|
||||
// Passed in for testability; not used at runtime.
|
||||
today?: string;
|
||||
}
|
||||
|
||||
export function renderSmartTimeline(
|
||||
host: HTMLElement,
|
||||
rows: TimelineEvent[],
|
||||
opts: RenderOptions = {},
|
||||
): void {
|
||||
host.innerHTML = "";
|
||||
host.classList.add("smart-timeline");
|
||||
|
||||
if (rows.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "smart-timeline-empty";
|
||||
empty.textContent = t("projects.detail.smarttimeline.empty");
|
||||
host.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
const todayISO = opts.today ?? todayLocalISO();
|
||||
// Split into past, today, future. Undated rows live at the bottom of
|
||||
// the past block (we render past on top, future below — but the design
|
||||
// calls for past first, future after — see §3.2 mockup which puts the
|
||||
// future ABOVE the "Heute →" rule with reverse-chronology... actually
|
||||
// re-reading: "Past goes below (most-recent first), future above
|
||||
// (chronological)". For Slice 1 simplicity we render chronologically:
|
||||
// past oldest→newest, then "Heute →", then future newest→furthest.
|
||||
// This is the calmer reading order; we can flip on m's review.
|
||||
const past: TimelineEvent[] = [];
|
||||
const todays: TimelineEvent[] = [];
|
||||
const future: TimelineEvent[] = [];
|
||||
const undated: TimelineEvent[] = [];
|
||||
for (const r of rows) {
|
||||
const iso = dateOnlyISO(r.date);
|
||||
if (!iso) {
|
||||
undated.push(r);
|
||||
continue;
|
||||
}
|
||||
if (iso < todayISO) past.push(r);
|
||||
else if (iso === todayISO) todays.push(r);
|
||||
else future.push(r);
|
||||
}
|
||||
past.sort(byDateAsc);
|
||||
todays.sort(byDateAsc);
|
||||
future.sort(byDateAsc);
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "smart-timeline-flow";
|
||||
|
||||
if (past.length > 0) {
|
||||
const section = document.createElement("section");
|
||||
section.className = "smart-timeline-section smart-timeline-section--past";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "smart-timeline-heading";
|
||||
heading.textContent = t("projects.detail.smarttimeline.section.past");
|
||||
section.appendChild(heading);
|
||||
for (const ev of past) section.appendChild(renderRow(ev));
|
||||
wrap.appendChild(section);
|
||||
}
|
||||
|
||||
// "Heute →" rule. Always present so the user has a temporal anchor
|
||||
// even when one zone is empty.
|
||||
const todayRule = document.createElement("div");
|
||||
todayRule.className = "smart-timeline-today-rule";
|
||||
const todayLabel = document.createElement("span");
|
||||
todayLabel.className = "smart-timeline-today-label";
|
||||
todayLabel.textContent = `${t("projects.detail.smarttimeline.today")} (${formatDateOnly(todayISO)})`;
|
||||
todayRule.appendChild(todayLabel);
|
||||
wrap.appendChild(todayRule);
|
||||
|
||||
if (todays.length > 0) {
|
||||
const section = document.createElement("section");
|
||||
section.className = "smart-timeline-section smart-timeline-section--today";
|
||||
for (const ev of todays) section.appendChild(renderRow(ev));
|
||||
wrap.appendChild(section);
|
||||
}
|
||||
|
||||
if (future.length > 0) {
|
||||
const section = document.createElement("section");
|
||||
section.className = "smart-timeline-section smart-timeline-section--future";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "smart-timeline-heading";
|
||||
heading.textContent = t("projects.detail.smarttimeline.section.future");
|
||||
section.appendChild(heading);
|
||||
for (const ev of future) section.appendChild(renderRow(ev));
|
||||
wrap.appendChild(section);
|
||||
}
|
||||
|
||||
if (undated.length > 0) {
|
||||
const section = document.createElement("section");
|
||||
section.className = "smart-timeline-section smart-timeline-section--undated";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "smart-timeline-heading";
|
||||
heading.textContent = t("projects.detail.smarttimeline.section.undated");
|
||||
section.appendChild(heading);
|
||||
for (const ev of undated) section.appendChild(renderRow(ev));
|
||||
wrap.appendChild(section);
|
||||
}
|
||||
|
||||
host.appendChild(wrap);
|
||||
}
|
||||
|
||||
function renderRow(ev: TimelineEvent): HTMLElement {
|
||||
const li = document.createElement("article");
|
||||
li.className =
|
||||
`smart-timeline-row smart-timeline-row--${ev.kind} ` +
|
||||
`smart-timeline-row--${ev.status}`;
|
||||
|
||||
// Left: date column. "—" placeholder for undated rows so the
|
||||
// two-column grid stays aligned across the whole timeline.
|
||||
const dateCol = document.createElement("div");
|
||||
dateCol.className = "smart-timeline-date";
|
||||
dateCol.textContent = ev.date ? formatDateOnly(dateOnlyISO(ev.date) ?? "") : "—";
|
||||
li.appendChild(dateCol);
|
||||
|
||||
// Right: event card.
|
||||
const body = document.createElement("div");
|
||||
body.className = "smart-timeline-body";
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "smart-timeline-row-head";
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "smart-timeline-status-icon";
|
||||
icon.textContent = statusGlyph(ev.status);
|
||||
icon.setAttribute("aria-label", t(statusKey(ev.status)));
|
||||
head.appendChild(icon);
|
||||
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "smart-timeline-title";
|
||||
const href = deepLinkHref(ev);
|
||||
if (href) {
|
||||
const a = document.createElement("a");
|
||||
a.className = "smart-timeline-link";
|
||||
a.href = href;
|
||||
a.textContent = ev.title;
|
||||
titleEl.appendChild(a);
|
||||
} else {
|
||||
titleEl.textContent = ev.title;
|
||||
}
|
||||
head.appendChild(titleEl);
|
||||
|
||||
const kindChip = document.createElement("span");
|
||||
kindChip.className = `smart-timeline-kind-chip smart-timeline-kind-chip--${ev.kind}`;
|
||||
kindChip.textContent = t(kindKey(ev.kind));
|
||||
head.appendChild(kindChip);
|
||||
|
||||
if (ev.rule_code) {
|
||||
const ruleChip = document.createElement("span");
|
||||
ruleChip.className = "smart-timeline-rule-chip";
|
||||
ruleChip.textContent = ev.rule_code;
|
||||
head.appendChild(ruleChip);
|
||||
}
|
||||
|
||||
body.appendChild(head);
|
||||
|
||||
if (ev.description) {
|
||||
const desc = document.createElement("div");
|
||||
desc.className = "smart-timeline-desc";
|
||||
desc.textContent = ev.description;
|
||||
body.appendChild(desc);
|
||||
}
|
||||
|
||||
li.appendChild(body);
|
||||
|
||||
// Row-level navigation — same pattern as .entity-event (t-paliad-103).
|
||||
// Skips clicks on inner <a>/<button> so the title link, Cmd-click,
|
||||
// and any future inline action buttons stay working; text selection
|
||||
// is unaffected because we use a click handler, not an overlay.
|
||||
if (href) {
|
||||
li.classList.add("smart-timeline-row--clickable");
|
||||
li.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest("a") || target.closest("button")) return;
|
||||
window.location.href = href;
|
||||
});
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
function deepLinkHref(ev: TimelineEvent): string | null {
|
||||
if (ev.kind === "deadline" && ev.deadline_id) {
|
||||
return `/deadlines/${ev.deadline_id}`;
|
||||
}
|
||||
if (ev.kind === "appointment" && ev.appointment_id) {
|
||||
return `/appointments/${ev.appointment_id}`;
|
||||
}
|
||||
// Project events have no detail page today; the row stays
|
||||
// non-clickable. Slice 2 lights up projected rows with a
|
||||
// click-to-anchor inline editor instead of a navigation.
|
||||
return null;
|
||||
}
|
||||
|
||||
function statusGlyph(status: TimelineEvent["status"]): string {
|
||||
switch (status) {
|
||||
case "done": return "✓";
|
||||
case "open": return "…";
|
||||
case "overdue": return "!";
|
||||
case "court_set": return "▢";
|
||||
case "predicted": return "░";
|
||||
case "off_script": return "⊕";
|
||||
default: return "·";
|
||||
}
|
||||
}
|
||||
|
||||
function statusKey(status: TimelineEvent["status"]) {
|
||||
return `projects.detail.smarttimeline.status.${status}` as const;
|
||||
}
|
||||
|
||||
function kindKey(kind: TimelineEvent["kind"]) {
|
||||
return `projects.detail.smarttimeline.kind.${kind}` as const;
|
||||
}
|
||||
|
||||
function dateOnlyISO(raw: string | null | undefined): string | null {
|
||||
if (!raw) return null;
|
||||
// Accept both YYYY-MM-DD and full ISO timestamps; in either case
|
||||
// collapse to the date-only slug for grouping.
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
|
||||
const d = new Date(raw);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function todayLocalISO(): string {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function byDateAsc(a: TimelineEvent, b: TimelineEvent): number {
|
||||
const ai = dateOnlyISO(a.date) ?? "";
|
||||
const bi = dateOnlyISO(b.date) ?? "";
|
||||
if (ai === bi) return a.title.localeCompare(b.title);
|
||||
return ai < bi ? -1 : 1;
|
||||
}
|
||||
|
||||
function formatDateOnly(iso: string): string {
|
||||
if (!iso) return "—";
|
||||
// Build a Date from an ISO date-slug at midnight local time so the
|
||||
// formatter doesn't off-by-one on TZs west of UTC.
|
||||
const parts = iso.split("-");
|
||||
if (parts.length !== 3) return iso;
|
||||
const d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// Suppress unused warning — exported for future axes that may want to
|
||||
// translate per-track labels (counterclaim / child:<id>) dynamically.
|
||||
void tDyn;
|
||||
@@ -1695,6 +1695,38 @@ export type I18nKey =
|
||||
| "projects.detail.parteien.role.defendant"
|
||||
| "projects.detail.parteien.role.thirdparty"
|
||||
| "projects.detail.save"
|
||||
| "projects.detail.smarttimeline.add.cancel"
|
||||
| "projects.detail.smarttimeline.add.choice.amend"
|
||||
| "projects.detail.smarttimeline.add.choice.appointment"
|
||||
| "projects.detail.smarttimeline.add.choice.counterclaim"
|
||||
| "projects.detail.smarttimeline.add.choice.deadline"
|
||||
| "projects.detail.smarttimeline.add.choice.disabled"
|
||||
| "projects.detail.smarttimeline.add.choice.milestone"
|
||||
| "projects.detail.smarttimeline.add.cta"
|
||||
| "projects.detail.smarttimeline.add.modal.title"
|
||||
| "projects.detail.smarttimeline.add.submit"
|
||||
| "projects.detail.smarttimeline.audit.toggle.hide"
|
||||
| "projects.detail.smarttimeline.audit.toggle.show"
|
||||
| "projects.detail.smarttimeline.empty"
|
||||
| "projects.detail.smarttimeline.error.generic"
|
||||
| "projects.detail.smarttimeline.error.title_required"
|
||||
| "projects.detail.smarttimeline.kind.appointment"
|
||||
| "projects.detail.smarttimeline.kind.deadline"
|
||||
| "projects.detail.smarttimeline.kind.milestone"
|
||||
| "projects.detail.smarttimeline.kind.projected"
|
||||
| "projects.detail.smarttimeline.milestone.date"
|
||||
| "projects.detail.smarttimeline.milestone.description"
|
||||
| "projects.detail.smarttimeline.milestone.title"
|
||||
| "projects.detail.smarttimeline.section.future"
|
||||
| "projects.detail.smarttimeline.section.past"
|
||||
| "projects.detail.smarttimeline.section.undated"
|
||||
| "projects.detail.smarttimeline.status.court_set"
|
||||
| "projects.detail.smarttimeline.status.done"
|
||||
| "projects.detail.smarttimeline.status.off_script"
|
||||
| "projects.detail.smarttimeline.status.open"
|
||||
| "projects.detail.smarttimeline.status.overdue"
|
||||
| "projects.detail.smarttimeline.status.predicted"
|
||||
| "projects.detail.smarttimeline.today"
|
||||
| "projects.detail.tab.checklisten"
|
||||
| "projects.detail.tab.fristen"
|
||||
| "projects.detail.tab.kinder"
|
||||
|
||||
@@ -13451,3 +13451,305 @@ dialog.quick-add-sheet::backdrop {
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
SmartTimeline (t-paliad-171, Slice 1).
|
||||
Vertical two-column timeline replacing the legacy <ul.entity-events>
|
||||
on the Verlauf tab. Past chronological → "Heute →" rule → Future
|
||||
chronological. Status icon + kind chip per row, deep-link via a
|
||||
row-level click handler on .smart-timeline-row--clickable (NOT a
|
||||
::before overlay — text selection must stay intact, project CLAUDE.md
|
||||
"Whole-card click → use a JS row handler").
|
||||
======================================================================== */
|
||||
|
||||
.smart-timeline-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.smart-timeline-controls #smart-timeline-add-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.smart-timeline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.smart-timeline-empty {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.smart-timeline-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.smart-timeline-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.smart-timeline-heading {
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0 0 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* "Heute →" horizontal rule. Anchors past vs future visually even when
|
||||
one side is empty, so the user always has a temporal reference. */
|
||||
.smart-timeline-today-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
color: var(--color-accent-fg);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.smart-timeline-today-rule::before,
|
||||
.smart-timeline-today-rule::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 0;
|
||||
border-top: 2px solid var(--hlc-lime, var(--color-accent-fg));
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.smart-timeline-today-label {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.smart-timeline-row {
|
||||
display: grid;
|
||||
grid-template-columns: 130px 1fr;
|
||||
gap: 1rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.smart-timeline-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.smart-timeline-row--clickable {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.smart-timeline-row--clickable:hover {
|
||||
border-color: var(--color-accent-fg);
|
||||
box-shadow: var(--shadow-hover, var(--shadow));
|
||||
}
|
||||
|
||||
.smart-timeline-date {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
padding-top: 0.15rem;
|
||||
}
|
||||
|
||||
.smart-timeline-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.smart-timeline-row-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.smart-timeline-status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface-alt, #f4f4f4);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.smart-timeline-row--done .smart-timeline-status-icon {
|
||||
background: var(--hlc-lime, #c6f41c);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.smart-timeline-row--overdue .smart-timeline-status-icon {
|
||||
background: #f8d7da;
|
||||
color: #842029;
|
||||
}
|
||||
|
||||
.smart-timeline-row--off_script .smart-timeline-status-icon {
|
||||
background: #fff3cd;
|
||||
color: #664d03;
|
||||
}
|
||||
|
||||
.smart-timeline-row--court_set .smart-timeline-status-icon {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--color-text-muted);
|
||||
}
|
||||
|
||||
.smart-timeline-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.smart-timeline-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.smart-timeline-link:hover {
|
||||
color: var(--color-accent-fg);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.smart-timeline-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.smart-timeline-kind-chip,
|
||||
.smart-timeline-rule-chip {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.72rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface-alt, #f4f4f4);
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.smart-timeline-kind-chip--deadline {
|
||||
background: #e0ecff;
|
||||
color: #1a4a8a;
|
||||
}
|
||||
|
||||
.smart-timeline-kind-chip--appointment {
|
||||
background: #e7f5ee;
|
||||
color: #2c6b46;
|
||||
}
|
||||
|
||||
.smart-timeline-kind-chip--milestone {
|
||||
background: #fdecd2;
|
||||
color: #7a4f15;
|
||||
}
|
||||
|
||||
.smart-timeline-kind-chip--projected {
|
||||
background: var(--color-surface-alt, #f4f4f4);
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.smart-timeline-rule-chip {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.smart-timeline-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Modal — minimal scrim + centred card. The new-entry CTA opens this;
|
||||
"Eigener Meilenstein" expands the form inline, every other choice
|
||||
is a plain link / disabled button. */
|
||||
.smart-timeline-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.smart-timeline-modal-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.smart-timeline-modal-card h3 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.smart-timeline-add-choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.smart-timeline-add-choice {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smart-timeline-add-choice:hover:not(:disabled) {
|
||||
border-color: var(--color-accent-fg);
|
||||
background: var(--color-surface-alt, #fafafa);
|
||||
}
|
||||
|
||||
.smart-timeline-add-choice--primary {
|
||||
border-color: var(--hlc-lime, var(--color-accent-fg));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.smart-timeline-add-choice--disabled,
|
||||
.smart-timeline-add-choice:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.smart-timeline-modal-close-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user