@skunkworksai/ui-kit 0.1.18 → 0.1.23
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/AGENTS.md +1 -1
- package/README.md +2 -0
- package/package.json +29 -19
- package/rules/button.md +1 -0
- package/rules/dashboard.md +56 -4
- package/rules/header.md +20 -1
- package/rules/input.md +34 -2
- package/rules/pagetitle.md +32 -0
- package/rules/table-manager.md +44 -1
- package/rules/textarea.md +23 -1
- package/rules/wizard-stepper.md +207 -0
package/AGENTS.md
CHANGED
|
@@ -231,7 +231,7 @@ Send `design-gaps.md` to the design team (email design@skunkworks.ai or paste in
|
|
|
231
231
|
|
|
232
232
|
Before using a component, read `node_modules/@skunkworksai/ui-kit/rules/<name>.md` for props reference, examples, and dos/don'ts. Replace `<name>` with the component name (examples: `button.md`, `alert-dialog.md`, `data-table.md`).
|
|
233
233
|
|
|
234
|
-
Available: accordion, alert, alert-dialog, aspect-ratio, avatar, badge, bento-menu, breadcrumb, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dashboard, data-table, date-picker, dialog, drawer, dropdown-menu, empty, field, form, header, hover-card, icons, input, kbd, label, location-tree-filter, logo, menubar, navigation-menu, notifications-menu, pagetitle, pagination, popover, profile, progress, radio-group, scroll-area, scroll-text, select, separator, sheet, sidebar, skeleton, spinner, switch, table, table-manager, tabs, textarea, toaster, toggle, toggle-group, tokens, tooltip, typography.
|
|
234
|
+
Available: accordion, alert, alert-dialog, aspect-ratio, avatar, badge, bento-menu, breadcrumb, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dashboard, data-table, date-picker, dialog, drawer, dropdown-menu, empty, field, form, header, hover-card, icons, input, kbd, label, location-tree-filter, logo, menubar, navigation-menu, notifications-menu, pagetitle, pagination, popover, profile, progress, radio-group, scroll-area, scroll-text, select, separator, sheet, sidebar, skeleton, spinner, switch, table, table-manager, tabs, textarea, toaster, toggle, toggle-group, tokens, tooltip, typography, wizard-stepper.
|
|
235
235
|
|
|
236
236
|
---
|
|
237
237
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ A React component library built on [shadcn/ui](https://ui.shadcn.com), Tailwind
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
+
> **Do not clone this repository.** This is a private source repo. Consume the package via `pnpm add` only — cloning gives you no working build and no support path.
|
|
10
|
+
|
|
9
11
|
This package is published to GitHub Packages. Authentication is required.
|
|
10
12
|
|
|
11
13
|
### 1. Create a GitHub Personal Access Token (PAT)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skunkworksai/ui-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"react-dom": ">=18"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@base-ui/react": "^1.
|
|
51
|
+
"@base-ui/react": "^1.5.0",
|
|
52
52
|
"@radix-ui/react-accordion": "^1.2.0",
|
|
53
53
|
"@radix-ui/react-alert-dialog": "^1.1.0",
|
|
54
54
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@radix-ui/react-label": "^2.1.0",
|
|
63
63
|
"@radix-ui/react-navigation-menu": "^1.2.0",
|
|
64
64
|
"@radix-ui/react-popover": "^1.1.0",
|
|
65
|
-
"@radix-ui/react-progress": "^1.1.
|
|
65
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
66
66
|
"@radix-ui/react-radio-group": "^1.1.0",
|
|
67
67
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
|
68
68
|
"@radix-ui/react-select": "^2.1.0",
|
|
@@ -78,40 +78,50 @@
|
|
|
78
78
|
"class-variance-authority": "^0.7.1",
|
|
79
79
|
"clsx": "^2.1.1",
|
|
80
80
|
"cmdk": "^1.0.0",
|
|
81
|
-
"date-fns": "^4.
|
|
81
|
+
"date-fns": "^4.3.0",
|
|
82
82
|
"embla-carousel": "^8.6.0",
|
|
83
83
|
"embla-carousel-react": "^8.6.0",
|
|
84
|
-
"lucide-react": "^
|
|
85
|
-
"motion": "^12.
|
|
84
|
+
"lucide-react": "^1.16.0",
|
|
85
|
+
"motion": "^12.40.0",
|
|
86
86
|
"radix-ui": "^1.4.3",
|
|
87
|
-
"react-day-picker": "^
|
|
88
|
-
"react-hook-form": "^7.
|
|
87
|
+
"react-day-picker": "^10.0.1",
|
|
88
|
+
"react-hook-form": "^7.76.1",
|
|
89
89
|
"recharts": "^3.8.1",
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"tailwind-merge": "^3.5.0",
|
|
90
|
+
"sonner": "^2.0.7",
|
|
91
|
+
"tailwind-merge": "^3.6.0",
|
|
93
92
|
"tw-animate-css": "^1.4.0"
|
|
94
93
|
},
|
|
95
94
|
"devDependencies": {
|
|
95
|
+
"@eslint/js": "^10.0.1",
|
|
96
96
|
"@storybook/addon-a11y": "^8",
|
|
97
97
|
"@storybook/addon-docs": "^8",
|
|
98
98
|
"@storybook/addon-onboarding": "^8",
|
|
99
99
|
"@storybook/react": "^8",
|
|
100
100
|
"@storybook/react-vite": "^8",
|
|
101
101
|
"@tailwindcss/vite": "^4",
|
|
102
|
-
"@types/node": "^
|
|
102
|
+
"@types/node": "^25",
|
|
103
103
|
"@types/react": "^19",
|
|
104
104
|
"@types/react-dom": "^19",
|
|
105
|
-
"@vitejs/plugin-react-swc": "^
|
|
106
|
-
"eslint": "^
|
|
107
|
-
"react": "
|
|
108
|
-
"react-
|
|
105
|
+
"@vitejs/plugin-react-swc": "^4",
|
|
106
|
+
"eslint": "^10",
|
|
107
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
108
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
109
|
+
"globals": "^17.6.0",
|
|
110
|
+
"react": "19.2.6",
|
|
111
|
+
"react-dom": "19.2.6",
|
|
112
|
+
"shadcn": "^4.8.0",
|
|
109
113
|
"storybook": "^8",
|
|
110
114
|
"tailwindcss": "^4",
|
|
111
115
|
"tailwindcss-animate": "^1.0.7",
|
|
112
|
-
"typescript": "^
|
|
116
|
+
"typescript": "^6",
|
|
117
|
+
"typescript-eslint": "^8.59.4",
|
|
113
118
|
"vite": "^6",
|
|
114
|
-
"vite-plugin-checker": "^0.
|
|
119
|
+
"vite-plugin-checker": "^0.13"
|
|
115
120
|
},
|
|
116
|
-
"packageManager": "pnpm@10.32.1"
|
|
121
|
+
"packageManager": "pnpm@10.32.1",
|
|
122
|
+
"pnpm": {
|
|
123
|
+
"overrides": {
|
|
124
|
+
"ws@>=8.0.0 <8.20.1": ">=8.20.1"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
117
127
|
}
|
package/rules/button.md
CHANGED
|
@@ -61,6 +61,7 @@ import { AppButton } from "@/components/primitives/AppButton"
|
|
|
61
61
|
- ✅ Use the `loading` prop for async actions — do not place `AppSpinner` inside manually
|
|
62
62
|
- ✅ Use `iconLeft` / `iconRight` for icons — do not wrap icons in extra `<div>` or `<span>`
|
|
63
63
|
- ✅ Use `variant` for color intent — do not override button color with Tailwind color classes
|
|
64
|
+
- ✅ `AppButton` forwards refs to the underlying `<button>` element — safe to use as `asChild` child of Radix primitives (e.g. `PopoverTrigger`, `TooltipTrigger`)
|
|
64
65
|
- ❌ Do not use `size="icon"` with text children — icon size is for icon-only buttons
|
|
65
66
|
- ❌ Do not set `gap` or `flex` on the button — icon spacing is controlled by the size variant
|
|
66
67
|
- ❌ If a prop you need is missing, stop and inform the design team
|
package/rules/dashboard.md
CHANGED
|
@@ -133,6 +133,58 @@ import { AppDashboardBackgroundWave, AppDashboardBackgroundDots } from '@skunkwo
|
|
|
133
133
|
| `AppDashboardBackgroundWave` | Perlin noise wave grid + mouse-follow glow. Default. |
|
|
134
134
|
| `AppDashboardBackgroundDots` | Uniform dot grid. Dots fade near cursor (closer = lower opacity). |
|
|
135
135
|
|
|
136
|
+
### Background preset props
|
|
137
|
+
|
|
138
|
+
Both presets share the following props in addition to `animated` and `interactive`:
|
|
139
|
+
|
|
140
|
+
#### `AppDashboardBackgroundDots`
|
|
141
|
+
|
|
142
|
+
| Prop | Type | Default | Description |
|
|
143
|
+
|------|------|---------|-------------|
|
|
144
|
+
| `color` | `string` | `--sidebar-ring` | Dot color. Any valid CSS color string: hex, oklch, hsl, or `var(--token)`. |
|
|
145
|
+
|
|
146
|
+
#### `AppDashboardBackgroundWave`
|
|
147
|
+
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
|------|------|---------|-------------|
|
|
150
|
+
| `color` | `string` | `--sidebar-ring` | Wave line color. Any valid CSS color string: hex, oklch, hsl, or `var(--token)`. |
|
|
151
|
+
| `glowColor` | `string` | `--sidebar-primary` / `--sidebar-accent` | Cursor glow color. Replaces both glow layers with a single color mixed at different opacities. |
|
|
152
|
+
|
|
153
|
+
### Customizing background color
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { AppDashboardBackgroundWave, AppDashboardBackgroundDots } from '@skunkworks-ai/ui-kit'
|
|
157
|
+
|
|
158
|
+
// Wave with custom line + glow color (oklch, hex, hsl, or CSS variable — all valid)
|
|
159
|
+
<AppDashboard background={
|
|
160
|
+
<AppDashboardBackgroundWave
|
|
161
|
+
color="oklch(0.75 0.18 150)"
|
|
162
|
+
glowColor="oklch(0.75 0.18 150)"
|
|
163
|
+
/>
|
|
164
|
+
}>
|
|
165
|
+
...
|
|
166
|
+
</AppDashboard>
|
|
167
|
+
|
|
168
|
+
// Dots with custom dot color
|
|
169
|
+
<AppDashboard background={
|
|
170
|
+
<AppDashboardBackgroundDots color="#ff6b35" />
|
|
171
|
+
}>
|
|
172
|
+
...
|
|
173
|
+
</AppDashboard>
|
|
174
|
+
|
|
175
|
+
// Using a design token from the consumer's theme
|
|
176
|
+
<AppDashboard background={
|
|
177
|
+
<AppDashboardBackgroundWave
|
|
178
|
+
color="var(--brand-primary)"
|
|
179
|
+
glowColor="var(--brand-primary)"
|
|
180
|
+
/>
|
|
181
|
+
}>
|
|
182
|
+
...
|
|
183
|
+
</AppDashboard>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When `color` is omitted, both presets fall back to the resolved value of `--sidebar-ring`. When `glowColor` is omitted on Wave, the glow uses `--sidebar-primary` and `--sidebar-accent`.
|
|
187
|
+
|
|
136
188
|
### Custom background contract
|
|
137
189
|
|
|
138
190
|
Custom backgrounds receive `animated`, `interactive`, and `themeClass` from `AppDashboardBackgroundContext`:
|
|
@@ -183,9 +235,11 @@ The canvas animation runs by default. Use `animated` to control it:
|
|
|
183
235
|
|
|
184
236
|
**When `animated="auto"` (default):** animation runs normally unless the user's OS has `prefers-reduced-motion: reduce` set, in which case the canvas is skipped automatically. No consumer code needed for accessibility compliance.
|
|
185
237
|
|
|
186
|
-
## AppThemeProvider
|
|
238
|
+
## AppThemeProvider
|
|
239
|
+
|
|
240
|
+
`AppDashboard` includes `AppThemeProvider` internally — the theme toggle works out of the box with no extra setup required.
|
|
187
241
|
|
|
188
|
-
|
|
242
|
+
If you use `AppHeader` standalone (outside `AppDashboard`), wrap your layout with `AppThemeProvider` for the toggle to function:
|
|
189
243
|
|
|
190
244
|
```tsx
|
|
191
245
|
import { AppThemeProvider } from '@skunkworksai/ui-kit'
|
|
@@ -201,8 +255,6 @@ export default function RootLayout({ children }) {
|
|
|
201
255
|
}
|
|
202
256
|
```
|
|
203
257
|
|
|
204
|
-
Without it, the toggle renders but does nothing.
|
|
205
|
-
|
|
206
258
|
## Nav types
|
|
207
259
|
|
|
208
260
|
```ts
|
package/rules/header.md
CHANGED
|
@@ -20,7 +20,7 @@ All right-side controls are included by default. Each can be removed or replaced
|
|
|
20
20
|
|------|------|---------|-------------|
|
|
21
21
|
| `searchbar` | `ReactNode` | standard search input | Searchbar slot. Pass `<AppInput />` to customise. `null` removes it entirely. |
|
|
22
22
|
| `actions` | `ReactNode` | network combobox + bento + theme toggle + avatar | Entire right-side slot. `null` removes all actions. Overrides all other action props. |
|
|
23
|
-
| `avatar` | `ReactNode` | `AppAvatar` with
|
|
23
|
+
| `avatar` | `ReactNode` | `AppAvatar` with `UserRound` icon | Avatar inside default actions. `null` hides it. Ignored when `actions` is provided. |
|
|
24
24
|
| `networkSelect` | `ReactNode` | `AppHeaderNetworkCombobox` | Network picker inside default actions. `null` hides it. Pass a custom node to override. Ignored when `actions` is provided. |
|
|
25
25
|
| `networks` | `NetworkOption[]` | 2 demo entries | List of networks for the built-in combobox. Ignored when `networkSelect` or `actions` is provided. |
|
|
26
26
|
| `currentNetwork` | `string` | first network in list | Controlled selected network. Omit for uncontrolled. Ignored when `networkSelect` or `actions` is provided. |
|
|
@@ -163,6 +163,8 @@ Customize via the `header` prop — never import `AppHeader` directly for this:
|
|
|
163
163
|
|
|
164
164
|
Only needed when building a custom layout that does not use `AppDashboard`:
|
|
165
165
|
|
|
166
|
+
> **Note:** `AppThemeProvider` is required for the theme toggle to function. When using `AppHeader` inside `AppDashboard`, this is handled automatically. For standalone usage, wrap your layout with `AppThemeProvider`.
|
|
167
|
+
|
|
166
168
|
```tsx
|
|
167
169
|
import { AppHeader, type NetworkOption } from '@skunkworks/ui'
|
|
168
170
|
|
|
@@ -226,6 +228,23 @@ import { AppInput } from '@skunkworksai/ui-kit'
|
|
|
226
228
|
<AppDashboard header={{ searchbar: null }}>
|
|
227
229
|
```
|
|
228
230
|
|
|
231
|
+
## Consumer Tailwind setup
|
|
232
|
+
|
|
233
|
+
`AppHeader` uses `md:hidden` to hide the mobile search icon (`AppHeaderSearchbar`) at desktop widths. If the consumer's `@tailwindcss/vite` plugin injects its utilities layer **after** the ui-kit bundle, it overwrites same-named utilities and `md:hidden` stops working.
|
|
234
|
+
|
|
235
|
+
**Required** — add this to the consumer's `app.css` (or equivalent global CSS entry):
|
|
236
|
+
|
|
237
|
+
```css
|
|
238
|
+
@layer theme, base, components, utilities;
|
|
239
|
+
@import "tailwindcss/utilities" layer(utilities);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This explicitly names the `utilities` layer so both the ui-kit bundle and the consumer's Tailwind output merge into the same layer in document order.
|
|
243
|
+
|
|
244
|
+
**Symptom of missing setup:** the mobile search icon is visible at all breakpoints instead of hiding at `md`.
|
|
245
|
+
|
|
246
|
+
See `rules/tokens.md` for the full consumer setup checklist.
|
|
247
|
+
|
|
229
248
|
## Rules
|
|
230
249
|
|
|
231
250
|
- **Do** provide `networks` with real data — the default 2-entry list is a demo placeholder only.
|
package/rules/input.md
CHANGED
|
@@ -12,10 +12,13 @@ import { AppInput } from "@/components/primitives/AppInput"
|
|
|
12
12
|
|------|------|---------|-------------|
|
|
13
13
|
| `leftAdornment` | `ReactNode` | — | Icon or element rendered inside the left edge of the input |
|
|
14
14
|
| `rightAdornment` | `ReactNode` | — | Icon or element rendered inside the right edge of the input |
|
|
15
|
+
| `wrapperClassName` | `string` | — | Applied to the outer wrapper `<div>` when adornments are present. Use to override the default `w-full`. Silently ignored when no adornments are set. |
|
|
15
16
|
| `type` | `string` | `"text"` | HTML input type (text, email, password, number, etc.) |
|
|
16
17
|
| `placeholder` | `string` | — | Placeholder text |
|
|
17
18
|
| `disabled` | `boolean` | `false` | Disables the input |
|
|
18
|
-
| `className` | `string` | — | Additional Tailwind classes applied to the input element |
|
|
19
|
+
| `className` | `string` | — | Additional Tailwind classes applied to the inner `<input>` element |
|
|
20
|
+
|
|
21
|
+
> `className` always routes to the inner `<input>`. Use `wrapperClassName` to control the outer wrapper size/layout when adornments are in use.
|
|
19
22
|
|
|
20
23
|
> Accepts all standard HTML `<input>` attributes in addition to the props above.
|
|
21
24
|
|
|
@@ -43,6 +46,33 @@ import { AppInput } from "@/components/primitives/AppInput"
|
|
|
43
46
|
/>
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
### Controlling wrapper width with adornments
|
|
50
|
+
|
|
51
|
+
Without adornments `className` width classes land directly on `<input>`. With adornments you must use `wrapperClassName` — `className` targets the inner element and a width there does not constrain the wrapper.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
{/* Fixed width on desktop, full-width on mobile */}
|
|
55
|
+
<AppInput
|
|
56
|
+
leftAdornment={<Search className="size-4" />}
|
|
57
|
+
placeholder="Search..."
|
|
58
|
+
wrapperClassName="w-full md:w-64"
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Fill remaining space in a flex row */}
|
|
62
|
+
<AppInput
|
|
63
|
+
leftAdornment={<Search className="size-4" />}
|
|
64
|
+
placeholder="Search..."
|
|
65
|
+
wrapperClassName="flex-1"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* Intrinsic width */}
|
|
69
|
+
<AppInput
|
|
70
|
+
leftAdornment={<Search className="size-4" />}
|
|
71
|
+
placeholder="Search..."
|
|
72
|
+
wrapperClassName="w-auto"
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
46
76
|
### Disabled
|
|
47
77
|
```tsx
|
|
48
78
|
<AppInput disabled placeholder="Not editable" />
|
|
@@ -59,8 +89,10 @@ import { AppInput } from "@/components/primitives/AppInput"
|
|
|
59
89
|
## Rules
|
|
60
90
|
- ✅ Always use `AppInput` — never use a raw `<input>` element
|
|
61
91
|
- ✅ Use `leftAdornment` / `rightAdornment` for icons inside the input — do not wrap `AppInput` in a relative `<div>` to position them manually
|
|
92
|
+
- ✅ Use `wrapperClassName` to control width/layout when adornments are present — `className` targets the inner `<input>` only
|
|
62
93
|
- ✅ Pair with `AppLabel` using matching `id` and `htmlFor` — never use a raw `<label>`
|
|
63
94
|
- ✅ Pair with `AppField` or `AppForm` for full form field layout with error states
|
|
64
95
|
- ❌ Do not add `pl-*` or `pr-*` padding when using adornments — padding is adjusted automatically
|
|
65
|
-
- ❌ Do not
|
|
96
|
+
- ❌ Do not add width classes via `className` when adornments are present — they land on `<input>`, not the wrapper; use `wrapperClassName` instead
|
|
97
|
+
- ❌ `wrapperClassName` has no effect without adornments — without `leftAdornment` or `rightAdornment` there is no wrapper element
|
|
66
98
|
- ❌ If a prop you need is missing, stop and inform the design team
|
package/rules/pagetitle.md
CHANGED
|
@@ -143,12 +143,44 @@ When `breadcrumbs` is omitted, the component reads `window.location.pathname` on
|
|
|
143
143
|
</AppDashboard>
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
## usePageTitle — ReactNode prop stability
|
|
147
|
+
|
|
148
|
+
`usePageTitle` compares props by identity, not by value. Primitive props (`title`, `subtitle`, `titleColor`) are compared by value and are safe to pass inline. ReactNode props (`actions`, `breadcrumbs`, `icon`) are compared by **reference** — passing them as inline JSX causes the effect to re-run on every render.
|
|
149
|
+
|
|
150
|
+
**Always memoize ReactNode props:**
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// ✅ Correct — stable reference
|
|
154
|
+
const actions = React.useMemo(
|
|
155
|
+
() => <AppButton onClick={handleAdd}>+ Add Site</AppButton>,
|
|
156
|
+
[handleAdd]
|
|
157
|
+
)
|
|
158
|
+
usePageTitle({ title: "Sites", actions })
|
|
159
|
+
|
|
160
|
+
// ✅ Also correct — module-scope constant (never changes)
|
|
161
|
+
const BREADCRUMBS = [
|
|
162
|
+
{ label: "Dashboard", href: "/" },
|
|
163
|
+
{ label: "Sites" },
|
|
164
|
+
]
|
|
165
|
+
usePageTitle({ title: "Sites", breadcrumbs: BREADCRUMBS })
|
|
166
|
+
|
|
167
|
+
// ❌ Wrong — new object/JSX every render, causes effect churn
|
|
168
|
+
usePageTitle({
|
|
169
|
+
title: "Sites",
|
|
170
|
+
actions: <AppButton>+ Add Site</AppButton>,
|
|
171
|
+
breadcrumbs: [{ label: "Dashboard", href: "/" }, { label: "Sites" }],
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> **Note:** Earlier versions used `JSON.stringify` to compare deps, which crashed when ReactNode props contained circular references (e.g. React elements with `$$typeof`). The current implementation compares individual fields by identity — memoization is now required to avoid redundant effect re-runs rather than crashes.
|
|
176
|
+
|
|
146
177
|
## Rules
|
|
147
178
|
|
|
148
179
|
- **Do** use `pageTitle` on `AppDashboard` rather than placing `AppPageTitle` manually inside the page content.
|
|
149
180
|
- **Do** pass `pageTitle={false}` to explicitly suppress the block — do not conditionally omit it based on state.
|
|
150
181
|
- **Do** put action buttons in the `actions` slot — do not place them above the title.
|
|
151
182
|
- **Do** size and style the `icon` yourself — pass `className="size-16 rounded-xl"` on the `<img>` or wrapper. The component does not enforce icon dimensions.
|
|
183
|
+
- **Do** memoize `actions`, `breadcrumbs`, and `icon` when passing to `usePageTitle` — these are compared by identity and inline JSX re-runs the effect every render.
|
|
152
184
|
- **Don't** hardcode breadcrumbs when auto-generation is correct — only override when the URL structure doesn't match the desired labels.
|
|
153
185
|
- **Don't** add additional margin-top to page content — `AppPageTitle` already provides `mb-6` spacing.
|
|
154
186
|
- **Don't** use raw color values for `titleColor` overrides — use the `titleColor` prop, not a `className` on the heading.
|
package/rules/table-manager.md
CHANGED
|
@@ -286,6 +286,44 @@ When `headerCell` is provided it fully replaces the header content (text + sort
|
|
|
286
286
|
</TableManagerToolbarActions>
|
|
287
287
|
```
|
|
288
288
|
|
|
289
|
+
## Props — TableManagerToolbar
|
|
290
|
+
|
|
291
|
+
| Prop | Type | Default | Description |
|
|
292
|
+
|------|------|---------|-------------|
|
|
293
|
+
| `layout` | `"wrap" \| "inline"` | `"wrap"` | Layout mode. See below. |
|
|
294
|
+
| `className` | `string` | — | Extra classes on the toolbar wrapper. |
|
|
295
|
+
|
|
296
|
+
### `layout` values
|
|
297
|
+
|
|
298
|
+
| Value | Behaviour |
|
|
299
|
+
|-------|-----------|
|
|
300
|
+
| `"wrap"` | `flex-wrap` — children reflow to multiple rows on narrow viewports. Back-compatible default. |
|
|
301
|
+
| `"inline"` | Mobile: search stacks above actions (`flex-col`). Desktop (md+): search + actions share one row with `justify-between`. Pair with `TableManagerSearch wrapperClassName="w-full md:w-64"` so the search does not stretch full-width on desktop. |
|
|
302
|
+
|
|
303
|
+
### Responsive inline toolbar recipe
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
<TableManagerToolbar layout="inline">
|
|
307
|
+
<TableManagerSearch wrapperClassName="w-full md:w-64" />
|
|
308
|
+
<TableManagerToolbarActions>
|
|
309
|
+
<TableManagerMultiFilter ... />
|
|
310
|
+
<TableManagerPagination />
|
|
311
|
+
<TableManagerColumnToggle />
|
|
312
|
+
</TableManagerToolbarActions>
|
|
313
|
+
</TableManagerToolbar>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Props — TableManagerSearch
|
|
317
|
+
|
|
318
|
+
| Prop | Type | Default | Description |
|
|
319
|
+
|------|------|---------|-------------|
|
|
320
|
+
| `placeholder` | `string` | `"Search..."` | Input placeholder. |
|
|
321
|
+
| `debounceMs` | `number` | `300` | Debounce delay before `onSearchChange` fires. |
|
|
322
|
+
| `wrapperClassName` | `string` | — | Forwarded to `AppInput.wrapperClassName`. Controls the outer wrapper width. Default `w-64`. |
|
|
323
|
+
| `className` | `string` | — | Applied to the inner `<input>` element. |
|
|
324
|
+
|
|
325
|
+
`wrapperClassName` overrides merge with the `w-64` default (later class wins via Tailwind Merge). Pass `wrapperClassName="w-full md:w-64"` for a mobile-first responsive width.
|
|
326
|
+
|
|
289
327
|
## Rules
|
|
290
328
|
|
|
291
329
|
- ✅ Always wrap everything in `<TableManager>` before using any sub-component
|
|
@@ -295,12 +333,17 @@ When `headerCell` is provided it fully replaces the header content (text + sort
|
|
|
295
333
|
- ✅ Implement OR logic in `onFilterChange` when a row can belong to multiple filter values
|
|
296
334
|
- ✅ Set `type: "date"` on date columns and `type: "text"` on primary text columns so the default sort works correctly
|
|
297
335
|
- ✅ Use `TableManagerToolbarActions` with `shrink-0` behavior built in — it will not compress when the search input grows
|
|
336
|
+
- ✅ Use `layout="inline"` on `TableManagerToolbar` when search + actions + pagination must coexist on one row at md+
|
|
337
|
+
- ✅ Pair `layout="inline"` with `wrapperClassName="w-full md:w-64"` on `TableManagerSearch` — otherwise search stretches full-width on desktop
|
|
298
338
|
- ❌ Never import sub-components outside of a `<TableManager>` tree — they read from internal context and will throw
|
|
299
339
|
- ❌ Never filter data internally in the block — always pass already-filtered data via the `data` prop
|
|
300
340
|
- ❌ Never add `date-fns` or a date library just for this block — it uses native `Date` methods
|
|
301
341
|
- ❌ Never use arbitrary CSS selectors (`:last-child`, `[&_th:last-child]`) to align column headers — use `headerAlign` / `headerCell` instead
|
|
342
|
+
- ❌ Do not pass a width class via `TableManagerSearch className` to control the wrapper width — `className` targets the inner `<input>`; use `wrapperClassName` instead
|
|
302
343
|
- ❌ If a prop you need is missing, stop and inform the design team
|
|
303
344
|
|
|
304
345
|
## Responsive
|
|
305
346
|
|
|
306
|
-
`TableManagerToolbar` wraps with `flex-wrap` on small screens. `TableManagerToolbarActions` loses its `ml-auto` below `sm` and flows inline with other toolbar children. Wrapping is automatic — no consumer change needed.
|
|
347
|
+
`TableManagerToolbar layout="wrap"` (default) wraps with `flex-wrap` on small screens. `TableManagerToolbarActions` loses its `ml-auto` below `sm` and flows inline with other toolbar children. Wrapping is automatic — no consumer change needed.
|
|
348
|
+
|
|
349
|
+
`layout="inline"` gives explicit two-tier stacking: search on top on mobile, side-by-side on md+. Use this when `layout="wrap"` produces too many rows on narrow viewports.
|
package/rules/textarea.md
CHANGED
|
@@ -12,10 +12,21 @@ import { AppTextarea } from "@/components/primitives/AppTextarea"
|
|
|
12
12
|
|------|------|---------|-------------|
|
|
13
13
|
| `placeholder` | `string` | — | Placeholder text |
|
|
14
14
|
| `disabled` | `boolean` | `false` | Disables the textarea |
|
|
15
|
-
| `className` | `string` | — | Additional Tailwind classes |
|
|
15
|
+
| `className` | `string` | — | Additional Tailwind classes. Merged after the built-in bg classes so your overrides always win. |
|
|
16
16
|
|
|
17
17
|
> Accepts all standard HTML `<textarea>` attributes in addition to the props above.
|
|
18
18
|
|
|
19
|
+
## Dark mode background
|
|
20
|
+
|
|
21
|
+
`AppTextarea` applies `bg-transparent dark:bg-input/30` internally so the textarea surface is visible in dark mode, matching `AppInput`'s appearance. You do not need to add background classes for dark mode.
|
|
22
|
+
|
|
23
|
+
`className` is merged last — pass a `bg-*` class to override the default for special cases.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
{/* Override background for a specific context */}
|
|
27
|
+
<AppTextarea className="bg-card dark:bg-card" placeholder="Custom bg..." />
|
|
28
|
+
```
|
|
29
|
+
|
|
19
30
|
## Usage
|
|
20
31
|
|
|
21
32
|
### Basic
|
|
@@ -31,8 +42,19 @@ import { AppTextarea } from "@/components/primitives/AppTextarea"
|
|
|
31
42
|
</div>
|
|
32
43
|
```
|
|
33
44
|
|
|
45
|
+
### Controlled height
|
|
46
|
+
```tsx
|
|
47
|
+
{/* Fixed height via rows */}
|
|
48
|
+
<AppTextarea rows={6} placeholder="Detailed description..." />
|
|
49
|
+
|
|
50
|
+
{/* Tailwind height class */}
|
|
51
|
+
<AppTextarea className="h-40" placeholder="Notes..." />
|
|
52
|
+
```
|
|
53
|
+
|
|
34
54
|
## Rules
|
|
35
55
|
- ✅ Always use `AppTextarea` — never use a raw `<textarea>` element
|
|
36
56
|
- ✅ Use `AppLabel` for accessibility
|
|
37
57
|
- ✅ Control height via `rows` attribute or Tailwind `h-*` classes
|
|
58
|
+
- ✅ Dark mode background is handled automatically — no `dark:bg-*` needed unless overriding
|
|
59
|
+
- ❌ Do not add `dark:bg-transparent` to fix a dark-mode visibility issue — `AppTextarea` already applies `dark:bg-input/30` for parity with `AppInput`
|
|
38
60
|
- ❌ If a prop you need is missing, stop and inform the design team
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# AppWizardStepper
|
|
2
|
+
|
|
3
|
+
> Docs: https://ui-kit.vercel.app/docs/blocks/wizard-stepper
|
|
4
|
+
|
|
5
|
+
**This is a block — import from `blocks/AppWizardStepper`, not `primitives/`.**
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import {
|
|
9
|
+
AppWizardStepper,
|
|
10
|
+
type AppWizardStep,
|
|
11
|
+
type AppWizardStepperProps,
|
|
12
|
+
} from "@skunkworks-ai/ui-kit"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
| Prop | Type | Default | Description |
|
|
18
|
+
|------|------|---------|-------------|
|
|
19
|
+
| `steps` | `AppWizardStep[]` | required | Ordered list of steps. Each step needs a stable `id` and a `label`. |
|
|
20
|
+
| `activeIndex` | `number` | required | 0-based index of the currently active step. Steps before this index are rendered as completed. |
|
|
21
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction. |
|
|
22
|
+
| `onStepClick` | `(step: AppWizardStep, index: number) => void` | — | Fires when a completed or active step is clicked. Upcoming steps are never interactive. |
|
|
23
|
+
| `hideLabels` | `boolean \| undefined` | `undefined` | Controls label visibility. See tristate values below. |
|
|
24
|
+
| `className` | `string` | — | Extra classes on the root `<ol>`. |
|
|
25
|
+
|
|
26
|
+
### `AppWizardStep` type
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
type AppWizardStep = {
|
|
30
|
+
id: string // Stable identifier used as React key and in onStepClick
|
|
31
|
+
label: string // Displayed step name
|
|
32
|
+
description?: string // Optional sub-label shown under the main label
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `hideLabels` tristate
|
|
37
|
+
|
|
38
|
+
| Value | Behaviour |
|
|
39
|
+
|-------|-----------|
|
|
40
|
+
| `undefined` (default) | Labels hidden below `md`, visible on `md+` (CSS-only; screen-reader visible at all sizes) |
|
|
41
|
+
| `true` | Always hidden — `sr-only` at all breakpoints |
|
|
42
|
+
| `false` | Always visible — no responsive hiding |
|
|
43
|
+
|
|
44
|
+
## Step states
|
|
45
|
+
|
|
46
|
+
| State | Condition | Visual |
|
|
47
|
+
|-------|-----------|--------|
|
|
48
|
+
| `completed` | `index < activeIndex` | Filled circle with check icon (`--primary` bg) |
|
|
49
|
+
| `active` | `index === activeIndex` | Outlined circle with primary ring (`--primary` border + ring) |
|
|
50
|
+
| `upcoming` | `index > activeIndex` | Muted circle with number (`--muted` bg, `--border` border) |
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Horizontal (default)
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
const STEPS: AppWizardStep[] = [
|
|
58
|
+
{ id: "details", label: "Details" },
|
|
59
|
+
{ id: "review", label: "Review" },
|
|
60
|
+
{ id: "confirm", label: "Confirm" },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
function WizardPage() {
|
|
64
|
+
const [activeIndex, setActiveIndex] = React.useState(0)
|
|
65
|
+
|
|
66
|
+
const handleStepClick = React.useCallback(
|
|
67
|
+
(_step: AppWizardStep, index: number) => setActiveIndex(index),
|
|
68
|
+
[]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<AppWizardStepper
|
|
73
|
+
steps={STEPS}
|
|
74
|
+
activeIndex={activeIndex}
|
|
75
|
+
onStepClick={handleStepClick}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Vertical
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<AppWizardStepper
|
|
85
|
+
steps={STEPS}
|
|
86
|
+
activeIndex={1}
|
|
87
|
+
orientation="vertical"
|
|
88
|
+
onStepClick={handleStepClick}
|
|
89
|
+
/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### With descriptions
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const STEPS: AppWizardStep[] = [
|
|
96
|
+
{ id: "account", label: "Account", description: "Basic info" },
|
|
97
|
+
{ id: "profile", label: "Profile", description: "Personal details" },
|
|
98
|
+
{ id: "done", label: "Done", description: "Review & submit" },
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Always show labels
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<AppWizardStepper steps={STEPS} activeIndex={1} hideLabels={false} />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Icon-only (always hidden labels)
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<AppWizardStepper steps={STEPS} activeIndex={1} hideLabels={true} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Controlled active index (full wizard flow)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
function MultiStepForm() {
|
|
118
|
+
const [activeIndex, setActiveIndex] = React.useState(0)
|
|
119
|
+
|
|
120
|
+
const handleNext = () => setActiveIndex((i) => Math.min(i + 1, STEPS.length - 1))
|
|
121
|
+
const handleBack = () => setActiveIndex((i) => Math.max(i - 1, 0))
|
|
122
|
+
|
|
123
|
+
// Gate forward nav yourself — onStepClick only fires for completed/active steps
|
|
124
|
+
const handleStepClick = React.useCallback(
|
|
125
|
+
(_step: AppWizardStep, index: number) => setActiveIndex(index),
|
|
126
|
+
[]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="space-y-6">
|
|
131
|
+
<AppWizardStepper
|
|
132
|
+
steps={STEPS}
|
|
133
|
+
activeIndex={activeIndex}
|
|
134
|
+
onStepClick={handleStepClick}
|
|
135
|
+
/>
|
|
136
|
+
<StepContent index={activeIndex} />
|
|
137
|
+
<div className="flex gap-2">
|
|
138
|
+
<AppButton variant="outline" onClick={handleBack} disabled={activeIndex === 0}>
|
|
139
|
+
Back
|
|
140
|
+
</AppButton>
|
|
141
|
+
<AppButton onClick={handleNext} disabled={activeIndex === STEPS.length - 1}>
|
|
142
|
+
Next
|
|
143
|
+
</AppButton>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Clickability gate
|
|
151
|
+
|
|
152
|
+
`onStepClick` only fires for steps in `completed` or `active` state. Upcoming steps are rendered as static `<div>` elements — not `<button>` elements — so they cannot be focused or activated. This is intentional: forward navigation must be gated by the parent component.
|
|
153
|
+
|
|
154
|
+
Do not try to navigate forward by clicking an upcoming step indicator — the component will not call `onStepClick` for it.
|
|
155
|
+
|
|
156
|
+
## Accessibility
|
|
157
|
+
|
|
158
|
+
- Root element is `<ol role="list" aria-label="Progress">`
|
|
159
|
+
- Each step is `<li role="listitem">`
|
|
160
|
+
- Active step carries `aria-current="step"`
|
|
161
|
+
- Clickable steps are native `<button type="button">` with `aria-label="Go to step N: Label"`
|
|
162
|
+
- Indicator circles are `aria-hidden="true"` — label text provides the accessible name
|
|
163
|
+
- Labels are `sr-only` on mobile by default, ensuring screen readers always have access to step names regardless of the `hideLabels` setting
|
|
164
|
+
|
|
165
|
+
## Performance
|
|
166
|
+
|
|
167
|
+
Memoize `steps` and `onStepClick` to avoid unnecessary re-renders. `steps` is typically a module-level constant; `onStepClick` should be wrapped in `useCallback`.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// ✅ Correct
|
|
171
|
+
const STEPS = [{ id: "a", label: "A" }, { id: "b", label: "B" }] // module scope
|
|
172
|
+
|
|
173
|
+
const handleStepClick = React.useCallback(
|
|
174
|
+
(_step: AppWizardStep, i: number) => setActive(i),
|
|
175
|
+
[]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// ❌ Wrong — new array + new function every render
|
|
179
|
+
<AppWizardStepper
|
|
180
|
+
steps={[{ id: "a", label: "A" }]}
|
|
181
|
+
onStepClick={(_s, i) => setActive(i)}
|
|
182
|
+
...
|
|
183
|
+
/>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Token usage
|
|
187
|
+
|
|
188
|
+
| Visual element | Token |
|
|
189
|
+
|----------------|-------|
|
|
190
|
+
| Completed circle fill | `--primary` / `--primary-foreground` |
|
|
191
|
+
| Active circle border + ring | `--primary` / `ring-primary/30` |
|
|
192
|
+
| Upcoming circle fill | `--muted` / `--muted-foreground` |
|
|
193
|
+
| Upcoming circle border | `--border` |
|
|
194
|
+
| Connector line (default) | `--border` |
|
|
195
|
+
| Connector line (completed segment) | `--primary` |
|
|
196
|
+
|
|
197
|
+
## Rules
|
|
198
|
+
|
|
199
|
+
- ✅ Define `steps` at module scope or memoize — avoid recreating the array on every render
|
|
200
|
+
- ✅ Wrap `onStepClick` in `useCallback`
|
|
201
|
+
- ✅ Gate forward navigation yourself — `onStepClick` only fires for completed/active steps
|
|
202
|
+
- ✅ Use `hideLabels={false}` when you always want labels visible regardless of breakpoint
|
|
203
|
+
- ✅ Use `orientation="vertical"` for tall, sidebar-style step flows
|
|
204
|
+
- ❌ Do not attempt to make upcoming steps clickable — the block intentionally blocks this; gate forward nav in the parent
|
|
205
|
+
- ❌ Do not manage the completed/upcoming state externally — derive it from `activeIndex`; steps before it are automatically completed
|
|
206
|
+
- ❌ Do not add custom color classes to the indicator circle — all step colors come from design tokens
|
|
207
|
+
- ❌ If a prop you need is missing, stop and inform the design team
|