Files
EventSnap/FOLLOWUPS.md
MechaCat02 309c25bc06 feat(frontend): UX review followups — primitives + a11y/UX fixes across 4 passes
New shared primitives:
- Toaster + toast-store, ConfirmSheet, Modal, focusTrap action,
  pullToRefresh action, avatarPalette + initials helper, Skeleton,
  HeartBurst, haptics, export-status store with onClearAuth hook

Critical UX/a11y:
- Replaced window.confirm with branded ConfirmSheet
- Focus management + Escape on every modal (PIN, Lightbox,
  Onboarding, ContextSheet, data-mode sheet, leave-confirm,
  HTML guide, host/admin ban + PIN-display modals)
- Sheet backdrops are real buttons with aria-label
- Silent ApiError catches now surface via global Toaster

Major polish:
- Dark-mode parity on HashtagChips + avatars (shared palette)
- Conditional Export tab in BottomNav (badge dot when ZIP ready)
- Back chevrons on /recover (history-aware) and /export
- Upload composer discard confirmation when content is staged
- Camera segmented Photo/Video shutter
- PIN auto-submit on 4th digit, paste-flash-free (controlled input)
- Welcome-back toast on /feed after PIN recovery

Minor:
- Skeleton states on feed; pull-to-refresh with live drag indicator
- Haptics on like / capture / submit / PIN-copy / onboarding complete
- Comment 500-char counter; quota "Fast voll" / "Limit erreicht" labels
- Onboarding pip ≥24px tap targets; long-press hint step
- overscroll-behavior lock on <html> while feed mounted
- teardownExportStatus wired via onClearAuth (covers 401 + explicit logout)
- ConfirmSheet per-instance titleId; Modal requires titleId or ariaLabel

Tests (7 new Playwright specs):
- 01-auth/pin-auto-submit, 01-auth/back-chevron
- 03-feed/confirm-sheet-delete, 03-feed/toast-on-failure
- 09-mobile/focus-trap, 09-mobile/sheet-escape,
  09-mobile/upload-cancel-confirm

FOLLOWUPS.md captures the deferred AT inert containment work
with acceptance criteria + implementation sketches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:50:28 +02:00

54 lines
3.6 KiB
Markdown

# Follow-ups
Tracked work that was deferred during the multi-round UI/UX review pass.
Each item has a clear acceptance criterion so a future pass can land it
without re-deriving the context.
## A11y — assistive-tech containment inside modals
**Problem.** Open modals (LightboxModal, ConfirmSheet, Modal, OnboardingGuide,
join PIN modal, account data-mode sheet, export HTML guide) trap keyboard Tab
via `focusTrap`, but VoiceOver rotor / TalkBack arrow-key navigation can still
escape into the page content behind the dialog. Screen-reader users hear the
wrong context.
**Why deferred.** A naive sibling-walk that sets `inert` on direct children of
the modal's parent silences the global `<Toaster>` (`aria-live="polite"` region
mounted in `+layout.svelte`) and the `<BottomNav>` while a modal is open —
breaking toast announcements and the visible nav state. SvelteKit has no
built-in portal mechanism, so dialogs render inside the route tree alongside
the Toaster.
**Acceptance criterion.** With any modal open:
- VoiceOver rotor (iOS Safari) and TalkBack swipe navigation (Android Chrome)
cannot leave the dialog subtree.
- Toasts that fire while a modal is open are still announced.
- Nested modals (e.g. ConfirmSheet opened from inside ContextSheet) maintain
correct containment when the inner closes.
**Sketch of an approach.** One of:
1. **Portal pattern.** Render dialogs into a dedicated `<div id="modal-root">`
that's a sibling of the main app root in `app.html`. `focusTrap` then sets
`inert` on the main root, leaving the modal root and the toast region (also
moved to its own portal root) untouched.
2. **Opt-out marker.** Walk siblings and inert them, but skip any node carrying
a `data-modal-passthrough` attribute. Mark `<Toaster>` with it. Document
the contract.
3. **Stack-aware containment.** Maintain a module-level stack of open dialog
nodes; the topmost owns the inert state, popped dialogs restore the
previous layer. Avoids the nested-modal restoration bug.
Approach 1 is the cleanest long-term but the highest blast radius. Approach 2
is the smallest patch.
**Files to touch.**
- [frontend/src/lib/actions/focus-trap.ts](frontend/src/lib/actions/focus-trap.ts) — add inert logic
- [frontend/src/lib/components/Toaster.svelte](frontend/src/lib/components/Toaster.svelte) — add passthrough marker (if approach 2) or move to a portal (if approach 1)
- [frontend/src/app.html](frontend/src/app.html) — add `<div id="modal-root">` (if approach 1)
## Smaller nits, optional
- **Auto-submit on retried 4th digit.** [recover/+page.svelte](frontend/src/routes/recover/+page.svelte), [join/+page.svelte](frontend/src/routes/join/+page.svelte) — after a wrong PIN, deleting one digit and retyping triggers an immediate submit. Backend's 3-attempts/15-min lockout makes this safe; could feel hair-trigger after a typo. Consider gating the second auto-submit per input session behind an explicit button press.
- **Onboarding pip tap target on the vertical axis.** [OnboardingGuide.svelte](frontend/src/lib/components/OnboardingGuide.svelte) — current `p-2.5` yields ~26 px height, meets WCAG 2.2 AA (≥24 px) but below iOS HIG / Material's 44 / 48 dp recommendation. Bumping to `p-3` is the easy improvement; further increases start crowding the row.
- **Migrate bespoke focus-trapped dialogs to `<Modal>`.** Join PIN modal, OnboardingGuide, LightboxModal, HTML guide, account data-mode sheet — all currently roll their own shell with `focusTrap`. They're correct, just not using the canonical primitive. Migrate when `<Modal>` gains features (e.g. the inert work above) you'd want everywhere.