feat(ui): elegant wedding white/silver/gold redesign
Reskin the whole app via design tokens in tailwind-theme.css (remapped color ramps) plus a define-once component layer in lib/styles/components.css (.btn, .card, .input, .chip, .badge, .sheet, …). Buttons are muted/outlined gold rather than flat fills. Self-host Inter + Fraunces (woff2) under the existing font-src 'self' CSP. Restyle the shared components and the account, admin, host, join, recover and upload screens against the new tokens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
@import './tailwind-theme.css';
|
||||
@import './lib/styles/components.css';
|
||||
|
||||
/* Respect the OS "reduce motion" setting. Collapses every animation/transition
|
||||
* (HeartBurst, Skeleton pulse, diashow cross-fades, sheet/FAB slides) to a
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="text-scale" content="scale" />
|
||||
<!-- Light/dark theme-color so the browser chrome matches the active theme. -->
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#030712" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#faf9f7" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#100f0f" media="(prefers-color-scheme: dark)" />
|
||||
<!-- Installable PWA keepsake: manifest + Apple home-screen metadata. -->
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
|
||||
@@ -61,31 +61,29 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
background: #f9fafb;
|
||||
background: #faf9f7;
|
||||
}
|
||||
html.dark #app-boot {
|
||||
background: #030712;
|
||||
background: #100f0f;
|
||||
}
|
||||
.app-boot__spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 9999px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-top-color: #2563eb;
|
||||
border: 2px solid #e5e4e1;
|
||||
border-top-color: #8a6a2b;
|
||||
animation: app-boot-spin 0.6s linear infinite;
|
||||
}
|
||||
html.dark .app-boot__spinner {
|
||||
border-color: #374151;
|
||||
border-top-color: #60a5fa;
|
||||
border-color: #3b3a38;
|
||||
border-top-color: #c6a24a;
|
||||
}
|
||||
.app-boot__label {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
color: #8a6a2b;
|
||||
}
|
||||
@keyframes app-boot-spin {
|
||||
to {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<div class="relative -translate-y-3">
|
||||
<button
|
||||
onclick={() => ($uploadSheetOpen = true)}
|
||||
class="relative flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition active:scale-95 hover:bg-blue-700"
|
||||
class="relative flex h-14 w-14 items-center justify-center rounded-full border-2 border-primary-500 bg-primary-100 text-primary-700 shadow-lg transition active:scale-95 hover:bg-primary-200 dark:border-primary-500 dark:bg-primary-950/60 dark:text-primary-300"
|
||||
aria-label="Hochladen"
|
||||
>
|
||||
<!-- Camera + plus icon -->
|
||||
|
||||
@@ -80,10 +80,7 @@
|
||||
onclick={handleConfirm}
|
||||
disabled={busy}
|
||||
data-testid="confirm-sheet-confirm"
|
||||
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone ===
|
||||
'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400'}"
|
||||
class="btn btn-block mb-3 {tone === 'danger' ? 'btn-danger' : 'btn-primary'}"
|
||||
>
|
||||
{#if busy}
|
||||
<span
|
||||
@@ -98,7 +95,7 @@
|
||||
onclick={onCancel}
|
||||
disabled={busy}
|
||||
data-testid="confirm-sheet-cancel"
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
|
||||
class="btn btn-secondary btn-block"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
|
||||
@@ -147,11 +147,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="mt-2 w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<button type="button" onclick={onClose} class="btn btn-secondary btn-block mt-2">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -66,170 +66,168 @@
|
||||
<!-- Off-screen cards are removed from the DOM entirely by the parent VirtualFeed
|
||||
window-virtualizer, so this card no longer needs content-visibility — and must
|
||||
not use it, since the virtualizer measures each card's real rendered height. -->
|
||||
<article
|
||||
class="bg-white dark:bg-gray-900"
|
||||
use:longpress={{ duration: 500 }}
|
||||
onlongpress={openContext}
|
||||
>
|
||||
<!-- Uploader row -->
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
||||
<article class="px-3 py-2" use:longpress={{ duration: 500 }} onlongpress={openContext}>
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||||
>
|
||||
<!-- Uploader row -->
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
||||
{avatarPalette(upload.uploader_name)}"
|
||||
>
|
||||
{initials(upload.uploader_name)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{upload.uploader_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
||||
>
|
||||
{initials(upload.uploader_name)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{upload.uploader_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if oncontextmenu}
|
||||
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
openContext();
|
||||
}}
|
||||
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
|
||||
aria-label="Mehr Aktionen"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
|
||||
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="2"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if oncontextmenu}
|
||||
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
openContext();
|
||||
}}
|
||||
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
|
||||
aria-label="Mehr Aktionen"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
|
||||
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="2"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Media -->
|
||||
<button
|
||||
class="relative block w-full"
|
||||
use:doubletap
|
||||
onsingletap={() => onselect(upload)}
|
||||
ondoubletap={handleDoubleTap}
|
||||
onclick={(e) => {
|
||||
if (e.detail === 0) onselect(upload);
|
||||
}}
|
||||
aria-label="Bild vergrößern"
|
||||
>
|
||||
<HeartBurst active={heartBurst} />
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<div class="relative aspect-video w-full bg-gray-900">
|
||||
{#if upload.thumbnail_url || upload.preview_url}
|
||||
<!-- Media -->
|
||||
<button
|
||||
class="relative block w-full"
|
||||
use:doubletap
|
||||
onsingletap={() => onselect(upload)}
|
||||
ondoubletap={handleDoubleTap}
|
||||
onclick={(e) => {
|
||||
if (e.detail === 0) onselect(upload);
|
||||
}}
|
||||
aria-label="Bild vergrößern"
|
||||
>
|
||||
<HeartBurst active={heartBurst} />
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<div class="relative aspect-video w-full bg-gray-900">
|
||||
{#if upload.thumbnail_url || upload.preview_url}
|
||||
<img
|
||||
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
||||
alt=""
|
||||
class="h-full w-full object-cover opacity-80"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span
|
||||
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
|
||||
>
|
||||
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if mediaSrc}
|
||||
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
||||
to height 0 and reflow as images stream in. The uncropped original is one
|
||||
tap away in the lightbox. -->
|
||||
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
||||
<img
|
||||
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
class="h-full w-full object-cover opacity-80"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span
|
||||
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
|
||||
>
|
||||
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if mediaSrc}
|
||||
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
||||
to height 0 and reflow as images stream in. The uncropped original is one
|
||||
tap away in the lightbox. -->
|
||||
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
||||
<img
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
||||
{:else}
|
||||
<div
|
||||
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="h-12 w-12 text-gray-300 dark:text-gray-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Actions row -->
|
||||
<div class="flex items-center gap-4 px-4 py-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
vibrate(10);
|
||||
onlike(upload.id);
|
||||
}}
|
||||
aria-pressed={upload.liked_by_me}
|
||||
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
|
||||
class="flex items-center gap-1.5 text-sm font-medium transition-colors
|
||||
{upload.liked_by_me
|
||||
? 'text-red-500 dark:text-red-400'
|
||||
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
|
||||
>
|
||||
<svg
|
||||
class="h-12 w-12 text-gray-300 dark:text-gray-600"
|
||||
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Actions row -->
|
||||
<div class="flex items-center gap-4 px-4 py-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
vibrate(10);
|
||||
onlike(upload.id);
|
||||
}}
|
||||
aria-pressed={upload.liked_by_me}
|
||||
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
|
||||
class="flex items-center gap-1.5 text-sm font-medium transition-colors
|
||||
{upload.liked_by_me
|
||||
? 'text-red-500 dark:text-red-400'
|
||||
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
{upload.like_count}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.like_count}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{#if isOwn}
|
||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{#if isOwn}
|
||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Caption -->
|
||||
{#if upload.caption}
|
||||
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
|
||||
{upload.caption}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Caption -->
|
||||
{#if upload.caption}
|
||||
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
|
||||
{upload.caption}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="border-b border-gray-100 dark:border-gray-800"></div>
|
||||
</article>
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
<button
|
||||
onclick={() => onselect(null)}
|
||||
aria-pressed={selected === null}
|
||||
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
|
||||
null
|
||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
|
||||
class="chip inline-flex min-h-11 shrink-0 items-center {selected === null
|
||||
? 'chip-active'
|
||||
: 'chip-inactive'}"
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
@@ -29,10 +28,9 @@
|
||||
<button
|
||||
onclick={() => onselect(h.tag)}
|
||||
aria-pressed={selected === h.tag}
|
||||
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
|
||||
h.tag
|
||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
|
||||
class="chip inline-flex min-h-11 shrink-0 items-center {selected === h.tag
|
||||
? 'chip-active'
|
||||
: 'chip-inactive'}"
|
||||
>
|
||||
#{h.tag}
|
||||
<span class="ml-1 text-xs opacity-70">{h.count}</span>
|
||||
|
||||
@@ -277,12 +277,12 @@
|
||||
bind:value={newComment}
|
||||
placeholder="Kommentar schreiben..."
|
||||
maxlength={COMMENT_MAX}
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
class="input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newComment.trim()}
|
||||
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 active:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
|
||||
@@ -49,9 +49,7 @@
|
||||
use:focusTrap={{ onclose: onClose }}
|
||||
use:scrollLock
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
|
||||
>
|
||||
<div class="card pointer-events-auto w-full max-w-sm p-6 shadow-xl">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,8 +130,17 @@
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mb-6 text-center">
|
||||
<div class="mb-3 text-5xl">{currentStep.icon}</div>
|
||||
<h2 id="onboarding-title" class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<div class="mb-4 flex justify-center">
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-50 text-3xl dark:bg-primary-900/30"
|
||||
>
|
||||
{currentStep.icon}
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
id="onboarding-title"
|
||||
class="mb-2 text-xl font-semibold text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
|
||||
@@ -193,10 +202,7 @@
|
||||
>
|
||||
Überspringen
|
||||
</button>
|
||||
<button
|
||||
onclick={next}
|
||||
class="flex-1 rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
>
|
||||
<button onclick={next} class="btn btn-primary flex-1">
|
||||
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -167,12 +167,7 @@
|
||||
liken und kommentieren.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={close}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<button onclick={close} class="btn btn-secondary btn-block"> Schließen </button>
|
||||
{:else}
|
||||
<!-- Gallery option -->
|
||||
<button
|
||||
@@ -236,12 +231,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
onclick={close}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onclick={close} class="btn btn-secondary btn-block"> Abbrechen </button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
120
frontend/src/lib/styles/components.css
Normal file
120
frontend/src/lib/styles/components.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* EventSnap component layer — the shared, "define-once" building blocks.
|
||||
*
|
||||
* Colours/type/spacing tokens live in `tailwind-theme.css`; this file turns the
|
||||
* *recurring UI patterns* (buttons, inputs, cards, badges, chips, sheets) into
|
||||
* named classes so every page consumes the same look. Change a button here and
|
||||
* it changes everywhere.
|
||||
*
|
||||
* These live in the `components` layer, which Tailwind emits BEFORE `utilities`
|
||||
* — so a one-off inline utility (e.g. `rounded-lg`, `w-full`) on an element
|
||||
* still overrides the component class. Reach for a component class first; drop
|
||||
* to inline utilities only for genuinely one-off tweaks.
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@layer components {
|
||||
/* ── Buttons ──────────────────────────────────────────────────────────
|
||||
* Base + intent modifier (+ optional size). e.g. `class="btn btn-primary"`,
|
||||
* `class="btn btn-secondary btn-sm"`, `class="btn btn-primary btn-block"`. */
|
||||
.btn {
|
||||
@apply inline-flex select-none items-center justify-center gap-2 rounded-xl border border-transparent px-4 py-3 text-sm font-semibold transition;
|
||||
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40;
|
||||
@apply active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
}
|
||||
.btn-lg {
|
||||
@apply px-5 py-3.5 text-base;
|
||||
}
|
||||
.btn-sm {
|
||||
@apply rounded-lg px-3 py-2;
|
||||
}
|
||||
.btn-block {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Primary = muted champagne fill + gold border + gold text (an elegant,
|
||||
* outlined "signal" rather than an aggressive solid gold block). */
|
||||
.btn-primary {
|
||||
@apply border-primary-400 bg-primary-100 text-primary-800 shadow-sm hover:border-primary-500 hover:bg-primary-200;
|
||||
@apply disabled:border-gray-200 disabled:bg-gray-100 disabled:text-gray-400 disabled:shadow-none;
|
||||
@apply dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200 dark:hover:border-primary-500 dark:hover:bg-primary-900/40;
|
||||
}
|
||||
.btn-secondary {
|
||||
@apply border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50;
|
||||
@apply dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700;
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply text-gray-600 hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50;
|
||||
@apply dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100;
|
||||
}
|
||||
.btn-danger {
|
||||
@apply bg-red-600 text-white shadow-sm hover:bg-red-700 disabled:opacity-50;
|
||||
@apply dark:bg-red-600 dark:hover:bg-red-500;
|
||||
}
|
||||
|
||||
/* ── Form inputs ──────────────────────────────────────────────────────
|
||||
* Covers text/number inputs and textareas. */
|
||||
.input {
|
||||
@apply w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 placeholder-gray-400 transition;
|
||||
@apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/25;
|
||||
@apply dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500;
|
||||
}
|
||||
|
||||
/* ── Surfaces ─────────────────────────────────────────────────────────
|
||||
* `card` — the standard raised surface (page → card → surface-muted is the
|
||||
* three-level elevation ramp).
|
||||
* `surface-muted` — a nested/inset panel inside a card. */
|
||||
.card {
|
||||
@apply rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900;
|
||||
}
|
||||
.surface-muted {
|
||||
@apply rounded-xl bg-gray-50 dark:bg-gray-800/60;
|
||||
}
|
||||
|
||||
/* ── Badges (status / role pills) ─────────────────────────────────────
|
||||
* `class="badge badge-gold"` etc. A leading dot is optional in markup. */
|
||||
.badge {
|
||||
@apply inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold;
|
||||
}
|
||||
.badge-primary {
|
||||
@apply bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-200;
|
||||
}
|
||||
.badge-gold {
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200;
|
||||
}
|
||||
.badge-gray {
|
||||
@apply bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300;
|
||||
}
|
||||
.badge-success {
|
||||
@apply bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300;
|
||||
}
|
||||
.badge-danger {
|
||||
@apply bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300;
|
||||
}
|
||||
|
||||
/* ── Chips (filter / toggle) ──────────────────────────────────────────
|
||||
* `class="chip chip-active"` / `class="chip chip-inactive"`. */
|
||||
.chip {
|
||||
@apply rounded-full border border-transparent px-4 py-2 text-sm font-medium transition;
|
||||
}
|
||||
.chip-inactive {
|
||||
@apply bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
|
||||
}
|
||||
/* Active = gold outline + soft champagne fill (matches .btn-primary). */
|
||||
.chip-active {
|
||||
@apply border-primary-400 bg-primary-100 text-primary-800 dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200;
|
||||
}
|
||||
|
||||
/* ── Bottom-sheet surface ─────────────────────────────────────────────
|
||||
* The rounded top panel shared by upload / context / confirm sheets. */
|
||||
.sheet {
|
||||
@apply rounded-t-3xl bg-white p-6 shadow-2xl dark:bg-gray-900 sm:rounded-2xl;
|
||||
}
|
||||
|
||||
/* ── Page scaffold + section heading ──────────────────────────────────*/
|
||||
.page {
|
||||
@apply min-h-screen bg-gray-50 dark:bg-gray-950;
|
||||
}
|
||||
.section-title {
|
||||
@apply font-display text-lg font-semibold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
}
|
||||
@@ -163,9 +163,7 @@
|
||||
|
||||
<div class="mx-auto max-w-lg space-y-3 p-4">
|
||||
<!-- Profile card -->
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-xl font-bold
|
||||
@@ -195,9 +193,7 @@
|
||||
|
||||
<!-- Dashboards section (host + admin only) -->
|
||||
{#if role === 'host' || role === 'admin'}
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||
<h2
|
||||
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
@@ -211,7 +207,7 @@
|
||||
>
|
||||
<!-- Star icon -->
|
||||
<svg
|
||||
class="h-5 w-5 text-amber-500"
|
||||
class="h-5 w-5 text-primary-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -270,10 +266,12 @@
|
||||
|
||||
<!-- PIN card -->
|
||||
<div
|
||||
class="rounded-xl border border-amber-200 bg-amber-50 p-5 dark:border-amber-800/60 dark:bg-amber-950/30"
|
||||
class="rounded-xl border border-primary-200 bg-primary-50 p-5 dark:border-primary-800/60 dark:bg-primary-950/30"
|
||||
>
|
||||
<h2 class="mb-1 font-semibold text-amber-900 dark:text-amber-200">Wiederherstellungs-PIN</h2>
|
||||
<p class="mb-3 text-sm text-amber-700 dark:text-amber-300/90">
|
||||
<h2 class="mb-1 font-semibold text-primary-800 dark:text-primary-200">
|
||||
Wiederherstellungs-PIN
|
||||
</h2>
|
||||
<p class="mb-3 text-sm text-primary-700 dark:text-primary-300/90">
|
||||
Du brauchst diesen PIN, um dein Konto auf einem anderen Gerät wiederherzustellen. Schreib
|
||||
ihn auf!
|
||||
</p>
|
||||
@@ -287,7 +285,7 @@
|
||||
>
|
||||
<button
|
||||
onclick={copyPin}
|
||||
class="inline-flex min-h-11 items-center rounded-md bg-amber-100 px-4 py-2 text-sm font-medium text-amber-800 transition hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60"
|
||||
class="inline-flex min-h-11 items-center rounded-md bg-primary-100 px-4 py-2 text-sm font-medium text-primary-800 transition hover:bg-primary-200 dark:bg-primary-900/40 dark:text-primary-200 dark:hover:bg-primary-900/60"
|
||||
>
|
||||
{pinCopied ? 'Kopiert!' : 'Kopieren'}
|
||||
</button>
|
||||
@@ -337,9 +335,7 @@
|
||||
</a>
|
||||
|
||||
<!-- Theme / Design -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Design
|
||||
@@ -394,9 +390,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Data mode -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Datennutzung
|
||||
@@ -439,9 +433,7 @@
|
||||
|
||||
<!-- Per-user quota widget -->
|
||||
{#if $quotaStore.enabled && $quotaStore.limit_bytes != null}
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<h2
|
||||
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
@@ -472,9 +464,7 @@
|
||||
|
||||
<!-- Datenschutzhinweis (preformatted, plain text) -->
|
||||
{#if $privacyNote.trim()}
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<h2
|
||||
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
@@ -486,9 +476,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- Konto section -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Konto
|
||||
@@ -611,16 +599,10 @@
|
||||
<p class="mb-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Original-Dateien können deutlich mehr Datenvolumen verbrauchen. Am besten im WLAN aktivieren.
|
||||
</p>
|
||||
<button
|
||||
onclick={confirmOriginalMode}
|
||||
class="mb-3 w-full rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white transition hover:bg-blue-700 active:bg-blue-700"
|
||||
>
|
||||
<button onclick={confirmOriginalMode} class="btn btn-primary btn-block mb-3">
|
||||
Aktivieren
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (dataModeWarningOpen = false)}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
|
||||
>
|
||||
<button onclick={() => (dataModeWarningOpen = false)} class="btn btn-secondary btn-block">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -453,10 +453,7 @@
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (pinModal = null)}
|
||||
class="w-full rounded-lg bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
>
|
||||
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block btn-sm">
|
||||
Schließen
|
||||
</button>
|
||||
{/if}
|
||||
@@ -473,16 +470,10 @@
|
||||
Diashow und Export, und die Sitzung wird beendet. Rückgängig machbar über „Entsperren“.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => (banTarget = null)}
|
||||
class="flex-1 rounded-lg border border-gray-300 py-2 text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
|
||||
<button onclick={() => (banTarget = null)} class="btn btn-secondary btn-sm flex-1"
|
||||
>Abbrechen</button
|
||||
>
|
||||
<button
|
||||
onclick={confirmBan}
|
||||
disabled={banSubmitting}
|
||||
class="flex-1 rounded-lg bg-red-600 py-2 text-sm font-medium text-white hover:bg-red-700 active:bg-red-700 disabled:opacity-50 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400"
|
||||
>
|
||||
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger btn-sm flex-1">
|
||||
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -543,33 +534,25 @@
|
||||
<div class="space-y-3">
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div
|
||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-4 text-center">
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{stats.user_count}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Gäste</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-4 text-center">
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{stats.upload_count}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-4 text-center">
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{stats.comment_count}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-4 text-center">
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{diskPct(stats)} %
|
||||
</p>
|
||||
@@ -577,9 +560,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Disk bar -->
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<div
|
||||
class="mb-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
@@ -611,9 +592,7 @@
|
||||
{:else if activeTab === 'config'}
|
||||
<div class="relative space-y-3 pb-20">
|
||||
{#each CONFIG_GROUPS as group (group.title)}
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||
<h3
|
||||
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
@@ -652,7 +631,7 @@
|
||||
id={field.key}
|
||||
rows="6"
|
||||
bind:value={configDraft[field.key]}
|
||||
class="w-full resize-none rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||
class="input resize-none font-mono text-sm"
|
||||
></textarea>
|
||||
{#if field.hint}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
||||
@@ -670,7 +649,7 @@
|
||||
min="0"
|
||||
inputmode="decimal"
|
||||
bind:value={configDraft[field.key]}
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||
class="input text-sm"
|
||||
/>
|
||||
{#if field.hint}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
||||
@@ -686,11 +665,7 @@
|
||||
<div
|
||||
class="sticky bottom-0 -mx-4 border-t border-gray-100 bg-white px-5 py-3 dark:border-gray-800 dark:bg-gray-900 sm:mx-0 sm:rounded-b-xl"
|
||||
>
|
||||
<button
|
||||
onclick={saveConfig}
|
||||
disabled={saving}
|
||||
class="w-full rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
>
|
||||
<button onclick={saveConfig} disabled={saving} class="btn btn-primary btn-block">
|
||||
{saving ? 'Wird gespeichert…' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -700,9 +675,7 @@
|
||||
{:else if activeTab === 'export'}
|
||||
<div class="space-y-3">
|
||||
<!-- Gallery release -->
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-gray-100">Galerie</h3>
|
||||
<button
|
||||
onclick={() =>
|
||||
@@ -714,16 +687,14 @@
|
||||
tone: 'danger',
|
||||
run: releaseGallery
|
||||
})}
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Galerie freigeben
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Export jobs -->
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Export-Jobs</h3>
|
||||
<button
|
||||
@@ -781,9 +752,7 @@
|
||||
|
||||
<!-- ── Nutzer tab ───────────────────────────────────────────────── -->
|
||||
{:else if activeTab === 'users'}
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Search -->
|
||||
<div class="p-4">
|
||||
<div
|
||||
@@ -824,21 +793,12 @@
|
||||
>{user.display_name}</span
|
||||
>
|
||||
{#if user.role === 'host'}
|
||||
<span
|
||||
class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
|
||||
>Host</span
|
||||
>
|
||||
<span class="badge badge-primary">Host</span>
|
||||
{:else if user.role === 'admin'}
|
||||
<span
|
||||
class="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200"
|
||||
>Admin</span
|
||||
>
|
||||
<span class="badge badge-gold">Admin</span>
|
||||
{/if}
|
||||
{#if user.is_banned}
|
||||
<span
|
||||
class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200"
|
||||
>Gesperrt</span
|
||||
>
|
||||
<span class="badge badge-danger">Gesperrt</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
placeholder="Passwort"
|
||||
autocomplete="current-password"
|
||||
data-testid="admin-password-input"
|
||||
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
class="input mb-3 text-lg"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
@@ -71,7 +71,7 @@
|
||||
type="submit"
|
||||
disabled={loading || !password}
|
||||
data-testid="admin-login-submit"
|
||||
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
>
|
||||
{loading ? 'Wird angemeldet…' : 'Anmelden'}
|
||||
</button>
|
||||
|
||||
@@ -437,12 +437,7 @@
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (pinModal = null)}
|
||||
class="w-full rounded-lg bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block"> Schließen </button>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
@@ -458,17 +453,10 @@
|
||||
ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar über „Entsperren“.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => (banTarget = null)}
|
||||
class="flex-1 rounded-lg border border-gray-300 py-2 text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
|
||||
>
|
||||
<button onclick={() => (banTarget = null)} class="btn btn-secondary flex-1">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onclick={confirmBan}
|
||||
disabled={banSubmitting}
|
||||
class="flex-1 rounded-lg bg-red-600 py-2 text-sm font-medium text-white hover:bg-red-700 active:bg-red-700 disabled:opacity-50 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400"
|
||||
>
|
||||
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger flex-1">
|
||||
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -551,15 +539,13 @@
|
||||
{/if}
|
||||
|
||||
<!-- ── Statistiken ─────────────────────────────────────────────── -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<button
|
||||
onclick={() => (statsOpen = !statsOpen)}
|
||||
aria-expanded={statsOpen}
|
||||
class="flex w-full items-center justify-between px-5 py-4"
|
||||
>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Statistiken</h2>
|
||||
<h2 class="section-title">Statistiken</h2>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {statsOpen
|
||||
? 'rotate-180'
|
||||
@@ -580,50 +566,50 @@
|
||||
<div
|
||||
class="grid grid-cols-2 gap-3 border-t border-gray-100 p-4 dark:border-gray-700 sm:grid-cols-4"
|
||||
>
|
||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
||||
<div class="surface-muted p-4 text-center">
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{users.length}</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Teilnehmer</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
||||
<div class="surface-muted p-4 text-center">
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{users.reduce((s, u) => s + u.upload_count, 0)}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
||||
<p
|
||||
class="text-2xl font-bold {event.uploads_locked
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-green-600 dark:text-green-400'}"
|
||||
<div class="surface-muted flex flex-col items-center p-4 text-center">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.uploads_locked
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300'
|
||||
: 'bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300'}"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
|
||||
{event.uploads_locked ? 'Gesperrt' : 'Offen'}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||
</span>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
||||
<p
|
||||
class="text-2xl font-bold {event.export_released
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-400 dark:text-gray-500'}"
|
||||
<div class="surface-muted flex flex-col items-center p-4 text-center">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.export_released
|
||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/50 dark:text-purple-200'
|
||||
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300'}"
|
||||
>
|
||||
{event.export_released ? 'Ja' : 'Nein'}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Freigegeben</p>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
|
||||
{event.export_released ? 'Freigegeben' : 'Noch nicht'}
|
||||
</span>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Galerie</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Event-Einstellungen ─────────────────────────────────────── -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<button
|
||||
onclick={() => (settingsOpen = !settingsOpen)}
|
||||
aria-expanded={settingsOpen}
|
||||
class="flex w-full items-center justify-between px-5 py-4"
|
||||
>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Event-Einstellungen</h2>
|
||||
<h2 class="section-title">Event-Einstellungen</h2>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {settingsOpen
|
||||
? 'rotate-180'
|
||||
@@ -644,10 +630,10 @@
|
||||
<div class="flex flex-wrap gap-3 border-t border-gray-100 p-5 dark:border-gray-700">
|
||||
<button
|
||||
onclick={toggleEventLock}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition
|
||||
class="rounded-lg border px-4 py-2 text-sm font-medium transition
|
||||
{event.uploads_locked
|
||||
? 'bg-green-600 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-400'
|
||||
: 'bg-amber-500 text-white hover:bg-amber-600 dark:bg-amber-500 dark:hover:bg-amber-400'}"
|
||||
? 'border-green-600 bg-green-600 text-white hover:bg-green-700 dark:border-green-500 dark:bg-green-500 dark:hover:bg-green-400'
|
||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
{event.uploads_locked ? 'Uploads wieder öffnen' : 'Uploads sperren'}
|
||||
</button>
|
||||
@@ -662,10 +648,7 @@
|
||||
run: releaseGallery
|
||||
})}
|
||||
disabled={event.export_released}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition
|
||||
{event.export_released
|
||||
? 'cursor-default bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'}"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
{event.export_released ? 'Galerie bereits freigegeben' : 'Galerie freigeben'}
|
||||
</button>
|
||||
@@ -751,15 +734,13 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Nutzerverwaltung ───────────────────────────────────────── -->
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="card overflow-hidden">
|
||||
<button
|
||||
onclick={() => (usersOpen = !usersOpen)}
|
||||
aria-expanded={usersOpen}
|
||||
class="flex w-full items-center justify-between px-5 py-4"
|
||||
>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Nutzerverwaltung</h2>
|
||||
<h2 class="section-title">Nutzerverwaltung</h2>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {usersOpen
|
||||
? 'rotate-180'
|
||||
@@ -818,21 +799,12 @@
|
||||
>{user.display_name}</span
|
||||
>
|
||||
{#if user.role === 'host'}
|
||||
<span
|
||||
class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
|
||||
>Host</span
|
||||
>
|
||||
<span class="badge badge-primary">Host</span>
|
||||
{:else if user.role === 'admin'}
|
||||
<span
|
||||
class="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200"
|
||||
>Admin</span
|
||||
>
|
||||
<span class="badge badge-gold">Admin</span>
|
||||
{/if}
|
||||
{#if user.is_banned}
|
||||
<span
|
||||
class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200"
|
||||
>Gesperrt</span
|
||||
>
|
||||
<span class="badge badge-danger">Gesperrt</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -139,142 +139,184 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950">
|
||||
<div class="w-full max-w-sm">
|
||||
{#if nameTaken}
|
||||
<!-- Name-taken state: sign in with PIN or choose a different name -->
|
||||
<div
|
||||
class="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-b from-primary-50 via-gray-50 to-gray-50 px-4 py-10 dark:from-gray-900 dark:via-gray-950 dark:to-gray-950"
|
||||
>
|
||||
<!-- Soft celebratory glow behind the card — a champagne-gold and a silver wash
|
||||
for the classic warm/cool wedding tension, kept subtle. -->
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -top-20 h-72 w-72 rounded-full bg-primary-200/30 blur-3xl dark:bg-primary-900/25"
|
||||
></div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -bottom-24 right-0 h-72 w-72 rounded-full bg-gray-200/50 blur-3xl dark:bg-gray-800/40"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full max-w-sm">
|
||||
<!-- Brand -->
|
||||
<div class="mb-7 flex flex-col items-center text-center">
|
||||
<div
|
||||
class="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/60 dark:bg-amber-950/30"
|
||||
class="mb-3 flex h-16 w-16 items-center justify-center rounded-2xl bg-blue-600 text-white shadow-lg shadow-primary-600/30"
|
||||
>
|
||||
<p class="font-semibold text-amber-900 dark:text-amber-200">
|
||||
„{takenName}" ist bereits vergeben.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
|
||||
Wähle einen anderen Namen, z. B. einen Spitznamen oder füge deinen Nachnamen hinzu („{takenName}
|
||||
M." oder „{takenName} aus Berlin").
|
||||
</p>
|
||||
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2c.5 0 .95-.28 1.17-.72l.42-.85A1.5 1.5 0 0 1 10.9 3.6h2.2c.57 0 1.09.32 1.34.83l.42.85c.22.44.67.72 1.17.72h1.2A2.5 2.5 0 0 1 20 8.5v7A2.5 2.5 0 0 1 17.5 18h-11A2.5 2.5 0 0 1 4 15.5v-7Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.6" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Falls du das bist, melde dich mit deinem PIN an:
|
||||
</p>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleInlineRecover();
|
||||
}}
|
||||
<span
|
||||
class="font-display text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
|
||||
>EventSnap</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={recoveryPin}
|
||||
oninput={onRecoveryPinInput}
|
||||
placeholder="4-stelliger PIN"
|
||||
maxlength={4}
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
data-testid="recovery-pin-input"
|
||||
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-center text-2xl font-mono tracking-widest text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if recoveryError}
|
||||
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
|
||||
{recoveryError}
|
||||
<!-- Invitation card -->
|
||||
<div class="card p-6 shadow-xl">
|
||||
{#if nameTaken}
|
||||
<!-- Name-taken state: sign in with PIN or choose a different name -->
|
||||
<div
|
||||
class="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/60 dark:bg-amber-950/30"
|
||||
>
|
||||
<p class="font-semibold text-amber-900 dark:text-amber-200">
|
||||
„{takenName}" ist bereits vergeben.
|
||||
</p>
|
||||
{/if}
|
||||
<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
|
||||
Wähle einen anderen Namen, z. B. einen Spitznamen oder füge deinen Nachnamen hinzu
|
||||
(„{takenName}
|
||||
M." oder „{takenName} aus Berlin").
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Falls du das bist, melde dich mit deinem PIN an:
|
||||
</p>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleInlineRecover();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={recoveryPin}
|
||||
oninput={onRecoveryPinInput}
|
||||
placeholder="4-stelliger PIN"
|
||||
maxlength={4}
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
data-testid="recovery-pin-input"
|
||||
class="input mb-3 text-center font-mono text-2xl tracking-widest"
|
||||
/>
|
||||
|
||||
{#if recoveryError}
|
||||
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
|
||||
{recoveryError}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={recoveryLoading || recoveryPin.length < 4}
|
||||
data-testid="recovery-submit"
|
||||
class="btn btn-primary btn-block mb-3"
|
||||
>
|
||||
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={recoveryLoading || recoveryPin.length < 4}
|
||||
data-testid="recovery-submit"
|
||||
class="mb-3 w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
onclick={tryDifferentName}
|
||||
data-testid="try-different-name"
|
||||
class="btn btn-secondary btn-block"
|
||||
>
|
||||
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
|
||||
Anderen Namen wählen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button
|
||||
onclick={tryDifferentName}
|
||||
data-testid="try-different-name"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Anderen Namen wählen
|
||||
</button>
|
||||
|
||||
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
|
||||
{#if pinRequestSent}
|
||||
<p
|
||||
class="mt-3 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700 dark:bg-green-950/30 dark:text-green-300"
|
||||
>
|
||||
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
|
||||
der neuen PIN anmelden.
|
||||
</p>
|
||||
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
|
||||
{#if pinRequestSent}
|
||||
<p
|
||||
class="mt-3 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700 dark:bg-green-950/30 dark:text-green-300"
|
||||
>
|
||||
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
|
||||
der neuen PIN anmelden.
|
||||
</p>
|
||||
{:else}
|
||||
<button
|
||||
onclick={requestPinReset}
|
||||
disabled={pinRequestLoading}
|
||||
data-testid="request-pin-reset"
|
||||
class="mt-3 w-full text-center text-sm text-blue-600 underline decoration-dotted underline-offset-2 hover:text-blue-700 disabled:opacity-50 dark:text-blue-400"
|
||||
>
|
||||
{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
onclick={requestPinReset}
|
||||
disabled={pinRequestLoading}
|
||||
data-testid="request-pin-reset"
|
||||
class="mt-3 w-full text-center text-sm text-blue-600 underline decoration-dotted underline-offset-2 hover:text-blue-700 disabled:opacity-50 dark:text-blue-400"
|
||||
<!-- Normal join form -->
|
||||
<p class="mb-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Willkommen bei
|
||||
</p>
|
||||
{#if eventName}
|
||||
<h1
|
||||
class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100"
|
||||
data-testid="join-event-name"
|
||||
>
|
||||
{eventName}
|
||||
</h1>
|
||||
{:else}
|
||||
<h1 class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
dem Event
|
||||
</h1>
|
||||
{/if}
|
||||
<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
|
||||
Gib deinen Namen ein, um Fotos zu teilen und die Galerie zu sehen.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleJoin();
|
||||
}}
|
||||
>
|
||||
{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Normal join form -->
|
||||
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
Willkommen!
|
||||
</h1>
|
||||
{#if eventName}
|
||||
<p
|
||||
class="mb-1 text-center text-lg font-semibold text-blue-600 dark:text-blue-400"
|
||||
data-testid="join-event-name"
|
||||
>
|
||||
{eventName}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={displayName}
|
||||
placeholder="Dein Name"
|
||||
maxlength={50}
|
||||
data-testid="join-name-input"
|
||||
class="input mb-3 text-lg"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !displayName.trim()}
|
||||
data-testid="join-submit"
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
>
|
||||
{loading ? 'Wird geladen...' : 'Beitreten'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-center text-sm">
|
||||
<a
|
||||
href="/recover"
|
||||
data-testid="link-to-recover"
|
||||
class="text-blue-600 hover:underline dark:text-blue-400"
|
||||
>Ich habe bereits einen Account</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
|
||||
Gib deinen Namen ein, um dem Event beizutreten.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleJoin();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={displayName}
|
||||
placeholder="Dein Name"
|
||||
maxlength={50}
|
||||
data-testid="join-name-input"
|
||||
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !displayName.trim()}
|
||||
data-testid="join-submit"
|
||||
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
>
|
||||
{loading ? 'Wird geladen...' : 'Beitreten'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-center text-sm">
|
||||
<a
|
||||
href="/recover"
|
||||
data-testid="link-to-recover"
|
||||
class="text-blue-600 hover:underline dark:text-blue-400">Ich habe bereits einen Account</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +326,7 @@
|
||||
data-testid="pin-modal"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg dark:bg-gray-900"
|
||||
class="card w-full max-w-sm p-6 shadow-xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="pin-modal-title"
|
||||
@@ -298,18 +340,12 @@
|
||||
wiederherzustellen.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="mb-4 flex items-center justify-center gap-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800"
|
||||
>
|
||||
<div class="surface-muted mb-4 flex items-center justify-center gap-3 p-4">
|
||||
<span
|
||||
class="text-4xl font-mono font-bold tracking-widest text-gray-900 dark:text-gray-100"
|
||||
class="font-mono text-4xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
|
||||
data-testid="pin-display">{pin}</span
|
||||
>
|
||||
<button
|
||||
onclick={copyPin}
|
||||
data-testid="pin-copy"
|
||||
class="min-h-11 min-w-11 rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-gray-600"
|
||||
>
|
||||
<button onclick={copyPin} data-testid="pin-copy" class="btn btn-secondary btn-sm min-h-11">
|
||||
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -317,14 +353,14 @@
|
||||
<button
|
||||
onclick={goToFeed}
|
||||
data-testid="continue-to-feed"
|
||||
class="mb-2 w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
class="btn btn-primary btn-block mb-2"
|
||||
>
|
||||
Weiter zur Galerie
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={closePinModal}
|
||||
class="w-full rounded-lg py-2 text-sm text-gray-500 hover:text-gray-700 active:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 dark:active:text-gray-200"
|
||||
class="btn btn-ghost btn-block text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
placeholder="Dein Name"
|
||||
maxlength={50}
|
||||
data-testid="recover-name-input"
|
||||
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
class="input mb-3 text-lg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
@@ -143,7 +143,7 @@
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
data-testid="recover-pin-input"
|
||||
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-center text-2xl font-mono tracking-widest text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
class="input mb-3 text-center text-2xl font-mono tracking-widest"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
@@ -156,7 +156,7 @@
|
||||
type="submit"
|
||||
disabled={loading || !displayName.trim() || pin.length < 4}
|
||||
data-testid="recover-submit"
|
||||
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
>
|
||||
{loading ? 'Wird geladen...' : 'Wiederherstellen'}
|
||||
</button>
|
||||
|
||||
@@ -151,8 +151,7 @@
|
||||
onclick={handleSubmit}
|
||||
disabled={stagedFiles.length === 0 || submitting}
|
||||
data-testid="upload-submit-header"
|
||||
class="rounded-lg bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white transition
|
||||
hover:bg-blue-700 disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
{submitting ? 'Wird hochgeladen…' : 'Hochladen'}
|
||||
</button>
|
||||
@@ -199,31 +198,30 @@
|
||||
{:else}
|
||||
<!-- No files: prompt to go back and pick some -->
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<svg
|
||||
class="h-16 w-16 text-gray-200 dark:text-gray-700"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
<div
|
||||
class="flex h-20 w-20 items-center justify-center rounded-2xl bg-primary-50 dark:bg-primary-900/20"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M9 9.75h.008v.008H9V9.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="h-9 w-9 text-primary-500 dark:text-primary-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M9 9.75h.008v.008H9V9.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">
|
||||
Geh zurück und tippe auf den Plus-Button.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={cancel}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<button onclick={cancel} class="btn btn-secondary btn-sm"> Zurück </button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -236,9 +234,7 @@
|
||||
data-testid="upload-caption"
|
||||
placeholder="Beschreibung hinzufügen… (#hashtags möglich)"
|
||||
rows="4"
|
||||
class="w-full resize-none rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-900
|
||||
placeholder-gray-400 focus:border-blue-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-blue-200
|
||||
dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:bg-gray-800"
|
||||
class="input resize-none text-sm"
|
||||
></textarea>
|
||||
<div class="mt-1 text-xs text-gray-500 text-right dark:text-gray-400">
|
||||
{caption.length} / {MAX_CAPTION_LENGTH}
|
||||
@@ -316,8 +312,7 @@
|
||||
onclick={handleSubmit}
|
||||
disabled={stagedFiles.length === 0 || submitting}
|
||||
data-testid="upload-submit"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-xl bg-blue-600 py-3.5 text-sm font-semibold
|
||||
text-white transition hover:bg-blue-700 active:scale-[0.98] disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
class="btn btn-primary btn-block"
|
||||
>
|
||||
{#if submitting}
|
||||
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
*
|
||||
* Tailwind v4 reads `@theme` blocks to populate utility classes; everything declared
|
||||
* here becomes a `bg-primary`, `text-accent`, `rounded-card`, etc.
|
||||
*
|
||||
* ── DESIGN LANGUAGE: "Warm & celebratory" ──────────────────────────────────
|
||||
* The whole product exists to relive an event together, so the palette is a
|
||||
* wedding-invitation one: warm ivory paper, warm-charcoal ink, a dusty-rose
|
||||
* brand and champagne-gold accent. Headings are set in Fraunces (an elegant
|
||||
* display serif); body copy in Inter. Both are self-hosted (see @font-face
|
||||
* below) because the CSP is `font-src: 'self'` and the keepsake runs offline.
|
||||
*
|
||||
* The bulk of the app was authored in raw Tailwind classes (`bg-gray-900`,
|
||||
* `text-blue-600`, …). Rather than touch 900+ class usages, we REMAP the
|
||||
* underlying Tailwind ramps here so the warmth cascades everywhere:
|
||||
* gray → warm "sand" neutrals
|
||||
* blue → dusty-rose brand
|
||||
* purple → champagne gold (hashtag chips / accents)
|
||||
* Semantic red / green / amber keep their meaning and stay close to default.
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
@@ -15,32 +30,108 @@
|
||||
* `:where(...)` keeps the specificity low so existing utilities still override. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* ── Self-hosted fonts (variable woff2, latin subset covers German umlauts) ── */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Inter.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Fraunces.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@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;
|
||||
/* ── Type ─────────────────────────────────────────────────────────────── */
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-display: 'Fraunces', ui-serif, Georgia, 'Times New Roman', 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 */
|
||||
/* ── Brand: champagne / antique gold — the "signal" colour (remaps `blue-*`)
|
||||
* Wedding research: treat gold as the signal (brand, primary CTA, active
|
||||
* state, key highlights); let silver/greys carry the system. `600` is a deep
|
||||
* antique gold so white button text clears WCAG AA. */
|
||||
--color-blue-50: #faf6ea;
|
||||
--color-blue-100: #f3ead0;
|
||||
--color-blue-200: #e8d6a3;
|
||||
--color-blue-300: #d8bd73;
|
||||
--color-blue-400: #c6a24a;
|
||||
--color-blue-500: #ab8433;
|
||||
--color-blue-600: #8a6a2b; /* primary buttons / FAB / active tab — white text passes AA */
|
||||
--color-blue-700: #6f5523;
|
||||
--color-blue-800: #59441e;
|
||||
--color-blue-900: #493819;
|
||||
--color-blue-950: #29200d;
|
||||
|
||||
/* Semantic aliases so new work can use `bg-primary` intentionally. Full ramp
|
||||
* (mirrors the gold `blue-*` values) so `@apply` in the component layer can
|
||||
* reference any shade — a partial ramp hard-fails `@apply bg-primary-900`. */
|
||||
--color-primary-50: #faf6ea;
|
||||
--color-primary-100: #f3ead0;
|
||||
--color-primary-200: #e8d6a3;
|
||||
--color-primary-300: #d8bd73;
|
||||
--color-primary-400: #c6a24a;
|
||||
--color-primary-500: #ab8433;
|
||||
--color-primary-600: #8a6a2b;
|
||||
--color-primary-700: #6f5523;
|
||||
--color-primary-800: #59441e;
|
||||
--color-primary-900: #493819;
|
||||
--color-primary-950: #29200d;
|
||||
|
||||
/* ── Accent: same champagne gold (remaps `purple-*` / `violet-*`) so hashtag
|
||||
* chips + badges share the one signal colour rather than a competing hue. */
|
||||
--color-purple-50: #faf6ea;
|
||||
--color-purple-100: #f3ead0;
|
||||
--color-purple-200: #e8d6a3;
|
||||
--color-purple-300: #d8bd73;
|
||||
--color-purple-400: #c6a24a;
|
||||
--color-purple-500: #ab8433;
|
||||
--color-purple-600: #8a6a2b;
|
||||
--color-purple-700: #6f5523;
|
||||
--color-purple-800: #59441e;
|
||||
--color-purple-900: #493819;
|
||||
--color-violet-500: #ab8433;
|
||||
--color-violet-600: #8a6a2b;
|
||||
--color-accent-500: #ab8433;
|
||||
--color-accent-600: #8a6a2b;
|
||||
|
||||
/* ── Neutrals: pearl → silver → graphite (remaps `gray-*`). Whisper-warm
|
||||
* pearl at the light end (research: "warm pearl, not stark white"), turning
|
||||
* neutral-cool through the mids/darks so structure reads as silver, not sand. */
|
||||
--color-gray-50: #faf9f7; /* soft pearl — light page background */
|
||||
--color-gray-100: #f2f1ef; /* light silver — muted surfaces */
|
||||
--color-gray-200: #e5e4e1; /* silver borders / dividers */
|
||||
--color-gray-300: #d1cfcc;
|
||||
--color-gray-400: #a6a4a1; /* silver — placeholder / disabled icon */
|
||||
--color-gray-500: #767471; /* secondary text (AA on pearl) */
|
||||
--color-gray-600: #545350;
|
||||
--color-gray-700: #3b3a38; /* graphite — dark-mode borders */
|
||||
--color-gray-800: #262524; /* dark-mode cards */
|
||||
--color-gray-900: #1a1918; /* ink (light) / dark surface */
|
||||
--color-gray-950: #100f0f; /* darkest — dark-mode page background */
|
||||
|
||||
/* Surface aliases (kept from the original tokens for the export viewer). */
|
||||
--color-surface-0: #ffffff;
|
||||
--color-surface-50: #faf9f7;
|
||||
--color-surface-100: #f2f1ef;
|
||||
--color-surface-200: #e5e4e1;
|
||||
|
||||
/* ── Radii — softer, keepsake-y corners ───────────────────────────────── */
|
||||
--radius-card: 1rem; /* rounded-card */
|
||||
--radius-sheet: 1.5rem; /* rounded-sheet */
|
||||
|
||||
/* ── Elevation — soft, cool-neutral shadows (remaps Tailwind shadow ramp) ─ */
|
||||
--shadow-2xs: 0 1px 2px 0 rgba(28, 28, 35, 0.05);
|
||||
--shadow-xs: 0 1px 3px 0 rgba(28, 28, 35, 0.06);
|
||||
--shadow-sm: 0 2px 6px -1px rgba(28, 28, 35, 0.07), 0 1px 2px -1px rgba(28, 28, 35, 0.05);
|
||||
--shadow-md: 0 6px 16px -4px rgba(28, 28, 35, 0.09), 0 2px 6px -2px rgba(28, 28, 35, 0.06);
|
||||
--shadow-lg: 0 12px 28px -6px rgba(28, 28, 35, 0.1), 0 4px 10px -4px rgba(28, 28, 35, 0.07);
|
||||
--shadow-xl: 0 24px 48px -12px rgba(28, 28, 35, 0.16), 0 8px 16px -8px rgba(28, 28, 35, 0.09);
|
||||
}
|
||||
|
||||
/* Baseline body background + text colour so pages that haven't been re-themed yet
|
||||
@@ -48,13 +139,26 @@
|
||||
* 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 */
|
||||
background-color: #faf9f7; /* soft pearl (bg-gray-50) */
|
||||
color: #1a1918; /* ink (text-gray-900) */
|
||||
color-scheme: light;
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
html.dark {
|
||||
background-color: #030712; /* matches bg-gray-950 */
|
||||
color: #f3f4f6; /* matches text-gray-100 */
|
||||
background-color: #100f0f; /* graphite near-black (bg-gray-950) */
|
||||
color: #f2f1ef; /* light silver (text-gray-100) */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Headings wear the display serif by default. Individual headings can still
|
||||
* opt out with `font-sans`; the brand wordmark opts in via `.font-display`. */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/static/fonts/Fraunces.woff2
Normal file
BIN
frontend/static/fonts/Fraunces.woff2
Normal file
Binary file not shown.
BIN
frontend/static/fonts/Inter.woff2
Normal file
BIN
frontend/static/fonts/Inter.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user