frontend(plumbing): shared theme tokens, cross-cutting stores, gestures, sheets

The plumbing layer the v0.16 UI features (and dark mode) build on.

Shared design tokens (Tailwind v4):
- tailwind-theme.css (new): @custom-variant dark (class-driven, beats OS
  default) + @theme color/font/radius tokens + baseline html/html.dark
  rules so any page that hasn't been re-themed still renders the right
  body bg + color-scheme.
- src/app.css + export-viewer/src/app.css now import the shared theme.
- src/app.html: 6-line FOUC guard sets <html class="dark"> before paint
  (mirrored from theme-store.ts) so dark reloads no longer flash white.
  Adds <meta name="theme-color"> kept in sync by initTheme().

Cross-cutting stores (one per concern, per docs/FEATURES §2.9):
- data-mode-store.ts: 'saver' | 'original' per-device, plus pickMediaUrl
  helper so feed cards / lightbox / diashow all resolve URLs the same way.
- privacy-note-store.ts: hydrated from /me/context, refreshed on SSE
  event-updated.
- quota-store.ts: { enabled, used, limit, active_uploaders, free_disk },
  refreshed after each upload completes.
- theme-store.ts: 'system' | 'light' | 'dark' preference + derived
  appliedTheme + initTheme() that syncs <html class>, localStorage,
  and the theme-color meta. Listens to prefers-color-scheme.
- auth.ts: currentPin writable mirror + clearPin() helper called from
  the global pin-reset SSE handler — fixes the stale-PIN bug where the
  localStorage copy survived a reset.

DTO mirror:
- types.ts: QuotaDto, MeContextDto, PinResetResponse, DeltaResponse each
  carry a `// mirrors backend/...` comment per the lib README convention.

SSE client:
- sse.ts: KNOWN_EVENTS registry (one entry per server-emitted type),
  synthetic feed-delta dispatched after foreground reconnect via the
  /feed/delta?since= endpoint, exponential backoff (1 → 60 s + jitter)
  on errors, attempt counter reset on user-initiated visibility resume.

Upload queue:
- upload-queue.ts: IDB schema bumped to v2 — entries tagged with userId;
  loadQueue filters by current user (no cross-user leak on shared
  devices); uploadItem refuses to upload an entry whose userId differs
  from getUserId() (defense-in-depth); new clearQueue() called on
  explicit logout. v2 upgrade wipes pre-v2 entries (no userId, can't
  attribute safely).

Mobile primitives:
- actions/longpress.ts: 500 ms hold with 10 px move tolerance, swallows
  the next click + the right-click contextmenu so the gesture doesn't
  double-fire the inner button's onclick.
- actions/doubletap.ts: tap-pair detector that preventDefaults the
  second tap so iOS Safari doesn't also zoom on double-tap.
- components/ContextSheet.svelte: generic bottom sheet driven by a
  ContextAction[] prop. Reused by feed posts, comments, host user rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-16 14:32:37 +02:00
parent 2e98f5ddf5
commit 251f9f1469
15 changed files with 733 additions and 34 deletions

View File

@@ -0,0 +1,60 @@
/* Shared design tokens for the live app and the offline HTML-viewer export.
* Both `frontend/src/app.css` and `frontend/export-viewer/src/app.css` import this file
* so the keepsake stays visually in sync with the live app. Edit tokens here, rebuild
* the export-viewer, and re-commit `backend/static/export-viewer/`.
*
* Tailwind v4 reads `@theme` blocks to populate utility classes; everything declared
* here becomes a `bg-primary`, `text-accent`, `rounded-card`, etc.
*/
@import "tailwindcss";
/* Class-based dark variant. Tailwind v4 defaults to `prefers-color-scheme`; we want
* the user's explicit selection (saved in `theme-store.ts`) to win, so we re-bind
* the `dark:` variant to apply whenever `<html>` has the `dark` class.
* `:where(...)` keeps the specificity low so existing utilities still override. */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Brand palette — the blue used for primary buttons, FAB, active tabs. */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
/* Accent for hashtag chips and highlights. */
--color-accent-500: #a855f7;
--color-accent-600: #9333ea;
/* Surface scale matches the existing gray-* usage. Listed here so the viewer
* picks up the same shade in case Tailwind defaults ever drift. */
--color-surface-0: #ffffff;
--color-surface-50: #f9fafb;
--color-surface-100: #f3f4f6;
--color-surface-200: #e5e7eb;
/* Typography. Keepsake should feel like the live app — same defaults. */
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", "Courier New", monospace;
/* Radii — keep cards and bottom sheets consistent. */
--radius-card: 0.75rem; /* rounded-card */
--radius-sheet: 1.25rem; /* rounded-sheet */
}
/* Baseline body background + text colour so pages that haven't been re-themed yet
* at least don't render light-on-light or dark-on-dark. Pages and cards still set
* their own backgrounds via `bg-*` utilities, but this catches any gaps. */
@layer base {
html {
background-color: #f9fafb; /* matches bg-gray-50 */
color: #111827; /* matches text-gray-900 */
color-scheme: light;
}
html.dark {
background-color: #030712; /* matches bg-gray-950 */
color: #f3f4f6; /* matches text-gray-100 */
color-scheme: dark;
}
}