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:
@@ -49,7 +49,7 @@
|
||||
<div class="relative -translate-y-3">
|
||||
<button
|
||||
onclick={() => ($uploadSheetOpen = true)}
|
||||
class="relative flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition active:scale-95 hover:bg-blue-700"
|
||||
class="relative flex h-14 w-14 items-center justify-center rounded-full border-2 border-primary-500 bg-primary-100 text-primary-700 shadow-lg transition active:scale-95 hover:bg-primary-200 dark:border-primary-500 dark:bg-primary-950/60 dark:text-primary-300"
|
||||
aria-label="Hochladen"
|
||||
>
|
||||
<!-- Camera + plus icon -->
|
||||
|
||||
@@ -80,10 +80,7 @@
|
||||
onclick={handleConfirm}
|
||||
disabled={busy}
|
||||
data-testid="confirm-sheet-confirm"
|
||||
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone ===
|
||||
'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400'}"
|
||||
class="btn btn-block mb-3 {tone === 'danger' ? 'btn-danger' : 'btn-primary'}"
|
||||
>
|
||||
{#if busy}
|
||||
<span
|
||||
@@ -98,7 +95,7 @@
|
||||
onclick={onCancel}
|
||||
disabled={busy}
|
||||
data-testid="confirm-sheet-cancel"
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
|
||||
class="btn btn-secondary btn-block"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
|
||||
@@ -147,11 +147,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="mt-2 w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<button type="button" onclick={onClose} class="btn btn-secondary btn-block mt-2">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -66,170 +66,168 @@
|
||||
<!-- Off-screen cards are removed from the DOM entirely by the parent VirtualFeed
|
||||
window-virtualizer, so this card no longer needs content-visibility — and must
|
||||
not use it, since the virtualizer measures each card's real rendered height. -->
|
||||
<article
|
||||
class="bg-white dark:bg-gray-900"
|
||||
use:longpress={{ duration: 500 }}
|
||||
onlongpress={openContext}
|
||||
>
|
||||
<!-- Uploader row -->
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
||||
<article class="px-3 py-2" use:longpress={{ duration: 500 }} onlongpress={openContext}>
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||||
>
|
||||
<!-- Uploader row -->
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
|
||||
{avatarPalette(upload.uploader_name)}"
|
||||
>
|
||||
{initials(upload.uploader_name)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{upload.uploader_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
||||
>
|
||||
{initials(upload.uploader_name)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{upload.uploader_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if oncontextmenu}
|
||||
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
openContext();
|
||||
}}
|
||||
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
|
||||
aria-label="Mehr Aktionen"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
|
||||
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="2"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if oncontextmenu}
|
||||
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
openContext();
|
||||
}}
|
||||
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
|
||||
aria-label="Mehr Aktionen"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
|
||||
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="2"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Media -->
|
||||
<button
|
||||
class="relative block w-full"
|
||||
use:doubletap
|
||||
onsingletap={() => onselect(upload)}
|
||||
ondoubletap={handleDoubleTap}
|
||||
onclick={(e) => {
|
||||
if (e.detail === 0) onselect(upload);
|
||||
}}
|
||||
aria-label="Bild vergrößern"
|
||||
>
|
||||
<HeartBurst active={heartBurst} />
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<div class="relative aspect-video w-full bg-gray-900">
|
||||
{#if upload.thumbnail_url || upload.preview_url}
|
||||
<!-- Media -->
|
||||
<button
|
||||
class="relative block w-full"
|
||||
use:doubletap
|
||||
onsingletap={() => onselect(upload)}
|
||||
ondoubletap={handleDoubleTap}
|
||||
onclick={(e) => {
|
||||
if (e.detail === 0) onselect(upload);
|
||||
}}
|
||||
aria-label="Bild vergrößern"
|
||||
>
|
||||
<HeartBurst active={heartBurst} />
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<div class="relative aspect-video w-full bg-gray-900">
|
||||
{#if upload.thumbnail_url || upload.preview_url}
|
||||
<img
|
||||
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
||||
alt=""
|
||||
class="h-full w-full object-cover opacity-80"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span
|
||||
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
|
||||
>
|
||||
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if mediaSrc}
|
||||
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
||||
to height 0 and reflow as images stream in. The uncropped original is one
|
||||
tap away in the lightbox. -->
|
||||
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
||||
<img
|
||||
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
class="h-full w-full object-cover opacity-80"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span
|
||||
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
|
||||
>
|
||||
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if mediaSrc}
|
||||
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
|
||||
to height 0 and reflow as images stream in. The uncropped original is one
|
||||
tap away in the lightbox. -->
|
||||
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
|
||||
<img
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
||||
{:else}
|
||||
<div
|
||||
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="h-12 w-12 text-gray-300 dark:text-gray-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Actions row -->
|
||||
<div class="flex items-center gap-4 px-4 py-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
vibrate(10);
|
||||
onlike(upload.id);
|
||||
}}
|
||||
aria-pressed={upload.liked_by_me}
|
||||
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
|
||||
class="flex items-center gap-1.5 text-sm font-medium transition-colors
|
||||
{upload.liked_by_me
|
||||
? 'text-red-500 dark:text-red-400'
|
||||
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
|
||||
>
|
||||
<svg
|
||||
class="h-12 w-12 text-gray-300 dark:text-gray-600"
|
||||
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Actions row -->
|
||||
<div class="flex items-center gap-4 px-4 py-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
vibrate(10);
|
||||
onlike(upload.id);
|
||||
}}
|
||||
aria-pressed={upload.liked_by_me}
|
||||
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
|
||||
class="flex items-center gap-1.5 text-sm font-medium transition-colors
|
||||
{upload.liked_by_me
|
||||
? 'text-red-500 dark:text-red-400'
|
||||
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
{upload.like_count}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.like_count}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{#if isOwn}
|
||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{#if isOwn}
|
||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Caption -->
|
||||
{#if upload.caption}
|
||||
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
|
||||
{upload.caption}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Caption -->
|
||||
{#if upload.caption}
|
||||
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
|
||||
{upload.caption}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="border-b border-gray-100 dark:border-gray-800"></div>
|
||||
</article>
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
<button
|
||||
onclick={() => onselect(null)}
|
||||
aria-pressed={selected === null}
|
||||
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
|
||||
null
|
||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
|
||||
class="chip inline-flex min-h-11 shrink-0 items-center {selected === null
|
||||
? 'chip-active'
|
||||
: 'chip-inactive'}"
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
@@ -29,10 +28,9 @@
|
||||
<button
|
||||
onclick={() => onselect(h.tag)}
|
||||
aria-pressed={selected === h.tag}
|
||||
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
|
||||
h.tag
|
||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
|
||||
class="chip inline-flex min-h-11 shrink-0 items-center {selected === h.tag
|
||||
? 'chip-active'
|
||||
: 'chip-inactive'}"
|
||||
>
|
||||
#{h.tag}
|
||||
<span class="ml-1 text-xs opacity-70">{h.count}</span>
|
||||
|
||||
@@ -277,12 +277,12 @@
|
||||
bind:value={newComment}
|
||||
placeholder="Kommentar schreiben..."
|
||||
maxlength={COMMENT_MAX}
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
class="input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newComment.trim()}
|
||||
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 active:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
|
||||
@@ -49,9 +49,7 @@
|
||||
use:focusTrap={{ onclose: onClose }}
|
||||
use:scrollLock
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
|
||||
>
|
||||
<div class="card pointer-events-auto w-full max-w-sm p-6 shadow-xl">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,8 +130,17 @@
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mb-6 text-center">
|
||||
<div class="mb-3 text-5xl">{currentStep.icon}</div>
|
||||
<h2 id="onboarding-title" class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<div class="mb-4 flex justify-center">
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-50 text-3xl dark:bg-primary-900/30"
|
||||
>
|
||||
{currentStep.icon}
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
id="onboarding-title"
|
||||
class="mb-2 text-xl font-semibold text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
|
||||
@@ -193,10 +202,7 @@
|
||||
>
|
||||
Überspringen
|
||||
</button>
|
||||
<button
|
||||
onclick={next}
|
||||
class="flex-1 rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
|
||||
>
|
||||
<button onclick={next} class="btn btn-primary flex-1">
|
||||
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -167,12 +167,7 @@
|
||||
liken und kommentieren.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={close}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<button onclick={close} class="btn btn-secondary btn-block"> Schließen </button>
|
||||
{:else}
|
||||
<!-- Gallery option -->
|
||||
<button
|
||||
@@ -236,12 +231,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
onclick={close}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onclick={close} class="btn btn-secondary btn-block"> Abbrechen </button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
120
frontend/src/lib/styles/components.css
Normal file
120
frontend/src/lib/styles/components.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* EventSnap component layer — the shared, "define-once" building blocks.
|
||||
*
|
||||
* Colours/type/spacing tokens live in `tailwind-theme.css`; this file turns the
|
||||
* *recurring UI patterns* (buttons, inputs, cards, badges, chips, sheets) into
|
||||
* named classes so every page consumes the same look. Change a button here and
|
||||
* it changes everywhere.
|
||||
*
|
||||
* These live in the `components` layer, which Tailwind emits BEFORE `utilities`
|
||||
* — so a one-off inline utility (e.g. `rounded-lg`, `w-full`) on an element
|
||||
* still overrides the component class. Reach for a component class first; drop
|
||||
* to inline utilities only for genuinely one-off tweaks.
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@layer components {
|
||||
/* ── Buttons ──────────────────────────────────────────────────────────
|
||||
* Base + intent modifier (+ optional size). e.g. `class="btn btn-primary"`,
|
||||
* `class="btn btn-secondary btn-sm"`, `class="btn btn-primary btn-block"`. */
|
||||
.btn {
|
||||
@apply inline-flex select-none items-center justify-center gap-2 rounded-xl border border-transparent px-4 py-3 text-sm font-semibold transition;
|
||||
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40;
|
||||
@apply active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
}
|
||||
.btn-lg {
|
||||
@apply px-5 py-3.5 text-base;
|
||||
}
|
||||
.btn-sm {
|
||||
@apply rounded-lg px-3 py-2;
|
||||
}
|
||||
.btn-block {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Primary = muted champagne fill + gold border + gold text (an elegant,
|
||||
* outlined "signal" rather than an aggressive solid gold block). */
|
||||
.btn-primary {
|
||||
@apply border-primary-400 bg-primary-100 text-primary-800 shadow-sm hover:border-primary-500 hover:bg-primary-200;
|
||||
@apply disabled:border-gray-200 disabled:bg-gray-100 disabled:text-gray-400 disabled:shadow-none;
|
||||
@apply dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200 dark:hover:border-primary-500 dark:hover:bg-primary-900/40;
|
||||
}
|
||||
.btn-secondary {
|
||||
@apply border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50;
|
||||
@apply dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700;
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply text-gray-600 hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50;
|
||||
@apply dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100;
|
||||
}
|
||||
.btn-danger {
|
||||
@apply bg-red-600 text-white shadow-sm hover:bg-red-700 disabled:opacity-50;
|
||||
@apply dark:bg-red-600 dark:hover:bg-red-500;
|
||||
}
|
||||
|
||||
/* ── Form inputs ──────────────────────────────────────────────────────
|
||||
* Covers text/number inputs and textareas. */
|
||||
.input {
|
||||
@apply w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 placeholder-gray-400 transition;
|
||||
@apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/25;
|
||||
@apply dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500;
|
||||
}
|
||||
|
||||
/* ── Surfaces ─────────────────────────────────────────────────────────
|
||||
* `card` — the standard raised surface (page → card → surface-muted is the
|
||||
* three-level elevation ramp).
|
||||
* `surface-muted` — a nested/inset panel inside a card. */
|
||||
.card {
|
||||
@apply rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900;
|
||||
}
|
||||
.surface-muted {
|
||||
@apply rounded-xl bg-gray-50 dark:bg-gray-800/60;
|
||||
}
|
||||
|
||||
/* ── Badges (status / role pills) ─────────────────────────────────────
|
||||
* `class="badge badge-gold"` etc. A leading dot is optional in markup. */
|
||||
.badge {
|
||||
@apply inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold;
|
||||
}
|
||||
.badge-primary {
|
||||
@apply bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-200;
|
||||
}
|
||||
.badge-gold {
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200;
|
||||
}
|
||||
.badge-gray {
|
||||
@apply bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300;
|
||||
}
|
||||
.badge-success {
|
||||
@apply bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300;
|
||||
}
|
||||
.badge-danger {
|
||||
@apply bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300;
|
||||
}
|
||||
|
||||
/* ── Chips (filter / toggle) ──────────────────────────────────────────
|
||||
* `class="chip chip-active"` / `class="chip chip-inactive"`. */
|
||||
.chip {
|
||||
@apply rounded-full border border-transparent px-4 py-2 text-sm font-medium transition;
|
||||
}
|
||||
.chip-inactive {
|
||||
@apply bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
|
||||
}
|
||||
/* Active = gold outline + soft champagne fill (matches .btn-primary). */
|
||||
.chip-active {
|
||||
@apply border-primary-400 bg-primary-100 text-primary-800 dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200;
|
||||
}
|
||||
|
||||
/* ── Bottom-sheet surface ─────────────────────────────────────────────
|
||||
* The rounded top panel shared by upload / context / confirm sheets. */
|
||||
.sheet {
|
||||
@apply rounded-t-3xl bg-white p-6 shadow-2xl dark:bg-gray-900 sm:rounded-2xl;
|
||||
}
|
||||
|
||||
/* ── Page scaffold + section heading ──────────────────────────────────*/
|
||||
.page {
|
||||
@apply min-h-screen bg-gray-50 dark:bg-gray-950;
|
||||
}
|
||||
.section-title {
|
||||
@apply font-display text-lg font-semibold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user