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
+229 -227
View File
@@ -12,252 +12,254 @@
<title>Edit: {data.board.name}</title>
</svelte:head>
<div class="mx-auto max-w-4xl px-4 py-8">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-white">Edit Board</h1>
<a
href="/boards/{data.board.id}"
class="rounded-lg bg-gray-700 px-4 py-2 text-sm text-gray-200 hover:bg-gray-600 transition-colors"
>
Back to Board
</a>
</div>
<!-- Board Properties -->
<section class="mb-8 rounded-lg border border-gray-700 bg-gray-800/50 p-6">
<h2 class="mb-4 text-lg font-semibold text-white">Board Properties</h2>
<form method="POST" action="?/updateBoard" use:enhance>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="board-name" class="mb-1 block text-sm font-medium text-gray-300">Name</label>
<input
id="board-name"
name="name"
type="text"
value={data.board.name}
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-indigo-500 focus:outline-none"
required
/>
</div>
<div>
<label for="board-icon" class="mb-1 block text-sm font-medium text-gray-300">Icon</label>
<input
id="board-icon"
name="icon"
type="text"
value={data.board.icon ?? ''}
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-indigo-500 focus:outline-none"
placeholder="e.g. layout-dashboard"
/>
</div>
<div class="sm:col-span-2">
<label for="board-desc" class="mb-1 block text-sm font-medium text-gray-300">Description</label>
<textarea
id="board-desc"
name="description"
rows="2"
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-indigo-500 focus:outline-none"
>{data.board.description ?? ''}</textarea>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
name="isDefault"
checked={data.board.isDefault}
class="rounded border-gray-600 bg-gray-700"
/>
Default Board
</label>
<label class="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
name="isGuestAccessible"
checked={data.board.isGuestAccessible}
class="rounded border-gray-600 bg-gray-700"
/>
Guest Accessible
</label>
</div>
</div>
<div class="mt-4">
<button
type="submit"
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors"
>
Save Board
</button>
</div>
</form>
</section>
<!-- Sections -->
<section class="mb-8">
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-white">Sections</h2>
<button
type="button"
onclick={() => (showAddSection = !showAddSection)}
class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500 transition-colors"
<div class="p-6">
<div class="mx-auto max-w-4xl">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">Edit Board</h1>
<a
href="/boards/{data.board.id}"
class="rounded-lg border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-accent"
>
{showAddSection ? 'Cancel' : 'Add Section'}
</button>
Back to Board
</a>
</div>
{#if showAddSection}
<div class="mb-4 rounded-lg border border-gray-700 bg-gray-800/50 p-4">
<form
method="POST"
action="?/addSection"
use:enhance={() => {
return async ({ update }) => {
await update();
showAddSection = false;
};
}}
<!-- Board Properties -->
<section class="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-card-foreground">Board Properties</h2>
<form method="POST" action="?/updateBoard" use:enhance>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="board-name" class="mb-1 block text-sm font-medium text-foreground">Name</label>
<input
id="board-name"
name="name"
type="text"
value={data.board.name}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
required
/>
</div>
<div>
<label for="board-icon" class="mb-1 block text-sm font-medium text-foreground">Icon</label>
<input
id="board-icon"
name="icon"
type="text"
value={data.board.icon ?? ''}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
placeholder="e.g. layout-dashboard"
/>
</div>
<div class="sm:col-span-2">
<label for="board-desc" class="mb-1 block text-sm font-medium text-foreground">Description</label>
<textarea
id="board-desc"
name="description"
rows="2"
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
>{data.board.description ?? ''}</textarea>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-foreground">
<input
type="checkbox"
name="isDefault"
checked={data.board.isDefault}
class="h-4 w-4 rounded border-input accent-primary"
/>
Default Board
</label>
<label class="flex items-center gap-2 text-sm text-foreground">
<input
type="checkbox"
name="isGuestAccessible"
checked={data.board.isGuestAccessible}
class="h-4 w-4 rounded border-input accent-primary"
/>
Guest Accessible
</label>
</div>
</div>
<div class="mt-4">
<button
type="submit"
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Save Board
</button>
</div>
</form>
</section>
<!-- Sections -->
<section class="mb-8">
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-foreground">Sections</h2>
<button
type="button"
onclick={() => (showAddSection = !showAddSection)}
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label for="section-title" class="mb-1 block text-sm font-medium text-gray-300">Title</label>
<input
id="section-title"
name="title"
type="text"
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-indigo-500 focus:outline-none"
required
/>
</div>
<div>
<label for="section-icon" class="mb-1 block text-sm font-medium text-gray-300">Icon</label>
<input
id="section-icon"
name="icon"
type="text"
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-indigo-500 focus:outline-none"
placeholder="Optional"
/>
</div>
</div>
<div class="mt-3">
<button
type="submit"
class="rounded-lg bg-green-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-green-500 transition-colors"
>
Create Section
</button>
</div>
</form>
{showAddSection ? 'Cancel' : 'Add Section'}
</button>
</div>
{/if}
{#if data.board.sections.length === 0}
<div class="rounded-lg border border-gray-700 bg-gray-800/50 p-8 text-center">
<p class="text-gray-400">No sections yet. Add one to get started.</p>
</div>
{:else}
<div class="space-y-4">
{#each data.board.sections as section (section.id)}
<div class="rounded-lg border border-gray-700 bg-gray-800/50 p-4">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="font-medium text-white">{section.title}</span>
<span class="text-xs text-gray-400">Order: {section.order}</span>
{#if section.icon}
<span class="text-xs text-gray-500">({section.icon})</span>
{/if}
{#if showAddSection}
<div class="mb-4 rounded-xl border border-border bg-card p-4 shadow-sm">
<form
method="POST"
action="?/addSection"
use:enhance={() => {
return async ({ update }) => {
await update();
showAddSection = false;
};
}}
>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label for="section-title" class="mb-1 block text-sm font-medium text-foreground">Title</label>
<input
id="section-title"
name="title"
type="text"
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
required
/>
</div>
<div class="flex items-center gap-2">
<button
type="button"
onclick={() => (addWidgetSectionId = addWidgetSectionId === section.id ? null : section.id)}
class="rounded bg-indigo-600 px-2 py-1 text-xs font-medium text-white hover:bg-indigo-500 transition-colors"
>
Add Widget
</button>
<form method="POST" action="?/deleteSection" use:enhance>
<input type="hidden" name="sectionId" value={section.id} />
<div>
<label for="section-icon" class="mb-1 block text-sm font-medium text-foreground">Icon</label>
<input
id="section-icon"
name="icon"
type="text"
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
placeholder="Optional"
/>
</div>
</div>
<div class="mt-3">
<button
type="submit"
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Create Section
</button>
</div>
</form>
</div>
{/if}
{#if data.board.sections.length === 0}
<div class="rounded-xl border border-border bg-card/50 p-8 text-center">
<p class="text-muted-foreground">No sections yet. Add one to get started.</p>
</div>
{:else}
<div class="space-y-4">
{#each data.board.sections as section (section.id)}
<div class="rounded-xl border border-border bg-card p-4 shadow-sm">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="font-medium text-foreground">{section.title}</span>
<span class="text-xs text-muted-foreground">Order: {section.order}</span>
{#if section.icon}
<span class="text-xs text-muted-foreground">({section.icon})</span>
{/if}
</div>
<div class="flex items-center gap-2">
<button
type="submit"
class="rounded bg-red-600 px-2 py-1 text-xs font-medium text-white hover:bg-red-500 transition-colors"
type="button"
onclick={() => (addWidgetSectionId = addWidgetSectionId === section.id ? null : section.id)}
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Delete
Add Widget
</button>
</form>
</div>
</div>
{#if addWidgetSectionId === section.id}
<div class="mb-3 rounded border border-gray-600 bg-gray-700/50 p-3">
<form
method="POST"
action="?/addWidget"
use:enhance={() => {
return async ({ update }) => {
await update();
addWidgetSectionId = null;
};
}}
>
<input type="hidden" name="sectionId" value={section.id} />
<input type="hidden" name="type" value="app" />
<div>
<label for="widget-app-{section.id}" class="mb-1 block text-sm font-medium text-gray-300">Select App</label>
<select
id="widget-app-{section.id}"
name="appId"
class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white focus:border-indigo-500 focus:outline-none"
required
>
<option value="">Choose an app...</option>
{#each data.apps as app (app.id)}
<option value={app.id}>{app.name}</option>
{/each}
</select>
</div>
<div class="mt-2">
<form method="POST" action="?/deleteSection" use:enhance>
<input type="hidden" name="sectionId" value={section.id} />
<button
type="submit"
class="rounded bg-green-600 px-2 py-1 text-xs font-medium text-white hover:bg-green-500 transition-colors"
class="rounded-md bg-destructive px-2 py-1 text-xs font-medium text-destructive-foreground transition-colors hover:bg-destructive/90"
>
Add
Delete
</button>
</div>
</form>
</form>
</div>
</div>
{/if}
<!-- Widgets list -->
{#if section.widgets.length === 0}
<p class="text-sm text-gray-500">No widgets in this section.</p>
{:else}
<div class="space-y-2">
{#each section.widgets as widget (widget.id)}
<div class="flex items-center justify-between rounded border border-gray-600 bg-gray-700/30 px-3 py-2">
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-indigo-400 uppercase">{widget.type}</span>
{#if widget.app}
<span class="text-sm text-white">{widget.app.name}</span>
<span class="text-xs text-gray-400">({widget.app.url})</span>
{:else}
<span class="text-sm text-gray-400">Widget #{widget.order}</span>
{/if}
{#if addWidgetSectionId === section.id}
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
<form
method="POST"
action="?/addWidget"
use:enhance={() => {
return async ({ update }) => {
await update();
addWidgetSectionId = null;
};
}}
>
<input type="hidden" name="sectionId" value={section.id} />
<input type="hidden" name="type" value="app" />
<div>
<label for="widget-app-{section.id}" class="mb-1 block text-sm font-medium text-foreground">Select App</label>
<select
id="widget-app-{section.id}"
name="appId"
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
required
>
<option value="">Choose an app...</option>
{#each data.apps as app (app.id)}
<option value={app.id}>{app.name}</option>
{/each}
</select>
</div>
<form method="POST" action="?/deleteWidget" use:enhance>
<input type="hidden" name="widgetId" value={widget.id} />
<div class="mt-2">
<button
type="submit"
class="rounded bg-red-600 px-2 py-1 text-xs font-medium text-white hover:bg-red-500 transition-colors"
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Remove
Add
</button>
</form>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/if}
</section>
</div>
</form>
</div>
{/if}
<!-- Widgets list -->
{#if section.widgets.length === 0}
<p class="text-sm text-muted-foreground">No widgets in this section.</p>
{:else}
<div class="space-y-2">
{#each section.widgets as widget (widget.id)}
<div class="flex items-center justify-between rounded-lg border border-border bg-background/50 px-3 py-2">
<div class="flex items-center gap-2">
<span class="text-xs font-medium uppercase text-primary">{widget.type}</span>
{#if widget.app}
<span class="text-sm text-foreground">{widget.app.name}</span>
<span class="text-xs text-muted-foreground">({widget.app.url})</span>
{:else}
<span class="text-sm text-muted-foreground">Widget #{widget.order}</span>
{/if}
</div>
<form method="POST" action="?/deleteWidget" use:enhance>
<input type="hidden" name="widgetId" value={widget.id} />
<button
type="submit"
class="rounded-md bg-destructive px-2 py-1 text-xs font-medium text-destructive-foreground transition-colors hover:bg-destructive/90"
>
Remove
</button>
</form>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/if}
</section>
</div>
</div>