Hoists the $navigating store into a shared __mocks__ module so tests can
drive it through real transitions. Adds two specs covering (a) skeleton
visible while $navigating && topDocs empty and (b) skeleton hidden when
topDocs is non-empty. Also sets aria-busy="true" on the skeleton so
screen readers announce the loading state (Leonie's a11y suggestion).
Addresses Sara's and Felix's review concern that the skeleton branch was
dead code in the test world.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composes UploadSuccessBanner + DashboardNeedsMetadata and reserves a
360px skeleton while \$navigating re-runs the loader with a fresh
incomplete list. Prevents the layout-shift jump after a batch upload
(Leonie's resolved decision #3 on issue #296).
Renders nothing when there is nothing to show — keeps the clean empty
dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Transient post-upload banner for issue #296: singular/plural German copy,
aria-live=polite for screen readers, manual X dismiss, 8s auto-dismiss.
"Jetzt ergänzen →" CTA links directly to /enrich so seniors can continue
straight into the enrichment flow after a batch upload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches to two props — topDocs (max 5, capped by caller) and totalCount —
so the footer link can surface "Alle 12 anzeigen →" even when only 5
items are shown. Each row gets a generic document icon, title, relative
upload time and a chevron, wrapped in a single <a> per the issue spec.
Still returns null when topDocs is empty, keeping the empty dashboard
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two PR #288 blockers from Felix and Leonie:
Felix: verbText.indexOf(docTitle) broke when the title was empty (indexOf
returned 0, the before/after slices both emptied) or when the title
substring-matched any word in the compiled Paraglide message (e.g. "Brief"
appearing inside a translated verb). Swap to a sentinel approach: interpolate
{doc} with U+0001, then split the compiled text on that sentinel — robust
regardless of title content or translator sentence order. Two new red tests
lock the invariant: empty title still renders the row link; short titles
that could substring-match render exactly once as a single chronik-doc-title
span.
Leonie: the comment variant rendered „{documentTitle}" as a placeholder,
which made the row show the same title twice — once as the underlined link,
once as the italic "preview quote" — implying the comment was quoting
itself. Replace with an italic ellipsis „…". A new red test asserts the
preview no longer contains the document title text verbatim.
While here, add a SECURITY comment next to the TODO so the next person who
wires item.commentPreview knows the backend must truncate/strip server-side
and the frontend must use {text}, never {@html} (Nora, issue #285#3552).
Part of #285, address PR #288 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTML5 forbids interactive content (<button>, <a>, <input>...) as descendants
of <a>. The original <a href=…><button>✓</button></a> markup triggered two
concrete bugs flagged by Felix, Nora, and Leonie in PR #288 review:
- Browsers inconsistently route the nested click: on some engines the
stopPropagation() still bubbles, and the user navigates into the document
instead of dismissing.
- The senior audience (60+) tap-selects with a slight drag, and the OS
treats the interaction as anchor vs. button inconsistently — a
reproducible usability failure Leonie has seen in testing before.
Refactor to the Option-C layout from issue #285 comment #3573: outer <li>
flex container, <a> wrapping avatar + body + time, <button> as a sibling.
Independent focus stops, invalid-HTML gone, no behavioural regression.
A new spec locks the invariant: `dismiss.closest('a')` must be null.
Part of #285, address PR #288 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Alle anzeigen" link at the bottom of the notification dropdown now
points to /chronik with the new "Zur Chronik →" label key, matching the
unified activity page introduced in #285.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- "Alle anzeigen" link now goes to /chronik (was /documents — the dead-end
bug called out in #285).
- Rollup rows (count > 1) render a primary-colored count badge plus a
compound timestamp line: "14. Apr. · 14:02–14:32" (en-dash U+2013).
- Singleton rows render the existing "14. Apr. 2026" date line.
- BLOCK_REVIEWED now has a verb mapping (re-using the annotation verb until
the spec pins a distinct copy).
- Three new spec cases: rollup count badge + en-dash range, no badge on
singletons, /chronik link assertion.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All under src/lib/components/chronik/:
- ChronikRow.svelte — single orchestrator for four variants (comment / for-you /
rollup / simple), discriminated via $derived. Outer <a> wraps avatar + body +
time; document title is a styled <span> (no nested anchors). Rollup shows
count badge + en-dash time range; for-you gets accent left border + @ marker
hidden below sm:.
- ChronikTimeline.svelte — buckets items by day using bucketByDay() and renders
Heute/Gestern/Diese Woche/Älter section headers with <span> trailing rule.
- ChronikFuerDichBox.svelte — unread mentions card with inbox-zero variant,
per-row Dismiss button (prevents bubbling, calls onMarkRead), aria-live count
badge, and a .fade-in class gated by prefers-reduced-motion.
- ChronikFilterPills.svelte — role=radiogroup with 5 pills, ArrowLeft/Right
keyboard navigation wrapping across the group, single tabstop via dynamic
tabindex.
- ChronikEmptyState.svelte — three variants (first-run / filter-empty /
inbox-zero) sharing a centered-column layout.
- ChronikErrorCard.svelte — warning card with retry button, optional custom
message override.
Verbs map to chronik_singleton_* / chronik_rollup_* per AuditKind so no ICU
pluralization is needed. Comment preview is a TODO placeholder (currently the
document title) pending a backend preview DTO follow-up.
All 40 unit tests green. No type-check or lint errors in these files.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Left over from the hook→singleton refactor — NotificationDropdown still
imported from the deleted $lib/hooks path.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the per-component createNotificationStream() factory with a shared
$lib/stores/notifications.svelte.ts singleton. Ref-counted init()/destroy()
ensures one EventSource per tab no matter how many consumers mount
simultaneously.
Motivation: the /chronik "Für dich" box (#285) needs the same live-arrival
stream that NotificationBell already consumes. Two factories would open two
SSE connections per tab — this refactor avoids the silent regression before
it ships.
- New: src/lib/stores/notifications.svelte.ts (module state, refcount)
- New: src/lib/stores/notifications.svelte.spec.ts (proves single EventSource
across multiple consumers + ref-counted teardown)
- Deleted: src/lib/hooks/useNotificationStream.svelte.ts (factory)
- Deleted: src/lib/hooks/__tests__/useNotificationStream.svelte.test.ts
- NotificationBell now imports the singleton
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ProgressRing renders SVG + percentage label as a flex column (~52px
total). With items-center, the contributor circles aligned to the middle
of the full block, placing them 8px below the ring center. Changed to
items-start on the container and wrapped ContributorStack in h-9 (36px =
SVG height) flex items-center so both circles center at the same 18px.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved 9 conflicts:
- AuditLogQueryRepository/Service: keep HEAD (findRecentContributorsForDocuments)
- ContributorStack: merge main key fix + text-[10px] with HEAD safeColor + aria
- DashboardResumeStrip: merge main text-[10px] with HEAD safeColor
- +page.server/svelte + tests: keep HEAD (pure dashboard, no isDashboard)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace (actor.name ?? actor.initials + i) with (actor.initials + '-' + actor.color)
to fix operator-precedence bug that made keys order-dependent when name is null
- Add role="img" + aria-label={actor.name ?? actor.initials} so screen readers
and touch users can access contributor names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +layout.svelte: Upload button in header (authenticated users only)
- +page.server.ts: call /api/dashboard/resume, /pulse, /activity;
remove deprecated /api/documents/incomplete and /recent-activity
- +page.svelte: 2-col grid layout (main + 320px sidebar), greeting,
DashboardFamilyPulse + DashboardActivityFeed in sidebar
- DashboardResumeStrip: refactored to use server data (resumeDoc prop),
SVG thumbnail, progress bar with aria-*, empty state, CTA
- DashboardFamilyPulse: new component — weekly stats from audit_log
- DashboardActivityFeed: new component — activity feed with "für dich" badge
- Update specs for new data shapes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DashboardResumeStrip: text-[10px] → text-xs on collaborator initials (WCAG 1.4.4)
- documents/+page.server.ts: use !result.response.ok per CLAUDE.md; keep narrow try/catch for network-level failures only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ContributorStack: text-xs for WCAG 1.4.4 (was text-[10px]), safeColor()
validation to block CSS injection via actor.color, role="img" aria-label
on empty placeholder, {#each} keyed by index
- ContributorStack spec: update empty-state assertion to getByRole('img')
- DocumentRow spec: add stopPropagation regression test for tag click
- documents/page.svelte.spec.ts: new — debounce, URL building, initial state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace (actor.name ?? actor.initials + i) with (actor.initials + '-' + actor.color)
to fix operator-precedence bug that made keys order-dependent when name is null
- Add role="img" + aria-label={actor.name ?? actor.initials} so screen readers
and touch users can access contributor names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +layout.svelte: Upload button in header (authenticated users only)
- +page.server.ts: call /api/dashboard/resume, /pulse, /activity;
remove deprecated /api/documents/incomplete and /recent-activity
- +page.svelte: 2-col grid layout (main + 320px sidebar), greeting,
DashboardFamilyPulse + DashboardActivityFeed in sidebar
- DashboardResumeStrip: refactored to use server data (resumeDoc prop),
SVG thumbnail, progress bar with aria-*, empty state, CTA
- DashboardFamilyPulse: new component — weekly stats from audit_log
- DashboardActivityFeed: new component — activity feed with "für dich" badge
- Update specs for new data shapes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add * to Datum and Absender labels (both are required fields)
- Add required prop to PersonTypeahead to show * in its label
- Move "Optional" divider in DescriptionSection to after Titel (the only
required field), so Tags and Inhalt appear below the divider where they belong
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds label_required_fields to all three locales. Fixes "Datei ersetzen"
toolbar colors to use semantic ink tokens (readable in both light and dark
pdf-bg themes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract DocumentEditLayout shared component for the PDF+form split-panel
UI, replacing the old scrolling layout on /documents/[id]/edit with the
same fixed-panel structure used by /enrich/[id]. Removes TranscriptionSection
and FileSectionEdit from the edit page; file upload/replace is now handled
by the shared layout. Delete SaveBar and FileSectionEdit as dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- error_file_upload_failed key used in enrich upload handler
- label_optional key added (de/en/es) and used in DescriptionSection divider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- text-[9px]/text-[10px] in required-fields bar raised to text-xs (12px),
meeting the project minimum for the 60+ audience (WCAG 1.4.4)
- Upload animation now uses motion-safe: prefix so it stops for users
with prefers-reduced-motion set (WCAG 2.1 SC 2.3.3)
- Strengthened UploadZone tests: onCancel uses [role=status] button
selector instead of first-button heuristic; added positive file
selection test (valid PDF calls onFile), file-too-large test, and
MIME rejection now also asserts the error message is visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Field order: Titel → Schlagworte → Kurzinhalt → [Optional divider] → Aufbewahrungsort.
currentTitle is now bindable so the enrich page can derive the required-fields progress bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Required fields (Datum, Absender) move to row 1; optional fields (Empfänger, Ort)
to row 2. dateIso is now bindable for the progress bar. Autofocus lands on the
first empty required field on page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>