@weichenlin/swell-ui 0.2.0 → 0.4.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 +172 -36
- package/dist/index.d.ts +422 -43
- package/dist/index.js +2941 -545
- package/dist/theme.css +185 -77
- package/package.json +1 -2
- package/dist/themes/warm.css +0 -63
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# swell-ui
|
|
2
2
|
|
|
3
|
-
Hand-built React 19 components styled with Tailwind CSS v4: Badge, Button, Card, Checkbox,
|
|
3
|
+
Hand-built React 19 components styled with Tailwind CSS v4: Accordion, Badge, Button, Card, Checkbox,
|
|
4
4
|
CheckboxGroup, Dialog, Drawer, Input (plus Search, Password and Copy), Meter, Select, Sidebar,
|
|
5
|
-
Switch, Tabs, TabNav, ThemeSwitcher, Toast and Tooltip.
|
|
5
|
+
Slider, RangeSlider, Switch, Table, Tabs, TabNav, ThemeSwitcher, Toast and Tooltip.
|
|
6
6
|
|
|
7
7
|
The package ships ESM, type declarations and two CSS files. Your app compiles the CSS with its own
|
|
8
8
|
Tailwind v4, so the design tokens become your utilities too (`bg-primary`, `text-fg`, ...).
|
|
@@ -38,6 +38,94 @@ export function App() {
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### Accordion
|
|
42
|
+
|
|
43
|
+
Compose `Accordion`, `AccordionItem`, `AccordionTrigger`, and `AccordionContent`. Item values must
|
|
44
|
+
be unique and non-empty; triggers contain non-interactive content and include their own heading.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@weichenlin/swell-ui'
|
|
48
|
+
|
|
49
|
+
;<Accordion defaultValue="project" variant="line" headingLevel={3}>
|
|
50
|
+
<AccordionItem value="project">
|
|
51
|
+
<AccordionTrigger description="Name and identity">Project</AccordionTrigger>
|
|
52
|
+
<AccordionContent>Project settings</AccordionContent>
|
|
53
|
+
</AccordionItem>
|
|
54
|
+
</Accordion>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Single mode uses `string | null`; `type="multiple"` uses `readonly string[]`. Use `value` with
|
|
58
|
+
`onValueChange` for controlled state, or `defaultValue` for uncontrolled state. Single mode allows
|
|
59
|
+
all panels to close by default; `collapsible={false}` prevents closing the open item. Change mode
|
|
60
|
+
with a new React key. `variant="outline"` adds individual borders and a surface background.
|
|
61
|
+
|
|
62
|
+
Content stays mounted while closed. `keepMounted={false}` unmounts children after the closing
|
|
63
|
+
animation; `region` opts in to a named landmark. Hidden mounted fields still participate in native
|
|
64
|
+
validation and FormData: expand the first invalid section before focusing its field. Storybook's
|
|
65
|
+
**UI / Accordion / Form Validation** demonstrates that flow. Arrow keys move header focus without
|
|
66
|
+
expanding, Enter/Space toggles, and reduced motion disables transitions.
|
|
67
|
+
|
|
68
|
+
### Slider
|
|
69
|
+
|
|
70
|
+
`Slider` uses a number; `RangeSlider` uses a `readonly [number, number]` and requires a name for
|
|
71
|
+
each thumb. Both use native range inputs for accessibility and form submission.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { Slider, RangeSlider } from '@weichenlin/swell-ui'
|
|
75
|
+
|
|
76
|
+
;<Slider label="Volume" defaultValue={40} formatValue={(value) => `${value}%`} />
|
|
77
|
+
|
|
78
|
+
;<RangeSlider
|
|
79
|
+
label="Price range"
|
|
80
|
+
thumbLabels={['Minimum price', 'Maximum price']}
|
|
81
|
+
defaultValue={[200, 800]}
|
|
82
|
+
min={0}
|
|
83
|
+
max={1000}
|
|
84
|
+
step={50}
|
|
85
|
+
minStepsBetweenThumbs={2}
|
|
86
|
+
name={['priceMin', 'priceMax']}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `value` and `onValueChange` for controlled state, or `defaultValue` for uncontrolled state.
|
|
91
|
+
Controlled owners must accept preview changes immediately. `onValueCommit` runs once after a
|
|
92
|
+
changed pointer or keyboard gesture ends; put expensive work there. Cancellation restores the
|
|
93
|
+
starting value with `onValueChange` reason `cancel`, without committing. Thumbs never swap or push.
|
|
94
|
+
|
|
95
|
+
Defaults are `min=0`, `max=100`, `step=1`, `showValue=true`, and `minStepsBetweenThumbs=0`. Without
|
|
96
|
+
an initial value, single starts at `min` and range starts at `[min,max]`. Bounds must be finite,
|
|
97
|
+
`min < max`, and `(max-min)` must be a whole number of positive steps. External values must be on
|
|
98
|
+
that grid, ordered, and satisfy the gap. `marks` are visual only, not a restricted list of values.
|
|
99
|
+
|
|
100
|
+
`formatValue` formats both the visible and spoken value; `getValueText` overrides only the spoken
|
|
101
|
+
text. Use `aria-label` when omitting a visible `label`. `className` and `ref` target the outer div.
|
|
102
|
+
`name` enables native FormData; range accepts two explicit names. `form` supports an external
|
|
103
|
+
form. Native reset restores uncontrolled defaults; controlled reset remains the owner's choice.
|
|
104
|
+
Keyboard, RTL, cancellation, forms, exact input composition and theme examples live in
|
|
105
|
+
**UI / Slider** in Storybook. Touch screen reader interaction requires device acceptance.
|
|
106
|
+
|
|
107
|
+
### Table
|
|
108
|
+
|
|
109
|
+
Pass the current rows, column definitions, and a stable row ID. Add `search`, `sorting`, `pagination`,
|
|
110
|
+
or `selection` to show controlled UI. The caller handles filtering, sorting, and fetching or slicing
|
|
111
|
+
rows. The pagination `rowCount` is the total after filtering, before paging.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { Table } from '@weichenlin/swell-ui'
|
|
115
|
+
|
|
116
|
+
;<Table
|
|
117
|
+
title="Deployments"
|
|
118
|
+
data={deployments}
|
|
119
|
+
getRowId={(row) => row.id}
|
|
120
|
+
columns={[
|
|
121
|
+
{ id: 'service', header: 'Service', rowHeader: true, cell: (row) => row.service },
|
|
122
|
+
{ id: 'duration', header: 'Duration', numeric: true, cell: (row) => row.duration },
|
|
123
|
+
]}
|
|
124
|
+
emptyState="No deployments yet"
|
|
125
|
+
noResults="No matching deployments"
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
41
129
|
### Change the brand
|
|
42
130
|
|
|
43
131
|
The default look is neutral gray with a near-black primary. A rebrand is two tokens, set once,
|
|
@@ -79,49 +167,80 @@ contrast, override just that one:
|
|
|
79
167
|
Set tokens in `@theme` (or a plain `:root` rule) rather than on an inner element: the derived
|
|
80
168
|
tokens are resolved on the root.
|
|
81
169
|
|
|
170
|
+
### Change the palette
|
|
171
|
+
|
|
172
|
+
Colors come in two layers. A handful of **base colors** carry literal values; every other color
|
|
173
|
+
token is computed from them at runtime (`var()` and `color-mix()`), so overriding a base color
|
|
174
|
+
carries through to borders, muted text, fields, tints and so on:
|
|
175
|
+
|
|
176
|
+
```css
|
|
177
|
+
@theme {
|
|
178
|
+
--color-bg: light-dark(#f5f4ed, #141413);
|
|
179
|
+
--color-surface: light-dark(#faf9f5, #30302e);
|
|
180
|
+
--color-fg: light-dark(#141413, #faf9f5);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The mix percentages are tuned so the default palette, and a warm one like the example above, pass WCAG contrast (text >=4.5:1,
|
|
185
|
+
field borders >=3:1). A palette far from these (a mid-tone page, low-contrast text) can fall
|
|
186
|
+
short, so check contrast after changing `bg`, `surface` or `fg`. Any computed token can still be
|
|
187
|
+
overridden on its own.
|
|
188
|
+
|
|
189
|
+
Override at the root (`@theme` or `:root`): computed tokens are resolved there, so a base color
|
|
190
|
+
set on an inner element does not reach them.
|
|
191
|
+
|
|
82
192
|
### Tokens
|
|
83
193
|
|
|
84
|
-
Every
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| `--color-bg`
|
|
92
|
-
| `--color-surface
|
|
93
|
-
| `--color-
|
|
94
|
-
| `--color-
|
|
95
|
-
| `--color-
|
|
96
|
-
| `--color-
|
|
97
|
-
| `--color-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
| `--color-
|
|
104
|
-
| `--color-
|
|
105
|
-
| `--color-
|
|
106
|
-
| `--color-
|
|
107
|
-
| `--color-
|
|
108
|
-
| `--color-
|
|
109
|
-
| `--color-
|
|
110
|
-
| `--color-
|
|
111
|
-
| `--
|
|
112
|
-
| `--
|
|
113
|
-
|
|
114
|
-
|
|
194
|
+
Every pair is written `light-dark(<light>, <dark>)`; override it the same way, once. All color
|
|
195
|
+
tokens are also Tailwind utilities (`bg-surface`, `text-fg-muted`, `border-input-border`, ...).
|
|
196
|
+
|
|
197
|
+
**Base colors**
|
|
198
|
+
|
|
199
|
+
| Token | Used for |
|
|
200
|
+
| --------------------------------------- | ------------------------------------------------------ |
|
|
201
|
+
| `--color-bg` | Page background |
|
|
202
|
+
| `--color-surface` | Cards, panels and text fields |
|
|
203
|
+
| `--color-surface-raised` | Popovers and dialogs |
|
|
204
|
+
| `--color-fg` | Text |
|
|
205
|
+
| `--color-primary`, `--color-primary-fg` | Brand fill and the text on it. Default: `fg` (neutral) |
|
|
206
|
+
| `--color-focus` | Keyboard focus outline (blue) |
|
|
207
|
+
| `--color-{danger,success,warning,info}` | Status tones: solid fill, icon, and danger text |
|
|
208
|
+
|
|
209
|
+
**Computed** (from the base colors; override only to fine-tune)
|
|
210
|
+
|
|
211
|
+
| Token | Used for | Computed from |
|
|
212
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
213
|
+
| `--color-accent` | Secondary brand color; unused by components | `primary` |
|
|
214
|
+
| `--color-surface-muted` | Hover fills, secondary button | fg into bg |
|
|
215
|
+
| `--color-fg-strong`, `-fg-muted`, `-fg-subtle` | Text from strong to quiet (`fg-subtle` >=4.5:1 on every surface) | fg into bg |
|
|
216
|
+
| `--color-border`, `-border-strong`, `-ring`, `-ring-deep` | Dividers, hairlines, 1px button and tab-list rings | fg into bg |
|
|
217
|
+
| `--color-inverse`, `-inverse-fg` | `dark` button, tooltips | fg into bg; the page on dark |
|
|
218
|
+
| `--color-input-bg`, `-input-bg-hover` | Text field fill (read-only fields use `bg`) | `surface` |
|
|
219
|
+
| `--color-input-border`, `-input-border-hover` | Field border; also the off track of Switch and box of Checkbox | fg into bg; `fg-strong` |
|
|
220
|
+
| `--color-input-placeholder` | Placeholder text | fg into bg |
|
|
221
|
+
| `--color-input-focus`, `-input-halo` | Focused field border and its 3px ring | `primary`; input-focus at 14% |
|
|
222
|
+
| `--color-input-halo-invalid`, `-invalid-focus` | Same ring for invalid fields | danger at 12% / 22% |
|
|
223
|
+
| `--color-control-checked`, `-control-checked-fg` | Checked Switch/Checkbox fill and the check on it | `primary` / `primary-fg` |
|
|
224
|
+
| `--color-switch-handle`, `-switch-handle-checked` | Switch thumb, off / on | off: `surface-raised` / dark `fg`; on: white or black by track lightness |
|
|
225
|
+
| `--color-popover-bg`, `-popover-border`, `-popover-highlight` | Select picker and dialogs; highlight is the active row | `surface-raised`, fg into it |
|
|
226
|
+
| `--color-dialog-backdrop` | Dimmed page behind Dialog and Drawer | fg at 40%; black at 60% on dark |
|
|
227
|
+
| `--color-neutral` | Neutral status tone | fg into bg |
|
|
228
|
+
| `--color-{tone}-fg` | Text on a solid status fill | `surface` / `bg` |
|
|
229
|
+
| `--color-{tone}-subtle-bg`, `-border` | Tinted badge and its hairline | tone into bg |
|
|
230
|
+
| `--color-{tone}-subtle-fg` | Text on the tinted badge (>=5:1) | tone pushed toward fg |
|
|
231
|
+
|
|
232
|
+
Shadows (`--shadow-input`, `-input-lift`, `-switch-thumb`, `-popover`, `-dialog`, `-whisper`) and
|
|
233
|
+
the input highlight are black or white at low opacity: they shade, so they have no palette color.
|
|
234
|
+
`--container-page` is `max-w-page` (1200px).
|
|
235
|
+
|
|
236
|
+
### Serif headings
|
|
115
237
|
|
|
116
238
|
```css
|
|
117
239
|
@import 'tailwindcss';
|
|
118
240
|
@import '@weichenlin/swell-ui/styles.css';
|
|
119
|
-
@import '@weichenlin/swell-ui/themes/warm.css'; /* parchment surfaces, brown primary */
|
|
120
241
|
@import '@weichenlin/swell-ui/typography.css'; /* h1-h3 in the serif stack, weight 500 */
|
|
121
242
|
```
|
|
122
243
|
|
|
123
|
-
Both are independent; your own `@theme` overrides go after them.
|
|
124
|
-
|
|
125
244
|
### Fonts
|
|
126
245
|
|
|
127
246
|
swell-ui sets no font names: text uses Tailwind's `--font-sans` (the system UI stack), and
|
|
@@ -266,6 +385,23 @@ links do) closes it.
|
|
|
266
385
|
</TabNav>
|
|
267
386
|
```
|
|
268
387
|
|
|
388
|
+
`Button` takes `asChild` too, for a navigation that should look like a button. It keeps the
|
|
389
|
+
button's look, icons and ripple, and stays a real link (open in a new tab, copy the URL):
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
<Button asChild intent="secondary" iconLeft={<Plus />}>
|
|
393
|
+
<Link to="/apps/new">New app</Link>
|
|
394
|
+
</Button>
|
|
395
|
+
|
|
396
|
+
<Button asChild icon={<Settings />} aria-label="Settings">
|
|
397
|
+
<Link to="/settings" />
|
|
398
|
+
</Button>
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`disabled` (with or without `disabledReason`) works as on the button: the link stays focusable with
|
|
402
|
+
`aria-disabled`, and clicks are stopped before your Link sees them, so it never navigates. `loading`
|
|
403
|
+
and `type` are button-only.
|
|
404
|
+
|
|
269
405
|
## Develop
|
|
270
406
|
|
|
271
407
|
Node 26.11.1 (`.node-version`, via fnm) and pnpm 12.
|