zabi-components 7.0.2 → 8.0.0
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/README.md +46 -3
- package/THEMING.md +255 -15
- package/dist/atoms/ActionPanel.svelte +12 -7
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Badge.svelte +93 -56
- package/dist/atoms/Badge.svelte.d.ts +15 -3
- package/dist/atoms/Button.svelte +49 -49
- package/dist/atoms/Button.svelte.d.ts +3 -0
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +10 -2
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +4 -0
- package/dist/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/atoms/CodeBlock.svelte +15 -7
- package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
- package/dist/atoms/ColorPicker.svelte +19 -9
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
- package/dist/atoms/Container.svelte +2 -3
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- package/dist/atoms/Heading.svelte +56 -27
- package/dist/atoms/Heading.svelte.d.ts +17 -4
- package/dist/atoms/IconButton.svelte +30 -37
- package/dist/atoms/Input.svelte +26 -34
- package/dist/atoms/List.svelte +10 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +2 -2
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +4 -1
- package/dist/atoms/Progress.svelte.d.ts +2 -0
- package/dist/atoms/Radio.svelte +4 -0
- package/dist/atoms/Radio.svelte.d.ts +2 -0
- package/dist/atoms/Select.svelte +65 -67
- package/dist/atoms/Select.svelte.d.ts +4 -0
- package/dist/atoms/SelectionControl.svelte +5 -1
- package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
- package/dist/atoms/Skeleton.svelte +5 -3
- package/dist/atoms/Table.svelte +2 -1
- package/dist/atoms/Text.svelte +34 -5
- package/dist/atoms/Text.svelte.d.ts +7 -1
- package/dist/atoms/Textarea.svelte +7 -6
- package/dist/atoms/ThemeToggle.svelte +17 -14
- package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
- package/dist/atoms/Toast.svelte +10 -8
- package/dist/atoms/Toggle.svelte +21 -13
- package/dist/atoms/Toggle.svelte.d.ts +7 -0
- package/dist/atoms/Tooltip.svelte +80 -22
- package/dist/atoms/Tooltip.svelte.d.ts +15 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +1 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/util/cn.d.ts +8 -0
- package/dist/components/util/cn.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -6
- package/dist/molecules/Alert.svelte +43 -64
- package/dist/molecules/Alert.svelte.d.ts +7 -1
- package/dist/molecules/ComponentDemo.svelte +10 -2
- package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
- package/dist/molecules/ContactForm.svelte +12 -2
- package/dist/molecules/ContactForm.svelte.d.ts +2 -0
- package/dist/molecules/Dropdown.svelte +40 -11
- package/dist/molecules/Dropdown.svelte.d.ts +4 -0
- package/dist/molecules/EmptyState.svelte +2 -1
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +2 -1
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +54 -15
- package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
- package/dist/molecules/Modal.svelte +110 -31
- package/dist/molecules/Modal.svelte.d.ts +9 -6
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +36 -38
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -2
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +10 -2
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +8 -2
- package/dist/molecules/Page.svelte.d.ts +2 -0
- package/dist/molecules/PropsTable.svelte +6 -3
- package/dist/molecules/PropsTable.svelte.d.ts +2 -0
- package/dist/molecules/RadioGroup.svelte +5 -1
- package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/molecules/Section.svelte +12 -4
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SidebarBrandHeader.svelte +13 -4
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
- package/dist/molecules/SidebarFooter.svelte +12 -4
- package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +85 -21
- package/dist/molecules/SlideUp.svelte.d.ts +5 -4
- package/dist/molecules/Tabs.svelte +25 -19
- package/dist/molecules/Tabs.svelte.d.ts +8 -4
- package/dist/molecules/Toaster.svelte +3 -2
- package/dist/molecules/ToasterToast.svelte +41 -10
- package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
- package/dist/organisms/SidebarAccountPanel.svelte +4 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarNavigation.svelte +102 -59
- package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
- package/dist/organisms/SidebarPanel.svelte +19 -11
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +112 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
- package/dist/organisms/TopNavbar.svelte +88 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
- package/dist/organisms/index.d.ts +1 -0
- package/dist/organisms/index.js +1 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/events.js +1 -0
- package/dist/types/index.js +3 -0
- package/dist/util/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -0
- package/dist/util/ssr-safe.d.ts +4 -4
- package/dist/zabi-components-colors.css +2 -2
- package/dist/zabi-components-theme-dark-only.css +139 -92
- package/dist/zabi-components-theme-dark.css +139 -92
- package/dist/zabi-components-theme-only.css +268 -122
- package/dist/zabi-components-theme.css +268 -122
- package/dist/zabi-components.css +831 -883
- package/docs/lucide-icons.md +3 -1
- package/package.json +19 -24
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Svelte **5** UI primitives (runes, `onclick` / `oninput`, …), Tailwind **v4** design tokens, SSR-safe. Requires **Svelte ≥ 5.43.8**.
|
|
4
4
|
|
|
5
|
+
Docs: **[zabi-components.vercel.app](https://zabi-components.vercel.app/)**
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Quick start
|
|
@@ -12,7 +14,7 @@ Svelte **5** UI primitives (runes, `onclick` / `oninput`, …), Tailwind **v4**
|
|
|
12
14
|
npm install zabi-components
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Ensure `svelte@^5.43.8` is installed — SvelteKit apps usually already match; add `@sveltejs/kit@^2` only for Kit.
|
|
17
|
+
Ensure `svelte@^5.43.8` is installed — SvelteKit apps usually already match; add `@sveltejs/kit@^2` only for Kit. The package is Svelte-only (the unmaintained `zabi-components/react` export was removed in 8.0.0).
|
|
16
18
|
|
|
17
19
|
**2. Global CSS** (e.g. SvelteKit `src/app.css` — adjust for your bundler entry)
|
|
18
20
|
|
|
@@ -100,6 +102,24 @@ Stick to **documented package subpaths** from `package.json` `exports`. Do not r
|
|
|
100
102
|
</NavigationMenu>
|
|
101
103
|
```
|
|
102
104
|
|
|
105
|
+
Some components ship both ways. `SidebarNavigation` is the data-driven one — one
|
|
106
|
+
component, ~39 props, excellent when your sidebar is shaped like that one.
|
|
107
|
+
`SidebarShell` is the same chrome with the regions left open, for when it isn't:
|
|
108
|
+
|
|
109
|
+
```svelte
|
|
110
|
+
<SidebarShell mode="collapsed">
|
|
111
|
+
{#snippet header({ collapsed })}
|
|
112
|
+
<SidebarBrandHeader {collapsed} brandName="Zabi" />
|
|
113
|
+
{/snippet}
|
|
114
|
+
|
|
115
|
+
<SidebarNavSection title="Main" sectionKey="main">…</SidebarNavSection>
|
|
116
|
+
|
|
117
|
+
{#snippet footer({ collapsed, insetX })}
|
|
118
|
+
<SidebarFooter {collapsed} class={insetX} />
|
|
119
|
+
{/snippet}
|
|
120
|
+
</SidebarShell>
|
|
121
|
+
```
|
|
122
|
+
|
|
103
123
|
Wrong mental model: treating composable roots as optional — see **Common pitfalls**.
|
|
104
124
|
|
|
105
125
|
---
|
|
@@ -162,10 +182,21 @@ Components use **DOM-style props** (`onclick`, `oninput`), not legacy `on:click`
|
|
|
162
182
|
|
|
163
183
|
| Token / class | Role |
|
|
164
184
|
|---------------|------|
|
|
165
|
-
| `surface-
|
|
185
|
+
| `bg-surface-base` · `raised` · `elevated` · `overlay` | The four surface levels. Anything floating above content uses `overlay`. In dark mode each level is the one below it with more light on it — shadows don't read on a dark page. |
|
|
186
|
+
| `surface-1` / `surface-2` | Compatibility aliases for `raised` / `elevated`. Prefer the named levels in new code. |
|
|
187
|
+
| `shadow-sm` / `shadow-lg` | Elevation is **two steps**: raised, and floating. `shadow-none` is the explicit absence of one. `shadow-md`, `shadow-xl` and a bare `shadow` fail the build. |
|
|
188
|
+
| `rounded-control` · `container` · `overlay` · `pill` | Radius is chosen by **role**, never by size. A large button is a bigger box with the same corner as a small one. |
|
|
189
|
+
| `hover:bg-surface-hover` / `active:bg-surface-active` | Interaction fills. Surface-relative on purpose — a fixed ramp step can land on exactly the colour it sits on. |
|
|
166
190
|
| `.focus-ring` / `focus-ring--nav` | Keyboard focus rings — don’t replace with `outline-none` unless you substitute an equivalent visible focus style. |
|
|
167
191
|
|
|
168
|
-
|
|
192
|
+
Spacing rides a **4px grid**: `gap-*`, `space-*` and padding land on whole steps.
|
|
193
|
+
Half-steps survive only as optical nudges, and only on margins (`mt-0.5` on an
|
|
194
|
+
icon beside a first line of text).
|
|
195
|
+
|
|
196
|
+
`Heading` and `Text` share **one** type scale, so `<Text size="md">` sits on the
|
|
197
|
+
same line box as an `h6` and `size="lg"` as an `h5`.
|
|
198
|
+
|
|
199
|
+
Details: [THEME.md](./THEME.md) · [THEMING.md](./THEMING.md).
|
|
169
200
|
|
|
170
201
|
---
|
|
171
202
|
|
|
@@ -186,6 +217,16 @@ import Button from "some-path/node_modules/zabi-components/dist/atoms/Button.sve
|
|
|
186
217
|
|
|
187
218
|
Load theme CSS (Quick start). Unstyled components usually mean missing `@import "zabi-components/theme-only"` (and dark overrides if you use `.dark`).
|
|
188
219
|
|
|
220
|
+
`class` is the public prop on every component and is merged **last**, through
|
|
221
|
+
[tailwind-merge](https://github.com/dcastil/tailwind-merge), so your utility
|
|
222
|
+
wins outright rather than racing the component's own:
|
|
223
|
+
|
|
224
|
+
```svelte
|
|
225
|
+
<Card class="rounded-pill" /> <!-- rounded-container is dropped, not fought -->
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`className` still works everywhere as a deprecated alias.
|
|
229
|
+
|
|
189
230
|
**Library code (contributors)**
|
|
190
231
|
|
|
191
232
|
```ts
|
|
@@ -232,6 +273,8 @@ import type { Foo } from "../types/variants.js";
|
|
|
232
273
|
|-------|---------|---------|
|
|
233
274
|
| Unit / component | `npm run test` | Vitest + Testing Library — logic and regressions |
|
|
234
275
|
| Interaction | `npm run test:e2e` | Playwright — overlays, focus, keyboard flows |
|
|
276
|
+
| Types & structure | `npm run check` | `svelte-check`, import-path and layout-width rules, plus `check:design` |
|
|
277
|
+
| Design system | `npm run check:design` | Ramp lightness, WCAG AA on every rendered pair, interaction fills that differ from their surface, control geometry, and token violations (raw palette classes, off-scale shadows, half-step spacing) |
|
|
235
278
|
|
|
236
279
|
---
|
|
237
280
|
|
package/THEMING.md
CHANGED
|
@@ -116,7 +116,8 @@ Dark mapping rule reference:
|
|
|
116
116
|
|
|
117
117
|
### Change Primary Button Color
|
|
118
118
|
|
|
119
|
-
The primary button uses `--color-action-primary
|
|
119
|
+
The primary button uses `--color-action-primary`, which defaults to `brand-600`
|
|
120
|
+
(4.86:1 against a white label). To change it:
|
|
120
121
|
|
|
121
122
|
```css
|
|
122
123
|
@theme {
|
|
@@ -135,11 +136,15 @@ The brand scale is used throughout the system:
|
|
|
135
136
|
- **brand-50 to brand-100**: Subtle backgrounds, hover states
|
|
136
137
|
- **brand-300 to brand-400**: Light accents, disabled states
|
|
137
138
|
- **brand-500**: Focus rings, medium emphasis
|
|
138
|
-
- **brand-600**: Primary
|
|
139
|
-
- **brand-700**:
|
|
140
|
-
- **brand-800**:
|
|
141
|
-
- **brand-900**:
|
|
142
|
-
|
|
139
|
+
- **brand-600**: **Primary buttons** (`action-primary`)
|
|
140
|
+
- **brand-700**: Primary hover, links
|
|
141
|
+
- **brand-800**: Primary active, link hover
|
|
142
|
+
- **brand-900 / 950**: Darkest brand shades
|
|
143
|
+
|
|
144
|
+
Hover and active always move *toward* the dark end in light mode. The `.dark`
|
|
145
|
+
mirror flips that automatically, so "more pressed" means "more contrast against
|
|
146
|
+
the page" in both themes. Never point `-active` at a step on the far side of the
|
|
147
|
+
ramp — that is how the pressed primary label ended up at 1.66:1.
|
|
143
148
|
|
|
144
149
|
### Dark Mode
|
|
145
150
|
|
|
@@ -162,15 +167,11 @@ Dark mode action colors are handled in `src/app.css` in the `.dark` block (lines
|
|
|
162
167
|
- `base-600` (light mode) → `base-400` (dark mode) automatically
|
|
163
168
|
|
|
164
169
|
### Danger Actions
|
|
165
|
-
- **
|
|
166
|
-
|
|
167
|
-
.dark
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
--color-action-danger-active: theme(colors.red.700);
|
|
171
|
-
/* ... */
|
|
172
|
-
}
|
|
173
|
-
```
|
|
170
|
+
- **No re-pointing needed.** Danger resolves through `--color-error-*`, which
|
|
171
|
+
mirrors like every other ramp, so dark danger is the same step of the same
|
|
172
|
+
ramp as light danger. The `.dark` block restates the aliases only so that
|
|
173
|
+
`zabi-components/theme-dark-only` remains a complete standalone import
|
|
174
|
+
(`validate-theme.js` enforces that).
|
|
174
175
|
|
|
175
176
|
### In Generated Files
|
|
176
177
|
|
|
@@ -179,6 +180,245 @@ Dark mode action colors are handled in `src/app.css` in the `.dark` block (lines
|
|
|
179
180
|
- **`dist/zabi-components-theme-only.css`**: Only has light mode (@theme block), no dark mode
|
|
180
181
|
- **`dist/zabi-components-theme.css`**: Only has light mode (@theme block), no dark mode
|
|
181
182
|
|
|
183
|
+
## 🎨 Calibrated Colour Ramps
|
|
184
|
+
|
|
185
|
+
Every chromatic ramp (`brand`, `citron`, `pine`, `iris`, `warning`, `error`) is
|
|
186
|
+
**generated against one shared lightness curve**, so `<ramp>-600` means the same
|
|
187
|
+
perceptual lightness in every ramp. That is what lets the semantic families read
|
|
188
|
+
as one family instead of six unrelated colours.
|
|
189
|
+
|
|
190
|
+
| step | 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 |
|
|
191
|
+
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
192
|
+
| CIE L\* | 97 | 94 | 88 | 80 | 70 | 58 | **47** | 38 | 29 | 20 | 12 |
|
|
193
|
+
|
|
194
|
+
Step **600** is the solid-fill step: dark enough to clear 4.5:1 against white,
|
|
195
|
+
light enough to still read as a colour. Every semantic token points at it.
|
|
196
|
+
|
|
197
|
+
- **Source of truth:** `tokens/chromatic-scales.js` — hue, peak chroma, the
|
|
198
|
+
chroma envelope, and the target curve.
|
|
199
|
+
- **Regenerate:** `node scripts/generate-ramps.js` (runs inside `npm run sync:tokens`).
|
|
200
|
+
- **Verify:** `node scripts/check-ramp-lightness.js` — fails if any step drifts
|
|
201
|
+
more than ±1.5 L\* off the curve, or if two adjacent steps differ by more than
|
|
202
|
+
15 L\* (a cliff rather than a ramp).
|
|
203
|
+
|
|
204
|
+
To rebrand, edit `hue` / `peakChroma` for a ramp in `tokens/chromatic-scales.js`
|
|
205
|
+
and re-run the generator. **Do not hand-edit `--zabi-<ramp>-*` in `src/app.css`** —
|
|
206
|
+
the generator overwrites them, in both the `@theme` block and the `.dark` mirror.
|
|
207
|
+
|
|
208
|
+
The `base` (neutral) ramp is deliberately **not** on this curve: it is wider on
|
|
209
|
+
purpose because it also drives text, borders and the surface levels. Semantic
|
|
210
|
+
`neutral` aliases the base step closest to the chromatic 600s.
|
|
211
|
+
|
|
212
|
+
## 🧩 Semantic Families
|
|
213
|
+
|
|
214
|
+
Each family exposes the same roles, built from the same steps:
|
|
215
|
+
|
|
216
|
+
| token | step | use |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `--color-<family>` | 600 | solid fill — badge `emphasis="solid"`, progress bars |
|
|
219
|
+
| `--color-<family>-weak` | 700 | pressed / emphasis |
|
|
220
|
+
| `--color-<family>-medium` | 800 | strongest fill |
|
|
221
|
+
| `--color-<family>-strong` | 900 | darkest |
|
|
222
|
+
| `--color-<family>-subtle` | 100 | tinted fill — badges, alerts |
|
|
223
|
+
| `--color-<family>-border` | 200 | tinted edge |
|
|
224
|
+
| `--color-<family>-text` | 700 | text on a subtle fill or a page surface |
|
|
225
|
+
|
|
226
|
+
Families: `success`, `warning`, `error`, `info`, `energetic`, `neutral`.
|
|
227
|
+
|
|
228
|
+
Prefer the **subtle** trio (`-subtle` fill + `-border` edge + `-text` label) for
|
|
229
|
+
anything informational. Solid fills are for the one element on a screen that has
|
|
230
|
+
to shout.
|
|
231
|
+
|
|
232
|
+
## 📐 Control Geometry
|
|
233
|
+
|
|
234
|
+
One height scale is shared by **every** form control, so a Button, Input, Select
|
|
235
|
+
and IconButton of the same size line up in a row:
|
|
236
|
+
|
|
237
|
+
| size | height | token |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `sm` | 32px | `--control-height-sm` |
|
|
240
|
+
| `md` | 40px | `--control-height-md` |
|
|
241
|
+
| `lg` | 48px | `--control-height-lg` |
|
|
242
|
+
|
|
243
|
+
Enforced by `scripts/check-control-geometry.js`.
|
|
244
|
+
|
|
245
|
+
## 🔲 Border Radius
|
|
246
|
+
|
|
247
|
+
Four radii, chosen by **role**, never by size. A large button is a bigger box
|
|
248
|
+
with the same corner as a small one.
|
|
249
|
+
|
|
250
|
+
| token | value | use |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `--radius-control` | 8px | buttons, inputs, selects, toggles |
|
|
253
|
+
| `--radius-container` | 12px | cards, alerts, panels, list items, code blocks |
|
|
254
|
+
| `--radius-overlay` | 16px | modals, sheets, menus, popovers, toasts |
|
|
255
|
+
| `--radius-pill` | full | badges, avatars, status dots |
|
|
256
|
+
|
|
257
|
+
`--radius-sm/md/lg/xl` remain as legacy aliases for consumer overrides, but
|
|
258
|
+
components must not use them — `check-control-geometry.js` fails the build if a
|
|
259
|
+
component reaches for a t-shirt radius.
|
|
260
|
+
|
|
261
|
+
## ✋ Interaction Fills
|
|
262
|
+
|
|
263
|
+
Hover and active fills are **surface-relative alpha tints**, not fixed ramp steps:
|
|
264
|
+
|
|
265
|
+
```css
|
|
266
|
+
/* light */
|
|
267
|
+
--color-surface-hover: rgba(9, 9, 11, 0.06);
|
|
268
|
+
--color-surface-active: rgba(9, 9, 11, 0.11);
|
|
269
|
+
/* dark */
|
|
270
|
+
--color-surface-hover: rgba(250, 250, 250, 0.08);
|
|
271
|
+
--color-surface-active: rgba(250, 250, 250, 0.14);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A fixed step can coincide exactly with the surface it sits on — `base-100` in
|
|
275
|
+
dark mode *is* `--color-surface-base`, which is how ghost-button hover became
|
|
276
|
+
invisible. `check-token-violations.js` now fails on `hover:bg-base-*` /
|
|
277
|
+
`active:bg-base-*` in component source.
|
|
278
|
+
|
|
279
|
+
Use `hover:bg-surface-hover` / `active:bg-surface-active` for quiet controls, and
|
|
280
|
+
the `action-*-hover` / `action-*-active` tokens for filled ones.
|
|
281
|
+
|
|
282
|
+
## ♿ Contrast
|
|
283
|
+
|
|
284
|
+
`scripts/check-contrast.js` resolves every fill/foreground pair a component can
|
|
285
|
+
render — through the same token chain the CSS uses — in **both** themes, and
|
|
286
|
+
fails below WCAG AA. Run it after re-pointing any token:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npm run check:contrast # or: npm run check:design (all four guards)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 🧱 Surface Elevation Levels
|
|
293
|
+
|
|
294
|
+
Every surface uses one of four semantic levels. Use these tokens instead of raw `base-*` steps for backgrounds:
|
|
295
|
+
|
|
296
|
+
| Level | Token / utility | Use for | Light | Dark (OKLCH L) |
|
|
297
|
+
|---|---|---|---|---|
|
|
298
|
+
| Base | `--color-surface-base` · `bg-surface-base` | Page / app shell (the darkest level in dark mode) | `base-100` | `#18181b` · 21 |
|
|
299
|
+
| Raised | `--color-surface-raised` · `bg-surface-raised` | Cards, panels, sidebars | `#ffffff` | `#262629` · 27 |
|
|
300
|
+
| Elevated | `--color-surface-elevated` · `bg-surface-elevated` | Nested cards, hover and active fills | `base-50` | `#35353a` · 33 |
|
|
301
|
+
| Overlay | `--color-surface-overlay` · `bg-surface-overlay` | Modals, sheets, dropdown/select menus, navigation panels, toasts | `#ffffff` | `#44444c` · 39 |
|
|
302
|
+
|
|
303
|
+
Supporting tokens:
|
|
304
|
+
- `--color-surface-overlay-hover` (`bg-surface-overlay-hover`): row and icon-button hover **inside** an overlay. It is lighter than the overlay in dark mode.
|
|
305
|
+
- `--color-border-overlay` (`border-border-overlay`): 1px edge on overlays. Transparent in light mode, visible in dark mode.
|
|
306
|
+
|
|
307
|
+
**The dark levels are generated, not hand-picked.** Each one is `#fafafa` — the
|
|
308
|
+
same light as the dark hover tint `--color-surface-hover` — washed over the page
|
|
309
|
+
at 6.4% / 13.1% / 19.7%, so every level is literally the level below it with more
|
|
310
|
+
light on it. The ladder lives in `tokens/surface-ladder.js`; edit the alphas
|
|
311
|
+
there and run `npm run sync:tokens`, never the hex in `app.css`.
|
|
312
|
+
|
|
313
|
+
The composite is baked to an opaque hex at build time rather than shipped as
|
|
314
|
+
`rgba()`. Flat colours keep the static toolchain working — `check-contrast.js`
|
|
315
|
+
can resolve every AA pair against a surface, `check-surface-elevation.js` can
|
|
316
|
+
measure the steps — and an opaque overlay means a modal never lets the page bleed
|
|
317
|
+
through it. The trade: nesting past the four levels is not automatic. A card
|
|
318
|
+
inside a card does not self-lighten, it names the next level up.
|
|
319
|
+
|
|
320
|
+
**Why dark mode steps lightness instead of using shadows:** a drop shadow simulates light blocked by a raised object, which reads on a light page. On a dark page the shadow is as dark as the background, so the signal disappears. In dark mode each level is therefore **lighter** than the one below it, by +6 OKLCH lightness points on the neutral base hue. Light mode keeps white surfaces and uses shadows for elevation.
|
|
321
|
+
|
|
322
|
+
**Rules:**
|
|
323
|
+
- Anything that floats above content must use a lighter level than what it floats over. Floating components (Modal, SlideUp, Dropdown/Select menu, NavigationMenu panel, Toaster/Toast) use `bg-surface-overlay`.
|
|
324
|
+
- Hover fills on overlays use `bg-surface-overlay-hover`, not `bg-base-*`, which would go darker in dark mode.
|
|
325
|
+
- Existing tokens remain as aliases: `background` → base, `card` / `surface-1` → raised, `surface-2` / `card-hover` → elevated, `surface-3` → overlay (dark), `card-elevated` → overlay (light) / elevated (dark).
|
|
326
|
+
|
|
327
|
+
**Enforced by** `scripts/check-surface-elevation.js`, which `validate-theme.js` runs during `npm run build:css`. It fails when:
|
|
328
|
+
- dark levels aren't strictly increasing;
|
|
329
|
+
- any step is outside 5–8 OKLCH L points;
|
|
330
|
+
- overlay hover or tooltip fills are darker than the overlay;
|
|
331
|
+
- a floating component paints a surface class below overlay.
|
|
332
|
+
|
|
333
|
+
To retune the dark levels, edit the four `--color-surface-*` values in the `.dark` block of `src/app.css` and run `node scripts/check-surface-elevation.js`.
|
|
334
|
+
|
|
335
|
+
> The light surface tokens must stay **below** the `/* Background Colors */` marker in `@theme`. `scripts/sync-theme-tokens.js` regenerates everything between the base-scale aliases and that marker.
|
|
336
|
+
|
|
337
|
+
## 🎛️ Overriding a Component's Classes
|
|
338
|
+
|
|
339
|
+
`class` is the public prop on every component and is merged **last**, so a
|
|
340
|
+
call-site utility wins. That promise needs `cn()` (`src/components/util/cn.ts`,
|
|
341
|
+
tailwind-merge) to be true: `rounded-control` and `rounded-container` are
|
|
342
|
+
equal-specificity utilities, so plain concatenation leaves the winner to
|
|
343
|
+
whichever one Tailwind emits later in the stylesheet, not to the caller.
|
|
344
|
+
|
|
345
|
+
```svelte
|
|
346
|
+
<Card class="rounded-pill" /> <!-- rounded-container is dropped, not fought -->
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Most semantic utilities work with stock tailwind-merge — `bg-card` vs
|
|
350
|
+
`bg-surface-overlay`, `text-headline` vs `text-description` all resolve. The
|
|
351
|
+
role-based radii are the exception and are declared explicitly in `cn.ts`,
|
|
352
|
+
because `control`/`container`/`overlay`/`pill` are role names rather than scale
|
|
353
|
+
values. **Add any future custom scale to that config**, or tailwind-merge keeps
|
|
354
|
+
both classes and stylesheet order silently decides again.
|
|
355
|
+
|
|
356
|
+
Covered by `tests/class-merge.test.ts`.
|
|
357
|
+
|
|
358
|
+
## 🌑 Shadow Scale
|
|
359
|
+
|
|
360
|
+
Elevation in light mode is **two steps**, not a ramp. A shadow says "this is
|
|
361
|
+
above the page" or "this floats over it" — there is no third meaning, and an
|
|
362
|
+
in-between value just makes two neighbouring surfaces look accidentally
|
|
363
|
+
different.
|
|
364
|
+
|
|
365
|
+
| Utility | Reads as | Use for |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| `shadow-sm` | Raised | Cards, tables, sidebars, toggle knobs, colour-picker thumbs |
|
|
368
|
+
| `shadow-lg` | Floating | Modals, sheets, dropdown/select menus, toasts, popovers |
|
|
369
|
+
|
|
370
|
+
`shadow-none` is the explicit *absence* of a shadow (outlined and flat cards,
|
|
371
|
+
disabled buttons), not a third step.
|
|
372
|
+
|
|
373
|
+
Rules:
|
|
374
|
+
- A raised element that becomes floating on interaction may go `sm` → `lg` on
|
|
375
|
+
hover. Something already at `lg` does not deepen further — it changes its
|
|
376
|
+
background instead.
|
|
377
|
+
- `shadow-md`, `shadow-xl` and a bare `shadow` are build failures. The bare
|
|
378
|
+
`shadow` utility is the sneaky one: it resolves to a Tailwind default that
|
|
379
|
+
`app.css` never defines, so no token controls it.
|
|
380
|
+
- Shadows carry elevation in **light mode only**. In dark mode the surface
|
|
381
|
+
levels above do the work — see Surface Elevation Levels.
|
|
382
|
+
|
|
383
|
+
Enforced by `scripts/check-token-violations.js`.
|
|
384
|
+
|
|
385
|
+
## 📏 Spacing Rhythm
|
|
386
|
+
|
|
387
|
+
Layout spacing rides a **4px grid**. Half-steps (`gap-1.5`, `px-2.5`, `py-2.5`)
|
|
388
|
+
set up a second rhythm competing with the first: a 6px gap beside an 8px gap is
|
|
389
|
+
a 2px wobble nobody chose, and it accumulates down a dense form.
|
|
390
|
+
|
|
391
|
+
- `gap-*`, `space-x/y-*` and every padding utility must land on a whole step.
|
|
392
|
+
- **Margins are exempt**, because they carry optical nudges. `mt-0.5` on an
|
|
393
|
+
icon sitting beside a first line of text is an alignment correction, not
|
|
394
|
+
rhythm — Alert, ListItem, ToasterToast and the radio control all rely on it.
|
|
395
|
+
Keep those to 0.5 and keep them on margins so the distinction stays visible.
|
|
396
|
+
- Icon sizes are their own scale (`size-3.5` = 14px, `size-4`, `size-5`), not
|
|
397
|
+
spacing, and are unaffected.
|
|
398
|
+
|
|
399
|
+
Enforced by `scripts/check-token-violations.js`.
|
|
400
|
+
|
|
401
|
+
## 🔠 Type Scale
|
|
402
|
+
|
|
403
|
+
`Heading` and `Text` sit on **one** ramp rather than each carrying its own, so a
|
|
404
|
+
heading and the copy beneath it share line boxes instead of nearly matching.
|
|
405
|
+
|
|
406
|
+
| Step | `Heading` | `Text` |
|
|
407
|
+
|---|---|---|
|
|
408
|
+
| 36px | `level={1}` | — |
|
|
409
|
+
| 30px | `level={2}` | — |
|
|
410
|
+
| 24px | `level={3}` | — |
|
|
411
|
+
| 20px | `level={4}` | — |
|
|
412
|
+
| 18px | `level={5}` | `size="lg"` |
|
|
413
|
+
| 16px | `level={6}` | `size="md"` (default) |
|
|
414
|
+
| 14px | — | `size="sm"` |
|
|
415
|
+
| 12px | — | `size="xs"` |
|
|
416
|
+
|
|
417
|
+
Headings tighten tracking as they grow and are `semibold`/`bold`; `Text` takes a
|
|
418
|
+
`weight` prop (`normal` · `medium` · `semibold` · `bold`) so body copy can carry
|
|
419
|
+
emphasis without being promoted to a heading. `tone="label"` defaults to
|
|
420
|
+
`medium`; every other tone defaults to `normal`.
|
|
421
|
+
|
|
182
422
|
## 📁 File Structure
|
|
183
423
|
|
|
184
424
|
```
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Badge from "./Badge.svelte";
|
|
3
3
|
import type { ExtendedSemanticVariant } from "../types/variants.js";
|
|
4
4
|
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
5
6
|
interface Props {
|
|
6
7
|
title: string;
|
|
7
8
|
description: string;
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
badgeVariant?: ExtendedSemanticVariant;
|
|
14
15
|
/** Overrides the default accessible name for the clickable region. */
|
|
15
16
|
ariaLabel?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
/** @deprecated use `class`. */
|
|
16
19
|
className?: string;
|
|
17
20
|
target?: HTMLAnchorElement["target"];
|
|
18
21
|
rel?: string;
|
|
@@ -28,12 +31,17 @@
|
|
|
28
31
|
badgeText,
|
|
29
32
|
badgeVariant = "neutral",
|
|
30
33
|
ariaLabel,
|
|
31
|
-
|
|
34
|
+
class: classAttr = "",
|
|
35
|
+
className: legacyClass = "",
|
|
32
36
|
target,
|
|
33
37
|
rel,
|
|
34
38
|
...restProps
|
|
35
39
|
}: Props = $props();
|
|
36
40
|
|
|
41
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
42
|
+
* Both are merged here so existing call sites keep working. */
|
|
43
|
+
const className = $derived(`${classAttr} ${legacyClass}`.trim());
|
|
44
|
+
|
|
37
45
|
const isDisabled = $derived(disabled || loading);
|
|
38
46
|
|
|
39
47
|
const computedAriaLabel = $derived(
|
|
@@ -41,16 +49,13 @@
|
|
|
41
49
|
);
|
|
42
50
|
|
|
43
51
|
const baseClasses =
|
|
44
|
-
"group focus-ring focus-ring--muted block rounded-
|
|
52
|
+
"group focus-ring focus-ring--muted block rounded-container border border-border bg-card p-6 text-left transition-colors duration-150 hover:border-border-medium hover:bg-surface-hover active:scale-[0.99] active:bg-surface-active";
|
|
45
53
|
|
|
46
54
|
const disabledClasses =
|
|
47
|
-
"opacity-50 cursor-not-allowed pointer-events-none hover:border-
|
|
55
|
+
"opacity-50 cursor-not-allowed pointer-events-none hover:border-border hover:bg-card active:scale-100";
|
|
48
56
|
|
|
49
57
|
const panelClasses = $derived(
|
|
50
|
-
|
|
51
|
-
.filter(Boolean)
|
|
52
|
-
.join(" ")
|
|
53
|
-
.trim(),
|
|
58
|
+
cn(baseClasses, isDisabled ? disabledClasses : "", className),
|
|
54
59
|
);
|
|
55
60
|
|
|
56
61
|
function handleKeydown(event: KeyboardEvent) {
|
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
badgeVariant?: ExtendedSemanticVariant;
|
|
11
11
|
/** Overrides the default accessible name for the clickable region. */
|
|
12
12
|
ariaLabel?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** @deprecated use `class`. */
|
|
13
15
|
className?: string;
|
|
14
16
|
target?: HTMLAnchorElement["target"];
|
|
15
17
|
rel?: string;
|
package/dist/atoms/Badge.svelte
CHANGED
|
@@ -1,82 +1,119 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { Check, TriangleAlert, X, Info, Zap } from "@lucide/svelte";
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
3
6
|
import type {
|
|
4
7
|
ExtendedSemanticVariant,
|
|
5
8
|
SizeVariant,
|
|
6
9
|
} from "../types/variants.js";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
/**
|
|
12
|
+
* `subtle` is the default because a page full of saturated fills is noise —
|
|
13
|
+
* a badge is a label, not a button. `solid` is there for the one badge on
|
|
14
|
+
* a screen that has to shout.
|
|
15
|
+
*/
|
|
16
|
+
type BadgeEmphasis = "subtle" | "solid";
|
|
17
|
+
|
|
18
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
9
19
|
variant?: ExtendedSemanticVariant;
|
|
10
20
|
size?: SizeVariant;
|
|
11
|
-
|
|
21
|
+
emphasis?: BadgeEmphasis;
|
|
22
|
+
/** Label text. Ignored when `children` is provided. */
|
|
23
|
+
text?: string;
|
|
12
24
|
showIcon?: boolean;
|
|
13
|
-
|
|
25
|
+
class?: string;
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
};
|
|
14
28
|
|
|
15
29
|
let {
|
|
16
30
|
variant = "default",
|
|
17
31
|
size = "md",
|
|
32
|
+
emphasis = "subtle",
|
|
18
33
|
text = "",
|
|
19
34
|
showIcon = false,
|
|
35
|
+
class: className = "",
|
|
36
|
+
children,
|
|
37
|
+
...restProps
|
|
20
38
|
}: Props = $props();
|
|
21
39
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const sizeClass =
|
|
27
|
-
size === "sm"
|
|
28
|
-
? "px-2 py-0.5 text-xs"
|
|
29
|
-
: size === "lg"
|
|
30
|
-
? "px-4 py-2 text-base"
|
|
31
|
-
: "px-3 py-1 text-sm";
|
|
32
|
-
|
|
33
|
-
const variantClass =
|
|
34
|
-
variant === "success"
|
|
35
|
-
? "bg-success border-success text-card"
|
|
36
|
-
: variant === "warning"
|
|
37
|
-
? "bg-warning border-warning text-card"
|
|
38
|
-
: variant === "error"
|
|
39
|
-
? "bg-error border-error text-card"
|
|
40
|
-
: variant === "info"
|
|
41
|
-
? "bg-info border-info text-card"
|
|
42
|
-
: variant === "neutral"
|
|
43
|
-
? "bg-neutral border-neutral text-card"
|
|
44
|
-
: variant === "energetic"
|
|
45
|
-
? "bg-energetic border-energetic text-card"
|
|
46
|
-
: "bg-secondary border-secondary text-card";
|
|
47
|
-
|
|
48
|
-
return `${baseClasses} ${sizeClass} ${variantClass}`.trim();
|
|
40
|
+
const sizeClass = $derived.by(() => {
|
|
41
|
+
if (size === "sm") return { box: "h-5 px-2 gap-1 text-xs", icon: 12 };
|
|
42
|
+
if (size === "lg") return { box: "h-7 px-3 gap-2 text-sm", icon: 16 };
|
|
43
|
+
return { box: "h-6 px-2 gap-1 text-xs", icon: 14 };
|
|
49
44
|
});
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Both emphases are built from the SAME step of each family's ramp, so
|
|
48
|
+
* every variant reads at the same weight. Subtle is 100/200/800 (8.2:1),
|
|
49
|
+
* solid is 600 with its `-text` foreground (4.9:1). Before the ramps were
|
|
50
|
+
* calibrated, a solid warning badge sat at 2.15:1 and energetic at 1.97:1.
|
|
51
|
+
*/
|
|
52
|
+
const variantClass = $derived.by(() => {
|
|
53
|
+
const solid = emphasis === "solid";
|
|
54
|
+
switch (variant) {
|
|
55
|
+
case "success":
|
|
56
|
+
return solid
|
|
57
|
+
? "bg-success text-card border-transparent"
|
|
58
|
+
: "bg-success-subtle text-success-text border-success-border";
|
|
59
|
+
case "warning":
|
|
60
|
+
return solid
|
|
61
|
+
? "bg-warning text-card border-transparent"
|
|
62
|
+
: "bg-warning-subtle text-warning-text border-warning-border";
|
|
63
|
+
case "error":
|
|
64
|
+
return solid
|
|
65
|
+
? "bg-error text-card border-transparent"
|
|
66
|
+
: "bg-error-subtle text-error-text border-error-border";
|
|
67
|
+
case "info":
|
|
68
|
+
return solid
|
|
69
|
+
? "bg-info text-card border-transparent"
|
|
70
|
+
: "bg-info-subtle text-info-text border-info-border";
|
|
71
|
+
case "energetic":
|
|
72
|
+
return solid
|
|
73
|
+
? "bg-energetic text-card border-transparent"
|
|
74
|
+
: "bg-energetic-subtle text-energetic-text border-energetic-border";
|
|
75
|
+
case "neutral":
|
|
76
|
+
case "default":
|
|
77
|
+
default:
|
|
78
|
+
return solid
|
|
79
|
+
? "bg-neutral text-card border-transparent"
|
|
80
|
+
: "bg-neutral-subtle text-neutral-text border-neutral-border";
|
|
81
|
+
}
|
|
53
82
|
});
|
|
54
83
|
|
|
55
|
-
const
|
|
56
|
-
|
|
84
|
+
const badgeClasses = $derived(
|
|
85
|
+
cn(`inline-flex items-center justify-center whitespace-nowrap rounded-pill border font-medium ${sizeClass.box} ${variantClass} ${className}`),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
/** One outline icon family, so the variants don't look like a rummage. */
|
|
89
|
+
const Icon = $derived.by(() => {
|
|
90
|
+
switch (variant) {
|
|
91
|
+
case "success":
|
|
92
|
+
return Check;
|
|
93
|
+
case "warning":
|
|
94
|
+
return TriangleAlert;
|
|
95
|
+
case "error":
|
|
96
|
+
return X;
|
|
97
|
+
case "energetic":
|
|
98
|
+
return Zap;
|
|
99
|
+
default:
|
|
100
|
+
return Info;
|
|
101
|
+
}
|
|
57
102
|
});
|
|
103
|
+
|
|
104
|
+
const hasLabel = $derived(Boolean(children) || Boolean(text));
|
|
58
105
|
</script>
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
{#if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/>
|
|
69
|
-
{:else if variant === "error"}
|
|
70
|
-
<X size={iconSize()} class="{iconSpacingClass()} text-card" />
|
|
71
|
-
{:else if variant === "info"}
|
|
72
|
-
<Info size={iconSize()} class="{iconSpacingClass()} text-card" />
|
|
73
|
-
{:else if variant === "neutral"}
|
|
74
|
-
<Info size={iconSize()} class="{iconSpacingClass()} text-card" />
|
|
75
|
-
{:else if variant === "energetic"}
|
|
76
|
-
<Info size={iconSize()} class="{iconSpacingClass()} text-card" />
|
|
107
|
+
{#if hasLabel}
|
|
108
|
+
<span class={badgeClasses} {...restProps}>
|
|
109
|
+
{#if showIcon}
|
|
110
|
+
{@const BadgeIcon = Icon}
|
|
111
|
+
<BadgeIcon size={sizeClass.icon} class="shrink-0" aria-hidden="true" />
|
|
112
|
+
{/if}
|
|
113
|
+
{#if children}
|
|
114
|
+
{@render children()}
|
|
77
115
|
{:else}
|
|
78
|
-
|
|
116
|
+
{text}
|
|
79
117
|
{/if}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</span>
|
|
118
|
+
</span>
|
|
119
|
+
{/if}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
1
3
|
import type { ExtendedSemanticVariant, SizeVariant } from "../types/variants.js";
|
|
2
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `subtle` is the default because a page full of saturated fills is noise —
|
|
6
|
+
* a badge is a label, not a button. `solid` is there for the one badge on
|
|
7
|
+
* a screen that has to shout.
|
|
8
|
+
*/
|
|
9
|
+
type BadgeEmphasis = "subtle" | "solid";
|
|
10
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
3
11
|
variant?: ExtendedSemanticVariant;
|
|
4
12
|
size?: SizeVariant;
|
|
5
|
-
|
|
13
|
+
emphasis?: BadgeEmphasis;
|
|
14
|
+
/** Label text. Ignored when `children` is provided. */
|
|
15
|
+
text?: string;
|
|
6
16
|
showIcon?: boolean;
|
|
7
|
-
|
|
17
|
+
class?: string;
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
};
|
|
8
20
|
declare const Badge: import("svelte").Component<Props, {}, "">;
|
|
9
21
|
type Badge = ReturnType<typeof Badge>;
|
|
10
22
|
export default Badge;
|