GeschichteSidebar gains optional geschichteId/items props and renders the
panel only when geschichteId is set. GeschichteEditor derives both from
its existing GeschichteView prop — null on /geschichten/new, so the panel
stays hidden there (create-then-edit, same as journeys). JourneyEditor's
sidebar call site is untouched, so journeys never show the panel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sidebar-section-styled panel (p-4 card, mobile <details> accordion, no
inner scroll clamp) that lists a story's journey items in position order.
Add is pessimistic via POST /items; remove is optimistic with snapshot
rollback via DELETE /items/{id}; both through csrfFetch. Already-linked
documents are unselectable in the reused DocumentPickerDropdown (visible
label wired via inputId). Document-less items (ON DELETE SET NULL)
render as removable placeholder rows. 409 capacity/duplicate map to
story-worded messages, everything else through getErrorMessage(). Add/
remove are announced in a polite live region and focus moves to the
previous row's remove button (picker input when the list empties).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Panel header, hint, empty state, picker label + placeholder, deleted-
document placeholder, remove-button label, live-region announcements,
and the story-worded capacity/duplicate errors (the generic
JOURNEY_AT_CAPACITY / JOURNEY_DOCUMENT_ALREADY_ADDED messages say
"Lesereise" — the wrong frame inside a STORY panel).
Side effect: the JSON round-trip collapsed three pre-existing duplicate
keys per locale (identical values, last-wins either way) —
error_geschichte_title_too_long, error_geschichte_intro_too_long,
person_unknown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The input always carries an id (generated doc-picker-input-{uid} default,
mirroring the listbox id scheme). When a caller passes inputId it wires a
visible <label for> — the aria-label fallback is dropped then so the
visible label wins. JourneyAddBar stays untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the JOURNEY-only type guard in JourneyItemService.append() so the
existing item endpoints serve both Geschichte types. GeschichteType has
exactly two constants, so an allowlist replacement would be unreachable
dead code. Fix the not-found messages that claimed "Journey", and remove
the now-orphaned GESCHICHTE_TYPE_MISMATCH error code end to end
(ErrorCode, errors.ts union + mapping, i18n keys in de/en/es).
Tests: three STORY append unit tests written red against the guard, plus
end-to-end STORY coverage (append+retrieve, V72-style position-gap rows
incl. removal, dangling document-deleted item). The two STORY-rejection
tests die with the guard — no third enum value exists to feed it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
handleAddDocument/handleAddInterlude were identical except the POST
body. Behavior unchanged — all 35 editor specs stay green.
Review round 3: Felix suggestion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- extractText.ts states the real safety invariant (output is text-only;
JOURNEY intros arrive unsanitised by design)
- geschichte README: stale glyph/pill cells updated, formatAuthorName no
longer claims an email fallback, formatDocumentMetaLine documented
- reader spec HTMLs: bg-white list-card cell and the mobile BottomSheet
rows struck with the implemented decision (inline metabar actions)
Review round 3: Nora (2), Markus (2), Felix, Elicit (3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- JourneyItemCard: 'Brief öffnen' back to a >=44px touch target with the
height regression spec restored
- GeschichteListRow: REISE badges text-[10px] -> text-xs; drop the
hardcoded aria-label and the mobile badge's aria-hidden so phone screen
readers learn a row is a Lesereise; mobile avatar initials -> color dot
- detail page: badge text-xs, metabar Edit/Delete h-9 -> h-11, avatar
color keyed by name to match the list
- JourneyReader: dead border-subtle class -> border-line-2
- DocumentPickerDropdown: aria-controls only while the listbox exists
- JourneyAddBar: aria-expanded/aria-controls on both toggles + focus
hand-off into the revealed picker input / interlude textarea
- GeschichteSidebar: section h2s hidden below sm (summary already shows
the label there)
- JourneyCreate: bg-brand-navy -> semantic bg-primary/text-primary-fg;
title maxlength=255
- JourneyItemRow interlude: neutral frame border + left accent only,
token utilities instead of arbitrary var() syntax and inline style
Review round 3: Leonie (1-8 + round-1 leftovers), Elicit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
joinNameOrUnknown()/unknownPersonName() in personFormat.ts replace the
three hand-rolled '[Unbekannt]' literals (geschichte/utils,
GeschichtenCard, personOption) — the fallback is now the i18n key
person_unknown (de/en/es), and formatAuthorDisplayName localizes the
server-side literal on the pass-through path.
Review round 3: Felix, Markus, Leonie (7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An abandoned query no longer fires a stale fetch after the dropdown
closed, and loading can't stay stuck on true. Test pins both.
Review round 3: Felix suggestion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The c3afd57e fix made the edit page's handleSubmit throw on !res.ok but
only JourneyEditor caught it. Now: GeschichteEditor.save() catches and
keeps its dirty state (no unhandled rejection -> no GlitchTip noise on a
failed STORY save); StoryCreate throws on failure so a failed STORY
create no longer silently disarms the unsaved guard; both handleSubmit
implementations catch network rejections, surface a message, and rethrow.
Contract documented on both editors' Props. GeschichteEditor also gets
the title maxlength=255. Spec: rejecting onSubmit is caught and the
editor stays usable.
Review round 3: Felix §2, Tobias S1, Nora (3), Markus (concern 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- handleNotePatch routes failures through failureMessage() so a backend
JOURNEY_NOTE_TOO_LONG renders its specific message in the row
- handleNoteBlur: '' vs undefined no-op guard (no spurious PATCH
{note:null}), empty blur collapses the textarea, clearing an existing
note collapses after the PATCH lands (spec LE-3)
- 'Notiz hinzufügen' toggle gets aria-expanded and moves focus into the
revealed textarea
- journey_remove_item_aria interpolates the item title (de/en/es); dead
journey_drag_aria_label key deleted from all locales
- editor item list is an <ol> (screen readers announce the ordering)
- editor title input gets maxlength=255 + border-danger error cue; intro
textarea gets maxlength=4000
- Briefmeta line (date · von X an Y) under document titles in the editor
row via the shared formatDocumentMetaLine (spec LE-2)
- new specs: successful save clears the unsaved warning; item add does
not arm the guard; server-confirmed order after successful reorder;
blur-without-typing no-op; focus hand-off into the note textarea
Review round 3: Sara (1-3), Felix, Elicit (LE-2/LE-3), Leonie.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Title: requireTitle() turns the VARCHAR(255) DB bound into a friendly 400
(GESCHICHTE_TITLE_TOO_LONG). JOURNEY intro: MAX_INTRO_LENGTH = 4000 in
bodyForType() plus a V75 CHECK as atomic backstop (defensive clamp first,
STORY bodies exempt) — GESCHICHTE_INTRO_TOO_LONG. Both codes wired through
ErrorCode.java, errors.ts, getErrorMessage and de/en/es. DB-layer boundary
pins added: exactly-2000 note insert (V74 CHECK) and 4000/4001 intro
inserts against real Postgres. Docs: error-code lists, db puml diagrams.
The matching maxlength attributes land with the component commits.
Review round 3: Markus (b), Nora (1), Sara (DB 2000 boundary).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dedup DELETE + note clamp before the DDL so the unique index and CHECK
cannot fail mid-migration (failed Flyway row -> backend boot loop) on a
DB that served writes under the old code (no dedup guard, 5000-char
notes). No-ops on a clean database.
Note: this changes V74's checksum — dev databases that already ran V74
on this branch need 'flyway repair' (or a fresh DB). CI and prod run
from V73 or clean and are unaffected.
Review round 3: Tobias (1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Any other DataIntegrityViolationException at flush (e.g. an FK violation
on a concurrently deleted document) is rethrown instead of being
mislabeled as JOURNEY_DOCUMENT_ALREADY_ADDED. Match on the
uq_journey_items_geschichte_document constraint name.
Review round 3: Markus (a), Felix suggestion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The author column stayed at w-28 after the typography bump, wrapping
names into a cramped two-line stack. w-40 gives name and date one
comfortable line each at the full-width layout.
Refs #802
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rows kept their compact sizes (15px titles, 12px excerpts/meta) after
the overview widened to max-w-7xl, leaving the text undersized in
full-width rows. Title is now text-lg, excerpt and meta text-sm; R-1
impl-ref updated.
Closes#802
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Captures the read-model invariant that was only living in a code comment:
views are assembled in-transaction, entities never cross the controller
boundary in the geschichte domain. CLAUDE.md §DTOs now names the exception
so the convention text stops contradicting the code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OWASP sanitizer entity-encodes ('&' → '&') while JourneyReader
renders the intro via Svelte text interpolation — a curator typing
'Müller & Söhne' saw 'Müller & Söhne', re-encoded cumulatively on every
editor round-trip. JOURNEY bodies now bypass the sanitizer (safe: the reader
never uses {@html}); STORY bodies keep the full allow-list sanitization.
This makes the code match the PR's documented design note.
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>