Adding the link div after the {#each} broke last:border-0 — the last
mention item was no longer the last child, so it kept its border-b,
creating a double line with the link's border-t. Wrapping the each in
its own div restores correct last:border-0 targeting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracted from NotificationBell.svelte into $lib/utils/notifications.ts so the
history page can reuse them. relativeTime() now accepts an optional `now` param
for deterministic unit testing. Added parseNotificationEvent() for SSE payload
shape validation (NullX Finding 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moved the "hat erwähnt / hat geantwortet" span outside the <a> so
hover:underline only applies to the actor name, not the muted label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move text-decoration-thickness/underline-offset into the global a:hover
base rule so every link that shows an underline on hover gets identical
treatment: 2px thick, 4px offset, accent colour.
Remove the now-redundant per-component decoration-brand-mint / decoration-
accent / decoration-2 / underline-offset-{2,4} utilities from DocumentList,
enrich, persons, PersonDocumentList, and PanelMetadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Box content links (document titles, actor names) raised from text-sm to
text-lg for improved readability and touch target size. "Show all" stays
at text-sm to maintain hierarchy — box links are the primary action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DocumentService.getRecentActivity: replace findAll(Sort)+stream().limit()
with findAll(PageRequest) so LIMIT is pushed to the database
- +page.svelte: collapse two-column grid to single column when mentions is empty
- DashboardNeedsMetadata: raise "show all" link from text-xs (12px) to text-sm
(14px) and add hover:underline for WCAG 1.4.1
- DashboardRecentDocuments: add comment explaining why T12:00:00 noon-anchor
is absent (updatedAt is a full ISO datetime, not a date-only string)
- DocumentServiceTest: update getRecentActivity tests to assert PageRequest
usage instead of findAll(Sort)
- DocumentRepositoryTest: add @DataJpaTest verifying findAll(PageRequest)
returns only size rows, not the full table
- DocumentControllerTest: add test for default size=5 when param is omitted
- NotificationServiceTest: add test documenting that type+read=true falls
through to the type-only query (intentional)
- page.server.spec.ts: replace stale tests with full dashboard-mode coverage
- DashboardMentions.svelte.spec.ts: add tests for REPLY type and absent documentId
- DashboardResumeStrip.svelte.spec.ts: add corrupt localStorage test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace recent-by-creation fetch with GET /api/documents/recent-activity
(sorted by updatedAt) in the dashboard. Update DashboardRecentDocuments
component to use doc.updatedAt, update i18n heading to "Zuletzt aktiv" /
"Recent Activity" / "Actividad reciente", and regenerate API types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Notification widget builds full link with ?commentId= and
&annotationId= params, matching the bell notification behaviour
- Recent docs widget shows createdAt (upload date) instead of
documentDate (the date on the original document)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all hardcoded German strings in dashboard components with
Paraglide translation keys. Date locale uses getLocale() instead
of the hardcoded 'de-DE'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add type-only filter to notification repo/service (previously only
worked with type+read=false together)
- Dashboard widget now fetches all recent notifications (mentions +
replies, both read and unread) instead of unread mentions only
- Update component heading and show type label per row
Root cause: Berit's mentions were read=true, so the unread-only filter
returned 0 results. The recent docs widget had no REVIEWED documents
because 'marking ready' sets metadata_complete, not status=REVIEWED.
Recent docs now shows all uploads without a status filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four dashboard components (ResumeStrip, Mentions, NeedsMetadata, RecentDocuments)
used static brand colors that do not adapt to dark mode. Replace with bg-surface,
border-line, text-ink, text-ink-2 throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows recently reviewed documents as a dashboard widget with formatted
dates. Renders nothing when the list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows documents with missing metadata as a dashboard widget with links
to the enrich workflow. Renders nothing when the list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows unread mention notifications as a dashboard widget. Renders
nothing when the mentions list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Component reads familienarchiv.lastVisited from localStorage and
shows a 'Zuletzt geöffnet' link to the last-visited document
- Renders nothing when no localStorage entry exists
- Document detail page writes id+title to localStorage on mount
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--c-accent (#a1dcd8 light / #00c7b1 dark) is a decorative mint token —
1.52:1 on white, nowhere near WCAG AA. Every place it appeared as the
colour of a text label or interactive button is switched to text-primary
(#012851, 16.8:1 on white) with hover:text-ink-2 for consistency.
Affected: UsersTab, GroupsTab, CommentThread (Reply), DocumentList
(Clear search), PdfViewer (Direkt öffnen link).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications are already fetched lazily inside toggleDropdown() when
the user opens the dropdown. Only fetchUnreadCount() is needed on mount
to show the badge.
Closes#725
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AnnotationSidePanel: cover visibility (null vs set annotationId),
close button callback, and targetCommentId forwarding
- layout.svelte.spec: mock $env/static/public to satisfy
PUBLIC_NOTIFICATION_POLL_MS import from NotificationBell
- mention.spec: update assertion to match span-based mention rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationBell now includes annotationId in the deep-link URL when available
- +page.svelte reads ?annotationId= param and sets activeAnnotationId on mount,
opening the side panel instead of the bottom discussion drawer
- AnnotationSidePanel accepts and forwards targetCommentId to CommentThread
so the specific comment is highlighted when navigating via a notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BLOCKERs:
- Remove direct AppUserRepository/CommentRepository access from CommentService and
NotificationService — replaced with UserService.findAllById() and UserService
(fixes layering contract from CLAUDE.md)
- Switch Optional<JavaMailSender> constructor injection — removes @Autowired(required=false)
field and ReflectionTestUtils hack in tests
- Add @RequirePermission(READ_ALL) to UserSearchController — prevents user enumeration
without read access
Data bug:
- Promote actorName from @Transient to persisted VARCHAR column (V18 migration)
- Set actorName in notifyReply and notifyMentions from comment.getAuthorName()
Architecture:
- Add @RequirePermission(READ_ALL) to NotificationController
- Introduce NotificationDTO — controller returns DTO instead of Notification entity,
eliminating lazy-load N+1 and AppUser field leakage
- Change mentions FetchType to EAGER — fixes LazyInitializationException outside transaction
- Add @Transactional(propagation=REQUIRES_NEW) to notifyReply/notifyMentions so a
notification failure cannot roll back the parent comment
- N+1 fix: replace per-ID findById loops with single findAllById bulk fetch
- Move collectParticipantIds to CommentService; notifyReply accepts Set<UUID> directly
Security:
- Escape displayName before injecting into renderBody HTML span
- Replace <a href="#"> with <span class="mention"> — no profile page to link to, and
the anchor's scroll-to-top behaviour is harmful
Tests added/fixed:
- markRead_throwsNotFound, markAllRead_delegatesToRepository, countUnread_delegatesToRepository
- markOneRead_returns401, @RequirePermission 403 coverage for both controllers
- postComment/replyToComment_triggersNotifyMentions_whenMentionedUserIdsProvided
- search_returnsAtMostTenResults now asserts $.length() <= 10
- XSS regression test for escaped displayName in mention.spec.ts
Frontend minors:
- relativeTime() uses Intl.RelativeTimeFormat (locale-aware, not German-hardcoded)
- aria-label uses m.notification_unread() Paraglide key (de/en/es added)
- <div role="button"> replaced with <button> (native Enter+Space handling)
- onDestroy clears debounceTimer in MentionEditor
- setTimeout(100) replaced with await tick() + requestAnimationFrame in CommentThread
- Notification prefs form uses checkbox name attributes + formData.has() pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +page.svelte: read ?commentId= from URL; on mount, if present open bottom panel to discussion tab
- CommentThread: add targetCommentId prop — scrolls to comment on mount (scrollIntoView), applies ring highlight, removes highlight on first user interaction (click/keydown/scroll)
- CommentThread: add data-comment-id attributes to thread root and reply divs
- PanelDiscussion / DocumentBottomPanel: thread targetCommentId prop through the chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationBell.svelte: bell icon in header with unread badge, dropdown showing last 10 notifications, mark-all-read, click-outside close, keyboard Escape support, polls every PUBLIC_NOTIFICATION_POLL_MS ms
- Wire NotificationBell into +layout.svelte between ThemeToggle and UserMenu (authenticated users only)
- Profile page: add notification preferences card with notifyOnReply / notifyOnMention toggles, loaded via GET and saved via PUT /api/users/me/notification-preferences
- i18n: de/en/es message keys for bell, notifications list, and preference labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add touch-action:none to container when in annotate mode so the
browser doesn't intercept touch gestures for scroll/pan
- Replace onmouseenter/onmouseleave with onpointerenter/onpointerleave
so the highlight effect also fires on touch/stylus
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes duplicated locale logic from +layout.svelte and AppNav.svelte.
Context-specific sizing (text-xs/min-h-[44px]) stays in the wrapper
via [&_button]: selectors so the component itself is layout-agnostic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced position:fixed on the bottom panel with shrink-0 flex child,
so the viewer (flex-1) naturally stops at the panel top instead of
extending behind it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
At 320px, showing "Annotieren" + "Bearbeiten" + download pushed the
toolbar past its bounds. Icon-only at mobile, labels revealed at sm:.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap tabs in overflow-x-auto container with hidden scrollbar so all 4
German labels ("Transkription" etc.) are reachable at 320px. Close
button stays pinned outside the scroll area, always visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In dark mode --c-primary switches from navy (#012851) to mint (#a1dcd8).
Buttons using bg-primary+text-white showed white text on mint at 1.4:1
contrast — invisible. bg-brand-navy buttons were also invisible (navy on
near-black canvas, 1.3:1).
Replaced in 28 components app-wide:
- bg-primary ... text-white → text-primary-fg
- hover:bg-primary hover:text-white → hover:text-primary-fg
- bg-brand-navy ... text-white + hover:bg-brand-navy/90 →
bg-primary ... text-primary-fg + hover:bg-primary/90
Light mode is unchanged: primary-fg = white in light mode.
Dark mode: primary-fg = navy (#012851) on mint bg = readable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructure the "New Document" page so users can save quickly:
- FileSectionNew becomes the first element, redesigned as a prominent
upload zone with an icon and large click target
- Title field is rendered standalone below the upload zone; it
auto-populates from the filename (via parseFilename + stripExtension
fallback) unless the user has already typed something
- All remaining metadata (who/when, description, transcription) moves
into a collapsible "Weitere Details" section that auto-expands when
URL prefill data or a form error is present, or when filename parsing
detects a date/person
- title is no longer required — the form can be saved with only a file
- DescriptionSection gains a `hideTitle` prop for use in this layout
- `form_label_title` translation key no longer carries a hardcoded `*`;
the asterisk is rendered by the template only when `titleRequired` is
set (currently only the edit form)
- E2E tests added for all three scenarios from the issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In dark mode --c-primary flips to mint (#a1dcd8), making text-white
unreadable. text-primary-fg is already paired correctly in both modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the discussion count badge on every state (including 0) instead of
a separate nudge button. Simpler, less intrusive, and works without
needing an extra element near the panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add comment count badge on the Discussion tab (seeded from SSR, updated live)
- Add 'Diskussion starten' nudge above collapsed panel when no comments exist
- Add empty state hint with speech-bubble icon inside the discussion panel
- Fix CommentThread to fire onCountChange with SSR-seeded count on mount
- Add tests for all three behaviours in CommentThread and DocumentBottomPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a file is selected on the new document page, parseFilename runs
on the filename and suggests date, sender name and title via the new
suggestedDateIso / suggestedSenderName / suggestedTitle props. Each
suggestion is applied only while the respective field is still clean
(not dirty), so manual input is never overwritten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates type duplication across 6 files by introducing a single
shared types module:
- Comment + CommentReply: were identically defined in CommentThread,
PanelDiscussion, and DocumentBottomPanel
- DocumentPanelTab: was identically defined in DocumentBottomPanel
and documents/[id]/+page.svelte
- Annotation: was defined in both AnnotationLayer and PdfViewer
(PdfViewer's variant with fileHash? is now the canonical definition)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The root-comment and reply rendering blocks were near-identical (view mode
with author/time/edit-delete, and edit mode with textarea/save/cancel).
Extracted a local {#snippet commentEntry(comment, threadId, showReplyButton)}
that handles both states, introducing Svelte 5 snippets to the codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PdfViewer: add $effect that forces showAnnotations=true when annotateMode
becomes true, so hiding annotations before drawing no longer breaks drawing
- DocumentViewer: restore missing fileHash field on Doc type and pass
documentFileHash to PdfViewer (lost when rebase dropped the merge commit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace text-gray-*, bg-gray-*, border-gray-*, divide-gray-*, placeholder-gray-*,
focus:border-blue-*, focus:ring-blue-*, hover:bg-blue-*, and ring-brand-mint with
their semantic-token equivalents (text-ink, bg-muted, border-line, etc.) across
all pages and shared components so dark mode renders correctly everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces bg-white, text-brand-navy, border-brand-sand, text-gray-*, bg-[#2A2A2A],
bg-brand-purple/15, hover:bg-brand-sand, etc. across all 35 .svelte files with
semantic token utilities (bg-surface, text-ink, border-line, bg-pdf-bg, bg-nav-active,
bg-muted, text-accent, bg-primary, ...).
Also adds CSS filter: invert(1) in layout.css for De Gruyter <img> icons in dark mode,
excluding icons that carry .invert already (to prevent double-inversion).
Closes#64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inline <script> in app.html applies saved localStorage theme before first
paint to prevent flash of wrong theme
- ThemeToggle.svelte: moon/sun button, localStorage persistence, sets
data-theme on <html>, defaults to system preference on first visit
- Placed in +layout.svelte between language selector and user menu
- E2E tests cover visibility, toggle, reverse toggle, persistence, and
no-flash behaviour — all 6 passing
Refs #64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No logic changes — whitespace and indentation only. These were flagged
by the pre-commit hook when running lint after layout.css was modified.
Refs #64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep the new bottom-panel / AnnotationSidePanel architecture from this branch
while pulling in the documentFileHash / visibleAnnotations filter that was added
on main. Thread documentFileHash through DocumentViewer so outdated-annotation
filtering works end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pointer-events-none and pointer-events-auto were both present as static
and conditional Tailwind classes simultaneously. CSS specificity meant
pointer-events-none always won, so clicks passed through to the
annotation toggle button behind the panel. Now pointer-events-none is
only applied when the panel is hidden (translated off-screen).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Annotation threads now open in a slide-in side panel (320 px, right
edge of the PDF viewer) instead of expanding the bottom drawer.
The PDF stays visible while the user reads and writes annotation
comments.
- Add AnnotationSidePanel component (absolute-positioned, CSS slide
transition, keyed CommentThread, close via X or Escape)
- Remove the $effect that opened the bottom drawer on annotation click
- Simplify PanelDiscussion back to document-level thread only (no
annotation sub-tabs)
- Remove annotation-related props from DocumentBottomPanel and
PanelDiscussion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The document viewer container was using fixed inset-0 z-50 which
covered the sticky global nav bar. Now measures nav height at mount
and offsets the container top accordingly, dropping z-index to z-40.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>