The detail page stayed at max-w-3xl and looked narrower than every
other page. The outer container now matches the directory width
(max-w-7xl) so the sheet spans the page like Dokumente/Personen, while
an inner max-w-3xl column keeps the prose line length readable.
Refs #799
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reader interlude and the LESEREISE badge hardcoded orange-50/200/
400/700, leaving light text on a light background in dark mode. Switch
to the existing journey-tint/journey/journey-border tokens already used
by the list and editor rows.
Closes#801
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Intro, letter titles, curator annotations, and interludes rendered at
12-14px — copied from the scaled mockup sizes in LR-2. Narrative text
is now 16-18px, meta lines and links 14px; the LR-2 impl-ref table is
updated to match.
Closes#800
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The list page used max-w-4xl while every other directory page uses
max-w-7xl. The detail page intentionally stays max-w-3xl (reading
column per spec R-2).
Closes#799
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reading sheet used bg-surface (white), so the document cards inside
the article had the same background as the sheet itself. The spec's
three-level hierarchy is canvas → article panel (#FAFAF7) → white cards;
introduce --color-sheet (mode-aware) and use it on the article. Also
move JourneyItemCard from bg-white to bg-surface so dark mode remaps it,
and tint the curator annotation with bg-muted so it stands off the card.
Refs #797
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The R-2 mockup shows the article on a distinct light panel, but the
impl-ref table only specified the centered container, so the page
rendered flat on the canvas. Wrap the <article> in the standard card
pattern (BackButton stays outside on the canvas) and record the sheet
in both spec impl-ref tables so mockup and impl-ref agree.
Closes#797
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
border-mint is not a generated utility (the token is --color-brand-mint),
so the border-l-2 fell back to currentColor and rendered navy instead of
the mint accent specified in LR-2.
Closes#798
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interlude was redesigned to the orange-left-border block from spec
LR-2 (no ❦ glyph), but the spec test still asserted the glyph and was
red at HEAD.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Detail header gains the author avatar with a two-line author block;
journeys say 'zusammengestellt am' instead of 'veröffentlicht am'.
Bearbeiten/Löschen move into the metabar for stories too (were at the
article bottom). StoryReader renders real document reference cards
(icon, title, date · von X an Y) instead of a placeholder link, person
chips get avatar initials, and journey items lose the doubled spacing.
Shared formatDocumentMetaLine() in geschichte/utils feeds both readers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single white list card with the person-filter pills inside, rows split
into an author meta column (avatar, name, date, REISE badge) and a
content column (serif title, two-line excerpt). Badge moves into the
meta column on desktop and next to the title on mobile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JourneyItemCard: restructure from full-<a> to div+card with meta line
(date · von X an Y) and explicit "Brief öffnen →" link; note renders as
mint-border annotation inside the card.
JourneyInterlude: remove ❦ ornament; orange-400 left-border spec classes.
JourneyReader: fix intro classes (dashed border-b); remove bottom author
actions (moved to +page.svelte metabar).
+page.svelte geschichten/[id]: badge above title with spec orange-50 classes;
Bearbeiten/Löschen in metabar right side for isJourney + canBlogWrite.
JourneyItemRow: items-center on main row; drag handle self-center; note
textarea inline in content column (removes border-t section below).
i18n: add journey_item_open, journey_item_meta_from_to to de/en/es.
Tests: update JourneyItemCard + JourneyReader specs to match new structure;
fix datePrecision 'FULL'→'DAY', add receiverCount: 0 to all test fixtures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
try/finally without catch swallowed TypeError network failures silently.
Added catch block that sets errorMessage so the UI shows role=alert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Skip-first-run $effect tracks selectedPersons array length; any add/remove
after mount marks the editor dirty so the unsaved-warning fires on nav.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the incorrect claim "isDirty stays false" from the test name.
The banner tests added in the previous commit cover the isDirty signal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JourneyEditor now renders UnsavedWarningBanner when showUnsavedWarning
is true. save() wraps onSubmit in try/catch so clearOnSuccess only fires
on success. edit/+page.svelte handleSubmit throws instead of returning
on non-ok responses so JourneyEditor sees the failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Focus moves to Cancel when confirm appears (no focus-drops-to-body),
confirm area has role=group with aria-label, and Escape dismisses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Status/error/empty messages rendered outside <ul role="listbox"> so the
element only contains role="option" children (fixes aria-required-children
axe violation). Options no longer carry tabindex or onkeydown — keyboard
navigation stays on the input per ARIA combobox pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI caught three spots the targeted local runs missed: the relevance-path
unit tests still stubbed findAllById (the path now calls findByIdIn — the
batchMetadata stubs legitimately keep findAllById), the second
GeschichtenCard test file still expected the removed email fallback, and
the AND/OR-toggle describe lacked the wait-for-slide-transition guard its
sibling describe documents — the flake that failed run 2208.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the remaining #792 review blockers and concerns in the journey
editor cluster:
- Interlude rows show 'Zwischentext' (dedicated key), not the add-button text
- All four mutation handlers route the backend ErrorCode through
getErrorMessage (a 409 duplicate no longer says 'bitte Seite neu laden')
and console.error their failures so client-side errors leave a trace
- Remove implements the spec'd pending state: row stays dimmed with an
aria-live 'wird entfernt…' until the DELETE resolves; failure keeps the row
- Move announcements fire after the reorder resolves (no false 'verschoben')
- Touch targets ≥44px (remove ×, note links, create submit); focus moves to
the new row after add, to a sensible neighbor after remove, back to × on
confirm-cancel; drag handle is pointer-only; title/intro get aria-labels;
publish-disabled reason is a visible hint, not a title tooltip
- Amber warning styles use new --color-warning-* tokens with dark remaps
- Blocked interlude-clear restores the draft instead of showing phantom text
- useBlockDragDrop moves to $lib/shared/hooks — geschichte no longer imports
another domain's internals
- Test hardening: reorder-failure rollback (non-ok + reject), publish/
unpublish/empty-warning surface, destructive confirm path, maxlength
assertions, JourneyCreate failure path, edit-page STORY/JOURNEY branch,
fixture factory, m.* assertions, all fixed sleeps replaced with polling
67 component tests green across 6 spec files; transcription consumer of the
moved hook re-verified (30 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The input declared role=combobox + aria-autocomplete=list while arrow keys
did nothing (WCAG 4.1.2). Wired the useTypeahead activeIndex the same way
PersonTypeahead does: ArrowUp/Down cycle, Enter/Space select, Escape closes,
aria-activedescendant tracks the active option; the list is a real listbox
with option roles again (the interim role downgrade is reverted). Zero hits
now render a 'Keine Treffer' row instead of silently vanishing, aria-expanded
matches the visible state, and the hook sets loading at setQuery so the
debounce window can't read as 'no results'. DocumentMultiSelect renders the
shared error state too. _uid counter replaced with $props.id().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DocumentMetadataDrawer links to /geschichten?documentId={id}, but the list
loader silently dropped the param — the user got the unfiltered list. The
loader now validates the UUID and forwards it to GET /api/geschichten,
returning it as documentIdFilter in page data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JourneyEditor fed GeschichteView.PersonView (no displayName) straight into
the displayName-rendering PersonMultiSelect, so every chip on a journey was
empty. The name mapping both editors need now lives once in
$lib/person/personOption.ts (with the [Unbekannt] fallback matching
GeschichteService.toView), and PersonMultiSelect/GeschichteSidebar import
the narrow PersonOption contract from there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/geschichten shipped every author's AppUser email to all readers via
GeschichteSummary.AuthorSummary — contradicting the documented rule that
author projections never expose email or group memberships. The frontend
only used it as a display-name fallback; it now falls back to [Unbekannt],
matching the server-side rule in GeschichteService.toView.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GESCHICHTE_TYPE_IMMUTABLE and JOURNEY_NOTE_TOO_LONG were declared in
errors.ts, translated, and documented — but never existed in the backend.
update() now rejects a type change with 409 (omitted/same type still pass);
note length is enforced at 2000 with its own code, matching the frontend
maxlength and the i18n message (resolves the #793 discrepancy in favour of
the spec). JOURNEY_ITEM_NOT_IN_JOURNEY is deleted everywhere instead — the
deliberate 404 posture for cross-journey item ids must not leak existence
via a distinct code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With create/update returning GeschichteView, no endpoint serves the raw
Geschichte entity and springdoc drops its schema. Dashboard modules and the
home loader now use GeschichteSummary; GeschichteEditor takes GeschichteView
and maps persons into the displayName shape PersonMultiSelect renders —
fixing blank person chips on story edit. PersonMultiSelect/Sidebar narrow to
Pick<Person, 'id' | 'displayName'>, mirroring the DocumentOption precedent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The document picker parsed the response without checking r.ok, so a 401/500
rendered identically to 'no matches' and the dropdown silently vanished —
which is how the broken relevance path shipped invisibly. The fetch now
throws on non-OK, the useTypeahead hook exposes an error flag, and the
picker renders a visible failure message (de/en/es).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
px-2 py-1 gave ~28px height — half the WCAG 2.2 / project 44px minimum.
Changed to min-h-[44px] inline-flex items-center for both buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#6b7280 (gray-500) on #f5f4f0 yielded ~4.0:1 — below the 4.5:1 AA minimum
for normal text. Changed to #4b5563 (gray-600) which provides ~6.9:1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
role=listbox + role=option without arrow-key navigation is misleading — the
WAI-ARIA combobox pattern requires aria-activedescendant handling that isn't
implemented. Downgraded to plain <ul>/<li>; input keeps role=combobox +
aria-controls pointing to the list id.
listboxId was a module-level constant so two simultaneous instances would share
the same DOM id. Fixed with a <script module> counter.
Updated spec queries from getByRole('option') to getByText() — tests behaviour,
not the ARIA implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentPickerDropdown and DocumentMultiSelect had identical createTypeahead
configs, fetch logic, and formatDocLabel helpers. Extracted to
documentTypeahead.ts; all four consumers import from the shared module.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
track_reactivity_loss in Svelte 5 async.js fires when a $bindable write
happens while the slide transition is still being tracked asynchronously.
Waiting for the toggle to be visible ensures the transition has settled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
aria-disabled alone leaves the button keyboard-activatable, violating
WCAG 4.1.2. Native disabled removes it from the tab order and prevents
activation via Enter/Space.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleNoteRemove mutated UI state optimistically without try/catch.
A failed PATCH left the note visually deleted while it survived on the
server. Now uses snapshot/rollback identical to handleNoteBlur.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
null ?? undefined evaluated to undefined, causing JSON.stringify to omit
the key entirely — the backend treated an absent note field as a no-op,
so clearing a note never persisted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NVDA+Chrome and VoiceOver+Safari can re-announce a persistent non-empty
aria-live region when adjacent DOM mutations occur. Clearing with a
500ms delay gives the announcement time to fire once before going quiet.
The two svelte-ignore a11y_no_static_element_interactions suppressions are
given preceding comments explaining the keyboard accessibility contract so
they are not mistaken for unaddressed tech debt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A persistent non-empty aria-live region can cause stale re-announcements
on adjacent DOM mutations. This test confirms the region is empty after
the 500ms clear timeout fires following a move operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The noteError alert path (role=alert paragraph) was untested.
The catch block in handleNoteBlur was already implemented; this
test verifies it renders the alert when onNotePatch rejects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check used document.body.textContent which includes ARIA labels
and hidden elements — a match in those could misfire the indexOf comparison.
compareDocumentPosition(DOCUMENT_POSITION_FOLLOWING) checks DOM tree position
directly and is not affected by non-visible text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'remove confirm' tests were still finding the remove button by the
old label ('Wirklich entfernen?'). After the aria-label change the
button is named 'Eintrag entfernen'; the visible confirmation text
'Wirklich entfernen?' in the DOM is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the inline vi.unstubAllGlobals() call from the end of the
'reveals picker' test body and added it to the shared afterEach hook
so every test in the file gets the same global cleanup regardless of
which test runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>