feat(ui): cozy polish — primitives, motion, empty states

Adds 7 reusable primitives in src/lib/components/ui/ and migrates ~70 hand-rolled
call sites across forms, admin panels, and routes. Tokens unchanged — same Cozy
Home palette, just consistently applied.

Primitives
- Switch: pill toggle, role=switch, terracotta track, cubic-bezier knob
- Button: 5 variants × 4 sizes, press-squash, loading spinner, buttonClass()
  helper for <a> link-as-CTA cases
- Checkbox: rounded square with animated check-draw + indeterminate
- Select: native <select> with Cozy chevron + matched radius
- Slider: gradient track, terracotta-bordered knob, aria-valuetext
- Input + Field: documented in CLAUDE.md for future use
- 9 buttonClass unit tests

Migrations
- 23 <input type=checkbox> → <Switch> (boolean settings)
- 5 multi-select checkboxes → <Checkbox> (DiscoveryPanel, sys-stats metrics)
- ~28 <select> → <Select>
- 17 <input type=range> → <Slider> (ThemeCustomizer's hue picker kept custom)
- ~25 hand-rolled buttons → <Button> / buttonClass()

Surface polish
- Admin section wrappers: rounded-lg → rounded-[1.4rem] + shadow-soft
  (resolves the Phase-5 tradeoff from the Cozy migration memo)
- BoardPropertiesPanel: live theme preview swatch showing computed hsl() on a
  sample button; hue/sat use Slider; bg/cardSize use Select
- AppHealthBadge: role=status + aria-live=polite; .status-degraded (slow
  amber breathing) and .status-offline (single attention flash) now applied
- AppForm collapse triggers: rotating chevron + aria-expanded
- Empty states for /boards and /apps: inline SVGs using --room-* tokens
  (peach/sky/sage/butter) instead of generic Lucide icons
- Login Remember Me: showcase Switch (first-impression surface)

Motion (src/app.css)
- New cozy-rise / cozy-rise-stagger for staggered grid reveals (/boards, /apps)
- New cozy-expand for accordion sections (healthcheck, integration, wallpaper)
- All motion respects prefers-reduced-motion

CLAUDE.md
- New project guide with a mandatory Frontend reuse table — every primitive
  documented with "never use raw <input type=checkbox>/<select>/<range>" and
  "do not repeat rounded-xl bg-primary px-4 py-2 ..." rules

Verification
- npm run check: 0 errors, 0 warnings, 5831 files
- npm test: 301 passing
- npm run lint: 0 errors (19 pre-existing warnings unchanged)
- npm run build: ✔ done

Branch is feat/cozy-polish, ready to PR against master.
This commit is contained in:
2026-05-28 14:39:53 +03:00
parent 555ac9ea63
commit f087551454
49 changed files with 1317 additions and 613 deletions
@@ -1,5 +1,6 @@
<script lang="ts">
import type { IntegrationFieldDescriptor } from '$lib/server/integrations/types.js';
import Switch from '$lib/components/ui/Switch.svelte';
interface Props {
fields: IntegrationFieldDescriptor[];
@@ -23,16 +24,15 @@
{/if}
</label>
{#if field.type === 'boolean'}
<label class="flex items-center gap-2">
<input
<div class="flex items-center gap-3">
<Switch
id="{idPrefix}-{field.name}"
type="checkbox"
checked={!!values[field.name]}
onchange={(e) => onchange(field.name, e.currentTarget.checked)}
class="h-4 w-4 rounded border-input accent-primary"
onchange={(checked) => onchange(field.name, checked)}
ariaLabel={field.label}
/>
<span class="text-sm text-muted-foreground">{field.description ?? ''}</span>
</label>
</div>
{:else if field.type === 'number'}
<input
id="{idPrefix}-{field.name}"