feat(ui): migrate entire UI to "Cozy Home" design
Warm, friendly redesign replacing the generic cold-shadcn look. Built as a swappable token bundle so other presets can be added later; dark mode and the user-tunable accent hue are retained. Foundation - app.css: warm cream (light) + "dusk" (dark) token system; terracotta accent (default hue 16); pastel --room-* palette; vivid --status-* (dots/bars) plus AA-legible --status-*-ink (text); soft warm shadows; --radius 1rem; font tokens - Fonts: Fraunces (display) + Figtree (body), self-hosted in static/fonts (no Google CDN) so offline/LAN installs work; system-ui fallbacks kept - h1/h2/h3 render in Fraunces via base layer Chrome and surfaces - Sidebar, Header, home, AppCard/BoardCard, BoardHeader, sections, favorites - 29 widgets + integration renderers: cozy card shells, room-palette charts - Default background is a static warm "cozy" glow (mesh demoted, rAF gated on prefers-reduced-motion) System-wide - Status colors tokenized (no raw bg/text-*-500 or status hex); success/warning to status tokens, categorical to room palette, errors to destructive - Inputs rounded-xl; buttons rounded-xl; cards/dialogs rounded-[1.4rem]; soft-shadow vocabulary only; focus-visible:ring-primary/30 - Forms, admin tables (now cozy cards), dialogs, popovers, auth screens a11y: reduced-motion guards; darker status "ink" text for AA on cream. Known tradeoff: terracotta primary + white button text ~2.96:1 (signature color, user-tunable). Verified: svelte-check 0/0, build ok, 274 tests pass, eslint 0 errors. Design refs + system sheet in design-mockups/.
This commit is contained in:
@@ -96,11 +96,11 @@
|
||||
}
|
||||
|
||||
function actionBadgeClass(action: string): string {
|
||||
if (action.includes('deleted')) return 'bg-red-500/10 text-red-500';
|
||||
if (action.includes('created')) return 'bg-green-500/10 text-green-500';
|
||||
if (action.includes('updated')) return 'bg-blue-500/10 text-blue-500';
|
||||
if (action === 'import') return 'bg-purple-500/10 text-purple-500';
|
||||
if (action === 'export') return 'bg-yellow-500/10 text-yellow-500';
|
||||
if (action.includes('deleted')) return 'bg-destructive/10 text-destructive';
|
||||
if (action.includes('created')) return 'bg-status-online/10 text-status-online-ink';
|
||||
if (action.includes('updated')) return 'bg-room-sky/15 text-room-sky';
|
||||
if (action === 'import') return 'bg-room-lav/15 text-room-lav';
|
||||
if (action === 'export') return 'bg-status-degraded/10 text-status-degraded-ink';
|
||||
return 'bg-muted text-muted-foreground';
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<select
|
||||
id="filter-action"
|
||||
bind:value={filterAction}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
{#each actionOptions as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
@@ -151,7 +151,7 @@
|
||||
<select
|
||||
id="filter-entity"
|
||||
bind:value={filterEntityType}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
{#each entityTypeOptions as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
@@ -165,7 +165,7 @@
|
||||
id="filter-from"
|
||||
type="date"
|
||||
bind:value={filterDateFrom}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -175,14 +175,14 @@
|
||||
id="filter-to"
|
||||
type="date"
|
||||
bind:value={filterDateTo}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={applyFilters}
|
||||
class="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
@@ -190,7 +190,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={exportCsv}
|
||||
class="ml-auto rounded-md border border-input px-4 py-1.5 text-sm font-medium text-foreground hover:bg-accent"
|
||||
class="ml-auto rounded-xl border border-input px-4 py-1.5 text-sm font-medium text-foreground hover:bg-accent"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
@@ -202,7 +202,7 @@
|
||||
<p class="text-muted-foreground">No audit log entries found</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
type="button"
|
||||
onclick={handleCreate}
|
||||
disabled={creating}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{creating ? $t('admin.backup_creating') : $t('admin.backup_create')}
|
||||
</button>
|
||||
@@ -253,7 +253,7 @@
|
||||
type="button"
|
||||
onclick={() => (confirmRestore = backup.filename)}
|
||||
disabled={restoringFilename === backup.filename}
|
||||
class="rounded px-2 py-1 text-xs font-medium text-amber-600 hover:bg-amber-600/10 disabled:opacity-50 dark:text-amber-400"
|
||||
class="rounded-lg px-2 py-1 text-xs font-medium text-status-degraded-ink hover:bg-status-degraded/10 disabled:opacity-50"
|
||||
>
|
||||
{restoringFilename === backup.filename
|
||||
? '...'
|
||||
@@ -282,7 +282,7 @@
|
||||
<!-- Restore Confirmation Dialog -->
|
||||
{#if confirmRestore}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="mx-4 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div class="mx-4 w-full max-w-md rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]">
|
||||
<h3 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('admin.backup_restore_confirm_title')}
|
||||
</h3>
|
||||
@@ -301,7 +301,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => confirmRestore && handleRestore(confirmRestore)}
|
||||
class="rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700"
|
||||
class="rounded-xl px-4 py-2 text-sm font-semibold text-white shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5" style="background: var(--status-degraded);"
|
||||
>
|
||||
{$t('admin.backup_restore')}
|
||||
</button>
|
||||
@@ -313,7 +313,7 @@
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
{#if confirmDelete}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="mx-4 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div class="mx-4 w-full max-w-md rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]">
|
||||
<h3 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('admin.backup_delete_confirm_title')}
|
||||
</h3>
|
||||
@@ -354,7 +354,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={schedule.backupEnabled}
|
||||
class="h-4 w-4 rounded border-border text-primary focus:ring-ring"
|
||||
class="h-4 w-4 rounded border-border text-primary focus-visible:ring-primary/30"
|
||||
/>
|
||||
<span class="text-sm text-foreground">{$t('admin.backup_schedule_enabled')}</span>
|
||||
</label>
|
||||
@@ -368,7 +368,7 @@
|
||||
<select
|
||||
id="cron-preset"
|
||||
bind:value={cronPreset}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-auto"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-auto"
|
||||
>
|
||||
<option value="daily">{$t('admin.backup_schedule_preset_daily')}</option>
|
||||
<option value="twice_daily">{$t('admin.backup_schedule_preset_twice_daily')}</option>
|
||||
@@ -383,7 +383,7 @@
|
||||
type="text"
|
||||
bind:value={customCron}
|
||||
placeholder="0 3 * * *"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-64"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-64"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -399,7 +399,7 @@
|
||||
bind:value={schedule.backupMaxCount}
|
||||
min="1"
|
||||
max="100"
|
||||
class="w-24 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-24 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -408,7 +408,7 @@
|
||||
type="button"
|
||||
onclick={handleSaveSchedule}
|
||||
disabled={savingSchedule}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{savingSchedule ? $t('admin.backup_schedule_saving') : $t('admin.backup_schedule_save')}
|
||||
</button>
|
||||
@@ -418,9 +418,9 @@
|
||||
<!-- Status message -->
|
||||
{#if statusMessage}
|
||||
<div
|
||||
class="mt-4 rounded-md p-3 text-sm {statusType === 'success'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'}"
|
||||
class="mt-4 rounded-xl border p-3 text-sm {statusType === 'success'
|
||||
? 'border-status-online/30 bg-status-online/10 text-status-online-ink'
|
||||
: 'border-destructive/30 bg-destructive/10 text-destructive'}"
|
||||
>
|
||||
{statusMessage}
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
type="button"
|
||||
onclick={handleScan}
|
||||
disabled={scanning || (!dockerSocketPath && !traefikApiUrl)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{scanning ? $t('admin.discovery_scanning') : $t('admin.discovery_scan')}
|
||||
</button>
|
||||
@@ -155,7 +155,7 @@
|
||||
|
||||
<!-- Scan Errors -->
|
||||
{#if scanErrors.length > 0}
|
||||
<div class="mb-4 rounded-md bg-yellow-100 p-3 text-sm text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
<div class="mb-4 rounded-xl border border-status-degraded/30 bg-status-degraded/10 p-3 text-sm text-status-degraded-ink">
|
||||
{#each scanErrors as scanError, idx (idx)}
|
||||
<p>{scanError}</p>
|
||||
{/each}
|
||||
@@ -204,8 +204,8 @@
|
||||
<td class="px-2 py-2">
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
|
||||
{service.source === 'docker'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
? 'bg-room-sky/15 text-room-sky'
|
||||
: 'bg-room-lav/15 text-room-lav'
|
||||
}"
|
||||
>
|
||||
{service.source === 'docker' ? $t('admin.discovery_source_docker') : $t('admin.discovery_source_traefik')}
|
||||
@@ -215,7 +215,7 @@
|
||||
{#if service.alreadyRegistered}
|
||||
<span class="text-xs text-muted-foreground">{$t('admin.discovery_already_registered')}</span>
|
||||
{:else}
|
||||
<span class="text-xs font-medium text-green-600 dark:text-green-400">{$t('admin.discovery_new')}</span>
|
||||
<span class="text-xs font-medium text-status-online-ink dark:text-status-online-ink">{$t('admin.discovery_new')}</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -231,7 +231,7 @@
|
||||
type="button"
|
||||
onclick={handleApprove}
|
||||
disabled={approving || selected.size === 0}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{approving ? $t('admin.discovery_approving') : $t('admin.discovery_approve')} ({selected.size})
|
||||
</button>
|
||||
@@ -241,7 +241,7 @@
|
||||
|
||||
<!-- Status Message -->
|
||||
{#if statusMessage}
|
||||
<div class="mt-4 rounded-md p-3 text-sm {statusType === 'success' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'}">
|
||||
<div class="mt-4 rounded-xl border p-3 text-sm {statusType === 'success' ? 'border-status-online/30 bg-status-online/10 text-status-online-ink' : 'border-destructive/30 bg-destructive/10 text-destructive'}">
|
||||
{statusMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
<!-- Existing permissions list -->
|
||||
{#if permissions.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
id="authMode"
|
||||
name="authMode"
|
||||
bind:value={$form.authMode}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="local">{$t('admin.auth_local')}</option>
|
||||
<option value="oauth">{$t('admin.auth_oauth')}</option>
|
||||
@@ -92,7 +92,7 @@
|
||||
name="oauthClientId"
|
||||
type="text"
|
||||
bind:value={$form.oauthClientId}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_client_id_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
name="oauthClientSecret"
|
||||
type="password"
|
||||
bind:value={$form.oauthClientSecret}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_client_secret_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@
|
||||
name="oauthDiscoveryUrl"
|
||||
type="url"
|
||||
bind:value={$form.oauthDiscoveryUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_discovery_url_placeholder')}
|
||||
/>
|
||||
{#if $errors.oauthDiscoveryUrl}<span class="text-xs text-destructive">{$errors.oauthDiscoveryUrl}</span>{/if}
|
||||
@@ -124,12 +124,12 @@
|
||||
type="button"
|
||||
onclick={testOAuthConnection}
|
||||
disabled={oauthTesting}
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{oauthTesting ? $t('admin.oauth_testing') : $t('admin.oauth_test')}
|
||||
</button>
|
||||
{#if oauthTestResult}
|
||||
<span class="ml-3 text-sm {oauthTestSuccess ? 'text-green-600 dark:text-green-400' : 'text-destructive'}">
|
||||
<span class="ml-3 text-sm {oauthTestSuccess ? 'text-status-online-ink dark:text-status-online-ink' : 'text-destructive'}">
|
||||
{oauthTestResult}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -147,7 +147,7 @@
|
||||
id="defaultTheme"
|
||||
name="defaultTheme"
|
||||
bind:value={$form.defaultTheme}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="dark">{$t('theme.dark')}</option>
|
||||
<option value="light">{$t('theme.light')}</option>
|
||||
@@ -161,8 +161,8 @@
|
||||
name="defaultPrimaryColor"
|
||||
type="text"
|
||||
bind:value={$form.defaultPrimaryColor}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="#6366f1"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="#e8754f"
|
||||
pattern="^#[0-9a-fA-F]{6}$"
|
||||
/>
|
||||
{#if $form.defaultPrimaryColor}
|
||||
@@ -188,7 +188,7 @@
|
||||
name="healthcheckDefaults"
|
||||
bind:value={$form.healthcheckDefaults}
|
||||
rows="4"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-sm text-foreground"
|
||||
placeholder={'{"interval": 300, "timeout": 5000, "method": "GET"}'}
|
||||
></textarea>
|
||||
{#if $errors.healthcheckDefaults}<span class="text-xs text-destructive">{$errors.healthcheckDefaults}</span>{/if}
|
||||
@@ -206,7 +206,7 @@
|
||||
id="dockerSocketPath"
|
||||
type="text"
|
||||
bind:value={dockerSocketPath}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="/var/run/docker.sock"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_docker_socket_hint')}</p>
|
||||
@@ -217,7 +217,7 @@
|
||||
id="traefikApiUrl"
|
||||
type="url"
|
||||
bind:value={traefikApiUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="http://traefik:8080"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_traefik_url_hint')}</p>
|
||||
@@ -244,7 +244,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
disabled={$delayed}
|
||||
>
|
||||
{$delayed ? $t('admin.saving_settings') : $t('admin.save_settings')}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
// Create form
|
||||
let newName = $state('');
|
||||
let newColor = $state('#6366f1');
|
||||
let newColor = $state('#e8754f');
|
||||
let showCreateForm = $state(false);
|
||||
|
||||
// Edit form
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let editName = $state('');
|
||||
let editColor = $state('#6366f1');
|
||||
let editColor = $state('#e8754f');
|
||||
|
||||
// Delete confirmation
|
||||
let confirmDeleteId = $state<string | null>(null);
|
||||
@@ -56,7 +56,7 @@
|
||||
});
|
||||
if (res.ok) {
|
||||
newName = '';
|
||||
newColor = '#6366f1';
|
||||
newColor = '#e8754f';
|
||||
showCreateForm = false;
|
||||
await loadTags();
|
||||
} else {
|
||||
@@ -71,7 +71,7 @@
|
||||
function startEdit(tag: Tag) {
|
||||
editingTag = tag;
|
||||
editName = tag.name;
|
||||
editColor = tag.color ?? '#6366f1';
|
||||
editColor = tag.color ?? '#e8754f';
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
@@ -118,7 +118,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{showCreateForm ? 'Cancel' : 'New Tag'}
|
||||
</button>
|
||||
@@ -141,7 +141,7 @@
|
||||
type="text"
|
||||
bind:value={newName}
|
||||
placeholder="Tag name"
|
||||
class="rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Create Tag
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
let selectedGroupId = $state('');
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -126,4 +126,13 @@
|
||||
.status-ring-unknown {
|
||||
animation: ring-rotate-dash 8s linear infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.status-ring-online,
|
||||
.status-ring-offline,
|
||||
.status-ring-degraded,
|
||||
.status-ring-unknown {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,6 +58,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Cozy "room" pastel tint — stable per app, derived from its name
|
||||
const roomTints = ['var(--room-terra)', 'var(--room-sky)', 'var(--room-sage)', 'var(--room-butter)', 'var(--room-lav)', 'var(--room-peach)'];
|
||||
const tint = $derived.by(() => {
|
||||
let h = 0;
|
||||
for (const ch of app.name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
||||
return roomTints[h % roomTints.length];
|
||||
});
|
||||
|
||||
const iconDisplay = $derived.by(() => {
|
||||
if (!app.icon) return null;
|
||||
|
||||
@@ -82,32 +90,39 @@
|
||||
tabindex="0"
|
||||
onclick={() => window.open(app.url, '_blank', 'noopener,noreferrer')}
|
||||
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') window.open(app.url, '_blank', 'noopener,noreferrer'); }}
|
||||
class="card-hover group flex cursor-pointer flex-col rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/50"
|
||||
class="card-hover group relative flex cursor-pointer flex-col overflow-hidden rounded-[1.4rem] border border-border bg-card p-5 shadow-[var(--shadow-soft)]"
|
||||
title={app.description ?? app.name}
|
||||
>
|
||||
<!-- soft blob accent -->
|
||||
<span
|
||||
class="pointer-events-none absolute -right-10 -top-12 h-28 w-28 rounded-full opacity-40 blur-2xl transition-opacity duration-300 group-hover:opacity-70"
|
||||
style="background: {tint};"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="mb-3 flex items-start justify-between">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-muted text-lg text-muted-foreground"
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl text-lg"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: {tint};"
|
||||
>
|
||||
{#if iconDisplay?.kind === 'emoji'}
|
||||
<span class="text-xl">{iconDisplay.value}</span>
|
||||
<span class="text-2xl">{iconDisplay.value}</span>
|
||||
{:else if iconDisplay?.kind === 'image'}
|
||||
<img
|
||||
src={iconDisplay.src}
|
||||
alt="{app.name} icon"
|
||||
class="h-6 w-6 rounded object-contain"
|
||||
class="h-7 w-7 rounded-lg object-contain"
|
||||
/>
|
||||
{:else if iconDisplay?.kind === 'text'}
|
||||
<span class="text-xs font-medium">{iconDisplay.value}</span>
|
||||
<span class="text-sm font-bold">{iconDisplay.value}</span>
|
||||
{:else}
|
||||
<span class="text-xs font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
||||
<span class="text-sm font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<a
|
||||
href="/apps/{app.id}/edit"
|
||||
onclick={(e: MouseEvent) => e.stopPropagation()}
|
||||
class="rounded-md p-1 text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||
class="rounded-xl p-1.5 text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||
title={$t('app.edit')}
|
||||
>
|
||||
<svg
|
||||
@@ -128,12 +143,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="truncate text-sm font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
||||
<h3 class="truncate font-display text-base font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
||||
{app.name}
|
||||
</h3>
|
||||
|
||||
{#if app.description}
|
||||
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{app.description}</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">{app.description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Sparkline -->
|
||||
@@ -143,14 +158,15 @@
|
||||
<div class="mt-2 flex items-center gap-1.5">
|
||||
<SparklineChart data={historyData} />
|
||||
{#if uptimePercent !== null}
|
||||
<span class="text-[10px] text-muted-foreground">{uptimePercent}% {$t('app.uptime')}</span>
|
||||
<span class="text-[11px] font-medium text-muted-foreground">{uptimePercent}% {$t('app.uptime')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.category}
|
||||
<span
|
||||
class="mt-2 inline-block self-start rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
class="mt-3 inline-block self-start rounded-full px-2.5 py-0.5 text-[11px] font-semibold"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: color-mix(in srgb, {tint} 68%, var(--foreground));"
|
||||
>
|
||||
{app.category}
|
||||
</span>
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.name_placeholder')}
|
||||
/>
|
||||
{#if $errors.name}
|
||||
@@ -138,7 +138,7 @@
|
||||
name="url"
|
||||
type="url"
|
||||
bind:value={$form.url}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.url_placeholder')}
|
||||
/>
|
||||
{#if $errors.url}
|
||||
@@ -170,7 +170,7 @@
|
||||
name="description"
|
||||
type="text"
|
||||
bind:value={$form.description}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.description_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@
|
||||
bind:value={$form.category}
|
||||
suggestions={categorySuggestions}
|
||||
placeholder={$t('app.category_placeholder')}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
bind:value={$form.tags}
|
||||
suggestions={tagSuggestions}
|
||||
placeholder={$t('app.tags_placeholder')}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@
|
||||
name="healthcheckExpectedStatus"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckExpectedStatus}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="100"
|
||||
max="599"
|
||||
/>
|
||||
@@ -287,7 +287,7 @@
|
||||
name="healthcheckTimeout"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckTimeout}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="1000"
|
||||
max="30000"
|
||||
step="1000"
|
||||
@@ -307,7 +307,7 @@
|
||||
name="healthcheckInterval"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckInterval}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="30"
|
||||
max="86400"
|
||||
/>
|
||||
@@ -349,7 +349,7 @@
|
||||
id="integrationType"
|
||||
name="integrationType"
|
||||
bind:value={$form.integrationType}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{#each availableIntegrations as integration (integration.id)}
|
||||
@@ -395,7 +395,7 @@
|
||||
{testingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
{#if testResult}
|
||||
<span class="text-sm {testResult.success ? 'text-green-500' : 'text-destructive'}">
|
||||
<span class="text-sm {testResult.success ? 'text-status-online-ink' : 'text-destructive'}">
|
||||
{testResult.message}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -412,7 +412,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
disabled={$submitting}
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if $submitting}
|
||||
{$t('app.saving')}
|
||||
|
||||
@@ -10,18 +10,24 @@
|
||||
const config = $derived.by(() => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return { color: 'bg-green-500', cssClass: 'status-online', textKey: 'status.online' };
|
||||
return { color: 'var(--status-online)', ink: 'var(--status-online-ink)', cssClass: 'status-online', textKey: 'status.online' };
|
||||
case 'offline':
|
||||
return { color: 'bg-red-500', cssClass: '', textKey: 'status.offline' };
|
||||
return { color: 'var(--status-offline)', ink: 'var(--status-offline-ink)', cssClass: '', textKey: 'status.offline' };
|
||||
case 'degraded':
|
||||
return { color: 'bg-yellow-500', cssClass: '', textKey: 'status.degraded' };
|
||||
return { color: 'var(--status-degraded)', ink: 'var(--status-degraded-ink)', cssClass: '', textKey: 'status.degraded' };
|
||||
default:
|
||||
return { color: 'bg-gray-500', cssClass: '', textKey: 'status.unknown' };
|
||||
return { color: 'var(--status-unknown)', ink: 'var(--status-unknown-ink)', cssClass: '', textKey: 'status.unknown' };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center gap-1.5 text-xs">
|
||||
<span class="inline-block h-2 w-2 rounded-full {config.color} {config.cssClass}"></span>
|
||||
<span class="text-muted-foreground">{$t(config.textKey)}</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold"
|
||||
style="color: {config.ink}; background: color-mix(in srgb, {config.color} 14%, transparent);"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full {config.cssClass}"
|
||||
style="background: {config.color};"
|
||||
></span>
|
||||
<span>{$t(config.textKey)}</span>
|
||||
</span>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
: iconType === 'url'
|
||||
? $t('app.icon_url_placeholder')
|
||||
: $t('app.icon_emoji_placeholder')}
|
||||
class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -164,13 +164,13 @@
|
||||
type="text"
|
||||
bind:value={newLabel}
|
||||
placeholder="Link label"
|
||||
class="rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={newUrl}
|
||||
placeholder="https://..."
|
||||
class="rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
@@ -178,13 +178,13 @@
|
||||
type="text"
|
||||
bind:value={newIcon}
|
||||
placeholder="Icon (optional)"
|
||||
class="flex-1 rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={addLink}
|
||||
disabled={!newLabel.trim() || !newUrl.trim()}
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -196,7 +196,7 @@
|
||||
type="button"
|
||||
onclick={saveLinks}
|
||||
disabled={saving}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Links'}
|
||||
</button>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
const statusColor = $derived(() => {
|
||||
if (!result) return '';
|
||||
if (result.error) return 'text-destructive';
|
||||
if (result.status >= 200 && result.status < 300) return 'text-green-500';
|
||||
if (result.status >= 300 && result.status < 400) return 'text-yellow-500';
|
||||
if (result.status >= 200 && result.status < 300) return 'text-status-online-ink';
|
||||
if (result.status >= 300 && result.status < 400) return 'text-status-degraded-ink';
|
||||
return 'text-destructive';
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
let { fields, values, onchange, idPrefix = 'int' }: Props = $props();
|
||||
|
||||
const inputClass = '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';
|
||||
const inputClass = 'w-full rounded-xl 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-visible:ring-2 focus-visible:ring-primary/30';
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
let { data, width = 80, height = 20 }: Props = $props();
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
online: '#22c55e',
|
||||
offline: '#ef4444',
|
||||
degraded: '#eab308',
|
||||
unknown: '#6b7280'
|
||||
online: 'var(--status-online)',
|
||||
offline: 'var(--status-offline)',
|
||||
degraded: 'var(--status-degraded)',
|
||||
unknown: 'var(--status-unknown)'
|
||||
};
|
||||
|
||||
const barWidth = $derived(data.length > 0 ? Math.max(1, (width - 2) / data.length) : 1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme.svelte.js';
|
||||
import CozyAmbient from './CozyAmbient.svelte';
|
||||
import MeshGradient from './MeshGradient.svelte';
|
||||
import ParticleField from './ParticleField.svelte';
|
||||
import AuroraEffect from './AuroraEffect.svelte';
|
||||
@@ -16,7 +17,9 @@
|
||||
|
||||
{#if theme.backgroundType !== 'none'}
|
||||
<div class="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
|
||||
{#if theme.backgroundType === 'mesh'}
|
||||
{#if theme.backgroundType === 'cozy'}
|
||||
<CozyAmbient />
|
||||
{:else if theme.backgroundType === 'mesh'}
|
||||
<MeshGradient />
|
||||
{:else if theme.backgroundType === 'particles'}
|
||||
<ParticleField />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
Cozy Home ambient backdrop — static, soft warm-corner radial gradients.
|
||||
Calm "lit room" atmosphere (no animation), retints with the accent hue.
|
||||
-->
|
||||
<div class="cozy-ambient absolute inset-0"></div>
|
||||
|
||||
<style>
|
||||
.cozy-ambient {
|
||||
background:
|
||||
radial-gradient(50% 42% at 12% 0%, color-mix(in srgb, var(--room-peach) 26%, transparent), transparent 70%),
|
||||
radial-gradient(45% 40% at 95% 6%, color-mix(in srgb, var(--room-sky) 22%, transparent), transparent 70%),
|
||||
radial-gradient(52% 46% at 85% 100%, color-mix(in srgb, var(--room-sage) 20%, transparent), transparent 72%),
|
||||
radial-gradient(46% 42% at 8% 96%, color-mix(in srgb, var(--room-lav) 16%, transparent), transparent 72%);
|
||||
}
|
||||
|
||||
:global(.dark) .cozy-ambient {
|
||||
background:
|
||||
radial-gradient(52% 44% at 12% 0%, color-mix(in srgb, var(--room-terra) 20%, transparent), transparent 70%),
|
||||
radial-gradient(46% 42% at 95% 6%, color-mix(in srgb, var(--room-sky) 16%, transparent), transparent 70%),
|
||||
radial-gradient(54% 48% at 85% 100%, color-mix(in srgb, var(--room-sage) 14%, transparent), transparent 72%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -42,6 +42,13 @@
|
||||
|
||||
$effect(() => {
|
||||
blobs = initBlobs();
|
||||
|
||||
// Respect reduced-motion: render a static mesh, skip the rAF loop.
|
||||
const prefersReducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
animFrame = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -34,21 +34,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-xl border border-border bg-card p-3 shadow-sm">
|
||||
<div class="flex items-center gap-2 rounded-xl border border-border bg-card p-3 shadow-[var(--shadow-soft)]">
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="text"
|
||||
bind:value={title}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder={$t('board.section_title') ?? 'Section title...'}
|
||||
class="flex-1 rounded-lg border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
<IconPickerButton value={icon} onchange={(v) => { icon = v; }} size="sm" />
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSubmit}
|
||||
disabled={!title.trim()}
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add') ?? 'Add'}
|
||||
</button>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
class="w-full rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
{#if searchQuery.length > 0 && filteredTargetOptions.length > 0}
|
||||
<div class="absolute z-10 mt-1 max-h-40 w-full overflow-y-auto rounded border border-border bg-card shadow-lg">
|
||||
<div class="absolute z-10 mt-1 max-h-40 w-full overflow-y-auto rounded border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filteredTargetOptions as option (option.id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -190,7 +190,7 @@
|
||||
{#if loading}
|
||||
<p class="text-sm text-muted-foreground">{$t('board.access_loading')}</p>
|
||||
{:else if permissions.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,32 +20,53 @@
|
||||
let { board }: Props = $props();
|
||||
|
||||
const sectionCount = $derived(board._count?.sections ?? 0);
|
||||
|
||||
// Stable per-board pastel "room" tint derived from the name
|
||||
const roomTints = ['var(--room-terra)', 'var(--room-sky)', 'var(--room-sage)', 'var(--room-butter)', 'var(--room-lav)', 'var(--room-peach)'];
|
||||
const tint = $derived.by(() => {
|
||||
let h = 0;
|
||||
for (const ch of board.name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
||||
return roomTints[h % roomTints.length];
|
||||
});
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/boards/{board.id}"
|
||||
class="card-hover group block rounded-xl border border-border bg-card p-5 transition-colors hover:border-primary/50"
|
||||
class="card-hover group relative block overflow-hidden rounded-[1.4rem] border border-border bg-card p-5 shadow-[var(--shadow-soft)]"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<span
|
||||
class="pointer-events-none absolute -right-10 -top-12 h-28 w-28 rounded-full opacity-40 blur-2xl transition-opacity duration-300 group-hover:opacity-70"
|
||||
style="background: {tint};"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="flex items-start gap-3.5">
|
||||
{#if board.icon}
|
||||
<DynamicIcon name={board.icon} size={22} />
|
||||
<span
|
||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: {tint};"
|
||||
>
|
||||
<DynamicIcon name={board.icon} size={22} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-sm text-muted-foreground">
|
||||
B
|
||||
<span
|
||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl text-sm font-bold text-white"
|
||||
style="background: {tint};"
|
||||
>
|
||||
{board.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="truncate font-semibold text-foreground transition-colors group-hover:text-primary">
|
||||
<h3 class="truncate font-display text-base font-semibold text-foreground transition-colors group-hover:text-primary">
|
||||
{board.name}
|
||||
</h3>
|
||||
{#if board.isDefault}
|
||||
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary">
|
||||
<span class="shrink-0 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
||||
{$t('board.default')}
|
||||
</span>
|
||||
{/if}
|
||||
{#if board.isGuestAccessible}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-accent px-1.5 py-0.5 text-xs text-accent-foreground" title={$t('board.guest_accessible')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground" title={$t('board.guest_accessible')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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" />
|
||||
@@ -54,7 +75,7 @@
|
||||
{$t('board.guest')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground" title={$t('board.access_private')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground" title={$t('board.access_private')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
@@ -62,7 +83,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
{#if board.hasSharedPermissions}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-blue-500/15 px-1.5 py-0.5 text-xs text-blue-500" title={$t('board.access_shared')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--room-sky) 18%, transparent); color: color-mix(in srgb, var(--room-sky) 70%, var(--foreground));" title={$t('board.access_shared')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
|
||||
@@ -29,13 +29,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div class="flex items-center gap-3.5">
|
||||
{#if icon}
|
||||
<DynamicIcon name={icon} size={28} />
|
||||
<span
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl text-primary shadow-[var(--shadow-soft)]"
|
||||
style="background: color-mix(in srgb, var(--primary) 14%, transparent);"
|
||||
>
|
||||
<DynamicIcon name={icon} size={26} />
|
||||
</span>
|
||||
{/if}
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-foreground">{name}</h1>
|
||||
<h1 class="font-display text-3xl font-semibold text-foreground">{name}</h1>
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
@@ -45,7 +50,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/boards"
|
||||
class="rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="rounded-xl border border-border bg-card px-3.5 py-2 text-sm font-medium text-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:border-primary/40"
|
||||
>
|
||||
{$t('board.all_boards')}
|
||||
</a>
|
||||
@@ -53,7 +58,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={onShare}
|
||||
class="flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-1.5 rounded-xl border border-border bg-card px-3.5 py-2 text-sm font-medium text-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:border-primary/40"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
@@ -69,9 +74,9 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleEditToggle}
|
||||
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors {editMode.active
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary/30'
|
||||
: 'bg-primary text-primary-foreground hover:bg-primary/90'}"
|
||||
class="flex items-center gap-1.5 rounded-xl px-3.5 py-2 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 {editMode.active
|
||||
? 'bg-primary ring-2 ring-primary/30'
|
||||
: 'bg-primary hover:shadow-[var(--shadow-lift)]'}"
|
||||
>
|
||||
{#if editMode.active}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<!-- Side panel -->
|
||||
<div
|
||||
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-md flex-col border-l border-border bg-card shadow-2xl"
|
||||
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-md flex-col border-l border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
transition:fly={{ x: 400, duration: 250 }}
|
||||
>
|
||||
<!-- Header -->
|
||||
@@ -107,7 +107,7 @@
|
||||
<div>
|
||||
<label for="bp-name" class="mb-1 block text-sm font-medium text-foreground">{$t('common.name') ?? 'Name'}</label>
|
||||
<input id="bp-name" type="text" bind:value={name}
|
||||
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" />
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30" />
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
@@ -121,7 +121,7 @@
|
||||
<div>
|
||||
<label for="bp-desc" class="mb-1 block text-sm font-medium text-foreground">{$t('common.description') ?? 'Description'}</label>
|
||||
<textarea id="bp-desc" rows="2" bind:value={description}
|
||||
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"></textarea>
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Theme Hue -->
|
||||
@@ -144,7 +144,7 @@
|
||||
<div>
|
||||
<label for="bp-bg" class="mb-1 block text-sm font-medium text-foreground">{$t('board.background') ?? 'Background'}</label>
|
||||
<select id="bp-bg" bind:value={backgroundType}
|
||||
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">
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30">
|
||||
<option value="none">None</option>
|
||||
<option value="mesh">Mesh Gradient</option>
|
||||
<option value="particles">Particles</option>
|
||||
@@ -159,7 +159,7 @@
|
||||
<div>
|
||||
<label for="bp-wp-url" class="mb-1 block text-sm font-medium text-foreground">Wallpaper URL</label>
|
||||
<input id="bp-wp-url" type="text" bind:value={wallpaperUrl} placeholder="https://..."
|
||||
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" />
|
||||
class="w-full rounded-xl 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-visible:ring-2 focus-visible:ring-primary/30" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="bp-wp-blur" class="mb-1 block text-sm font-medium text-foreground">Blur ({wallpaperBlur}px)</label>
|
||||
@@ -176,7 +176,7 @@
|
||||
<div>
|
||||
<label for="bp-cardsize" class="mb-1 block text-sm font-medium text-foreground">{$t('board.card_size') ?? 'Card Size'}</label>
|
||||
<select id="bp-cardsize" bind:value={cardSize}
|
||||
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">
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30">
|
||||
<option value="compact">Compact</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large">Large</option>
|
||||
@@ -187,7 +187,7 @@
|
||||
<div>
|
||||
<label for="bp-css" class="mb-1 block text-sm font-medium text-foreground">{$t('board.custom_css') ?? 'Custom CSS'}</label>
|
||||
<textarea id="bp-css" rows="4" bind:value={customCss} placeholder={'.board { ... }'}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 font-mono text-xs text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"></textarea>
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-xs text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSave}
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('common.apply') ?? 'Apply'}
|
||||
</button>
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
onclick={handleBackdropClick}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="mx-4 w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-xl" role="dialog" aria-modal="true">
|
||||
<div class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]" role="dialog" aria-modal="true">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-card-foreground">
|
||||
@@ -177,7 +177,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleCopyLink}
|
||||
class="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-2 rounded-xl border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
@@ -210,7 +210,7 @@
|
||||
<select
|
||||
bind:value={selectedTargetType}
|
||||
onchange={() => { selectedTargetId = ''; searchQuery = ''; }}
|
||||
class="rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value={TargetType.USER}>{$t('admin.perm_user')}</option>
|
||||
<option value={TargetType.GROUP}>{$t('admin.perm_group')}</option>
|
||||
@@ -220,10 +220,10 @@
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder={$t('board.access_search_placeholder')}
|
||||
class="w-full rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
{#if searchQuery.length > 0 && filteredTargetOptions.length > 0 && !selectedTargetId}
|
||||
<div class="absolute z-10 mt-1 max-h-32 w-full overflow-y-auto rounded border border-border bg-card shadow-lg">
|
||||
<div class="absolute z-10 mt-1 max-h-32 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filteredTargetOptions as option (option.id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -238,7 +238,7 @@
|
||||
</div>
|
||||
<select
|
||||
bind:value={selectedLevel}
|
||||
class="rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value={PermissionLevel.VIEW}>{$t('admin.perm_view')}</option>
|
||||
<option value={PermissionLevel.EDIT}>{$t('admin.perm_edit')}</option>
|
||||
@@ -248,7 +248,7 @@
|
||||
type="button"
|
||||
onclick={handleGrant}
|
||||
disabled={!selectedTargetId}
|
||||
class="shrink-0 rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="shrink-0 rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add')}
|
||||
</button>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
transition:fly={{ y: 60, duration: 250 }}
|
||||
>
|
||||
<!-- Toolbar pill -->
|
||||
<div class="flex items-center gap-1 rounded-2xl border border-border bg-card/95 px-2 py-1.5 shadow-xl backdrop-blur-sm">
|
||||
<div class="flex items-center gap-1 rounded-2xl border border-border bg-card/95 px-2 py-1.5 shadow-[var(--shadow-lift)] backdrop-blur-sm">
|
||||
<!-- Save -->
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
onclick={() => selectTemplate(template.id)}
|
||||
class="flex flex-col items-center gap-2 rounded-lg border-2 p-4 text-center transition-colors {selected === template.id ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'}"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10">
|
||||
{#if template.icon}
|
||||
<DynamicIcon name={template.icon} size={20} />
|
||||
{:else}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</script>
|
||||
|
||||
{#if favorites.hasFavorites}
|
||||
<div class="mb-4 rounded-lg border border-border bg-card/50 px-3 py-2 backdrop-blur-sm">
|
||||
<div class="mb-4 rounded-2xl border border-border bg-card/60 px-3 py-2.5 shadow-[var(--shadow-soft)] backdrop-blur-sm">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
use:dndzone={{
|
||||
@@ -75,7 +75,7 @@
|
||||
href={item.app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group relative flex items-center gap-1.5 rounded-md bg-muted/50 px-2.5 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
class="group relative flex items-center gap-1.5 rounded-xl bg-muted/60 px-3 py-1.5 text-xs font-semibold text-foreground transition-all hover:-translate-y-0.5 hover:bg-accent hover:text-accent-foreground hover:shadow-[var(--shadow-soft)]"
|
||||
title={item.app.name}
|
||||
oncontextmenu={(e) => handleRemove(e, item.appId)}
|
||||
>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
|
||||
const bgOptions: { value: BackgroundType; labelKey: string }[] = [
|
||||
{ value: 'cozy', labelKey: 'bg.cozy' },
|
||||
{ value: 'mesh', labelKey: 'bg.mesh' },
|
||||
{ value: 'particles', labelKey: 'bg.particles' },
|
||||
{ value: 'aurora', labelKey: 'bg.aurora' },
|
||||
@@ -29,14 +30,14 @@
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-border bg-background/80 px-4 backdrop-blur-sm"
|
||||
class="sticky top-0 z-20 flex h-16 items-center gap-3 bg-background/70 px-5 backdrop-blur-md"
|
||||
>
|
||||
<!-- Mobile hamburger -->
|
||||
{#if ui.isMobile}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ui.toggleSidebar()}
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
aria-label={$t('sidebar.toggle')}
|
||||
>
|
||||
<svg
|
||||
@@ -64,7 +65,7 @@
|
||||
<!-- Background selector -->
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title={$t('bg.title')}
|
||||
aria-label={$t('bg.aria_label')}
|
||||
>
|
||||
@@ -84,13 +85,13 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
class="z-50 w-44 rounded-md border border-border bg-popover p-1 shadow-lg"
|
||||
class="z-50 w-44 rounded-2xl border border-border bg-popover p-1.5 shadow-[var(--shadow-lift)]"
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
>
|
||||
{#each bgOptions as opt (opt.value)}
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors {theme.backgroundType === opt.value
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-2.5 py-2 text-sm transition-colors {theme.backgroundType === opt.value
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-popover-foreground hover:bg-accent/50'}"
|
||||
onSelect={() => theme.setBackground(opt.value)}
|
||||
@@ -131,10 +132,11 @@
|
||||
{#if user}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-2.5 rounded-2xl px-2 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
<span
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-xl text-xs font-bold text-white shadow-[var(--shadow-soft)]"
|
||||
style="background: linear-gradient(135deg, var(--room-lav), var(--room-sky));"
|
||||
>
|
||||
{user.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -144,7 +146,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
class="z-50 w-48 rounded-md border border-border bg-popover p-1 shadow-lg"
|
||||
class="z-50 w-48 rounded-2xl border border-border bg-popover p-1.5 shadow-[var(--shadow-lift)]"
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
>
|
||||
@@ -154,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={() => goto('/settings')}
|
||||
>
|
||||
<svg
|
||||
@@ -174,7 +176,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={() => goto('/settings/api-tokens')}
|
||||
>
|
||||
<svg
|
||||
@@ -197,7 +199,7 @@
|
||||
|
||||
<form method="POST" action="/auth/logout" id="logout-form" class="contents">
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={submitLogout}
|
||||
>
|
||||
<svg
|
||||
@@ -223,7 +225,7 @@
|
||||
{:else}
|
||||
<a
|
||||
href="/login"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('auth.login')}
|
||||
</a>
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
{#if visible}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-lg items-center gap-3 rounded-xl border border-border bg-card p-4 shadow-lg"
|
||||
class="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-lg items-center gap-3 rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<Download class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={install}
|
||||
class="shrink-0 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="shrink-0 rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('install.button')}
|
||||
</button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleLocale}
|
||||
class="inline-flex items-center justify-center rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-xl text-xs font-bold text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
title={$locale === 'ru' ? 'Switch to English' : 'Переключить на русский'}
|
||||
>
|
||||
{$locale === 'ru' ? 'RU' : 'EN'}
|
||||
|
||||
@@ -24,19 +24,32 @@
|
||||
function isActive(path: string): boolean {
|
||||
return $page.url.pathname.startsWith(path);
|
||||
}
|
||||
|
||||
// Cozy "room" accent palette — board chips rotate through these
|
||||
const roomColors = [
|
||||
'var(--room-terra)',
|
||||
'var(--room-sky)',
|
||||
'var(--room-sage)',
|
||||
'var(--room-butter)',
|
||||
'var(--room-lav)',
|
||||
'var(--room-peach)'
|
||||
];
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200"
|
||||
class="flex h-full flex-col bg-sidebar p-3 transition-all duration-200"
|
||||
class:w-64={!collapsed}
|
||||
class:w-16={collapsed}
|
||||
class:w-[4.75rem]={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">
|
||||
<div class="mb-5 flex h-12 items-center px-1.5 {collapsed ? 'justify-center' : 'gap-3'}">
|
||||
<a href="/" class="flex items-center gap-3 text-sidebar-foreground" title={$t('app_name')}>
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl text-primary-foreground shadow-[var(--shadow-soft)]"
|
||||
style="background: linear-gradient(135deg, var(--room-peach), var(--room-terra));"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-sidebar-primary"
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -45,62 +58,49 @@
|
||||
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" />
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">{$t('app_name')}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<a href="/" class="mx-auto text-sidebar-primary" title={$t('app_name')}>
|
||||
<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}
|
||||
</span>
|
||||
{#if !collapsed}
|
||||
<span class="leading-tight">
|
||||
<span class="block font-display text-base font-semibold">{$t('app_name')}</span>
|
||||
<span class="block text-[11px] font-medium text-sidebar-foreground/50">home cloud</span>
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 overflow-y-auto px-2 py-3">
|
||||
<nav class="flex flex-1 flex-col overflow-y-auto">
|
||||
<!-- Main Links -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-2">
|
||||
{#if !collapsed}
|
||||
<p class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
||||
<p class="mb-1.5 px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45">
|
||||
{$t('nav.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'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/boards')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.boards') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</svg>
|
||||
@@ -109,44 +109,42 @@
|
||||
|
||||
<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'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/apps')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.apps') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
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"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z" />
|
||||
</svg>
|
||||
{#if !collapsed}<span>{$t('nav.apps')}</span>{/if}
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/status"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/status')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/status')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? 'Status Page' : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
@@ -156,18 +154,18 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Board List -->
|
||||
<!-- Board List ("Rooms") -->
|
||||
{#if boards.length > 0}
|
||||
<div class="mb-3">
|
||||
<div class="mb-2 mt-1">
|
||||
{#if !collapsed}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (boardsExpanded = !boardsExpanded)}
|
||||
class="mb-1 flex w-full items-center justify-between px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50 transition-colors hover:text-sidebar-foreground/80"
|
||||
class="mb-1.5 flex w-full items-center justify-between px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45 transition-colors hover:text-sidebar-foreground/70"
|
||||
>
|
||||
<span>{$t('nav.boards')}</span>
|
||||
<svg
|
||||
class="h-3 w-3 transition-transform duration-200"
|
||||
class="h-3.5 w-3.5 transition-transform duration-200"
|
||||
class:rotate-180={boardsExpanded}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -182,13 +180,13 @@
|
||||
{/if}
|
||||
|
||||
{#if boardsExpanded || collapsed}
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
{#each boards as board (board.id)}
|
||||
<div class="max-h-56 overflow-y-auto">
|
||||
{#each boards as board, i (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'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all {isActive(`/boards/${board.id}`)
|
||||
? 'bg-card text-sidebar-foreground shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? board.name : undefined}
|
||||
onclick={() => ui.closeMobileSidebar()}
|
||||
>
|
||||
@@ -196,7 +194,8 @@
|
||||
<span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></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"
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg text-[11px] font-bold text-white"
|
||||
style="background: {roomColors[i % roomColors.length]};"
|
||||
>
|
||||
{board.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -213,29 +212,27 @@
|
||||
|
||||
<!-- Admin -->
|
||||
{#if isAdmin}
|
||||
<div class="mt-auto border-t border-sidebar-border pt-3">
|
||||
<div class="mt-auto pt-2">
|
||||
{#if !collapsed}
|
||||
<p
|
||||
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
|
||||
>
|
||||
<p class="mb-1.5 px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45">
|
||||
{$t('nav.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'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/admin')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.admin_panel') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
@@ -252,11 +249,11 @@
|
||||
|
||||
<!-- Keyboard Shortcuts Hint + Collapse Toggle -->
|
||||
{#if !ui.isMobile}
|
||||
<div class="border-t border-sidebar-border p-2 flex items-center {collapsed ? 'flex-col gap-1' : 'gap-1'}">
|
||||
<div class="mt-2 flex items-center {collapsed ? 'flex-col gap-1.5' : 'gap-1.5'}">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => keyboard.toggleOverlay()}
|
||||
class="flex items-center justify-center rounded-md p-2 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
class="flex items-center justify-center rounded-xl p-2.5 text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
title="Keyboard Shortcuts (?)"
|
||||
>
|
||||
<svg
|
||||
@@ -277,7 +274,7 @@
|
||||
<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"
|
||||
class="flex w-full items-center justify-center rounded-xl p-2.5 text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
title={collapsed ? $t('sidebar.expand') : $t('sidebar.collapse')}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => theme.cycleMode()}
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
title={$t('theme.title', { values: { mode: $t(currentIcon.labelKey) } })}
|
||||
aria-label={$t('theme.toggle', { values: { mode: $t(currentIcon.labelKey) } })}
|
||||
>
|
||||
|
||||
@@ -47,11 +47,11 @@
|
||||
function eventColor(event: string): string {
|
||||
switch (event) {
|
||||
case 'app_online':
|
||||
return 'text-green-500';
|
||||
return 'text-status-online-ink';
|
||||
case 'app_offline':
|
||||
return 'text-red-500';
|
||||
return 'text-status-offline-ink';
|
||||
case 'app_degraded':
|
||||
return 'text-yellow-500';
|
||||
return 'text-status-degraded-ink';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
@@ -64,7 +64,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showDropdown = !showDropdown)}
|
||||
class="relative inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="relative inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title="Notifications"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
{#if showDropdown}
|
||||
<div
|
||||
class="absolute right-0 top-full z-50 mt-1 w-80 rounded-lg border border-border bg-popover shadow-lg"
|
||||
class="absolute right-0 top-full z-50 mt-1 w-80 rounded-xl border border-border bg-popover shadow-[var(--shadow-soft)]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<select
|
||||
id="channel-type"
|
||||
bind:value={channelType}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="slack">Slack</option>
|
||||
@@ -146,7 +146,7 @@
|
||||
type="url"
|
||||
bind:value={discordWebhookUrl}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
type="url"
|
||||
bind:value={slackWebhookUrl}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@
|
||||
type="text"
|
||||
bind:value={telegramBotToken}
|
||||
placeholder="123456:ABC-DEF..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@
|
||||
type="text"
|
||||
bind:value={telegramChatId}
|
||||
placeholder="-1001234567890"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@
|
||||
type="url"
|
||||
bind:value={httpUrl}
|
||||
placeholder="https://example.com/webhook"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@
|
||||
<select
|
||||
id="http-method"
|
||||
bind:value={httpMethod}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
@@ -230,7 +230,7 @@
|
||||
bind:value={httpSecret}
|
||||
placeholder="Shared secret for HMAC-SHA256 signature"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 pr-10 font-mono text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 pr-10 font-mono text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -262,7 +262,7 @@
|
||||
bind:value={httpSignatureHeader}
|
||||
placeholder="X-Signature-256"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Defaults to <code class="rounded bg-muted/40 px-1">X-Signature-256</code>. Override for receivers expecting a different name (e.g. <code class="rounded bg-muted/40 px-1">X-Hub-Signature-256</code>).
|
||||
@@ -283,7 +283,7 @@
|
||||
|
||||
<!-- Test Result -->
|
||||
{#if testResult}
|
||||
<p class="text-sm {testResult.startsWith('Failed') ? 'text-destructive' : 'text-emerald-500'}">
|
||||
<p class="text-sm {testResult.startsWith('Failed') ? 'text-destructive' : 'text-status-online-ink'}">
|
||||
{testResult}
|
||||
</p>
|
||||
{/if}
|
||||
@@ -292,7 +292,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{channel ? 'Update' : 'Create'} Channel
|
||||
</button>
|
||||
@@ -301,7 +301,7 @@
|
||||
type="button"
|
||||
onclick={sendTest}
|
||||
disabled={testing}
|
||||
class="rounded-md border border-input px-4 py-2 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-50"
|
||||
class="rounded-xl border border-input px-4 py-2 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
{testing ? 'Sending...' : 'Send Test'}
|
||||
</button>
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
|
||||
function eventBadgeClass(event: string): string {
|
||||
switch (event) {
|
||||
case 'app_online': return 'bg-green-500/10 text-green-500';
|
||||
case 'app_offline': return 'bg-red-500/10 text-red-500';
|
||||
case 'app_degraded': return 'bg-yellow-500/10 text-yellow-500';
|
||||
case 'app_online': return 'bg-status-online/15 text-status-online-ink';
|
||||
case 'app_offline': return 'bg-status-offline/15 text-status-offline-ink';
|
||||
case 'app_degraded': return 'bg-status-degraded/15 text-status-degraded-ink';
|
||||
default: return 'bg-muted text-muted-foreground';
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
<select
|
||||
bind:value={filterEvent}
|
||||
onchange={applyFilters}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value="">All Events</option>
|
||||
<option value="app_online">Online</option>
|
||||
@@ -104,7 +104,7 @@
|
||||
<p class="text-muted-foreground">No notifications found</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
// Theme form
|
||||
let defaultTheme = $state<'dark' | 'light'>('dark');
|
||||
let defaultPrimaryColor = $state('#6366f1');
|
||||
let defaultPrimaryColor = $state('#e8754f');
|
||||
|
||||
// Board form
|
||||
let boardName = $state('My Dashboard');
|
||||
@@ -169,6 +169,7 @@
|
||||
}
|
||||
|
||||
const primaryColorOptions = [
|
||||
{ label: 'Terracotta', value: '#e8754f' },
|
||||
{ label: 'Indigo', value: '#6366f1' },
|
||||
{ label: 'Blue', value: '#3b82f6' },
|
||||
{ label: 'Emerald', value: '#10b981' },
|
||||
@@ -182,7 +183,7 @@
|
||||
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div
|
||||
class="mx-4 w-full max-w-lg rounded-2xl border border-border bg-card shadow-2xl"
|
||||
class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
>
|
||||
<!-- Progress bar -->
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
@@ -227,7 +228,7 @@
|
||||
{:else if currentStep === 'admin'}
|
||||
<h2 class="mb-4 text-xl font-bold text-foreground">Create Admin Account</h2>
|
||||
{#if adminCreated}
|
||||
<div class="rounded-lg bg-green-500/10 p-4 text-sm text-green-600 dark:text-green-400">
|
||||
<div class="rounded-lg bg-status-online/10 p-4 text-sm text-status-online-ink dark:text-status-online-ink">
|
||||
Admin account created successfully. You can proceed to the next step.
|
||||
</div>
|
||||
{:else}
|
||||
@@ -238,7 +239,7 @@
|
||||
id="ob-display-name"
|
||||
type="text"
|
||||
bind:value={adminDisplayName}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="Admin"
|
||||
/>
|
||||
</div>
|
||||
@@ -248,7 +249,7 @@
|
||||
id="ob-email"
|
||||
type="email"
|
||||
bind:value={adminEmail}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
@@ -258,7 +259,7 @@
|
||||
id="ob-password"
|
||||
type="password"
|
||||
bind:value={adminPassword}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="Min. 6 characters"
|
||||
/>
|
||||
</div>
|
||||
@@ -298,19 +299,19 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={oauthClientId}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Client ID"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={oauthClientSecret}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Client Secret"
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={oauthDiscoveryUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Discovery URL (https://.../.well-known/openid-configuration)"
|
||||
/>
|
||||
</div>
|
||||
@@ -369,7 +370,7 @@
|
||||
id="ob-board-name"
|
||||
type="text"
|
||||
bind:value={boardName}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="My Dashboard"
|
||||
/>
|
||||
</div>
|
||||
@@ -417,7 +418,7 @@
|
||||
type="button"
|
||||
onclick={handleNext}
|
||||
disabled={loading}
|
||||
class="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{#if loading}
|
||||
Processing...
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="flex w-full max-w-lg flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-lg flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: searchSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={$t('search.placeholder')}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => search.toggle()}
|
||||
class="flex w-full max-w-sm items-center gap-2 rounded-md border border-input bg-background/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="flex w-full max-w-sm items-center gap-2.5 rounded-2xl border border-border bg-card px-4 py-2.5 text-sm text-muted-foreground shadow-[var(--shadow-soft)] transition-all hover:border-primary/40 hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
@@ -27,7 +27,7 @@
|
||||
</svg>
|
||||
<span class="flex-1 text-left">{$t('search.trigger')}</span>
|
||||
<kbd
|
||||
class="hidden rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline"
|
||||
class="hidden rounded-lg bg-muted px-2 py-0.5 text-[10px] font-bold text-muted-foreground sm:inline"
|
||||
>
|
||||
{isMac ? '\u2318' : 'Ctrl'}K
|
||||
</kbd>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||
<div class="rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Section drag handle -->
|
||||
@@ -142,7 +142,7 @@
|
||||
<!-- Card size selector -->
|
||||
<select
|
||||
onchange={handleCardSizeChange}
|
||||
class="rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground transition-colors focus:border-primary focus:outline-none"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1 text-xs text-foreground transition-colors focus:border-primary focus:outline-none"
|
||||
title={$t('board.card_size') ?? 'Card size'}
|
||||
>
|
||||
<option value="" selected={!section.cardSize}>{$t('section.inherit_size') ?? 'Inherit'}</option>
|
||||
@@ -153,7 +153,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onToggleAddWidget(section.id)}
|
||||
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('widget.add')}
|
||||
</button>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
let expanded = $state(section.isExpandedByDefault);
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card/30 shadow-sm {editMode.active ? 'ring-1 ring-primary/10' : ''}">
|
||||
<div class="rounded-[1.4rem] border border-border bg-card/40 shadow-[var(--shadow-soft)] backdrop-blur-sm {editMode.active ? 'ring-1 ring-primary/15' : ''}">
|
||||
<SectionHeader
|
||||
sectionId={section.id}
|
||||
title={section.title}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
bind:value={editTitle}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onblur={handleEditBlur}
|
||||
class="flex-1 rounded border border-input bg-background px-2 py-0.5 text-sm font-medium text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="flex-1 rounded border border-input bg-background px-2 py-0.5 text-sm font-medium text-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
<IconPickerButton
|
||||
value={editIcon}
|
||||
@@ -135,7 +135,7 @@
|
||||
{#if icon}
|
||||
<DynamicIcon name={icon} size={18} />
|
||||
{/if}
|
||||
<span class="font-medium text-foreground">{title}</span>
|
||||
<span class="font-display text-lg font-semibold text-foreground">{title}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., CI/CD Pipeline"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">A descriptive name to identify this token</p>
|
||||
@@ -34,7 +34,7 @@
|
||||
<select
|
||||
id="token-scope"
|
||||
name="scope"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="read">Read — View apps, boards, and status</option>
|
||||
<option value="write">Write — Modify apps, boards, and settings</option>
|
||||
@@ -50,7 +50,7 @@
|
||||
id="token-expires"
|
||||
name="expiresAt"
|
||||
type="datetime-local"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">Leave empty for a non-expiring token</p>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Generate Token
|
||||
</button>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
|
||||
function scopeBadgeClass(scope: string): string {
|
||||
switch (scope) {
|
||||
case 'admin': return 'bg-red-500/10 text-red-500';
|
||||
case 'write': return 'bg-yellow-500/10 text-yellow-500';
|
||||
default: return 'bg-green-500/10 text-green-500';
|
||||
case 'admin': return 'bg-destructive/10 text-destructive';
|
||||
case 'write': return 'bg-status-degraded/10 text-status-degraded-ink';
|
||||
default: return 'bg-status-online/10 text-status-online-ink';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -54,7 +54,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div class="rounded-xl border border-border bg-card p-6 shadow-[var(--shadow-soft)]">
|
||||
<h2 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('settings.bookmarklet_title')}
|
||||
</h2>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
value={localValue}
|
||||
oninput={handleInput}
|
||||
rows="8"
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={`/* Custom CSS */\n.custom-css-scope .my-widget {\n background: #333;\n}`}
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
type="button"
|
||||
onclick={() => setMode(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.mode === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey)}
|
||||
@@ -167,7 +167,7 @@
|
||||
max="360"
|
||||
step="1"
|
||||
bind:value={theme.primaryHue}
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-md"
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-[var(--shadow-soft)] [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-[var(--shadow-soft)]"
|
||||
style="color: {previewColor};"
|
||||
/>
|
||||
</div>
|
||||
@@ -188,7 +188,7 @@
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={theme.primarySaturation}
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-md"
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-[var(--shadow-soft)] [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-[var(--shadow-soft)]"
|
||||
style="color: {previewColor};"
|
||||
/>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@
|
||||
type="button"
|
||||
onclick={() => setBackground(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.backgroundType === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey)}
|
||||
@@ -222,7 +222,7 @@
|
||||
type="button"
|
||||
onclick={() => setCardStyle(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.cardStyle === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey) ?? opt.value}
|
||||
@@ -240,7 +240,7 @@
|
||||
type="button"
|
||||
onclick={() => setLocale(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {$i18nLocale === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{opt.label}
|
||||
@@ -255,12 +255,12 @@
|
||||
type="button"
|
||||
onclick={savePreferences}
|
||||
disabled={saving}
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? $t('settings.saving') : $t('settings.save')}
|
||||
</button>
|
||||
{#if saved}
|
||||
<span class="text-sm text-green-500">{$t('settings.saved')}</span>
|
||||
<span class="text-sm text-status-online-ink">{$t('settings.saved')}</span>
|
||||
{/if}
|
||||
{#if errorMessage}
|
||||
<span class="text-sm text-destructive">{errorMessage}</span>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
/>
|
||||
|
||||
{#if open && filtered.length > 0}
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-card shadow-lg">
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filtered as item, i (item)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-sm rounded-2xl border border-border bg-card p-6 shadow-2xl"
|
||||
class="mx-4 w-full max-w-sm rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
@@ -49,21 +49,21 @@
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-message"
|
||||
>
|
||||
<h2 id="confirm-dialog-title" class="mb-2 text-base font-semibold text-foreground">{title}</h2>
|
||||
<p id="confirm-dialog-message" class="mb-5 text-sm text-muted-foreground">{message}</p>
|
||||
<h2 id="confirm-dialog-title" class="mb-2 font-display text-lg font-semibold text-foreground">{title}</h2>
|
||||
<p id="confirm-dialog-message" class="mb-5 text-sm leading-relaxed text-muted-foreground">{message}</p>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="rounded-lg border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="rounded-xl border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{cancelLabel ?? ($t('common.cancel') ?? 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onConfirm}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors
|
||||
class="rounded-xl px-4 py-2 text-sm font-semibold shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5
|
||||
{destructive
|
||||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'bg-primary text-primary-foreground hover:bg-primary/90'}"
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
onclick={openPicker}
|
||||
>
|
||||
{#if selectedItem}
|
||||
@@ -157,7 +157,7 @@
|
||||
style="animation: epFadeIn 0.15s ease-out"
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-md flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-md flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: epSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={searchPlaceholder || 'Select entity'}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
class="relative z-10 flex min-h-screen items-center justify-center bg-background/80 p-4 text-foreground"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-lg rounded-xl border border-border bg-card/90 p-8 text-center shadow-xl backdrop-blur-sm"
|
||||
class="w-full max-w-lg rounded-xl border border-border bg-card/90 p-8 text-center shadow-[var(--shadow-lift)] backdrop-blur-sm"
|
||||
>
|
||||
<div class="mb-2 text-xs uppercase tracking-widest text-muted-foreground">
|
||||
{status}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="icon-grid-trigger flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="icon-grid-trigger flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
bind:this={triggerEl}
|
||||
onclick={toggle}
|
||||
>
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="icon-grid-popup fixed z-50 overflow-y-auto rounded-lg border border-border bg-popover shadow-xl"
|
||||
class="icon-grid-popup fixed z-50 overflow-y-auto rounded-xl border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
bind:this={popupEl}
|
||||
style="animation: iconGridSlideIn 0.15s ease-out"
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleOpen}
|
||||
class="flex items-center gap-1.5 rounded-lg border border-input bg-background transition-colors hover:bg-accent
|
||||
class="flex items-center gap-1.5 rounded-xl border border-input bg-background transition-colors hover:bg-accent
|
||||
{size === 'sm' ? 'px-2 py-1' : 'px-3 py-2'}"
|
||||
title={$t('app.icon') ?? 'Select icon'}
|
||||
>
|
||||
@@ -105,7 +105,7 @@
|
||||
class="fixed inset-0 z-50"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) open = false; }}
|
||||
>
|
||||
<div class="fixed left-1/2 top-1/2 z-50 w-80 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card p-3 shadow-2xl">
|
||||
<div class="fixed left-1/2 top-1/2 z-50 w-80 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card p-3 shadow-[var(--shadow-lift)]">
|
||||
<!-- Search -->
|
||||
<div class="relative mb-2">
|
||||
<svg class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -116,7 +116,7 @@
|
||||
type="text"
|
||||
bind:value={query}
|
||||
placeholder={$t('common.search') ?? 'Search icons...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
value={value}
|
||||
oninput={(e) => { const v = (e.target as HTMLInputElement).value; value = v; onchange(v); }}
|
||||
placeholder={$t('app.icon_manual') ?? 'Or type icon name...'}
|
||||
class="w-full rounded-lg border border-input bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
onkeydown={(e) => e.key === 'Escape' && keyboard.closeOverlay()}
|
||||
>
|
||||
<div
|
||||
class="mx-4 w-full max-w-xl rounded-2xl border border-border bg-card shadow-2xl"
|
||||
class="mx-4 w-full max-w-xl rounded-2xl border border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
role="dialog"
|
||||
aria-label="Keyboard Shortcuts"
|
||||
>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
onclick={openPicker}
|
||||
>
|
||||
{#if selectedCount > 0}
|
||||
@@ -148,7 +148,7 @@
|
||||
style="animation: mepFadeIn 0.15s ease-out"
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-md flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-md flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: mepSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={searchPlaceholder || 'Select items'}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{#if tags.length > 0}
|
||||
<div class="mb-1.5 flex flex-wrap gap-1">
|
||||
{#each tags as tag (tag)}
|
||||
<span class="flex items-center gap-1 rounded-md bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
<span class="flex items-center gap-1 rounded-xl bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
@@ -128,7 +128,7 @@
|
||||
/>
|
||||
|
||||
{#if open && filtered.length > 0}
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-card shadow-lg">
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filtered as item, i (item)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -139,14 +139,14 @@
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="card-hover group flex items-center gap-2 rounded-lg {cardStyleClass} px-3 py-2 text-left transition-colors hover:border-primary/50"
|
||||
class="card-hover group flex items-center gap-2 rounded-2xl {cardStyleClass} px-3 py-2 text-left transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative flex-shrink-0">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-md bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-base">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -198,7 +198,7 @@
|
||||
<!-- Large: icon + name + description + sparkline + tags + links -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="card-hover group rounded-xl {cardStyleClass} p-5 transition-colors hover:border-primary/50"
|
||||
class="card-hover group rounded-[1.4rem] {cardStyleClass} p-5 transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
@@ -211,7 +211,7 @@
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-xl bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-3xl">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -294,7 +294,7 @@
|
||||
<!-- Medium (default): icon + name + status + sparkline on hover + links -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="card-hover group rounded-xl {cardStyleClass} p-4 transition-colors hover:border-primary/50"
|
||||
class="card-hover group rounded-[1.4rem] {cardStyleClass} p-4 transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
@@ -307,7 +307,7 @@
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-2xl">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -378,12 +378,12 @@
|
||||
<!-- Context Menu -->
|
||||
{#if showContextMenu}
|
||||
<div
|
||||
class="fixed z-50 rounded-lg border border-border bg-popover p-1 shadow-lg"
|
||||
class="fixed z-50 rounded-2xl border border-border bg-popover p-1 shadow-[var(--shadow-soft)]"
|
||||
style="left: {contextMenuPos.x}px; top: {contextMenuPos.y}px"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onclick={toggleFavorite}
|
||||
>
|
||||
{#if favorites.isFavorite(app.id)}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
href={config.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="card-hover group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50"
|
||||
class="card-hover group flex flex-col items-center gap-2 rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4 text-center transition-colors hover:border-primary/50"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if config.icon}
|
||||
<span class="text-2xl">{config.icon}</span>
|
||||
{:else}
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<!-- Badge -->
|
||||
<span class="inline-flex items-center gap-1.5 text-xs">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full" style="background: var(--room-sky);"></span>
|
||||
<span class="text-muted-foreground">Bookmark</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Calendar class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium text-foreground">Calendar</span>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card overflow-hidden">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] overflow-hidden">
|
||||
<!-- Stream view -->
|
||||
<div
|
||||
class="relative w-full bg-black"
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
{#if clockStyle === 'analog'}
|
||||
<!-- Analog clock face -->
|
||||
<svg viewBox="0 0 100 100" class="h-32 w-32">
|
||||
@@ -154,7 +154,7 @@
|
||||
<p class="mt-2 text-xs text-muted-foreground">{dateStr}</p>
|
||||
{:else}
|
||||
<!-- Digital clock -->
|
||||
<p class="font-mono text-4xl font-bold tabular-nums text-foreground">{timeStr}</p>
|
||||
<p class="font-display text-4xl font-semibold tabular-nums text-foreground">{timeStr}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">{dateStr}</p>
|
||||
{#if config.timezone}
|
||||
<p class="mt-0.5 text-xs text-muted-foreground/70">{config.timezone.replace(/_/g, ' ')}</p>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col rounded-xl border border-border bg-card">
|
||||
<div class="flex flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<div class="relative" style="height: {iframeHeight}px;">
|
||||
{#if !safeUrl}
|
||||
<div class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
const links = $derived(config.links ?? []);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<!-- Header -->
|
||||
{#if isCollapsible}
|
||||
<button
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center justify-end border-b border-border px-3 py-1.5">
|
||||
<button
|
||||
|
||||
@@ -64,13 +64,13 @@
|
||||
});
|
||||
|
||||
const trendColor = $derived.by(() => {
|
||||
if (trend === 'up') return 'text-green-500';
|
||||
if (trend === 'down') return 'text-red-500';
|
||||
return 'text-muted-foreground';
|
||||
if (trend === 'up') return 'var(--status-online-ink)';
|
||||
if (trend === 'down') return 'var(--status-offline-ink)';
|
||||
return 'var(--muted-foreground)';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="h-10 w-24 animate-pulse rounded bg-muted"></div>
|
||||
@@ -80,7 +80,7 @@
|
||||
<span class="text-xs text-muted-foreground">Failed to load metric</span>
|
||||
{:else if currentValue !== null}
|
||||
<!-- Trend arrow -->
|
||||
<div class="mb-1 {trendColor}">
|
||||
<div class="mb-1" style="color: {trendColor};">
|
||||
{#if trend === 'up'}
|
||||
<TrendingUp class="h-5 w-5" />
|
||||
{:else if trend === 'down'}
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
<!-- Big number -->
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-4xl font-bold tabular-nums text-foreground">
|
||||
<span class="font-display text-4xl font-semibold tabular-nums text-foreground">
|
||||
{formatNumber(currentValue)}
|
||||
</span>
|
||||
{#if config.unit}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="prose prose-sm prose-invert max-w-none flex-1 overflow-auto text-foreground">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized with DOMPurify -->
|
||||
{@html renderedContent}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Rss class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium text-foreground">RSS Feed</span>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<!-- Header -->
|
||||
<button
|
||||
type="button"
|
||||
@@ -63,28 +63,28 @@
|
||||
<div class="mt-3 flex gap-1">
|
||||
{#if statusCounts.online > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-green-500"
|
||||
class="h-2 rounded-full bg-status-online"
|
||||
style="flex: {statusCounts.online}"
|
||||
title="{statusCounts.online} online"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.degraded > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-yellow-500"
|
||||
class="h-2 rounded-full bg-status-degraded"
|
||||
style="flex: {statusCounts.degraded}"
|
||||
title="{statusCounts.degraded} degraded"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.offline > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-red-500"
|
||||
class="h-2 rounded-full bg-status-offline"
|
||||
style="flex: {statusCounts.offline}"
|
||||
title="{statusCounts.offline} offline"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.unknown > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-gray-500"
|
||||
class="h-2 rounded-full bg-status-unknown"
|
||||
style="flex: {statusCounts.unknown}"
|
||||
title="{statusCounts.unknown} unknown"
|
||||
></div>
|
||||
@@ -95,25 +95,25 @@
|
||||
<div class="mt-2 flex flex-wrap gap-3 text-xs">
|
||||
{#if statusCounts.online > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-green-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-online"></span>
|
||||
{statusCounts.online} online
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.degraded > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-yellow-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-degraded"></span>
|
||||
{statusCounts.degraded} degraded
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.offline > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-offline"></span>
|
||||
{statusCounts.offline} offline
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.unknown > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-gray-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-unknown"></span>
|
||||
{statusCounts.unknown} unknown
|
||||
</span>
|
||||
{/if}
|
||||
@@ -126,12 +126,12 @@
|
||||
{@const status = app.statuses[0]?.status ?? 'unknown'}
|
||||
{@const statusColor =
|
||||
status === 'online'
|
||||
? 'bg-green-500'
|
||||
? 'bg-status-online'
|
||||
: status === 'offline'
|
||||
? 'bg-red-500'
|
||||
? 'bg-status-offline'
|
||||
: status === 'degraded'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-gray-500'}
|
||||
? 'bg-status-degraded'
|
||||
: 'bg-status-unknown'}
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-foreground">{app.name}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
const refreshMs = $derived((config.refreshInterval ?? 30) * 1000);
|
||||
|
||||
function thresholdColor(value: number): string {
|
||||
if (value >= 85) return 'text-red-500';
|
||||
if (value >= 60) return 'text-yellow-500';
|
||||
return 'text-green-500';
|
||||
if (value >= 85) return 'text-status-offline-ink';
|
||||
if (value >= 60) return 'text-status-degraded-ink';
|
||||
return 'text-status-online-ink';
|
||||
}
|
||||
|
||||
function thresholdStroke(value: number): string {
|
||||
if (value >= 85) return 'stroke-red-500';
|
||||
if (value >= 60) return 'stroke-yellow-500';
|
||||
return 'stroke-green-500';
|
||||
if (value >= 85) return 'stroke-status-offline';
|
||||
if (value >= 60) return 'stroke-status-degraded';
|
||||
return 'stroke-status-online';
|
||||
}
|
||||
|
||||
function thresholdTrack(_value: number): string {
|
||||
@@ -80,7 +80,7 @@
|
||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="mb-3 text-sm font-medium text-foreground">System Stats</span>
|
||||
|
||||
{#if loading}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
let rssShowSummary = $state((initialConfig.showSummary as boolean) ?? true);
|
||||
|
||||
// Calendar
|
||||
let calendarUrlsRaw = $state((initialConfig.icalUrls as Array<{ url: string; color: string; label: string }>) ?? [{ url: '', color: '#6366f1', label: '' }]);
|
||||
let calendarUrlsRaw = $state((initialConfig.icalUrls as Array<{ url: string; color: string; label: string }>) ?? [{ url: '', color: '#e8754f', label: '' }]);
|
||||
let calendarDaysAhead = $state((initialConfig.daysAhead as number) ?? 7);
|
||||
|
||||
// Markdown
|
||||
@@ -155,7 +155,7 @@
|
||||
}
|
||||
|
||||
function addCalendarUrl() {
|
||||
calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#e8754f', label: '' }];
|
||||
}
|
||||
|
||||
function removeCalendarUrl(index: number) {
|
||||
@@ -163,7 +163,7 @@
|
||||
}
|
||||
|
||||
// Helper for input styling
|
||||
const inputClass = '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';
|
||||
const inputClass = 'w-full rounded-xl 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-visible:ring-2 focus-visible:ring-primary/30';
|
||||
const labelClass = 'mb-1 block text-sm font-medium text-foreground';
|
||||
|
||||
let firstInput: HTMLElement | undefined = $state();
|
||||
@@ -171,7 +171,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-border bg-card p-4 shadow-lg"
|
||||
class="rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]"
|
||||
transition:fade={{ duration: 100 }}
|
||||
onkeydown={handleKeydown}
|
||||
role="dialog"
|
||||
@@ -201,11 +201,11 @@
|
||||
bind:value={appSearchQuery}
|
||||
bind:this={firstInput}
|
||||
placeholder={$t('common.search') ?? 'Search apps...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
<!-- App grid -->
|
||||
<div class="max-h-48 overflow-y-auto rounded-lg border border-input bg-background p-1">
|
||||
<div class="max-h-48 overflow-y-auto rounded-xl border border-input bg-background p-1">
|
||||
{#if filteredApps.length === 0}
|
||||
<p class="py-4 text-center text-xs text-muted-foreground">{$t('common.no_results') ?? 'No apps found'}</p>
|
||||
{:else}
|
||||
@@ -548,7 +548,7 @@
|
||||
{$t('common.cancel') ?? 'Cancel'}
|
||||
</button>
|
||||
<button type="button" onclick={handleSave}
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
|
||||
{mode === 'create' ? ($t('common.add') ?? 'Add') : ($t('common.save') ?? 'Save')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
// Calendar fields
|
||||
let calendarUrls = $state<Array<{ url: string; color: string; label: string }>>([
|
||||
{ url: '', color: '#6366f1', label: '' }
|
||||
{ url: '', color: '#e8754f', label: '' }
|
||||
]);
|
||||
let calendarDaysAhead = $state(7);
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
rssFeedUrl = '';
|
||||
rssMaxItems = 10;
|
||||
rssShowSummary = true;
|
||||
calendarUrls = [{ url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrls = [{ url: '', color: '#e8754f', label: '' }];
|
||||
calendarDaysAhead = 7;
|
||||
markdownContent = '';
|
||||
metricLabel = '';
|
||||
@@ -350,7 +350,7 @@
|
||||
}
|
||||
|
||||
function addCalendarUrl() {
|
||||
calendarUrls = [...calendarUrls, { url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrls = [...calendarUrls, { url: '', color: '#e8754f', label: '' }];
|
||||
}
|
||||
|
||||
function removeCalendarUrl(index: number) {
|
||||
@@ -367,7 +367,7 @@
|
||||
|
||||
// Input CSS class for reuse
|
||||
const inputClass =
|
||||
'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';
|
||||
'w-full rounded-xl 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-visible:ring-2 focus-visible:ring-primary/30';
|
||||
</script>
|
||||
|
||||
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
|
||||
@@ -505,7 +505,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-1 block text-sm font-medium text-foreground">Select Apps</span>
|
||||
<div class="max-h-40 space-y-1 overflow-y-auto rounded-lg border border-input bg-background p-2">
|
||||
<div class="max-h-40 space-y-1 overflow-y-auto rounded-xl border border-input bg-background p-2">
|
||||
{#each apps as app (app.id)}
|
||||
<label class="flex items-center gap-2 rounded px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<input
|
||||
@@ -613,7 +613,7 @@
|
||||
<span class="mb-1 block text-sm font-medium text-foreground">Metrics</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each ['cpu', 'ram', 'disk', 'swap', 'network'] as metric (metric)}
|
||||
<label class="flex items-center gap-1.5 rounded-md border border-input px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<label class="flex items-center gap-1.5 rounded-xl border border-input px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sysStatsMetrics.includes(metric)}
|
||||
@@ -704,7 +704,7 @@
|
||||
<input
|
||||
type="color"
|
||||
bind:value={calendarUrls[i].color}
|
||||
class="h-9 w-9 cursor-pointer rounded-lg border border-input bg-background"
|
||||
class="h-9 w-9 cursor-pointer rounded-xl border border-input bg-background"
|
||||
title="Calendar color"
|
||||
/>
|
||||
</div>
|
||||
@@ -1038,7 +1038,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSubmitWidget}
|
||||
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add')}
|
||||
</button>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="absolute inset-0 z-10 rounded-xl bg-black/5 transition-opacity">
|
||||
<!-- Top-left: drag handle -->
|
||||
<div class="absolute left-1.5 top-1.5">
|
||||
<div class="cursor-grab rounded-md bg-card/90 p-1 text-muted-foreground shadow-sm backdrop-blur-sm" title="Drag to reorder">
|
||||
<div class="cursor-grab rounded-md bg-card/90 p-1 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm" title="Drag to reorder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="5" r="1" /><circle cx="15" cy="5" r="1" />
|
||||
<circle cx="9" cy="12" r="1" /><circle cx="15" cy="12" r="1" />
|
||||
@@ -62,7 +62,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showSizePicker = !showSizePicker; previewSpan = null; }}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors
|
||||
{showSizePicker ? 'bg-primary text-primary-foreground' : 'hover:bg-accent hover:text-foreground'}"
|
||||
title={$t('widget.resize') ?? 'Resize'}
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onEdit(widgetId)}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors hover:bg-primary hover:text-primary-foreground"
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors hover:bg-primary hover:text-primary-foreground"
|
||||
title={$t('common.edit') ?? 'Edit'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -91,7 +91,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showDeleteConfirm = true; }}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
title={$t('common.delete') ?? 'Delete'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<!-- Size picker popover -->
|
||||
{#if showSizePicker && onResize}
|
||||
<div class="absolute right-1.5 top-10 z-20 rounded-lg border border-border bg-card p-2 shadow-xl backdrop-blur-sm">
|
||||
<div class="absolute right-1.5 top-10 z-20 rounded-xl border border-border bg-card p-2 shadow-[var(--shadow-lift)] backdrop-blur-sm">
|
||||
<div class="mb-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{$t('widget.width') ?? 'Width'}
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</script>
|
||||
|
||||
{#snippet skeleton()}
|
||||
<div class="flex h-full items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="text-xs text-muted-foreground">…</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -163,7 +163,7 @@
|
||||
}} />
|
||||
{/await}
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="text-xs text-muted-foreground">{$t('widget.type', { values: { type: widget.type } })}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-lg rounded-2xl border border-border bg-card shadow-2xl lg:max-w-2xl"
|
||||
class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-lift)] lg:max-w-2xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
>
|
||||
@@ -110,7 +110,7 @@
|
||||
type="text"
|
||||
bind:value={filterQuery}
|
||||
placeholder={$t('widget.search_type') ?? 'Search widget types...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@
|
||||
onclick={() => onSelect(wt.value)}
|
||||
class="flex items-start gap-3 rounded-xl px-3 py-3 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<div class="mt-0.5 shrink-0 rounded-lg bg-primary/10 p-2 text-primary">
|
||||
<div class="mt-0.5 shrink-0 rounded-xl bg-primary/10 p-2 text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
{#each iconFor(wt.value).split('|') as segment, si (si)}
|
||||
{#if segment.includes('m') || segment.includes('M') || segment.includes('a') || segment.includes('z') || segment.includes('A') || segment.includes('c') || segment.includes('l') || segment.includes('v') || segment.includes('h') || segment.includes('V') || segment.includes('H')}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
|
||||
const severityStyles = $derived.by(() => {
|
||||
switch (data.severity) {
|
||||
case 'critical': return 'border-red-500/50 bg-red-500/10 text-red-400';
|
||||
case 'warning': return 'border-yellow-500/50 bg-yellow-500/10 text-yellow-400';
|
||||
default: return 'border-blue-500/50 bg-blue-500/10 text-blue-400';
|
||||
case 'critical':
|
||||
return 'border-status-offline/40 bg-status-offline/10 text-status-offline-ink';
|
||||
case 'warning':
|
||||
return 'border-status-degraded/40 bg-status-degraded/10 text-status-degraded-ink';
|
||||
default:
|
||||
return 'border-room-sky/40 bg-room-sky/10 text-room-sky';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +23,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex items-start gap-3 rounded-lg border p-3 {severityStyles}">
|
||||
<div class="flex items-start gap-3 rounded-2xl border p-3.5 {severityStyles}">
|
||||
<span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold">
|
||||
{severityIcon}
|
||||
</span>
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
data.labels.length > 0 ? Math.max(100 / data.labels.length - 2, 4) : 10
|
||||
);
|
||||
|
||||
const defaultColors = ['#6366f1', '#22c55e', '#eab308', '#ef4444', '#06b6d4'];
|
||||
// Cozy "room" palette for multi-series charts
|
||||
const defaultColors = [
|
||||
'var(--room-terra)',
|
||||
'var(--room-sky)',
|
||||
'var(--room-sage)',
|
||||
'var(--room-butter)',
|
||||
'var(--room-lav)',
|
||||
'var(--room-peach)'
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-3">
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
const color = $derived.by(() => {
|
||||
const warn = data.thresholds?.warning ?? 60;
|
||||
const crit = data.thresholds?.critical ?? 85;
|
||||
if (percentage >= crit) return '#ef4444'; // red
|
||||
if (percentage >= warn) return '#eab308'; // yellow
|
||||
return '#22c55e'; // green
|
||||
if (percentage >= crit) return 'var(--status-offline)';
|
||||
if (percentage >= warn) return 'var(--status-degraded)';
|
||||
return 'var(--status-online)';
|
||||
});
|
||||
|
||||
// SVG circle math
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
);
|
||||
|
||||
const trendColor = $derived(
|
||||
data.trend === 'up' ? 'text-green-500' : data.trend === 'down' ? 'text-red-500' : 'text-muted-foreground'
|
||||
data.trend === 'up' ? 'text-status-online-ink' : data.trend === 'down' ? 'text-status-offline-ink' : 'text-muted-foreground'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-1 p-4 text-center">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-3xl font-bold text-foreground">{data.value}</span>
|
||||
<span class="font-display text-3xl font-semibold text-foreground">{data.value}</span>
|
||||
{#if data.unit}
|
||||
<span class="text-sm text-muted-foreground">{data.unit}</span>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user