Sidebar was constructing /documents/:id?commentId=… without the
annotationId, so clicking a mention there no-op'ed the deep-link
scroll helper. Route the href through buildCommentHref so the
bell and the chronik sidebar produce identical URLs.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the inline conditional href construction in favour of the
shared helper. Identical URL shape — behaviour preserved.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth for constructing /documents/:id?commentId=…
(&annotationId=…) URLs. Used by the notification bell, the chronik
"Für dich" sidebar, and the chronik main feed so the three surfaces
can no longer diverge.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two new optional fields on ActivityFeedItemDTO in the
generated openapi-typescript output. Matches exactly what
'npm run generate:api' would emit against the updated backend DTO;
regenerate on a live backend before merge to confirm drift-free.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comments only render inside TranscriptionEditView, so a deep-link
into a document with existing reviewed transcriptions landed the
user in read mode with no comment element in the DOM — the scroll
target silently missed.
scrollToCommentFromQuery now takes a setPanelMode callback and calls
it with 'edit' whenever both query params are present. The page's
own transcribe-mode $effect checks a skipInitialPanelMode flag the
deep-link flow sets, so its default-panel-mode logic doesn't race
against the explicit override.
Two new helper tests pin the contract: panel mode is forced to
'edit' both when transcribe mode is off (entering fresh) and when
it is already on (same-page notification click).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notification deep-link scroll targets #comment-{id}. Add the id to
the article wrapper along with tabindex="-1" so scrollIntoView +
.focus({preventScroll:true}) can land screen-reader and keyboard
focus on the specific comment. A focus-visible ring appears only
for keyboard users so mouse clicks don't trigger a visible outline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function that reads commentId + annotationId from the page URL,
enters transcribe mode if needed, activates the block's annotation,
scrolls the target comment into view, focuses it for screen readers,
fires the existing annotation flash, and strips the params via the
injected callback.
All side effects go through callbacks so the helper is unit-testable
without mounting the page or a DOM (only scrollIntoView/focus are
called on the injected element). Eight tests cover both absent params,
happy path, transcribe-mode activation, missing DOM target, reduced
motion, flash trigger, and URL strip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract filterFeed(items, filter) from +page.svelte inline switch to a
pure function, widening the fuer-dich branch to include youParticipated.
Regenerate ActivityFeedItemDTO type to include the new field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If a row ever receives a malformed uploadedAt (e.g. manual SQL migration,
backend regression), the helper now falls back to "vor 0 Minute(n)"
rather than rendering "vor NaN Tag(en)" to the user.
Addresses Nora's review suggestion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- UploadSuccessBanner dismiss button: 24×24 → 40×40 hit area (icon stays
at 16px). Matches senior-first baseline Leonie flagged.
- DashboardNeedsMetadata chevron: adds motion-reduce:transition-none and
motion-reduce:group-hover:translate-x-0 so users with prefers-reduced-
motion do not see the hover translate.
- Row title prefixed with an sr-only "PDF: " span so assistive tech
announces the document affordance alongside the title.
Addresses Leonie's review concerns #2, #3, and the sr-only nit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Pure function with injectable now — lets the dashboard enrichment block
render "vor 2 Min." / "vor 3 Std." / "vor 2 Tagen" without clock-based
test flakiness. Reuses the existing comment_time_minutes / _hours /
_days Paraglide keys, no new translations needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the restored list endpoint and the new uploadedAt field on
IncompleteDocumentDTO (issue #296).
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>
Pure function bucketByDay(date, now?, locale?) returns one of
'today'|'yesterday'|'thisWeek'|'older' so ChronikTimeline can
bucket activity rows by relative day without pulling a date
library.
Handles:
- midnight boundary (startOfDay comparison)
- locale-aware week start (Monday for most locales, Sunday for en-US,
en-CA, en-PH, ja-JP, he-IL, pt-BR)
- DST transitions (works off local calendar days)
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>
Adds count (required) and happenedAtUntil (optional) to the TypeScript DTO so
Chronik + DashboardActivityFeed can consume rollup rows type-safely.
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>
Adds DashboardResumeDTO, DashboardPulseDTO, ActivityFeedItemDTO,
ActivityActorDTO and the three /api/dashboard/* paths.
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>