Files
familienarchiv/frontend/src/lib/geschichte/JourneyAddBar.svelte
Marcel 66e9309d8a fix(geschichte-ui): a11y and visual round-3 batch
- 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>
2026-06-11 08:30:59 +02:00

127 lines
4.0 KiB
Svelte

<script lang="ts">
import { tick } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import DocumentPickerDropdown from '$lib/document/DocumentPickerDropdown.svelte';
import type { DocumentOption } from '$lib/document/documentTypeahead';
interface Props {
alreadyAddedIds?: Set<string>;
onAddDocument: (doc: DocumentOption) => void;
onAddInterlude: (text: string) => void;
}
let { alreadyAddedIds = new Set(), onAddDocument, onAddInterlude }: Props = $props();
let showPicker = $state(false);
let showInterludeForm = $state(false);
let interludeDraft = $state('');
let rootEl: HTMLElement | null = $state(null);
const canConfirmInterlude = $derived(interludeDraft.trim().length > 0);
async function togglePicker() {
showPicker = !showPicker;
showInterludeForm = false;
if (showPicker) {
// Keyboard users need a perceivable result of activating the toggle.
await tick();
rootEl?.querySelector<HTMLInputElement>('#journey-add-picker input')?.focus();
}
}
async function toggleInterludeForm() {
showInterludeForm = !showInterludeForm;
showPicker = false;
if (showInterludeForm) {
await tick();
rootEl?.querySelector<HTMLTextAreaElement>('#journey-add-interlude textarea')?.focus();
}
}
function handleDocumentSelect(doc: DocumentOption) {
showPicker = false;
onAddDocument(doc);
}
function handleInterludeConfirm() {
if (!canConfirmInterlude) return;
const text = interludeDraft.trim();
interludeDraft = '';
showInterludeForm = false;
onAddInterlude(text);
}
function handleInterludeCancel() {
interludeDraft = '';
showInterludeForm = false;
}
</script>
<div bind:this={rootEl} class="flex flex-col gap-3">
<div class="flex flex-wrap gap-2">
<button
type="button"
data-add-document
onclick={togglePicker}
aria-expanded={showPicker}
aria-controls={showPicker ? 'journey-add-picker' : undefined}
class="inline-flex h-11 items-center rounded border border-line bg-surface px-4 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
+ {m.journey_add_document()}
</button>
<button
type="button"
onclick={toggleInterludeForm}
aria-expanded={showInterludeForm}
aria-controls={showInterludeForm ? 'journey-add-interlude' : undefined}
class="inline-flex h-11 items-center rounded border border-line bg-surface px-4 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
+ {m.journey_add_interlude()}
</button>
</div>
{#if showPicker}
<div id="journey-add-picker">
<DocumentPickerDropdown
alreadyAddedIds={alreadyAddedIds}
onSelect={handleDocumentSelect}
placeholder={m.journey_add_document()}
/>
</div>
{/if}
{#if showInterludeForm}
<div id="journey-add-interlude" class="flex flex-col gap-2">
<textarea
bind:value={interludeDraft}
placeholder={m.journey_interlude_placeholder()}
rows={3}
maxlength={2000}
class="block w-full resize-y rounded border border-line bg-surface px-3 py-2 font-sans text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
></textarea>
<div class="flex gap-2">
<button
type="button"
onclick={handleInterludeConfirm}
disabled={!canConfirmInterlude}
class={[
'inline-flex h-11 items-center rounded px-4 font-sans text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring',
canConfirmInterlude
? 'bg-primary text-primary-fg hover:opacity-90'
: 'cursor-not-allowed bg-primary/40 text-primary-fg/60'
].join(' ')}
>
{m.journey_add_interlude_confirm()}
</button>
<button
type="button"
onclick={handleInterludeCancel}
class="inline-flex h-11 items-center rounded border border-line bg-surface px-4 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.journey_remove_confirm_cancel()}
</button>
</div>
</div>
{/if}
</div>