@spaethtech/svelte-ui 0.3.1-dev.5.6aa9d2d → 0.3.1-dev.7.3dc4fbb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: svelte-ui
3
+ description: How to build UIs with the @spaethtech/svelte-ui component library in a CONSUMER project — one-time setup (theme.css + Tailwind @source + unplugin-icons), the import + MDI-icon-snippet patterns, the shared variant/size axes, the composition standard (use library components, never raw <button>/<select>/<input>), and the component catalog. Use whenever adding or editing UI that uses @spaethtech/svelte-ui components. For palette/token/theming/inverse-surface work, use the companion `themes` skill instead.
4
+ ---
5
+
6
+ # Using @spaethtech/svelte-ui
7
+
8
+ A **Svelte 5 (runes)** component library styled with **Tailwind CSS v4** and **MDI icons**. Every
9
+ component is theme-aware via CSS variables and most consume the shared `variant` + `size` axes.
10
+ Import from the package name — **`@spaethtech/svelte-ui`** — never a deep `dist/` path.
11
+
12
+ > Depth lives in the package's shipped docs (`node_modules/@spaethtech/svelte-ui/docs/*.md`) and this
13
+ > repo's `CLAUDE.md`. Theming (tokens, dark mode, inverse surfaces, brand bridging) → the **`themes`**
14
+ > skill. This skill is the usage mental-model + patterns; read the docs for full per-component APIs.
15
+
16
+ ## One-time setup (per consumer project)
17
+
18
+ Peer deps (not bundled): `svelte@^5`, `tailwindcss@^4`, `unplugin-icons@^23`, `@iconify-json/mdi@^1`.
19
+
20
+ **1. Vite — add the icon resolver** (`vite.config.ts`), because the package ships bare `~icons/mdi/*`
21
+ imports that resolve at the consumer's build time:
22
+
23
+ ```ts
24
+ import Icons from "unplugin-icons/vite";
25
+ export default defineConfig({
26
+ plugins: [/* sveltekit(), tailwindcss(), */ Icons({ compiler: "svelte" })],
27
+ });
28
+ ```
29
+
30
+ **2. CSS** (`app.css`, Tailwind v4) — import the theme and scan the dist so Tailwind emits the classes
31
+ the components use:
32
+
33
+ ```css
34
+ @import "tailwindcss";
35
+ @import "@spaethtech/svelte-ui/theme.css";
36
+ @source "../node_modules/@spaethtech/svelte-ui/dist"; /* path relative to this file */
37
+ ```
38
+
39
+ ## Import & basic use
40
+
41
+ ```svelte
42
+ <script>
43
+ import { Button, Input, Select } from "@spaethtech/svelte-ui";
44
+ let name = $state("");
45
+ </script>
46
+
47
+ <Input bind:value={name} placeholder="Name" />
48
+ <Button text="Save" variant="primary" onclick={() => save(name)} />
49
+ ```
50
+
51
+ ## The two shared axes (mirror each other)
52
+
53
+ - **`variant`** — color/style: `primary` `secondary` `danger` `info` `success` `warning` `ghost`
54
+ `plain` (`neutral` on some components). `plain` = borderless/transparent, for dense icon controls.
55
+ - **`size`** — `sm` (24px) · `md` (32px, **default**) · `lg` (40px). Consistent height across every
56
+ sizeable component, so a `Button`, `Input`, and `Select` at the same `size` line up.
57
+
58
+ Both accept a responsive map too, e.g. `size={{ base: "sm", md: "lg" }}`.
59
+
60
+ ## Icons — no `<Icon>` component
61
+
62
+ Import an MDI icon component from `~icons/mdi/*` and render it into a component's **`icon` snippet**;
63
+ the library sizes and colors it to match `size`/`variant`:
64
+
65
+ ```svelte
66
+ <script>
67
+ import { Button } from "@spaethtech/svelte-ui";
68
+ import IconHome from "~icons/mdi/home";
69
+ </script>
70
+
71
+ <Button title="Home" variant="plain">{#snippet icon()}<IconHome />{/snippet}</Button>
72
+ ```
73
+
74
+ ## Composition standard (important)
75
+
76
+ Build UI out of these components — **never** raw `<button>` / `<select>` / `<input>` / `<textarea>`
77
+ for things the library covers. A raw interactive element where a library component fits is a bug: you
78
+ lose the theme, the axes, focus states, and a11y. Compound pieces are already composed this way
79
+ (e.g. DataTable's controls are `<Button variant="plain">` + `<Select size="sm">`).
80
+
81
+ ## Toasts
82
+
83
+ Mount `<Toaster />` once at the app root, then push from anywhere with `toast.success(...)` /
84
+ `toast.error(...)` / etc. — no prop plumbing:
85
+
86
+ ```svelte
87
+ <script>
88
+ import { Toaster, toast } from "@spaethtech/svelte-ui";
89
+ </script>
90
+ <Toaster />
91
+ <Button text="Go" onclick={() => toast.success("Saved")} />
92
+ ```
93
+
94
+ ## Component catalog
95
+
96
+ All from `@spaethtech/svelte-ui` (see the shipped `docs/components.md` + `docs/usage.md` for props +
97
+ examples):
98
+
99
+ - **Form:** `Button` `ButtonDropdown` `Input` `Select` `List` `TextArea` `Checkbox` `Toggle` `Rating`
100
+ - **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput`
101
+ - **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`
102
+ - **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
103
+ (query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`).
104
+ - **Overlays:** `Dialog` `Popup` `Menu` · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
105
+ - **Navigation:** `TabStrip` `SideBarMenu`
106
+ - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
107
+ - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter`
108
+ - **Display / theme:** `NotesEditor` `ThemeSelector`
109
+ - **Types / utils:** `Variant` `Size` `TabDefinition` (types), `responsiveClasses` / `resolveScalar`,
110
+ `BREAKPOINT_PX`.
111
+
112
+ ## Adopting updates
113
+
114
+ The lib is versioned on npm. Pin a caret range (`^x.y.z`) and `npm update` for fixes; a `dev` dist-tag
115
+ tracks the newest `main` build (`npm install @spaethtech/svelte-ui@dev`) for testing unreleased work.
116
+ Pre-1.0, a **minor** bump can be breaking — check the package's `CHANGELOG.md` before bumping the range.
@@ -0,0 +1,303 @@
1
+ ---
2
+ name: themes
3
+ description: How svelte-ui theming works and how to build/consume a theme — the --ui-* token surface, the --ui-accent interactive-tint system + variants, light/dark application, scoped "inverse" surfaces, the custom-property freeze rule, and bridging to another design system. Use when swapping palette/sizing, tinting a surface by variant, putting components on an inverse (dark-on-light / light-on-dark) surface, or bridging svelte-ui to DaisyUI/shadcn/brand tokens.
4
+ ---
5
+
6
+ # Theming svelte-ui
7
+
8
+ svelte-ui themes via **plain CSS custom properties** — no Sass, no JS theme object, no
9
+ compile step. Every component reads a small set of **`--ui-*`** tokens; redefine those and
10
+ every component follows. Two built-in themes, **light** (default) and **dark**, follow the
11
+ system preference and can be forced with the `.theme-light` / `.theme-dark` classes.
12
+
13
+ Authoritative source: [`src/lib/theme.css`](../../src/lib/theme.css). Everything below reflects
14
+ that file — if they ever disagree, the file wins and this skill is stale.
15
+
16
+ ## The token surface — everything is `--ui-*`
17
+
18
+ There are **no unprefixed tokens** and **no component-scoped tokens**. Components consume the
19
+ shared vars directly. (The old `--color-*` palette, `--ui-padding*`/`--ui-border` density, and
20
+ per-component `--dt-*`/`--card-*` namespaces have all been removed — don't reintroduce them.)
21
+
22
+ ### Palette — `--ui-color-*`
23
+
24
+ ```
25
+ --ui-color-background --ui-color-text
26
+ --ui-color-primary --ui-color-secondary
27
+ --ui-color-success --ui-color-warning
28
+ --ui-color-error --ui-color-info
29
+ ```
30
+
31
+ These are the only palette colours a component references. The `variant` axis
32
+ (`types/variants.ts` → `variantToken`) maps each `Variant` to one of these (`danger`→`error`; the two
33
+ non-accent variants `neutral` and `ghost` → `--ui-color-text`). Canonical order:
34
+ `neutral · primary · secondary · success · info · warning · danger · ghost`.
35
+
36
+ - `ghost` — borderless, transparent, hover-tint only (the minimal control).
37
+ - `neutral` — the _uncoloured_ variant (mixes from `--ui-color-text`): **solid** in solid contexts
38
+ (Button/Toggle/Banner → black-on-white / white-on-black), a **tint** in soft contexts (Badge/Alert).
39
+ - (`plain` was removed — it's now `ghost`. Old bordered-`ghost` → `neutral`.)
40
+
41
+ ### Interactive-surface tints — `--ui-accent` + hover/surface/active
42
+
43
+ One tint language for every hover / section / pressed surface, three steps of the SAME mix:
44
+
45
+ ```
46
+ --ui-color-hover 4% row / option / ghost-button hover (lightest)
47
+ --ui-color-surface 8% header / footer / section bands (a notch darker)
48
+ --ui-color-active 12% pressed / held (strongest)
49
+ ```
50
+
51
+ Each is `color-mix(in srgb, var(--ui-accent) N%, transparent)`. The percentages are
52
+ **theme-independent** (identical in light and dark); only the accent colour flips. `--ui-accent`
53
+ **defaults to `--ui-color-text`**, so out of the box these are neutral grey tints. Point
54
+ `--ui-accent` at a semantic colour and all three follow — that is how a **variant tints a whole
55
+ surface** (see the accent system below).
56
+
57
+ ### Tinted-surface scale — `--ui-tint-*`
58
+
59
+ A variant-_tinted_ surface (a Badge chip, a Card frame, a `filled` Alert) mixes the component's OWN
60
+ variant token at shared strengths, so every tinted surface is calculated identically:
61
+
62
+ ```
63
+ --ui-tint-soft 12% soft resting fill (Badge fill, Alert `filled`)
64
+ --ui-tint-border 50% bordered surface, REST (Badge/Alert/Banner ring · Card/DataTable/Input/
65
+ TextArea frame · Button outline)
66
+ --ui-tint-border-hover 80% bordered field, HOVER (Input/TextArea/Card/DataTable/Button — the
67
+ stronger step on hover/focus)
68
+ --ui-tint-strong 80% foreground text (Badge)
69
+ ```
70
+
71
+ e.g. `color-mix(in srgb, var(<variant-token>) var(--ui-tint-soft), transparent)`. A `primary` badge is
72
+ primary@these; a `neutral` one is text@these. Theme-independent (a theme MAY override them to tune the
73
+ whole tint language). **Every bordered surface uses ONE calc** — `--ui-tint-border` at rest,
74
+ `--ui-tint-border-hover` on hover/focus, from the variant token — with **no `secondary` special case**.
75
+ `--ui-border-color-strong`/`-strong-hover` have been **REMOVED** (migrate any consumer use to
76
+ `color-mix(in srgb, var(--ui-color-secondary) var(--ui-tint-border), transparent)`, hover →
77
+ `--ui-tint-border-hover`). (Distinct from the interactive tints above, which mix from `--ui-accent`.)
78
+
79
+ ### The `borderless` prop (border-colour swap, never a border-width change)
80
+
81
+ Every surface/framed component (`Button`, `Badge`, `Banner`, `Alert`, `Card`, `Input`, `DataTable`)
82
+ takes a **`borderless`** boolean. The border is **always 1px** — only its _colour_ moves, so toggling
83
+ it can't shift the box by a pixel:
84
+
85
+ - `borderless={true}` → border-colour = the component's **own background** (ring hidden, space kept).
86
+ - `borderless={false}` → border-colour = the **standard tint-border** (`--ui-tint-border` at rest,
87
+ `--ui-tint-border-hover` on hover/focus for the interactive fields) — the same calc for every surface.
88
+
89
+ Defaults differ by component (fill-y things default `true`: Button/Badge/Banner; framed things default
90
+ `false`: Alert/Card/Input/DataTable). One consequence worth knowing: a **`ghost` Button with
91
+ `borderless={false}`** is the **outline button** (transparent fill + a `--ui-color-text` tint edge) —
92
+ there is deliberately no separate `outline`/`ghost-bordered` variant. Toggle/Checkbox intentionally do
93
+ NOT take `borderless`.
94
+
95
+ ### Border / radius — `--ui-border-*`
96
+
97
+ ```
98
+ --ui-border-radius corner radius — ONE fixed value for every rectangular surface; NOT
99
+ scaled by `size`, no -sm/-lg (pills use rounded-full)
100
+ --ui-border-color soft internal divider (row separators, section splits)
101
+ (--ui-border-color-strong/-strong-hover were REMOVED — use the
102
+ `--ui-tint-border` / `--ui-tint-border-hover` scheme instead)
103
+ ```
104
+
105
+ ### Density / text / icon — the `size` axis
106
+
107
+ ```
108
+ --ui-height-sm / --ui-height / --ui-height-lg control square heights (sm/md/lg)
109
+ --ui-height-touch explicit 44px touch target (not a size tier)
110
+ --ui-text-sm / --ui-text / --ui-text-lg per-size text
111
+ --ui-icon-sm / --ui-icon / --ui-icon-lg per-size inline icons
112
+ ```
113
+
114
+ The `sm/md/lg` triplets are tied to the `size` prop on every sized component — override one
115
+ tier and every component on that tier shifts. (Per-component _padding/gap_ is NOT a token; it's
116
+ a `Record<Size,string>` Tailwind class map inside each component.)
117
+
118
+ ### Breakpoints (in `@theme`)
119
+
120
+ Narrow-mobile tiers `4xs` 320 / `3xs` 360 / `2xs` 420 / `xs` 480 and ultrawide `3xl` 1920, on
121
+ top of Tailwind's defaults — so `DataTable` column placements can target the full range.
122
+
123
+ ## The accent / variant system
124
+
125
+ `--ui-color-hover/surface/active` mix from `--ui-accent`. To tint a surface, override
126
+ `--ui-accent` — **but you must also RE-MIX** the three tokens where you override it, via the
127
+ `.ui-accent` class:
128
+
129
+ ```html
130
+ <!-- a DataTable, but any surface works the same -->
131
+ <div class="ui-accent" style="--ui-accent: var(--ui-color-primary)">…</div>
132
+ ```
133
+
134
+ `.ui-accent` (defined in `theme.css`) redefines hover/surface/active from the local
135
+ `--ui-accent`. Setting `--ui-accent` **alone is not enough** — see the freeze rule below for
136
+ why. The reference consumer is **`<DataTable variant="…">`**: its root always carries
137
+ `.ui-accent` and, when a `variant` is passed, sets `--ui-accent: var(<variantToken>)`, tinting
138
+ row hover + header/footer bands with that colour. No variant → neutral grey.
139
+
140
+ ### Implementing `variant` tinting in a component (author recipe)
141
+
142
+ Every themed component does this the **same** way (DataTable / List / Menu / CardBody / TabStrip).
143
+ Follow it exactly — do **not** invent a new mechanism:
144
+
145
+ 1. Resolve the variant to its colour token and build an inline-`style` string that sets the shared
146
+ `--ui-accent` — **only for a real accent** (greyscale variants resolve to `--ui-color-text`, the
147
+ default, so leave `--ui-accent` unset):
148
+ ```svelte
149
+ const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : '');
150
+ ```
151
+ 2. Put `.ui-accent` on the component root and apply the style: `class="ui-accent …" style={accentVar}`.
152
+ 3. Everywhere you need the variant colour, read **`var(--ui-accent)`** from a **static** Tailwind
153
+ class — the accent tints (`--ui-color-surface/hover/active`) automatically; for text/borders read
154
+ `var(--ui-accent)` directly, e.g. `[color:var(--ui-accent)]`,
155
+ `[border-color:color-mix(in_srgb,var(--ui-accent)_var(--ui-tint-border),transparent)]`.
156
+
157
+ **Two hard rules — both are silent failures, not `svelte-check` errors:**
158
+
159
+ - **NEVER a per-component custom property.** No `--ui-tab-border`, `--dt-*`, `--card-*`, etc. The
160
+ variant colour lives in the shared `--ui-accent`; components only ever _read_ it. (See "Extending
161
+ upstream" — lift a hard-coded value into a shared `--ui-*` token, never a component namespace.)
162
+ - **NEVER build a Tailwind class by interpolating a token/value at runtime.** Tailwind only emits
163
+ arbitrary-property CSS for class strings it finds **literally in source**. A runtime-built
164
+ `` `[--ui-accent:var(${token})]` `` or `` `[border-color:${x}]` `` is never scanned → the class
165
+ applies at runtime but **no CSS backs it**, so it silently does nothing (every variant falls back to
166
+ neutral). Two consequences: dynamic custom-prop values go through inline **`style`** (step 1), and
167
+ repeated `color-mix(…var(--ui-accent)…)` classes are written out **in full**, per state, not
168
+ factored into a `const` and interpolated.
169
+
170
+ ## Applying a theme
171
+
172
+ ### Light / dark, and the switcher rule
173
+
174
+ `.theme-light` / `.theme-dark` are set on **`<html>` (documentElement)** — matching the
175
+ pre-hydration script in `app.html` and the system `@media (prefers-color-scheme)` fallback. A
176
+ theme switcher **must toggle the class on `documentElement`, never `<body>`** (`ThemeSelector`
177
+ does this). Putting it on `<body>` leaves `<html>` on the system path and freezes the
178
+ accent-derived tints to the wrong theme (see below).
179
+
180
+ ### Global palette override (whole app)
181
+
182
+ Set `--ui-color-*` at `:root` after importing the theme:
183
+
184
+ ```css
185
+ @import "tailwindcss";
186
+ @import "svelte-ui/theme.css";
187
+ :root {
188
+ --ui-color-primary: #ff6600;
189
+ --ui-color-background: #0d1117;
190
+ --ui-color-text: #e6edf3;
191
+ }
192
+ ```
193
+
194
+ ### Scoped override / **inverse surfaces** (a subtree)
195
+
196
+ Because the theme classes are ordinary CSS, apply one to **any container** to re-theme just
197
+ that subtree. This is the blessed pattern for an **inverse surface** — e.g. a dark header on a
198
+ light page, where component text/icons would otherwise be dark-on-dark and invisible:
199
+
200
+ ```svelte
201
+ <header class="theme-dark bg-black">
202
+ <Button variant="primary">…</Button>
203
+ <!-- text/icons resolve LIGHT → visible -->
204
+ <ThemeSelector />
205
+ </header>
206
+ ```
207
+
208
+ Every svelte-ui component inside inherits the subtree's tokens (text flips to light, hover/
209
+ surface/active/accent all re-mix for the dark surface). **Prefer this over a per-component
210
+ `inverse` prop** — one class covers the whole subtree, composes at any depth, and adds no API
211
+ surface. For a single control, put the class on its nearest wrapper. This only works correctly
212
+ because the tint tokens are defined **per theme scope** (the freeze rule) — without that,
213
+ `.theme-dark` on a subtree would leave hover/surface frozen to `:root`.
214
+
215
+ ## The custom-property freeze rule (the mechanic behind everything above)
216
+
217
+ A custom property whose value contains `var(...)` is resolved **at the element where it is
218
+ DEFINED**, then inherited already-resolved. A descendant that overrides the referenced variable
219
+ does **not** retroactively re-resolve it. Verified: `--m: color-mix(var(--acc)…)` at `:root`
220
+ with `--acc` overridden on a child stays the `:root` colour; redefining `--m` in the child's
221
+ scope picks up the child's `--acc`.
222
+
223
+ Two consequences baked into `theme.css`:
224
+
225
+ 1. **Tints are repeated in EVERY theme scope** (`:root`, `@media (prefers-color-scheme:dark)
226
+ :root`, `.theme-light`, `.theme-dark`) rather than defined once at `:root` — so they
227
+ re-resolve against whichever theme (and `--ui-accent`) is in scope. This is why scoped
228
+ themes and the switcher-on-`<html>` rule work, and why a switcher on `<body>` breaks them.
229
+ 2. **Overriding `--ui-accent` requires the `.ui-accent` re-mix class** — the override alone
230
+ can't re-tint tokens computed higher up.
231
+
232
+ Also: these tints are **plain `:root`/class CSS, not `@theme`**. Tailwind v4 only emits an
233
+ `@theme` variable that backs a recognised utility; it never looks inside arbitrary classes like
234
+ `[background-color:var(--ui-color-hover)]`, so an `@theme` home would get them tree-shaken out
235
+ of the final bundle (the symptom: hover works off the SSR critical CSS during load, then dies).
236
+
237
+ ## The self-reference trap
238
+
239
+ CSS silently invalidates a custom property that resolves to a `var()` of **itself**:
240
+
241
+ ```css
242
+ /* WRONG — becomes guaranteed-invalid/unset; components reading it lose their colour */
243
+ .theme-x {
244
+ --ui-color-success: var(--ui-color-success);
245
+ }
246
+ ```
247
+
248
+ To keep an inherited value, **omit the line entirely** — `--ui-*` already inherits from the
249
+ outer scope. Only list a token when you want a _different_ value. If the outer scope doesn't
250
+ define it at all, either import `svelte-ui/theme.css` globally so `:root` has the full palette,
251
+ or give a literal value in the wrapper.
252
+
253
+ ## Building a custom theme / bridging another design system
254
+
255
+ The common case isn't picking colours from scratch — it's **aliasing svelte-ui's `--ui-*`
256
+ tokens to a palette already loaded in the host** (DaisyUI, shadcn, MUI, brand vars). Because
257
+ custom properties resolve at use-time, the host system's cascade applies automatically.
258
+
259
+ ```css
260
+ /* scoped bridge onto a host's tokens */
261
+ .theme-myapp {
262
+ --ui-color-background: var(--color-base-100); /* host token */
263
+ --ui-color-text: var(--color-base-content);
264
+ /* only list what differs — the rest inherit; NEVER self-reference (see the trap) */
265
+ --ui-height-sm: 1.75rem;
266
+ --ui-height: 2.25rem;
267
+ --ui-height-lg: 2.75rem; /* match host control heights */
268
+ }
269
+ ```
270
+
271
+ Apply globally at `:root` (svelte-ui _is_ the design system) or scoped on a wrapper subtree
272
+ (svelte-ui opted into per route/component while the rest of the app uses the host system).
273
+
274
+ ## Extending upstream instead of forking
275
+
276
+ If a component hard-codes something you need to theme, **lift it into a shared `--ui-*` token
277
+ in `theme.css`** with the current literal as the default (zero visual diff), then reference the
278
+ token from the component via arbitrary-value syntax (`[background-color:var(--ui-color-surface)]`).
279
+ Do **not** add a new component-scoped namespace — the `--dt-*`-style per-component tokens were
280
+ removed in favour of the shared surface language (`--ui-color-hover/surface/active`, `--ui-accent`).
281
+ Verify light + dark before/after.
282
+
283
+ ## Consumer note (Shearwood Creek)
284
+
285
+ SC consumes these `--ui-*` tokens **directly** — there is no `--color-*`/`--border-*` alias
286
+ layer (it was removed). Per-tenant branding overrides `--ui-color-primary`/`--ui-color-secondary`
287
+ at `html.theme-light,html.theme-dark` (see SC `$lib/branding.ts`); neutrals and semantic colours
288
+ stay theme-controlled.
289
+
290
+ ## Authoring checklist
291
+
292
+ 1. Scope: global (`:root`) or wrapped (`.theme-myname` / an inverse-surface container).
293
+ 2. Palette: only list `--ui-color-*` that differ; inherit the rest (no self-reference).
294
+ 3. Density: override `--ui-height*` only if controls must differ; most themes inherit.
295
+ 4. Accent/variant: to tint a surface, set `--ui-accent` **and** add `.ui-accent`.
296
+ 5. Light + dark: test both (`prefers-color-scheme`) if you don't pin values.
297
+ 6. Hit a hard-code? Extend `theme.css` + the component with a shared token first.
298
+
299
+ ## Reference
300
+
301
+ - Source of truth — [`src/lib/theme.css`](../../src/lib/theme.css)
302
+ - Variant → colour map — [`src/lib/types/variants.ts`](../../src/lib/types/variants.ts)
303
+ - Public token docs — [`docs/themes.md`](../../docs/themes.md)
package/CLAUDE.md ADDED
@@ -0,0 +1,248 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code when working with the svelte-ui component library.
4
+
5
+ ## Before every commit (REQUIRED)
6
+
7
+ This library is consumed by multiple external projects (each with its own Claude Code agent) that
8
+ adopt changes by reading our docs — **not** by reverse-engineering our git log. So a change is not
9
+ "done" until its documentation is too. Before you commit, ensure ALL of these are current:
10
+
11
+ - **`CHANGELOG.md`** — add/extend the entry. Any **breaking** change (renamed/removed token, changed
12
+ variant look, removed prop) MUST get a `### Migration` block with a **runnable codemod** (grep +
13
+ sed/perl) and a verify step, and bumps the MINOR version (pre-1.0). Silent visual breaks especially
14
+ — note them explicitly (they aren't `svelte-check` errors).
15
+ - **Skills** (`.claude/skills/**`, esp. `themes`) — the source of truth for the token/variant/theming
16
+ model. Keep token names, the variant taxonomy, and examples in sync.
17
+ - **Docs** (`docs/*.md`) and any **component spec** — keep API tables, code examples, and CSS-variable
18
+ references matching the code.
19
+
20
+ If a commit changes the token surface, a variant, a prop, or a component's behaviour, the matching
21
+ doc/skill/CHANGELOG edit goes in the SAME commit (or the commit isn't ready).
22
+
23
+ ## Project Overview
24
+
25
+ **svelte-ui** is a reusable Svelte 5 (runes) component library styled with Tailwind CSS v4 and MDI
26
+ icons (via `unplugin-icons`). All components are theme-aware using CSS variables, and most consume
27
+ the shared `variant` (color/style) and `size` (sm/md/lg) axes.
28
+
29
+ ## Quick Reference
30
+
31
+ ### Interactive Commands
32
+
33
+ - `/specify <name>` - Create component specification interactively
34
+ - `/implement <name>` - Implement component from specification
35
+ - `/validate <name>` - Validate implementation against specification
36
+
37
+ ### Documentation
38
+
39
+ - `docs/components.md` - Complete component list and details
40
+ - `docs/usage.md` - Code examples for all components
41
+ - `docs/themes.md` - CSS variables and theming guide
42
+ - `docs/paradigm.md` - Development paradigm and workflow
43
+
44
+ **Consumer-facing surfaces that ship in the npm package** (via the `files` whitelist — `dist` + these):
45
+ `CLAUDE.md`, `docs/`, and two skills — **`.claude/skills/svelte-ui`** (how to USE the library) and
46
+ **`.claude/skills/themes`** (theming). Consumers symlink those two skills into their own
47
+ `.claude/skills/` so their Claude Code agents can invoke them. Keep both accurate when the component
48
+ set / axes / token model change. The other skills (`demo`, `playwright-cli`) are internal-only and are
49
+ NOT shipped.
50
+
51
+ ### Tech Stack
52
+
53
+ - Svelte 5 with runes (`$state`, `$props`, `$bindable`)
54
+ - TypeScript with strict typing
55
+ - Tailwind CSS v4 (peer dependency)
56
+ - MDI icons via `unplugin-icons` + `@iconify-json/mdi` (peer deps) — rendered into snippets (`~icons/mdi/*`)
57
+
58
+ ### Project Structure
59
+
60
+ ```
61
+ src/lib/components/
62
+ ├── ComponentName/ # Colocated components
63
+ │ ├── ComponentName.svelte
64
+ │ ├── ComponentName.spec.md
65
+ │ ├── ComponentName.test.ts
66
+ │ └── index.ts
67
+ └── SimpleComponent.svelte # Non-colocated components
68
+ ```
69
+
70
+ ## Development Workflow
71
+
72
+ ### Spec-Driven Development
73
+
74
+ 1. **Write spec first**: Use `/specify` or create `.spec.md`
75
+ 2. **Implement**: Use `/implement` or code to match spec
76
+ 3. **Validate**: Use `/validate` to ensure compliance
77
+
78
+ ### Key Principles
79
+
80
+ - **Always check existing patterns** before implementing
81
+ - **Use colocated structure** for new components
82
+ - **Follow TypeScript discriminated unions** for variant props
83
+ - **Ensure theme compatibility** with CSS variables
84
+ - **Test edge cases** defined in specifications
85
+
86
+ ### Building & Testing
87
+
88
+ ```bash
89
+ npm run dev # Development server
90
+ npm run package # Build library (the publishable artifact)
91
+ npm run check # TypeScript validation
92
+ npm run test # Run Playwright tests
93
+ ```
94
+
95
+ ### Publish Workflow (changes to `src/lib/**`)
96
+
97
+ The lib is published to npm as **`@spaethtech/svelte-ui`**; downstream apps consume
98
+ it from the registry and adopt changes with `npm update`. The `dist/` artifact
99
+ consumers resolve against is built by **CI at publish time** (the `prepack` script) and
100
+ is NOT tracked by git — **never commit `dist/`**. You do not hand-build/hand-publish;
101
+ pushing the right git ref does it. Full detail (OIDC auth, provenance-off rationale, the
102
+ dev channel) lives in the README "Releasing" section and `.github/workflows/publish.yml`.
103
+
104
+ Two ways a `src/lib` change reaches consumers:
105
+
106
+ 1. **Release → `latest`** (deliberate, versioned):
107
+ 1. Edit `src/lib/**`; `npm run check`.
108
+ 2. Roll `CHANGELOG.md` `[Unreleased]` into the new version heading (pre-1.0:
109
+ breaking changes bump the **MINOR** — see the top-of-file commit rules).
110
+ 3. `npm version <patch|minor|major>` — bumps `package.json` + lockfile and creates
111
+ the `vX.Y.Z` git tag.
112
+ 4. `git push && git push --tags` — the **tag** triggers CI, which type-checks, builds
113
+ `dist/`, and `npm publish`es under `latest`. (Versions are **immutable** — don't
114
+ tag until the CHANGELOG/version are right.)
115
+ 5. Consumers get it via `npm update` (within their caret range), or by bumping the
116
+ range for a new minor.
117
+
118
+ 2. **Dev / canary → `dev`** (automatic): **every push to `main`** publishes a prerelease
119
+ under the `dev` dist-tag (`<next-patch>-dev.<run>.<sha>`). Consumers opt in with
120
+ `npm install @spaethtech/svelte-ui@dev` to try unreleased work. `latest` and caret
121
+ ranges (`^0.3.0`) are untouched, so this never leaks into a normal install.
122
+
123
+ **Local verification / co-development.** To exercise a change in a consumer *before* it's
124
+ on `latest`: after pushing to `main`, install the freshly-published `@dev` build there; or
125
+ link a local checkout with a `file:` dep (`"@spaethtech/svelte-ui": "file:../svelte-ui"`)
126
+ and run `npm run package` here to rebuild the `dist/` the consumer resolves against
127
+ (`npm run dev:linked` does package-watch + the demo). When you can't run the consumer,
128
+ **say so** — `npm run check` here is not a substitute for running the actual code.
129
+
130
+ `npm run build` also runs `vite build` for the demo site (heavier) — use `npm run package`
131
+ for just the library artifact.
132
+
133
+ ### Live UI Testing
134
+
135
+ **IMPORTANT**: Drive the running demo site when working on UI components — verify behavior in a real
136
+ browser, don't rely on `svelte-check` alone.
137
+
138
+ Development server runs at `http://localhost:5173/` (`npm run dev`).
139
+
140
+ Use the **`/playwright-cli`** workflow for browser-driven testing (navigation, snapshots/screenshots,
141
+ console inspection, interaction). Point it at the dev server URL and the relevant component demo
142
+ route (e.g. `/tabstrip`). The old Chrome DevTools MCP (`.mcp.json` + `chrome-devtools-mcp`) has been
143
+ removed in favor of this.
144
+
145
+ ### Demo pages (`src/routes/<component>/`)
146
+
147
+ The **`demo` skill** (`.claude/skills/demo/`) is the source of truth for building/maintaining a
148
+ component's demo route: section structure, the readonly monospace `<TextArea>` code-sample
149
+ convention, the rule that **code samples are complete & runnable** (include the data array + bound
150
+ state + `'svelte-ui'` import, not just the `<Component/>` tag), and using library components (not raw
151
+ `<input>`/`<textarea>`) for demo controls. Read it before editing a route page. (`docs/*` is
152
+ consumer-facing; the working convention lives in the skill.)
153
+
154
+ ## Composition Standard (ABSOLUTE)
155
+
156
+ Compound components are built **exclusively** from our own core/compound components — **never** raw
157
+ `<button>` / `<select>` / `<input>` / `<textarea>`. e.g. DataTable's icon controls + pagination use
158
+ `<Button variant="plain" size="sm">`, per-page uses `<Select size="sm">`; Query is built on `<Input>`
159
+ with its clear/help actions as `<Button>`. A raw interactive element inside a compound component is a bug.
160
+
161
+ Two shared **cross-cutting axes** every sizeable/themeable component consumes (mirror each other):
162
+
163
+ - **`variant`** (`types/variants.ts`) — color/style: `primary` `secondary` `danger` `info` `success`
164
+ `warning` `ghost` `plain`. `plain` = borderless/transparent, for dense icon controls.
165
+ - **`size`** (`types/sizes.ts`) — `sm` (24px) · `md` (32px, **default**) · `lg` (40px), tied to
166
+ `--ui-height-sm` / `--ui-height` / `--ui-height-lg` (`--ui-height-touch` 44px is a separate token
167
+ for explicit touch targets, not `lg`). Each component maps `size` to its height/text/padding/icon,
168
+ the same way it maps `variant` to colors.
169
+
170
+ ## Component Guidelines
171
+
172
+ ### Accessibility
173
+
174
+ - All interactive components need keyboard support
175
+ - Use proper ARIA attributes
176
+ - Focus states with `:focus-visible`
177
+ - Support screen readers
178
+
179
+ ### Styling
180
+
181
+ - Use Tailwind utility classes
182
+ - **Theme colors via `var(--ui-color-*)`** — every token is `--ui-*`-prefixed; there are NO
183
+ unprefixed `--color-*` and NO per-component (`--dt-*`) tokens (both removed). Interactive
184
+ surfaces (hover/section/pressed) use the shared `--ui-color-hover|surface|active` tints, which
185
+ mix from `--ui-accent`. Full model + rules: the **`themes` skill** (`.claude/skills/themes/`).
186
+ - Consistent heights: `--ui-height` (32px); `--ui-height-sm|lg|touch` for the other tiers
187
+ - Padding/gap is per-component (a `Record<Size,string>` class map), **not** a token
188
+
189
+ ### Performance
190
+
191
+ - Minimal DOM nodes
192
+ - Efficient re-renders with Svelte 5 runes
193
+ - Lazy loading for heavy components
194
+ - Proper cleanup in `$effect` returns
195
+
196
+ ## Common Patterns
197
+
198
+ ### Icon Usage
199
+
200
+ ```svelte
201
+ import IconHome from '~icons/mdi/home';
202
+ <!-- Render into a component's `icon` snippet — the lib sizes + colors it. -->
203
+ <Button title="Home">{#snippet icon()}<IconHome />{/snippet}</Button>
204
+ ```
205
+
206
+ ### Theme-Aware Colors
207
+
208
+ ```svelte
209
+ class="text-[var(--ui-color-text)] bg-[var(--ui-color-background)]"
210
+ ```
211
+
212
+ For hover/section/pressed surfaces use the shared tints, not ad-hoc `color-mix`:
213
+
214
+ ```svelte
215
+ class="[background-color:var(--ui-color-surface)] hover:[background-color:var(--ui-color-hover)]"
216
+ ```
217
+
218
+ ### Component Props Interface
219
+
220
+ ```typescript
221
+ import type { Variant } from "../types/variants.js"; // shared color/style axis
222
+ import type { Size } from "../types/sizes.js"; // shared sm/md/lg axis
223
+ interface Props {
224
+ value?: string;
225
+ variant?: Variant;
226
+ size?: Size;
227
+ children?: Snippet;
228
+ }
229
+ let { value = "", variant = "secondary", size = "md", children }: Props = $props();
230
+ ```
231
+
232
+ ## Known Issues (do not "fix" in code)
233
+
234
+ - **White / outline-less `text`/`grab`/`grabbing`/`crosshair` cursor on Windows** is a Chromium +
235
+ Windows **Multiplane Overlay (MPO)** GPU bug (common on AMD), NOT a svelte-ui bug. It's
236
+ independent of `color-scheme`, hydration, and CSS, and reproduces across unrelated sites. Do
237
+ **not** attempt a component-code workaround (a custom `cursor:` harms every healthy user and
238
+ can't be feature-detected). Client fix: DWM `OverlayTestMode=5` + reboot. Full write-up and
239
+ links in README "Known issues" (crbug 40239916 / 339818333).
240
+
241
+ ## Important Notes
242
+
243
+ - **Peer dependencies only** - no runtime dependencies
244
+ - **Test files excluded** from npm package automatically
245
+ - **Documentation in docs/** - Component reference, examples, theme variables
246
+ - **Interactive commands** - Use `/specify`, `/implement`, `/validate` for workflow
247
+
248
+ For detailed information, see the documentation files in the `docs/` folder.