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 './tailwind-theme.css';
|
||||||
|
@import './lib/styles/components.css';
|
||||||
|
|
||||||
/* Respect the OS "reduce motion" setting. Collapses every animation/transition
|
/* Respect the OS "reduce motion" setting. Collapses every animation/transition
|
||||||
* (HeartBurst, Skeleton pulse, diashow cross-fades, sheet/FAB slides) to a
|
* (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="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="text-scale" content="scale" />
|
<meta name="text-scale" content="scale" />
|
||||||
<!-- Light/dark theme-color so the browser chrome matches the active theme. -->
|
<!-- 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="#faf9f7" media="(prefers-color-scheme: light)" />
|
||||||
<meta name="theme-color" content="#030712" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#100f0f" media="(prefers-color-scheme: dark)" />
|
||||||
<!-- Installable PWA keepsake: manifest + Apple home-screen metadata. -->
|
<!-- Installable PWA keepsake: manifest + Apple home-screen metadata. -->
|
||||||
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
|
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
|
<link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
|
||||||
@@ -61,31 +61,29 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
background: #f9fafb;
|
background: #faf9f7;
|
||||||
}
|
}
|
||||||
html.dark #app-boot {
|
html.dark #app-boot {
|
||||||
background: #030712;
|
background: #100f0f;
|
||||||
}
|
}
|
||||||
.app-boot__spinner {
|
.app-boot__spinner {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 2px solid #d1d5db;
|
border: 2px solid #e5e4e1;
|
||||||
border-top-color: #2563eb;
|
border-top-color: #8a6a2b;
|
||||||
animation: app-boot-spin 0.6s linear infinite;
|
animation: app-boot-spin 0.6s linear infinite;
|
||||||
}
|
}
|
||||||
html.dark .app-boot__spinner {
|
html.dark .app-boot__spinner {
|
||||||
border-color: #374151;
|
border-color: #3b3a38;
|
||||||
border-top-color: #60a5fa;
|
border-top-color: #c6a24a;
|
||||||
}
|
}
|
||||||
.app-boot__label {
|
.app-boot__label {
|
||||||
font-family:
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
system-ui,
|
font-size: 1rem;
|
||||||
-apple-system,
|
font-weight: 600;
|
||||||
sans-serif;
|
letter-spacing: 0.01em;
|
||||||
font-size: 0.875rem;
|
color: #8a6a2b;
|
||||||
font-weight: 500;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
}
|
||||||
@keyframes app-boot-spin {
|
@keyframes app-boot-spin {
|
||||||
to {
|
to {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<div class="relative -translate-y-3">
|
<div class="relative -translate-y-3">
|
||||||
<button
|
<button
|
||||||
onclick={() => ($uploadSheetOpen = true)}
|
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"
|
aria-label="Hochladen"
|
||||||
>
|
>
|
||||||
<!-- Camera + plus icon -->
|
<!-- Camera + plus icon -->
|
||||||
|
|||||||
@@ -80,10 +80,7 @@
|
|||||||
onclick={handleConfirm}
|
onclick={handleConfirm}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
data-testid="confirm-sheet-confirm"
|
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 ===
|
class="btn btn-block mb-3 {tone === 'danger' ? 'btn-danger' : 'btn-primary'}"
|
||||||
'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'}"
|
|
||||||
>
|
>
|
||||||
{#if busy}
|
{#if busy}
|
||||||
<span
|
<span
|
||||||
@@ -98,7 +95,7 @@
|
|||||||
onclick={onCancel}
|
onclick={onCancel}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
data-testid="confirm-sheet-cancel"
|
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}
|
{cancelLabel}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -147,11 +147,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<button
|
<button type="button" onclick={onClose} class="btn btn-secondary btn-block mt-2">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,170 +66,168 @@
|
|||||||
<!-- Off-screen cards are removed from the DOM entirely by the parent VirtualFeed
|
<!-- 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
|
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. -->
|
not use it, since the virtualizer measures each card's real rendered height. -->
|
||||||
<article
|
<article class="px-3 py-2" use:longpress={{ duration: 500 }} onlongpress={openContext}>
|
||||||
class="bg-white dark:bg-gray-900"
|
<div
|
||||||
use:longpress={{ duration: 500 }}
|
class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||||||
onlongpress={openContext}
|
>
|
||||||
>
|
<!-- Uploader row -->
|
||||||
<!-- Uploader row -->
|
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||||
<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 min-w-0 items-center gap-3">
|
<div
|
||||||
<div
|
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
||||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
|
||||||
{avatarPalette(upload.uploader_name)}"
|
{avatarPalette(upload.uploader_name)}"
|
||||||
>
|
>
|
||||||
{initials(upload.uploader_name)}
|
{initials(upload.uploader_name)}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{upload.uploader_name}
|
{upload.uploader_name}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{#if oncontextmenu}
|
<!-- Media -->
|
||||||
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
|
<button
|
||||||
<button
|
class="relative block w-full"
|
||||||
type="button"
|
use:doubletap
|
||||||
onclick={(e) => {
|
onsingletap={() => onselect(upload)}
|
||||||
e.stopPropagation();
|
ondoubletap={handleDoubleTap}
|
||||||
openContext();
|
onclick={(e) => {
|
||||||
}}
|
if (e.detail === 0) onselect(upload);
|
||||||
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"
|
aria-label="Bild vergrößern"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
|
<HeartBurst active={heartBurst} />
|
||||||
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
|
{#if isVideo(upload.mime_type)}
|
||||||
cx="19"
|
<div class="relative aspect-video w-full bg-gray-900">
|
||||||
cy="12"
|
{#if upload.thumbnail_url || upload.preview_url}
|
||||||
r="2"
|
<img
|
||||||
/></svg
|
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
||||||
>
|
alt=""
|
||||||
</button>
|
class="h-full w-full object-cover opacity-80"
|
||||||
{/if}
|
loading="lazy"
|
||||||
</div>
|
decoding="async"
|
||||||
|
/>
|
||||||
<!-- Media -->
|
{/if}
|
||||||
<button
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
class="relative block w-full"
|
<span
|
||||||
use:doubletap
|
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
|
||||||
onsingletap={() => onselect(upload)}
|
>
|
||||||
ondoubletap={handleDoubleTap}
|
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
onclick={(e) => {
|
<path d="M8 5v14l11-7z" />
|
||||||
if (e.detail === 0) onselect(upload);
|
</svg>
|
||||||
}}
|
</span>
|
||||||
aria-label="Bild vergrößern"
|
</div>
|
||||||
>
|
</div>
|
||||||
<HeartBurst active={heartBurst} />
|
{:else if mediaSrc}
|
||||||
{#if isVideo(upload.mime_type)}
|
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
||||||
<div class="relative aspect-video w-full bg-gray-900">
|
to height 0 and reflow as images stream in. The uncropped original is one
|
||||||
{#if upload.thumbnail_url || upload.preview_url}
|
tap away in the lightbox. -->
|
||||||
|
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
||||||
<img
|
<img
|
||||||
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
src={mediaSrc}
|
||||||
alt=""
|
alt=""
|
||||||
class="h-full w-full object-cover opacity-80"
|
class="h-full w-full object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
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>
|
||||||
</div>
|
{:else}
|
||||||
{:else if mediaSrc}
|
<div
|
||||||
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
||||||
to height 0 and reflow as images stream in. The uncropped original is one
|
>
|
||||||
tap away in the lightbox. -->
|
<svg
|
||||||
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
class="h-12 w-12 text-gray-300 dark:text-gray-600"
|
||||||
<img
|
fill="none"
|
||||||
src={mediaSrc}
|
viewBox="0 0 24 24"
|
||||||
alt=""
|
stroke="currentColor"
|
||||||
class="h-full w-full object-cover"
|
>
|
||||||
loading="lazy"
|
<path
|
||||||
decoding="async"
|
stroke-linecap="round"
|
||||||
/>
|
stroke-linejoin="round"
|
||||||
</div>
|
stroke-width="1.5"
|
||||||
{:else}
|
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"
|
||||||
<div
|
/>
|
||||||
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
</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
|
<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"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="1.5"
|
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"
|
||||||
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>
|
</svg>
|
||||||
</div>
|
{upload.like_count}
|
||||||
{/if}
|
</button>
|
||||||
</button>
|
<button
|
||||||
|
onclick={() => oncomment(upload.id)}
|
||||||
<!-- Actions row -->
|
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"
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
stroke-linecap="round"
|
<path
|
||||||
stroke-linejoin="round"
|
stroke-linecap="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"
|
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.like_count}
|
</svg>
|
||||||
</button>
|
{upload.comment_count}
|
||||||
<button
|
</button>
|
||||||
onclick={() => oncomment(upload.id)}
|
{#if isOwn}
|
||||||
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"
|
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||||
>
|
{/if}
|
||||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
</div>
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
<!-- Caption -->
|
||||||
stroke-linejoin="round"
|
{#if upload.caption}
|
||||||
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"
|
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
|
||||||
/>
|
{upload.caption}
|
||||||
</svg>
|
</p>
|
||||||
{upload.comment_count}
|
|
||||||
</button>
|
|
||||||
{#if isOwn}
|
|
||||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</article>
|
||||||
|
|||||||
@@ -18,10 +18,9 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() => onselect(null)}
|
onclick={() => onselect(null)}
|
||||||
aria-pressed={selected === 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 ===
|
class="chip inline-flex min-h-11 shrink-0 items-center {selected === null
|
||||||
null
|
? 'chip-active'
|
||||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
: 'chip-inactive'}"
|
||||||
: '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'}"
|
|
||||||
>
|
>
|
||||||
Alle
|
Alle
|
||||||
</button>
|
</button>
|
||||||
@@ -29,10 +28,9 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() => onselect(h.tag)}
|
onclick={() => onselect(h.tag)}
|
||||||
aria-pressed={selected === 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 ===
|
class="chip inline-flex min-h-11 shrink-0 items-center {selected === h.tag
|
||||||
h.tag
|
? 'chip-active'
|
||||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
: 'chip-inactive'}"
|
||||||
: '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'}"
|
|
||||||
>
|
>
|
||||||
#{h.tag}
|
#{h.tag}
|
||||||
<span class="ml-1 text-xs opacity-70">{h.count}</span>
|
<span class="ml-1 text-xs opacity-70">{h.count}</span>
|
||||||
|
|||||||
@@ -277,12 +277,12 @@
|
|||||||
bind:value={newComment}
|
bind:value={newComment}
|
||||||
placeholder="Kommentar schreiben..."
|
placeholder="Kommentar schreiben..."
|
||||||
maxlength={COMMENT_MAX}
|
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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !newComment.trim()}
|
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
|
Senden
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -49,9 +49,7 @@
|
|||||||
use:focusTrap={{ onclose: onClose }}
|
use:focusTrap={{ onclose: onClose }}
|
||||||
use:scrollLock
|
use:scrollLock
|
||||||
>
|
>
|
||||||
<div
|
<div class="card pointer-events-auto w-full max-w-sm p-6 shadow-xl">
|
||||||
class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,8 +130,17 @@
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="mb-6 text-center">
|
<div class="mb-6 text-center">
|
||||||
<div class="mb-3 text-5xl">{currentStep.icon}</div>
|
<div class="mb-4 flex justify-center">
|
||||||
<h2 id="onboarding-title" class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">
|
<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}
|
{currentStep.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -193,10 +202,7 @@
|
|||||||
>
|
>
|
||||||
Überspringen
|
Überspringen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onclick={next} class="btn btn-primary flex-1">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
|
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,12 +167,7 @@
|
|||||||
liken und kommentieren.
|
liken und kommentieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onclick={close} class="btn btn-secondary btn-block"> Schließen </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>
|
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Gallery option -->
|
<!-- Gallery option -->
|
||||||
<button
|
<button
|
||||||
@@ -236,12 +231,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Cancel -->
|
<!-- Cancel -->
|
||||||
<button
|
<button onclick={close} class="btn btn-secondary btn-block"> Abbrechen </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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mx-auto max-w-lg space-y-3 p-4">
|
||||||
<!-- Profile card -->
|
<!-- Profile card -->
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-xl font-bold
|
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) -->
|
<!-- Dashboards section (host + admin only) -->
|
||||||
{#if role === 'host' || role === 'admin'}
|
{#if role === 'host' || role === 'admin'}
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||||
<h2
|
<h2
|
||||||
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||||
@@ -211,7 +207,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Star icon -->
|
<!-- Star icon -->
|
||||||
<svg
|
<svg
|
||||||
class="h-5 w-5 text-amber-500"
|
class="h-5 w-5 text-primary-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -270,10 +266,12 @@
|
|||||||
|
|
||||||
<!-- PIN card -->
|
<!-- PIN card -->
|
||||||
<div
|
<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>
|
<h2 class="mb-1 font-semibold text-primary-800 dark:text-primary-200">
|
||||||
<p class="mb-3 text-sm text-amber-700 dark:text-amber-300/90">
|
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
|
Du brauchst diesen PIN, um dein Konto auf einem anderen Gerät wiederherzustellen. Schreib
|
||||||
ihn auf!
|
ihn auf!
|
||||||
</p>
|
</p>
|
||||||
@@ -287,7 +285,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onclick={copyPin}
|
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'}
|
{pinCopied ? 'Kopiert!' : 'Kopieren'}
|
||||||
</button>
|
</button>
|
||||||
@@ -337,9 +335,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Theme / Design -->
|
<!-- Theme / Design -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
<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">
|
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
Design
|
Design
|
||||||
@@ -394,9 +390,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data mode -->
|
<!-- Data mode -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
<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">
|
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
Datennutzung
|
Datennutzung
|
||||||
@@ -439,9 +433,7 @@
|
|||||||
|
|
||||||
<!-- Per-user quota widget -->
|
<!-- Per-user quota widget -->
|
||||||
{#if $quotaStore.enabled && $quotaStore.limit_bytes != null}
|
{#if $quotaStore.enabled && $quotaStore.limit_bytes != null}
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
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) -->
|
<!-- Datenschutzhinweis (preformatted, plain text) -->
|
||||||
{#if $privacyNote.trim()}
|
{#if $privacyNote.trim()}
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
@@ -486,9 +476,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Konto section -->
|
<!-- Konto section -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
<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">
|
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
Konto
|
Konto
|
||||||
@@ -611,16 +599,10 @@
|
|||||||
<p class="mb-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
<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.
|
Original-Dateien können deutlich mehr Datenvolumen verbrauchen. Am besten im WLAN aktivieren.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button onclick={confirmOriginalMode} class="btn btn-primary btn-block mb-3">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Aktivieren
|
Aktivieren
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onclick={() => (dataModeWarningOpen = false)} class="btn btn-secondary btn-block">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -453,10 +453,7 @@
|
|||||||
Kopieren
|
Kopieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block btn-sm">
|
||||||
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
|
Schließen
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -473,16 +470,10 @@
|
|||||||
Diashow und Export, und die Sitzung wird beendet. Rückgängig machbar über „Entsperren“.
|
Diashow und Export, und die Sitzung wird beendet. Rückgängig machbar über „Entsperren“.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button onclick={() => (banTarget = null)} class="btn btn-secondary btn-sm flex-1"
|
||||||
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"
|
|
||||||
>Abbrechen</button
|
>Abbrechen</button
|
||||||
>
|
>
|
||||||
<button
|
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger btn-sm flex-1">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -543,33 +534,25 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<div
|
<div class="card p-4 text-center">
|
||||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{stats.user_count}
|
{stats.user_count}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Gäste</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Gäste</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="card p-4 text-center">
|
||||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{stats.upload_count}
|
{stats.upload_count}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="card p-4 text-center">
|
||||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{stats.comment_count}
|
{stats.comment_count}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="card p-4 text-center">
|
||||||
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{diskPct(stats)} %
|
{diskPct(stats)} %
|
||||||
</p>
|
</p>
|
||||||
@@ -577,9 +560,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Disk bar -->
|
<!-- Disk bar -->
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="mb-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
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'}
|
{:else if activeTab === 'config'}
|
||||||
<div class="relative space-y-3 pb-20">
|
<div class="relative space-y-3 pb-20">
|
||||||
{#each CONFIG_GROUPS as group (group.title)}
|
{#each CONFIG_GROUPS as group (group.title)}
|
||||||
<div
|
<div class="card">
|
||||||
class="rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
|
||||||
<h3
|
<h3
|
||||||
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||||
@@ -652,7 +631,7 @@
|
|||||||
id={field.key}
|
id={field.key}
|
||||||
rows="6"
|
rows="6"
|
||||||
bind:value={configDraft[field.key]}
|
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>
|
></textarea>
|
||||||
{#if field.hint}
|
{#if field.hint}
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
||||||
@@ -670,7 +649,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
bind:value={configDraft[field.key]}
|
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}
|
{#if field.hint}
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
|
||||||
@@ -686,11 +665,7 @@
|
|||||||
<div
|
<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"
|
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
|
<button onclick={saveConfig} disabled={saving} class="btn btn-primary btn-block">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{saving ? 'Wird gespeichert…' : 'Speichern'}
|
{saving ? 'Wird gespeichert…' : 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -700,9 +675,7 @@
|
|||||||
{:else if activeTab === 'export'}
|
{:else if activeTab === 'export'}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- Gallery release -->
|
<!-- Gallery release -->
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-gray-100">Galerie</h3>
|
<h3 class="mb-3 font-semibold text-gray-900 dark:text-gray-100">Galerie</h3>
|
||||||
<button
|
<button
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
@@ -714,16 +687,14 @@
|
|||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
run: releaseGallery
|
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
|
Galerie freigeben
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Export jobs -->
|
<!-- Export jobs -->
|
||||||
<div
|
<div class="card p-5">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Export-Jobs</h3>
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Export-Jobs</h3>
|
||||||
<button
|
<button
|
||||||
@@ -781,9 +752,7 @@
|
|||||||
|
|
||||||
<!-- ── Nutzer tab ───────────────────────────────────────────────── -->
|
<!-- ── Nutzer tab ───────────────────────────────────────────────── -->
|
||||||
{:else if activeTab === 'users'}
|
{:else if activeTab === 'users'}
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div
|
<div
|
||||||
@@ -824,21 +793,12 @@
|
|||||||
>{user.display_name}</span
|
>{user.display_name}</span
|
||||||
>
|
>
|
||||||
{#if user.role === 'host'}
|
{#if user.role === 'host'}
|
||||||
<span
|
<span class="badge badge-primary">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
|
|
||||||
>
|
|
||||||
{:else if user.role === 'admin'}
|
{:else if user.role === 'admin'}
|
||||||
<span
|
<span class="badge badge-gold">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
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if user.is_banned}
|
{#if user.is_banned}
|
||||||
<span
|
<span class="badge badge-danger">Gesperrt</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
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500">
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
data-testid="admin-password-input"
|
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}
|
{#if error}
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !password}
|
disabled={loading || !password}
|
||||||
data-testid="admin-login-submit"
|
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'}
|
{loading ? 'Wird angemeldet…' : 'Anmelden'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -437,12 +437,7 @@
|
|||||||
Kopieren
|
Kopieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block"> Schließen </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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -458,17 +453,10 @@
|
|||||||
ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar über „Entsperren“.
|
ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar über „Entsperren“.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button onclick={() => (banTarget = null)} class="btn btn-secondary flex-1">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger flex-1">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -551,15 +539,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ── Statistiken ─────────────────────────────────────────────── -->
|
<!-- ── Statistiken ─────────────────────────────────────────────── -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onclick={() => (statsOpen = !statsOpen)}
|
onclick={() => (statsOpen = !statsOpen)}
|
||||||
aria-expanded={statsOpen}
|
aria-expanded={statsOpen}
|
||||||
class="flex w-full items-center justify-between px-5 py-4"
|
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
|
<svg
|
||||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {statsOpen
|
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {statsOpen
|
||||||
? 'rotate-180'
|
? 'rotate-180'
|
||||||
@@ -580,50 +566,50 @@
|
|||||||
<div
|
<div
|
||||||
class="grid grid-cols-2 gap-3 border-t border-gray-100 p-4 dark:border-gray-700 sm:grid-cols-4"
|
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="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>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Teilnehmer</p>
|
||||||
</div>
|
</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">
|
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{users.reduce((s, u) => s + u.upload_count, 0)}
|
{users.reduce((s, u) => s + u.upload_count, 0)}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
<div class="surface-muted flex flex-col items-center p-4 text-center">
|
||||||
<p
|
<span
|
||||||
class="text-2xl font-bold {event.uploads_locked
|
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.uploads_locked
|
||||||
? 'text-red-600 dark:text-red-400'
|
? 'bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300'
|
||||||
: 'text-green-600 dark:text-green-400'}"
|
: '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'}
|
{event.uploads_locked ? 'Gesperrt' : 'Offen'}
|
||||||
</p>
|
</span>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
|
<div class="surface-muted flex flex-col items-center p-4 text-center">
|
||||||
<p
|
<span
|
||||||
class="text-2xl font-bold {event.export_released
|
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.export_released
|
||||||
? 'text-blue-600 dark:text-blue-400'
|
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/50 dark:text-purple-200'
|
||||||
: 'text-gray-400 dark:text-gray-500'}"
|
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300'}"
|
||||||
>
|
>
|
||||||
{event.export_released ? 'Ja' : 'Nein'}
|
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
|
||||||
</p>
|
{event.export_released ? 'Freigegeben' : 'Noch nicht'}
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Freigegeben</p>
|
</span>
|
||||||
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Galerie</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Event-Einstellungen ─────────────────────────────────────── -->
|
<!-- ── Event-Einstellungen ─────────────────────────────────────── -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onclick={() => (settingsOpen = !settingsOpen)}
|
onclick={() => (settingsOpen = !settingsOpen)}
|
||||||
aria-expanded={settingsOpen}
|
aria-expanded={settingsOpen}
|
||||||
class="flex w-full items-center justify-between px-5 py-4"
|
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
|
<svg
|
||||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {settingsOpen
|
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {settingsOpen
|
||||||
? 'rotate-180'
|
? 'rotate-180'
|
||||||
@@ -644,10 +630,10 @@
|
|||||||
<div class="flex flex-wrap gap-3 border-t border-gray-100 p-5 dark:border-gray-700">
|
<div class="flex flex-wrap gap-3 border-t border-gray-100 p-5 dark:border-gray-700">
|
||||||
<button
|
<button
|
||||||
onclick={toggleEventLock}
|
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
|
{event.uploads_locked
|
||||||
? 'bg-green-600 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-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'
|
||||||
: 'bg-amber-500 text-white hover:bg-amber-600 dark:bg-amber-500 dark:hover:bg-amber-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'}
|
{event.uploads_locked ? 'Uploads wieder öffnen' : 'Uploads sperren'}
|
||||||
</button>
|
</button>
|
||||||
@@ -662,10 +648,7 @@
|
|||||||
run: releaseGallery
|
run: releaseGallery
|
||||||
})}
|
})}
|
||||||
disabled={event.export_released}
|
disabled={event.export_released}
|
||||||
class="rounded-lg px-4 py-2 text-sm font-medium transition
|
class="btn btn-primary btn-sm"
|
||||||
{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'}"
|
|
||||||
>
|
>
|
||||||
{event.export_released ? 'Galerie bereits freigegeben' : 'Galerie freigeben'}
|
{event.export_released ? 'Galerie bereits freigegeben' : 'Galerie freigeben'}
|
||||||
</button>
|
</button>
|
||||||
@@ -751,15 +734,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Nutzerverwaltung ───────────────────────────────────────── -->
|
<!-- ── Nutzerverwaltung ───────────────────────────────────────── -->
|
||||||
<div
|
<div class="card overflow-hidden">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onclick={() => (usersOpen = !usersOpen)}
|
onclick={() => (usersOpen = !usersOpen)}
|
||||||
aria-expanded={usersOpen}
|
aria-expanded={usersOpen}
|
||||||
class="flex w-full items-center justify-between px-5 py-4"
|
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
|
<svg
|
||||||
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {usersOpen
|
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {usersOpen
|
||||||
? 'rotate-180'
|
? 'rotate-180'
|
||||||
@@ -818,21 +799,12 @@
|
|||||||
>{user.display_name}</span
|
>{user.display_name}</span
|
||||||
>
|
>
|
||||||
{#if user.role === 'host'}
|
{#if user.role === 'host'}
|
||||||
<span
|
<span class="badge badge-primary">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
|
|
||||||
>
|
|
||||||
{:else if user.role === 'admin'}
|
{:else if user.role === 'admin'}
|
||||||
<span
|
<span class="badge badge-gold">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
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if user.is_banned}
|
{#if user.is_banned}
|
||||||
<span
|
<span class="badge badge-danger">Gesperrt</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
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500">
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
|||||||
@@ -139,142 +139,184 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950">
|
<div
|
||||||
<div class="w-full max-w-sm">
|
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"
|
||||||
{#if nameTaken}
|
>
|
||||||
<!-- Name-taken state: sign in with PIN or choose a different name -->
|
<!-- 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
|
<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">
|
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
„{takenName}" ist bereits vergeben.
|
<path
|
||||||
</p>
|
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"
|
||||||
<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
|
stroke="currentColor"
|
||||||
Wähle einen anderen Namen, z. B. einen Spitznamen oder füge deinen Nachnamen hinzu („{takenName}
|
stroke-width="1.6"
|
||||||
M." oder „{takenName} aus Berlin").
|
/>
|
||||||
</p>
|
<circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.6" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
class="font-display text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
|
||||||
Falls du das bist, melde dich mit deinem PIN an:
|
>EventSnap</span
|
||||||
</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onsubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleInlineRecover();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
</div>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if recoveryError}
|
<!-- Invitation card -->
|
||||||
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
|
<div class="card p-6 shadow-xl">
|
||||||
{recoveryError}
|
{#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>
|
</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
|
<button
|
||||||
type="submit"
|
onclick={tryDifferentName}
|
||||||
disabled={recoveryLoading || recoveryPin.length < 4}
|
data-testid="try-different-name"
|
||||||
data-testid="recovery-submit"
|
class="btn btn-secondary btn-block"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
|
Anderen Namen wählen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
|
|
||||||
<button
|
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
|
||||||
onclick={tryDifferentName}
|
{#if pinRequestSent}
|
||||||
data-testid="try-different-name"
|
<p
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
Anderen Namen wählen
|
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
|
||||||
</button>
|
der neuen PIN anmelden.
|
||||||
|
</p>
|
||||||
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
|
{:else}
|
||||||
{#if pinRequestSent}
|
<button
|
||||||
<p
|
onclick={requestPinReset}
|
||||||
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"
|
disabled={pinRequestLoading}
|
||||||
>
|
data-testid="request-pin-reset"
|
||||||
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
|
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"
|
||||||
der neuen PIN anmelden.
|
>
|
||||||
</p>
|
{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<!-- Normal join form -->
|
||||||
onclick={requestPinReset}
|
<p class="mb-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
disabled={pinRequestLoading}
|
Willkommen bei
|
||||||
data-testid="request-pin-reset"
|
</p>
|
||||||
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"
|
{#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'}
|
<input
|
||||||
</button>
|
type="text"
|
||||||
{/if}
|
bind:value={displayName}
|
||||||
{:else}
|
placeholder="Dein Name"
|
||||||
<!-- Normal join form -->
|
maxlength={50}
|
||||||
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">
|
data-testid="join-name-input"
|
||||||
Willkommen!
|
class="input mb-3 text-lg"
|
||||||
</h1>
|
/>
|
||||||
{#if eventName}
|
|
||||||
<p
|
{#if error}
|
||||||
class="mb-1 text-center text-lg font-semibold text-blue-600 dark:text-blue-400"
|
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
|
||||||
data-testid="join-event-name"
|
{error}
|
||||||
>
|
</p>
|
||||||
{eventName}
|
{/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>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
@@ -284,7 +326,7 @@
|
|||||||
data-testid="pin-modal"
|
data-testid="pin-modal"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="pin-modal-title"
|
aria-labelledby="pin-modal-title"
|
||||||
@@ -298,18 +340,12 @@
|
|||||||
wiederherzustellen.
|
wiederherzustellen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div class="surface-muted mb-4 flex items-center justify-center gap-3 p-4">
|
||||||
class="mb-4 flex items-center justify-center gap-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<span
|
<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
|
data-testid="pin-display">{pin}</span
|
||||||
>
|
>
|
||||||
<button
|
<button onclick={copyPin} data-testid="pin-copy" class="btn btn-secondary btn-sm min-h-11">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{copied ? 'Kopiert!' : 'Kopieren'}
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,14 +353,14 @@
|
|||||||
<button
|
<button
|
||||||
onclick={goToFeed}
|
onclick={goToFeed}
|
||||||
data-testid="continue-to-feed"
|
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
|
Weiter zur Galerie
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={closePinModal}
|
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
|
Schließen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
placeholder="Dein Name"
|
placeholder="Dein Name"
|
||||||
maxlength={50}
|
maxlength={50}
|
||||||
data-testid="recover-name-input"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
data-testid="recover-pin-input"
|
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}
|
{#if error}
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !displayName.trim() || pin.length < 4}
|
disabled={loading || !displayName.trim() || pin.length < 4}
|
||||||
data-testid="recover-submit"
|
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'}
|
{loading ? 'Wird geladen...' : 'Wiederherstellen'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -151,8 +151,7 @@
|
|||||||
onclick={handleSubmit}
|
onclick={handleSubmit}
|
||||||
disabled={stagedFiles.length === 0 || submitting}
|
disabled={stagedFiles.length === 0 || submitting}
|
||||||
data-testid="upload-submit-header"
|
data-testid="upload-submit-header"
|
||||||
class="rounded-lg bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white transition
|
class="btn btn-primary btn-sm"
|
||||||
hover:bg-blue-700 disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
|
|
||||||
>
|
>
|
||||||
{submitting ? 'Wird hochgeladen…' : 'Hochladen'}
|
{submitting ? 'Wird hochgeladen…' : 'Hochladen'}
|
||||||
</button>
|
</button>
|
||||||
@@ -199,31 +198,30 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- No files: prompt to go back and pick some -->
|
<!-- 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">
|
<div class="flex flex-1 flex-col items-center justify-center gap-4 p-8 text-center">
|
||||||
<svg
|
<div
|
||||||
class="h-16 w-16 text-gray-200 dark:text-gray-700"
|
class="flex h-20 w-20 items-center justify-center rounded-2xl bg-primary-50 dark:bg-primary-900/20"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
class="h-9 w-9 text-primary-500 dark:text-primary-500"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke="currentColor"
|
||||||
</svg>
|
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>
|
<div>
|
||||||
<p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p>
|
<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">
|
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">
|
||||||
Geh zurück und tippe auf den Plus-Button.
|
Geh zurück und tippe auf den Plus-Button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onclick={cancel} class="btn btn-secondary btn-sm"> Zurück </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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -236,9 +234,7 @@
|
|||||||
data-testid="upload-caption"
|
data-testid="upload-caption"
|
||||||
placeholder="Beschreibung hinzufügen… (#hashtags möglich)"
|
placeholder="Beschreibung hinzufügen… (#hashtags möglich)"
|
||||||
rows="4"
|
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
|
class="input resize-none text-sm"
|
||||||
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"
|
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="mt-1 text-xs text-gray-500 text-right dark:text-gray-400">
|
<div class="mt-1 text-xs text-gray-500 text-right dark:text-gray-400">
|
||||||
{caption.length} / {MAX_CAPTION_LENGTH}
|
{caption.length} / {MAX_CAPTION_LENGTH}
|
||||||
@@ -316,8 +312,7 @@
|
|||||||
onclick={handleSubmit}
|
onclick={handleSubmit}
|
||||||
disabled={stagedFiles.length === 0 || submitting}
|
disabled={stagedFiles.length === 0 || submitting}
|
||||||
data-testid="upload-submit"
|
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
|
class="btn btn-primary btn-block"
|
||||||
text-white transition hover:bg-blue-700 active:scale-[0.98] disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
|
|
||||||
>
|
>
|
||||||
{#if submitting}
|
{#if submitting}
|
||||||
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
<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
|
* Tailwind v4 reads `@theme` blocks to populate utility classes; everything declared
|
||||||
* here becomes a `bg-primary`, `text-accent`, `rounded-card`, etc.
|
* 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';
|
@import 'tailwindcss';
|
||||||
@@ -15,32 +30,108 @@
|
|||||||
* `:where(...)` keeps the specificity low so existing utilities still override. */
|
* `:where(...)` keeps the specificity low so existing utilities still override. */
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@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 {
|
@theme {
|
||||||
/* Brand palette — the blue used for primary buttons, FAB, active tabs. */
|
/* ── Type ─────────────────────────────────────────────────────────────── */
|
||||||
--color-primary-50: #eff6ff;
|
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
--color-primary-100: #dbeafe;
|
--font-display: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
|
||||||
--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;
|
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', 'Courier New', monospace;
|
||||||
|
|
||||||
/* Radii — keep cards and bottom sheets consistent. */
|
/* ── Brand: champagne / antique gold — the "signal" colour (remaps `blue-*`)
|
||||||
--radius-card: 0.75rem; /* rounded-card */
|
* Wedding research: treat gold as the signal (brand, primary CTA, active
|
||||||
--radius-sheet: 1.25rem; /* rounded-sheet */
|
* 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
|
/* 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. */
|
* their own backgrounds via `bg-*` utilities, but this catches any gaps. */
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
background-color: #f9fafb; /* matches bg-gray-50 */
|
background-color: #faf9f7; /* soft pearl (bg-gray-50) */
|
||||||
color: #111827; /* matches text-gray-900 */
|
color: #1a1918; /* ink (text-gray-900) */
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
html.dark {
|
html.dark {
|
||||||
background-color: #030712; /* matches bg-gray-950 */
|
background-color: #100f0f; /* graphite near-black (bg-gray-950) */
|
||||||
color: #f3f4f6; /* matches text-gray-100 */
|
color: #f2f1ef; /* light silver (text-gray-100) */
|
||||||
color-scheme: dark;
|
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