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>
137 lines
5.0 KiB
Svelte
137 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { page } from '$app/stores';
|
|
import { uploadSheetOpen, uploadBadgeCount } from '$lib/ui-store';
|
|
import { exportStatus, initExportStatus } from '$lib/export-status-store';
|
|
|
|
function isActive(path: string): boolean {
|
|
return $page.url.pathname.startsWith(path);
|
|
}
|
|
|
|
onMount(() => {
|
|
// Hydrate export status once when the bottom nav first mounts (i.e. the user
|
|
// is authenticated). Subsequent SSE events update the snapshot live so the
|
|
// Export tab can fade in mid-session.
|
|
initExportStatus();
|
|
});
|
|
|
|
let showExport = $derived($exportStatus.released);
|
|
let zipReady = $derived($exportStatus.zip?.status === 'done');
|
|
</script>
|
|
|
|
<!-- Bottom navigation bar — fixed, full-width, safe-area aware -->
|
|
<nav
|
|
class="fixed bottom-0 left-0 right-0 z-40 border-t border-gray-200 bg-white/90 backdrop-blur-md dark:border-gray-800 dark:bg-gray-900/90"
|
|
style="padding-bottom: env(safe-area-inset-bottom)"
|
|
data-testid="bottom-nav"
|
|
>
|
|
<div class="mx-auto flex h-14 max-w-2xl items-end justify-around px-4 pb-1">
|
|
<!-- Feed tab -->
|
|
<a
|
|
href="/feed"
|
|
class="flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
|
|
{isActive('/feed')
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
|
|
aria-label="Galerie"
|
|
>
|
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
|
/>
|
|
</svg>
|
|
<span>Galerie</span>
|
|
</a>
|
|
|
|
<!-- Upload FAB (center, elevated) -->
|
|
<div class="relative -translate-y-3">
|
|
<button
|
|
onclick={() => ($uploadSheetOpen = true)}
|
|
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 -->
|
|
<svg
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
|
/>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
|
/>
|
|
</svg>
|
|
<!-- Badge -->
|
|
{#if $uploadBadgeCount > 0}
|
|
<span
|
|
class="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white"
|
|
>
|
|
{$uploadBadgeCount > 9 ? '9+' : $uploadBadgeCount}
|
|
</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Export tab — appears only when the host has released the export. -->
|
|
{#if showExport}
|
|
<a
|
|
href="/export"
|
|
data-testid="bottom-nav-export"
|
|
class="relative flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
|
|
{isActive('/export')
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
|
|
aria-label="Export"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
|
/>
|
|
</svg>
|
|
<span>Export</span>
|
|
{#if zipReady}
|
|
<span class="absolute right-2 top-0 h-2 w-2 rounded-full bg-blue-500" aria-hidden="true"
|
|
></span>
|
|
{/if}
|
|
</a>
|
|
{/if}
|
|
|
|
<!-- Account tab -->
|
|
<a
|
|
href="/account"
|
|
class="flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
|
|
{isActive('/account')
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
|
|
aria-label="Konto"
|
|
>
|
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
|
/>
|
|
</svg>
|
|
<span>Konto</span>
|
|
</a>
|
|
</div>
|
|
</nav>
|