From 956a23d0a8607e5d5606a80c6e71a9f7b1f029ea Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 13:16:33 +0200 Subject: [PATCH] feat(timeline): add precision-aware date-label facade timelineDateLabel delegates to the shared formatDocumentDate so a timeline chip renders identically to the same date on a document, in the active locale (REQ-001/REQ-002). UNKNOWN precision and null/undefined/'' eventDate short-circuit to null with no formatter call (REQ-003/REQ-004); raw is always null since timeline events carry no verbatim spreadsheet cell. The facade owns no precision logic of its own (REQ-005). Register the new `timeline` frontend domain in the eslint boundaries config (allowed to import only `shared`) and add src/lib/timeline/** to the vitest coverage include (REQ-006). The spec partially mocks the paraglide runtime via importOriginal so getLocale is stubbed while the formatter still resolves real season/range message exports. Refs #778 Co-Authored-By: Claude Opus 4.8 --- frontend/eslint.config.js | 2 ++ frontend/src/lib/timeline/dateLabel.spec.ts | 9 +++++-- frontend/src/lib/timeline/dateLabel.ts | 30 +++++++++++++++++++++ frontend/vite.config.ts | 1 + 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/timeline/dateLabel.ts diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 4488ca79..40aee11b 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -158,6 +158,7 @@ export default defineConfig( { type: 'ocr', pattern: 'src/lib/ocr/**' }, { type: 'activity', pattern: 'src/lib/activity/**' }, { type: 'conversation', pattern: 'src/lib/conversation/**' }, + { type: 'timeline', pattern: 'src/lib/timeline/**' }, { type: 'shared', pattern: 'src/lib/shared/**' }, { type: 'routes', pattern: 'src/routes/**' } ] @@ -198,6 +199,7 @@ export default defineConfig( { from: { type: 'user' }, allow: { to: { type: ['shared'] } } }, { from: { type: 'notification' }, allow: { to: { type: ['shared'] } } }, { from: { type: 'conversation' }, allow: { to: { type: ['shared'] } } }, + { from: { type: 'timeline' }, allow: { to: { type: ['shared'] } } }, { from: { type: 'shared' }, allow: { to: { type: ['shared'] } } }, { from: { type: 'routes' }, diff --git a/frontend/src/lib/timeline/dateLabel.spec.ts b/frontend/src/lib/timeline/dateLabel.spec.ts index 7ec9e9c4..3caabed5 100644 --- a/frontend/src/lib/timeline/dateLabel.spec.ts +++ b/frontend/src/lib/timeline/dateLabel.spec.ts @@ -5,8 +5,13 @@ import { timelineDateLabel } from './dateLabel'; // The mocked path MUST include the `.js` suffix to match the import in // dateLabel.ts — a specifier mismatch silently skips the mock and the helper -// would resolve the real locale. -vi.mock('$lib/paraglide/runtime.js', () => ({ getLocale: vi.fn(() => 'de') })); +// would resolve the real locale. We partially mock via importOriginal so only +// getLocale is stubbed: the shared formatter still pulls real Paraglide message +// exports (season words, range prefixes) off the same runtime module. +vi.mock('$lib/paraglide/runtime.js', async (importOriginal) => ({ + ...(await importOriginal()), + getLocale: vi.fn(() => 'de') +})); describe('timelineDateLabel', () => { beforeEach(() => { diff --git a/frontend/src/lib/timeline/dateLabel.ts b/frontend/src/lib/timeline/dateLabel.ts new file mode 100644 index 00000000..98e1cb42 --- /dev/null +++ b/frontend/src/lib/timeline/dateLabel.ts @@ -0,0 +1,30 @@ +import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate'; +import { getLocale } from '$lib/paraglide/runtime.js'; + +/** + * Renders a timeline event's date by delegating to the shared + * {@link formatDocumentDate} — so a timeline chip reads identically to the same + * date on a document, in whichever locale is active. This module is a thin + * façade: it owns NO precision or rendering logic (REQ-005), only the two + * timeline-specific decisions below. + * + * @param eventDate the event's anchor day (`YYYY-MM-DD`). `null`, `undefined` + * and `''` are equivalent — all mean "undated" and yield `null` WITHOUT + * calling the formatter (REQ-004). + * @param precision the event's precision metadata; `'UNKNOWN'` yields `null` + * (no chip) even when a date is present (REQ-003). + * @param eventDateEnd the RANGE end day; `undefined` and `null` are equivalent + * and both mean an open-ended range. + * @returns the localized label, or `null` for an UNKNOWN/undated event. + */ +export function timelineDateLabel( + eventDate: string | null | undefined, + precision: DatePrecision, + eventDateEnd?: string | null +): string | null { + if (precision === 'UNKNOWN' || !eventDate) return null; + // raw is always null for timeline events — there is no verbatim spreadsheet + // cell to interpolate; season words derive from the structured anchor month + // (never from untrusted text). See documentDate.ts for the raw contract. + return formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale()); +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 34686e4b..371a78a2 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -72,6 +72,7 @@ export default defineConfig({ 'src/lib/shared/server/**', 'src/lib/shared/discussion/**', 'src/lib/document/**', + 'src/lib/timeline/**', 'src/hooks.server.ts' ], exclude: [