@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 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.18",
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.3.0",
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.0",
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.1.0",
81
+ "date-fns": "^4.3.0",
82
82
  "embla-carousel": "^8.6.0",
83
83
  "embla-carousel-react": "^8.6.0",
84
- "lucide-react": "^0.577.0",
85
- "motion": "^12.38.0",
84
+ "lucide-react": "^1.16.0",
85
+ "motion": "^12.40.0",
86
86
  "radix-ui": "^1.4.3",
87
- "react-day-picker": "^9.4.0",
88
- "react-hook-form": "^7.54.0",
87
+ "react-day-picker": "^10.0.1",
88
+ "react-hook-form": "^7.76.1",
89
89
  "recharts": "^3.8.1",
90
- "shadcn": "^4.1.0",
91
- "sonner": "^1.7.0",
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": "^20",
102
+ "@types/node": "^25",
103
103
  "@types/react": "^19",
104
104
  "@types/react-dom": "^19",
105
- "@vitejs/plugin-react-swc": "^3",
106
- "eslint": "^9",
107
- "react": "19.2.4",
108
- "react-dom": "19.2.4",
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": "^5",
116
+ "typescript": "^6",
117
+ "typescript-eslint": "^8.59.4",
113
118
  "vite": "^6",
114
- "vite-plugin-checker": "^0.9"
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
@@ -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 requirement
238
+ ## AppThemeProvider
239
+
240
+ `AppDashboard` includes `AppThemeProvider` internally — the theme toggle works out of the box with no extra setup required.
187
241
 
188
- The default header includes a theme toggle that requires `AppThemeProvider` in the tree:
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 fallback `"KA"` | Avatar inside default actions. `null` hides it. Ignored when `actions` is provided. |
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 set `width` directly on `AppInput` — control width from the parent container
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
@@ -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.
@@ -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