feat(mvp): phase 7 - UI polish & ambient backgrounds

Add layout system (sidebar, header, main layout), dark/light/system theme
with HSL customization, 3 ambient backgrounds (mesh gradient, particle field,
aurora), Cmd/Ctrl+K search dialog, page transitions, card hover effects,
status pulse animations, skeleton loaders, and responsive design. Polish
all existing pages with consistent theming.
This commit is contained in:
2026-03-24 21:37:16 +03:00
parent c5166ba3a9
commit 0bd30c5e17
41 changed files with 2106 additions and 391 deletions
+233
View File
@@ -0,0 +1,233 @@
<script lang="ts">
import { ui } from '$lib/stores/ui.svelte.js';
import { page } from '$app/stores';
interface BoardLink {
id: string;
name: string;
icon: string | null;
}
interface Props {
boards: BoardLink[];
isAdmin: boolean;
collapsed: boolean;
}
let { boards, isAdmin, collapsed }: Props = $props();
function isActive(path: string): boolean {
return $page.url.pathname.startsWith(path);
}
</script>
<aside
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200"
class:w-64={!collapsed}
class:w-16={collapsed}
>
<!-- Brand -->
<div class="flex h-14 items-center border-b border-sidebar-border px-4">
{#if !collapsed}
<a href="/" class="flex items-center gap-2 text-sidebar-foreground">
<svg
class="h-6 w-6 text-sidebar-primary"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
<span class="text-sm font-semibold">App Launcher</span>
</a>
{:else}
<a href="/" class="mx-auto text-sidebar-primary" title="App Launcher">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
</a>
{/if}
</div>
<!-- Navigation -->
<nav class="flex-1 overflow-y-auto px-2 py-3">
<!-- Main Links -->
<div class="mb-3">
{#if !collapsed}
<p class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
Navigation
</p>
{/if}
<a
href="/boards"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/boards')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Boards' : undefined}
>
<svg
class="h-4 w-4 shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</svg>
{#if !collapsed}<span>Boards</span>{/if}
</a>
<a
href="/apps"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/apps')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Apps' : undefined}
>
<svg
class="h-4 w-4 shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
/>
</svg>
{#if !collapsed}<span>Apps</span>{/if}
</a>
</div>
<!-- Board List -->
{#if boards.length > 0}
<div class="mb-3">
{#if !collapsed}
<p
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
>
Boards
</p>
{/if}
{#each boards as board (board.id)}
<a
href="/boards/{board.id}"
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors {isActive(`/boards/${board.id}`)
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? board.name : undefined}
onclick={() => ui.closeMobileSidebar()}
>
{#if board.icon}
<span class="shrink-0 text-base">{board.icon}</span>
{:else}
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-sidebar-accent text-[10px] font-medium text-sidebar-foreground"
>
{board.name.charAt(0).toUpperCase()}
</span>
{/if}
{#if !collapsed}
<span class="truncate">{board.name}</span>
{/if}
</a>
{/each}
</div>
{/if}
<!-- Admin -->
{#if isAdmin}
<div class="mt-auto border-t border-sidebar-border pt-3">
{#if !collapsed}
<p
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
>
Admin
</p>
{/if}
<a
href="/admin/users"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/admin')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Admin Panel' : undefined}
>
<svg
class="h-4 w-4 shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
{#if !collapsed}<span>Admin Panel</span>{/if}
</a>
</div>
{/if}
</nav>
<!-- Collapse Toggle (desktop only) -->
{#if !ui.isMobile}
<div class="border-t border-sidebar-border p-2">
<button
type="button"
onclick={() => ui.toggleSidebar()}
class="flex w-full items-center justify-center rounded-md p-2 text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
<svg
class="h-4 w-4 transition-transform duration-200"
class:rotate-180={collapsed}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
</div>
{/if}
</aside>