@spaethtech/svelte-ui 0.18.1-dev.92.355f4cd → 0.18.1-dev.94.031fb33
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.md +19 -6
- package/docs/themes.md +1 -1
- package/package.json +2 -4
- package/.claude/skills/svelte-ui/SKILL.md +0 -309
- package/.claude/skills/themes/SKILL.md +0 -357
package/CLAUDE.md
CHANGED
|
@@ -41,12 +41,25 @@ the shared `variant` (color/style) and `size` (sm/md/lg) axes.
|
|
|
41
41
|
- `docs/themes.md` - CSS variables and theming guide
|
|
42
42
|
- `docs/paradigm.md` - Development paradigm and workflow
|
|
43
43
|
|
|
44
|
-
**Consumer-facing surfaces
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
**Consumer-facing surfaces.** The npm package ships (via the `files` whitelist) `dist`, `CLAUDE.md`
|
|
45
|
+
and `docs/` only. The two **consumer skills** are no longer bundled in the tarball — they are
|
|
46
|
+
**published to the private `spaethtech/claude-plugins` marketplace** as the **`svelte-ui`** plugin, so
|
|
47
|
+
a consumer installs them with `claude plugin install svelte-ui@spaethtech-plugins` (then restarts the
|
|
48
|
+
session) instead of symlinking. They arrive namespaced: **`svelte-ui:usage`** (how to USE the library —
|
|
49
|
+
source: `.claude/skills/svelte-ui/`) and **`svelte-ui:themes`** (theming — source:
|
|
50
|
+
`.claude/skills/themes/`). Keep both accurate when the component set / axes / token model change, then
|
|
51
|
+
**republish** (see "Publishing skills" below). The other skills (`demo`, `playwright-cli`) are
|
|
52
|
+
internal-only and are NOT published.
|
|
53
|
+
|
|
54
|
+
**Publishing skills (after editing `.claude/skills/svelte-ui` or `.claude/skills/themes`).** This
|
|
55
|
+
repo is a marketplace publishing project — its `.claude-plugin/plugin.json` (the card) +
|
|
56
|
+
`.claude-plugin/skills.json` (maps `svelte-ui`→`usage`, `themes`→`themes`) drive
|
|
57
|
+
`~/claude-plugins/bin/skills.sh`. A skill edit only reaches consumers after a **version bump +
|
|
58
|
+
republish**: `~/claude-plugins/bin/skills.sh bump [patch|minor|major] --project .`, commit + push this
|
|
59
|
+
repo, then `~/claude-plugins/bin/skills.sh publish --project .` (dry-run first). Consumers update by
|
|
60
|
+
version, so a republish without a bump is refused. `minor` for a real capability change, `major` only
|
|
61
|
+
if a published skill is renamed/removed (breaks invoke-by-name). See
|
|
62
|
+
`plugins/marketplace/skills/publishing/SKILL.md` in the marketplace for the full contract.
|
|
50
63
|
|
|
51
64
|
### Tech Stack
|
|
52
65
|
|
package/docs/themes.md
CHANGED
|
@@ -172,7 +172,7 @@ tint tokens are re-defined per theme scope; see the themes skill.
|
|
|
172
172
|
This page documents the token _surface_. For the authoring side — swapping palette/sizing, tinting
|
|
173
173
|
by variant (`--ui-accent` + `.ui-accent`), scoping/inverse surfaces, the custom-property freeze
|
|
174
174
|
rule, the self-reference trap, and bridging to another design system (DaisyUI, shadcn, …) — see the
|
|
175
|
-
themes skill
|
|
175
|
+
**`svelte-ui:themes`** skill (install with `claude plugin install svelte-ui@spaethtech-plugins`).
|
|
176
176
|
|
|
177
177
|
> **Any scope that overrides `--ui-color-*` must also restate `--ui-color-neutral`** (and re-derive
|
|
178
178
|
> `--ui-border-color`). Neutral borders/surfaces derive from `--ui-color-neutral`, which — by the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spaethtech/svelte-ui",
|
|
3
|
-
"version": "0.18.1-dev.
|
|
3
|
+
"version": "0.18.1-dev.94.031fb33",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/spaethtech/svelte-ui.git"
|
|
@@ -35,9 +35,7 @@
|
|
|
35
35
|
"!dist/**/*.test.*",
|
|
36
36
|
"!dist/**/*.spec.*",
|
|
37
37
|
"CLAUDE.md",
|
|
38
|
-
"docs"
|
|
39
|
-
".claude/skills/themes",
|
|
40
|
-
".claude/skills/svelte-ui"
|
|
38
|
+
"docs"
|
|
41
39
|
],
|
|
42
40
|
"sideEffects": [
|
|
43
41
|
"**/*.css"
|
|
@@ -1,309 +0,0 @@
|
|
|
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 — the **`@spaethtech/svelte-ui`** barrel, or a **per-component subpath**
|
|
11
|
-
(`@spaethtech/svelte-ui/button`) — never a raw `dist/` path.
|
|
12
|
-
|
|
13
|
-
> Depth lives in the package's shipped docs (`node_modules/@spaethtech/svelte-ui/docs/*.md`) and this
|
|
14
|
-
> repo's `CLAUDE.md`. Theming (tokens, dark mode, inverse surfaces, brand bridging) → the **`themes`**
|
|
15
|
-
> skill. This skill is the usage mental-model + patterns; read the docs for full per-component APIs.
|
|
16
|
-
|
|
17
|
-
## One-time setup (per consumer project)
|
|
18
|
-
|
|
19
|
-
Peer deps (not bundled): `svelte@^5`, `tailwindcss@^4`, `unplugin-icons@^23`, `@iconify-json/mdi@^1`.
|
|
20
|
-
|
|
21
|
-
**1. Vite — add the icon resolver** (`vite.config.ts`), because the package ships bare `~icons/mdi/*`
|
|
22
|
-
imports that resolve at the consumer's build time:
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
import Icons from "unplugin-icons/vite";
|
|
26
|
-
export default defineConfig({
|
|
27
|
-
plugins: [/* sveltekit(), tailwindcss(), */ Icons({ compiler: "svelte" })],
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**2. CSS** (`app.css`, Tailwind v4) — import the theme and scan the dist so Tailwind emits the classes
|
|
32
|
-
the components use:
|
|
33
|
-
|
|
34
|
-
```css
|
|
35
|
-
@import "tailwindcss";
|
|
36
|
-
@import "@spaethtech/svelte-ui/theme.css";
|
|
37
|
-
@source "../node_modules/@spaethtech/svelte-ui/dist"; /* path relative to this file */
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
> **Missing borders/styles after `npm update`?** Two causes: (1) the `@source` line above is absent or
|
|
41
|
-
> its path is wrong, so Tailwind never emits the components' (arbitrary) classes — fix the glob. (2) A
|
|
42
|
-
> version bumped a class string (e.g. 0.17 rewrote the neutral border classes), and Tailwind's build
|
|
43
|
-
> **cached** the old scan — restart the dev server / clear the Vite+Tailwind cache and rebuild. If a
|
|
44
|
-
> border resolves to the wrong colour (not missing), it's a **theme** issue — see the `themes` skill's
|
|
45
|
-
> "restate `--ui-color-neutral` per scope" note.
|
|
46
|
-
|
|
47
|
-
## Import & basic use
|
|
48
|
-
|
|
49
|
-
```svelte
|
|
50
|
-
<script>
|
|
51
|
-
import { Button, Input, Select } from "@spaethtech/svelte-ui";
|
|
52
|
-
let name = $state("");
|
|
53
|
-
</script>
|
|
54
|
-
|
|
55
|
-
<Input bind:value={name} placeholder="Name" />
|
|
56
|
-
<Button text="Save" variant="primary" onclick={() => save(name)} />
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Barrel vs. per-component subpaths (dev cold-start / memory)
|
|
60
|
-
|
|
61
|
-
Two import styles, both resolving to the same source (identical output, identical HMR-into-lib):
|
|
62
|
-
|
|
63
|
-
- **Barrel** — `import { Button, Input } from "@spaethtech/svelte-ui"`. Convenient, but importing
|
|
64
|
-
*anything* from the barrel pulls *all* 71 component `.svelte` files into the dev compile graph (Vite
|
|
65
|
-
doesn't tree-shake a barrel in `dev`), so the dev server compiles the whole library on cold start.
|
|
66
|
-
Prod builds tree-shake normally, so this is a dev-time cost only.
|
|
67
|
-
- **Per-component subpath** — `import Button from "@spaethtech/svelte-ui/button"`. Kebab-case of the
|
|
68
|
-
component name (`DataTable` → `/data-table`, `SideBarMenu` → `/side-bar-menu`, `DatePicker` →
|
|
69
|
-
`/date-picker`; multi-export folders keep their group, e.g. `Avatar`/`AvatarGroup` at `/avatar`).
|
|
70
|
-
Only the components you actually import get compiled. **On a memory-constrained dev host, prefer
|
|
71
|
-
subpaths exclusively** — it's the single biggest lever on svelte-ui's cold-start footprint.
|
|
72
|
-
|
|
73
|
-
Non-component runtime **values** have their own component-free entries so they don't drag the barrel
|
|
74
|
-
in either: **`@spaethtech/svelte-ui/utils`** (`toast`, `anchored`/`tooltip`, `setUiI18n`/`uiI18n`,
|
|
75
|
-
`responsiveClasses`, …) and **`@spaethtech/svelte-ui/data`** (`createGrid`, query AST). **Types** are
|
|
76
|
-
free from anywhere — `import type { Variant } from "@spaethtech/svelte-ui"` is erased at compile time
|
|
77
|
-
and pulls no components, so type-only imports may stay on the barrel.
|
|
78
|
-
|
|
79
|
-
```svelte
|
|
80
|
-
<script>
|
|
81
|
-
import Button from "@spaethtech/svelte-ui/button";
|
|
82
|
-
import DataTable from "@spaethtech/svelte-ui/data-table";
|
|
83
|
-
import { toast } from "@spaethtech/svelte-ui/utils";
|
|
84
|
-
import { createGrid } from "@spaethtech/svelte-ui/data";
|
|
85
|
-
import type { Variant } from "@spaethtech/svelte-ui"; // type-only ⇒ free, no compile
|
|
86
|
-
</script>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Localization (i18n) — opt-in
|
|
90
|
-
|
|
91
|
-
Components render their own microcopy (aria-labels, titles, "per page", "No results", month names…)
|
|
92
|
-
through a message registry with **English defaults**. Do nothing → identical English output. To
|
|
93
|
-
localize, call **`setUiI18n`** ONCE near the app root (a layout); partial `messages` merge over the
|
|
94
|
-
defaults, so translate a subset and the rest stays English. `locale` also feeds the date components'
|
|
95
|
-
`Intl.*` formatting (falls back to `navigator.language` when unset; a per-instance `locale` prop still
|
|
96
|
-
overrides).
|
|
97
|
-
|
|
98
|
-
```svelte
|
|
99
|
-
<script>
|
|
100
|
-
import { setUiI18n } from "@spaethtech/svelte-ui";
|
|
101
|
-
import { getLocale } from "$lib/paraglide/runtime";
|
|
102
|
-
import { m } from "$lib/paraglide/messages";
|
|
103
|
-
setUiI18n({
|
|
104
|
-
locale: () => getLocale(), // getter ⇒ reactive; or a plain string
|
|
105
|
-
messages: () => ({
|
|
106
|
-
dismiss: m.ui_dismiss(),
|
|
107
|
-
selectAll: m.ui_select_all(),
|
|
108
|
-
rowRange: ({ start, end, total }) => m.ui_row_range({ start, end, total }), // "1–10 of 247"
|
|
109
|
-
removeFile: ({ name }) => m.ui_remove_file({ name }),
|
|
110
|
-
// …any subset of `UiMessages`
|
|
111
|
-
}),
|
|
112
|
-
});
|
|
113
|
-
</script>
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Parameterized entries are functions (`rowRange`, `removeFile`) — 1:1 with Paraglide's `m.key(params)`.
|
|
117
|
-
Values may be plain or getters (getters make components re-render on locale change; plain is fine for
|
|
118
|
-
per-navigation locale). Full key list: the `UiMessages` type; `defaultUiMessages` is the English source.
|
|
119
|
-
Strings already exposed as PROPS (`Select placeholder`, `EmptyState title`, `CommandPalette empty`…) you
|
|
120
|
-
localize directly on the component. **Authoring rule (this repo):** every new user-facing string a
|
|
121
|
-
component renders must go through `uiI18n().messages` + `defaultUiMessages`, never a bare literal.
|
|
122
|
-
|
|
123
|
-
## The two shared axes (mirror each other)
|
|
124
|
-
|
|
125
|
-
- **`variant`** — color/style: `primary` `secondary` `danger` `info` `success` `warning` `ghost`
|
|
126
|
-
`plain` (`neutral` on some components). `plain` = borderless/transparent, for dense icon controls.
|
|
127
|
-
- **`size`** — `sm` (24px) · `md` (32px, **default**) · `lg` (40px). Consistent height across every
|
|
128
|
-
sizeable component, so a `Button`, `Input`, and `Select` at the same `size` line up.
|
|
129
|
-
|
|
130
|
-
Both accept a responsive map too, e.g. `size={{ base: "sm", md: "lg" }}`.
|
|
131
|
-
|
|
132
|
-
## Field chrome (label / help / error)
|
|
133
|
-
|
|
134
|
-
Every field-shaped control — `Input`, `Select`, `TextArea`, `Checkbox`, `Toggle`, and the specialized
|
|
135
|
-
inputs — renders its own label + help/error frame from a shared set of props. **Don't hand-wrap fields
|
|
136
|
-
in `<label>` + hint spans** — use these:
|
|
137
|
-
|
|
138
|
-
- **`label`** (string) — rendered **above** the control, left-aligned. Uniform for *every* control
|
|
139
|
-
(checkbox/toggle labels sit above too), so a row of mixed controls at the same `size` lines up
|
|
140
|
-
pixel-perfect. For any other placement, omit `label` and bring your own `<label>`.
|
|
141
|
-
- **`aside`** (Snippet) — right-aligned in the label row: a char count, status badge, link, button.
|
|
142
|
-
- **`error`** (string | Snippet) — inline error row; also flips the error border + `aria-invalid`.
|
|
143
|
-
String is rendered as `{@html}`.
|
|
144
|
-
- **`description`** (string | Snippet) — muted help row below the error (they coexist). String is
|
|
145
|
-
`{@html}` — **author-controlled markup only**, never untrusted input (use the Snippet form for that).
|
|
146
|
-
- **`required`** — appends `*` to the label + sets `aria-required`.
|
|
147
|
-
- **`id`** — auto-generated if omitted; wires `<label for>` + `aria-describedby`.
|
|
148
|
-
|
|
149
|
-
`label`/aria wiring is automatic. Omit all of them and the field renders bare, exactly as a plain
|
|
150
|
-
control.
|
|
151
|
-
|
|
152
|
-
```svelte
|
|
153
|
-
<Input
|
|
154
|
-
label="Slug"
|
|
155
|
-
description="Lowercase, hyphens only."
|
|
156
|
-
error={slugTaken ? "That slug is already taken." : null}
|
|
157
|
-
bind:value={slug}
|
|
158
|
-
/>
|
|
159
|
-
|
|
160
|
-
<!-- Mixed controls in a grid all align on the same label row + control baseline: -->
|
|
161
|
-
<div class="grid grid-cols-4 gap-4 items-start">
|
|
162
|
-
<Input label="Name" bind:value={name} />
|
|
163
|
-
<Select label="Role" {options} bind:value={role} />
|
|
164
|
-
<Toggle label="Notifications" bind:checked={notify} />
|
|
165
|
-
<Checkbox label="Agree" bind:checked={agree} required />
|
|
166
|
-
</div>
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
**Validation vs. error:** the built-in `validate={(v) => true | "message"}` (on Input/TextArea)
|
|
170
|
-
surfaces through the **native validity tooltip** (shown on form submit); the controlled **`error`**
|
|
171
|
-
prop is the inline row. Both flip the field's error border.
|
|
172
|
-
|
|
173
|
-
**Groups:** for a set of radios/checkboxes/toggles under one legend, wrap them in **`FieldGroup`**
|
|
174
|
-
(same `label`/`description`/`error`/`required` vocab on a `<fieldset>`, plus `columns` — a number or
|
|
175
|
-
responsive map; default 1 = stacked, higher lays items across equal auto-placed columns — and `gap`,
|
|
176
|
-
a number or `{ x, y }`). For non-field content, **`Grid`** is the same auto-placement as a plain
|
|
177
|
-
primitive (`columns` + `gap`, any children). Inside a
|
|
178
|
-
group, item labels render **inline** (right of the control). For a radio group, `bind:value` on the
|
|
179
|
-
`FieldGroup` (+ optional `name`) and the child `<Radio>`s share it automatically — no `bind:group`:
|
|
180
|
-
|
|
181
|
-
```svelte
|
|
182
|
-
<FieldGroup label="Delivery" bind:value={method} name="method">
|
|
183
|
-
<Radio value="email" label="Email" />
|
|
184
|
-
<Radio value="sms" label="SMS" />
|
|
185
|
-
</FieldGroup>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Icons — no `<Icon>` component
|
|
189
|
-
|
|
190
|
-
Import an MDI icon component from `~icons/mdi/*` and render it into a component's **`icon` snippet**;
|
|
191
|
-
the library sizes and colors it to match `size`/`variant`:
|
|
192
|
-
|
|
193
|
-
```svelte
|
|
194
|
-
<script>
|
|
195
|
-
import { Button } from "@spaethtech/svelte-ui";
|
|
196
|
-
import IconHome from "~icons/mdi/home";
|
|
197
|
-
</script>
|
|
198
|
-
|
|
199
|
-
<Button title="Home" variant="plain">{#snippet icon()}<IconHome />{/snippet}</Button>
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Composition standard (important)
|
|
203
|
-
|
|
204
|
-
Build UI out of these components — **never** raw `<button>` / `<select>` / `<input>` / `<textarea>`
|
|
205
|
-
for things the library covers. A raw interactive element where a library component fits is a bug: you
|
|
206
|
-
lose the theme, the axes, focus states, and a11y. Compound pieces are already composed this way
|
|
207
|
-
(e.g. DataTable's controls are `<Button variant="plain">` + `<Select size="sm">`).
|
|
208
|
-
|
|
209
|
-
## Toasts
|
|
210
|
-
|
|
211
|
-
Mount `<Toaster />` once at the app root, then push from anywhere with `toast.success(...)` /
|
|
212
|
-
`toast.error(...)` / etc. — no prop plumbing:
|
|
213
|
-
|
|
214
|
-
```svelte
|
|
215
|
-
<script>
|
|
216
|
-
import { Toaster, toast } from "@spaethtech/svelte-ui";
|
|
217
|
-
</script>
|
|
218
|
-
<Toaster />
|
|
219
|
-
<Button text="Go" onclick={() => toast.success("Saved")} />
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Component catalog
|
|
223
|
-
|
|
224
|
-
All from `@spaethtech/svelte-ui` (see the shipped `docs/components.md` + `docs/usage.md` for props +
|
|
225
|
-
examples):
|
|
226
|
-
|
|
227
|
-
- **Form:** `Button` `ButtonGroup` `Input` `Select` `Combobox` `Slider` `List` `TextArea` `Checkbox`
|
|
228
|
-
`Toggle` `Radio` `Rating` (display `average`/`count`, OR `interactive` + `bind:value` star picker
|
|
229
|
-
with `allowHalf`, click/hover/arrow-keys) · **`Combobox`** (free-text autocomplete — arbitrary value + suggestions,
|
|
230
|
-
client `options` or async `suggest`; vs `Select` which is a closed choice. Composes Input + Popup.) ·
|
|
231
|
-
**`TokenInput`** (multi-value entry — Enter/comma adds a removable `Chip` token, Backspace-on-empty
|
|
232
|
-
removes; `bind:values`, `max`, `allowDuplicates`; FieldChrome.) · **`FileUpload`** (drag-drop + click
|
|
233
|
-
dropzone; `bind:files`, `accept`/`multiple`/`maxSize`, image thumbnails, `progress` map → `Progress`
|
|
234
|
-
bar; FieldChrome.) · **`Slider`** (draggable number/range; `range` dual-thumb, `min`/`max`/`step`,
|
|
235
|
-
`showValue`/`format`; FieldChrome + `variant`/`size`; keyboard + `role="slider"`) · **`FieldGroup`**
|
|
236
|
-
(fieldset wrapper for radio/checkbox/toggle sets) · **`ButtonGroup`**
|
|
237
|
-
(joined row/`col` of `<Button>`s as one unit — an action toolbar or a **controlled** segmented
|
|
238
|
-
selector; `items` of `ButtonGroupItem` {`value`,`text`,`icon`,`variant`,`disabled`,`loading`,`href`/
|
|
239
|
-
`target`,`menu`,`menuOnly`,`onclick`} or a presentational default slot; `select`
|
|
240
|
-
`'none'|'single'|'multi'` with `bind:value`/`bind:values`; selected = variant-swap to filled,
|
|
241
|
-
unselected = `ghost`; the group owns the frame. An item `menu` (`MenuItem[]`) makes a **split
|
|
242
|
-
button** (action + caret); add `menuOnly` for a caret-only **menu-trigger** cell (e.g. an
|
|
243
|
-
`[action][▾]` pair = a plain item + a `menuOnly` item). This replaces the old `ButtonDropdown`. Not
|
|
244
|
-
a form field — for legend/`name`/submit use `FieldGroup`; for view switching use `TabStrip`.)
|
|
245
|
-
- **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput` (formatting +
|
|
246
|
-
`percent`/`stepper`/`clamp`/`liveFormat`) `PhoneInput` (stores E.164; dep-free, inject
|
|
247
|
-
`parse`/`format` for per-country)
|
|
248
|
-
- **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`.
|
|
249
|
-
For a start–end range use **`DatePicker range`** (`bind:value` = `{ start, end }`) — it composes
|
|
250
|
-
`Calendar mode="range"`.
|
|
251
|
-
- **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
|
|
252
|
-
(query-language parser/AST via `parseQuery`, `createGrid`, `DataGrid<T>`, `DataSet`). For a
|
|
253
|
-
**client-held array**, use **`localFetch(rows, columns, opts?)`** as the grid's `fetch` — it parses
|
|
254
|
-
the query, filters via **`matchesRow(row, ast)`** (the in-memory AST evaluator: cmp/and/or/not/
|
|
255
|
-
match/in/range/search, honouring column `id`/`searchable`/`sortKey`), then sorts + paginates. Only a
|
|
256
|
-
remote/DB backend needs to compile the AST itself; do NOT hand-roll a "strip `$` + substring" filter.
|
|
257
|
-
`createGrid` also takes **`selectableRow: (row) => boolean`** — rows returning `false` are locked out
|
|
258
|
-
of selection (disabled checkbox, excluded from select-all, never in `grid.selected`), so
|
|
259
|
-
`bulkActions(selected)` never sees them; don't filter locked ids in the handler.
|
|
260
|
-
· **`Pagination`** (standalone
|
|
261
|
-
pager styled like the DataTable footer: tinted band, per-page `Select`, range total, and a
|
|
262
|
-
`First·Prev·[page]/N·Next·Last` stepper whose indicator is an editable `NumberInput` quick-jump;
|
|
263
|
-
`bind:page`/`bind:perPage`, `total`, `perPageOptions`, `showEdges`/`showTotal`, `borderless`;
|
|
264
|
-
`variant` tints band+hover only; built from `Button`+`Select`+`NumberInput`).
|
|
265
|
-
- **Overlays:** `Dialog` (pure modal shell — BYO content; `width`/`height` take any CSS value;
|
|
266
|
-
`onShow(ctx)`/`onHide(ctx)` control focus — set `ctx.autoFocus` to a selector/element/`false`,
|
|
267
|
-
`ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
|
|
268
|
-
· **`Drawer`** (edge-anchored sliding panel / "sheet"; `side` left/right/top/bottom, `size` CSS
|
|
269
|
-
length, `bind:open`, `dismissible`/`backdrop`; same modal mechanics as Dialog — a pure shell, put a
|
|
270
|
-
Card inside) · `Popup` `Menu` · **`ContextMenu`** (right-click menu at the pointer; `items` MenuItem[]; on `Menu`) · **`CommandPalette`** (⌘K/Ctrl+K fuzzy command launcher; `bind:open`, `commands` Command[] {`label`,`keywords`,`group`,`icon`,`shortcut`,`disabled`,`onrun`}, global `hotkey` default `"mod+k"`; ↑/↓/Enter/Esc; on `Input`+`Kbd`) · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
|
|
271
|
-
- **Navigation:** **`Tree`** (hierarchical expandable tree — `items` of `TreeNode`; `bind:expanded`/`selected`; roving-tabindex keyboard; ARIA `tree`) · **`Breadcrumbs`** (trail of `BreadcrumbItem` {`label`,`href`,`onclick`,`icon`}; last =
|
|
272
|
-
current page; `maxItems` collapses the middle to a `⋯` Menu; `separator` slot) · `TabStrip` (tabs;
|
|
273
|
-
`size`/`variant`, `gap`/`pad` bordered spacer cells, `height` for a
|
|
274
|
-
fixed-height bar with bottom-aligned tabs, `activeSurface` to flow the active tab into the content
|
|
275
|
-
below) · **`SideBarMenu`** (data-driven vertical nav: `items`/`bottomItems` of `SideBarMenuItem`
|
|
276
|
-
{`icon`,`label`,`link`,`badge`,`children`,`onclick`,`popout`,`reload`,`trailing` snippet,`activeMatch`};
|
|
277
|
-
responsive `modes` map + `iconHoverExpand`; `variant` tints only hover/active; `size`; `side`;
|
|
278
|
-
`interactMode` `'hover'|'click'` popouts; `currentPath` active-match (per-item `activeMatch` for
|
|
279
|
-
query-keyed/exact rules); `bind:open`/`bind:api`/`bind:activeMode`; `publishWidthVar` → CSS width var;
|
|
280
|
-
popouts expose `data-sbm-popout`/`data-open`)
|
|
281
|
-
- **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast` · **`Kbd`** (keyboard keycap; `text`/`keys` combo) · **`Spinner`** (indeterminate loading
|
|
282
|
-
ring; `variant`/`size`, omit `variant` to inherit `currentColor`. `Button` has a `loading` prop that
|
|
283
|
-
renders one in place of its icon and blocks interaction.) · **`Progress`** (horizontal bar;
|
|
284
|
-
determinate `value`/`max` or `indeterminate` sweep; `variant`/`size` = 4/8/12px; `label`/`showValue`.)
|
|
285
|
-
· **`Skeleton`** (neutral loading placeholder; `shape` text/rect/circle, `lines`, `width`/`height`,
|
|
286
|
-
`animate` shimmer. No `variant` — it's absent-content, not a themed surface. Compose to sketch a card.)
|
|
287
|
-
- **Layout:** `Card` `CardHeader` `CardBody` `CardFooter` · **`Grid`** (auto-placed equal-cell grid;
|
|
288
|
-
`columns` + `gap`) · **`ScrollArea`** (themed thin-scrollbar container; `orientation`, `maxHeight`, `variant` thumb) · **`Splitter`** (two resizable panes + draggable divider; `bind:size` %, `orientation`, keyboard) · **`Carousel`** (sliding viewport; `items` + `slide` snippet, `bind:index`, `autoplay`, controls + dots) · **`Stat`** (metric/KPI card — `label`/`value`/`delta` up-down + `icon`; on `Card`) · **`Timeline`** (vertical event list — `items` of `TimelineItem` {`title`,`time`,`description`,`icon`,`variant`}) · **`Divider`** (thin `role="separator"` rule; `orientation` h/v, optional inline
|
|
289
|
-
`label`+`labelAlign`; `--ui-border-color`)
|
|
290
|
-
- **Flow:** **`Stepper`** (linear multi-step indicator — `steps` of `StepItem` {`label`,`description`,
|
|
291
|
-
`icon`}, `bind:current`, `orientation` h/v, `onstep` clickable; done/current/upcoming markers +
|
|
292
|
-
connectors. Panels + Next/Back are the consumer's.)
|
|
293
|
-
- **Disclosure:** **`Disclosure`** (one expand/collapse section; `title`/`header`, `bind:open`, panel
|
|
294
|
-
stays mounted so state is preserved) · **`Accordion`** (coordinated group — `multiple` single/multi,
|
|
295
|
-
`bind:value`; children are `<Disclosure value="…">`, no per-item `bind:open`)
|
|
296
|
-
- **Display / theme:** **`EmptyState`** (zero-data placeholder — icon + title + description + `action`) · **`Chip`** (interactive pill — `selected` toggle fill, `removable` ×/`onremove`,
|
|
297
|
-
`onclick`/`href`, `icon`; the interactive counterpart to display-only `Badge`) · **`Avatar`** (image →
|
|
298
|
-
`name` initials → `icon` → glyph fallback; `size`, `variant` fill, `shape`, `status` dot) +
|
|
299
|
-
**`AvatarGroup`** (overlapped stack; `items`+`max` → `+N`) ·
|
|
300
|
-
`NotesEditor` · `ThemeSelector` (Auto/Light/Dark Select) · `ThemeToggle`
|
|
301
|
-
(compact ☀/☾ icon button, same persisted state)
|
|
302
|
-
- **Types / utils:** `Variant` `Size` `TabDefinition` `MenuItem` `ButtonGroupItem` `ButtonGroupSelect`
|
|
303
|
-
(types), `responsiveClasses` / `resolveScalar`, `BREAKPOINT_PX`.
|
|
304
|
-
|
|
305
|
-
## Adopting updates
|
|
306
|
-
|
|
307
|
-
The lib is versioned on npm. Pin a caret range (`^x.y.z`) and `npm update` for fixes; a `dev` dist-tag
|
|
308
|
-
tracks the newest `main` build (`npm install @spaethtech/svelte-ui@dev`) for testing unreleased work.
|
|
309
|
-
Pre-1.0, a **minor** bump can be breaking — check the package's `CHANGELOG.md` before bumping the range.
|
|
@@ -1,357 +0,0 @@
|
|
|
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 --ui-color-neutral
|
|
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-neutral`**). Canonical order:
|
|
34
|
-
`neutral · primary · secondary · success · info · warning · danger · ghost`.
|
|
35
|
-
|
|
36
|
-
**`neutral` is the default/undefined variant** — every sizeable/themeable component defaults to
|
|
37
|
-
`variant="neutral"`, so out of the box the whole library is **monochrome**; a colour appears only when
|
|
38
|
-
you set an explicit `variant`. **`--ui-color-neutral`** is the themeable neutral base: it **defaults to
|
|
39
|
-
`var(--ui-color-text)`** (so it flips opposite `--ui-color-background` per theme, and a border mixed
|
|
40
|
-
toward `transparent` always reads against the background), but a theme can override it **independently
|
|
41
|
-
of body text** to recolour every neutral border/surface at once. Every neutral border/divider — and
|
|
42
|
-
`--ui-border-color` — derives from it, so it's the single knob for "the colour of all neutral borders".
|
|
43
|
-
|
|
44
|
-
- `ghost` — borderless, transparent, hover-tint only (the minimal control). For the box-shaped
|
|
45
|
-
**fields** (Input/Select/TextArea) this also drives the **field background**: `ghost` → transparent,
|
|
46
|
-
every other variant → `--ui-color-background` (so a field reads as a field on any surface).
|
|
47
|
-
- `neutral` — the _uncoloured_, **default** variant (mixes from `--ui-color-neutral`, which defaults to
|
|
48
|
-
`--ui-color-text`): **solid** in solid contexts (Button/Toggle/Banner → black-on-white /
|
|
49
|
-
white-on-black), a **tint** in soft/bordered contexts (Badge/Alert/Input/Card frame).
|
|
50
|
-
- (`plain` was removed — it's now `ghost`. Old bordered-`ghost` → `neutral`.)
|
|
51
|
-
|
|
52
|
-
Field **help/error text** uses no new token: the muted `description` row is an inline
|
|
53
|
-
`color-mix(in srgb, var(--ui-color-text) ~65%, transparent)`, and the `error` row uses
|
|
54
|
-
`--ui-color-error`.
|
|
55
|
-
|
|
56
|
-
### Interactive-surface tints — `--ui-accent` + hover/surface/active
|
|
57
|
-
|
|
58
|
-
One tint language for every hover / section / pressed surface, three steps of the SAME mix:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
--ui-color-hover 4% row / option / ghost-button hover (lightest)
|
|
62
|
-
--ui-color-surface 8% header / footer / section bands (a notch darker)
|
|
63
|
-
--ui-color-active 12% pressed / held (strongest)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Each is `color-mix(in srgb, var(--ui-accent) N%, transparent)`. The percentages are
|
|
67
|
-
**theme-independent** (identical in light and dark); only the accent colour flips. `--ui-accent`
|
|
68
|
-
**defaults to `--ui-color-text`**, so out of the box these are neutral grey tints. Point
|
|
69
|
-
`--ui-accent` at a semantic colour and all three follow — that is how a **variant tints a whole
|
|
70
|
-
surface** (see the accent system below).
|
|
71
|
-
|
|
72
|
-
### Tinted-surface scale — `--ui-tint-*`
|
|
73
|
-
|
|
74
|
-
A variant-_tinted_ surface (a Badge chip, a Card frame, a `filled` Alert) mixes the component's OWN
|
|
75
|
-
variant token at shared strengths, so every tinted surface is calculated identically:
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
--ui-tint-soft 12% soft resting fill (Badge fill, Alert `filled`)
|
|
79
|
-
--ui-tint-divider 30% soft internal divider (CardHeader/CardFooter separators; the default
|
|
80
|
-
NEUTRAL divider is `--ui-border-color`)
|
|
81
|
-
--ui-tint-border 50% bordered surface, REST (Badge/Alert/Banner ring · Card/DataTable/Input/
|
|
82
|
-
TextArea frame · Button outline)
|
|
83
|
-
--ui-tint-border-hover 80% bordered field, HOVER (Input/TextArea/Card/DataTable/Button — the
|
|
84
|
-
stronger step on hover/focus)
|
|
85
|
-
--ui-tint-strong 80% foreground text (Badge)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
e.g. `color-mix(in srgb, var(<variant-token>) var(--ui-tint-soft), transparent)`. A `primary` badge is
|
|
89
|
-
primary@these; a `neutral` (default) one is **`--ui-color-neutral`**@these. Theme-independent (a theme
|
|
90
|
-
MAY override them to tune the whole tint language). **Every bordered surface uses ONE calc** —
|
|
91
|
-
`--ui-tint-border` at rest, `--ui-tint-border-hover` on hover/focus, from the variant token — with **no
|
|
92
|
-
`secondary` special case**. A default (neutral) frame is therefore `--ui-color-neutral` @ these; a
|
|
93
|
-
default divider is `--ui-color-neutral` @ `--ui-tint-divider` (= `--ui-border-color`). Internal
|
|
94
|
-
**dividers** use the softer `--ui-tint-divider` (30%) tier, likewise one calc per variant — so a
|
|
95
|
-
divider always reads lighter than the frame at any variant. `--ui-border-color-strong`/`-strong-hover`
|
|
96
|
-
have been **REMOVED** (migrate to `color-mix(in srgb, var(--ui-color-neutral) var(--ui-tint-border),
|
|
97
|
-
transparent)`, hover → `--ui-tint-border-hover`). (Distinct from the interactive tints above, which mix
|
|
98
|
-
from `--ui-accent`.)
|
|
99
|
-
|
|
100
|
-
### The `borderless` prop (border-colour swap, never a border-width change)
|
|
101
|
-
|
|
102
|
-
Every surface/framed component (`Button`, `Badge`, `Banner`, `Alert`, `Card`, `Input`, `DataTable`)
|
|
103
|
-
takes a **`borderless`** boolean. The border is **always 1px** — only its _colour_ moves, so toggling
|
|
104
|
-
it can't shift the box by a pixel:
|
|
105
|
-
|
|
106
|
-
- `borderless={true}` → border-colour = the component's **own background** (ring hidden, space kept).
|
|
107
|
-
- `borderless={false}` → border-colour = the **standard tint-border** (`--ui-tint-border` at rest,
|
|
108
|
-
`--ui-tint-border-hover` on hover/focus for the interactive fields) — the same calc for every surface.
|
|
109
|
-
|
|
110
|
-
Defaults differ by component (fill-y things default `true`: Button/Badge/Banner; framed things default
|
|
111
|
-
`false`: Alert/Card/Input/DataTable). One consequence worth knowing: a **`ghost` Button with
|
|
112
|
-
`borderless={false}`** is the **outline button** (transparent fill + a `--ui-color-text` tint edge) —
|
|
113
|
-
there is deliberately no separate `outline`/`ghost-bordered` variant. Toggle/Checkbox intentionally do
|
|
114
|
-
NOT take `borderless`.
|
|
115
|
-
|
|
116
|
-
### Border / radius — `--ui-border-*`
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
--ui-border-radius corner radius — ONE fixed value for every rectangular surface; NOT
|
|
120
|
-
scaled by `size`, no -sm/-lg (pills use rounded-full)
|
|
121
|
-
--ui-border-color soft internal divider (row separators, section splits) — NEUTRAL:
|
|
122
|
-
`--ui-color-neutral` @ `--ui-tint-divider` (30%). Repeated per theme
|
|
123
|
-
scope so it re-resolves per theme + survives @theme tree-shaking.
|
|
124
|
-
(--ui-border-color-strong/-strong-hover were REMOVED — use the
|
|
125
|
-
`--ui-tint-border` / `--ui-tint-border-hover` scheme instead)
|
|
126
|
-
--ui-color-neutral the neutral/undefined-variant base colour; defaults to
|
|
127
|
-
`var(--ui-color-text)`. Override to recolour ALL neutral borders +
|
|
128
|
-
surfaces at once, independent of body text + the variant colours.
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**One hue, two weights.** All *neutral* borders share the `--ui-color-neutral` hue; only the tint
|
|
132
|
-
weight differs by role — **divider** `--ui-tint-divider` (30%, `--ui-border-color`) vs **frame**
|
|
133
|
-
`--ui-tint-border` (50%). A `variant`-set border tints from that variant's token instead (a `secondary`
|
|
134
|
-
Card = cyan border) — that's intended; the neutral default is what's monochrome.
|
|
135
|
-
|
|
136
|
-
### Density / text / icon — the `size` axis
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
--ui-height-sm / --ui-height / --ui-height-lg control square heights (sm/md/lg)
|
|
140
|
-
--ui-height-touch explicit 44px touch target (not a size tier)
|
|
141
|
-
--ui-text-sm / --ui-text / --ui-text-lg per-size text
|
|
142
|
-
--ui-icon-sm / --ui-icon / --ui-icon-lg per-size inline icons
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The `sm/md/lg` triplets are tied to the `size` prop on every sized component — override one
|
|
146
|
-
tier and every component on that tier shifts. (Per-component _padding/gap_ is NOT a token; it's
|
|
147
|
-
a `Record<Size,string>` Tailwind class map inside each component.)
|
|
148
|
-
|
|
149
|
-
### Breakpoints (in `@theme`)
|
|
150
|
-
|
|
151
|
-
Narrow-mobile tiers `4xs` 320 / `3xs` 360 / `2xs` 420 / `xs` 480 and ultrawide `3xl` 1920, on
|
|
152
|
-
top of Tailwind's defaults — so `DataTable` column placements can target the full range.
|
|
153
|
-
|
|
154
|
-
## The accent / variant system
|
|
155
|
-
|
|
156
|
-
`--ui-color-hover/surface/active` mix from `--ui-accent`. To tint a surface, override
|
|
157
|
-
`--ui-accent` — **but you must also RE-MIX** the three tokens where you override it, via the
|
|
158
|
-
`.ui-accent` class:
|
|
159
|
-
|
|
160
|
-
```html
|
|
161
|
-
<!-- a DataTable, but any surface works the same -->
|
|
162
|
-
<div class="ui-accent" style="--ui-accent: var(--ui-color-primary)">…</div>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
`.ui-accent` (defined in `theme.css`) redefines hover/surface/active from the local
|
|
166
|
-
`--ui-accent`. Setting `--ui-accent` **alone is not enough** — see the freeze rule below for
|
|
167
|
-
why. The reference consumer is **`<DataTable variant="…">`**: its root always carries
|
|
168
|
-
`.ui-accent` and, when a `variant` is passed, sets `--ui-accent: var(<variantToken>)`, tinting
|
|
169
|
-
row hover + header/footer bands with that colour. No variant → neutral grey.
|
|
170
|
-
|
|
171
|
-
### Implementing `variant` tinting in a component (author recipe)
|
|
172
|
-
|
|
173
|
-
Every themed component does this the **same** way (DataTable / List / Menu / CardBody / TabStrip).
|
|
174
|
-
Follow it exactly — do **not** invent a new mechanism:
|
|
175
|
-
|
|
176
|
-
1. Resolve the variant to its colour token and build an inline-`style` string that sets the shared
|
|
177
|
-
`--ui-accent` — **only for a real accent** (greyscale variants resolve to `--ui-color-text`, the
|
|
178
|
-
default, so leave `--ui-accent` unset):
|
|
179
|
-
```svelte
|
|
180
|
-
const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : '');
|
|
181
|
-
```
|
|
182
|
-
2. Put `.ui-accent` on the component root and apply the style: `class="ui-accent …" style={accentVar}`.
|
|
183
|
-
3. Everywhere you need the variant colour, read **`var(--ui-accent)`** from a **static** Tailwind
|
|
184
|
-
class — the accent tints (`--ui-color-surface/hover/active`) automatically; for text/borders read
|
|
185
|
-
`var(--ui-accent)` directly, e.g. `[color:var(--ui-accent)]`,
|
|
186
|
-
`[border-color:color-mix(in_srgb,var(--ui-accent)_var(--ui-tint-border),transparent)]`.
|
|
187
|
-
|
|
188
|
-
**Two hard rules — both are silent failures, not `svelte-check` errors:**
|
|
189
|
-
|
|
190
|
-
- **NEVER a per-component custom property.** No `--ui-tab-border`, `--dt-*`, `--card-*`, etc. The
|
|
191
|
-
variant colour lives in the shared `--ui-accent`; components only ever _read_ it. (See "Extending
|
|
192
|
-
upstream" — lift a hard-coded value into a shared `--ui-*` token, never a component namespace.)
|
|
193
|
-
- **NEVER build a Tailwind class by interpolating a token/value at runtime.** Tailwind only emits
|
|
194
|
-
arbitrary-property CSS for class strings it finds **literally in source**. A runtime-built
|
|
195
|
-
`` `[--ui-accent:var(${token})]` `` or `` `[border-color:${x}]` `` is never scanned → the class
|
|
196
|
-
applies at runtime but **no CSS backs it**, so it silently does nothing (every variant falls back to
|
|
197
|
-
neutral). Two consequences: dynamic custom-prop values go through inline **`style`** (step 1), and
|
|
198
|
-
repeated `color-mix(…var(--ui-accent)…)` classes are written out **in full**, per state, not
|
|
199
|
-
factored into a `const` and interpolated.
|
|
200
|
-
|
|
201
|
-
## Applying a theme
|
|
202
|
-
|
|
203
|
-
### Light / dark, and the switcher rule
|
|
204
|
-
|
|
205
|
-
`.theme-light` / `.theme-dark` are set on **`<html>` (documentElement)** — matching the
|
|
206
|
-
pre-hydration script in `app.html` and the system `@media (prefers-color-scheme)` fallback. A
|
|
207
|
-
theme switcher **must toggle the class on `documentElement`, never `<body>`** (`ThemeSelector`
|
|
208
|
-
does this). Putting it on `<body>` leaves `<html>` on the system path and freezes the
|
|
209
|
-
accent-derived tints to the wrong theme (see below).
|
|
210
|
-
|
|
211
|
-
### Global palette override (whole app)
|
|
212
|
-
|
|
213
|
-
Set `--ui-color-*` at `:root` after importing the theme:
|
|
214
|
-
|
|
215
|
-
```css
|
|
216
|
-
@import "tailwindcss";
|
|
217
|
-
@import "svelte-ui/theme.css";
|
|
218
|
-
:root {
|
|
219
|
-
--ui-color-primary: #ff6600;
|
|
220
|
-
--ui-color-background: #0d1117;
|
|
221
|
-
--ui-color-text: #e6edf3;
|
|
222
|
-
--ui-color-neutral: var(--ui-color-text); /* ≥0.17 — restate so neutral borders track your text */
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
A bare `:root` override like this works because `--ui-color-neutral` (declared at `:root` in
|
|
227
|
-
`theme.css`) re-resolves on `:root`. But the moment you override the palette in a **more specific
|
|
228
|
-
scope** (`.theme-*`, a branding class, an inverse-surface subtree), you MUST restate
|
|
229
|
-
`--ui-color-neutral` + `--ui-border-color` in that scope too (freeze rule).
|
|
230
|
-
|
|
231
|
-
### Scoped override / **inverse surfaces** (a subtree)
|
|
232
|
-
|
|
233
|
-
Because the theme classes are ordinary CSS, apply one to **any container** to re-theme just
|
|
234
|
-
that subtree. This is the blessed pattern for an **inverse surface** — e.g. a dark header on a
|
|
235
|
-
light page, where component text/icons would otherwise be dark-on-dark and invisible:
|
|
236
|
-
|
|
237
|
-
```svelte
|
|
238
|
-
<header class="theme-dark bg-black">
|
|
239
|
-
<Button variant="primary">…</Button>
|
|
240
|
-
<!-- text/icons resolve LIGHT → visible -->
|
|
241
|
-
<ThemeSelector />
|
|
242
|
-
</header>
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Every svelte-ui component inside inherits the subtree's tokens (text flips to light, hover/
|
|
246
|
-
surface/active/accent all re-mix for the dark surface). **Prefer this over a per-component
|
|
247
|
-
`inverse` prop** — one class covers the whole subtree, composes at any depth, and adds no API
|
|
248
|
-
surface. For a single control, put the class on its nearest wrapper. This only works correctly
|
|
249
|
-
because the tint tokens are defined **per theme scope** (the freeze rule) — without that,
|
|
250
|
-
`.theme-dark` on a subtree would leave hover/surface frozen to `:root`.
|
|
251
|
-
|
|
252
|
-
## The custom-property freeze rule (the mechanic behind everything above)
|
|
253
|
-
|
|
254
|
-
A custom property whose value contains `var(...)` is resolved **at the element where it is
|
|
255
|
-
DEFINED**, then inherited already-resolved. A descendant that overrides the referenced variable
|
|
256
|
-
does **not** retroactively re-resolve it. Verified: `--m: color-mix(var(--acc)…)` at `:root`
|
|
257
|
-
with `--acc` overridden on a child stays the `:root` colour; redefining `--m` in the child's
|
|
258
|
-
scope picks up the child's `--acc`.
|
|
259
|
-
|
|
260
|
-
Two consequences baked into `theme.css`:
|
|
261
|
-
|
|
262
|
-
1. **Tints AND neutral-derived tokens are repeated in EVERY theme scope** (`:root`, `@media
|
|
263
|
-
(prefers-color-scheme:dark) :root`, `.theme-light`, `.theme-dark`) rather than defined once at
|
|
264
|
-
`:root` — so they re-resolve against whichever theme (and `--ui-accent`) is in scope. This
|
|
265
|
-
covers `--ui-color-hover/surface/active` **and** `--ui-color-neutral` + `--ui-border-color`
|
|
266
|
-
(both derive from `--ui-color-text`). This is why scoped themes and the switcher-on-`<html>`
|
|
267
|
-
rule work, why a switcher on `<body>` breaks them, and why **any custom scope that overrides the
|
|
268
|
-
palette must also restate `--ui-color-neutral`/`--ui-border-color`** (see Building a custom theme).
|
|
269
|
-
2. **Overriding `--ui-accent` requires the `.ui-accent` re-mix class** — the override alone
|
|
270
|
-
can't re-tint tokens computed higher up.
|
|
271
|
-
|
|
272
|
-
Also: these tints are **plain `:root`/class CSS, not `@theme`**. Tailwind v4 only emits an
|
|
273
|
-
`@theme` variable that backs a recognised utility; it never looks inside arbitrary classes like
|
|
274
|
-
`[background-color:var(--ui-color-hover)]`, so an `@theme` home would get them tree-shaken out
|
|
275
|
-
of the final bundle (the symptom: hover works off the SSR critical CSS during load, then dies).
|
|
276
|
-
|
|
277
|
-
## The self-reference trap
|
|
278
|
-
|
|
279
|
-
CSS silently invalidates a custom property that resolves to a `var()` of **itself**:
|
|
280
|
-
|
|
281
|
-
```css
|
|
282
|
-
/* WRONG — becomes guaranteed-invalid/unset; components reading it lose their colour */
|
|
283
|
-
.theme-x {
|
|
284
|
-
--ui-color-success: var(--ui-color-success);
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
To keep an inherited value, **omit the line entirely** — `--ui-*` already inherits from the
|
|
289
|
-
outer scope. Only list a token when you want a _different_ value. If the outer scope doesn't
|
|
290
|
-
define it at all, either import `svelte-ui/theme.css` globally so `:root` has the full palette,
|
|
291
|
-
or give a literal value in the wrapper.
|
|
292
|
-
|
|
293
|
-
## Building a custom theme / bridging another design system
|
|
294
|
-
|
|
295
|
-
The common case isn't picking colours from scratch — it's **aliasing svelte-ui's `--ui-*`
|
|
296
|
-
tokens to a palette already loaded in the host** (DaisyUI, shadcn, MUI, brand vars). Because
|
|
297
|
-
custom properties resolve at use-time, the host system's cascade applies automatically.
|
|
298
|
-
|
|
299
|
-
```css
|
|
300
|
-
/* scoped bridge onto a host's tokens */
|
|
301
|
-
.theme-myapp {
|
|
302
|
-
--ui-color-background: var(--color-base-100); /* host token */
|
|
303
|
-
--ui-color-text: var(--color-base-content);
|
|
304
|
-
/* REQUIRED in ANY scope that overrides the palette: the neutral base + the divider it derives
|
|
305
|
-
* do NOT re-resolve across scopes (freeze rule), so restate them here — otherwise neutral
|
|
306
|
-
* borders/surfaces freeze to `:root`'s colour (wrong hue, or invisible on a dark brand). */
|
|
307
|
-
--ui-color-neutral: var(--ui-color-text);
|
|
308
|
-
--ui-border-color: color-mix(in srgb, var(--ui-color-neutral) var(--ui-tint-divider), transparent);
|
|
309
|
-
/* only list what differs — the rest inherit; NEVER self-reference (see the trap) */
|
|
310
|
-
--ui-height-sm: 1.75rem;
|
|
311
|
-
--ui-height: 2.25rem;
|
|
312
|
-
--ui-height-lg: 2.75rem; /* match host control heights */
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
> **Upgrading an existing theme to ≥ 0.17:** `--ui-color-neutral` is new (it's what all *neutral*
|
|
317
|
-
> borders/surfaces derive from). A pre-0.17 custom theme scope defines the palette but not this token,
|
|
318
|
-
> so after upgrading, neutral borders (Card/Input frames, Menu, DataTable/SideBarMenu edges…) render
|
|
319
|
-
> with the wrong colour or vanish. **Add `--ui-color-neutral` (and re-derive `--ui-border-color`) to
|
|
320
|
-
> every scope where you override `--ui-color-*`.**
|
|
321
|
-
|
|
322
|
-
Apply globally at `:root` (svelte-ui _is_ the design system) or scoped on a wrapper subtree
|
|
323
|
-
(svelte-ui opted into per route/component while the rest of the app uses the host system).
|
|
324
|
-
|
|
325
|
-
## Extending upstream instead of forking
|
|
326
|
-
|
|
327
|
-
If a component hard-codes something you need to theme, **lift it into a shared `--ui-*` token
|
|
328
|
-
in `theme.css`** with the current literal as the default (zero visual diff), then reference the
|
|
329
|
-
token from the component via arbitrary-value syntax (`[background-color:var(--ui-color-surface)]`).
|
|
330
|
-
Do **not** add a new component-scoped namespace — the `--dt-*`-style per-component tokens were
|
|
331
|
-
removed in favour of the shared surface language (`--ui-color-hover/surface/active`, `--ui-accent`).
|
|
332
|
-
Verify light + dark before/after.
|
|
333
|
-
|
|
334
|
-
## Consumer note (Shearwood Creek)
|
|
335
|
-
|
|
336
|
-
SC consumes these `--ui-*` tokens **directly** — there is no `--color-*`/`--border-*` alias
|
|
337
|
-
layer (it was removed). Per-tenant branding overrides `--ui-color-primary`/`--ui-color-secondary`
|
|
338
|
-
at `html.theme-light,html.theme-dark` (see SC `$lib/branding.ts`); neutrals and semantic colours
|
|
339
|
-
stay theme-controlled. **≥ 0.17:** those branding scopes must also set
|
|
340
|
-
`--ui-color-neutral: var(--ui-color-text);` (and re-derive `--ui-border-color`) — the freeze rule
|
|
341
|
-
means the scope's `--ui-color-text` override won't otherwise reach the neutral borders, so they'd
|
|
342
|
-
freeze to `:root` (wrong hue / invisible on a dark tenant).
|
|
343
|
-
|
|
344
|
-
## Authoring checklist
|
|
345
|
-
|
|
346
|
-
1. Scope: global (`:root`) or wrapped (`.theme-myname` / an inverse-surface container).
|
|
347
|
-
2. Palette: only list `--ui-color-*` that differ; inherit the rest (no self-reference).
|
|
348
|
-
3. Density: override `--ui-height*` only if controls must differ; most themes inherit.
|
|
349
|
-
4. Accent/variant: to tint a surface, set `--ui-accent` **and** add `.ui-accent`.
|
|
350
|
-
5. Light + dark: test both (`prefers-color-scheme`) if you don't pin values.
|
|
351
|
-
6. Hit a hard-code? Extend `theme.css` + the component with a shared token first.
|
|
352
|
-
|
|
353
|
-
## Reference
|
|
354
|
-
|
|
355
|
-
- Source of truth — [`src/lib/theme.css`](../../src/lib/theme.css)
|
|
356
|
-
- Variant → colour map — [`src/lib/types/variants.ts`](../../src/lib/types/variants.ts)
|
|
357
|
-
- Public token docs — [`docs/themes.md`](../../docs/themes.md)
|