Danilo Fragoso 4 mēneši atpakaļ
revīzija
c5f123495f
83 mainītis faili ar 8772 papildinājumiem un 0 dzēšanām
  1. 32 0
      .gitignore
  2. 1 0
      .npmrc
  3. 66 0
      README.md
  4. 490 0
      design.md
  5. 1632 0
      package-lock.json
  6. 26 0
      package.json
  7. 325 0
      src/app.css
  8. 13 0
      src/app.d.ts
  9. 18 0
      src/app.html
  10. 1 0
      src/lib/assets/favicon.svg
  11. 55 0
      src/lib/components/data/CodeBlock.svelte
  12. 200 0
      src/lib/components/data/DataTable.svelte
  13. 33 0
      src/lib/components/data/EmptyState.svelte
  14. 106 0
      src/lib/components/data/JsonViewer.svelte
  15. 17 0
      src/lib/components/data/KanbanBoard.svelte
  16. 79 0
      src/lib/components/data/KanbanCard.svelte
  17. 68 0
      src/lib/components/data/KanbanColumn.svelte
  18. 60 0
      src/lib/components/data/KeyValue.svelte
  19. 65 0
      src/lib/components/data/MetricCard.svelte
  20. 86 0
      src/lib/components/data/Table.svelte
  21. 149 0
      src/lib/components/forms/Combobox.svelte
  22. 212 0
      src/lib/components/forms/DatePicker.svelte
  23. 105 0
      src/lib/components/forms/FileUpload.svelte
  24. 30 0
      src/lib/components/forms/FormField.svelte
  25. 200 0
      src/lib/components/forms/MultiSelect.svelte
  26. 73 0
      src/lib/components/forms/SearchInput.svelte
  27. 58 0
      src/lib/components/layout/Breadcrumbs.svelte
  28. 21 0
      src/lib/components/layout/Card.svelte
  29. 9 0
      src/lib/components/layout/CardContent.svelte
  30. 9 0
      src/lib/components/layout/CardDescription.svelte
  31. 9 0
      src/lib/components/layout/CardFooter.svelte
  32. 9 0
      src/lib/components/layout/CardHeader.svelte
  33. 9 0
      src/lib/components/layout/CardTitle.svelte
  34. 11 0
      src/lib/components/layout/Page.svelte
  35. 27 0
      src/lib/components/layout/PageHeader.svelte
  36. 19 0
      src/lib/components/layout/ScrollArea.svelte
  37. 82 0
      src/lib/components/layout/Tabs.svelte
  38. 180 0
      src/lib/components/navigation/CommandPalette.svelte
  39. 36 0
      src/lib/components/navigation/NavItem.svelte
  40. 95 0
      src/lib/components/navigation/Navbar.svelte
  41. 114 0
      src/lib/components/navigation/Sidebar.svelte
  42. 92 0
      src/lib/components/navigation/SidebarItem.svelte
  43. 26 0
      src/lib/components/navigation/SidebarSection.svelte
  44. 77 0
      src/lib/components/navigation/ThemeToggle.svelte
  45. 48 0
      src/lib/components/overlay/Alert.svelte
  46. 87 0
      src/lib/components/overlay/AlertDialog.svelte
  47. 121 0
      src/lib/components/overlay/Dialog.svelte
  48. 103 0
      src/lib/components/overlay/DropdownMenu.svelte
  49. 92 0
      src/lib/components/overlay/Popover.svelte
  50. 57 0
      src/lib/components/overlay/Progress.svelte
  51. 126 0
      src/lib/components/overlay/Sheet.svelte
  52. 30 0
      src/lib/components/overlay/Skeleton.svelte
  53. 75 0
      src/lib/components/overlay/Toaster.svelte
  54. 69 0
      src/lib/components/overlay/Tooltip.svelte
  55. 47 0
      src/lib/components/primitives/Avatar.svelte
  56. 34 0
      src/lib/components/primitives/Badge.svelte
  57. 102 0
      src/lib/components/primitives/Button.svelte
  58. 73 0
      src/lib/components/primitives/Checkbox.svelte
  59. 26 0
      src/lib/components/primitives/Input.svelte
  60. 23 0
      src/lib/components/primitives/Label.svelte
  61. 46 0
      src/lib/components/primitives/RadioGroup.svelte
  62. 62 0
      src/lib/components/primitives/RadioItem.svelte
  63. 51 0
      src/lib/components/primitives/Select.svelte
  64. 20 0
      src/lib/components/primitives/Separator.svelte
  65. 70 0
      src/lib/components/primitives/Slider.svelte
  66. 60 0
      src/lib/components/primitives/Switch.svelte
  67. 26 0
      src/lib/components/primitives/Textarea.svelte
  68. 78 0
      src/lib/index.ts
  69. 8 0
      src/lib/utils/cn.ts
  70. 43 0
      src/lib/utils/toast.svelte.ts
  71. 133 0
      src/routes/+layout.svelte
  72. 180 0
      src/routes/+page.svelte
  73. 103 0
      src/routes/DocBlock.svelte
  74. 345 0
      src/routes/data/+page.svelte
  75. 245 0
      src/routes/forms/+page.svelte
  76. 168 0
      src/routes/layout/+page.svelte
  77. 216 0
      src/routes/navigation/+page.svelte
  78. 395 0
      src/routes/overlay/+page.svelte
  79. 338 0
      src/routes/primitives/+page.svelte
  80. 3 0
      static/robots.txt
  81. 17 0
      svelte.config.js
  82. 20 0
      tsconfig.json
  83. 7 0
      vite.config.ts

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+# Dependencies
+node_modules
+
+# Build output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build
+/dist
+
+# Environment
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Editor
+.vscode
+.idea
+
+# Claude
+.claude
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+engine-strict=true

+ 66 - 0
README.md

@@ -0,0 +1,66 @@
+# 🫒 olive
+
+A calm, precise component library built with Svelte 5 and Tailwind v4. Designed for data-dense interfaces that need to feel trustworthy.
+
+## Development
+
+```sh
+npm install
+npm run dev
+```
+
+## Publishing to npm
+
+Olive uses [`@sveltejs/package`](https://kit.svelte.dev/docs/packaging) to build the library.
+
+### 1. Install packaging tools
+
+```sh
+npm install -D @sveltejs/package publint
+```
+
+### 2. Configure `package.json`
+
+```json
+{
+  "name": "olive-ui",
+  "version": "0.1.0",
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build",
+    "package": "svelte-kit sync && svelte-package && publint",
+    "prepublishOnly": "npm run package"
+  },
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "svelte": "./dist/index.js"
+    },
+    "./styles": "./dist/app.css"
+  },
+  "svelte": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "files": ["dist"]
+}
+```
+
+### 3. Build and publish
+
+```sh
+# Build into dist/
+npm run package
+
+# Verify the package exports are correct
+npx publint
+
+# Publish
+npm publish
+```
+
+### How it works
+
+- `svelte-package` reads `src/lib/` and compiles everything into `dist/`
+- `src/lib/index.ts` is the public API — only what's exported there is available to consumers
+- Svelte files ship as source so consuming apps can tree-shake and SSR natively
+- TypeScript types are generated automatically alongside each file
+- `publint` validates the `package.json` exports field before you publish

+ 490 - 0
design.md

@@ -0,0 +1,490 @@
+# Olive Design System
+
+Olive is a Svelte component library and design system. It provides the visual language, design tokens, and component primitives for building professional, human-scale software tools.
+
+---
+
+## The Character of Olive
+
+Olive has a specific character — a way of feeling when you use it. It is not neutral. It is not generic. Every decision in this system is in service of that character.
+
+**Calm precision.** The interface should feel like a well-organized workbench: everything in its place, nothing extraneous, the right tool within reach. There is no decoration for decoration's sake. Whitespace is not empty — it is the breathing room that lets content speak clearly. When the screen is calm, the user can focus on the work, not the interface.
+
+**Warmth without whimsy.** Professional tools can have a soul. The color is a soft yellow-green — the color of a garden, of something grown. It does not scream. It does not demand attention. But it is unmistakably alive, and it makes the interface feel like something made by people who care. Empty states are friendly. Confirmations are human. Nothing is cold or corporate.
+
+**Trust through honesty.** Olive is used to build tools that handle important things. The UI must communicate reliability. That means no layout shifts. No misleading affordances. No animation that obscures rather than clarifies. Numbers are formatted precisely. Errors say exactly what happened. Timestamps are readable. The system respects the user's intelligence and treats them as a professional.
+
+**Invisible infrastructure.** The best UI moment is when the user stops thinking about the interface and thinks only about their work. Chrome — navbars, cards, borders, toolbars — is scaffolding. It should be there when needed and recede when not. Content is the primary element. Structure serves it.
+
+---
+
+## Motion
+
+Motion in Olive is purposeful. Every transition answers the question: "what changed, and where did it go?"
+
+Animations communicate state, not personality. They orient the user — a panel slides in from the side so you know where it came from. A dialog fades up from center so you know it's floating above the page. A row highlights for 150ms so you know it was just updated.
+
+Motion is never decorative. Nothing spins for the sake of spinning. Nothing bounces. Nothing draws attention to itself.
+
+### Timing
+
+| Duration | Usage |
+|---|---|
+| `100ms` | Micro-feedback: button press, checkbox tick |
+| `150ms` | Hover states, focus rings, badge color changes |
+| `200ms` | Component appear/disappear (toast, tooltip, dropdown) |
+| `250ms` | Panel slides, sheet entrances, modal fade-up |
+| `350ms` | Page-level transitions, large layout changes |
+
+Easing:
+- Entering elements: `cubic-bezier(0.16, 1, 0.3, 1)` — fast start, smooth landing
+- Exiting elements: `cubic-bezier(0.4, 0, 1, 1)` — quick, clean exit
+- Position changes: `cubic-bezier(0.25, 0.46, 0.45, 0.94)` — natural arc
+
+**Only ever animate `opacity` and `transform`.** Never animate `height`, `width`, `top`, `left`, or any property that triggers layout. Layout animation causes jank. `opacity` + `transform` are GPU-composited and always smooth.
+
+Do not show a loading indicator for operations under 200ms. Add a deliberate short delay before revealing any loading state — a spinner that flashes for 80ms is worse than no spinner at all.
+
+Respect `prefers-reduced-motion`. Wrap all non-essential transitions in a media query check and disable or reduce them.
+
+---
+
+## Typography
+
+Text is the primary material of Olive interfaces. It should be legible at every size, on every screen, in both light and dark modes.
+
+**Primary font**: Plus Jakarta Sans, weights 400/500/600/700. Its geometry is soft, slightly rounded — professional without being cold. It pairs naturally with the olive-green palette because both have warmth without loudness. Never substitute it. Never add additional font families.
+
+**Monospace font**: `ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace`. Used for any content the user would copy-paste: identifiers, connection strings, keys, queries, payloads, schema names. If it came from a system or will go back into one, it is monospaced. This distinction is important — it immediately communicates "this is data" vs "this is UI".
+
+Font smoothing is always on: `-webkit-font-smoothing: antialiased`. Sub-pixel rendering makes text look heavy on modern displays.
+
+### Type Scale
+
+| Step | Size | Weight | Usage |
+|---|---|---|---|
+| `display` | `2xl` (1.5rem) | `semibold` | Page titles. One per page. |
+| `title` | `lg` (1.125rem) | `semibold` | Card titles, section headers |
+| `label` | `sm` (0.875rem) | `medium` | Nav items, form labels, column headers |
+| `body` | `sm` (0.875rem) | `normal` | All body text |
+| `caption` | `xs` (0.75rem) | `normal` | Metadata, timestamps, secondary info |
+| `micro` | `xs` (0.75rem) + uppercase + tracking-wide | `medium` | Section labels, tiny category tags |
+| `code` | `sm` (0.875rem) | `normal` | Inline code, monospaced content |
+
+### Typography Rules
+
+- Use 2–3 weights per page maximum (400, 500, 600). More than three weights creates visual noise.
+- Secondary information lives at `muted-foreground`. It should be clearly subordinate, readable, but not competing.
+- Never use colored text for emphasis in body copy. Use weight (`font-medium`, `font-semibold`) instead. Reserve colored text for interactive elements and status indicators.
+- ALL CAPS only for micro labels. Never for headings or action labels.
+- Keep descriptions under 15 words.
+- Line length: `max-w-prose` (65ch) for reading text. Never let a line of prose stretch edge-to-edge.
+
+---
+
+## Color
+
+The palette is built around a yellow-green primary at hue ~75 — the color of fresh basil, of olives on the branch. It is warm, slightly muted, alive. Every neutral in the system carries a faint echo of this hue (hue 60–70) so the palette feels cohesive and nothing reads as cold or disconnected.
+
+Backgrounds are never pure white or pure black. In light mode the background is a warm off-white. In dark mode it is a deep charcoal with a hint of green. This softness is deliberate — it reduces eye fatigue over long sessions and keeps the primary color from feeling harsh against it.
+
+### Semantic Tokens
+
+| Token | Light | Dark | Usage |
+|---|---|---|---|
+| `--background` | `hsl(60 8% 95%)` | `hsl(70 4% 9%)` | Page background |
+| `--foreground` | `hsl(70 8% 15%)` | `hsl(60 8% 85%)` | Primary text |
+| `--card` | `hsl(60 6% 97%)` | `hsl(70 4% 11%)` | Card and panel surfaces |
+| `--card-foreground` | `hsl(70 8% 15%)` | `hsl(60 8% 85%)` | Text on card surfaces |
+| `--muted` | `hsl(65 8% 91%)` | `hsl(70 4% 15%)` | Subtle fills, toggles, pill containers |
+| `--muted-foreground` | `hsl(70 4% 45%)` | `hsl(65 4% 50%)` | Secondary text, metadata, placeholders |
+| `--border` | `hsl(65 6% 86%)` | `hsl(70 4% 18%)` | Borders, dividers |
+| `--primary` | `hsl(75 60% 44%)` | `hsl(75 55% 52%)` | Brand accent — buttons, focus rings, active states |
+| `--primary-foreground` | `hsl(70 8% 15%)` | `hsl(70 4% 9%)` | Text on primary-colored surfaces |
+| `--destructive` | `hsl(0 55% 52%)` | `hsl(0 45% 42%)` | Error, delete actions |
+| `--destructive-foreground` | `hsl(0 0% 98%)` | `hsl(0 0% 98%)` | Text on destructive surfaces |
+| `--ring` | `hsl(75 60% 44%)` | `hsl(75 55% 52%)` | Focus rings (equals `--primary`) |
+| `--radius` | `0.75rem` | — | Global border radius (12px) — cards, panels, inputs |
+| `--radius-button` | `0.4375rem` | — | Button border radius (7px) — tighter than cards for a more grounded feel |
+
+### Primitive Palette
+
+Not used directly in components. These inform the semantic tokens.
+
+```
+olive-50   hsl(75 60% 96%)
+olive-100  hsl(75 60% 90%)
+olive-200  hsl(75 58% 78%)
+olive-300  hsl(75 56% 66%)
+olive-400  hsl(75 58% 54%)
+olive-500  hsl(75 60% 44%)   ← primary (light mode)
+olive-600  hsl(75 55% 38%)
+olive-700  hsl(75 50% 32%)
+olive-800  hsl(75 45% 24%)
+olive-900  hsl(75 40% 16%)
+```
+
+### Semantic Status Colors
+
+Status colors communicate state meaning. They are always used in icon backgrounds and small tints — never as large solid fills, which read as heavy.
+
+| Status | Background | Text | Dark text | Usage |
+|---|---|---|---|---|
+| Success / Active | `emerald-500/10` | `emerald-600` | `emerald-400` | Active, running, OK |
+| Warning / Paused | `amber-500/10` | `amber-600` | `amber-400` | Paused, pending, attention |
+| Error | `red-500/10` | `red-600` | `red-400` | Errors, failures, deletes |
+| Info | `sky-500/10` | `sky-600` | `sky-400` | Informational |
+| Debug | `purple-500/10` | `purple-600` | `purple-400` | Debug, verbose |
+| Trace | `muted/50` | `muted-foreground` | `muted-foreground` | Trace, lowest priority |
+
+In dark mode: increase alpha to `/15`, shift text to the `*-400` step.
+
+### Badge Classes
+
+For standalone status pills and type labels, use named CSS classes rather than composed Tailwind utilities. This is required for Tailwind v4 compatibility — composed utilities may not be generated if they don't appear in source.
+
+| Class | Meaning |
+|---|---|
+| `.badge-active` | Active / running |
+| `.badge-paused` | Paused / pending |
+| `.badge-error` | Error / failed |
+| `.badge-warning` | Warning / degraded |
+| `.badge-info` | Informational |
+| `.badge-debug` | Debug level |
+| `.badge-trace` | Trace level |
+| `.badge-write` | Write operation |
+| `.badge-delete` | Delete operation |
+
+### Color Rules
+
+- Use CSS variable classes (`text-primary`, `bg-muted`, `bg-primary/10`) over hardcoded Tailwind colors for all brand and neutral elements.
+- The only hardcoded Tailwind color classes acceptable are the semantic status colors above (emerald, amber, red, sky, purple), and only because they are explicitly listed in source.
+- No gradients. Flat, solid colors only. The primary color is strong enough without them.
+- No pure black (`#000`) or pure white (`#fff`). No cold gray Tailwind classes (`gray-*`, `slate-*`, `zinc-*`). Use the token system.
+- Icon container backgrounds use a `/10` tint. Never a solid fill.
+
+---
+
+## Responsive Design
+
+Olive interfaces must work on any screen — a phone held in one hand, a tablet at a café, a large monitor at a desk, and a display on a wall. This is not an afterthought. The layout system must flow gracefully across all of these.
+
+### Breakpoints
+
+| Name | Min-width | Typical device |
+|---|---|---|
+| (base) | 0px | Small phones |
+| `sm` | 640px | Large phones, small tablets |
+| `md` | 768px | Tablets |
+| `lg` | 1024px | Laptops |
+| `xl` | 1280px | Desktops |
+| `2xl` | 1536px | Wide monitors, TVs |
+
+### Layout Strategy
+
+- **Mobile first.** Write base styles for the smallest screen, then add overrides at larger breakpoints. Never start from desktop and work down.
+- **Fluid containers.** Page content uses `px-4 sm:px-6 lg:px-8` with `max-w-7xl`. Content breathes at every width.
+- **Responsive grids.** Cards and metrics use grids that reflow: `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`. Never hardcode column counts for all screens.
+- **Touch targets.** Interactive elements must be at minimum `44×44px` on touch screens. Use `size="lg"` for primary page-level CTAs on mobile. Never rely on hover for important affordances — touch devices have no hover state.
+- **Scrollable on small screens.** Horizontal-scrolling containers (`overflow-x-auto`) for tables and data views. Never clip or crop content — always provide a scroll path.
+- **Large screens.** At `2xl`, content should not stretch uncomfortably wide. The `max-w-7xl` constraint applies everywhere. Very wide displays see more breathing room around the content, not wider content.
+
+### Component-Level Responsiveness
+
+- Navigation collapses to a hamburger/sheet at `sm` and below.
+- Dialogs are `max-w-[calc(100vw-2rem)]` on mobile — always inset from the screen edge.
+- Sidebars and panels collapse to overlays on small screens.
+- Stat grids drop to 2 columns on mobile, 4 on desktop.
+- Tables get horizontal scroll containers on mobile rather than reflowing into cards (data tables need their columns).
+
+---
+
+## Spacing
+
+All spacing comes from the 4px/8px base scale. No arbitrary pixel values.
+
+| Step | px | rem | Common use |
+|---|---|---|---|
+| 1 | 4px | 0.25rem | Tight internal gaps, icon margins |
+| 2 | 8px | 0.5rem | Icon-to-label gaps, input padding |
+| 3 | 12px | 0.75rem | Component internal padding |
+| 4 | 16px | 1rem | Standard gap, card padding |
+| 6 | 24px | 1.5rem | Section gaps, between form fields |
+| 8 | 32px | 2rem | Between distinct content groups |
+| 12 | 48px | 3rem | Between page sections |
+| 16 | 64px | 4rem | Empty state padding, hero spacing |
+
+Generous whitespace is not wasteful. It is what lets the eye move without effort. When in doubt, add more space, not less.
+
+---
+
+## Shadows
+
+Shadows follow the foreground color at low opacity so they respect both light and dark themes.
+
+```css
+--shadow-sm:  0 1px 2px hsl(var(--foreground) / 0.06);
+--shadow-md:  0 4px 12px hsl(var(--foreground) / 0.08);
+--shadow-lg:  0 8px 24px hsl(var(--foreground) / 0.10);
+```
+
+In dark mode: reduce alpha by ~30%. Never use `box-shadow` with hardcoded colors.
+
+---
+
+## Border Radius
+
+Two radius tokens. Use the right one for the right surface — they serve different visual roles.
+
+| Token | Value | Use |
+|---|---|---|
+| `--radius` | `0.75rem` (12px) | Cards, panels, inputs, dropdowns, dialogs — surfaces that contain content |
+| `--radius-button` | `0.4375rem` (7px) | Buttons only — tighter rounding grounds them visually and distinguishes them from containers |
+
+Buttons use a smaller radius deliberately. Pill-shaped buttons (high radius) feel consumer and casual. A 7px radius reads as intentional and precise without feeling sharp.
+
+Never use `rounded-full` on buttons. Avatars and purely decorative circular elements are the only exception.
+
+---
+
+## Light and Dark Mode
+
+Both modes are first-class. Every component must work in light mode, dark mode, and with system preference. This is not optional. Test both before considering a component done.
+
+The CSS custom property system handles the majority of theming automatically. The soft, warm-tinted backgrounds mean dark mode looks like a candlelit room, not a cave. It is easy on the eyes for long sessions.
+
+Implementation:
+- `.dark` class on the root element (`<html>` or `<body>`)
+- Store explicit preference in `localStorage` as `"light"`, `"dark"`, or `"system"`
+- Default to `"system"` — respect `prefers-color-scheme` unless overridden
+- The ThemeToggle component cycles through all three states
+
+Watch for these dark mode edge cases:
+- Semi-transparent backgrounds: `bg-card/80` — test that the blurred content behind reads well in both modes
+- Semantic color tints: bump alpha from `/10` to `/15` in dark mode (`dark:bg-emerald-500/15`)
+- Hardcoded semantic text classes: add `dark:` variants (`text-emerald-600 dark:text-emerald-400`)
+- Glass morphism (`backdrop-blur`): test that the overlay is legible over both light and dark content behind it
+
+---
+
+## Accessibility
+
+Accessibility is not an enhancement — it is a baseline requirement.
+
+- **Focus rings**: All interactive elements show a visible focus ring using `--ring`. Never suppress `:focus-visible`. Keyboard users navigate these interfaces too.
+- **Touch targets**: Minimum `44×44px` for all interactive elements on touch screens.
+- **Color contrast**: All text must meet WCAG AA (4.5:1 for body text, 3:1 for large text). The token system is calibrated to meet this in both modes.
+- **Labels**: All form inputs have associated labels. Icon-only buttons always have a `aria-label` and a visible `Tooltip`.
+- **Keyboard navigation**: Dropdowns, dialogs, and menus are keyboard navigable (arrow keys, Escape, Enter/Space). Never build custom interactive elements without keyboard handling.
+- **Announcements**: Toasts and dynamic status changes use `aria-live` regions so screen readers catch them.
+- **Motion**: Respect `prefers-reduced-motion`. Wrap all transitions in the appropriate media query.
+
+---
+
+## Components
+
+The full Olive component inventory. All components are Svelte primitives exported from the library.
+
+---
+
+### Primitives
+
+The atoms. Everything else composes from these.
+
+| Component | Description |
+|---|---|
+| `Button` | Primary action surface. Variants: `default`, `outline`, `ghost`, `destructive`, `link`. Sizes: `sm` (28px), `md` (32px), `lg` (40px), `icon` (32px square). Uses `--radius-button` (7px). Default and destructive variants carry a subtle box-shadow for depth — inner top highlight and outer bottom shadow. Active state collapses the shadow inward. |
+| `Input` | Text input. Focus ring, placeholder, disabled, error state. |
+| `Textarea` | Multiline input. Vertical resize. |
+| `Label` | Form field label, associated via `for`. |
+| `Checkbox` | Binary toggle. Supports indeterminate state. |
+| `RadioGroup` + `RadioItem` | Mutually exclusive option group. |
+| `Select` + `SelectItem` | Dropdown selector. Keyboard navigable. |
+| `Switch` | Toggle for boolean settings. |
+| `Slider` | Range input. Single value and range variants. |
+| `Badge` | Status and label pill. Variants map to semantic status colors. |
+| `Avatar` | Image or initials identifier circle. |
+| `Separator` | Horizontal or vertical divider. |
+
+---
+
+### Layout
+
+Structure and containment.
+
+| Component | Description |
+|---|---|
+| `Card` + `CardHeader` + `CardTitle` + `CardDescription` + `CardContent` + `CardFooter` | Standard content container for any distinct entity. Do not nest cards unless there is a clear parent–child relationship. |
+| `Page` | Outermost page wrapper. Applies background, max-width, standard padding. |
+| `PageHeader` | Page title, description, and primary action slot. |
+| `Breadcrumbs` | Hierarchical path navigation. |
+| `Panel` | Fixed or floating side panel. Collapsible. |
+| `Sidebar` | Persistent lateral navigation for section-level structure. |
+| `Resizable` | Draggable split pane, horizontal or vertical. |
+| `ScrollArea` | Custom scrollbar container. Cross-platform consistent. |
+| `Tabs` + `TabList` + `Tab` + `TabPanel` | Section-level tab navigation. State-driven. |
+
+---
+
+### Navigation
+
+| Component | Description |
+|---|---|
+| `Navbar` | Sticky top bar with backdrop blur. Logo, nav links, and actions slots. `h-14` height. |
+| `NavItem` | Single nav link. Icon + label or label only. Active state. |
+| `MobileMenu` | Collapsible navigation panel for small screens. Opens from Navbar. |
+| `ThemeToggle` | Light / dark / system mode toggle. Icon button with smooth icon transition. |
+| `CommandPalette` | Keyboard-driven search and navigation overlay (⌘K). Debounced search, arrow-key navigation, shortcut hints. |
+
+---
+
+### Overlay & Feedback
+
+| Component | Description |
+|---|---|
+| `Dialog` + `DialogHeader` + `DialogContent` + `DialogFooter` | Modal for creation forms and destructive confirmations. Always requires explicit dismiss. |
+| `Sheet` | Sliding panel from screen edge. For detail views and settings drawers. |
+| `Tooltip` | Hover-activated label. Required on all icon-only buttons. |
+| `Popover` | Anchored floating container for inline content. |
+| `DropdownMenu` + `DropdownItem` + `DropdownSeparator` | Contextual action menu. Keyboard navigable. |
+| `ContextMenu` | Right-click menu. Same structure as DropdownMenu. |
+| `Toast` | Transient notification. Fixed bottom center. Auto-dismiss. Variants: default, success, warning, error. |
+| `Alert` | Inline status message. Not dismissible. Variants: info, success, warning, error. |
+| `AlertDialog` | Blocking confirmation for destructive actions. Explicit confirm + cancel required. |
+| `Progress` | Linear progress bar. Determinate and indeterminate. Color maps to semantic status. |
+| `Skeleton` | Loading placeholder. Matches the geometry of the content it replaces. |
+
+---
+
+### Data Display
+
+| Component | Description |
+|---|---|
+| `Table` + `TableHeader` + `TableRow` + `TableCell` + `TableBody` | Tabular data. Sticky headers. Monospace cells for data values. Hover row. Truncation with tooltip. |
+| `DataTable` | Table with sorting, filtering, and pagination built in. |
+| `CodeBlock` | Syntax-highlighted code display. Monospace. Copy-to-clipboard. Language label. |
+| `CodeEditor` | Monaco-based code editor. Dark/light theme sync. |
+| `JsonViewer` | Collapsible JSON tree. Syntax highlighted. Copy individual values. |
+| `KeyValue` | Compact key–value pair display. Monospace values. Copy action. |
+| `MetricCard` | Single metric: icon with tinted background, label, value. |
+| `StatGrid` | Responsive grid of MetricCards. |
+| `EmptyState` | Centered message and CTA for zero-data views. Always includes a primary action. |
+| `Timeline` | Interactive time-range control with play/pause/step playback. |
+| `RelationsGraph` | SVG entity relationship diagram. Node cards, Bezier curve edges, hover labels. |
+| `KanbanBoard` + `KanbanColumn` + `KanbanCard` | Drag-and-drop board. Columns with drop targets. Cards with hover-reveal actions. |
+
+---
+
+### Forms
+
+| Component | Description |
+|---|---|
+| `Form` + `FormField` + `FormLabel` + `FormControl` + `FormMessage` | Form structure with validation state, error messages, and accessible labeling. |
+| `Combobox` | Searchable select with type-ahead. Keyboard navigable. |
+| `MultiSelect` | Tag-style multi-value select. |
+| `DatePicker` | Calendar popover for date selection. |
+| `DateRangePicker` | Calendar popover for start and end date selection. |
+| `FileUpload` | Drop zone and click-to-browse. Drag-over state. File type and size validation. |
+| `SearchInput` | Input with search icon, clear button, and debounce. |
+
+---
+
+### Specialized
+
+Purpose-built for technical and infrastructure contexts.
+
+| Component | Description |
+|---|---|
+| `LogStream` | High-performance virtualized log viewer. Level badges, timestamp formatting, jump-to-bottom, pause-on-scroll. |
+| `SqlEditor` | Monaco editor configured for SQL. Schema-aware autocomplete, execution state. |
+| `SchemaView` | Table schema display: columns, types, constraints, indexes, foreign keys. |
+| `ConnectionString` | Formatted connection string with copy action and optional value masking. |
+| `ApiKeyCard` | Key display with masked value, type badge, copy button, created date, revoke action. |
+| `OperationBadge` | Badge for data operation types (Write, Delete, Update) with semantic color. |
+| `DrawCanvas` | Full-screen canvas. Tool states, pan, zoom, pointer event handling. |
+| `Toolbar` | Fixed vertical or horizontal toolbar. Tool sections with icon buttons and collapse. |
+| `PropertyPanel` | Collapsible properties sidebar. Sections, field rows with labels and controls. |
+
+---
+
+## Do's
+
+**Layout & Spacing**
+- Generous whitespace. More padding, not less. Space is structure.
+- `max-w-7xl` on all page content. Never edge-to-edge on wide screens.
+- 4px/8px spacing scale only. No arbitrary pixel values.
+- Left-align labels and headings. Right-align numeric data and actions.
+- Navbar at `h-14`. Present but not dominant.
+
+**Typography**
+- Plus Jakarta Sans everywhere. Do not add fonts.
+- Monospace for any content that belongs to a system.
+- `font-semibold` for titles, `font-medium` for labels, `font-normal` for body.
+- `muted-foreground` for secondary information.
+
+**Color**
+- CSS variable classes for all brand and neutral colors.
+- Semantic colors only: emerald, amber, red, sky, purple — and only for their defined meanings.
+- Background tints at `/10`–`/15` alpha. No solid colored areas.
+
+**Interactions**
+- Focus rings on every interactive element. Never suppress them.
+- Glass morphism (`bg-card/80 backdrop-blur-xl`) only for navbars and overlays.
+- Confirm destructive actions in an AlertDialog. Never on a single click.
+
+**Data**
+- Format numbers: `1.2K`, not `1200`.
+- Format timestamps: `Mar 11, 2026` or `2 hours ago`. Never raw ISO strings.
+- Skeleton states for loading. Empty states with a message and primary CTA.
+
+---
+
+## Don'ts
+
+- No gradients on UI elements. Flat, solid colors.
+- No new brand colors. `--primary` is the only accent.
+- No pure black, pure white, or cold grays. Always use the token system.
+- No arbitrary spacing or radius values.
+- No `!important`.
+- No nested cards without a clear parent–child relationship.
+- No center-aligned body text. Center only for auth headings and empty states.
+- No ALL CAPS except micro-labels.
+- No browser `alert()` or `confirm()`.
+- No hover-only affordances for critical actions.
+- No auto-close on dialog success. Show the result; let the user dismiss.
+- No loading spinners for operations under 200ms.
+- No raw system error strings. Translate them for humans.
+- No CSS animations on layout-triggering properties.
+
+---
+
+## Tone & Copy
+
+- Direct. "Create key" not "Get started by generating your first API key".
+- Sentence case. "API keys" not "API Keys" (exceptions: proper nouns, acronyms like SQL, JSON, UUID).
+- Precise vocabulary. "Query", "schema", "index" — not watered-down alternatives.
+- Error messages state what happened and what to do: "Name already taken. Choose a different one."
+- Confirmation messages are specific: "'production' deleted" not "Item removed successfully."
+- Under 15 words for any description.
+
+---
+
+## File Structure
+
+```
+olive/
+  src/
+    lib/
+      components/
+        primitives/     # Button, Input, Badge, Label, Checkbox, etc.
+        layout/         # Card, Page, Panel, Sidebar, Tabs, etc.
+        navigation/     # Navbar, NavItem, ThemeToggle, CommandPalette, etc.
+        overlay/        # Dialog, Toast, Tooltip, Popover, Sheet, etc.
+        data/           # Table, DataTable, CodeBlock, MetricCard, etc.
+        forms/          # Form, Combobox, DatePicker, FileUpload, etc.
+        specialized/    # LogStream, SqlEditor, DrawCanvas, Toolbar, etc.
+      tokens/           # index.css — all CSS custom properties
+      utils/            # cn(), formatNumber(), formatDate(), formatBytes()
+    routes/
+      +page.svelte      # Component showcase
+```

+ 1632 - 0
package-lock.json

@@ -0,0 +1,1632 @@
+{
+	"name": "olive",
+	"version": "0.0.1",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "olive",
+			"version": "0.0.1",
+			"dependencies": {
+				"@tailwindcss/vite": "^4.2.4",
+				"tailwindcss": "^4.2.4"
+			},
+			"devDependencies": {
+				"@sveltejs/adapter-auto": "^7.0.1",
+				"@sveltejs/kit": "^2.57.0",
+				"@sveltejs/vite-plugin-svelte": "^7.0.0",
+				"svelte": "^5.55.2",
+				"svelte-check": "^4.4.6",
+				"typescript": "^6.0.2",
+				"vite": "^8.0.7"
+			}
+		},
+		"node_modules/@emnapi/core": {
+			"version": "1.9.2",
+			"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+			"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"@emnapi/wasi-threads": "1.2.1",
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@emnapi/runtime": {
+			"version": "1.9.2",
+			"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+			"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@emnapi/wasi-threads": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+			"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@jridgewell/gen-mapping": {
+			"version": "0.3.13",
+			"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+			"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/sourcemap-codec": "^1.5.0",
+				"@jridgewell/trace-mapping": "^0.3.24"
+			}
+		},
+		"node_modules/@jridgewell/remapping": {
+			"version": "2.3.5",
+			"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+			"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/gen-mapping": "^0.3.5",
+				"@jridgewell/trace-mapping": "^0.3.24"
+			}
+		},
+		"node_modules/@jridgewell/resolve-uri": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+			"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
+		"node_modules/@jridgewell/sourcemap-codec": {
+			"version": "1.5.5",
+			"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+			"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+			"license": "MIT"
+		},
+		"node_modules/@jridgewell/trace-mapping": {
+			"version": "0.3.31",
+			"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+			"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/resolve-uri": "^3.1.0",
+				"@jridgewell/sourcemap-codec": "^1.4.14"
+			}
+		},
+		"node_modules/@napi-rs/wasm-runtime": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+			"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"@tybys/wasm-util": "^0.10.1"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/Brooooooklyn"
+			},
+			"peerDependencies": {
+				"@emnapi/core": "^1.7.1",
+				"@emnapi/runtime": "^1.7.1"
+			}
+		},
+		"node_modules/@oxc-project/types": {
+			"version": "0.126.0",
+			"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
+			"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
+			"license": "MIT",
+			"funding": {
+				"url": "https://github.com/sponsors/Boshen"
+			}
+		},
+		"node_modules/@polka/url": {
+			"version": "1.0.0-next.29",
+			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+			"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@rolldown/binding-android-arm64": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
+			"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-darwin-arm64": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
+			"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-darwin-x64": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
+			"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-freebsd-x64": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
+			"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
+			"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-arm64-gnu": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
+			"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-arm64-musl": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
+			"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-ppc64-gnu": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
+			"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
+			"cpu": [
+				"ppc64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-s390x-gnu": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
+			"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
+			"cpu": [
+				"s390x"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-x64-gnu": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
+			"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-linux-x64-musl": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
+			"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-openharmony-arm64": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
+			"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"openharmony"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-wasm32-wasi": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
+			"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
+			"cpu": [
+				"wasm32"
+			],
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"@emnapi/core": "1.9.2",
+				"@emnapi/runtime": "1.9.2",
+				"@napi-rs/wasm-runtime": "^1.1.4"
+			},
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-win32-arm64-msvc": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
+			"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/binding-win32-x64-msvc": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
+			"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			}
+		},
+		"node_modules/@rolldown/pluginutils": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
+			"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
+			"license": "MIT"
+		},
+		"node_modules/@standard-schema/spec": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+			"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@sveltejs/acorn-typescript": {
+			"version": "1.0.9",
+			"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
+			"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"acorn": "^8.9.0"
+			}
+		},
+		"node_modules/@sveltejs/adapter-auto": {
+			"version": "7.0.1",
+			"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz",
+			"integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"@sveltejs/kit": "^2.0.0"
+			}
+		},
+		"node_modules/@sveltejs/kit": {
+			"version": "2.57.1",
+			"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.57.1.tgz",
+			"integrity": "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@standard-schema/spec": "^1.0.0",
+				"@sveltejs/acorn-typescript": "^1.0.5",
+				"@types/cookie": "^0.6.0",
+				"acorn": "^8.14.1",
+				"cookie": "^0.6.0",
+				"devalue": "^5.6.4",
+				"esm-env": "^1.2.2",
+				"kleur": "^4.1.5",
+				"magic-string": "^0.30.5",
+				"mrmime": "^2.0.0",
+				"set-cookie-parser": "^3.0.0",
+				"sirv": "^3.0.0"
+			},
+			"bin": {
+				"svelte-kit": "svelte-kit.js"
+			},
+			"engines": {
+				"node": ">=18.13"
+			},
+			"peerDependencies": {
+				"@opentelemetry/api": "^1.0.0",
+				"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
+				"svelte": "^4.0.0 || ^5.0.0-next.0",
+				"typescript": "^5.3.3 || ^6.0.0",
+				"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
+			},
+			"peerDependenciesMeta": {
+				"@opentelemetry/api": {
+					"optional": true
+				},
+				"typescript": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@sveltejs/vite-plugin-svelte": {
+			"version": "7.0.0",
+			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz",
+			"integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"deepmerge": "^4.3.1",
+				"magic-string": "^0.30.21",
+				"obug": "^2.1.0",
+				"vitefu": "^1.1.2"
+			},
+			"engines": {
+				"node": "^20.19 || ^22.12 || >=24"
+			},
+			"peerDependencies": {
+				"svelte": "^5.46.4",
+				"vite": "^8.0.0-beta.7 || ^8.0.0"
+			}
+		},
+		"node_modules/@tailwindcss/node": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
+			"integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/remapping": "^2.3.5",
+				"enhanced-resolve": "^5.19.0",
+				"jiti": "^2.6.1",
+				"lightningcss": "1.32.0",
+				"magic-string": "^0.30.21",
+				"source-map-js": "^1.2.1",
+				"tailwindcss": "4.2.4"
+			}
+		},
+		"node_modules/@tailwindcss/oxide": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz",
+			"integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 20"
+			},
+			"optionalDependencies": {
+				"@tailwindcss/oxide-android-arm64": "4.2.4",
+				"@tailwindcss/oxide-darwin-arm64": "4.2.4",
+				"@tailwindcss/oxide-darwin-x64": "4.2.4",
+				"@tailwindcss/oxide-freebsd-x64": "4.2.4",
+				"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
+				"@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
+				"@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
+				"@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
+				"@tailwindcss/oxide-linux-x64-musl": "4.2.4",
+				"@tailwindcss/oxide-wasm32-wasi": "4.2.4",
+				"@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
+				"@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-android-arm64": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz",
+			"integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-darwin-arm64": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz",
+			"integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-darwin-x64": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz",
+			"integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-freebsd-x64": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz",
+			"integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz",
+			"integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz",
+			"integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz",
+			"integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz",
+			"integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-x64-musl": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz",
+			"integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-wasm32-wasi": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz",
+			"integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
+			"bundleDependencies": [
+				"@napi-rs/wasm-runtime",
+				"@emnapi/core",
+				"@emnapi/runtime",
+				"@tybys/wasm-util",
+				"@emnapi/wasi-threads",
+				"tslib"
+			],
+			"cpu": [
+				"wasm32"
+			],
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"@emnapi/core": "^1.8.1",
+				"@emnapi/runtime": "^1.8.1",
+				"@emnapi/wasi-threads": "^1.1.0",
+				"@napi-rs/wasm-runtime": "^1.1.1",
+				"@tybys/wasm-util": "^0.10.1",
+				"tslib": "^2.8.1"
+			},
+			"engines": {
+				"node": ">=14.0.0"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
+			"integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz",
+			"integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 20"
+			}
+		},
+		"node_modules/@tailwindcss/vite": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz",
+			"integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==",
+			"license": "MIT",
+			"dependencies": {
+				"@tailwindcss/node": "4.2.4",
+				"@tailwindcss/oxide": "4.2.4",
+				"tailwindcss": "4.2.4"
+			},
+			"peerDependencies": {
+				"vite": "^5.2.0 || ^6 || ^7 || ^8"
+			}
+		},
+		"node_modules/@tybys/wasm-util": {
+			"version": "0.10.1",
+			"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+			"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+			"license": "MIT",
+			"optional": true,
+			"dependencies": {
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@types/cookie": {
+			"version": "0.6.0",
+			"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+			"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@types/estree": {
+			"version": "1.0.8",
+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+			"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@types/trusted-types": {
+			"version": "2.0.7",
+			"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+			"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/acorn": {
+			"version": "8.16.0",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+			"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+			"dev": true,
+			"license": "MIT",
+			"bin": {
+				"acorn": "bin/acorn"
+			},
+			"engines": {
+				"node": ">=0.4.0"
+			}
+		},
+		"node_modules/aria-query": {
+			"version": "5.3.1",
+			"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
+			"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/axobject-query": {
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+			"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/chokidar": {
+			"version": "4.0.3",
+			"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+			"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"readdirp": "^4.0.1"
+			},
+			"engines": {
+				"node": ">= 14.16.0"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/clsx": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+			"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/cookie": {
+			"version": "0.6.0",
+			"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+			"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/deepmerge": {
+			"version": "4.3.1",
+			"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+			"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/detect-libc": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+			"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/devalue": {
+			"version": "5.7.1",
+			"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz",
+			"integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/enhanced-resolve": {
+			"version": "5.20.1",
+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+			"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+			"license": "MIT",
+			"dependencies": {
+				"graceful-fs": "^4.2.4",
+				"tapable": "^2.3.0"
+			},
+			"engines": {
+				"node": ">=10.13.0"
+			}
+		},
+		"node_modules/esm-env": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+			"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/esrap": {
+			"version": "2.2.5",
+			"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz",
+			"integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/sourcemap-codec": "^1.4.15"
+			},
+			"peerDependencies": {
+				"@typescript-eslint/types": "^8.2.0"
+			},
+			"peerDependenciesMeta": {
+				"@typescript-eslint/types": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/fdir": {
+			"version": "6.5.0",
+			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+			"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"peerDependencies": {
+				"picomatch": "^3 || ^4"
+			},
+			"peerDependenciesMeta": {
+				"picomatch": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/fsevents": {
+			"version": "2.3.3",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+			"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+			"hasInstallScript": true,
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+			}
+		},
+		"node_modules/graceful-fs": {
+			"version": "4.2.11",
+			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+			"license": "ISC"
+		},
+		"node_modules/is-reference": {
+			"version": "3.0.3",
+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+			"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@types/estree": "^1.0.6"
+			}
+		},
+		"node_modules/jiti": {
+			"version": "2.6.1",
+			"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+			"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+			"license": "MIT",
+			"bin": {
+				"jiti": "lib/jiti-cli.mjs"
+			}
+		},
+		"node_modules/kleur": {
+			"version": "4.1.5",
+			"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+			"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/lightningcss": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+			"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+			"license": "MPL-2.0",
+			"dependencies": {
+				"detect-libc": "^2.0.3"
+			},
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			},
+			"optionalDependencies": {
+				"lightningcss-android-arm64": "1.32.0",
+				"lightningcss-darwin-arm64": "1.32.0",
+				"lightningcss-darwin-x64": "1.32.0",
+				"lightningcss-freebsd-x64": "1.32.0",
+				"lightningcss-linux-arm-gnueabihf": "1.32.0",
+				"lightningcss-linux-arm64-gnu": "1.32.0",
+				"lightningcss-linux-arm64-musl": "1.32.0",
+				"lightningcss-linux-x64-gnu": "1.32.0",
+				"lightningcss-linux-x64-musl": "1.32.0",
+				"lightningcss-win32-arm64-msvc": "1.32.0",
+				"lightningcss-win32-x64-msvc": "1.32.0"
+			}
+		},
+		"node_modules/lightningcss-android-arm64": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+			"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-darwin-arm64": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+			"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-darwin-x64": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+			"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-freebsd-x64": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+			"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm-gnueabihf": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+			"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm64-gnu": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+			"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm64-musl": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+			"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-x64-gnu": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+			"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-x64-musl": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+			"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-win32-arm64-msvc": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+			"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-win32-x64-msvc": {
+			"version": "1.32.0",
+			"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+			"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/locate-character": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+			"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/magic-string": {
+			"version": "0.30.21",
+			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+			"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/sourcemap-codec": "^1.5.5"
+			}
+		},
+		"node_modules/mri": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+			"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/mrmime": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+			"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/nanoid": {
+			"version": "3.3.11",
+			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+			"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"bin": {
+				"nanoid": "bin/nanoid.cjs"
+			},
+			"engines": {
+				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+			}
+		},
+		"node_modules/obug": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+			"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+			"dev": true,
+			"funding": [
+				"https://github.com/sponsors/sxzz",
+				"https://opencollective.com/debug"
+			],
+			"license": "MIT"
+		},
+		"node_modules/picocolors": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+			"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+			"license": "ISC"
+		},
+		"node_modules/picomatch": {
+			"version": "4.0.4",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+			"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/jonschlinkert"
+			}
+		},
+		"node_modules/postcss": {
+			"version": "8.5.10",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+			"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+			"funding": [
+				{
+					"type": "opencollective",
+					"url": "https://opencollective.com/postcss/"
+				},
+				{
+					"type": "tidelift",
+					"url": "https://tidelift.com/funding/github/npm/postcss"
+				},
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"dependencies": {
+				"nanoid": "^3.3.11",
+				"picocolors": "^1.1.1",
+				"source-map-js": "^1.2.1"
+			},
+			"engines": {
+				"node": "^10 || ^12 || >=14"
+			}
+		},
+		"node_modules/readdirp": {
+			"version": "4.1.2",
+			"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+			"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 14.18.0"
+			},
+			"funding": {
+				"type": "individual",
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/rolldown": {
+			"version": "1.0.0-rc.16",
+			"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
+			"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
+			"license": "MIT",
+			"dependencies": {
+				"@oxc-project/types": "=0.126.0",
+				"@rolldown/pluginutils": "1.0.0-rc.16"
+			},
+			"bin": {
+				"rolldown": "bin/cli.mjs"
+			},
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			},
+			"optionalDependencies": {
+				"@rolldown/binding-android-arm64": "1.0.0-rc.16",
+				"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
+				"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
+				"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
+				"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
+				"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
+				"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
+				"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
+				"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
+				"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
+				"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
+				"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
+				"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
+				"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
+				"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
+			}
+		},
+		"node_modules/sade": {
+			"version": "1.8.1",
+			"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+			"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"mri": "^1.1.0"
+			},
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/set-cookie-parser": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
+			"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/sirv": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+			"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@polka/url": "^1.0.0-next.24",
+				"mrmime": "^2.0.0",
+				"totalist": "^3.0.0"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/source-map-js": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+			"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+			"license": "BSD-3-Clause",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/svelte": {
+			"version": "5.55.4",
+			"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz",
+			"integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/remapping": "^2.3.4",
+				"@jridgewell/sourcemap-codec": "^1.5.0",
+				"@sveltejs/acorn-typescript": "^1.0.5",
+				"@types/estree": "^1.0.5",
+				"@types/trusted-types": "^2.0.7",
+				"acorn": "^8.12.1",
+				"aria-query": "5.3.1",
+				"axobject-query": "^4.1.0",
+				"clsx": "^2.1.1",
+				"devalue": "^5.6.4",
+				"esm-env": "^1.2.1",
+				"esrap": "^2.2.4",
+				"is-reference": "^3.0.3",
+				"locate-character": "^3.0.0",
+				"magic-string": "^0.30.11",
+				"zimmerframe": "^1.1.2"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/svelte-check": {
+			"version": "4.4.6",
+			"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz",
+			"integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/trace-mapping": "^0.3.25",
+				"chokidar": "^4.0.1",
+				"fdir": "^6.2.0",
+				"picocolors": "^1.0.0",
+				"sade": "^1.7.4"
+			},
+			"bin": {
+				"svelte-check": "bin/svelte-check"
+			},
+			"engines": {
+				"node": ">= 18.0.0"
+			},
+			"peerDependencies": {
+				"svelte": "^4.0.0 || ^5.0.0-next.0",
+				"typescript": ">=5.0.0"
+			}
+		},
+		"node_modules/tailwindcss": {
+			"version": "4.2.4",
+			"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
+			"integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
+			"license": "MIT"
+		},
+		"node_modules/tapable": {
+			"version": "2.3.3",
+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+			"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/webpack"
+			}
+		},
+		"node_modules/tinyglobby": {
+			"version": "0.2.16",
+			"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+			"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+			"license": "MIT",
+			"dependencies": {
+				"fdir": "^6.5.0",
+				"picomatch": "^4.0.4"
+			},
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/SuperchupuDev"
+			}
+		},
+		"node_modules/totalist": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+			"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/tslib": {
+			"version": "2.8.1",
+			"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+			"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+			"license": "0BSD",
+			"optional": true
+		},
+		"node_modules/typescript": {
+			"version": "6.0.3",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+			"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"bin": {
+				"tsc": "bin/tsc",
+				"tsserver": "bin/tsserver"
+			},
+			"engines": {
+				"node": ">=14.17"
+			}
+		},
+		"node_modules/vite": {
+			"version": "8.0.9",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
+			"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
+			"license": "MIT",
+			"dependencies": {
+				"lightningcss": "^1.32.0",
+				"picomatch": "^4.0.4",
+				"postcss": "^8.5.10",
+				"rolldown": "1.0.0-rc.16",
+				"tinyglobby": "^0.2.16"
+			},
+			"bin": {
+				"vite": "bin/vite.js"
+			},
+			"engines": {
+				"node": "^20.19.0 || >=22.12.0"
+			},
+			"funding": {
+				"url": "https://github.com/vitejs/vite?sponsor=1"
+			},
+			"optionalDependencies": {
+				"fsevents": "~2.3.3"
+			},
+			"peerDependencies": {
+				"@types/node": "^20.19.0 || >=22.12.0",
+				"@vitejs/devtools": "^0.1.0",
+				"esbuild": "^0.27.0 || ^0.28.0",
+				"jiti": ">=1.21.0",
+				"less": "^4.0.0",
+				"sass": "^1.70.0",
+				"sass-embedded": "^1.70.0",
+				"stylus": ">=0.54.8",
+				"sugarss": "^5.0.0",
+				"terser": "^5.16.0",
+				"tsx": "^4.8.1",
+				"yaml": "^2.4.2"
+			},
+			"peerDependenciesMeta": {
+				"@types/node": {
+					"optional": true
+				},
+				"@vitejs/devtools": {
+					"optional": true
+				},
+				"esbuild": {
+					"optional": true
+				},
+				"jiti": {
+					"optional": true
+				},
+				"less": {
+					"optional": true
+				},
+				"sass": {
+					"optional": true
+				},
+				"sass-embedded": {
+					"optional": true
+				},
+				"stylus": {
+					"optional": true
+				},
+				"sugarss": {
+					"optional": true
+				},
+				"terser": {
+					"optional": true
+				},
+				"tsx": {
+					"optional": true
+				},
+				"yaml": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/vitefu": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+			"integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+			"dev": true,
+			"license": "MIT",
+			"workspaces": [
+				"tests/deps/*",
+				"tests/projects/*",
+				"tests/projects/workspace/packages/*"
+			],
+			"peerDependencies": {
+				"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+			},
+			"peerDependenciesMeta": {
+				"vite": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/zimmerframe": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
+			"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
+			"dev": true,
+			"license": "MIT"
+		}
+	}
+}

+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "olive",
+  "version": "0.0.1",
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build",
+    "preview": "vite preview",
+    "prepare": "svelte-kit sync || echo ''",
+    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
+  },
+  "devDependencies": {
+    "@sveltejs/adapter-auto": "^7.0.1",
+    "@sveltejs/kit": "^2.57.0",
+    "@sveltejs/vite-plugin-svelte": "^7.0.0",
+    "svelte": "^5.55.2",
+    "svelte-check": "^4.4.6",
+    "typescript": "^6.0.2",
+    "vite": "^8.0.7"
+  },
+  "dependencies": {
+    "@tailwindcss/vite": "^4.2.4",
+    "tailwindcss": "^4.2.4"
+  }
+}

+ 325 - 0
src/app.css

@@ -0,0 +1,325 @@
+@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
+@import "tailwindcss";
+
+/* ─── Design Tokens ─────────────────────────────────────────────────────── */
+
+@theme {
+  /* Fonts */
+  --font-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
+  --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", "Fira Code", monospace;
+
+  /* Olive primitive palette */
+  --color-olive-50:  hsl(75 60% 96%);
+  --color-olive-100: hsl(75 60% 90%);
+  --color-olive-200: hsl(75 58% 78%);
+  --color-olive-300: hsl(75 56% 66%);
+  --color-olive-400: hsl(75 58% 54%);
+  --color-olive-500: hsl(75 60% 44%);
+  --color-olive-600: hsl(75 55% 38%);
+  --color-olive-700: hsl(75 50% 32%);
+  --color-olive-800: hsl(75 45% 24%);
+  --color-olive-900: hsl(75 40% 16%);
+
+  /* Border radius */
+  --radius: 0.75rem;        /* 12px — cards, panels, inputs */
+  --radius-button: 0.4375rem; /* 7px  — buttons specifically */
+  --radius-sm: calc(var(--radius) * 0.5);
+  --radius-lg: calc(var(--radius) * 1.5);
+  --radius-full: 9999px;
+
+  /* Motion */
+  --duration-micro:  100ms;
+  --duration-fast:   150ms;
+  --duration-base:   200ms;
+  --duration-slow:   250ms;
+  --duration-page:   350ms;
+
+  --ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
+  --ease-exit:  cubic-bezier(0.4, 0, 1, 1);
+  --ease-move:  cubic-bezier(0.25, 0.46, 0.45, 0.94);
+}
+
+/* ─── Semantic color variables ──────────────────────────────────────────── */
+
+:root {
+  --background:          hsl(60 8% 95%);
+  --foreground:          hsl(70 8% 15%);
+
+  --card:                hsl(60 6% 97%);
+  --card-foreground:     hsl(70 8% 15%);
+
+  --muted:               hsl(65 8% 91%);
+  --muted-foreground:    hsl(70 4% 45%);
+
+  --border:              hsl(65 6% 86%);
+
+  --primary:             hsl(75 60% 44%);
+  --primary-foreground:  hsl(70 8% 15%);
+
+  --destructive:         hsl(0 55% 52%);
+  --destructive-foreground: hsl(0 0% 98%);
+
+  --ring:                hsl(75 60% 44%);
+
+  --shadow-sm: 0 1px 2px hsl(70 8% 15% / 0.06);
+  --shadow-md: 0 4px 12px hsl(70 8% 15% / 0.08);
+  --shadow-lg: 0 8px 24px hsl(70 8% 15% / 0.10);
+}
+
+.dark {
+  --background:          hsl(70 4% 9%);
+  --foreground:          hsl(60 8% 85%);
+
+  --card:                hsl(70 4% 11%);
+  --card-foreground:     hsl(60 8% 85%);
+
+  --muted:               hsl(70 4% 15%);
+  --muted-foreground:    hsl(65 4% 50%);
+
+  --border:              hsl(70 4% 18%);
+
+  --primary:             hsl(75 55% 52%);
+  --primary-foreground:  hsl(70 4% 9%);
+
+  --destructive:         hsl(0 45% 42%);
+  --destructive-foreground: hsl(0 0% 98%);
+
+  --ring:                hsl(75 55% 52%);
+
+  --shadow-sm: 0 1px 2px hsl(70 4% 4% / 0.4);
+  --shadow-md: 0 4px 12px hsl(70 4% 4% / 0.5);
+  --shadow-lg: 0 8px 24px hsl(70 4% 4% / 0.6);
+}
+
+/* ─── Tailwind v4 CSS variable bridge ───────────────────────────────────── */
+/*
+   Tailwind v4 uses @theme for its utility generation, but our semantic tokens
+   live in :root/:dark. We bridge them here so Tailwind utilities like
+   bg-background, text-foreground etc. work correctly.
+*/
+
+@theme inline {
+  --color-background:         var(--background);
+  --color-foreground:         var(--foreground);
+  --color-card:               var(--card);
+  --color-card-foreground:    var(--card-foreground);
+  --color-muted:              var(--muted);
+  --color-muted-foreground:   var(--muted-foreground);
+  --color-border:             var(--border);
+  --color-primary:            var(--primary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-destructive:        var(--destructive);
+  --color-destructive-foreground: var(--destructive-foreground);
+  --color-ring:               var(--ring);
+}
+
+/* ─── Base styles ───────────────────────────────────────────────────────── */
+
+*, *::before, *::after {
+  box-sizing: border-box;
+}
+
+html {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-rendering: optimizeLegibility;
+}
+
+body {
+  background-color: var(--background);
+  color: var(--foreground);
+  font-family: var(--font-sans);
+  font-size: 0.875rem;
+  line-height: 1.5;
+  min-height: 100dvh;
+}
+
+/* Respect reduced motion */
+@media (prefers-reduced-motion: reduce) {
+  *, *::before, *::after {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+  }
+}
+
+/* ─── Badge semantic classes ─────────────────────────────────────────────── */
+/*
+  Light mode: solid-ish tinted backgrounds — enough opacity to be distinct
+  against the warm off-white canvas. Borders are full-opacity, slightly deeper
+  than the bg.
+  Dark mode: keeps the semi-transparent glow approach that works so well.
+*/
+
+.badge-active {
+  background-color: hsl(160 55% 88%);
+  color: hsl(160 70% 24%);
+  border-color: hsl(160 50% 76%);
+}
+.dark .badge-active {
+  background-color: hsl(160 84% 39% / 0.15);
+  color: hsl(160 70% 58%);
+  border-color: hsl(160 84% 39% / 0.32);
+}
+
+.badge-paused {
+  background-color: hsl(38 80% 88%);
+  color: hsl(38 80% 26%);
+  border-color: hsl(38 70% 74%);
+}
+.dark .badge-paused {
+  background-color: hsl(38 92% 50% / 0.15);
+  color: hsl(38 92% 68%);
+  border-color: hsl(38 92% 50% / 0.32);
+}
+
+.badge-error {
+  background-color: hsl(0 70% 92%);
+  color: hsl(0 65% 34%);
+  border-color: hsl(0 60% 80%);
+}
+.dark .badge-error {
+  background-color: hsl(0 72% 51% / 0.15);
+  color: hsl(0 72% 70%);
+  border-color: hsl(0 72% 51% / 0.32);
+}
+
+.badge-warning {
+  background-color: hsl(38 80% 88%);
+  color: hsl(38 80% 22%);
+  border-color: hsl(38 70% 74%);
+}
+.dark .badge-warning {
+  background-color: hsl(38 92% 50% / 0.15);
+  color: hsl(38 92% 68%);
+  border-color: hsl(38 92% 50% / 0.32);
+}
+
+.badge-info {
+  background-color: hsl(199 70% 90%);
+  color: hsl(199 80% 26%);
+  border-color: hsl(199 60% 78%);
+}
+.dark .badge-info {
+  background-color: hsl(199 89% 48% / 0.15);
+  color: hsl(199 89% 68%);
+  border-color: hsl(199 89% 48% / 0.32);
+}
+
+.badge-debug {
+  background-color: hsl(271 60% 92%);
+  color: hsl(271 65% 34%);
+  border-color: hsl(271 50% 80%);
+}
+.dark .badge-debug {
+  background-color: hsl(271 81% 56% / 0.15);
+  color: hsl(271 81% 72%);
+  border-color: hsl(271 81% 56% / 0.32);
+}
+
+.badge-trace {
+  background-color: var(--muted);
+  color: var(--muted-foreground);
+  border-color: var(--border);
+}
+
+.badge-write {
+  background-color: hsl(199 70% 90%);
+  color: hsl(199 80% 26%);
+  border-color: hsl(199 60% 78%);
+}
+.dark .badge-write {
+  background-color: hsl(199 89% 48% / 0.15);
+  color: hsl(199 89% 68%);
+  border-color: hsl(199 89% 48% / 0.32);
+}
+
+.badge-delete {
+  background-color: hsl(0 70% 92%);
+  color: hsl(0 65% 34%);
+  border-color: hsl(0 60% 80%);
+}
+.dark .badge-delete {
+  background-color: hsl(0 72% 51% / 0.15);
+  color: hsl(0 72% 70%);
+  border-color: hsl(0 72% 51% / 0.32);
+}
+
+
+/* ─── Alert semantic classes ─────────────────────────────────────────────── */
+/*
+  Same approach as badges:
+  Light: solid tinted backgrounds, rich text for legibility on the warm off-white canvas.
+  Dark:  semi-transparent glows — the color shines through the dark background.
+*/
+
+.alert-info {
+  background-color: hsl(199 70% 90%);
+  border-color: hsl(199 60% 78%);
+  color: hsl(199 80% 22%);
+}
+.dark .alert-info {
+  background-color: hsl(199 89% 48% / 0.12);
+  border-color: hsl(199 89% 48% / 0.28);
+  color: hsl(199 80% 72%);
+}
+
+.alert-success {
+  background-color: hsl(160 55% 88%);
+  border-color: hsl(160 50% 74%);
+  color: hsl(160 70% 20%);
+}
+.dark .alert-success {
+  background-color: hsl(160 84% 39% / 0.12);
+  border-color: hsl(160 84% 39% / 0.28);
+  color: hsl(160 60% 62%);
+}
+
+.alert-warning {
+  background-color: hsl(45 90% 88%);
+  border-color: hsl(38 80% 72%);
+  color: hsl(38 80% 22%);
+}
+.dark .alert-warning {
+  background-color: hsl(38 92% 50% / 0.12);
+  border-color: hsl(38 92% 50% / 0.28);
+  color: hsl(38 90% 68%);
+}
+
+.alert-error {
+  background-color: hsl(0 70% 92%);
+  border-color: hsl(0 60% 78%);
+  color: hsl(0 65% 28%);
+}
+.dark .alert-error {
+  background-color: hsl(0 72% 51% / 0.12);
+  border-color: hsl(0 72% 51% / 0.28);
+  color: hsl(0 72% 72%);
+}
+
+/* ─── Focus ring utility ─────────────────────────────────────────────────── */
+
+.focus-ring {
+  outline: none;
+}
+.focus-ring:focus-visible {
+  outline: 2px solid var(--ring);
+  outline-offset: 2px;
+}
+
+/* ─── Scrollbar ──────────────────────────────────────────────────────────── */
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+::-webkit-scrollbar-thumb {
+  background: var(--border);
+  border-radius: var(--radius-full);
+}
+::-webkit-scrollbar-thumb:hover {
+  background: var(--muted-foreground);
+}

+ 13 - 0
src/app.d.ts

@@ -0,0 +1,13 @@
+// See https://svelte.dev/docs/kit/types#app.d.ts
+// for information about these interfaces
+declare global {
+	namespace App {
+		// interface Error {}
+		// interface Locals {}
+		// interface PageData {}
+		// interface PageState {}
+		// interface Platform {}
+	}
+}
+
+export {};

+ 18 - 0
src/app.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		%sveltekit.head%
+		<script>
+			(function(){
+				var s=localStorage.getItem('olive-theme');
+				var d=window.matchMedia('(prefers-color-scheme: dark)').matches;
+				if(s==='dark'||(s!=='light'&&d))document.documentElement.classList.add('dark');
+			})();
+		</script>
+	</head>
+	<body data-sveltekit-preload-data="hover">
+		<div style="display: contents">%sveltekit.body%</div>
+	</body>
+</html>

+ 1 - 0
src/lib/assets/favicon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

+ 55 - 0
src/lib/components/data/CodeBlock.svelte

@@ -0,0 +1,55 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    code: string;
+    language?: string;
+    class?: string;
+  }
+
+  let { code, language, class: className = '' }: Props = $props();
+
+  let copied = $state(false);
+
+  async function copy() {
+    await navigator.clipboard.writeText(code);
+    copied = true;
+    setTimeout(() => { copied = false; }, 2000);
+  }
+</script>
+
+<div class={cn('relative rounded-[var(--radius)] border border-[var(--border)] bg-[var(--muted)]/50 overflow-hidden', className)}>
+  <!-- Header bar -->
+  <div class="flex items-center justify-between px-4 py-2 border-b border-[var(--border)]">
+    {#if language}
+      <span class="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide">{language}</span>
+    {:else}
+      <span></span>
+    {/if}
+    <button
+      type="button"
+      onclick={copy}
+      aria-label="Copy code"
+      class={cn(
+        'flex items-center gap-1.5 text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)]',
+        'cursor-pointer transition-colors duration-[150ms]',
+        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] rounded'
+      )}
+    >
+      {#if copied}
+        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+          <path d="M20 6L9 17l-5-5"/>
+        </svg>
+        Copied
+      {:else}
+        <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="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
+        </svg>
+        Copy
+      {/if}
+    </button>
+  </div>
+
+  <!-- Code -->
+  <pre class="overflow-x-auto px-4 py-3 text-sm font-mono text-[var(--foreground)] leading-relaxed"><code>{code}</code></pre>
+</div>

+ 200 - 0
src/lib/components/data/DataTable.svelte

@@ -0,0 +1,200 @@
+<script lang="ts" generics="T extends Record<string, unknown>">
+  import { cn } from '$lib/utils/cn.js';
+
+  type SortDir = 'asc' | 'desc' | null;
+
+  interface Column<T> {
+    key: keyof T;
+    header: string;
+    sortable?: boolean;
+    width?: string;
+    render?: (value: T[keyof T], row: T) => string;
+  }
+
+  interface Props {
+    data: T[];
+    columns: Column<T>[];
+    pageSize?: number;
+    searchable?: boolean;
+    searchPlaceholder?: string;
+    empty?: string;
+    class?: string;
+  }
+
+  let {
+    data,
+    columns,
+    pageSize = 10,
+    searchable = false,
+    searchPlaceholder = 'Search…',
+    empty = 'No results',
+    class: className = '',
+  }: Props = $props();
+
+  let query = $state('');
+  let sortKey = $state<keyof T | null>(null);
+  let sortDir = $state<SortDir>(null);
+  let page = $state(0);
+
+  let filtered = $derived.by(() => {
+    if (!query.trim()) return data;
+    const q = query.toLowerCase();
+    return data.filter(row =>
+      columns.some(col => String(row[col.key] ?? '').toLowerCase().includes(q))
+    );
+  });
+
+  let sorted = $derived.by(() => {
+    const key = sortKey;
+    const dir = sortDir;
+    if (!key || !dir) return filtered;
+    return [...filtered].sort((a, b) => {
+      const av = a[key] as string | number;
+      const bv = b[key] as string | number;
+      const cmp = av < bv ? -1 : av > bv ? 1 : 0;
+      return dir === 'asc' ? cmp : -cmp;
+    });
+  });
+
+  let totalPages = $derived(Math.max(1, Math.ceil(sorted.length / pageSize)));
+  let paged = $derived(sorted.slice(page * pageSize, (page + 1) * pageSize));
+
+  $effect(() => { query; page = 0; });
+  $effect(() => { sortKey; sortDir; page = 0; });
+
+  function toggleSort(key: keyof T) {
+    if (sortKey === key) {
+      sortDir = sortDir === 'asc' ? 'desc' : sortDir === 'desc' ? null : 'asc';
+      if (sortDir === null) sortKey = null;
+    } else {
+      sortKey = key;
+      sortDir = 'asc';
+    }
+  }
+
+  function cellValue(row: T, col: Column<T>): string {
+    const v = row[col.key];
+    return col.render ? col.render(v, row) : String(v ?? '');
+  }
+</script>
+
+<div class={cn('flex flex-col gap-3', className)}>
+  {#if searchable}
+    <div class="relative flex items-center">
+      <svg class="pointer-events-none absolute left-3 h-4 w-4 text-[var(--muted-foreground)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+        <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
+      </svg>
+      <input
+        type="text"
+        bind:value={query}
+        placeholder={searchPlaceholder}
+        class={cn(
+          'flex h-9 w-full rounded-[var(--radius)] border border-[var(--border)] bg-transparent',
+          'pl-9 pr-3 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]',
+          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+          'transition-colors duration-[150ms]'
+        )}
+      />
+    </div>
+  {/if}
+
+  <div class="overflow-x-auto rounded-[var(--radius)] border border-[var(--border)]">
+    <table class="w-full text-sm">
+      <thead>
+        <tr class="border-b border-[var(--border)] bg-[var(--muted)]/40">
+          {#each columns as col}
+            <th
+              class={cn(
+                'px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]',
+                col.sortable && 'cursor-pointer select-none hover:text-[var(--foreground)] transition-colors'
+              )}
+              style={col.width ? `width: ${col.width}` : ''}
+              onclick={() => col.sortable && toggleSort(col.key)}
+            >
+              <span class="inline-flex items-center gap-1.5">
+                {col.header}
+                {#if col.sortable}
+                  <span class="flex flex-col gap-px">
+                    <svg width="8" height="5" viewBox="0 0 8 5" class={cn('transition-colors', sortKey === col.key && sortDir === 'asc' ? 'text-[var(--foreground)]' : 'text-[var(--border)]')}>
+                      <path d="M0 5L4 0L8 5H0Z" fill="currentColor"/>
+                    </svg>
+                    <svg width="8" height="5" viewBox="0 0 8 5" class={cn('transition-colors', sortKey === col.key && sortDir === 'desc' ? 'text-[var(--foreground)]' : 'text-[var(--border)]')}>
+                      <path d="M0 0L4 5L8 0H0Z" fill="currentColor"/>
+                    </svg>
+                  </span>
+                {/if}
+              </span>
+            </th>
+          {/each}
+        </tr>
+      </thead>
+      <tbody>
+        {#if paged.length === 0}
+          <tr>
+            <td colspan={columns.length} class="px-4 py-12 text-center text-sm text-[var(--muted-foreground)]">{empty}</td>
+          </tr>
+        {:else}
+          {#each paged as row, i}
+            <tr class={cn('border-b border-[var(--border)] last:border-0 transition-colors duration-[100ms] hover:bg-[var(--muted)]/30', i % 2 === 1 && 'bg-[var(--muted)]/10')}>
+              {#each columns as col}
+                <td class="px-4 py-3 text-[var(--foreground)]">{@html cellValue(row, col)}</td>
+              {/each}
+            </tr>
+          {/each}
+        {/if}
+      </tbody>
+    </table>
+  </div>
+
+  {#if totalPages > 1}
+    <div class="flex items-center justify-between text-sm text-[var(--muted-foreground)]">
+      <span>{sorted.length} result{sorted.length !== 1 ? 's' : ''}</span>
+      <div class="flex items-center gap-1">
+        <button
+          type="button"
+          onclick={() => page = Math.max(0, page - 1)}
+          disabled={page === 0}
+          class={cn(
+            'flex h-7 w-7 cursor-pointer items-center justify-center rounded-[var(--radius-button)] border border-[var(--border)] transition-colors',
+            page === 0 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-[var(--muted)]'
+          )}
+        >
+          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+            <path d="M15 18l-6-6 6-6"/>
+          </svg>
+        </button>
+
+        {#each Array.from({ length: totalPages }, (_, i) => i) as p}
+          {#if totalPages <= 7 || p === 0 || p === totalPages - 1 || Math.abs(p - page) <= 1}
+            <button
+              type="button"
+              onclick={() => page = p}
+              class={cn(
+                'flex h-7 min-w-7 cursor-pointer items-center justify-center rounded-[var(--radius-button)] border px-1.5 text-xs transition-colors',
+                p === page
+                  ? 'border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)] font-medium'
+                  : 'border-[var(--border)] hover:bg-[var(--muted)] text-[var(--foreground)]'
+              )}
+            >{p + 1}</button>
+          {:else if Math.abs(p - page) === 2}
+            <span class="px-0.5">…</span>
+          {/if}
+        {/each}
+
+        <button
+          type="button"
+          onclick={() => page = Math.min(totalPages - 1, page + 1)}
+          disabled={page === totalPages - 1}
+          class={cn(
+            'flex h-7 w-7 cursor-pointer items-center justify-center rounded-[var(--radius-button)] border border-[var(--border)] transition-colors',
+            page === totalPages - 1 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-[var(--muted)]'
+          )}
+        >
+          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+            <path d="M9 18l6-6-6-6"/>
+          </svg>
+        </button>
+      </div>
+    </div>
+  {/if}
+</div>

+ 33 - 0
src/lib/components/data/EmptyState.svelte

@@ -0,0 +1,33 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    title: string;
+    description?: string;
+    class?: string;
+    icon?: Snippet;
+    actions?: Snippet;
+  }
+
+  let { title, description, class: className = '', icon, actions }: Props = $props();
+</script>
+
+<div class={cn('flex flex-col items-center justify-center gap-4 py-16 px-6 text-center', className)}>
+  {#if icon}
+    <div class="flex h-12 w-12 items-center justify-center rounded-[var(--radius)] bg-[var(--muted)] text-[var(--muted-foreground)]">
+      {@render icon()}
+    </div>
+  {/if}
+  <div class="flex flex-col gap-1.5 max-w-sm">
+    <h3 class="text-base font-semibold text-[var(--foreground)]">{title}</h3>
+    {#if description}
+      <p class="text-sm text-[var(--muted-foreground)]">{description}</p>
+    {/if}
+  </div>
+  {#if actions}
+    <div class="flex items-center gap-2 mt-2">
+      {@render actions()}
+    </div>
+  {/if}
+</div>

+ 106 - 0
src/lib/components/data/JsonViewer.svelte

@@ -0,0 +1,106 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import JsonViewer from './JsonViewer.svelte';
+
+  interface Props {
+    data: unknown;
+    depth?: number;
+    expandDepth?: number;
+    class?: string;
+  }
+
+  let {
+    data,
+    depth = 0,
+    expandDepth = 2,
+    class: className = '',
+  }: Props = $props();
+
+  type JsonType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
+
+  function getType(v: unknown): JsonType {
+    if (v === null) return 'null';
+    if (Array.isArray(v)) return 'array';
+    return typeof v as JsonType;
+  }
+
+  function isExpandable(v: unknown): boolean {
+    if (v === null) return false;
+    const t = typeof v;
+    return t === 'object';
+  }
+
+  function entryCount(v: unknown): number {
+    if (Array.isArray(v)) return v.length;
+    if (v && typeof v === 'object') return Object.keys(v as object).length;
+    return 0;
+  }
+
+  let collapsed = $state(depth >= expandDepth);
+
+  const type = getType(data);
+  const expandable = isExpandable(data);
+  const count = entryCount(data);
+
+  const openBracket = type === 'array' ? '[' : '{';
+  const closeBracket = type === 'array' ? ']' : '}';
+
+  let entries = $derived.by((): [string, unknown][] => {
+    if (type === 'array') return (data as unknown[]).map((v, i) => [String(i), v]);
+    if (type === 'object' && data !== null) return Object.entries(data as Record<string, unknown>);
+    return [];
+  });
+</script>
+
+<div class={cn(depth === 0 && 'font-mono text-xs leading-relaxed', className)}>
+  {#if expandable}
+    <span>
+      <button
+        type="button"
+        onclick={() => collapsed = !collapsed}
+        class="cursor-pointer text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors select-none"
+        aria-label={collapsed ? 'Expand' : 'Collapse'}
+      >
+        <svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor" class={cn('inline transition-transform duration-[100ms]', !collapsed && 'rotate-90')}>
+          <path d="M2 1.5L6 4L2 6.5V1.5Z"/>
+        </svg>
+      </button>
+      <span class="text-[var(--muted-foreground)]">{openBracket}</span>
+
+      {#if collapsed}
+        <button
+          type="button"
+          onclick={() => collapsed = false}
+          class="mx-1 cursor-pointer rounded px-1 text-[var(--muted-foreground)] hover:bg-[var(--muted)] transition-colors"
+        >
+          {count} {type === 'array' ? (count === 1 ? 'item' : 'items') : (count === 1 ? 'key' : 'keys')}
+        </button>
+        <span class="text-[var(--muted-foreground)]">{closeBracket}</span>
+      {:else}
+        <div class="ml-4 border-l border-[var(--border)] pl-3">
+          {#each entries as [key, val], i}
+            <div class="py-px">
+              {#if type === 'object'}
+                <span class="text-[var(--primary)]/80 font-medium">"{key}"</span>
+                <span class="text-[var(--muted-foreground)]">: </span>
+              {/if}
+              <JsonViewer data={val} depth={depth + 1} {expandDepth} />
+              {#if i < entries.length - 1}
+                <span class="text-[var(--muted-foreground)]">,</span>
+              {/if}
+            </div>
+          {/each}
+        </div>
+        <span class="text-[var(--muted-foreground)]">{closeBracket}</span>
+      {/if}
+    </span>
+  {:else if type === 'string'}
+    <span class="text-[hsl(160_60%_42%)] dark:text-[hsl(160_70%_58%)]">"{data as string}"</span>
+  {:else if type === 'number'}
+    <span class="text-[hsl(220_80%_55%)] dark:text-[hsl(220_80%_72%)]">{data as number}</span>
+  {:else if type === 'boolean'}
+    <span class="text-[hsl(35_90%_45%)] dark:text-[hsl(35_90%_62%)]">{String(data)}</span>
+  {:else}
+    <span class="text-[var(--muted-foreground)] italic">null</span>
+  {/if}
+</div>

+ 17 - 0
src/lib/components/data/KanbanBoard.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { class: className = '', children }: Props = $props();
+</script>
+
+<div class={cn('flex gap-4 overflow-x-auto pb-4', className)}>
+  {#if children}
+    {@render children()}
+  {/if}
+</div>

+ 79 - 0
src/lib/components/data/KanbanCard.svelte

@@ -0,0 +1,79 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    id: string;
+    title: string;
+    description?: string;
+    tags?: string[];
+    assignee?: string;
+    draggable?: boolean;
+    class?: string;
+    children?: Snippet;
+    ondragstart?: (id: string) => void;
+  }
+
+  let {
+    id,
+    title,
+    description,
+    tags = [],
+    assignee,
+    draggable = true,
+    class: className = '',
+    children,
+    ondragstart,
+  }: Props = $props();
+
+  let dragging = $state(false);
+
+  function handleDragStart(e: DragEvent) {
+    dragging = true;
+    e.dataTransfer?.setData('text/plain', id);
+    ondragstart?.(id);
+  }
+</script>
+
+<div
+  {draggable}
+  ondragstart={handleDragStart}
+  ondragend={() => { dragging = false; }}
+  class={cn(
+    'group rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] p-3',
+    'shadow-[var(--shadow-sm)] transition-all duration-[150ms]',
+    draggable && 'cursor-grab active:cursor-grabbing',
+    dragging && 'opacity-40 scale-[0.98]',
+    'hover:shadow-[var(--shadow-md)] hover:border-[var(--primary)]/30',
+    className
+  )}
+>
+  <p class="text-sm font-medium text-[var(--foreground)] leading-snug">{title}</p>
+
+  {#if description}
+    <p class="mt-1 text-xs text-[var(--muted-foreground)] leading-relaxed line-clamp-2">{description}</p>
+  {/if}
+
+  {#if children}
+    <div class="mt-2">
+      {@render children()}
+    </div>
+  {/if}
+
+  {#if tags.length > 0 || assignee}
+    <div class="mt-2.5 flex items-center justify-between gap-2">
+      <div class="flex flex-wrap gap-1">
+        {#each tags as tag}
+          <span class="rounded-full bg-[var(--muted)] px-2 py-0.5 text-[10px] font-medium text-[var(--muted-foreground)]">
+            {tag}
+          </span>
+        {/each}
+      </div>
+      {#if assignee}
+        <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--primary)]/20 text-[10px] font-semibold text-[var(--primary)] uppercase">
+          {assignee.charAt(0)}
+        </span>
+      {/if}
+    </div>
+  {/if}
+</div>

+ 68 - 0
src/lib/components/data/KanbanColumn.svelte

@@ -0,0 +1,68 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import type { Snippet } from 'svelte';
+
+  interface Props {
+    id: string;
+    title: string;
+    count?: number;
+    class?: string;
+    children?: Snippet;
+    ondrop?: (cardId: string, columnId: string) => void;
+  }
+
+  let {
+    id,
+    title,
+    count,
+    class: className = '',
+    children,
+    ondrop,
+  }: Props = $props();
+
+  let over = $state(false);
+
+  function handleDragOver(e: DragEvent) {
+    e.preventDefault();
+    over = true;
+  }
+
+  function handleDrop(e: DragEvent) {
+    e.preventDefault();
+    over = false;
+    const cardId = e.dataTransfer?.getData('text/plain');
+    if (cardId) ondrop?.(cardId, id);
+  }
+</script>
+
+<div
+  ondragover={handleDragOver}
+  ondragleave={() => { over = false; }}
+  ondrop={handleDrop}
+  class={cn(
+    'flex w-72 shrink-0 flex-col rounded-[var(--radius)] border bg-[var(--muted)]/30 transition-colors duration-[150ms]',
+    over ? 'border-[var(--primary)]/60 bg-[var(--primary)]/5' : 'border-[var(--border)]',
+    className
+  )}
+>
+  <!-- Column header -->
+  <div class="flex items-center justify-between px-3.5 py-3 border-b border-[var(--border)]">
+    <h3 class="text-sm font-semibold text-[var(--foreground)]">{title}</h3>
+    {#if count !== undefined}
+      <span class="flex h-5 min-w-5 items-center justify-center rounded-full bg-[var(--muted)] px-1.5 text-xs font-medium text-[var(--muted-foreground)]">
+        {count}
+      </span>
+    {/if}
+  </div>
+
+  <!-- Cards -->
+  <div class="flex flex-1 flex-col gap-2 overflow-y-auto p-2.5 min-h-[120px]">
+    {#if children}
+      {@render children()}
+    {/if}
+
+    {#if over}
+      <div class="rounded-[var(--radius)] border-2 border-dashed border-[var(--primary)]/40 h-14 transition-all duration-[150ms]"></div>
+    {/if}
+  </div>
+</div>

+ 60 - 0
src/lib/components/data/KeyValue.svelte

@@ -0,0 +1,60 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Item {
+    key: string;
+    value: string;
+    mono?: boolean;
+    copyable?: boolean;
+  }
+
+  interface Props {
+    items: Item[];
+    class?: string;
+  }
+
+  let { items, class: className = '' }: Props = $props();
+
+  let copied = $state<string | null>(null);
+
+  async function copy(value: string, key: string) {
+    await navigator.clipboard.writeText(value);
+    copied = key;
+    setTimeout(() => { copied = null; }, 2000);
+  }
+</script>
+
+<dl class={cn('flex flex-col divide-y divide-[var(--border)]', className)}>
+  {#each items as item}
+    <div class="flex items-center justify-between gap-4 py-2.5 first:pt-0 last:pb-0">
+      <dt class="text-sm text-[var(--muted-foreground)] shrink-0">{item.key}</dt>
+      <dd class={cn(
+        'text-sm text-[var(--foreground)] flex items-center gap-1.5 min-w-0',
+        item.mono && 'font-mono text-xs'
+      )}>
+        <span class="truncate">{item.value}</span>
+        {#if item.copyable}
+          <button
+            type="button"
+            onclick={() => copy(item.value, item.key)}
+            aria-label="Copy {item.key}"
+            class={cn(
+              'cursor-pointer shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)]',
+              'transition-colors duration-[150ms] focus-visible:outline-none'
+            )}
+          >
+            {#if copied === item.key}
+              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
+                <path d="M20 6L9 17l-5-5"/>
+              </svg>
+            {:else}
+              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+                <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
+              </svg>
+            {/if}
+          </button>
+        {/if}
+      </dd>
+    </div>
+  {/each}
+</dl>

+ 65 - 0
src/lib/components/data/MetricCard.svelte

@@ -0,0 +1,65 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Color = 'olive' | 'emerald' | 'amber' | 'red' | 'sky' | 'purple';
+
+  interface Props {
+    label: string;
+    value: string | number;
+    color?: Color;
+    class?: string;
+    icon?: Snippet;
+    trend?: { value: number; label?: string };
+  }
+
+  let { label, value, color = 'olive', class: className = '', icon, trend }: Props = $props();
+
+  const iconBg: Record<Color, string> = {
+    olive:   'bg-[var(--primary)]/10',
+    emerald: 'bg-emerald-500/10',
+    amber:   'bg-amber-500/10',
+    red:     'bg-red-500/10',
+    sky:     'bg-sky-500/10',
+    purple:  'bg-purple-500/10',
+  };
+
+  const iconColor: Record<Color, string> = {
+    olive:   'text-[var(--primary)]',
+    emerald: 'text-emerald-600 dark:text-emerald-400',
+    amber:   'text-amber-600 dark:text-amber-400',
+    red:     'text-red-600 dark:text-red-400',
+    sky:     'text-sky-600 dark:text-sky-400',
+    purple:  'text-purple-600 dark:text-purple-400',
+  };
+</script>
+
+<div
+  class={cn(
+    'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+    'shadow-[var(--shadow-sm)] p-4 flex items-center gap-3',
+    className
+  )}
+>
+  {#if icon}
+    <div class={cn('shrink-0 rounded-[calc(var(--radius)*0.67)] p-2', iconBg[color])}>
+      <span class={iconColor[color]}>
+        {@render icon()}
+      </span>
+    </div>
+  {/if}
+
+  <div class="flex-1 min-w-0">
+    <p class="text-xs text-[var(--muted-foreground)] truncate">{label}</p>
+    <p class="text-lg font-semibold text-[var(--foreground)] leading-tight">{value}</p>
+    {#if trend}
+      <p class={cn(
+        'text-xs mt-0.5',
+        trend.value > 0 ? 'text-emerald-600 dark:text-emerald-400' : trend.value < 0 ? 'text-red-600 dark:text-red-400' : 'text-[var(--muted-foreground)]'
+      )}>
+        {trend.value > 0 ? '↑' : trend.value < 0 ? '↓' : '–'}
+        {trend.label ?? `${Math.abs(trend.value)}%`}
+      </p>
+    {/if}
+  </div>
+</div>

+ 86 - 0
src/lib/components/data/Table.svelte

@@ -0,0 +1,86 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Column<T> {
+    key: keyof T | string;
+    label: string;
+    width?: string;
+    align?: 'left' | 'right' | 'center';
+    mono?: boolean;
+    render?: (row: T) => Snippet | string;
+  }
+
+  interface Props<T extends Record<string, unknown>> {
+    columns: Column<T>[];
+    rows: T[];
+    class?: string;
+    rowKey?: keyof T;
+    onrowclick?: (row: T) => void;
+  }
+
+  let {
+    columns,
+    rows,
+    class: className = '',
+    rowKey,
+    onrowclick,
+  }: Props<Record<string, unknown>> = $props();
+
+  function getCellValue(row: Record<string, unknown>, key: string): string {
+    const val = row[key];
+    if (val === null || val === undefined) return '—';
+    return String(val);
+  }
+</script>
+
+<div class={cn('w-full overflow-x-auto rounded-[var(--radius)] border border-[var(--border)]', className)}>
+  <table class="w-full text-sm border-collapse">
+    <thead>
+      <tr class="border-b border-[var(--border)] bg-[var(--muted)]/50">
+        {#each columns as col}
+          <th
+            class={cn(
+              'px-4 py-2.5 text-xs font-medium text-[var(--muted-foreground)] uppercase tracking-wide',
+              col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left',
+              col.width && `w-[${col.width}]`
+            )}
+          >{col.label}</th>
+        {/each}
+      </tr>
+    </thead>
+    <tbody>
+      {#each rows as row, i (rowKey ? row[rowKey as string] : i)}
+        <tr
+          class={cn(
+            'border-b border-[var(--border)] last:border-0',
+            'transition-colors duration-[100ms]',
+            onrowclick ? 'cursor-pointer hover:bg-[var(--muted)]/50' : 'hover:bg-[var(--muted)]/30',
+          )}
+          onclick={() => onrowclick?.(row)}
+        >
+          {#each columns as col}
+            <td
+              class={cn(
+                'px-4 py-3 text-[var(--foreground)]',
+                col.mono && 'font-mono text-xs',
+                col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left',
+                'max-w-[280px] truncate'
+              )}
+            >
+              {getCellValue(row, col.key as string)}
+            </td>
+          {/each}
+        </tr>
+      {/each}
+
+      {#if rows.length === 0}
+        <tr>
+          <td colspan={columns.length} class="px-4 py-10 text-center text-sm text-[var(--muted-foreground)]">
+            No data
+          </td>
+        </tr>
+      {/if}
+    </tbody>
+  </table>
+</div>

+ 149 - 0
src/lib/components/forms/Combobox.svelte

@@ -0,0 +1,149 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Option {
+    value: string;
+    label: string;
+    description?: string;
+  }
+
+  interface Props {
+    options: Option[];
+    value?: string;
+    placeholder?: string;
+    disabled?: boolean;
+    error?: boolean;
+    class?: string;
+    onchange?: (value: string) => void;
+  }
+
+  let {
+    options,
+    value = $bindable(''),
+    placeholder = 'Select…',
+    disabled = false,
+    error = false,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  let open = $state(false);
+  let query = $state('');
+  let activeIndex = $state(0);
+  let containerEl = $state<HTMLElement | null>(null);
+  let inputEl = $state<HTMLInputElement | null>(null);
+
+  let selected = $derived(options.find(o => o.value === value));
+
+  let filtered = $derived(
+    query
+      ? options.filter(o => o.label.toLowerCase().includes(query.toLowerCase()))
+      : options
+  );
+
+  function select(opt: Option) {
+    value = opt.value;
+    query = '';
+    open = false;
+    onchange?.(opt.value);
+  }
+
+  function handleInputKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') { open = false; return; }
+    if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = Math.min(activeIndex + 1, filtered.length - 1); }
+    if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); }
+    if (e.key === 'Enter') { e.preventDefault(); if (filtered[activeIndex]) select(filtered[activeIndex]); }
+  }
+
+  function handleOutside(e: MouseEvent) {
+    if (containerEl && !containerEl.contains(e.target as Node)) { open = false; query = ''; }
+  }
+
+  $effect(() => {
+    if (open) {
+      activeIndex = 0;
+      document.addEventListener('click', handleOutside);
+      return () => document.removeEventListener('click', handleOutside);
+    }
+  });
+</script>
+
+<div class={cn('relative', className)} bind:this={containerEl}>
+  <div
+    class={cn(
+      'flex h-9 w-full items-center rounded-[var(--radius)] border bg-transparent',
+      'text-sm transition-colors duration-[150ms]',
+      error ? 'border-[var(--destructive)]' : 'border-[var(--border)]',
+      open && 'ring-2 ring-[var(--ring)]',
+      disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'
+    )}
+  >
+    <input
+      bind:this={inputEl}
+      type="text"
+      {disabled}
+      placeholder={selected ? selected.label : placeholder}
+      bind:value={query}
+      onfocus={() => { open = true; }}
+      onkeydown={handleInputKeydown}
+      class={cn(
+        'flex-1 bg-transparent px-3 py-1 text-sm focus:outline-none',
+        selected && !query ? 'text-[var(--foreground)]' : 'text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]',
+        disabled && 'cursor-not-allowed'
+      )}
+    />
+    <button
+      type="button"
+      {disabled}
+      tabindex="-1"
+      onclick={() => { open = !open; if (open) inputEl?.focus(); }}
+      class="flex h-full cursor-pointer items-center px-2.5 text-[var(--muted-foreground)]"
+    >
+      <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
+        <path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+      </svg>
+    </button>
+  </div>
+
+  {#if open}
+    <div
+      class={cn(
+        'absolute top-full z-50 mt-1 w-full',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-md)] py-1',
+        'animate-[popoverIn_150ms_cubic-bezier(0.16,1,0.3,1)] max-h-56 overflow-y-auto'
+      )}
+    >
+      {#if filtered.length === 0}
+        <p class="px-3 py-6 text-center text-sm text-[var(--muted-foreground)]">No results</p>
+      {:else}
+        {#each filtered as opt, i}
+          <button
+            type="button"
+            onclick={() => select(opt)}
+            onmouseenter={() => { activeIndex = i; }}
+            class={cn(
+              'flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors duration-[100ms]',
+              i === activeIndex ? 'bg-[var(--muted)]' : 'hover:bg-[var(--muted)]/60',
+              opt.value === value ? 'text-[var(--primary)] font-medium' : 'text-[var(--foreground)]'
+            )}
+          >
+            <span class="flex-1 text-left truncate">{opt.label}</span>
+            {#if opt.value === value}
+              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
+                <path d="M20 6L9 17l-5-5"/>
+              </svg>
+            {/if}
+          </button>
+        {/each}
+      {/if}
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes popoverIn {
+    from { opacity: 0; transform: scale(0.97) translateY(-4px); }
+    to   { opacity: 1; transform: scale(1) translateY(0); }
+  }
+</style>

+ 212 - 0
src/lib/components/forms/DatePicker.svelte

@@ -0,0 +1,212 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    value?: Date | null;
+    placeholder?: string;
+    disabled?: boolean;
+    error?: boolean;
+    min?: Date;
+    max?: Date;
+    class?: string;
+    onchange?: (date: Date | null) => void;
+  }
+
+  let {
+    value = $bindable(null),
+    placeholder = 'Pick a date…',
+    disabled = false,
+    error = false,
+    min,
+    max,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  let open = $state(false);
+  let containerEl = $state<HTMLElement | null>(null);
+
+  const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
+  const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+
+  let cursor = $state(() => {
+    const d = value ?? new Date();
+    return { year: d.getFullYear(), month: d.getMonth() };
+  });
+
+  let viewYear = $state(cursor().year);
+  let viewMonth = $state(cursor().month);
+
+  function daysInMonth(y: number, m: number) { return new Date(y, m + 1, 0).getDate(); }
+  function firstWeekday(y: number, m: number) { return new Date(y, m, 1).getDay(); }
+
+  let cells = $derived.by(() => {
+    const total = daysInMonth(viewYear, viewMonth);
+    const offset = firstWeekday(viewYear, viewMonth);
+    return Array.from({ length: offset + total }, (_, i) => i < offset ? null : i - offset + 1);
+  });
+
+  function isSelected(day: number) {
+    if (!value) return false;
+    return value.getFullYear() === viewYear && value.getMonth() === viewMonth && value.getDate() === day;
+  }
+
+  function isToday(day: number) {
+    const t = new Date();
+    return t.getFullYear() === viewYear && t.getMonth() === viewMonth && t.getDate() === day;
+  }
+
+  function isDisabled(day: number) {
+    const d = new Date(viewYear, viewMonth, day);
+    if (min && d < new Date(min.getFullYear(), min.getMonth(), min.getDate())) return true;
+    if (max && d > new Date(max.getFullYear(), max.getMonth(), max.getDate())) return true;
+    return false;
+  }
+
+  function selectDay(day: number) {
+    if (isDisabled(day)) return;
+    value = new Date(viewYear, viewMonth, day);
+    open = false;
+    onchange?.(value);
+  }
+
+  function prevMonth() {
+    if (viewMonth === 0) { viewMonth = 11; viewYear--; }
+    else viewMonth--;
+  }
+
+  function nextMonth() {
+    if (viewMonth === 11) { viewMonth = 0; viewYear++; }
+    else viewMonth++;
+  }
+
+  function handleOutside(e: MouseEvent) {
+    if (containerEl && !containerEl.contains(e.target as Node)) { open = false; }
+  }
+
+  $effect(() => {
+    if (open) {
+      document.addEventListener('click', handleOutside);
+      return () => document.removeEventListener('click', handleOutside);
+    }
+  });
+
+  let displayValue = $derived(
+    value
+      ? value.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
+      : ''
+  );
+</script>
+
+<div class={cn('relative', className)} bind:this={containerEl}>
+  <button
+    type="button"
+    {disabled}
+    onclick={() => { if (!disabled) open = !open; }}
+    class={cn(
+      'flex h-9 w-full items-center gap-2 rounded-[var(--radius)] border bg-transparent px-3 text-sm',
+      'transition-colors duration-[150ms]',
+      error ? 'border-[var(--destructive)]' : 'border-[var(--border)]',
+      open && 'ring-2 ring-[var(--ring)]',
+      disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer',
+      displayValue ? 'text-[var(--foreground)]' : 'text-[var(--muted-foreground)]'
+    )}
+  >
+    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="shrink-0 text-[var(--muted-foreground)]">
+      <rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/>
+    </svg>
+    <span class="flex-1 text-left">{displayValue || placeholder}</span>
+  </button>
+  {#if value}
+    <button
+      type="button"
+      onclick={(e) => { e.stopPropagation(); value = null; onchange?.(null); }}
+      class="absolute right-2.5 top-1/2 -translate-y-1/2 flex h-4 w-4 cursor-pointer items-center justify-center rounded-full text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
+      aria-label="Clear date"
+    >
+      <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
+        <path d="M18 6L6 18M6 6l12 12"/>
+      </svg>
+    </button>
+  {/if}
+
+  {#if open}
+    <div
+      class={cn(
+        'absolute top-full z-50 mt-1',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-md)] p-3 w-[280px]',
+        'animate-[popoverIn_150ms_cubic-bezier(0.16,1,0.3,1)]'
+      )}
+    >
+      <!-- Header -->
+      <div class="mb-3 flex items-center justify-between">
+        <button
+          type="button"
+          onclick={prevMonth}
+          class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-[var(--radius-button)] text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
+          aria-label="Previous month"
+        >
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+            <path d="M15 18l-6-6 6-6"/>
+          </svg>
+        </button>
+        <span class="text-sm font-medium text-[var(--foreground)]">{MONTHS[viewMonth]} {viewYear}</span>
+        <button
+          type="button"
+          onclick={nextMonth}
+          class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-[var(--radius-button)] text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
+          aria-label="Next month"
+        >
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+            <path d="M9 18l6-6-6-6"/>
+          </svg>
+        </button>
+      </div>
+
+      <!-- Day names -->
+      <div class="mb-1 grid grid-cols-7">
+        {#each DAYS as d}
+          <div class="flex h-7 items-center justify-center text-xs font-medium text-[var(--muted-foreground)]">{d}</div>
+        {/each}
+      </div>
+
+      <!-- Day cells -->
+      <div class="grid grid-cols-7 gap-y-0.5">
+        {#each cells as day}
+          {#if day === null}
+            <div></div>
+          {:else}
+            {@const sel = isSelected(day)}
+            {@const tod = isToday(day)}
+            {@const dis = isDisabled(day)}
+            <button
+              type="button"
+              onclick={() => selectDay(day)}
+              disabled={dis}
+              class={cn(
+                'flex h-8 w-full items-center justify-center rounded-[var(--radius-button)] text-sm transition-colors duration-[100ms]',
+                sel
+                  ? 'bg-[var(--primary)] text-[var(--primary-foreground)] font-medium'
+                  : tod
+                    ? 'border border-[var(--primary)] text-[var(--primary)] font-medium hover:bg-[var(--primary)]/10'
+                    : 'text-[var(--foreground)] hover:bg-[var(--muted)]',
+                dis && 'opacity-30 cursor-not-allowed',
+                !dis && !sel && 'cursor-pointer'
+              )}
+            >
+              {day}
+            </button>
+          {/if}
+        {/each}
+      </div>
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes popoverIn {
+    from { opacity: 0; transform: scale(0.97) translateY(-4px); }
+    to   { opacity: 1; transform: scale(1) translateY(0); }
+  }
+</style>

+ 105 - 0
src/lib/components/forms/FileUpload.svelte

@@ -0,0 +1,105 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    accept?: string;
+    multiple?: boolean;
+    disabled?: boolean;
+    maxSize?: number;
+    class?: string;
+    onfiles?: (files: File[]) => void;
+  }
+
+  let {
+    accept,
+    multiple = false,
+    disabled = false,
+    maxSize,
+    class: className = '',
+    onfiles,
+  }: Props = $props();
+
+  let dragging = $state(false);
+  let inputEl = $state<HTMLInputElement | null>(null);
+  let error = $state('');
+
+  function processFiles(fileList: FileList | null) {
+    if (!fileList) return;
+    const files = Array.from(fileList);
+    if (maxSize) {
+      const oversized = files.find(f => f.size > maxSize);
+      if (oversized) {
+        error = `${oversized.name} exceeds the ${(maxSize / 1024 / 1024).toFixed(0)} MB limit.`;
+        return;
+      }
+    }
+    error = '';
+    onfiles?.(files);
+  }
+
+  function handleDrop(e: DragEvent) {
+    e.preventDefault();
+    dragging = false;
+    if (!disabled) processFiles(e.dataTransfer?.files ?? null);
+  }
+
+  function handleDragOver(e: DragEvent) {
+    e.preventDefault();
+    if (!disabled) dragging = true;
+  }
+</script>
+
+<div
+  role="button"
+  tabindex={disabled ? -1 : 0}
+  aria-label="Upload files"
+  class={cn(
+    'relative flex flex-col items-center justify-center gap-3',
+    'rounded-[var(--radius)] border-2 border-dashed',
+    'px-6 py-10 text-center transition-colors duration-[150ms]',
+    dragging
+      ? 'border-[var(--primary)] bg-[var(--primary)]/5'
+      : 'border-[var(--border)] bg-transparent hover:border-[var(--primary)]/50 hover:bg-[var(--muted)]/40',
+    disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer',
+    className
+  )}
+  ondrop={handleDrop}
+  ondragover={handleDragOver}
+  ondragleave={() => { dragging = false; }}
+  onclick={() => !disabled && inputEl?.click()}
+  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') inputEl?.click(); }}
+>
+  <input
+    bind:this={inputEl}
+    type="file"
+    {accept}
+    {multiple}
+    {disabled}
+    class="sr-only"
+    onchange={(e) => processFiles((e.target as HTMLInputElement).files)}
+  />
+
+  <div class="flex h-10 w-10 items-center justify-center rounded-[var(--radius)] bg-[var(--muted)] text-[var(--muted-foreground)]">
+    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
+      <polyline points="17 8 12 3 7 8"/>
+      <line x1="12" y1="3" x2="12" y2="15"/>
+    </svg>
+  </div>
+
+  <div>
+    <p class="text-sm font-medium text-[var(--foreground)]">
+      {dragging ? 'Drop to upload' : 'Drop files here or click to browse'}
+    </p>
+    {#if accept}
+      <p class="mt-1 text-xs text-[var(--muted-foreground)]">{accept.split(',').join(', ')}</p>
+    {/if}
+    {#if maxSize}
+      <p class="text-xs text-[var(--muted-foreground)]">Max {(maxSize / 1024 / 1024).toFixed(0)} MB</p>
+    {/if}
+  </div>
+
+  {#if error}
+    <p class="text-xs text-[var(--destructive)]">{error}</p>
+  {/if}
+</div>

+ 30 - 0
src/lib/components/forms/FormField.svelte

@@ -0,0 +1,30 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import { setContext } from 'svelte';
+
+  interface Props {
+    name?: string;
+    error?: string;
+    hint?: string;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { name = '', error = '', hint = '', class: className = '', children }: Props = $props();
+
+  setContext('form-field', {
+    get name() { return name; },
+    get error() { return error; },
+    get hint() { return hint; },
+  });
+</script>
+
+<div class={cn('flex flex-col gap-1.5', className)}>
+  {#if children}{@render children()}{/if}
+  {#if error}
+    <p class="text-xs text-[var(--destructive)]">{error}</p>
+  {:else if hint}
+    <p class="text-xs text-[var(--muted-foreground)]">{hint}</p>
+  {/if}
+</div>

+ 200 - 0
src/lib/components/forms/MultiSelect.svelte

@@ -0,0 +1,200 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Option {
+    value: string;
+    label: string;
+  }
+
+  interface Props {
+    options: Option[];
+    value?: string[];
+    placeholder?: string;
+    disabled?: boolean;
+    error?: boolean;
+    max?: number;
+    class?: string;
+    onchange?: (values: string[]) => void;
+  }
+
+  let {
+    options,
+    value = $bindable([]),
+    placeholder = 'Select…',
+    disabled = false,
+    error = false,
+    max,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  let open = $state(false);
+  let query = $state('');
+  let activeIndex = $state(0);
+  let containerEl = $state<HTMLElement | null>(null);
+  let inputEl = $state<HTMLInputElement | null>(null);
+
+  let filtered = $derived(
+    query
+      ? options.filter(o => o.label.toLowerCase().includes(query.toLowerCase()))
+      : options
+  );
+
+  let selectedOptions = $derived(options.filter(o => value.includes(o.value)));
+
+  function toggle(opt: Option) {
+    if (value.includes(opt.value)) {
+      value = value.filter(v => v !== opt.value);
+    } else {
+      if (max && value.length >= max) return;
+      value = [...value, opt.value];
+    }
+    query = '';
+    onchange?.(value);
+  }
+
+  function removeTag(v: string) {
+    value = value.filter(x => x !== v);
+    onchange?.(value);
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') { open = false; return; }
+    if (e.key === 'Backspace' && !query && value.length > 0) {
+      value = value.slice(0, -1);
+      onchange?.(value);
+      return;
+    }
+    if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = Math.min(activeIndex + 1, filtered.length - 1); }
+    if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); }
+    if (e.key === 'Enter') { e.preventDefault(); if (filtered[activeIndex]) toggle(filtered[activeIndex]); }
+  }
+
+  function handleOutside(e: MouseEvent) {
+    if (containerEl && !containerEl.contains(e.target as Node)) { open = false; query = ''; }
+  }
+
+  $effect(() => {
+    if (open) {
+      activeIndex = 0;
+      document.addEventListener('click', handleOutside);
+      return () => document.removeEventListener('click', handleOutside);
+    }
+  });
+</script>
+
+<div class={cn('relative', className)} bind:this={containerEl}>
+  <div
+    class={cn(
+      'flex min-h-9 w-full flex-wrap items-center gap-1.5 rounded-[var(--radius)] border bg-transparent px-2 py-1.5',
+      'text-sm transition-colors duration-[150ms]',
+      error ? 'border-[var(--destructive)]' : 'border-[var(--border)]',
+      open && 'ring-2 ring-[var(--ring)]',
+      disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-text'
+    )}
+    onclick={() => { if (!disabled) inputEl?.focus(); }}
+    role="none"
+  >
+    {#each selectedOptions as opt}
+      <span class="inline-flex items-center gap-1 rounded-[calc(var(--radius)-4px)] bg-[var(--muted)] px-2 py-0.5 text-xs font-medium text-[var(--foreground)]">
+        {opt.label}
+        <button
+          type="button"
+          onclick={(e) => { e.stopPropagation(); removeTag(opt.value); }}
+          class="flex h-3.5 w-3.5 cursor-pointer items-center justify-center rounded-full text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
+          aria-label="Remove {opt.label}"
+        >
+          <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
+            <path d="M18 6L6 18M6 6l12 12"/>
+          </svg>
+        </button>
+      </span>
+    {/each}
+
+    <input
+      bind:this={inputEl}
+      type="text"
+      {disabled}
+      placeholder={value.length === 0 ? placeholder : ''}
+      bind:value={query}
+      onfocus={() => { open = true; }}
+      onkeydown={handleKeydown}
+      class={cn(
+        'flex-1 min-w-[80px] bg-transparent text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:outline-none',
+        disabled && 'cursor-not-allowed'
+      )}
+    />
+
+    <button
+      type="button"
+      {disabled}
+      tabindex="-1"
+      onclick={() => { open = !open; if (open) inputEl?.focus(); }}
+      class="ml-auto flex cursor-pointer items-center pl-1 text-[var(--muted-foreground)]"
+    >
+      <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
+        <path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+      </svg>
+    </button>
+  </div>
+
+  {#if open}
+    <div
+      class={cn(
+        'absolute top-full z-50 mt-1 w-full',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-md)] py-1',
+        'animate-[popoverIn_150ms_cubic-bezier(0.16,1,0.3,1)] max-h-56 overflow-y-auto'
+      )}
+    >
+      {#if filtered.length === 0}
+        <p class="px-3 py-6 text-center text-sm text-[var(--muted-foreground)]">No results</p>
+      {:else}
+        {#each filtered as opt, i}
+          {@const selected = value.includes(opt.value)}
+          {@const atMax = !selected && !!max && value.length >= max}
+          <button
+            type="button"
+            onclick={() => toggle(opt)}
+            onmouseenter={() => { activeIndex = i; }}
+            disabled={atMax}
+            class={cn(
+              'flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors duration-[100ms]',
+              i === activeIndex ? 'bg-[var(--muted)]' : 'hover:bg-[var(--muted)]/60',
+              selected ? 'text-[var(--primary)] font-medium' : 'text-[var(--foreground)]',
+              atMax && 'opacity-40 cursor-not-allowed'
+            )}
+          >
+            <span
+              class={cn(
+                'flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-[3px] border transition-colors duration-[100ms]',
+                selected
+                  ? 'border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)]'
+                  : 'border-[var(--border)]'
+              )}
+            >
+              {#if selected}
+                <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M20 6L9 17l-5-5"/>
+                </svg>
+              {/if}
+            </span>
+            <span class="flex-1 text-left truncate">{opt.label}</span>
+          </button>
+        {/each}
+      {/if}
+      {#if max}
+        <p class="border-t border-[var(--border)] px-3 py-1.5 text-xs text-[var(--muted-foreground)]">
+          {value.length} / {max} selected
+        </p>
+      {/if}
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes popoverIn {
+    from { opacity: 0; transform: scale(0.97) translateY(-4px); }
+    to   { opacity: 1; transform: scale(1) translateY(0); }
+  }
+</style>

+ 73 - 0
src/lib/components/forms/SearchInput.svelte

@@ -0,0 +1,73 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    value?: string;
+    placeholder?: string;
+    debounce?: number;
+    disabled?: boolean;
+    class?: string;
+    oninput?: (value: string) => void;
+  }
+
+  let {
+    value = $bindable(''),
+    placeholder = 'Search…',
+    debounce = 200,
+    disabled = false,
+    class: className = '',
+    oninput,
+  }: Props = $props();
+
+  let timer: ReturnType<typeof setTimeout>;
+
+  function handleInput(e: Event) {
+    const v = (e.target as HTMLInputElement).value;
+    value = v;
+    clearTimeout(timer);
+    timer = setTimeout(() => oninput?.(v), debounce);
+  }
+
+  function clear() {
+    value = '';
+    oninput?.('');
+  }
+</script>
+
+<div class={cn('relative flex items-center', className)}>
+  <svg
+    class="pointer-events-none absolute left-3 h-4 w-4 text-[var(--muted-foreground)]"
+    viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+  >
+    <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
+  </svg>
+
+  <input
+    type="search"
+    {placeholder}
+    {disabled}
+    bind:value
+    oninput={handleInput}
+    class={cn(
+      'flex h-9 w-full rounded-[var(--radius)] border border-[var(--border)] bg-transparent',
+      'pl-9 pr-8 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]',
+      'transition-colors duration-[150ms]',
+      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+      'disabled:cursor-not-allowed disabled:opacity-40',
+      '[&::-webkit-search-cancel-button]:hidden'
+    )}
+  />
+
+  {#if value}
+    <button
+      type="button"
+      onclick={clear}
+      aria-label="Clear search"
+      class="absolute right-2.5 flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[var(--muted-foreground)]/40 text-[var(--background)] hover:bg-[var(--muted-foreground)]/60 transition-colors duration-[150ms]"
+    >
+      <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
+        <path d="M18 6L6 18M6 6l12 12"/>
+      </svg>
+    </button>
+  {/if}
+</div>

+ 58 - 0
src/lib/components/layout/Breadcrumbs.svelte

@@ -0,0 +1,58 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Crumb {
+    label: string;
+    href?: string;
+  }
+
+  interface Props {
+    items: Crumb[];
+    class?: string;
+  }
+
+  let { items, class: className = '' }: Props = $props();
+
+  const textBase = 'text-sm leading-none';
+</script>
+
+<nav aria-label="Breadcrumb" class={cn('flex items-baseline gap-0', className)}>
+  {#each items as crumb, i}
+    {#if i > 0}
+      <svg
+        aria-hidden="true"
+        width="16"
+        height="16"
+        viewBox="0 0 16 16"
+        fill="none"
+        class="shrink-0 self-center text-[var(--muted-foreground)] opacity-30"
+      >
+        <path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+      </svg>
+    {/if}
+
+    {#if crumb.href && i < items.length - 1}
+      <a
+        href={crumb.href}
+        class={cn(
+          textBase,
+          'px-1 rounded cursor-pointer',
+          'text-[var(--muted-foreground)] hover:text-[var(--foreground)]',
+          'transition-colors duration-[150ms]',
+          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'
+        )}
+      >{crumb.label}</a>
+    {:else}
+      <span
+        aria-current={i === items.length - 1 ? 'page' : undefined}
+        class={cn(
+          textBase,
+          'px-1',
+          i === items.length - 1
+            ? 'text-[var(--foreground)] font-medium'
+            : 'text-[var(--muted-foreground)]'
+        )}
+      >{crumb.label}</span>
+    {/if}
+  {/each}
+</nav>

+ 21 - 0
src/lib/components/layout/Card.svelte

@@ -0,0 +1,21 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { class: className = '', children }: Props = $props();
+</script>
+
+<div
+  class={cn(
+    'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)]',
+    'shadow-[var(--shadow-sm)]',
+    className
+  )}
+>
+  {#if children}{@render children()}{/if}
+</div>

+ 9 - 0
src/lib/components/layout/CardContent.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<div class={cn('px-5 py-4', className)}>
+  {#if children}{@render children()}{/if}
+</div>

+ 9 - 0
src/lib/components/layout/CardDescription.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<p class={cn('text-sm text-[var(--muted-foreground)]', className)}>
+  {#if children}{@render children()}{/if}
+</p>

+ 9 - 0
src/lib/components/layout/CardFooter.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<div class={cn('flex items-center px-5 pb-5 pt-0', className)}>
+  {#if children}{@render children()}{/if}
+</div>

+ 9 - 0
src/lib/components/layout/CardHeader.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<div class={cn('flex flex-col gap-1.5 px-5 pt-5', className)}>
+  {#if children}{@render children()}{/if}
+</div>

+ 9 - 0
src/lib/components/layout/CardTitle.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<h3 class={cn('text-base font-semibold leading-tight text-[var(--card-foreground)]', className)}>
+  {#if children}{@render children()}{/if}
+</h3>

+ 11 - 0
src/lib/components/layout/Page.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  interface Props { class?: string; children?: Snippet; }
+  let { class: className = '', children }: Props = $props();
+</script>
+<div class={cn('min-h-dvh bg-[var(--background)]', className)}>
+  <div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
+    {#if children}{@render children()}{/if}
+  </div>
+</div>

+ 27 - 0
src/lib/components/layout/PageHeader.svelte

@@ -0,0 +1,27 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    title: string;
+    description?: string;
+    class?: string;
+    actions?: Snippet;
+  }
+
+  let { title, description, class: className = '', actions }: Props = $props();
+</script>
+
+<div class={cn('flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between mb-8', className)}>
+  <div class="flex flex-col gap-1">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)] leading-tight">{title}</h1>
+    {#if description}
+      <p class="text-sm text-[var(--muted-foreground)]">{description}</p>
+    {/if}
+  </div>
+  {#if actions}
+    <div class="flex items-center gap-2 shrink-0">
+      {@render actions()}
+    </div>
+  {/if}
+</div>

+ 19 - 0
src/lib/components/layout/ScrollArea.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    class?: string;
+    maxHeight?: string;
+    children?: Snippet;
+  }
+
+  let { class: className = '', maxHeight, children }: Props = $props();
+</script>
+
+<div
+  class={cn('overflow-auto', className)}
+  style:max-height={maxHeight}
+>
+  {#if children}{@render children()}{/if}
+</div>

+ 82 - 0
src/lib/components/layout/Tabs.svelte

@@ -0,0 +1,82 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import { setContext } from 'svelte';
+
+  interface Tab {
+    id: string;
+    label: string;
+    icon?: Snippet;
+    disabled?: boolean;
+  }
+
+  interface Props {
+    tabs: Tab[];
+    active?: string;
+    class?: string;
+    panelClass?: string;
+    children?: Snippet<[string]>;
+    onchange?: (id: string) => void;
+  }
+
+  let {
+    tabs,
+    active = $bindable(tabs[0]?.id ?? ''),
+    class: className = '',
+    panelClass = '',
+    children,
+    onchange,
+  }: Props = $props();
+
+  function select(id: string) {
+    active = id;
+    onchange?.(id);
+  }
+
+  setContext('tabs', { get active() { return active; } });
+</script>
+
+<div class={cn('flex flex-col gap-0', className)}>
+  <!-- Tab list -->
+  <div
+    role="tablist"
+    class="flex items-center gap-0.5 border-b border-[var(--border)] overflow-x-auto"
+  >
+    {#each tabs as tab}
+      <button
+        role="tab"
+        aria-selected={active === tab.id}
+        aria-controls="tabpanel-{tab.id}"
+        disabled={tab.disabled}
+        onclick={() => select(tab.id)}
+        class={cn(
+          'relative flex items-center gap-1.5 px-3.5 py-2.5 text-sm font-medium',
+          'whitespace-nowrap transition-colors duration-[150ms]',
+          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-inset',
+          'cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
+          active === tab.id
+            ? 'text-[var(--foreground)]'
+            : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
+        )}
+      >
+        {#if tab.icon}{@render tab.icon()}{/if}
+        {tab.label}
+        <!-- Active indicator -->
+        {#if active === tab.id}
+          <span
+            class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--primary)] rounded-t-[2px]"
+          ></span>
+        {/if}
+      </button>
+    {/each}
+  </div>
+
+  <!-- Panel -->
+  <div
+    id="tabpanel-{active}"
+    role="tabpanel"
+    class={cn('mt-4', panelClass)}
+  >
+    {#if children}{@render children(active)}{/if}
+  </div>
+</div>

+ 180 - 0
src/lib/components/navigation/CommandPalette.svelte

@@ -0,0 +1,180 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import { browser } from '$app/environment';
+
+  interface CommandItem {
+    id: string;
+    label: string;
+    description?: string;
+    icon?: string;
+    group?: string;
+    shortcut?: string;
+    onselect: () => void;
+  }
+
+  interface Props {
+    items: CommandItem[];
+    placeholder?: string;
+    open?: boolean;
+    onclose?: () => void;
+  }
+
+  let {
+    items,
+    placeholder = 'Search…',
+    open = $bindable(false),
+    onclose,
+  }: Props = $props();
+
+  let query = $state('');
+  let activeIndex = $state(0);
+  let inputEl = $state<HTMLInputElement | null>(null);
+
+  function close() {
+    open = false;
+    query = '';
+    activeIndex = 0;
+    onclose?.();
+  }
+
+  let filtered = $derived.by(() => {
+    if (!query.trim()) return items;
+    const q = query.toLowerCase();
+    return items.filter(
+      i => i.label.toLowerCase().includes(q) || i.description?.toLowerCase().includes(q)
+    );
+  });
+
+  let groups = $derived.by(() => {
+    const map = new Map<string, CommandItem[]>();
+    for (const item of filtered) {
+      const g = item.group ?? '';
+      if (!map.has(g)) map.set(g, []);
+      map.get(g)!.push(item);
+    }
+    return map;
+  });
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') { close(); return; }
+    if (e.key === 'ArrowDown') {
+      e.preventDefault();
+      activeIndex = Math.min(activeIndex + 1, filtered.length - 1);
+    } else if (e.key === 'ArrowUp') {
+      e.preventDefault();
+      activeIndex = Math.max(activeIndex - 1, 0);
+    } else if (e.key === 'Enter') {
+      filtered[activeIndex]?.onselect();
+      close();
+    }
+  }
+
+  $effect(() => {
+    if (!browser) return;
+    const handler = (e: KeyboardEvent) => {
+      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+        e.preventDefault();
+        open = !open;
+      }
+    };
+    document.addEventListener('keydown', handler);
+    return () => document.removeEventListener('keydown', handler);
+  });
+
+  $effect(() => {
+    if (open) setTimeout(() => inputEl?.focus(), 10);
+  });
+
+  // Track global index across groups for keyboard nav
+  let flatIndex = $state(0);
+  $effect(() => { flatIndex; activeIndex = 0; });
+</script>
+
+{#if open}
+  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
+  <div
+    class="fixed inset-0 z-[60] flex items-start justify-center pt-[15vh] px-4"
+    onclick={(e) => { if (e.target === e.currentTarget) close(); }}
+  >
+    <div class="absolute inset-0 bg-black/40 backdrop-blur-sm animate-[fadeIn_150ms_ease-out]"></div>
+
+    <div
+      class={cn(
+        'relative z-10 w-full max-w-lg overflow-hidden',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-lg)]',
+        'animate-[slideUpFadeIn_200ms_cubic-bezier(0.16,1,0.3,1)]'
+      )}
+      onkeydown={handleKeydown}
+    >
+      <!-- Search input -->
+      <div class="flex items-center gap-3 border-b border-[var(--border)] px-4">
+        <svg class="h-4 w-4 shrink-0 text-[var(--muted-foreground)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+          <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
+        </svg>
+        <input
+          bind:this={inputEl}
+          bind:value={query}
+          {placeholder}
+          type="text"
+          role="combobox"
+          aria-expanded="true"
+          aria-autocomplete="list"
+          class="flex-1 bg-transparent py-4 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:outline-none"
+        />
+        <kbd class="hidden sm:flex items-center gap-1 rounded border border-[var(--border)] bg-[var(--muted)] px-1.5 py-0.5 text-xs text-[var(--muted-foreground)]">Esc</kbd>
+      </div>
+
+      <!-- Results -->
+      <div class="max-h-[340px] overflow-y-auto py-2" role="listbox">
+        {#if filtered.length === 0}
+          <p class="px-4 py-10 text-center text-sm text-[var(--muted-foreground)]">No results for "{query}"</p>
+        {:else}
+          {#each groups as [group, groupItems]}
+            {#if group}
+              <p class="px-4 pt-3 pb-1 text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">{group}</p>
+            {/if}
+            {#each groupItems as item, i}
+              {@const idx = filtered.indexOf(item)}
+              <button
+                role="option"
+                aria-selected={idx === activeIndex}
+                onclick={() => { item.onselect(); close(); }}
+                onmouseenter={() => { activeIndex = idx; }}
+                class={cn(
+                  'flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-[100ms]',
+                  idx === activeIndex
+                    ? 'bg-[var(--muted)] text-[var(--foreground)]'
+                    : 'text-[var(--foreground)] hover:bg-[var(--muted)]/60'
+                )}
+              >
+                {#if item.icon}
+                  <span class="text-base leading-none">{item.icon}</span>
+                {/if}
+                <div class="flex-1 min-w-0 text-left">
+                  <span class="block truncate font-medium">{item.label}</span>
+                  {#if item.description}
+                    <span class="block truncate text-xs text-[var(--muted-foreground)]">{item.description}</span>
+                  {/if}
+                </div>
+                {#if item.shortcut}
+                  <kbd class="shrink-0 rounded border border-[var(--border)] bg-[var(--muted)] px-1.5 py-0.5 text-xs text-[var(--muted-foreground)]">{item.shortcut}</kbd>
+                {/if}
+              </button>
+            {/each}
+          {/each}
+        {/if}
+      </div>
+    </div>
+  </div>
+{/if}
+
+<style>
+  @keyframes fadeIn {
+    from { opacity: 0; } to { opacity: 1; }
+  }
+  @keyframes slideUpFadeIn {
+    from { opacity: 0; transform: translateY(-8px) scale(0.98); }
+    to   { opacity: 1; transform: translateY(0) scale(1); }
+  }
+</style>

+ 36 - 0
src/lib/components/navigation/NavItem.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { page } from '$app/state';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    href: string;
+    class?: string;
+    exact?: boolean;
+    children?: Snippet;
+    icon?: Snippet;
+  }
+
+  let { href, class: className = '', exact = false, children, icon }: Props = $props();
+
+  let isActive = $derived(
+    exact ? page.url.pathname === href : page.url.pathname.startsWith(href)
+  );
+</script>
+
+<a
+  {href}
+  aria-current={isActive ? 'page' : undefined}
+  class={cn(
+    'flex items-center gap-2 rounded-[var(--radius)] px-3 py-1.5 text-sm font-medium',
+    'transition-colors duration-[150ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+    isActive
+      ? 'bg-[var(--primary)]/10 text-[var(--primary)]'
+      : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+    className
+  )}
+>
+  {#if icon}{@render icon()}{/if}
+  {#if children}{@render children()}{/if}
+</a>

+ 95 - 0
src/lib/components/navigation/Navbar.svelte

@@ -0,0 +1,95 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import ThemeToggle from './ThemeToggle.svelte';
+
+  interface Props {
+    class?: string;
+    logo?: Snippet;
+    nav?: Snippet;
+    actions?: Snippet;
+    hideThemeToggle?: boolean;
+  }
+
+  let {
+    class: className = '',
+    logo,
+    nav,
+    actions,
+    hideThemeToggle = false,
+  }: Props = $props();
+
+  let menuOpen = $state(false);
+</script>
+
+<header
+  class={cn(
+    'sticky top-0 z-40 h-14 w-full',
+    'bg-[var(--card)]/80 backdrop-blur-xl',
+    'border-b border-[var(--border)]',
+    className
+  )}
+>
+  <div class="mx-auto flex h-full w-full max-w-7xl items-center gap-4 px-4 sm:px-6 lg:px-8">
+    <!-- Logo -->
+    {#if logo}
+      <div class="flex shrink-0 items-center">
+        {@render logo()}
+      </div>
+    {/if}
+
+    <!-- Desktop nav -->
+    {#if nav}
+      <nav class="hidden md:flex items-center gap-1 flex-1">
+        {@render nav()}
+      </nav>
+    {:else}
+      <div class="flex-1"></div>
+    {/if}
+
+    <!-- Actions + theme toggle -->
+    <div class="flex items-center gap-1">
+      {#if actions}
+        {@render actions()}
+      {/if}
+      {#if !hideThemeToggle}
+        <ThemeToggle />
+      {/if}
+
+      <!-- Mobile menu button -->
+      {#if nav}
+        <button
+          type="button"
+          aria-label="Toggle menu"
+          aria-expanded={menuOpen}
+          onclick={() => { menuOpen = !menuOpen; }}
+          class={cn(
+            'md:hidden flex h-8 w-8 items-center justify-center rounded-[var(--radius)]',
+            'cursor-pointer text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+            'transition-all duration-[150ms]',
+            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+          )}
+        >
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            {#if menuOpen}
+              <path d="M18 6L6 18M6 6l12 12"/>
+            {:else}
+              <path d="M4 6h16M4 12h16M4 18h16"/>
+            {/if}
+          </svg>
+        </button>
+      {/if}
+    </div>
+  </div>
+
+  <!-- Mobile nav dropdown -->
+  {#if nav && menuOpen}
+    <div
+      class="md:hidden border-t border-[var(--border)] bg-[var(--card)]/95 backdrop-blur-xl"
+    >
+      <div class="mx-auto max-w-7xl px-4 py-3 flex flex-col gap-1">
+        {@render nav()}
+      </div>
+    </div>
+  {/if}
+</header>

+ 114 - 0
src/lib/components/navigation/Sidebar.svelte

@@ -0,0 +1,114 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    collapsed?: boolean;
+    mobileOpen?: boolean;
+    inline?: boolean;
+    header?: Snippet<[{ collapsed: boolean }]>;
+    footer?: Snippet<[{ collapsed: boolean }]>;
+    children?: Snippet<[{ collapsed: boolean }]>;
+    class?: string;
+    oncollapsechange?: (collapsed: boolean) => void;
+  }
+
+  let {
+    collapsed = $bindable(false),
+    mobileOpen = $bindable(false),
+    inline = false,
+    header,
+    footer,
+    children,
+    class: className = '',
+    oncollapsechange,
+  }: Props = $props();
+
+  function toggleCollapse() {
+    collapsed = !collapsed;
+    oncollapsechange?.(collapsed);
+  }
+</script>
+
+<!-- Mobile overlay -->
+{#if mobileOpen}
+  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
+  <div
+    class="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm lg:hidden animate-[fadeIn_150ms_ease-out]"
+    onclick={() => { mobileOpen = false; }}
+  ></div>
+{/if}
+
+<!-- Sidebar panel -->
+<aside
+  class={cn(
+    'flex flex-col',
+    'border-r border-[var(--border)] bg-[var(--card)]',
+    'transition-[width,transform] duration-[200ms] ease-[cubic-bezier(0.16,1,0.3,1)]',
+    collapsed ? 'w-[56px]' : 'w-[220px]',
+    inline
+      ? 'sticky top-0 h-screen shrink-0'
+      : [
+          'fixed inset-y-0 left-0 z-50',
+          mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
+        ],
+    className
+  )}
+  aria-label="Sidebar navigation"
+>
+  <!-- Header slot -->
+  {#if header}
+    <div class="flex shrink-0 items-center border-b border-[var(--border)] px-2 h-14">
+      {@render header({ collapsed })}
+    </div>
+  {/if}
+
+  <!-- Nav content -->
+  <div class="flex flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden p-2">
+    {#if children}
+      {@render children({ collapsed })}
+    {/if}
+  </div>
+
+  <!-- Footer slot -->
+  {#if footer}
+    <div class="flex shrink-0 flex-col gap-1 border-t border-[var(--border)] p-2">
+      {@render footer({ collapsed })}
+    </div>
+  {/if}
+
+  <!-- Collapse toggle (desktop, fixed mode only) -->
+  <button
+    type="button"
+    onclick={toggleCollapse}
+    aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
+    class={cn(
+      'absolute -right-3 top-[72px]',
+      inline ? 'hidden' : 'hidden lg:flex',
+      'h-6 w-6 cursor-pointer items-center justify-center',
+      'rounded-full border border-[var(--border)] bg-[var(--card)]',
+      'text-[var(--muted-foreground)] hover:text-[var(--foreground)]',
+      'shadow-[var(--shadow-sm)] transition-colors duration-[150ms]',
+      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'
+    )}
+  >
+    <svg
+      width="10"
+      height="10"
+      viewBox="0 0 24 24"
+      fill="none"
+      stroke="currentColor"
+      stroke-width="2.5"
+      stroke-linecap="round"
+      class={cn('transition-transform duration-[200ms]', collapsed && 'rotate-180')}
+    >
+      <path d="M15 18l-6-6 6-6"/>
+    </svg>
+  </button>
+</aside>
+
+<style>
+  @keyframes fadeIn {
+    from { opacity: 0; } to { opacity: 1; }
+  }
+</style>

+ 92 - 0
src/lib/components/navigation/SidebarItem.svelte

@@ -0,0 +1,92 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    href?: string;
+    label: string;
+    active?: boolean;
+    collapsed?: boolean;
+    badge?: string | number;
+    class?: string;
+    icon?: Snippet;
+    onclick?: () => void;
+  }
+
+  let {
+    href,
+    label,
+    active = false,
+    collapsed = false,
+    badge,
+    class: className = '',
+    icon,
+    onclick,
+  }: Props = $props();
+</script>
+
+{#if href}
+  <a
+    {href}
+    aria-current={active ? 'page' : undefined}
+    title={collapsed ? label : undefined}
+    class={cn(
+      'group relative flex items-center gap-3 rounded-[var(--radius)] px-2.5 py-2 text-sm font-medium',
+      'transition-colors duration-[150ms]',
+      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+      collapsed ? 'justify-center' : '',
+      active
+        ? 'bg-[var(--primary)]/10 text-[var(--primary)]'
+        : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+      className
+    )}
+  >
+    {#if icon}
+      <span class="flex h-4 w-4 shrink-0 items-center justify-center">{@render icon()}</span>
+    {/if}
+    {#if !collapsed}
+      <span class="flex-1 truncate">{label}</span>
+      {#if badge !== undefined}
+        <span class={cn(
+          'flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[10px] font-semibold',
+          active ? 'bg-[var(--primary)]/20 text-[var(--primary)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'
+        )}>{badge}</span>
+      {/if}
+    {/if}
+    {#if collapsed && badge !== undefined}
+      <span class="absolute right-1 top-1 flex h-2 w-2 items-center justify-center rounded-full bg-[var(--primary)]"></span>
+    {/if}
+  </a>
+{:else}
+  <button
+    type="button"
+    {onclick}
+    title={collapsed ? label : undefined}
+    class={cn(
+      'group relative flex w-full items-center gap-3 rounded-[var(--radius)] px-2.5 py-2 text-sm font-medium',
+      'cursor-pointer transition-colors duration-[150ms]',
+      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+      collapsed ? 'justify-center' : '',
+      active
+        ? 'bg-[var(--primary)]/10 text-[var(--primary)]'
+        : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+      className
+    )}
+  >
+    {#if icon}
+      <span class="flex h-4 w-4 shrink-0 items-center justify-center">{@render icon()}</span>
+    {/if}
+    {#if !collapsed}
+      <span class="flex-1 truncate text-left">{label}</span>
+      {#if badge !== undefined}
+        <span class={cn(
+          'flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[10px] font-semibold',
+          active ? 'bg-[var(--primary)]/20 text-[var(--primary)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'
+        )}>{badge}</span>
+      {/if}
+    {/if}
+    {#if collapsed && badge !== undefined}
+      <span class="absolute right-1 top-1 flex h-2 w-2 items-center justify-center rounded-full bg-[var(--primary)]"></span>
+    {/if}
+  </button>
+{/if}

+ 26 - 0
src/lib/components/navigation/SidebarSection.svelte

@@ -0,0 +1,26 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    label?: string;
+    collapsed?: boolean;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { label, collapsed = false, class: className = '', children }: Props = $props();
+</script>
+
+<div class={cn('flex flex-col gap-0.5', className)}>
+  {#if label && !collapsed}
+    <p class="mb-1 mt-2 px-2.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--muted-foreground)]/60 first:mt-0">
+      {label}
+    </p>
+  {:else if label && collapsed}
+    <div class="my-2 mx-auto h-px w-6 bg-[var(--border)] first:mt-0"></div>
+  {/if}
+  {#if children}
+    {@render children()}
+  {/if}
+</div>

+ 77 - 0
src/lib/components/navigation/ThemeToggle.svelte

@@ -0,0 +1,77 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import { browser } from '$app/environment';
+
+  type Theme = 'light' | 'dark' | 'system';
+
+  interface Props {
+    class?: string;
+  }
+
+  let { class: className = '' }: Props = $props();
+
+  function getInitialTheme(): Theme {
+    if (!browser) return 'system';
+    return (localStorage.getItem('olive-theme') as Theme) ?? 'system';
+  }
+
+  let theme = $state<Theme>(getInitialTheme());
+
+  function applyTheme(t: Theme) {
+    if (!browser) return;
+    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+    const isDark = t === 'dark' || (t === 'system' && prefersDark);
+    document.documentElement.classList.toggle('dark', isDark);
+    localStorage.setItem('olive-theme', t);
+  }
+
+  function cycle() {
+    const next: Record<Theme, Theme> = { light: 'dark', dark: 'system', system: 'light' };
+    theme = next[theme];
+    applyTheme(theme);
+  }
+
+  $effect(() => {
+    if (!browser) return;
+    const mq = window.matchMedia('(prefers-color-scheme: dark)');
+    const handler = () => { if (theme === 'system') applyTheme('system'); };
+    mq.addEventListener('change', handler);
+    return () => mq.removeEventListener('change', handler);
+  });
+</script>
+
+<button
+  type="button"
+  onclick={cycle}
+  aria-label="Toggle theme (current: {theme})"
+  title="Toggle theme"
+  class={cn(
+    'relative flex h-8 w-8 items-center justify-center rounded-[var(--radius)]',
+    'cursor-pointer text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+    'transition-all duration-[150ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+    className
+  )}
+>
+  <!-- Sun icon -->
+  <svg
+    class={cn('absolute transition-all duration-[200ms]', theme === 'light' ? 'opacity-100 scale-100' : 'opacity-0 scale-75')}
+    width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+  >
+    <circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
+  </svg>
+  <!-- Moon icon -->
+  <svg
+    class={cn('absolute transition-all duration-[200ms]', theme === 'dark' ? 'opacity-100 scale-100' : 'opacity-0 scale-75')}
+    width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+  >
+    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
+  </svg>
+  <!-- System/monitor icon -->
+  <svg
+    class={cn('absolute transition-all duration-[200ms]', theme === 'system' ? 'opacity-100 scale-100' : 'opacity-0 scale-75')}
+    width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+  >
+    <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
+  </svg>
+</button>

+ 48 - 0
src/lib/components/overlay/Alert.svelte

@@ -0,0 +1,48 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Variant = 'info' | 'success' | 'warning' | 'error';
+
+  interface Props {
+    variant?: Variant;
+    title?: string;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { variant = 'info', title, class: className = '', children }: Props = $props();
+
+  const variantClasses: Record<Variant, string> = {
+    info:    'alert-info',
+    success: 'alert-success',
+    warning: 'alert-warning',
+    error:   'alert-error',
+  };
+
+  const icons: Record<Variant, string> = {
+    info:    'M12 16v-4M12 8h.01M22 12A10 10 0 1 1 2 12a10 10 0 0 1 20 0z',
+    success: 'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4L12 14.01l-3-3',
+    warning: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01',
+    error:   'M18 6L6 18M6 6l12 12',
+  };
+</script>
+
+<div
+  role="alert"
+  class={cn(
+    'flex gap-3 rounded-[var(--radius)] border p-4 text-sm',
+    variantClasses[variant],
+    className
+  )}
+>
+  <svg class="mt-0.5 h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+    <path d={icons[variant]}/>
+  </svg>
+  <div class="flex-1 min-w-0">
+    {#if title}
+      <p class="font-medium mb-1">{title}</p>
+    {/if}
+    {#if children}{@render children()}{/if}
+  </div>
+</div>

+ 87 - 0
src/lib/components/overlay/AlertDialog.svelte

@@ -0,0 +1,87 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+  import Button from '$lib/components/primitives/Button.svelte';
+
+  interface Props {
+    open?: boolean;
+    title: string;
+    description?: string;
+    confirmLabel?: string;
+    cancelLabel?: string;
+    destructive?: boolean;
+    onconfirm?: () => void;
+    oncancel?: () => void;
+  }
+
+  let {
+    open = $bindable(false),
+    title,
+    description,
+    confirmLabel = 'Confirm',
+    cancelLabel = 'Cancel',
+    destructive = false,
+    onconfirm,
+    oncancel,
+  }: Props = $props();
+
+  function cancel() {
+    open = false;
+    oncancel?.();
+  }
+
+  function confirm() {
+    open = false;
+    onconfirm?.();
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') cancel();
+  }
+</script>
+
+{#if open}
+  <div
+    role="presentation"
+    class="fixed inset-0 z-50 flex items-center justify-center p-4"
+  >
+    <div class="absolute inset-0 bg-black/50 backdrop-blur-sm animate-[fadeIn_200ms_ease-out]"></div>
+
+    <div
+      role="alertdialog"
+      aria-modal="true"
+      tabindex="-1"
+      aria-labelledby="alert-title"
+      aria-describedby={description ? 'alert-desc' : undefined}
+      onkeydown={handleKeydown}
+      class={cn(
+        'relative z-10 w-full max-w-sm max-w-[calc(100vw-2rem)]',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-lg)]',
+        'animate-[slideUpFadeIn_250ms_cubic-bezier(0.16,1,0.3,1)]',
+      )}
+    >
+      <div class="px-6 pt-6 pb-4">
+        <h2 id="alert-title" class="text-base font-semibold text-[var(--foreground)]">{title}</h2>
+        {#if description}
+          <p id="alert-desc" class="mt-2 text-sm text-[var(--muted-foreground)]">{description}</p>
+        {/if}
+      </div>
+
+      <div class="flex items-center justify-end gap-2 border-t border-[var(--border)] px-6 py-4">
+        <Button variant="outline" size="sm" onclick={cancel}>{cancelLabel}</Button>
+        <Button variant={destructive ? 'destructive' : 'default'} size="sm" onclick={confirm}>{confirmLabel}</Button>
+      </div>
+    </div>
+  </div>
+{/if}
+
+<style>
+  @keyframes fadeIn {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+  }
+  @keyframes slideUpFadeIn {
+    from { opacity: 0; transform: translateY(8px) scale(0.97); }
+    to   { opacity: 1; transform: translateY(0) scale(1); }
+  }
+</style>

+ 121 - 0
src/lib/components/overlay/Dialog.svelte

@@ -0,0 +1,121 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    open?: boolean;
+    title?: string;
+    description?: string;
+    class?: string;
+    children?: Snippet;
+    footer?: Snippet;
+    onclose?: () => void;
+  }
+
+  let {
+    open = $bindable(false),
+    title,
+    description,
+    class: className = '',
+    children,
+    footer,
+    onclose,
+  }: Props = $props();
+
+  function close() {
+    open = false;
+    onclose?.();
+  }
+
+  function handleBackdrop(e: MouseEvent) {
+    if (e.target === e.currentTarget) close();
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') close();
+  }
+</script>
+
+{#if open}
+  <!-- Backdrop -->
+  <!-- svelte-ignore a11y_click_events_have_key_events -->
+  <div
+    role="presentation"
+    class="fixed inset-0 z-50 flex items-center justify-center p-4"
+    onclick={handleBackdrop}
+  >
+    <!-- Overlay -->
+    <div class="absolute inset-0 bg-black/40 backdrop-blur-sm animate-[fadeIn_200ms_ease-out]"></div>
+
+    <!-- Panel -->
+    <div
+      role="dialog"
+      aria-modal="true"
+      tabindex="-1"
+      aria-labelledby={title ? 'dialog-title' : undefined}
+      aria-describedby={description ? 'dialog-desc' : undefined}
+      onkeydown={handleKeydown}
+      class={cn(
+        'relative z-10 w-full max-w-md max-w-[calc(100vw-2rem)]',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-lg)]',
+        'animate-[slideUpFadeIn_250ms_cubic-bezier(0.16,1,0.3,1)]',
+        className
+      )}
+    >
+      <!-- Header -->
+      {#if title || description}
+        <div class="px-6 pt-6 pb-4">
+          {#if title}
+            <h2 id="dialog-title" class="text-base font-semibold text-[var(--foreground)]">{title}</h2>
+          {/if}
+          {#if description}
+            <p id="dialog-desc" class="mt-1 text-sm text-[var(--muted-foreground)]">{description}</p>
+          {/if}
+        </div>
+      {/if}
+
+      <!-- Close button -->
+      <button
+        type="button"
+        onclick={close}
+        aria-label="Close dialog"
+        class={cn(
+          'absolute right-4 top-4 flex h-7 w-7 items-center justify-center rounded-[calc(var(--radius)*0.67)]',
+          'cursor-pointer text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
+          'transition-all duration-[150ms]',
+          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
+        )}
+      >
+        <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="M18 6L6 18M6 6l12 12"/>
+        </svg>
+      </button>
+
+      <!-- Body -->
+      {#if children}
+        <div class="px-6 {title || description ? 'pb-4' : 'pt-6 pb-4'}">
+          {@render children()}
+        </div>
+      {/if}
+
+      <!-- Footer -->
+      {#if footer}
+        <div class="flex items-center justify-end gap-2 border-t border-[var(--border)] px-6 py-4">
+          {@render footer()}
+        </div>
+      {/if}
+    </div>
+  </div>
+{/if}
+
+<style>
+  @keyframes fadeIn {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+  }
+  @keyframes slideUpFadeIn {
+    from { opacity: 0; transform: translateY(8px) scale(0.97); }
+    to   { opacity: 1; transform: translateY(0) scale(1); }
+  }
+</style>

+ 103 - 0
src/lib/components/overlay/DropdownMenu.svelte

@@ -0,0 +1,103 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Item {
+    label: string;
+    icon?: string;
+    variant?: 'default' | 'destructive';
+    disabled?: boolean;
+    separator?: boolean;
+    onclick?: () => void;
+  }
+
+  interface Props {
+    items: Item[];
+    align?: 'start' | 'end';
+    class?: string;
+    trigger?: Snippet;
+  }
+
+  let { items, align = 'start', class: className = '', trigger }: Props = $props();
+
+  let open = $state(false);
+  let menuEl = $state<HTMLElement | null>(null);
+
+  function toggle() { open = !open; }
+  function close() { open = false; }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') close();
+  }
+
+  function handleOutside(e: MouseEvent) {
+    if (menuEl && !menuEl.contains(e.target as Node)) close();
+  }
+
+  $effect(() => {
+    if (open) {
+      document.addEventListener('click', handleOutside);
+      return () => document.removeEventListener('click', handleOutside);
+    }
+  });
+</script>
+
+<!-- svelte-ignore a11y_no_static_element_interactions -->
+<div
+  class={cn('relative inline-flex', className)}
+  bind:this={menuEl}
+  onkeydown={handleKeydown}
+>
+  <!-- Trigger -->
+  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
+  <div onclick={toggle} class="inline-flex">
+    {#if trigger}{@render trigger()}{/if}
+  </div>
+
+  <!-- Menu -->
+  {#if open}
+    <div
+      role="menu"
+      class={cn(
+        'absolute top-full z-50 mt-1 min-w-[160px]',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-md)] p-1',
+        'animate-[fadeInScale_150ms_cubic-bezier(0.16,1,0.3,1)]',
+        align === 'end' ? 'right-0' : 'left-0',
+      )}
+    >
+      {#each items as item}
+        {#if item.separator}
+          <div class="my-1 h-px bg-[var(--border)]"></div>
+        {:else}
+          <button
+            role="menuitem"
+            disabled={item.disabled}
+            onclick={() => { item.onclick?.(); close(); }}
+            class={cn(
+              'flex w-full items-center gap-2 rounded-[calc(var(--radius)*0.67)] px-2.5 py-1.5 text-sm',
+              'transition-colors duration-[100ms] text-left',
+              'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-inset',
+              'cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
+              item.variant === 'destructive'
+                ? 'text-[var(--destructive)] hover:bg-[var(--destructive)]/10'
+                : 'text-[var(--foreground)] hover:bg-[var(--muted)]',
+            )}
+          >
+            {#if item.icon}
+              <span class="text-[var(--muted-foreground)]">{item.icon}</span>
+            {/if}
+            {item.label}
+          </button>
+        {/if}
+      {/each}
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes fadeInScale {
+    from { opacity: 0; transform: scale(0.95); }
+    to   { opacity: 1; transform: scale(1); }
+  }
+</style>

+ 92 - 0
src/lib/components/overlay/Popover.svelte

@@ -0,0 +1,92 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Align = 'start' | 'center' | 'end';
+  type Side = 'top' | 'bottom' | 'left' | 'right';
+
+  interface Props {
+    open?: boolean;
+    align?: Align;
+    side?: Side;
+    class?: string;
+    trigger?: Snippet;
+    children?: Snippet;
+  }
+
+  let {
+    open = $bindable(false),
+    align = 'start',
+    side = 'bottom',
+    class: className = '',
+    trigger,
+    children,
+  }: Props = $props();
+
+  let containerEl = $state<HTMLElement | null>(null);
+
+  function toggle() { open = !open; }
+  function close() { open = false; }
+
+  function handleOutside(e: MouseEvent) {
+    if (containerEl && !containerEl.contains(e.target as Node)) close();
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') close();
+  }
+
+  $effect(() => {
+    if (open) {
+      document.addEventListener('click', handleOutside);
+      return () => document.removeEventListener('click', handleOutside);
+    }
+  });
+
+  const sideClasses: Record<Side, string> = {
+    bottom: 'top-full mt-2',
+    top:    'bottom-full mb-2',
+    left:   'right-full mr-2 top-0',
+    right:  'left-full ml-2 top-0',
+  };
+
+  const alignClasses: Record<Align, string> = {
+    start:  'left-0',
+    center: 'left-1/2 -translate-x-1/2',
+    end:    'right-0',
+  };
+</script>
+
+<!-- svelte-ignore a11y_no_static_element_interactions -->
+<div
+  class="relative inline-flex"
+  bind:this={containerEl}
+  onkeydown={handleKeydown}
+>
+  <div onclick={toggle} class="inline-flex">
+    {#if trigger}{@render trigger()}{/if}
+  </div>
+
+  {#if open}
+    <div
+      class={cn(
+        'absolute z-50 min-w-[220px]',
+        'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)]',
+        'shadow-[var(--shadow-md)] p-4',
+        'animate-[popoverIn_150ms_cubic-bezier(0.16,1,0.3,1)]',
+        sideClasses[side],
+        alignClasses[align],
+        className
+      )}
+    >
+      {#if children}{@render children()}{/if}
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes popoverIn {
+    from { opacity: 0; transform: scale(0.96); }
+    to   { opacity: 1; transform: scale(1); }
+  }
+</style>

+ 57 - 0
src/lib/components/overlay/Progress.svelte

@@ -0,0 +1,57 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  type Variant = 'default' | 'success' | 'warning' | 'error';
+
+  interface Props {
+    value?: number;
+    max?: number;
+    variant?: Variant;
+    indeterminate?: boolean;
+    class?: string;
+  }
+
+  let {
+    value = 0,
+    max = 100,
+    variant = 'default',
+    indeterminate = false,
+    class: className = '',
+  }: Props = $props();
+
+  let percent = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
+
+  const fillClasses: Record<Variant, string> = {
+    default: 'bg-[var(--primary)]',
+    success: 'bg-emerald-500',
+    warning: 'bg-amber-500',
+    error:   'bg-red-500',
+  };
+</script>
+
+<div
+  role="progressbar"
+  aria-valuenow={indeterminate ? undefined : value}
+  aria-valuemin={0}
+  aria-valuemax={max}
+  class={cn(
+    'relative h-1.5 w-full overflow-hidden rounded-full bg-[var(--muted)]',
+    className
+  )}
+>
+  <div
+    class={cn(
+      'h-full rounded-full transition-all duration-[250ms]',
+      fillClasses[variant],
+      indeterminate && 'animate-[indeterminate_1.4s_ease-in-out_infinite]'
+    )}
+    style:width={indeterminate ? '40%' : `${percent}%`}
+  ></div>
+</div>
+
+<style>
+  @keyframes indeterminate {
+    0%   { transform: translateX(-100%); }
+    100% { transform: translateX(350%); }
+  }
+</style>

+ 126 - 0
src/lib/components/overlay/Sheet.svelte

@@ -0,0 +1,126 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Side = 'left' | 'right' | 'top' | 'bottom';
+
+  interface Props {
+    open?: boolean;
+    side?: Side;
+    title?: string;
+    description?: string;
+    class?: string;
+    children?: Snippet;
+    footer?: Snippet;
+    onclose?: () => void;
+  }
+
+  let {
+    open = $bindable(false),
+    side = 'right',
+    title,
+    description,
+    class: className = '',
+    children,
+    footer,
+    onclose,
+  }: Props = $props();
+
+  function close() {
+    open = false;
+    onclose?.();
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === 'Escape') close();
+  }
+
+  const panelClasses: Record<Side, string> = {
+    right:  'inset-y-0 right-0 h-full w-full max-w-sm translate-x-0 border-l',
+    left:   'inset-y-0 left-0 h-full w-full max-w-sm border-r',
+    top:    'inset-x-0 top-0 w-full max-h-[60vh] border-b',
+    bottom: 'inset-x-0 bottom-0 w-full max-h-[60vh] border-t',
+  };
+
+  const enterFrom: Record<Side, string> = {
+    right:  'translate-x-full',
+    left:   '-translate-x-full',
+    top:    '-translate-y-full',
+    bottom: 'translate-y-full',
+  };
+</script>
+
+{#if open}
+  <div
+    role="presentation"
+    class="fixed inset-0 z-50 flex"
+    onkeydown={handleKeydown}
+  >
+    <!-- Backdrop -->
+    <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
+    <div
+      class="absolute inset-0 bg-black/40 backdrop-blur-sm animate-[fadeIn_200ms_ease-out]"
+      onclick={close}
+    ></div>
+
+    <!-- Panel -->
+    <div
+      role="dialog"
+      aria-modal="true"
+      tabindex="-1"
+      aria-labelledby={title ? 'sheet-title' : undefined}
+      class={cn(
+        'absolute flex flex-col bg-[var(--card)] border-[var(--border)]',
+        'shadow-[var(--shadow-lg)]',
+        'animate-[sheetIn_250ms_cubic-bezier(0.16,1,0.3,1)]',
+        panelClasses[side],
+        className
+      )}
+    >
+      <!-- Header -->
+      <div class="flex items-start justify-between gap-4 px-6 py-5 border-b border-[var(--border)]">
+        <div>
+          {#if title}
+            <h2 id="sheet-title" class="text-base font-semibold text-[var(--foreground)]">{title}</h2>
+          {/if}
+          {#if description}
+            <p class="mt-1 text-sm text-[var(--muted-foreground)]">{description}</p>
+          {/if}
+        </div>
+        <button
+          type="button"
+          onclick={close}
+          aria-label="Close"
+          class="cursor-pointer shrink-0 flex h-7 w-7 items-center justify-center rounded-[var(--radius-button)] text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)] transition-colors duration-[150ms] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]"
+        >
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+            <path d="M18 6L6 18M6 6l12 12"/>
+          </svg>
+        </button>
+      </div>
+
+      <!-- Body -->
+      <div class="flex-1 overflow-y-auto px-6 py-5">
+        {#if children}{@render children()}{/if}
+      </div>
+
+      <!-- Footer -->
+      {#if footer}
+        <div class="flex items-center justify-end gap-2 border-t border-[var(--border)] px-6 py-4">
+          {@render footer()}
+        </div>
+      {/if}
+    </div>
+  </div>
+{/if}
+
+<style>
+  @keyframes fadeIn {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+  }
+  @keyframes sheetIn {
+    from { opacity: 0; transform: translateX(100%); }
+    to   { opacity: 1; transform: translateX(0); }
+  }
+</style>

+ 30 - 0
src/lib/components/overlay/Skeleton.svelte

@@ -0,0 +1,30 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    class?: string;
+    width?: string;
+    height?: string;
+    rounded?: boolean;
+  }
+
+  let { class: className = '', width, height, rounded = false }: Props = $props();
+</script>
+
+<div
+  class={cn(
+    'animate-[shimmer_1.8s_ease-in-out_infinite]',
+    'bg-[var(--muted)]',
+    rounded ? 'rounded-full' : 'rounded-[var(--radius)]',
+    className
+  )}
+  style:width={width}
+  style:height={height ?? '1rem'}
+></div>
+
+<style>
+  @keyframes shimmer {
+    0%, 100% { opacity: 1; }
+    50%       { opacity: 0.5; }
+  }
+</style>

+ 75 - 0
src/lib/components/overlay/Toaster.svelte

@@ -0,0 +1,75 @@
+<script lang="ts">
+  import { toast, type ToastVariant } from '$lib/utils/toast.svelte.js';
+  import { cn } from '$lib/utils/cn.js';
+
+  const icons: Record<ToastVariant, string> = {
+    default: '',
+    success: '✓',
+    warning: '⚠',
+    error:   '✕',
+  };
+
+  const variantClasses: Record<ToastVariant, string> = {
+    default: 'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]',
+    success: 'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]',
+    warning: 'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]',
+    error:   'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]',
+  };
+
+  const iconClasses: Record<ToastVariant, string> = {
+    default: 'text-[var(--muted-foreground)]',
+    success: 'text-emerald-500',
+    warning: 'text-amber-500',
+    error:   'text-red-500',
+  };
+</script>
+
+<div
+  aria-live="polite"
+  aria-atomic="false"
+  class="fixed bottom-4 left-1/2 z-[100] flex -translate-x-1/2 flex-col gap-2 items-center pointer-events-none"
+>
+  {#each toast.toasts as t (t.id)}
+    <div
+      role="status"
+      class={cn(
+        'flex items-start gap-3 rounded-[var(--radius)] border px-4 py-3',
+        'shadow-[var(--shadow-lg)] pointer-events-auto min-w-[280px] max-w-[420px]',
+        'animate-[toastIn_200ms_cubic-bezier(0.16,1,0.3,1)]',
+        variantClasses[t.variant]
+      )}
+    >
+      {#if icons[t.variant]}
+        <span class={cn('mt-0.5 text-sm font-bold shrink-0', iconClasses[t.variant])}>
+          {icons[t.variant]}
+        </span>
+      {/if}
+      <div class="flex-1 min-w-0">
+        <p class="text-sm font-medium">{t.message}</p>
+        {#if t.description}
+          <p class="text-xs text-[var(--muted-foreground)] mt-0.5">{t.description}</p>
+        {/if}
+      </div>
+      <button
+        type="button"
+        onclick={() => toast.remove(t.id)}
+        aria-label="Dismiss"
+        class={cn(
+          'cursor-pointer shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)]',
+          'transition-colors duration-[150ms] focus-visible:outline-none'
+        )}
+      >
+        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
+          <path d="M18 6L6 18M6 6l12 12"/>
+        </svg>
+      </button>
+    </div>
+  {/each}
+</div>
+
+<style>
+  @keyframes toastIn {
+    from { opacity: 0; transform: translateY(8px); }
+    to   { opacity: 1; transform: translateY(0); }
+  }
+</style>

+ 69 - 0
src/lib/components/overlay/Tooltip.svelte

@@ -0,0 +1,69 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Side = 'top' | 'bottom' | 'left' | 'right';
+
+  interface Props {
+    content: string;
+    side?: Side;
+    delay?: number;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { content, side = 'top', delay = 400, class: className = '', children }: Props = $props();
+
+  let visible = $state(false);
+  let timer: ReturnType<typeof setTimeout>;
+
+  function show() {
+    timer = setTimeout(() => { visible = true; }, delay);
+  }
+
+  function hide() {
+    clearTimeout(timer);
+    visible = false;
+  }
+
+  const sideClasses: Record<Side, string> = {
+    top:    'bottom-full left-1/2 -translate-x-1/2 mb-2',
+    bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
+    left:   'right-full top-1/2 -translate-y-1/2 mr-2',
+    right:  'left-full top-1/2 -translate-y-1/2 ml-2',
+  };
+</script>
+
+<div
+  role="group"
+  class={cn('relative inline-flex', className)}
+  onmouseenter={show}
+  onmouseleave={hide}
+  onfocus={show}
+  onblur={hide}
+>
+  {#if children}{@render children()}{/if}
+
+  {#if visible}
+    <div
+      role="tooltip"
+      class={cn(
+        'pointer-events-none absolute z-50 whitespace-nowrap',
+        'rounded-[calc(var(--radius)*0.67)] bg-[var(--foreground)] px-2.5 py-1',
+        'text-xs font-medium text-[var(--background)]',
+        'shadow-[var(--shadow-md)]',
+        'animate-[fadeInScale_150ms_cubic-bezier(0.16,1,0.3,1)]',
+        sideClasses[side],
+      )}
+    >
+      {content}
+    </div>
+  {/if}
+</div>
+
+<style>
+  @keyframes fadeInScale {
+    from { opacity: 0; transform: scale(0.92) translateX(-50%); }
+    to   { opacity: 1; transform: scale(1) translateX(-50%); }
+  }
+</style>

+ 47 - 0
src/lib/components/primitives/Avatar.svelte

@@ -0,0 +1,47 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  type Size = 'sm' | 'md' | 'lg';
+
+  interface Props {
+    src?: string;
+    alt?: string;
+    initials?: string;
+    size?: Size;
+    class?: string;
+  }
+
+  let { src, alt = '', initials, size = 'md', class: className = '' }: Props = $props();
+
+  const sizes: Record<Size, string> = {
+    sm: 'h-6 w-6 text-xs',
+    md: 'h-8 w-8 text-sm',
+    lg: 'h-10 w-10 text-base',
+  };
+
+  let imgError = $state(false);
+</script>
+
+<div
+  class={cn(
+    'relative flex shrink-0 items-center justify-center overflow-hidden rounded-full',
+    'bg-[var(--muted)] text-[var(--muted-foreground)] font-medium select-none',
+    sizes[size],
+    className
+  )}
+>
+  {#if src && !imgError}
+    <img
+      {src}
+      {alt}
+      class="h-full w-full object-cover"
+      onerror={() => { imgError = true; }}
+    />
+  {:else if initials}
+    <span>{initials.slice(0, 2).toUpperCase()}</span>
+  {:else}
+    <svg viewBox="0 0 24 24" fill="currentColor" class="h-4/5 w-4/5 opacity-60">
+      <path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
+    </svg>
+  {/if}
+</div>

+ 34 - 0
src/lib/components/primitives/Badge.svelte

@@ -0,0 +1,34 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Variant = 'default' | 'outline' | 'active' | 'paused' | 'error' | 'warning' | 'info' | 'debug' | 'trace' | 'write' | 'delete';
+
+  interface Props {
+    variant?: Variant;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { variant = 'default', class: className = '', children }: Props = $props();
+
+  const base = 'inline-flex items-center rounded-[9999px] border px-2 py-0.5 text-xs font-medium transition-colors cursor-crosshair';
+
+  const variants: Record<Variant, string> = {
+    default:  'bg-[var(--muted)] text-[var(--foreground)] border-transparent',
+    outline:  'bg-transparent text-[var(--foreground)] border-[var(--border)]',
+    active:   'badge-active',
+    paused:   'badge-paused',
+    error:    'badge-error',
+    warning:  'badge-warning',
+    info:     'badge-info',
+    debug:    'badge-debug',
+    trace:    'badge-trace',
+    write:    'badge-write',
+    delete:   'badge-delete',
+  };
+</script>
+
+<span class={cn(base, variants[variant], className)}>
+  {#if children}{@render children()}{/if}
+</span>

+ 102 - 0
src/lib/components/primitives/Button.svelte

@@ -0,0 +1,102 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import type { HTMLButtonAttributes } from 'svelte/elements';
+  import { cn } from '$lib/utils/cn.js';
+
+  type Variant = 'default' | 'outline' | 'ghost' | 'destructive' | 'link';
+  type Size = 'sm' | 'md' | 'lg' | 'icon';
+
+  interface Props extends HTMLButtonAttributes {
+    variant?: Variant;
+    size?: Size;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let {
+    variant = 'default',
+    size = 'md',
+    class: className = '',
+    children,
+    ...rest
+  }: Props = $props();
+
+  const base = [
+    'inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap',
+    'transition-all duration-[150ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]',
+    'cursor-pointer disabled:pointer-events-none disabled:opacity-40',
+    'select-none',
+  ].join(' ');
+
+  const variants: Record<Variant, string> = {
+    default:     'btn-default bg-[var(--primary)] text-[var(--primary-foreground)] hover:bg-[var(--primary)]/90 active:bg-[var(--primary)]/80',
+    outline:     'btn-outline border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--muted)] active:bg-[var(--muted)]/80',
+    ghost:       'bg-transparent text-[var(--foreground)] hover:bg-[var(--muted)] active:bg-[var(--muted)]/80',
+    destructive: 'btn-destructive bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:bg-[var(--destructive)]/90 active:bg-[var(--destructive)]/80',
+    link:        'bg-transparent text-[var(--primary)] underline-offset-4 hover:underline h-auto',
+  };
+
+  const sizes: Record<Size, string> = {
+    sm:   'h-7 rounded-[var(--radius-button)] px-2.5 text-xs',
+    md:   'h-8 rounded-[var(--radius-button)] px-3.5 text-sm',
+    lg:   'h-10 rounded-[var(--radius-button)] px-5 text-sm',
+    icon: 'h-8 w-8 rounded-[var(--radius-button)]',
+  };
+</script>
+
+<button
+  class={cn(base, variants[variant], sizes[size], className)}
+  {...rest}
+>
+  {#if children}
+    {@render children()}
+  {/if}
+</button>
+
+<style>
+  /* Primary — depth via top inner highlight + bottom shadow */
+  :global(.btn-default) {
+    box-shadow:
+      0 1px 2px rgba(0, 0, 0, 0.18),
+      inset 0 1px 0 rgba(255, 255, 255, 0.16);
+  }
+  :global(.btn-default:hover) {
+    box-shadow:
+      0 2px 4px rgba(0, 0, 0, 0.18),
+      inset 0 1px 0 rgba(255, 255, 255, 0.16);
+  }
+  :global(.btn-default:active) {
+    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.18);
+  }
+
+  /* Outline — subtle lift */
+  :global(.btn-outline) {
+    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
+  }
+  :global(.btn-outline:active) {
+    box-shadow: none;
+  }
+
+  /* Destructive — same depth pattern as default */
+  :global(.btn-destructive) {
+    box-shadow:
+      0 1px 2px rgba(0, 0, 0, 0.2),
+      inset 0 1px 0 rgba(255, 255, 255, 0.12);
+  }
+  :global(.btn-destructive:hover) {
+    box-shadow:
+      0 2px 4px rgba(0, 0, 0, 0.2),
+      inset 0 1px 0 rgba(255, 255, 255, 0.12);
+  }
+  :global(.btn-destructive:active) {
+    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+  }
+
+  /* Disabled — flat, no shadow regardless of variant */
+  :global(.btn-default:disabled),
+  :global(.btn-outline:disabled),
+  :global(.btn-destructive:disabled) {
+    box-shadow: none;
+  }
+</style>

+ 73 - 0
src/lib/components/primitives/Checkbox.svelte

@@ -0,0 +1,73 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    checked?: boolean;
+    indeterminate?: boolean;
+    disabled?: boolean;
+    id?: string;
+    class?: string;
+    onchange?: (checked: boolean) => void;
+  }
+
+  let {
+    checked = $bindable(false),
+    indeterminate = false,
+    disabled = false,
+    id,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  function handleChange(e: Event) {
+    const input = e.target as HTMLInputElement;
+    checked = input.checked;
+    onchange?.(checked);
+  }
+</script>
+
+<label
+  class={cn(
+    'relative inline-flex items-center justify-center',
+    disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
+    className
+  )}
+>
+  <input
+    type="checkbox"
+    {id}
+    {disabled}
+    bind:checked
+    bind:indeterminate
+    onchange={handleChange}
+    class="sr-only peer"
+  />
+  <div
+    class={cn(
+      'h-4 w-4 rounded-[3px] border',
+      'flex items-center justify-center overflow-hidden',
+      'transition-colors duration-[100ms]',
+      'peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--ring)] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[var(--background)]',
+      checked || indeterminate
+        ? 'bg-[var(--primary)] border-[var(--primary)]'
+        : 'bg-transparent border-[var(--border)]',
+    )}
+  >
+    <svg
+      width="10"
+      height="8"
+      viewBox="0 0 10 8"
+      fill="none"
+      class={cn(
+        'transition-[transform,opacity] duration-[100ms]',
+        checked ? 'opacity-100 scale-100' : 'opacity-0 scale-75'
+      )}
+    >
+      {#if indeterminate}
+        <path d="M1 4H9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+      {:else}
+        <path d="M1 4L3.5 6.5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+      {/if}
+    </svg>
+  </div>
+</label>

+ 26 - 0
src/lib/components/primitives/Input.svelte

@@ -0,0 +1,26 @@
+<script lang="ts">
+  import type { HTMLInputAttributes } from 'svelte/elements';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props extends HTMLInputAttributes {
+    class?: string;
+    error?: boolean;
+  }
+
+  let { class: className = '', error = false, ...rest }: Props = $props();
+</script>
+
+<input
+  class={cn(
+    'flex h-9 w-full rounded-[var(--radius)] border bg-transparent px-3 py-1 text-sm',
+    'text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]',
+    'transition-colors duration-[150ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-0',
+    'disabled:cursor-not-allowed disabled:opacity-40',
+    error
+      ? 'border-[var(--destructive)] focus-visible:ring-[var(--destructive)]'
+      : 'border-[var(--border)] focus-visible:border-[var(--ring)]',
+    className
+  )}
+  {...rest}
+/>

+ 23 - 0
src/lib/components/primitives/Label.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import type { HTMLLabelAttributes } from 'svelte/elements';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props extends HTMLLabelAttributes {
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { class: className = '', children, ...rest }: Props = $props();
+</script>
+
+<label
+  class={cn(
+    'text-sm font-medium text-[var(--foreground)] leading-none',
+    'peer-disabled:cursor-not-allowed peer-disabled:opacity-40',
+    className
+  )}
+  {...rest}
+>
+  {#if children}{@render children()}{/if}
+</label>

+ 46 - 0
src/lib/components/primitives/RadioGroup.svelte

@@ -0,0 +1,46 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import { setContext } from 'svelte';
+
+  interface Props {
+    value?: string;
+    name?: string;
+    disabled?: boolean;
+    orientation?: 'horizontal' | 'vertical';
+    class?: string;
+    children?: Snippet;
+    onchange?: (value: string) => void;
+  }
+
+  let {
+    value = $bindable(''),
+    name = '',
+    disabled = false,
+    orientation = 'vertical',
+    class: className = '',
+    children,
+    onchange,
+  }: Props = $props();
+
+  setContext('radio-group', {
+    get value() { return value; },
+    get name() { return name; },
+    get disabled() { return disabled; },
+    select: (v: string) => {
+      value = v;
+      onchange?.(v);
+    },
+  });
+</script>
+
+<div
+  role="radiogroup"
+  class={cn(
+    'flex gap-2',
+    orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap',
+    className
+  )}
+>
+  {#if children}{@render children()}{/if}
+</div>

+ 62 - 0
src/lib/components/primitives/RadioItem.svelte

@@ -0,0 +1,62 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import { getContext } from 'svelte';
+
+  interface RadioGroupContext {
+    value: string;
+    name: string;
+    disabled: boolean;
+    select: (v: string) => void;
+  }
+
+  interface Props {
+    value: string;
+    disabled?: boolean;
+    id?: string;
+    class?: string;
+    children?: Snippet;
+  }
+
+  let { value, disabled: localDisabled = false, id, class: className = '', children }: Props = $props();
+
+  const ctx = getContext<RadioGroupContext>('radio-group');
+
+  let checked = $derived(ctx.value === value);
+  let isDisabled = $derived(ctx.disabled || localDisabled);
+</script>
+
+<label
+  class={cn(
+    'flex items-center gap-2 cursor-pointer',
+    isDisabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
+    className
+  )}
+>
+  <input
+    type="radio"
+    {id}
+    name={ctx.name}
+    {value}
+    {checked}
+    disabled={isDisabled}
+    onchange={() => ctx.select(value)}
+    class="sr-only peer"
+  />
+  <div
+    class={cn(
+      'h-4 w-4 rounded-full border border-[var(--border)]',
+      'flex items-center justify-center',
+      'transition-all duration-[150ms]',
+      'peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--ring)] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[var(--background)]',
+      checked ? 'border-[var(--primary)]' : '',
+    )}
+  >
+    {#if checked}
+      <div class="h-2 w-2 rounded-full bg-[var(--primary)]"></div>
+    {/if}
+  </div>
+  {#if children}
+    <span class="text-sm text-[var(--foreground)]">{@render children()}</span>
+  {/if}
+</label>

+ 51 - 0
src/lib/components/primitives/Select.svelte

@@ -0,0 +1,51 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    value?: string;
+    disabled?: boolean;
+    id?: string;
+    class?: string;
+    error?: boolean;
+    children?: Snippet;
+    onchange?: (value: string) => void;
+  }
+
+  let {
+    value = $bindable(''),
+    disabled = false,
+    id,
+    class: className = '',
+    error = false,
+    children,
+    onchange,
+  }: Props = $props();
+</script>
+
+<div class={cn('relative w-full', className)}>
+  <select
+    {id}
+    {disabled}
+    bind:value
+    onchange={(e) => onchange?.((e.target as HTMLSelectElement).value)}
+    class={cn(
+      'flex h-9 w-full appearance-none rounded-[var(--radius)] border bg-transparent px-3 py-1 pr-8 text-sm',
+      'text-[var(--foreground)]',
+      'transition-colors duration-[150ms]',
+      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-0',
+      'cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
+      error
+        ? 'border-[var(--destructive)] focus-visible:ring-[var(--destructive)]'
+        : 'border-[var(--border)] focus-visible:border-[var(--ring)]'
+    )}
+  >
+    {#if children}{@render children()}{/if}
+  </select>
+  <!-- Chevron icon -->
+  <div class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-[var(--muted-foreground)]">
+    <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
+      <path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    </svg>
+  </div>
+</div>

+ 20 - 0
src/lib/components/primitives/Separator.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    orientation?: 'horizontal' | 'vertical';
+    class?: string;
+  }
+
+  let { orientation = 'horizontal', class: className = '' }: Props = $props();
+</script>
+
+<div
+  role="separator"
+  aria-orientation={orientation}
+  class={cn(
+    'shrink-0 bg-[var(--border)]',
+    orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
+    className
+  )}
+></div>

+ 70 - 0
src/lib/components/primitives/Slider.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    value?: number;
+    min?: number;
+    max?: number;
+    step?: number;
+    disabled?: boolean;
+    id?: string;
+    class?: string;
+    onchange?: (value: number) => void;
+  }
+
+  let {
+    value = $bindable(0),
+    min = 0,
+    max = 100,
+    step = 1,
+    disabled = false,
+    id,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  let dragging = $state(false);
+  let percent = $derived(((value - min) / (max - min)) * 100);
+</script>
+
+<div class={cn('relative flex w-full touch-none select-none items-center h-5', className)}>
+  <!-- Track -->
+  <div class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-[var(--muted)]">
+    <!-- Fill — no transition, must track pointer instantly -->
+    <div
+      class="absolute h-full bg-[var(--primary)]"
+      style:width="{percent}%"
+    ></div>
+  </div>
+
+  <!-- Native input — invisible, drives the value -->
+  <input
+    type="range"
+    {id}
+    {min}
+    {max}
+    {step}
+    {disabled}
+    bind:value
+    oninput={() => onchange?.(value)}
+    onpointerdown={() => { dragging = true; }}
+    onpointerup={() => { dragging = false; }}
+    class={cn(
+      'absolute inset-0 h-full w-full opacity-0',
+      disabled ? 'cursor-not-allowed' : 'cursor-pointer'
+    )}
+  />
+
+  <!-- Visual thumb — position is immediate, only scale animates on drag -->
+  <div
+    class={cn(
+      'pointer-events-none absolute h-4 w-4 rounded-full',
+      'border-2 border-[var(--primary)] bg-[var(--background)]',
+      'shadow-[0_1px_3px_rgba(0,0,0,0.2)]',
+      'transition-transform duration-[100ms]',
+      dragging ? 'scale-125' : 'scale-100',
+      disabled && 'opacity-40'
+    )}
+    style:left="calc({percent}% - 8px)"
+  ></div>
+</div>

+ 60 - 0
src/lib/components/primitives/Switch.svelte

@@ -0,0 +1,60 @@
+<script lang="ts">
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props {
+    checked?: boolean;
+    disabled?: boolean;
+    id?: string;
+    class?: string;
+    onchange?: (checked: boolean) => void;
+  }
+
+  let {
+    checked = $bindable(false),
+    disabled = false,
+    id,
+    class: className = '',
+    onchange,
+  }: Props = $props();
+
+  function toggle() {
+    if (disabled) return;
+    checked = !checked;
+    onchange?.(checked);
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === ' ' || e.key === 'Enter') {
+      e.preventDefault();
+      toggle();
+    }
+  }
+</script>
+
+<button
+  type="button"
+  role="switch"
+  aria-checked={checked}
+  {id}
+  {disabled}
+  onclick={toggle}
+  onkeydown={handleKeydown}
+  class={cn(
+    'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center',
+    'rounded-[9999px] border-2 border-transparent',
+    'transition-colors duration-[200ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]',
+    'cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
+    checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]',
+    className
+  )}
+  aria-label={id ? undefined : 'Toggle'}
+>
+  <span
+    class={cn(
+      'pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm',
+      'transition-transform duration-[200ms]',
+      checked ? 'translate-x-4' : 'translate-x-0'
+    )}
+  ></span>
+</button>

+ 26 - 0
src/lib/components/primitives/Textarea.svelte

@@ -0,0 +1,26 @@
+<script lang="ts">
+  import type { HTMLTextareaAttributes } from 'svelte/elements';
+  import { cn } from '$lib/utils/cn.js';
+
+  interface Props extends HTMLTextareaAttributes {
+    class?: string;
+    error?: boolean;
+  }
+
+  let { class: className = '', error = false, ...rest }: Props = $props();
+</script>
+
+<textarea
+  class={cn(
+    'flex min-h-[80px] w-full rounded-[var(--radius)] border bg-transparent px-3 py-2 text-sm',
+    'text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]',
+    'resize-y transition-colors duration-[150ms]',
+    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-0',
+    'disabled:cursor-not-allowed disabled:opacity-40',
+    error
+      ? 'border-[var(--destructive)] focus-visible:ring-[var(--destructive)]'
+      : 'border-[var(--border)] focus-visible:border-[var(--ring)]',
+    className
+  )}
+  {...rest}
+></textarea>

+ 78 - 0
src/lib/index.ts

@@ -0,0 +1,78 @@
+// Primitives
+export { default as Button }     from './components/primitives/Button.svelte';
+export { default as Input }      from './components/primitives/Input.svelte';
+export { default as Textarea }   from './components/primitives/Textarea.svelte';
+export { default as Label }      from './components/primitives/Label.svelte';
+export { default as Checkbox }   from './components/primitives/Checkbox.svelte';
+export { default as Switch }     from './components/primitives/Switch.svelte';
+export { default as Badge }      from './components/primitives/Badge.svelte';
+export { default as Separator }  from './components/primitives/Separator.svelte';
+export { default as Avatar }     from './components/primitives/Avatar.svelte';
+export { default as Slider }     from './components/primitives/Slider.svelte';
+export { default as Select }     from './components/primitives/Select.svelte';
+export { default as RadioGroup } from './components/primitives/RadioGroup.svelte';
+export { default as RadioItem }  from './components/primitives/RadioItem.svelte';
+
+// Layout
+export { default as Card }             from './components/layout/Card.svelte';
+export { default as CardHeader }       from './components/layout/CardHeader.svelte';
+export { default as CardTitle }        from './components/layout/CardTitle.svelte';
+export { default as CardDescription }  from './components/layout/CardDescription.svelte';
+export { default as CardContent }      from './components/layout/CardContent.svelte';
+export { default as CardFooter }       from './components/layout/CardFooter.svelte';
+export { default as Page }             from './components/layout/Page.svelte';
+export { default as PageHeader }       from './components/layout/PageHeader.svelte';
+export { default as Tabs }             from './components/layout/Tabs.svelte';
+export { default as Breadcrumbs }      from './components/layout/Breadcrumbs.svelte';
+
+// Navigation
+export { default as Navbar }      from './components/navigation/Navbar.svelte';
+export { default as NavItem }     from './components/navigation/NavItem.svelte';
+export { default as ThemeToggle } from './components/navigation/ThemeToggle.svelte';
+
+// Overlay & Feedback
+export { default as Dialog }       from './components/overlay/Dialog.svelte';
+export { default as AlertDialog }  from './components/overlay/AlertDialog.svelte';
+export { default as Alert }        from './components/overlay/Alert.svelte';
+export { default as Toaster }      from './components/overlay/Toaster.svelte';
+export { default as Tooltip }      from './components/overlay/Tooltip.svelte';
+export { default as DropdownMenu } from './components/overlay/DropdownMenu.svelte';
+export { default as Progress }     from './components/overlay/Progress.svelte';
+export { default as Skeleton }     from './components/overlay/Skeleton.svelte';
+
+// Layout (continued)
+export { default as ScrollArea } from './components/layout/ScrollArea.svelte';
+
+// Navigation (continued)
+export { default as CommandPalette } from './components/navigation/CommandPalette.svelte';
+export { default as Sidebar        } from './components/navigation/Sidebar.svelte';
+export { default as SidebarItem    } from './components/navigation/SidebarItem.svelte';
+export { default as SidebarSection } from './components/navigation/SidebarSection.svelte';
+
+// Overlay (continued)
+export { default as Sheet }   from './components/overlay/Sheet.svelte';
+export { default as Popover } from './components/overlay/Popover.svelte';
+
+// Forms
+export { default as FormField   } from './components/forms/FormField.svelte';
+export { default as SearchInput } from './components/forms/SearchInput.svelte';
+export { default as FileUpload  } from './components/forms/FileUpload.svelte';
+export { default as Combobox    } from './components/forms/Combobox.svelte';
+export { default as MultiSelect } from './components/forms/MultiSelect.svelte';
+export { default as DatePicker  } from './components/forms/DatePicker.svelte';
+
+// Data Display
+export { default as Table      } from './components/data/Table.svelte';
+export { default as CodeBlock  } from './components/data/CodeBlock.svelte';
+export { default as MetricCard } from './components/data/MetricCard.svelte';
+export { default as EmptyState } from './components/data/EmptyState.svelte';
+export { default as KeyValue   } from './components/data/KeyValue.svelte';
+export { default as DataTable  } from './components/data/DataTable.svelte';
+export { default as JsonViewer } from './components/data/JsonViewer.svelte';
+export { default as KanbanBoard  } from './components/data/KanbanBoard.svelte';
+export { default as KanbanColumn } from './components/data/KanbanColumn.svelte';
+export { default as KanbanCard   } from './components/data/KanbanCard.svelte';
+
+// Utils
+export { cn }    from './utils/cn.js';
+export { toast } from './utils/toast.svelte.js';

+ 8 - 0
src/lib/utils/cn.ts

@@ -0,0 +1,8 @@
+type ClassValue = string | undefined | null | false | ClassValue[];
+
+export function cn(...classes: ClassValue[]): string {
+  return classes
+    .flat(Infinity as 1)
+    .filter(Boolean)
+    .join(' ');
+}

+ 43 - 0
src/lib/utils/toast.svelte.ts

@@ -0,0 +1,43 @@
+export type ToastVariant = 'default' | 'success' | 'warning' | 'error';
+
+export interface Toast {
+  id: string;
+  message: string;
+  description?: string;
+  variant: ToastVariant;
+  duration: number;
+}
+
+function createToastStore() {
+  let toasts = $state<Toast[]>([]);
+
+  function add(message: string, opts: Partial<Omit<Toast, 'id' | 'message'>> = {}) {
+    const id = Math.random().toString(36).slice(2);
+    const toast: Toast = {
+      id,
+      message,
+      variant: opts.variant ?? 'default',
+      duration: opts.duration ?? 4000,
+      description: opts.description,
+    };
+    toasts = [...toasts, toast];
+
+    setTimeout(() => remove(id), toast.duration);
+    return id;
+  }
+
+  function remove(id: string) {
+    toasts = toasts.filter(t => t.id !== id);
+  }
+
+  return {
+    get toasts() { return toasts; },
+    add,
+    remove,
+    success: (msg: string, opts?: Partial<Toast>) => add(msg, { ...opts, variant: 'success' }),
+    error:   (msg: string, opts?: Partial<Toast>) => add(msg, { ...opts, variant: 'error' }),
+    warning: (msg: string, opts?: Partial<Toast>) => add(msg, { ...opts, variant: 'warning' }),
+  };
+}
+
+export const toast = createToastStore();

+ 133 - 0
src/routes/+layout.svelte

@@ -0,0 +1,133 @@
+<script lang="ts">
+  import '../app.css';
+  import favicon from '$lib/assets/favicon.svg';
+  import type { Snippet } from 'svelte';
+  import { page } from '$app/state';
+  import { cn } from '$lib/utils/cn.js';
+  import ThemeToggle from '$lib/components/navigation/ThemeToggle.svelte';
+  import Toaster from '$lib/components/overlay/Toaster.svelte';
+
+  interface Props { children?: Snippet; }
+  let { children }: Props = $props();
+
+  let mobileOpen = $state(false);
+
+  const nav = [
+    { label: 'Getting started', href: '/',           icon: 'start' },
+    { section: 'Components' },
+    { label: 'Primitives',      href: '/primitives', icon: 'prim' },
+    { label: 'Layout',          href: '/layout',     icon: 'layout' },
+    { label: 'Navigation',      href: '/navigation', icon: 'nav' },
+    { label: 'Overlay',         href: '/overlay',    icon: 'overlay' },
+    { label: 'Forms',           href: '/forms',      icon: 'forms' },
+    { label: 'Data',            href: '/data',       icon: 'data' },
+  ];
+
+  const icons: Record<string, string> = {
+    start:   'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5',
+    prim:    'M12 2H2v10h10V2zM22 2h-6v6h6V2zM22 14h-6v6h6v-6zM16 14H6v6h10v-6z',
+    layout:  'M3 3h18v4H3zM3 10h8v11H3zM14 10h7v5h-7zM14 18h7v3h-7z',
+    nav:     'M3 12h18M3 6h18M3 18h18',
+    overlay: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
+    forms:   'M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
+    data:    'M3 3h18v18H3zM3 9h18M3 15h18M9 3v18',
+  };
+
+  function isActive(href: string) {
+    return href === '/' ? page.url.pathname === '/' : page.url.pathname.startsWith(href);
+  }
+</script>
+
+<svelte:head>
+  <link rel="icon" href={favicon} />
+</svelte:head>
+
+<Toaster />
+
+<div class="flex min-h-screen bg-[var(--background)]">
+  <!-- Mobile overlay -->
+  {#if mobileOpen}
+    <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
+    <div
+      class="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm lg:hidden"
+      onclick={() => { mobileOpen = false; }}
+    ></div>
+  {/if}
+
+  <!-- Sidebar -->
+  <aside
+    class={cn(
+      'fixed inset-y-0 left-0 z-50 flex w-[220px] flex-col',
+      'border-r border-[var(--border)] bg-[var(--card)]',
+      'transition-transform duration-[200ms] ease-[cubic-bezier(0.16,1,0.3,1)]',
+      'lg:sticky lg:top-0 lg:h-screen lg:translate-x-0',
+      mobileOpen ? 'translate-x-0' : '-translate-x-full'
+    )}
+  >
+    <!-- Logo -->
+    <div class="flex h-14 shrink-0 items-center border-b border-[var(--border)] px-4">
+      <a href="/" class="flex items-center gap-2 text-sm font-semibold text-[var(--foreground)] hover:text-[var(--primary)] transition-colors">
+        <span class="text-base">🫒</span> olive
+      </a>
+    </div>
+
+    <!-- Nav -->
+    <nav class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
+      {#each nav as item}
+        {#if 'section' in item}
+          <p class="mb-1 mt-3 px-2.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--muted-foreground)]/60 first:mt-1">{item.section}</p>
+        {:else if item.href}
+          {@const active = isActive(item.href)}
+          <a
+            href={item.href}
+            onclick={() => { mobileOpen = false; }}
+            class={cn(
+              'flex items-center gap-3 rounded-[var(--radius)] px-2.5 py-2 text-sm font-medium',
+              'transition-colors duration-[100ms]',
+              active
+                ? 'bg-[var(--primary)]/10 text-[var(--primary)]'
+                : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]'
+            )}
+          >
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="shrink-0">
+              <path d={icons[item.icon]}/>
+            </svg>
+            {item.label}
+          </a>
+        {/if}
+      {/each}
+    </nav>
+
+    <!-- Footer -->
+    <div class="flex shrink-0 items-center justify-between border-t border-[var(--border)] px-3 py-2.5">
+      <span class="text-xs text-[var(--muted-foreground)]">v0.1.0</span>
+      <ThemeToggle />
+    </div>
+  </aside>
+
+  <!-- Main -->
+  <div class="flex min-w-0 flex-1 flex-col">
+    <!-- Mobile topbar -->
+    <div class="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-[var(--border)] bg-[var(--card)]/80 px-4 backdrop-blur-xl lg:hidden">
+      <button
+        type="button"
+        onclick={() => { mobileOpen = !mobileOpen; }}
+        aria-label="Toggle navigation"
+        class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-[var(--radius)] text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
+      >
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+          {#if mobileOpen}
+            <path d="M18 6L6 18M6 6l12 12"/>
+          {:else}
+            <path d="M4 6h16M4 12h16M4 18h16"/>
+          {/if}
+        </svg>
+      </button>
+      <span class="text-sm font-semibold text-[var(--foreground)]">🫒 olive</span>
+    </div>
+
+    <main class="flex-1">
+      {#if children}{@render children()}{/if}
+    </main>
+  </div>
+</div>

+ 180 - 0
src/routes/+page.svelte

@@ -0,0 +1,180 @@
+<script lang="ts">
+  import CodeBlock from '$lib/components/data/CodeBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+
+  <!-- Hero -->
+  <div class="mb-10">
+    <div class="mb-3 flex items-center gap-2">
+      <span class="text-2xl">🫒</span>
+      <h1 class="text-2xl font-semibold text-[var(--foreground)]">olive</h1>
+    </div>
+    <p class="text-base text-[var(--muted-foreground)] max-w-xl leading-relaxed">
+      A calm, precise component library built with Svelte 5 and Tailwind v4.
+      Designed for data-dense interfaces that need to feel trustworthy.
+    </p>
+  </div>
+
+  <Separator />
+
+  <!-- Installation -->
+  <section class="mt-10 flex flex-col gap-6">
+    <div>
+      <h2 class="text-lg font-semibold text-[var(--foreground)]">Installation</h2>
+      <p class="mt-1 text-sm text-[var(--muted-foreground)]">Add olive to any SvelteKit project.</p>
+    </div>
+
+    <div class="flex flex-col gap-4">
+      <div>
+        <p class="mb-2 text-sm font-medium text-[var(--foreground)]">1. Install the package</p>
+        <CodeBlock code={`npm install olive-ui`} language="sh" />
+      </div>
+
+      <div>
+        <p class="mb-2 text-sm font-medium text-[var(--foreground)]">2. Import the styles in your <code class="rounded bg-[var(--muted)] px-1 py-0.5 text-xs font-mono">app.css</code></p>
+        <CodeBlock language="css" code={`@import 'tailwindcss';
+@import 'olive-ui/styles';`} />
+      </div>
+
+      <div>
+        <p class="mb-2 text-sm font-medium text-[var(--foreground)]">3. Add the theme init script to <code class="rounded bg-[var(--muted)] px-1 py-0.5 text-xs font-mono">app.html</code> to prevent flash-of-wrong-theme</p>
+        <CodeBlock language="html" code={`<head>
+  <!-- prevent FOUC -->
+  <script>
+    (function(){
+      var s = localStorage.getItem('olive-theme');
+      var d = window.matchMedia('(prefers-color-scheme: dark)').matches;
+      if (s === 'dark' || (s !== 'light' && d))
+        document.documentElement.classList.add('dark');
+    })();
+  <\/script>
+</head>`} />
+      </div>
+
+      <div>
+        <p class="mb-2 text-sm font-medium text-[var(--foreground)]">4. Use components</p>
+        <CodeBlock language="svelte" code={`<script>
+  import { Button, Input, Card } from 'olive-ui';
+<\/script>
+
+<Card>
+  <Input placeholder="Database name" />
+  <Button>Create</Button>
+</Card>`} />
+      </div>
+    </div>
+  </section>
+
+  <Separator class="mt-10" />
+
+  <!-- Theming -->
+  <section class="mt-10 flex flex-col gap-6">
+    <div>
+      <h2 class="text-lg font-semibold text-[var(--foreground)]">Theming</h2>
+      <p class="mt-1 text-sm text-[var(--muted-foreground)]">
+        All tokens are CSS custom properties. Override any of them to match your brand.
+      </p>
+    </div>
+
+    <CodeBlock language="css" code={`/* In your app.css, after @import 'olive-ui/styles' */
+:root {
+  --primary: hsl(160 84% 39%);   /* your brand green */
+  --radius: 10px;                 /* border radius for cards */
+  --radius-button: 6px;           /* border radius for buttons */
+}
+
+.dark {
+  --primary: hsl(160 70% 50%);
+}`} />
+
+    <div class="rounded-[var(--radius)] border border-[var(--border)] bg-[var(--muted)]/30 p-4">
+      <p class="text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)] mb-3">Token reference</p>
+      <div class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-xs font-mono">
+        {#each [
+          ['--background', 'Page background'],
+          ['--foreground', 'Default text'],
+          ['--card', 'Card / panel background'],
+          ['--border', 'Borders and dividers'],
+          ['--muted', 'Subtle backgrounds'],
+          ['--muted-foreground', 'Secondary text'],
+          ['--primary', 'Brand / accent color'],
+          ['--primary-foreground', 'Text on primary bg'],
+          ['--destructive', 'Error / delete actions'],
+          ['--ring', 'Focus ring color'],
+          ['--radius', 'Card border radius'],
+          ['--radius-button', 'Button border radius'],
+        ] as [token, desc]}
+          <span class="text-[var(--primary)]/80">{token}</span>
+          <span class="text-[var(--muted-foreground)]">{desc}</span>
+        {/each}
+      </div>
+    </div>
+  </section>
+
+  <Separator class="mt-10" />
+
+  <!-- AI Guidelines -->
+  <section class="mt-10 flex flex-col gap-4">
+    <div>
+      <h2 class="text-lg font-semibold text-[var(--foreground)]">AI guidelines</h2>
+      <p class="mt-1 text-sm text-[var(--muted-foreground)]">Paste this into your AI assistant (Cursor, Claude, Copilot, etc.) so it understands how to work with olive.</p>
+    </div>
+    <CodeBlock code={`You are working with olive, a Svelte 5 component library built with Tailwind v4.
+
+Import components from 'olive-ui':
+  import { Button, Input, Card } from 'olive-ui';
+
+Available components:
+  Primitives:  Button, Input, Textarea, Label, Checkbox, Switch, Badge,
+               Separator, Avatar, Slider, Select, RadioGroup, RadioItem
+  Layout:      Card, CardHeader, CardTitle, CardDescription, CardContent,
+               CardFooter, Tabs, Breadcrumbs, ScrollArea
+  Navigation:  Navbar, NavItem, ThemeToggle, CommandPalette,
+               Sidebar, SidebarItem, SidebarSection
+  Overlay:     Dialog, AlertDialog, Alert, Toaster, Tooltip,
+               DropdownMenu, Progress, Skeleton, Sheet, Popover
+  Forms:       FormField, SearchInput, FileUpload, Combobox, MultiSelect, DatePicker
+  Data:        Table, DataTable, CodeBlock, MetricCard, EmptyState,
+               KeyValue, JsonViewer, KanbanBoard, KanbanColumn, KanbanCard
+
+Design principles:
+  - Calm precision: every element earns its place, no decorative noise
+  - Warmth without whimsy: approachable but never playful
+  - Trust through honesty: states always visible, errors always clear
+
+Conventions:
+  - Svelte 5 runes: $state, $derived, $props, $bindable, snippets
+  - Design tokens are CSS custom properties: --primary, --foreground,
+    --border, --muted, --card, --radius, --radius-button, etc.
+  - Use cn() from 'olive-ui' to merge conditional classes
+  - Wrap inputs in FormField for consistent error/hint display
+  - Mount <Toaster /> once in the root layout; call toast.add(),
+    toast.success(), toast.warning(), toast.error() anywhere
+  - Dark mode is controlled by the .dark class on <html>,
+    toggled by ThemeToggle which persists to localStorage
+  - Prefer component props and composition over custom CSS`} />
+  </section>
+
+  <Separator class="mt-10" />
+
+  <!-- Design principles -->
+  <section class="mt-10 flex flex-col gap-4">
+    <h2 class="text-lg font-semibold text-[var(--foreground)]">Design principles</h2>
+    <div class="grid gap-3 sm:grid-cols-3">
+      {#each [
+        { title: 'Calm precision', body: 'Every element earns its place. No decorative noise — white space is load-bearing.' },
+        { title: 'Warmth without whimsy', body: 'Approachable but never playful. Tools that feel like they were made by experts, for experts.' },
+        { title: 'Trust through honesty', body: 'States are always visible. Errors are clear. Nothing hides behind ambiguity.' },
+      ] as p}
+        <div class="rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] p-4">
+          <p class="text-sm font-semibold text-[var(--foreground)] mb-1">{p.title}</p>
+          <p class="text-xs text-[var(--muted-foreground)] leading-relaxed">{p.body}</p>
+        </div>
+      {/each}
+    </div>
+  </section>
+
+  <div class="h-16"></div>
+</div>

+ 103 - 0
src/routes/DocBlock.svelte

@@ -0,0 +1,103 @@
+<script lang="ts">
+  import type { Snippet } from 'svelte';
+  import { cn } from '$lib/utils/cn.js';
+  import CodeBlock from '$lib/components/data/CodeBlock.svelte';
+
+  interface Prop {
+    name: string;
+    type: string;
+    default?: string;
+    description: string;
+  }
+
+  interface Props {
+    name: string;
+    description?: string;
+    code: string;
+    props?: Prop[];
+    preview?: Snippet;
+    class?: string;
+  }
+
+  let {
+    name,
+    description,
+    code,
+    props = [],
+    preview,
+    class: className = '',
+  }: Props = $props();
+
+  let tab = $state<'preview' | 'code' | 'props'>('preview');
+  const tabs = ['preview', 'code', ...(props.length ? ['props'] : [])] as const;
+</script>
+
+<div id={name.toLowerCase().replace(/\s+/g, '-')} class={cn('scroll-mt-20', className)}>
+  <!-- Title -->
+  <div class="mb-4">
+    <h3 class="text-base font-semibold text-[var(--foreground)]">{name}</h3>
+    {#if description}
+      <p class="mt-0.5 text-sm text-[var(--muted-foreground)]">{description}</p>
+    {/if}
+  </div>
+
+  <!-- Unified card -->
+  <div class="overflow-hidden rounded-[var(--radius)] border border-[var(--border)]">
+
+    <!-- Tab strip -->
+    <div class="flex items-center gap-0 border-b border-[var(--border)] bg-[var(--card)] px-1">
+      {#each tabs as t}
+        <button
+          type="button"
+          onclick={() => tab = t as typeof tab}
+          class={cn(
+            'relative px-3 py-2.5 text-xs font-medium cursor-pointer capitalize transition-colors duration-[100ms]',
+            'focus-visible:outline-none',
+            tab === t
+              ? 'text-[var(--foreground)]'
+              : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
+          )}
+        >
+          {t}
+          {#if tab === t}
+            <span class="absolute inset-x-0 bottom-0 h-px bg-[var(--foreground)]"></span>
+          {/if}
+        </button>
+      {/each}
+    </div>
+
+    <!-- Panel -->
+    {#if tab === 'preview'}
+      <div class="flex min-h-[120px] flex-wrap items-center gap-4 bg-[var(--background)] p-6">
+        {#if preview}
+          {@render preview()}
+        {/if}
+      </div>
+    {:else if tab === 'code'}
+      <CodeBlock {code} language="svelte" class="rounded-none border-0 shadow-none" />
+    {:else}
+      <div class="overflow-x-auto">
+        <table class="w-full text-sm">
+          <thead>
+            <tr class="border-b border-[var(--border)] bg-[var(--muted)]/40">
+              <th class="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">Prop</th>
+              <th class="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">Type</th>
+              <th class="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">Default</th>
+              <th class="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">Description</th>
+            </tr>
+          </thead>
+          <tbody>
+            {#each props as p, i}
+              <tr class={cn('border-b border-[var(--border)] last:border-0', i % 2 === 1 && 'bg-[var(--muted)]/10')}>
+                <td class="whitespace-nowrap px-4 py-2.5 font-mono text-xs text-[var(--foreground)]">{p.name}</td>
+                <td class="px-4 py-2.5 font-mono text-xs text-[var(--primary)]/80">{p.type}</td>
+                <td class="whitespace-nowrap px-4 py-2.5 font-mono text-xs text-[var(--muted-foreground)]">{p.default ?? '—'}</td>
+                <td class="px-4 py-2.5 text-xs text-[var(--foreground)]">{p.description}</td>
+              </tr>
+            {/each}
+          </tbody>
+        </table>
+      </div>
+    {/if}
+  </div>
+</div>

+ 345 - 0
src/routes/data/+page.svelte

@@ -0,0 +1,345 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+  import Card from '$lib/components/layout/Card.svelte';
+  import CardContent from '$lib/components/layout/CardContent.svelte';
+  import Badge from '$lib/components/primitives/Badge.svelte';
+  import Button from '$lib/components/primitives/Button.svelte';
+  import { toast } from '$lib/utils/toast.svelte.js';
+
+  import Table      from '$lib/components/data/Table.svelte';
+  import CodeBlock  from '$lib/components/data/CodeBlock.svelte';
+  import MetricCard from '$lib/components/data/MetricCard.svelte';
+  import EmptyState from '$lib/components/data/EmptyState.svelte';
+  import KeyValue   from '$lib/components/data/KeyValue.svelte';
+  import DataTable  from '$lib/components/data/DataTable.svelte';
+  import JsonViewer from '$lib/components/data/JsonViewer.svelte';
+  import KanbanBoard  from '$lib/components/data/KanbanBoard.svelte';
+  import KanbanColumn from '$lib/components/data/KanbanColumn.svelte';
+  import KanbanCard   from '$lib/components/data/KanbanCard.svelte';
+
+  const tableColumns = [
+    { key: 'name',   label: 'Name' },
+    { key: 'type',   label: 'Type', mono: true },
+    { key: 'status', label: 'Status' },
+  ];
+
+  const tableRows = [
+    { name: 'production', type: 'postgresql', status: 'active' },
+    { name: 'staging',    type: 'postgresql', status: 'paused' },
+    { name: 'dev',        type: 'pizzasql',   status: 'error' },
+  ];
+
+  const dtColumns = [
+    { key: 'name' as const,   header: 'Name',   sortable: true },
+    { key: 'region' as const, header: 'Region', sortable: true },
+    { key: 'size' as const,   header: 'Size',   sortable: true },
+  ];
+
+  const dtRows = [
+    { name: 'production',  region: 'us-east-1',  size: '2.4 GB' },
+    { name: 'staging',     region: 'us-west-2',  size: '340 MB' },
+    { name: 'dev',         region: 'local',       size: '18 MB' },
+    { name: 'analytics',   region: 'eu-west-1',  size: '6.7 GB' },
+    { name: 'archive',     region: 'us-east-1',  size: '12.1 GB' },
+    { name: 'replica-01',  region: 'us-west-2',  size: '2.4 GB' },
+  ];
+
+  const kvItems = [
+    { key: 'Host',     value: 'db.example.com', mono: true, copyable: true },
+    { key: 'Port',     value: '5432',            mono: true },
+    { key: 'Database', value: 'production',      mono: true, copyable: true },
+    { key: 'SSL',      value: 'required' },
+  ];
+
+  const sampleJson = {
+    id: 'db_abc123',
+    name: 'production',
+    config: { region: 'us-east-1', ssl: true, pool: { min: 2, max: 20 } },
+    tags: ['prod', 'critical'],
+    notes: null,
+  };
+
+  type CardData = { id: string; title: string; description?: string; tags?: string[]; column: string };
+
+  let kanbanCards = $state<CardData[]>([
+    { id: 'c1', title: 'Set up replication',    tags: ['infra'], column: 'todo' },
+    { id: 'c2', title: 'Optimize slow queries',  tags: ['perf'],  column: 'todo' },
+    { id: 'c3', title: 'Write migration script', description: 'v14 → v15', tags: ['db'], column: 'in-progress' },
+    { id: 'c4', title: 'SSL cert renewal',       tags: ['ops'],   column: 'done' },
+  ]);
+
+  const cols = [
+    { id: 'todo', title: 'To do' },
+    { id: 'in-progress', title: 'In progress' },
+    { id: 'done', title: 'Done' },
+  ];
+
+  function moveCard(cardId: string, toCol: string) {
+    kanbanCards = kanbanCards.map(c => c.id === cardId ? { ...c, column: toCol } : c);
+  }
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Data Display</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">Tables, boards, viewers, and metadata components.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="Table"
+      description="Simple static table. For sortable, searchable, paginated data use DataTable."
+      code={`<Table
+  columns={[
+    { key: 'name',   label: 'Name' },
+    { key: 'type',   label: 'Type', mono: true },
+    { key: 'status', label: 'Status' },
+    { key: 'size',   label: 'Size', align: 'right' },
+  ]}
+  rows={[
+    { name: 'production', type: 'postgresql', status: 'active', size: '2.4 GB' },
+  ]}
+/>`}
+      props={[
+        { name: 'columns', type: '{ key: string; label: string; mono?: boolean; align?: "left" | "right" }[]', description: 'Column definitions' },
+        { name: 'rows', type: 'Record<string, unknown>[]', description: 'Table data' },
+      ]}
+    >
+      {#snippet preview()}
+        <Table columns={tableColumns} rows={tableRows} class="w-full" />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="DataTable"
+      description="Full-featured table with client-side sorting, search filtering, and pagination."
+      code={`<DataTable
+  data={rows}
+  columns={[
+    { key: 'name',   header: 'Name',   sortable: true },
+    { key: 'region', header: 'Region', sortable: true },
+    { key: 'size',   header: 'Size',   sortable: true },
+  ]}
+  searchable
+  searchPlaceholder="Search databases…"
+  pageSize={5}
+/>`}
+      props={[
+        { name: 'data', type: 'T[]', description: 'Array of row objects' },
+        { name: 'columns', type: '{ key: keyof T; header: string; sortable?: boolean; width?: string; render?: fn }[]', description: 'Column definitions' },
+        { name: 'pageSize', type: 'number', default: '10', description: 'Rows per page' },
+        { name: 'searchable', type: 'boolean', default: 'false', description: 'Shows a search input above the table' },
+        { name: 'searchPlaceholder', type: 'string', default: "'Search…'", description: 'Search input placeholder' },
+        { name: 'empty', type: 'string', default: "'No results'", description: 'Empty state message' },
+      ]}
+    >
+      {#snippet preview()}
+        <DataTable data={dtRows} columns={dtColumns} searchable pageSize={3} class="w-full" />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="MetricCard"
+      description="Key figure with label, trend indicator, icon slot, and semantic color options."
+      code={`<MetricCard
+  label="Active queries"
+  value="248"
+  color="emerald"
+  trend={{ value: 12, label: 'vs last hour' }}
+>
+  {#snippet icon()}
+    <ActivityIcon />
+  {/snippet}
+</MetricCard>`}
+      props={[
+        { name: 'label', type: 'string', description: 'Metric name' },
+        { name: 'value', type: 'string | number', description: 'Primary value to display' },
+        { name: 'color', type: "'olive' | 'emerald' | 'sky' | 'amber' | 'red' | 'purple'", default: "'olive'", description: 'Accent color for the icon and trend' },
+        { name: 'trend', type: '{ value: number; label: string }', description: 'Signed delta and description. Positive = green, negative = red.' },
+        { name: 'icon', type: 'Snippet', description: 'SVG icon displayed in the colored badge' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="grid grid-cols-2 gap-3 w-full max-w-sm">
+          <MetricCard label="Databases" value="12" color="olive">
+            {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/></svg>{/snippet}
+          </MetricCard>
+          <MetricCard label="Error rate" value="0.04%" color="red" trend={{ value: -8, label: 'vs yesterday' }}>
+            {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>{/snippet}
+          </MetricCard>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="KeyValue"
+      description="Metadata pair list. Supports monospace values and copyable items."
+      code={`<KeyValue
+  items={[
+    { key: 'Host',     value: 'db.example.com', mono: true, copyable: true },
+    { key: 'Port',     value: '5432',            mono: true },
+    { key: 'Database', value: 'production',      mono: true, copyable: true },
+    { key: 'SSL',      value: 'required' },
+  ]}
+/>`}
+      props={[
+        { name: 'items', type: '{ key: string; value: string; mono?: boolean; copyable?: boolean }[]', description: 'Pairs to display' },
+      ]}
+    >
+      {#snippet preview()}
+        <Card class="max-w-xs w-full">
+          <CardContent>
+            <KeyValue items={kvItems} />
+          </CardContent>
+        </Card>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="CodeBlock"
+      description="Monospaced display with language label and one-click copy."
+      code={`<CodeBlock
+  language="sql"
+  code={\`SELECT * FROM users
+WHERE created_at > '2026-01-01'
+ORDER BY name;\`}
+/>`}
+      props={[
+        { name: 'code', type: 'string', description: 'The code string to display' },
+        { name: 'language', type: 'string', description: 'Language label shown in the header' },
+        { name: 'class', type: 'string', description: 'Extra classes' },
+      ]}
+    >
+      {#snippet preview()}
+        <CodeBlock language="sql" code={`SELECT name, count(*)
+FROM databases
+GROUP BY name
+ORDER BY count DESC;`} class="w-full max-w-sm" />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="EmptyState"
+      description="Zero-data view with icon, title, description, and action slot."
+      code={`<EmptyState
+  title="No databases yet"
+  description="Create your first database to get started."
+>
+  {#snippet icon()}
+    <DatabaseIcon />
+  {/snippet}
+  {#snippet actions()}
+    <Button>Create database</Button>
+  {/snippet}
+</EmptyState>`}
+      props={[
+        { name: 'title', type: 'string', description: 'Primary heading' },
+        { name: 'description', type: 'string', description: 'Supporting text' },
+        { name: 'icon', type: 'Snippet', description: 'Icon displayed above the title' },
+        { name: 'actions', type: 'Snippet', description: 'Call-to-action buttons' },
+      ]}
+    >
+      {#snippet preview()}
+        <Card class="w-full max-w-sm">
+          <EmptyState title="No databases yet" description="Create your first to get started.">
+            {#snippet icon()}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/></svg>{/snippet}
+            {#snippet actions()}<Button size="sm">Create database</Button>{/snippet}
+          </EmptyState>
+        </Card>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="JsonViewer"
+      description="Collapsible JSON tree with syntax-highlighted strings, numbers, booleans, and nulls."
+      code={`<JsonViewer
+  data={{ id: 'db_abc123', config: { ssl: true, pool: { max: 20 } } }}
+  expandDepth={2}
+/>`}
+      props={[
+        { name: 'data', type: 'unknown', description: 'Any JSON-serializable value' },
+        { name: 'expandDepth', type: 'number', default: '2', description: 'How many levels are expanded by default' },
+        { name: 'class', type: 'string', description: 'Extra classes on the root element' },
+      ]}
+    >
+      {#snippet preview()}
+        <Card class="w-full max-w-sm">
+          <CardContent>
+            <JsonViewer data={sampleJson} expandDepth={2} />
+          </CardContent>
+        </Card>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="KanbanBoard / KanbanColumn / KanbanCard"
+      description="Drag-and-drop task board. KanbanBoard is the scroll container. KanbanColumn accepts drops and fires ondrop. KanbanCard is draggable."
+      code={`<script>
+  let cards = $state([
+    { id: 'c1', title: 'Set up replication', tags: ['infra'], column: 'todo' },
+    { id: 'c2', title: 'Write migration',    tags: ['db'],    column: 'in-progress' },
+  ]);
+
+  function moveCard(cardId, toColumn) {
+    cards = cards.map(c => c.id === cardId ? { ...c, column: toColumn } : c);
+  }
+<\/script>
+
+<KanbanBoard>
+  {#each columns as col}
+    {@const colCards = cards.filter(c => c.column === col.id)}
+    <KanbanColumn id={col.id} title={col.title} count={colCards.length} ondrop={moveCard}>
+      {#each colCards as card (card.id)}
+        <KanbanCard id={card.id} title={card.title} tags={card.tags} />
+      {/each}
+    </KanbanColumn>
+  {/each}
+</KanbanBoard>`}
+      props={[
+        { name: 'KanbanBoard.class', type: 'string', description: 'Extra classes on the scroll container' },
+        { name: 'KanbanColumn.id', type: 'string', description: 'Column identifier passed to ondrop' },
+        { name: 'KanbanColumn.title', type: 'string', description: 'Column header text' },
+        { name: 'KanbanColumn.count', type: 'number', description: 'Badge count in header' },
+        { name: 'KanbanColumn.ondrop', type: '(cardId, columnId) => void', description: 'Called when a card is dropped on this column' },
+        { name: 'KanbanCard.id', type: 'string', description: 'Unique card id (used as drag data)' },
+        { name: 'KanbanCard.title', type: 'string', description: 'Card heading' },
+        { name: 'KanbanCard.description', type: 'string', description: 'Optional body text (2-line clamp)' },
+        { name: 'KanbanCard.tags', type: 'string[]', description: 'Tag chips shown at card bottom' },
+        { name: 'KanbanCard.assignee', type: 'string', description: 'Shows an avatar initials badge' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full overflow-x-auto">
+          <KanbanBoard>
+            {#each cols as col}
+              {@const cards = kanbanCards.filter(c => c.column === col.id)}
+              <KanbanColumn id={col.id} title={col.title} count={cards.length} ondrop={moveCard}>
+                {#each cards as card (card.id)}
+                  <KanbanCard id={card.id} title={card.title} description={card.description} tags={card.tags} />
+                {/each}
+              </KanbanColumn>
+            {/each}
+          </KanbanBoard>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 245 - 0
src/routes/forms/+page.svelte

@@ -0,0 +1,245 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+  import Label from '$lib/components/primitives/Label.svelte';
+  import Input from '$lib/components/primitives/Input.svelte';
+  import { toast } from '$lib/utils/toast.svelte.js';
+
+  import FormField   from '$lib/components/forms/FormField.svelte';
+  import SearchInput from '$lib/components/forms/SearchInput.svelte';
+  import FileUpload  from '$lib/components/forms/FileUpload.svelte';
+  import Combobox    from '$lib/components/forms/Combobox.svelte';
+  import MultiSelect from '$lib/components/forms/MultiSelect.svelte';
+  import DatePicker  from '$lib/components/forms/DatePicker.svelte';
+
+  let searchVal = $state('');
+  let comboVal = $state('');
+  let multiVal = $state<string[]>([]);
+  let dateVal = $state<Date | null>(null);
+
+  const regions = [
+    { value: 'us-east-1',     label: 'US East (Virginia)' },
+    { value: 'us-west-2',     label: 'US West (Oregon)' },
+    { value: 'eu-west-1',     label: 'EU West (Ireland)' },
+    { value: 'ap-southeast-1',label: 'Asia Pacific (Singapore)' },
+  ];
+
+  const permissions = [
+    { value: 'read',   label: 'Read' },
+    { value: 'write',  label: 'Write' },
+    { value: 'delete', label: 'Delete' },
+    { value: 'admin',  label: 'Admin' },
+  ];
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Forms</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">Composite form controls beyond native HTML inputs.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="FormField"
+      description="Wrapper that adds an error or hint message below any input. Use in place of ad-hoc error paragraphs."
+      code={`<FormField hint="Must be unique within your project.">
+  <Label>Name</Label>
+  <Input placeholder="my-database" />
+</FormField>
+
+<FormField error="That name is already taken.">
+  <Label>Name</Label>
+  <Input value="taken-name" error />
+</FormField>`}
+      props={[
+        { name: 'error', type: 'string', description: 'Error message shown in destructive color. Takes priority over hint.' },
+        { name: 'hint', type: 'string', description: 'Help text shown in muted color' },
+        { name: 'children', type: 'Snippet', description: 'Label + input content' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="grid gap-4 sm:grid-cols-2 w-full max-w-lg">
+          <FormField hint="Must be unique within your project.">
+            <Label>Database name</Label>
+            <Input placeholder="my-database" />
+          </FormField>
+          <FormField error="That name is already taken.">
+            <Label>Database name</Label>
+            <Input value="taken-name" error />
+          </FormField>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="SearchInput"
+      description="Search field with icon, debounced oninput callback, and a clear button."
+      code={`<script>
+  let query = $state('');
+<\/script>
+
+<SearchInput
+  bind:value={query}
+  placeholder="Search databases…"
+  debounce={300}
+  oninput={(v) => fetchResults(v)}
+/>`}
+      props={[
+        { name: 'value', type: 'string', description: 'Bindable search string' },
+        { name: 'placeholder', type: 'string', default: "'Search…'", description: 'Placeholder text' },
+        { name: 'debounce', type: 'number', default: '200', description: 'Debounce delay in ms before oninput fires' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the input' },
+        { name: 'oninput', type: '(value: string) => void', description: 'Debounced callback' },
+      ]}
+    >
+      {#snippet preview()}
+        <SearchInput bind:value={searchVal} placeholder="Search databases…" class="max-w-xs w-full" />
+        {#if searchVal}<p class="text-xs text-[var(--muted-foreground)]">Query: {searchVal}</p>{/if}
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Combobox"
+      description="Searchable single-value select with type-ahead filtering and keyboard navigation."
+      code={`<script>
+  let region = $state('');
+
+  const options = [
+    { value: 'us-east-1', label: 'US East (Virginia)' },
+    { value: 'eu-west-1', label: 'EU West (Ireland)' },
+  ];
+<\/script>
+
+<Combobox
+  options={options}
+  bind:value={region}
+  placeholder="Select a region…"
+  onchange={(v) => console.log(v)}
+/>`}
+      props={[
+        { name: 'options', type: '{ value: string; label: string; description?: string }[]', description: 'Available options' },
+        { name: 'value', type: 'string', description: 'Bindable selected value' },
+        { name: 'placeholder', type: 'string', default: "'Select…'", description: 'Placeholder when nothing is selected' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the combobox' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Destructive border state' },
+        { name: 'onchange', type: '(value: string) => void', description: 'Called when selection changes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full max-w-xs">
+          <Combobox options={regions} bind:value={comboVal} placeholder="Select a region…" />
+          {#if comboVal}<p class="mt-1.5 text-xs text-[var(--muted-foreground)]">{comboVal}</p>{/if}
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="MultiSelect"
+      description="Tag-style multi-value select. Selected items render as removable chips. Supports a max limit."
+      code={`<script>
+  let selected = $state([]);
+
+  const options = [
+    { value: 'read',   label: 'Read' },
+    { value: 'write',  label: 'Write' },
+    { value: 'delete', label: 'Delete' },
+    { value: 'admin',  label: 'Admin' },
+  ];
+<\/script>
+
+<MultiSelect
+  options={options}
+  bind:value={selected}
+  placeholder="Select permissions…"
+  max={3}
+/>`}
+      props={[
+        { name: 'options', type: '{ value: string; label: string }[]', description: 'Available options' },
+        { name: 'value', type: 'string[]', description: 'Bindable array of selected values' },
+        { name: 'placeholder', type: 'string', default: "'Select…'", description: 'Shown when nothing is selected' },
+        { name: 'max', type: 'number', description: 'Maximum number of selections' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the component' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Destructive border state' },
+        { name: 'onchange', type: '(values: string[]) => void', description: 'Called when selection changes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full max-w-xs">
+          <MultiSelect options={permissions} bind:value={multiVal} placeholder="Select permissions…" max={3} />
+          {#if multiVal.length > 0}<p class="mt-1.5 text-xs text-[var(--muted-foreground)]">{multiVal.join(', ')}</p>{/if}
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="DatePicker"
+      description="Calendar popover for selecting a single date. Supports min/max constraints and a clear button."
+      code={`<script>
+  let date = $state(null);
+<\/script>
+
+<DatePicker
+  bind:value={date}
+  placeholder="Pick a date…"
+  min={new Date()}
+  onchange={(d) => console.log(d?.toISOString())}
+/>`}
+      props={[
+        { name: 'value', type: 'Date | null', description: 'Bindable selected date' },
+        { name: 'placeholder', type: 'string', default: "'Pick a date…'", description: 'Shown when no date is selected' },
+        { name: 'min', type: 'Date', description: 'Earliest selectable date' },
+        { name: 'max', type: 'Date', description: 'Latest selectable date' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the picker' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Destructive border state' },
+        { name: 'onchange', type: '(date: Date | null) => void', description: 'Called when date changes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full max-w-xs">
+          <DatePicker bind:value={dateVal} />
+          {#if dateVal}<p class="mt-1.5 text-xs text-[var(--muted-foreground)]">{dateVal.toISOString().split('T')[0]}</p>{/if}
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="FileUpload"
+      description="Drag-and-drop zone with click-to-browse fallback, file type hints, and size validation."
+      code={`<FileUpload
+  accept=".sql,.csv,.json"
+  multiple
+  maxSize={10 * 1024 * 1024}
+  onfiles={(files) => upload(files)}
+/>`}
+      props={[
+        { name: 'accept', type: 'string', description: 'Comma-separated MIME types or extensions, e.g. ".sql,.csv"' },
+        { name: 'multiple', type: 'boolean', default: 'false', description: 'Allow multiple file selection' },
+        { name: 'maxSize', type: 'number', description: 'Maximum file size in bytes' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the drop zone' },
+        { name: 'onfiles', type: '(files: File[]) => void', description: 'Called with the accepted file list' },
+      ]}
+    >
+      {#snippet preview()}
+        <FileUpload
+          accept=".sql,.csv,.json"
+          maxSize={10 * 1024 * 1024}
+          class="max-w-sm w-full"
+          onfiles={(files) => toast.add(`${files.length} file(s) selected`)}
+        />
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 168 - 0
src/routes/layout/+page.svelte

@@ -0,0 +1,168 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+  import Badge from '$lib/components/primitives/Badge.svelte';
+  import Button from '$lib/components/primitives/Button.svelte';
+
+  import Card from '$lib/components/layout/Card.svelte';
+  import CardHeader from '$lib/components/layout/CardHeader.svelte';
+  import CardTitle from '$lib/components/layout/CardTitle.svelte';
+  import CardDescription from '$lib/components/layout/CardDescription.svelte';
+  import CardContent from '$lib/components/layout/CardContent.svelte';
+  import CardFooter from '$lib/components/layout/CardFooter.svelte';
+  import Tabs from '$lib/components/layout/Tabs.svelte';
+  import Breadcrumbs from '$lib/components/layout/Breadcrumbs.svelte';
+  import ScrollArea from '$lib/components/layout/ScrollArea.svelte';
+
+  const tabs = [
+    { id: 'overview', label: 'Overview' },
+    { id: 'settings', label: 'Settings' },
+    { id: 'logs', label: 'Logs' },
+  ];
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Layout</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">Structural containers and navigation patterns.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="Card"
+      description="Content container. Composed from Card, CardHeader, CardTitle, CardDescription, CardContent, and CardFooter."
+      code={`<Card>
+  <CardHeader>
+    <CardTitle>Production</CardTitle>
+    <CardDescription>PostgreSQL · us-east-1</CardDescription>
+  </CardHeader>
+  <CardContent>
+    <Badge variant="active">Active</Badge>
+  </CardContent>
+  <CardFooter class="gap-2">
+    <Button size="sm" variant="outline">Connect</Button>
+    <Button size="sm" variant="ghost">Settings</Button>
+  </CardFooter>
+</Card>`}
+      props={[
+        { name: 'class', type: 'string', description: 'Extra classes on the card wrapper' },
+        { name: 'children', type: 'Snippet', description: 'Card content' },
+      ]}
+    >
+      {#snippet preview()}
+        <Card class="w-64">
+          <CardHeader>
+            <CardTitle>Production</CardTitle>
+            <CardDescription>PostgreSQL · us-east-1</CardDescription>
+          </CardHeader>
+          <CardContent>
+            <Badge variant="active">Active</Badge>
+          </CardContent>
+          <CardFooter class="gap-2">
+            <Button size="sm" variant="outline">Connect</Button>
+            <Button size="sm" variant="ghost">Settings</Button>
+          </CardFooter>
+        </Card>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Tabs"
+      description="Section-level navigation within a page. Active tab is managed internally."
+      code={`<script>
+  const tabs = [
+    { id: 'overview', label: 'Overview' },
+    { id: 'settings', label: 'Settings' },
+    { id: 'logs',     label: 'Logs' },
+  ];
+<\/script>
+
+<Tabs {tabs}>
+  {#snippet children(active)}
+    {#if active === 'overview'}
+      <p>Overview content</p>
+    {:else if active === 'settings'}
+      <p>Settings content</p>
+    {:else}
+      <p>Logs content</p>
+    {/if}
+  {/snippet}
+</Tabs>`}
+      props={[
+        { name: 'tabs', type: '{ id: string; label: string }[]', description: 'Tab definitions' },
+        { name: 'default', type: 'string', description: 'Initially active tab id (defaults to first)' },
+        { name: 'children', type: 'Snippet<[string]>', description: 'Receives the active tab id' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full">
+          <Tabs {tabs}>
+            {#snippet children(active)}
+              {#if active === 'overview'}
+                <p class="text-sm text-[var(--muted-foreground)]">Overview content — connection strings, quick stats.</p>
+              {:else if active === 'settings'}
+                <p class="text-sm text-[var(--muted-foreground)]">Settings — configuration, scaling, backups.</p>
+              {:else}
+                <p class="text-sm text-[var(--muted-foreground)]">Logs — recent events, error stream.</p>
+              {/if}
+            {/snippet}
+          </Tabs>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Breadcrumbs"
+      description="Hierarchical path indicator. Items with href are links; the last item is the current page."
+      code={`<Breadcrumbs
+  items={[
+    { label: 'Home',      href: '/' },
+    { label: 'Databases', href: '/databases' },
+    { label: 'production' },
+  ]}
+/>`}
+      props={[
+        { name: 'items', type: '{ label: string; href?: string }[]', description: 'Path segments. Last item is current page.' },
+        { name: 'class', type: 'string', description: 'Extra classes on the nav wrapper' },
+      ]}
+    >
+      {#snippet preview()}
+        <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Databases', href: '/' }, { label: 'production' }]} />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="ScrollArea"
+      description="Overflow wrapper with optional max height. Thin custom scrollbar on supporting browsers."
+      code={`<ScrollArea maxHeight="200px">
+  <ul>
+    {#each items as item}
+      <li>{item}</li>
+    {/each}
+  </ul>
+</ScrollArea>`}
+      props={[
+        { name: 'maxHeight', type: 'string', description: 'CSS max-height value, e.g. "300px"' },
+        { name: 'class', type: 'string', description: 'Extra classes on the scroll container' },
+        { name: 'children', type: 'Snippet', description: 'Scrollable content' },
+      ]}
+    >
+      {#snippet preview()}
+        <ScrollArea maxHeight="140px" class="w-full max-w-xs rounded-[var(--radius)] border border-[var(--border)]">
+          {#each Array.from({ length: 10 }, (_, i) => `Row ${i + 1} — db-${String(i + 1).padStart(3,'0')}`) as row}
+            <div class="px-3 py-2 text-sm text-[var(--foreground)] border-b border-[var(--border)] last:border-0">{row}</div>
+          {/each}
+        </ScrollArea>
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 216 - 0
src/routes/navigation/+page.svelte

@@ -0,0 +1,216 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+  import Button from '$lib/components/primitives/Button.svelte';
+  import { toast } from '$lib/utils/toast.svelte.js';
+
+  import Navbar from '$lib/components/navigation/Navbar.svelte';
+  import ThemeToggle from '$lib/components/navigation/ThemeToggle.svelte';
+  import Breadcrumbs from '$lib/components/layout/Breadcrumbs.svelte';
+  import CommandPalette from '$lib/components/navigation/CommandPalette.svelte';
+  import SidebarItem from '$lib/components/navigation/SidebarItem.svelte';
+  import SidebarSection from '$lib/components/navigation/SidebarSection.svelte';
+
+  let cmdOpen = $state(false);
+  let sidebarActive = $state('dashboard');
+
+  const cmdItems = [
+    { id: 'new',    label: 'New database',  group: 'Actions', icon: '🗄️', onselect: () => toast.add('New database') },
+    { id: 'query',  label: 'Open query editor', group: 'Actions', icon: '⚡', onselect: () => toast.add('Query editor') },
+    { id: 'docs',   label: 'Documentation', group: 'Navigate', icon: '📖', onselect: () => toast.add('Docs') },
+    { id: 'theme',  label: 'Toggle theme',  group: 'Misc',    icon: '🌙', onselect: () => toast.add('Theme') },
+  ];
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Navigation</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">App-level navigation and command patterns.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="ThemeToggle"
+      description="Light/dark mode switcher. Persists to localStorage and respects prefers-color-scheme."
+      code={`<ThemeToggle />`}
+      props={[
+        { name: 'class', type: 'string', description: 'Extra classes' },
+      ]}
+    >
+      {#snippet preview()}
+        <ThemeToggle />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Navbar"
+      description="Sticky top bar with logo, desktop nav, actions, and a built-in mobile hamburger dropdown."
+      code={`<Navbar>
+  {#snippet logo()}
+    <span class="font-semibold text-sm">🫒 olive</span>
+  {/snippet}
+  {#snippet nav()}
+    <NavItem href="/dashboard">Dashboard</NavItem>
+    <NavItem href="/databases">Databases</NavItem>
+  {/snippet}
+  {#snippet actions()}
+    <Button size="sm">New database</Button>
+  {/snippet}
+</Navbar>`}
+      props={[
+        { name: 'logo', type: 'Snippet', description: 'Left-side brand content' },
+        { name: 'nav', type: 'Snippet', description: 'Desktop nav items (hidden on mobile, shown in hamburger dropdown)' },
+        { name: 'actions', type: 'Snippet', description: 'Right-side action content' },
+        { name: 'hideThemeToggle', type: 'boolean', default: 'false', description: 'Hides the built-in theme toggle' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="w-full overflow-hidden rounded-[var(--radius)] border border-[var(--border)]">
+          <div class="flex h-14 items-center gap-4 border-b border-[var(--border)] bg-[var(--card)]/80 px-4">
+            <span class="font-semibold text-sm text-[var(--foreground)]">🫒 olive</span>
+            <div class="flex flex-1 gap-1">
+              <span class="rounded-[var(--radius)] px-3 py-1.5 text-sm font-medium text-[var(--primary)] bg-[var(--primary)]/10">Dashboard</span>
+              <span class="rounded-[var(--radius)] px-3 py-1.5 text-sm font-medium text-[var(--muted-foreground)] hover:text-[var(--foreground)]">Databases</span>
+            </div>
+            <ThemeToggle />
+          </div>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Sidebar + SidebarItem + SidebarSection"
+      description="Collapsible side navigation. Fixed app sidebar (default) or inline layout mode. Desktop collapse button and mobile drawer built in."
+      code={`<script>
+  let collapsed = $state(false);
+  let mobileOpen = $state(false);
+<\/script>
+
+<!-- Hamburger button (mobile) -->
+<button onclick={() => mobileOpen = !mobileOpen}>
+  <MenuIcon />
+</button>
+
+<Sidebar bind:collapsed bind:mobileOpen>
+  {#snippet children({ collapsed })}
+    <SidebarSection>
+      <SidebarItem label="Dashboard" active href="/dashboard" {collapsed}>
+        {#snippet icon()}<DashboardIcon />{/snippet}
+      </SidebarItem>
+      <SidebarItem label="Databases" href="/databases" {collapsed} badge={4}>
+        {#snippet icon()}<DbIcon />{/snippet}
+      </SidebarItem>
+    </SidebarSection>
+    <SidebarSection label="Settings" {collapsed}>
+      <SidebarItem label="Team" href="/team" {collapsed}>
+        {#snippet icon()}<TeamIcon />{/snippet}
+      </SidebarItem>
+    </SidebarSection>
+  {/snippet}
+  {#snippet footer({ collapsed })}
+    <SidebarItem label="Settings" href="/settings" {collapsed}>
+      {#snippet icon()}<SettingsIcon />{/snippet}
+    </SidebarItem>
+  {/snippet}
+</Sidebar>
+
+<!-- Offset main content on desktop -->
+<main class="lg:pl-[220px]">...</main>`}
+      props={[
+        { name: 'collapsed', type: 'boolean', default: 'false', description: 'Bindable. Collapses to icon-only (56px) on desktop' },
+        { name: 'mobileOpen', type: 'boolean', default: 'false', description: 'Bindable. Shows/hides the drawer on mobile' },
+        { name: 'inline', type: 'boolean', default: 'false', description: 'Use sticky positioning instead of fixed — for layout composition' },
+        { name: 'header', type: 'Snippet<[{collapsed}]>', description: 'Top section (logo area)' },
+        { name: 'footer', type: 'Snippet<[{collapsed}]>', description: 'Bottom section (user, settings)' },
+        { name: 'children', type: 'Snippet<[{collapsed}]>', description: 'Main nav items. Receives collapsed state.' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex h-[280px] w-full overflow-hidden rounded-[var(--radius)] border border-[var(--border)]">
+          <!-- Mini sidebar -->
+          <div class="flex w-[180px] shrink-0 flex-col border-r border-[var(--border)] bg-[var(--card)]">
+            <div class="flex h-12 items-center border-b border-[var(--border)] px-3">
+              <span class="text-sm font-semibold text-[var(--foreground)]">🫒 olive</span>
+            </div>
+            <div class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
+              <SidebarSection>
+                <SidebarItem label="Dashboard" active={sidebarActive === 'dashboard'} onclick={() => sidebarActive = 'dashboard'}>
+                  {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="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>{/snippet}
+                </SidebarItem>
+                <SidebarItem label="Databases" badge={4} active={sidebarActive === 'databases'} onclick={() => sidebarActive = 'databases'}>
+                  {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/></svg>{/snippet}
+                </SidebarItem>
+                <SidebarItem label="Logs" active={sidebarActive === 'logs'} onclick={() => sidebarActive = 'logs'}>
+                  {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/></svg>{/snippet}
+                </SidebarItem>
+              </SidebarSection>
+              <SidebarSection label="Settings">
+                <SidebarItem label="Team" active={sidebarActive === 'team'} onclick={() => sidebarActive = 'team'}>
+                  {#snippet icon()}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="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"/></svg>{/snippet}
+                </SidebarItem>
+              </SidebarSection>
+            </div>
+          </div>
+          <!-- Content area -->
+          <div class="flex flex-1 items-center justify-center text-sm text-[var(--muted-foreground)]">
+            {sidebarActive}
+          </div>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="CommandPalette"
+      description="Global search and action launcher. Opens on ⌘K / Ctrl+K. Supports grouped results, icons, and keyboard shortcuts."
+      code={`<script>
+  let open = $state(false);
+
+  const items = [
+    {
+      id: 'new-db',
+      label: 'Create database',
+      description: 'Spin up a new instance',
+      group: 'Actions',
+      icon: '🗄️',
+      onselect: () => router.goto('/new'),
+    },
+    {
+      id: 'settings',
+      label: 'Settings',
+      group: 'Navigate',
+      shortcut: '⌘,',
+      onselect: () => router.goto('/settings'),
+    },
+  ];
+<\/script>
+
+<CommandPalette bind:open {items} placeholder="Search…" />`}
+      props={[
+        { name: 'open', type: 'boolean', default: 'false', description: 'Bindable. Controls visibility.' },
+        { name: 'items', type: 'CommandItem[]', description: 'Array of { id, label, description?, icon?, group?, shortcut?, onselect }' },
+        { name: 'placeholder', type: 'string', default: "'Search…'", description: 'Input placeholder text' },
+        { name: 'onclose', type: '() => void', description: 'Called when the palette closes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col items-center gap-3">
+          <Button variant="outline" onclick={() => cmdOpen = true}>
+            Open command palette
+            <kbd class="ml-2 rounded border border-[var(--border)] bg-[var(--muted)] px-1.5 py-0.5 text-xs text-[var(--muted-foreground)]">⌘K</kbd>
+          </Button>
+          <p class="text-xs text-[var(--muted-foreground)]">Also works globally with ⌘K / Ctrl+K</p>
+        </div>
+        <CommandPalette bind:open={cmdOpen} items={cmdItems} />
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 395 - 0
src/routes/overlay/+page.svelte

@@ -0,0 +1,395 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+  import Button from '$lib/components/primitives/Button.svelte';
+  import Input from '$lib/components/primitives/Input.svelte';
+  import Label from '$lib/components/primitives/Label.svelte';
+  import { toast } from '$lib/utils/toast.svelte.js';
+
+  import Dialog from '$lib/components/overlay/Dialog.svelte';
+  import AlertDialog from '$lib/components/overlay/AlertDialog.svelte';
+  import Alert from '$lib/components/overlay/Alert.svelte';
+  import Tooltip from '$lib/components/overlay/Tooltip.svelte';
+  import DropdownMenu from '$lib/components/overlay/DropdownMenu.svelte';
+  import Progress from '$lib/components/overlay/Progress.svelte';
+  import Skeleton from '$lib/components/overlay/Skeleton.svelte';
+  import Sheet from '$lib/components/overlay/Sheet.svelte';
+  import Popover from '$lib/components/overlay/Popover.svelte';
+
+  let dialogOpen = $state(false);
+  let alertDialogOpen = $state(false);
+  let sheetOpen = $state(false);
+
+  const dropdownItems = [
+    { label: 'Edit',      onclick: () => toast.add('Edit') },
+    { label: 'Duplicate', onclick: () => toast.add('Duplicate') },
+    { separator: true, label: '' },
+    { label: 'Delete', variant: 'destructive' as const, onclick: () => toast.add('Deleted') },
+  ];
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Overlay & Feedback</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">Modals, sheets, popovers, toasts, and status indicators.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="Alert"
+      description="Inline status message. Four semantic variants — all readable in light and dark mode."
+      code={`<Alert variant="info" title="Scheduled maintenance">
+  Next window is Apr 24, 2–4 AM UTC.
+</Alert>
+<Alert variant="success" title="Backup complete">
+  Daily backup completed at 03:00 UTC.
+</Alert>
+<Alert variant="warning" title="High connection count">
+  You are at 82% of your connection limit.
+</Alert>
+<Alert variant="error" title="Replication lag">
+  Replica is 45 seconds behind primary.
+</Alert>`}
+      props={[
+        { name: 'variant', type: "'info' | 'success' | 'warning' | 'error'", default: "'info'", description: 'Semantic color and icon' },
+        { name: 'title', type: 'string', description: 'Bold heading line' },
+        { name: 'children', type: 'Snippet', description: 'Body text' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-2.5 w-full max-w-md">
+          <Alert variant="info" title="Scheduled maintenance">Next window is Apr 24, 2–4 AM UTC.</Alert>
+          <Alert variant="success" title="Backup complete">Daily backup completed at 03:00 UTC.</Alert>
+          <Alert variant="warning" title="High connection count">At 82% of connection limit.</Alert>
+          <Alert variant="error" title="Replication lag">Replica is 45s behind primary.</Alert>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Progress"
+      description="Determinate or indeterminate progress bar. Three color variants reflect severity."
+      code={`<Progress value={68} />
+<Progress value={82} variant="warning" />
+<Progress value={97} variant="error" />
+<Progress indeterminate />`}
+      props={[
+        { name: 'value', type: 'number', description: '0–100. Omit or use indeterminate for spinner mode.' },
+        { name: 'variant', type: "'default' | 'warning' | 'error'", default: "'default'", description: 'Color preset' },
+        { name: 'indeterminate', type: 'boolean', default: 'false', description: 'Animated sweep when value is unknown' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-3 w-full max-w-sm">
+          <Progress value={68} />
+          <Progress value={82} variant="warning" />
+          <Progress value={97} variant="error" />
+          <Progress indeterminate />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Skeleton"
+      description="Loading placeholder that matches the geometry of incoming content."
+      code={`<Skeleton width="100%" height="14px" />
+<Skeleton width="60%" height="12px" />
+<Skeleton rounded width="40px" height="40px" />`}
+      props={[
+        { name: 'width', type: 'string', description: 'CSS width, e.g. "100%" or "120px"' },
+        { name: 'height', type: 'string', description: 'CSS height, e.g. "14px"' },
+        { name: 'rounded', type: 'boolean', default: 'false', description: 'Fully rounds corners (for avatars)' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-2 w-48">
+          <Skeleton width="100%" height="14px" />
+          <Skeleton width="80%" height="12px" />
+          <Skeleton width="60%" height="12px" />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Tooltip"
+      description="Hover label for iconographic elements. Appears after a short delay."
+      code={`<Tooltip content="Delete this database">
+  <Button variant="ghost" size="icon" aria-label="Delete">
+    <TrashIcon />
+  </Button>
+</Tooltip>`}
+      props={[
+        { name: 'content', type: 'string', description: 'Tooltip text' },
+        { name: 'side', type: "'top' | 'bottom' | 'left' | 'right'", default: "'top'", description: 'Preferred placement' },
+        { name: 'children', type: 'Snippet', description: 'The element that triggers the tooltip' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex gap-4">
+          <Tooltip content="Connect to database">
+            <Button variant="outline" size="sm">Connect</Button>
+          </Tooltip>
+          <Tooltip content="Delete permanently" side="bottom">
+            <Button variant="ghost" size="sm">Delete</Button>
+          </Tooltip>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Toaster"
+      description="Transient notification system. Mount once at root, trigger anywhere with the toast utility."
+      code={`<!-- In your layout -->
+<Toaster />
+
+<!-- Anywhere in your app -->
+<script>
+  import { toast } from 'olive-ui';
+
+  toast.add('Database saved');
+  toast.success('Backup complete', { description: 'All replicas in sync.' });
+  toast.warning('Connection limit at 82%');
+  toast.error('Query timed out', { description: 'Exceeded 30s limit.' });
+<\/script>`}
+      props={[
+        { name: 'position', type: "'bottom-right' | 'bottom-center' | 'top-right'", default: "'bottom-right'", description: 'Stack position on screen' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-wrap gap-2">
+          <Button variant="outline" size="sm" onclick={() => toast.add('Database saved')}>Default</Button>
+          <Button variant="outline" size="sm" onclick={() => toast.success('Backup complete')}>Success</Button>
+          <Button variant="outline" size="sm" onclick={() => toast.warning('Connection at 82%')}>Warning</Button>
+          <Button variant="outline" size="sm" onclick={() => toast.error('Query timed out')}>Error</Button>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="DropdownMenu"
+      description="Anchored action menu with support for separators and destructive items."
+      code={`<DropdownMenu
+  items={[
+    { label: 'Edit',      onclick: () => {} },
+    { label: 'Duplicate', onclick: () => {} },
+    { separator: true, label: '' },
+    { label: 'Delete', variant: 'destructive', onclick: () => {} },
+  ]}
+>
+  {#snippet trigger()}
+    <Button variant="outline" size="sm">
+      Actions <ChevronIcon />
+    </Button>
+  {/snippet}
+</DropdownMenu>`}
+      props={[
+        { name: 'items', type: 'MenuItem[]', description: '{ label, onclick, variant?, separator?, icon? }[]' },
+        { name: 'align', type: "'start' | 'end'", default: "'end'", description: 'Horizontal alignment relative to trigger' },
+        { name: 'trigger', type: 'Snippet', description: 'The button or element that opens the menu' },
+      ]}
+    >
+      {#snippet preview()}
+        <DropdownMenu items={dropdownItems} align="start">
+          {#snippet trigger()}
+            <Button variant="outline" size="sm">
+              Actions
+              <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
+            </Button>
+          {/snippet}
+        </DropdownMenu>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Popover"
+      description="Anchored floating container. Click outside to dismiss. Configurable side and alignment."
+      code={`<Popover side="bottom" align="start">
+  {#snippet trigger()}
+    <Button variant="outline">Connection info</Button>
+  {/snippet}
+  {#snippet children()}
+    <div class="flex flex-col gap-2 p-1">
+      <p class="text-sm font-medium">db.example.com:5432</p>
+      <Button size="sm" variant="outline">Copy</Button>
+    </div>
+  {/snippet}
+</Popover>`}
+      props={[
+        { name: 'side', type: "'top' | 'bottom' | 'left' | 'right'", default: "'bottom'", description: 'Preferred position relative to trigger' },
+        { name: 'align', type: "'start' | 'center' | 'end'", default: "'start'", description: 'Alignment along the side axis' },
+        { name: 'open', type: 'boolean', default: 'false', description: 'Bindable visibility state' },
+        { name: 'trigger', type: 'Snippet', description: 'The element that opens the popover' },
+        { name: 'children', type: 'Snippet', description: 'Popover panel content' },
+      ]}
+    >
+      {#snippet preview()}
+        <Popover side="bottom" align="start">
+          {#snippet trigger()}
+            <Button variant="outline">Connection info</Button>
+          {/snippet}
+          {#snippet children()}
+            <div class="flex flex-col gap-2 p-1">
+              <p class="text-sm font-medium text-[var(--foreground)]">Production</p>
+              <p class="text-xs text-[var(--muted-foreground)] font-mono">db.example.com:5432</p>
+              <Button size="sm" variant="outline" class="w-full">Copy string</Button>
+            </div>
+          {/snippet}
+        </Popover>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Dialog"
+      description="Modal overlay. Focus-trapped, scroll-locked, dismissible via Escape or backdrop click."
+      code={`<script>
+  let open = $state(false);
+<\/script>
+
+<Button onclick={() => open = true}>Open</Button>
+
+<Dialog bind:open title="Create database" description="Configure your new database.">
+  {#snippet children()}
+    <Input placeholder="my-database" />
+  {/snippet}
+  {#snippet footer()}
+    <Button variant="outline" onclick={() => open = false}>Cancel</Button>
+    <Button onclick={() => open = false}>Create</Button>
+  {/snippet}
+</Dialog>`}
+      props={[
+        { name: 'open', type: 'boolean', description: 'Bindable visibility state' },
+        { name: 'title', type: 'string', description: 'Dialog heading' },
+        { name: 'description', type: 'string', description: 'Subheading below the title' },
+        { name: 'children', type: 'Snippet', description: 'Dialog body content' },
+        { name: 'footer', type: 'Snippet', description: 'Action buttons area' },
+      ]}
+    >
+      {#snippet preview()}
+        <Button variant="outline" onclick={() => dialogOpen = true}>Open dialog</Button>
+        <Dialog bind:open={dialogOpen} title="Create database" description="Configure your new database.">
+          {#snippet children()}
+            <div class="flex flex-col gap-3">
+              <div class="flex flex-col gap-1.5">
+                <Label>Name</Label>
+                <Input placeholder="my-database" />
+              </div>
+            </div>
+          {/snippet}
+          {#snippet footer()}
+            <Button variant="outline" onclick={() => dialogOpen = false}>Cancel</Button>
+            <Button onclick={() => { dialogOpen = false; toast.success('Database created'); }}>Create</Button>
+          {/snippet}
+        </Dialog>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="AlertDialog"
+      description="Confirmation modal for destructive or irreversible actions. Requires explicit confirm."
+      code={`<script>
+  let open = $state(false);
+<\/script>
+
+<Button variant="destructive" onclick={() => open = true}>Delete</Button>
+
+<AlertDialog
+  bind:open
+  title="Delete database?"
+  description="This will permanently delete all data. Cannot be undone."
+  confirmLabel="Delete database"
+  cancelLabel="Cancel"
+  destructive
+  onconfirm={() => performDelete()}
+/>`}
+      props={[
+        { name: 'open', type: 'boolean', description: 'Bindable visibility state' },
+        { name: 'title', type: 'string', description: 'Dialog heading' },
+        { name: 'description', type: 'string', description: 'Warning body text' },
+        { name: 'confirmLabel', type: 'string', default: "'Confirm'", description: 'Text for the confirm button' },
+        { name: 'cancelLabel', type: 'string', default: "'Cancel'", description: 'Text for the cancel button' },
+        { name: 'destructive', type: 'boolean', default: 'false', description: 'Renders the confirm button in destructive variant' },
+        { name: 'onconfirm', type: '() => void', description: 'Called when user confirms' },
+      ]}
+    >
+      {#snippet preview()}
+        <Button variant="destructive" onclick={() => alertDialogOpen = true}>Delete database</Button>
+        <AlertDialog
+          bind:open={alertDialogOpen}
+          title="Delete database?"
+          description="This will permanently delete 'production' and all its data."
+          confirmLabel="Delete database"
+          cancelLabel="Cancel"
+          destructive
+          onconfirm={() => toast.error('Database deleted')}
+        />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Sheet"
+      description="Slide-in panel from any edge. Use for detail views, settings, and multi-step flows."
+      code={`<script>
+  let open = $state(false);
+<\/script>
+
+<Button onclick={() => open = true}>Open settings</Button>
+
+<Sheet bind:open side="right" title="Database settings">
+  {#snippet children()}
+    <Input placeholder="Display name" />
+  {/snippet}
+  {#snippet footer()}
+    <Button variant="outline" onclick={() => open = false}>Cancel</Button>
+    <Button onclick={() => open = false}>Save</Button>
+  {/snippet}
+</Sheet>`}
+      props={[
+        { name: 'open', type: 'boolean', description: 'Bindable visibility state' },
+        { name: 'side', type: "'left' | 'right' | 'top' | 'bottom'", default: "'right'", description: 'Edge the panel slides from' },
+        { name: 'title', type: 'string', description: 'Panel heading' },
+        { name: 'description', type: 'string', description: 'Subheading' },
+        { name: 'children', type: 'Snippet', description: 'Panel body' },
+        { name: 'footer', type: 'Snippet', description: 'Action buttons' },
+      ]}
+    >
+      {#snippet preview()}
+        <Button variant="outline" onclick={() => sheetOpen = true}>Open sheet</Button>
+        <Sheet bind:open={sheetOpen} title="Database settings" description="Adjust configuration for production.">
+          {#snippet children()}
+            <div class="flex flex-col gap-3">
+              <div class="flex flex-col gap-1.5">
+                <Label>Display name</Label>
+                <Input value="production" />
+              </div>
+            </div>
+          {/snippet}
+          {#snippet footer()}
+            <Button variant="outline" onclick={() => sheetOpen = false}>Cancel</Button>
+            <Button onclick={() => { sheetOpen = false; toast.success('Saved'); }}>Save changes</Button>
+          {/snippet}
+        </Sheet>
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 338 - 0
src/routes/primitives/+page.svelte

@@ -0,0 +1,338 @@
+<script lang="ts">
+  import DocBlock from '../DocBlock.svelte';
+  import Separator from '$lib/components/primitives/Separator.svelte';
+
+  import Button   from '$lib/components/primitives/Button.svelte';
+  import Input    from '$lib/components/primitives/Input.svelte';
+  import Textarea from '$lib/components/primitives/Textarea.svelte';
+  import Label    from '$lib/components/primitives/Label.svelte';
+  import Checkbox from '$lib/components/primitives/Checkbox.svelte';
+  import Switch   from '$lib/components/primitives/Switch.svelte';
+  import Badge    from '$lib/components/primitives/Badge.svelte';
+  import Avatar   from '$lib/components/primitives/Avatar.svelte';
+  import Slider   from '$lib/components/primitives/Slider.svelte';
+  import Select   from '$lib/components/primitives/Select.svelte';
+  import RadioGroup from '$lib/components/primitives/RadioGroup.svelte';
+  import RadioItem  from '$lib/components/primitives/RadioItem.svelte';
+
+  let checked = $state(false);
+  let switched = $state(false);
+  let sliderVal = $state(40);
+  let radioVal = $state('read');
+  let selectVal = $state('');
+</script>
+
+<div class="mx-auto max-w-3xl px-6 py-12 lg:px-10">
+  <div class="mb-10">
+    <h1 class="text-2xl font-semibold text-[var(--foreground)]">Primitives</h1>
+    <p class="mt-1 text-sm text-[var(--muted-foreground)]">Core interactive building blocks.</p>
+  </div>
+
+  <div class="flex flex-col gap-10">
+
+    <DocBlock
+      name="Button"
+      description="Primary action element. Variants cover every intent; sizes fit every context."
+      code={`<Button>Default</Button>
+<Button variant="outline">Outline</Button>
+<Button variant="ghost">Ghost</Button>
+<Button variant="destructive">Destructive</Button>
+<Button variant="link">Link</Button>
+<Button disabled>Disabled</Button>
+<Button size="sm">Small</Button>
+<Button size="lg">Large</Button>
+<Button size="icon" aria-label="Add">
+  <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
+    stroke="currentColor" stroke-width="2" stroke-linecap="round">
+    <path d="M12 5v14M5 12h14"/>
+  </svg>
+</Button>`}
+      props={[
+        { name: 'variant', type: "'default' | 'outline' | 'ghost' | 'destructive' | 'link'", default: "'default'", description: 'Visual style of the button' },
+        { name: 'size', type: "'sm' | 'md' | 'lg' | 'icon'", default: "'md'", description: 'Height and padding preset' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Prevents interaction and reduces opacity' },
+        { name: 'type', type: "'button' | 'submit' | 'reset'", default: "'button'", description: 'HTML button type' },
+        { name: 'class', type: 'string', description: 'Extra classes merged via cn()' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-wrap gap-2">
+          <Button>Default</Button>
+          <Button variant="outline">Outline</Button>
+          <Button variant="ghost">Ghost</Button>
+          <Button variant="destructive">Destructive</Button>
+          <Button variant="link">Link</Button>
+          <Button disabled>Disabled</Button>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Input"
+      description="Single-line text field. Supports error state and all native input attributes."
+      code={`<Input placeholder="Database name" />
+<Input value="taken-name" error />
+<Input type="password" placeholder="Password" />
+<Input disabled placeholder="Disabled" />`}
+      props={[
+        { name: 'value', type: 'string', description: 'Bindable value' },
+        { name: 'placeholder', type: 'string', description: 'Placeholder text' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Renders destructive border color' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the input' },
+        { name: 'type', type: 'string', default: "'text'", description: 'HTML input type' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-3 w-full max-w-xs">
+          <Input placeholder="Database name" />
+          <Input value="taken-name" error />
+          <Input disabled placeholder="Disabled" />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Textarea"
+      description="Multi-line text input with consistent styling."
+      code={`<Textarea placeholder="Add notes…" />
+<Textarea rows={6} placeholder="Longer content" />`}
+      props={[
+        { name: 'value', type: 'string', description: 'Bindable value' },
+        { name: 'placeholder', type: 'string', description: 'Placeholder text' },
+        { name: 'rows', type: 'number', default: '3', description: 'Visible row count' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the textarea' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Destructive border' },
+      ]}
+    >
+      {#snippet preview()}
+        <Textarea placeholder="Add notes about this configuration…" class="max-w-sm w-full" />
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Label"
+      description="Form label linked to an input via htmlFor / id."
+      code={`<Label for="name">Database name</Label>
+<Input id="name" placeholder="my-database" />`}
+      props={[
+        { name: 'for', type: 'string', description: 'id of the associated input element' },
+        { name: 'class', type: 'string', description: 'Extra classes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-1.5">
+          <Label for="demo-input">Database name</Label>
+          <Input id="demo-input" placeholder="my-database" class="max-w-xs" />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Checkbox"
+      description="Boolean toggle. Wrap in a Label or use id/for to make the entire row clickable."
+      code={`<script>
+  let checked = $state(false);
+<\/script>
+
+<div class="flex items-center gap-2.5">
+  <Checkbox id="backups" bind:checked />
+  <Label for="backups">Enable backups</Label>
+</div>`}
+      props={[
+        { name: 'checked', type: 'boolean', default: 'false', description: 'Bindable checked state' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction' },
+        { name: 'id', type: 'string', description: 'Links to a <Label for="…">' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex items-center gap-2.5">
+          <Checkbox id="cb-preview" bind:checked />
+          <Label for="cb-preview">Enable backups {checked ? '✓' : ''}</Label>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Switch"
+      description="On/off toggle. Visually distinct from Checkbox — use for settings that take effect immediately."
+      code={`<script>
+  let enabled = $state(false);
+<\/script>
+
+<div class="flex items-center gap-2.5">
+  <Switch id="autoscale" bind:checked={enabled} />
+  <Label for="autoscale">Auto-scale {enabled ? 'on' : 'off'}</Label>
+</div>`}
+      props={[
+        { name: 'checked', type: 'boolean', default: 'false', description: 'Bindable on/off state' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction' },
+        { name: 'id', type: 'string', description: 'Links to a <Label for="…">' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex items-center gap-2.5">
+          <Switch id="sw-preview" bind:checked={switched} />
+          <Label for="sw-preview">Auto-scale {switched ? 'on' : 'off'}</Label>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Slider"
+      description="Range input with zero-latency thumb tracking and tactile drag feedback."
+      code={`<script>
+  let retention = $state(30);
+<\/script>
+
+<div class="flex flex-col gap-2 w-48">
+  <Label>Retention: {retention} days</Label>
+  <Slider bind:value={retention} min={1} max={90} />
+</div>`}
+      props={[
+        { name: 'value', type: 'number', description: 'Bindable current value' },
+        { name: 'min', type: 'number', default: '0', description: 'Minimum value' },
+        { name: 'max', type: 'number', default: '100', description: 'Maximum value' },
+        { name: 'step', type: 'number', default: '1', description: 'Step increment' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-col gap-2 w-48">
+          <Label>Retention: {sliderVal} days</Label>
+          <Slider bind:value={sliderVal} min={1} max={90} />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Select"
+      description="Native select element with olive styling. Use Combobox for searchable options."
+      code={`<script>
+  let region = $state('');
+<\/script>
+
+<Select bind:value={region}>
+  <option value="">Select a region</option>
+  <option value="us-east-1">US East (Virginia)</option>
+  <option value="eu-west-1">EU West (Ireland)</option>
+</Select>`}
+      props={[
+        { name: 'value', type: 'string', description: 'Bindable selected value' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the select' },
+        { name: 'error', type: 'boolean', default: 'false', description: 'Destructive border' },
+      ]}
+    >
+      {#snippet preview()}
+        <Select bind:value={selectVal} class="max-w-xs w-full">
+          <option value="">Select a region</option>
+          <option value="us-east-1">US East (Virginia)</option>
+          <option value="us-west-2">US West (Oregon)</option>
+          <option value="eu-west-1">EU West (Ireland)</option>
+        </Select>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="RadioGroup / RadioItem"
+      description="Mutually exclusive options. RadioGroup provides the binding; RadioItem renders each option."
+      code={`<script>
+  let access = $state('read');
+<\/script>
+
+<RadioGroup bind:value={access} name="access-level">
+  <RadioItem value="read">Read only</RadioItem>
+  <RadioItem value="write">Read / Write</RadioItem>
+  <RadioItem value="admin">Admin</RadioItem>
+</RadioGroup>`}
+      props={[
+        { name: 'value', type: 'string', description: 'Bindable selected value (on RadioGroup)' },
+        { name: 'name', type: 'string', description: 'HTML name attribute for the group' },
+        { name: 'value (item)', type: 'string', description: 'Value emitted when this item is selected' },
+        { name: 'disabled', type: 'boolean', default: 'false', description: 'Disables a specific item' },
+      ]}
+    >
+      {#snippet preview()}
+        <RadioGroup bind:value={radioVal} name="docs-access">
+          <RadioItem value="read">Read only</RadioItem>
+          <RadioItem value="write">Read / Write</RadioItem>
+          <RadioItem value="admin">Admin</RadioItem>
+        </RadioGroup>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Badge"
+      description="Semantic status label. Uses crosshair cursor to signal non-interactivity."
+      code={`<Badge>Default</Badge>
+<Badge variant="active">Active</Badge>
+<Badge variant="paused">Paused</Badge>
+<Badge variant="error">Error</Badge>
+<Badge variant="warning">Warning</Badge>
+<Badge variant="info">Info</Badge>
+<Badge variant="write">Write</Badge>
+<Badge variant="delete">Delete</Badge>`}
+      props={[
+        { name: 'variant', type: "'default' | 'outline' | 'active' | 'paused' | 'error' | 'warning' | 'info' | 'debug' | 'trace' | 'write' | 'delete'", default: "'default'", description: 'Color and meaning' },
+        { name: 'class', type: 'string', description: 'Extra classes' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex flex-wrap gap-2">
+          <Badge>Default</Badge>
+          <Badge variant="active">Active</Badge>
+          <Badge variant="paused">Paused</Badge>
+          <Badge variant="error">Error</Badge>
+          <Badge variant="warning">Warning</Badge>
+          <Badge variant="info">Info</Badge>
+          <Badge variant="write">Write</Badge>
+          <Badge variant="delete">Delete</Badge>
+        </div>
+      {/snippet}
+    </DocBlock>
+
+    <Separator />
+
+    <DocBlock
+      name="Avatar"
+      description="User identity mark. Falls back to initials when image fails or is absent."
+      code={`<Avatar initials="DF" />
+<Avatar initials="AB" size="sm" />
+<Avatar initials="XY" size="lg" />
+<Avatar src="/avatar.jpg" alt="Dan" initials="DF" />`}
+      props={[
+        { name: 'initials', type: 'string', description: 'Fallback text (1–2 chars)' },
+        { name: 'src', type: 'string', description: 'Image URL — falls back to initials on error' },
+        { name: 'alt', type: 'string', description: 'Image alt text' },
+        { name: 'size', type: "'sm' | 'md' | 'lg'", default: "'md'", description: 'Diameter preset' },
+      ]}
+    >
+      {#snippet preview()}
+        <div class="flex items-center gap-3">
+          <Avatar initials="DF" size="sm" />
+          <Avatar initials="AB" />
+          <Avatar initials="XY" size="lg" />
+        </div>
+      {/snippet}
+    </DocBlock>
+
+  </div>
+  <div class="h-16"></div>
+</div>

+ 3 - 0
static/robots.txt

@@ -0,0 +1,3 @@
+# allow crawling everything by default
+User-agent: *
+Disallow:

+ 17 - 0
svelte.config.js

@@ -0,0 +1,17 @@
+import adapter from '@sveltejs/adapter-auto';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+	compilerOptions: {
+		// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
+		runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
+	},
+	kit: {
+		// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
+		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
+		// See https://svelte.dev/docs/kit/adapters for more information about adapters.
+		adapter: adapter()
+	}
+};
+
+export default config;

+ 20 - 0
tsconfig.json

@@ -0,0 +1,20 @@
+{
+	"extends": "./.svelte-kit/tsconfig.json",
+	"compilerOptions": {
+		"rewriteRelativeImportExtensions": true,
+		"allowJs": true,
+		"checkJs": true,
+		"esModuleInterop": true,
+		"forceConsistentCasingInFileNames": true,
+		"resolveJsonModule": true,
+		"skipLibCheck": true,
+		"sourceMap": true,
+		"strict": true,
+		"moduleResolution": "bundler"
+	}
+	// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
+	// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
+	//
+	// To make changes to top-level options such as include and exclude, we recommend extending
+	// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
+}

+ 7 - 0
vite.config.ts

@@ -0,0 +1,7 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+	plugins: [tailwindcss(), sveltekit()]
+});