@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.
- package/.claude/skills/svelte-ui/SKILL.md +116 -0
- package/.claude/skills/themes/SKILL.md +303 -0
- package/CLAUDE.md +248 -0
- package/docs/components.md +240 -0
- package/docs/examples.md +323 -0
- package/docs/paradigm.md +240 -0
- package/docs/themes.md +170 -0
- package/docs/usage.md +427 -0
- package/package.json +6 -2
|
@@ -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.
|