feat(ui): v0.16 features + dark mode across every page

Wires up everything from the previous commits into actual UI surfaces, and
applies Tailwind dark: variants throughout. All pages now support the
'system' / 'light' / 'dark' preference set in the onboarding step or in
Mein Konto → Design.

Layout & nav:
- routes/+layout.svelte: initTheme(), global pin-reset SSE handler that
  filters by user_id and calls clearPin(), one-shot /me/context fetch
  on boot to hydrate privacyNote + quota.
- components/BottomNav.svelte: dark variants on the frosted-glass bar.
- components/UploadSheet.svelte: dark variants on backdrop, sheet,
  source buttons.
- components/OnboardingGuide.svelte: new "Helles oder dunkles Design?"
  step (3-option custom-radio grid), reactive currentStep with proper
  type narrowing, dark variants throughout. Privacy-note nudge appears
  on the PIN step only when one is configured.

Feed:
- routes/feed/+page.svelte: diashow entry icon (tablet/desktop only),
  long-press → ContextSheet (Löschen for own posts, Original anzeigen
  for all), upload-deleted + feed-delta SSE handlers, dark variants on
  header, search, autocomplete, filter chips, empty states.
- components/FeedListCard.svelte: long-press wireup, double-tap-to-like,
  data-mode-aware mediaSrc via pickMediaUrl, kebab fallback for desktop,
  isOwn prop, dark variants.
- components/FeedGrid.svelte: long-press wireup, dark variants.
- components/LightboxModal.svelte: data-mode-aware src, double-tap heart
  burst, dark variants on card / comments / input.
- components/HashtagChips.svelte: dark variants.

Account:
- routes/account/+page.svelte: theme picker (3-button radio grid), data
  mode picker (with confirm sheet for Original), live quota widget,
  preformatted Datenschutzhinweis block, diashow tile (mobile only),
  pin now sourced from the $currentPin store so a global pin-reset
  clears it live, clearQueue() on explicit logout, dark variants
  across every card + both bottom sheets.

Upload:
- routes/upload/+page.svelte: per-user quota progress bar above the
  submit button, dark variants.

Host & Admin:
- routes/host/+page.svelte: PIN-reset confirm + one-time PIN modal,
  hosts may demote other hosts, canResetPinFor() helper, dark variants
  on all cards, modals, stats, toast.
- routes/admin/+page.svelte: Config form rebuilt as CONFIG_GROUPS with
  per-field kind (number / bool / text), renders toggles for the
  rate-limit + quota switches and a textarea for the privacy_note;
  Nutzer tab gains PIN reset + hosts-may-demote-hosts wiring; same
  one-time PIN modal; dark variants everywhere.
- routes/admin/login/+page.svelte: dark variants.

Join / Recover / Export:
- routes/join/+page.svelte: rename inline link to
  "Ich habe bereits einen Account", dark variants.
- routes/recover/+page.svelte: dark variants.
- routes/export/+page.svelte: dark variants on status cards + HTML
  guide modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-16 14:33:30 +02:00
parent 8a769b52bf
commit e619a3bd64
17 changed files with 1295 additions and 438 deletions

View File

@@ -1,16 +1,24 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
import '../app.css';
import { initAuth } from '$lib/auth';
import { onMount } from 'svelte';
import { initAuth, getToken, getUserId, clearPin } from '$lib/auth';
import { initTheme } from '$lib/theme-store';
import { onMount, onDestroy } from 'svelte';
import BottomNav from '$lib/components/BottomNav.svelte';
import UploadSheet from '$lib/components/UploadSheet.svelte';
import { showBottomNav } from '$lib/ui-store';
import { isAuthenticated } from '$lib/auth';
import { queueItems, isProcessing } from '$lib/upload-queue';
import { privacyNote } from '$lib/privacy-note-store';
import { refreshQuota } from '$lib/quota-store';
import { onSseEvent } from '$lib/sse';
import { api } from '$lib/api';
import type { MeContextDto } from '$lib/types';
let { children } = $props();
let unsubs: Array<() => void> = [];
// Slim progress bar: ratio of completed items to total, shown while processing.
let progressPct = $derived.by(() => {
const total = $queueItems.length;
@@ -19,8 +27,41 @@
return Math.round((done / total) * 100);
});
onMount(() => {
onMount(async () => {
initAuth();
// Hooks up the appliedTheme → <html class="dark"> sync. Must run early so the
// first paint after hydration matches the saved preference.
initTheme();
// Hydrate cross-cutting stores once on boot if the user is already authenticated.
// Page-level mounts will refresh again as needed.
if (getToken()) {
try {
const ctx = await api.get<MeContextDto>('/me/context');
privacyNote.set(ctx.privacy_note);
} catch {
// non-fatal; users without a session land on /join anyway
}
void refreshQuota();
}
// Global pin-reset listener — clears the now-invalid plaintext PIN from
// localStorage no matter which route the user is currently on. The reactive
// `currentPin` store carries the change into any page that reads it (My
// Account in particular).
unsubs.push(
onSseEvent('pin-reset', (data) => {
try {
const payload = JSON.parse(data) as { user_id: string };
if (payload.user_id === getUserId()) clearPin();
} catch {
// ignore malformed payload
}
})
);
});
onDestroy(() => {
for (const unsub of unsubs) unsub();
});
</script>