@weichenlin/swell-ui 0.0.0-stage → 0.1.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 +187 -2
- package/dist/index.d.ts +659 -0
- package/dist/index.js +3462 -0
- package/dist/styles.css +6 -0
- package/dist/theme.css +337 -0
- package/package.json +72 -3
package/README.md
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
-
#
|
|
1
|
+
# swell-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hand-built React 19 components styled with Tailwind CSS v4: Badge, Button, Card, Checkbox,
|
|
4
|
+
CheckboxGroup, Dialog, Drawer, Input (plus Search, Password and Copy), Meter, Select, Sidebar,
|
|
5
|
+
Switch, Tabs, TabNav, ThemeSwitcher, Toast and Tooltip.
|
|
6
|
+
|
|
7
|
+
The package ships ESM, type declarations and two CSS files. Your app compiles the CSS with its own
|
|
8
|
+
Tailwind v4, so the design tokens become your utilities too (`bg-primary`, `text-fg`, ...).
|
|
9
|
+
|
|
10
|
+
## Use it in an app
|
|
11
|
+
|
|
12
|
+
Peer dependencies: `react` and `react-dom` 19, `tailwindcss` 4.3+, `lucide-react`.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @weichenlin/swell-ui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
In the CSS file that imports Tailwind:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
@import 'tailwindcss';
|
|
22
|
+
@import '@weichenlin/swell-ui/styles.css';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`styles.css` brings the tokens (`theme.css`) and an `@source` that points Tailwind at the bundled
|
|
26
|
+
components, so their classes are generated even though Tailwind skips `node_modules` by default.
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Button, toast, Toaster } from '@weichenlin/swell-ui'
|
|
30
|
+
|
|
31
|
+
export function App() {
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<Button onClick={() => toast.success('Saved')}>Save</Button>
|
|
35
|
+
<Toaster />
|
|
36
|
+
</>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Override tokens
|
|
42
|
+
|
|
43
|
+
Redefine any token after the import. Light values go in `@theme`; dark values in the same two dark
|
|
44
|
+
blocks `theme.css` uses:
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
@import 'tailwindcss';
|
|
48
|
+
@import '@weichenlin/swell-ui/styles.css';
|
|
49
|
+
|
|
50
|
+
@theme {
|
|
51
|
+
--color-primary: #2563eb;
|
|
52
|
+
--radius-md: 6px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:root[data-theme='dark'] {
|
|
56
|
+
--color-primary: #60a5fa;
|
|
57
|
+
}
|
|
58
|
+
:root:not([data-theme='light']) {
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
--color-primary: #60a5fa;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Note that the theme replaces Tailwind's `rounded-sm/md/lg` values (6, 8 and 12px) and adds
|
|
66
|
+
`rounded-sharp` (4px), shadows (`shadow-popover`, `shadow-dialog`, ...) and `max-w-page`.
|
|
67
|
+
|
|
68
|
+
### Tokens only
|
|
69
|
+
|
|
70
|
+
To use the tokens without the components (no component classes generated), import `theme.css`
|
|
71
|
+
instead:
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
@import 'tailwindcss';
|
|
75
|
+
@import '@weichenlin/swell-ui/theme.css';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `cn`
|
|
79
|
+
|
|
80
|
+
`cn` (clsx + tailwind-merge) is exported and knows the custom tokens, so
|
|
81
|
+
`cn('rounded-sharp', 'rounded-md')` gives `rounded-md`.
|
|
82
|
+
|
|
83
|
+
## Dark mode
|
|
84
|
+
|
|
85
|
+
Without a `data-theme` attribute the theme follows the system (`prefers-color-scheme`).
|
|
86
|
+
`data-theme="light"` or `data-theme="dark"` on `<html>` forces one. The `dark:` variant follows the
|
|
87
|
+
same rule.
|
|
88
|
+
|
|
89
|
+
`useTheme` persists the choice and sets the attribute; `ThemeSwitcher` is the toggle button:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { ThemeSwitcher, useTheme } from '@weichenlin/swell-ui'
|
|
93
|
+
|
|
94
|
+
function Header() {
|
|
95
|
+
const { theme, setTheme } = useTheme({ storageKey: 'my-app-theme' }) // default 'swell-ui-theme'
|
|
96
|
+
return <ThemeSwitcher value={theme} onChange={setTheme} />
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
To avoid a flash of the wrong theme before React mounts, apply the saved value in `<head>`:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<script>
|
|
104
|
+
try {
|
|
105
|
+
const t = localStorage.getItem('my-app-theme')
|
|
106
|
+
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t
|
|
107
|
+
} catch {}
|
|
108
|
+
</script>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Router links: `asChild`
|
|
112
|
+
|
|
113
|
+
`SidebarItem` and `TabNavLink` render a plain `<a>`. With `asChild` they render their only child
|
|
114
|
+
instead (your router's Link) and merge their props, classes, ref and handlers into it. The link
|
|
115
|
+
sets `href` and `aria-current="page"`; the current item is styled from that attribute.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { Link } from '@tanstack/react-router' // any router's Link
|
|
119
|
+
import {
|
|
120
|
+
Sidebar,
|
|
121
|
+
SidebarBadge,
|
|
122
|
+
SidebarBody,
|
|
123
|
+
SidebarGroup,
|
|
124
|
+
SidebarIcon,
|
|
125
|
+
SidebarItem,
|
|
126
|
+
SidebarProvider,
|
|
127
|
+
SidebarTrigger,
|
|
128
|
+
} from '@weichenlin/swell-ui'
|
|
129
|
+
import { Box } from 'lucide-react'
|
|
130
|
+
|
|
131
|
+
function Nav() {
|
|
132
|
+
return (
|
|
133
|
+
<SidebarProvider>
|
|
134
|
+
<SidebarTrigger />
|
|
135
|
+
<Sidebar aria-label="Main">
|
|
136
|
+
<SidebarBody>
|
|
137
|
+
<SidebarGroup>
|
|
138
|
+
<SidebarItem asChild>
|
|
139
|
+
<Link to="/projects">
|
|
140
|
+
<SidebarIcon>
|
|
141
|
+
<Box />
|
|
142
|
+
</SidebarIcon>
|
|
143
|
+
Projects
|
|
144
|
+
<SidebarBadge>8</SidebarBadge>
|
|
145
|
+
</Link>
|
|
146
|
+
</SidebarItem>
|
|
147
|
+
</SidebarGroup>
|
|
148
|
+
</SidebarBody>
|
|
149
|
+
</Sidebar>
|
|
150
|
+
</SidebarProvider>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The accessible name is "Projects 8". Plain text children become the truncating label. Below 960px
|
|
156
|
+
the sidebar is a drawer; a click the link handles in place (it calls `preventDefault`, as router
|
|
157
|
+
links do) closes it.
|
|
158
|
+
|
|
159
|
+
`TabNavLink` works the same way and takes `icon` and `badge` props like `Tab`:
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<TabNav aria-label="Sections">
|
|
163
|
+
<TabNavLink asChild icon={<Boxes />} badge={<Badge size="sm">12</Badge>}>
|
|
164
|
+
<Link to="/resources">Resources</Link>
|
|
165
|
+
</TabNavLink>
|
|
166
|
+
</TabNav>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Develop
|
|
170
|
+
|
|
171
|
+
Node 26.11.1 (`.node-version`, via fnm) and pnpm 12.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm install
|
|
175
|
+
pnpm storybook # http://localhost:6006
|
|
176
|
+
pnpm test # vitest (jsdom)
|
|
177
|
+
pnpm typecheck # tsc -b
|
|
178
|
+
pnpm lint # vp check: format, lint, type-aware lint
|
|
179
|
+
pnpm knip # unused files, exports and dependencies
|
|
180
|
+
pnpm build # vp pack -> dist/index.js, index.d.ts, theme.css, styles.css
|
|
181
|
+
pnpm build-storybook
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`src/styles/theme.css` and `src/styles/styles.css` are copied into `dist/` as they are, so the
|
|
185
|
+
`@source "./*.js"` in `styles.css` is relative to `dist/`. Storybook and the lint use
|
|
186
|
+
`src/styles/dev.css`, which adds Tailwind and scans `src/` and `stories/`.
|
|
187
|
+
|
|
188
|
+
A lefthook pre-commit hook runs `vp check --fix` on staged files and `knip`.
|