@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 CHANGED
@@ -1,3 +1,188 @@
1
- # Temporary Holding Version
1
+ # swell-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`.