@devalok/shilp-sutra 0.40.1 → 0.42.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.
Files changed (41) hide show
  1. package/AGENTS.md +23 -1
  2. package/BREAKING.json +66 -0
  3. package/BREAKING.schema.json +184 -0
  4. package/MIGRATION.md +16 -0
  5. package/docs/recipes/install-next-app-router.md +54 -14
  6. package/docs/recipes/upgrading.md +19 -0
  7. package/llms-full.txt +1 -1
  8. package/llms-quick.txt +3 -1
  9. package/llms.txt +1 -0
  10. package/make-kit/Guidelines.md +71 -0
  11. package/make-kit/components/badge.md +162 -0
  12. package/make-kit/components/button.md +125 -0
  13. package/make-kit/components/card.md +147 -0
  14. package/make-kit/components/dialog.md +167 -0
  15. package/make-kit/components/dropdown-menu.md +205 -0
  16. package/make-kit/components/form.md +189 -0
  17. package/make-kit/components/icon.md +152 -0
  18. package/make-kit/components/input.md +154 -0
  19. package/make-kit/components/overview.md +308 -0
  20. package/make-kit/components/popover.md +201 -0
  21. package/make-kit/components/select.md +148 -0
  22. package/make-kit/components/stack.md +165 -0
  23. package/make-kit/components/table.md +215 -0
  24. package/make-kit/components/tabs.md +162 -0
  25. package/make-kit/components/text.md +139 -0
  26. package/make-kit/components/toast.md +193 -0
  27. package/make-kit/foundations/color.md +128 -0
  28. package/make-kit/foundations/dark-mode.md +81 -0
  29. package/make-kit/foundations/icons.md +107 -0
  30. package/make-kit/foundations/motion.md +134 -0
  31. package/make-kit/foundations/radius.md +78 -0
  32. package/make-kit/foundations/spacing.md +110 -0
  33. package/make-kit/foundations/surfaces.md +121 -0
  34. package/make-kit/foundations/typography.md +120 -0
  35. package/make-kit/setup.md +130 -0
  36. package/package.json +9 -2
  37. package/skill/SKILL.md +3 -3
  38. package/skill/references/components-full.md +1 -1
  39. package/skill/references/components.md +1 -0
  40. package/skill/references/setup-next-app-router.md +54 -14
  41. package/skill/references/upgrading.md +19 -0
@@ -0,0 +1,205 @@
1
+ # DropdownMenu
2
+
3
+ Click-triggered menu of actions. Use for overflow menus, row actions, account menus.
4
+
5
+ ```tsx
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuTrigger,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuLabel,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuShortcut,
14
+ DropdownMenuCheckboxItem,
15
+ DropdownMenuRadioGroup,
16
+ DropdownMenuRadioItem,
17
+ DropdownMenuGroup,
18
+ DropdownMenuSub,
19
+ DropdownMenuSubTrigger,
20
+ DropdownMenuSubContent,
21
+ } from '@devalok/shilp-sutra/ui/dropdown-menu'
22
+ ```
23
+
24
+ ## When to use
25
+
26
+ - Row / item actions (kebab menu — Edit, Duplicate, Delete).
27
+ - Account menu (Profile, Settings, Logout).
28
+ - Sort / filter pickers with a fixed list.
29
+ - Toolbar overflow ("More" button).
30
+ - Interactive panel (forms, calendars, complex pickers)? Use `<Popover>`.
31
+ - Single-choice from a fixed list inside a form field? Use `<Select>`.
32
+ - Hover-triggered rich preview? Use `<HoverCard>`.
33
+ - Right-click contextual menu? Use `<ContextMenu>` (separate component).
34
+
35
+ ## Compound shape
36
+
37
+ ```
38
+ DropdownMenu (root — open, onOpenChange, defaultOpen, modal)
39
+ DropdownMenuTrigger ← asChild around your button
40
+ DropdownMenuContent
41
+ DropdownMenuLabel ← non-interactive section heading
42
+ DropdownMenuSeparator
43
+ DropdownMenuItem (+ DropdownMenuShortcut) ← standard action
44
+ DropdownMenuCheckboxItem ← multi-select toggle
45
+ DropdownMenuRadioGroup
46
+ DropdownMenuRadioItem ← single-select group
47
+ DropdownMenuGroup ← visual grouping
48
+ DropdownMenuSub
49
+ DropdownMenuSubTrigger ← visible item that opens submenu
50
+ DropdownMenuSubContent ← the nested submenu panel
51
+ ```
52
+
53
+ ## Root state props (Radix passthrough)
54
+
55
+ | Prop | Type | Notes |
56
+ |---|---|---|
57
+ | `open` | `boolean` | Controlled. |
58
+ | `onOpenChange` | `(open: boolean) => void` | |
59
+ | `defaultOpen` | `boolean` | Uncontrolled. |
60
+ | `modal` | `boolean` | Default `true`. |
61
+
62
+ ## Examples
63
+
64
+ **Standard kebab menu:**
65
+ ```tsx
66
+ <DropdownMenu>
67
+ <DropdownMenuTrigger asChild>
68
+ <IconButton icon={<Icon icon={IconDots} />} variant="ghost" aria-label="Actions" />
69
+ </DropdownMenuTrigger>
70
+ <DropdownMenuContent>
71
+ <DropdownMenuItem onSelect={() => edit(item)}>
72
+ <Icon icon={IconEdit} /> Edit
73
+ </DropdownMenuItem>
74
+ <DropdownMenuItem onSelect={() => duplicate(item)}>
75
+ <Icon icon={IconCopy} /> Duplicate
76
+ </DropdownMenuItem>
77
+ <DropdownMenuSeparator />
78
+ <DropdownMenuItem onSelect={() => remove(item)}>
79
+ <Icon icon={IconTrash} /> Delete
80
+ </DropdownMenuItem>
81
+ </DropdownMenuContent>
82
+ </DropdownMenu>
83
+ ```
84
+
85
+ **With keyboard shortcut hints:**
86
+ ```tsx
87
+ <DropdownMenuContent>
88
+ <DropdownMenuItem onSelect={save}>
89
+ Save
90
+ <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
91
+ </DropdownMenuItem>
92
+ <DropdownMenuItem onSelect={find}>
93
+ Find
94
+ <DropdownMenuShortcut>⌘F</DropdownMenuShortcut>
95
+ </DropdownMenuItem>
96
+ </DropdownMenuContent>
97
+ ```
98
+
99
+ `DropdownMenuShortcut` is decorative — it does NOT bind the shortcut globally. Bind shortcuts separately (e.g. with a `useHotkeys` hook).
100
+
101
+ **Checkbox items (multi-select filters):**
102
+ ```tsx
103
+ <DropdownMenu>
104
+ <DropdownMenuTrigger asChild>
105
+ <Button variant="soft" endIcon={IconChevronDown}>View</Button>
106
+ </DropdownMenuTrigger>
107
+ <DropdownMenuContent>
108
+ <DropdownMenuLabel>Show columns</DropdownMenuLabel>
109
+ <DropdownMenuCheckboxItem
110
+ checked={cols.includes('owner')}
111
+ onCheckedChange={(checked) => toggleCol('owner', checked)}
112
+ onSelect={(e) => e.preventDefault()}
113
+ >
114
+ Owner
115
+ </DropdownMenuCheckboxItem>
116
+ <DropdownMenuCheckboxItem
117
+ checked={cols.includes('status')}
118
+ onCheckedChange={(checked) => toggleCol('status', checked)}
119
+ onSelect={(e) => e.preventDefault()}
120
+ >
121
+ Status
122
+ </DropdownMenuCheckboxItem>
123
+ </DropdownMenuContent>
124
+ </DropdownMenu>
125
+ ```
126
+
127
+ Calling `e.preventDefault()` inside `onSelect` keeps the menu open after toggling — standard for checkbox items.
128
+
129
+ **Radio items (single-select sort):**
130
+ ```tsx
131
+ <DropdownMenu>
132
+ <DropdownMenuTrigger asChild>
133
+ <Button variant="soft">Sort: {sort}</Button>
134
+ </DropdownMenuTrigger>
135
+ <DropdownMenuContent>
136
+ <DropdownMenuLabel>Sort by</DropdownMenuLabel>
137
+ <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
138
+ <DropdownMenuRadioItem value="recent">Most recent</DropdownMenuRadioItem>
139
+ <DropdownMenuRadioItem value="oldest">Oldest</DropdownMenuRadioItem>
140
+ <DropdownMenuRadioItem value="az">A → Z</DropdownMenuRadioItem>
141
+ </DropdownMenuRadioGroup>
142
+ </DropdownMenuContent>
143
+ </DropdownMenu>
144
+ ```
145
+
146
+ **Submenu:**
147
+ ```tsx
148
+ <DropdownMenuContent>
149
+ <DropdownMenuItem onSelect={() => share(item)}>Share</DropdownMenuItem>
150
+ <DropdownMenuSub>
151
+ <DropdownMenuSubTrigger>
152
+ <Icon icon={IconFolder} /> Move to
153
+ </DropdownMenuSubTrigger>
154
+ <DropdownMenuSubContent>
155
+ <DropdownMenuItem onSelect={() => move('inbox')}>Inbox</DropdownMenuItem>
156
+ <DropdownMenuItem onSelect={() => move('archive')}>Archive</DropdownMenuItem>
157
+ <DropdownMenuItem onSelect={() => move('trash')}>Trash</DropdownMenuItem>
158
+ </DropdownMenuSubContent>
159
+ </DropdownMenuSub>
160
+ </DropdownMenuContent>
161
+ ```
162
+
163
+ Both `SubTrigger` AND `SubContent` are required — missing either silently breaks the hover-open behavior.
164
+
165
+ **Account menu pattern:**
166
+ ```tsx
167
+ <DropdownMenu>
168
+ <DropdownMenuTrigger asChild>
169
+ <IconButton icon={<Avatar size="sm" src={user.avatar} alt={user.name} />} variant="ghost" aria-label="Account" />
170
+ </DropdownMenuTrigger>
171
+ <DropdownMenuContent align="end">
172
+ <DropdownMenuLabel>{user.email}</DropdownMenuLabel>
173
+ <DropdownMenuSeparator />
174
+ <DropdownMenuGroup>
175
+ <DropdownMenuItem onSelect={() => router.push('/profile')}>Profile</DropdownMenuItem>
176
+ <DropdownMenuItem onSelect={() => router.push('/settings')}>Settings</DropdownMenuItem>
177
+ </DropdownMenuGroup>
178
+ <DropdownMenuSeparator />
179
+ <DropdownMenuItem onSelect={signOut}>
180
+ <Icon icon={IconLogout} /> Sign out
181
+ </DropdownMenuItem>
182
+ </DropdownMenuContent>
183
+ </DropdownMenu>
184
+ ```
185
+
186
+ ## Composability
187
+
188
+ - **Radix DropdownMenu** underneath — `open` / `onOpenChange` / `defaultOpen` / `modal` standard state.
189
+ - **Keyboard:** Arrow keys navigate, Enter/Space activates, Esc closes, typeahead jumps to first letter. All pre-wired.
190
+ - **Trigger:** `<DropdownMenuTrigger asChild>` around any button. `IconButton` is the common pairing for kebab menus.
191
+ - **Closing from a handler:** `onSelect` auto-closes the menu. Call `e.preventDefault()` inside the handler to keep it open (checkbox items, multi-step interactions).
192
+ - **z-popover (1400):** Stacks above Dialog. Nesting dropdowns inside dialogs works correctly.
193
+
194
+ See `foundations/surfaces.md` for menu surface tokens, `foundations/motion.md` for the spring open animation.
195
+
196
+ ## Rules
197
+
198
+ - Use `<DropdownMenuTrigger asChild>` around a Button or IconButton. Don't use Dropdown's default injected trigger.
199
+ - For kebab menus, always set `aria-label` on the IconButton — "Actions" or the specific row context.
200
+ - Submenus require BOTH `DropdownMenuSubTrigger` AND `DropdownMenuSubContent`. Missing either silently breaks hover-to-open.
201
+ - `DropdownMenuShortcut` is decorative. Bind the actual shortcut separately.
202
+ - For checkbox items, call `e.preventDefault()` inside `onSelect` to keep the menu open after toggling.
203
+ - For interactive content beyond a list (calendars, forms), use `<Popover>` not DropdownMenu.
204
+ - Don't nest a DropdownMenu inside another DropdownMenu — use `<DropdownMenuSub>` for hierarchy.
205
+ - For right-click menus, use `<ContextMenu>` (separate component). DropdownMenu is click-triggered only.
@@ -0,0 +1,189 @@
1
+ # Form
2
+
3
+ Wrap each form field in a `<FormField>`. It cascades state + a11y wiring to compatible controls automatically.
4
+
5
+ ```tsx
6
+ import { FormField, FormHelperText, useFormField } from '@devalok/shilp-sutra/ui/form'
7
+ import { Label } from '@devalok/shilp-sutra/ui/label'
8
+ ```
9
+
10
+ ## When to use
11
+
12
+ - Every input field in a form. One `<FormField>` per logical field.
13
+ - Provides: validation state, helper text, `aria-describedby`, `aria-invalid`, `aria-required` — wired automatically.
14
+ - Not for layout — for stacking fields, use `<Stack gap="ds-05">`.
15
+
16
+ ## Compound shape
17
+
18
+ ```
19
+ FormField (state, required)
20
+ Label ← htmlFor auto-resolves from FormField inputId
21
+ Input | Textarea | NumberInput | InputOTP | Select | Checkbox | ...
22
+ FormHelperText ← reads state + helperTextId from context
23
+ ```
24
+
25
+ `FormField` generates a shared `inputId`. Both `<Label>` (via `htmlFor`) and `<Input>` (via `id`) read it from context — drop the manual id-matching dance. Explicit `htmlFor` / `id` on a child still wins.
26
+
27
+ ## FormField props
28
+
29
+ | Prop | Type | Notes |
30
+ |---|---|---|
31
+ | `state` | `'helper'\|'error'\|'warning'\|'success'` | Default `helper`. Cascades to compatible controls + FormHelperText. |
32
+ | `helperTextId` | `string` | Auto-generated if omitted. Used by `aria-describedby` wiring. |
33
+ | `inputId` | `string` | Auto-generated if omitted. Shared between `<Label htmlFor>` and `<Input id>`. |
34
+ | `required` | `boolean` | Sets `aria-required` on consuming controls. Does NOT auto-render the asterisk — that comes from `<Label required>`. |
35
+
36
+ ## FormHelperText props
37
+
38
+ | Prop | Type | Notes |
39
+ |---|---|---|
40
+ | `state` | `'helper'\|'error'\|'warning'\|'success'` | Inherits from FormField context. Override per-helper if needed. |
41
+
42
+ When `state="error"`, FormHelperText renders `role="alert"` so screen readers interrupt.
43
+
44
+ ## Label props
45
+
46
+ | Prop | Type | Notes |
47
+ |---|---|---|
48
+ | `htmlFor` | `string` | Optional inside `<FormField>` — falls back to FormField's `inputId`. Required when used outside FormField. |
49
+ | `required` | `boolean` | Renders red asterisk. Does NOT set `aria-required` (that comes from FormField). |
50
+
51
+ ## useFormField hook
52
+
53
+ ```ts
54
+ const field = useFormField()
55
+ // → { state, helperTextId, required } | undefined
56
+ ```
57
+
58
+ Use inside a custom control to consume FormField context.
59
+
60
+ ## What auto-consumes FormField context
61
+
62
+ | Control | Auto-receives |
63
+ |---|---|
64
+ | `<Input>` | `id` (from `inputId`), `state`, `aria-describedby`, `aria-invalid`, `aria-required` |
65
+ | `<Textarea>` | same |
66
+ | `<NumberInput>` | same |
67
+ | `<InputOTP>` | `state`, `aria-describedby`, `aria-required` |
68
+ | `<Label>` | `htmlFor` (from `inputId`) |
69
+
70
+ | Control | Manual wiring needed |
71
+ |---|---|
72
+ | `<Select>` / `<SelectTrigger>` | Set `color="error"` on `SelectTrigger` from your validation state. |
73
+ | `<Checkbox>`, `<Radio>`, `<Switch>` | Pair `<Label htmlFor>` manually. State is visual on the control. |
74
+
75
+ ## Examples
76
+
77
+ **Standard text field with error:**
78
+ ```tsx
79
+ <FormField state={errors.email ? 'error' : 'helper'}>
80
+ <Label htmlFor="email" required>Email</Label>
81
+ <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
82
+ <FormHelperText>
83
+ {errors.email ?? 'We will never share your email.'}
84
+ </FormHelperText>
85
+ </FormField>
86
+ ```
87
+
88
+ The Input receives `aria-describedby` (linked to FormHelperText), `aria-invalid="true"` when state is error, and `aria-required="true"`.
89
+
90
+ **Warning state:**
91
+ ```tsx
92
+ <FormField state="warning">
93
+ <Label htmlFor="password" required>Password</Label>
94
+ <Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
95
+ <FormHelperText>Password strength: weak. Add a number or symbol.</FormHelperText>
96
+ </FormField>
97
+ ```
98
+
99
+ **Textarea:**
100
+ ```tsx
101
+ <FormField state={errors.bio ? 'error' : 'helper'}>
102
+ <Label htmlFor="bio">Bio</Label>
103
+ <Textarea id="bio" rows={4} value={bio} onChange={(e) => setBio(e.target.value)} />
104
+ <FormHelperText>{errors.bio ?? `${bio.length} / 200`}</FormHelperText>
105
+ </FormField>
106
+ ```
107
+
108
+ **Required Checkbox (manual label pairing):**
109
+ ```tsx
110
+ <FormField required state={errors.terms ? 'error' : 'helper'}>
111
+ <Stack direction="horizontal" gap="ds-03" align="center">
112
+ <Checkbox id="terms" checked={agreed} onCheckedChange={setAgreed} />
113
+ <Label htmlFor="terms">I agree to the terms.</Label>
114
+ </Stack>
115
+ {errors.terms && <FormHelperText>{errors.terms}</FormHelperText>}
116
+ </FormField>
117
+ ```
118
+
119
+ **Full form layout:**
120
+ ```tsx
121
+ <form onSubmit={handleSubmit}>
122
+ <Stack gap="ds-05">
123
+ <FormField>
124
+ <Label htmlFor="name" required>Name</Label>
125
+ <Input id="name" />
126
+ </FormField>
127
+
128
+ <FormField>
129
+ <Label htmlFor="email" required>Email</Label>
130
+ <Input id="email" type="email" />
131
+ <FormHelperText>For account recovery only.</FormHelperText>
132
+ </FormField>
133
+
134
+ <FormField state={errors.role ? 'error' : 'helper'}>
135
+ <Label htmlFor="role">Role</Label>
136
+ <Select>
137
+ <SelectTrigger id="role" color={errors.role ? 'error' : 'default'}>
138
+ <SelectValue placeholder="Choose a role" />
139
+ </SelectTrigger>
140
+ <SelectContent>
141
+ <SelectItem value="admin">Admin</SelectItem>
142
+ <SelectItem value="member">Member</SelectItem>
143
+ </SelectContent>
144
+ </Select>
145
+ {errors.role && <FormHelperText>{errors.role}</FormHelperText>}
146
+ </FormField>
147
+
148
+ <Stack direction="horizontal" gap="ds-03" justify="end">
149
+ <Button variant="soft" type="button">Cancel</Button>
150
+ <Button type="submit">Save</Button>
151
+ </Stack>
152
+ </Stack>
153
+ </form>
154
+ ```
155
+
156
+ **Custom control consuming FormField context:**
157
+ ```tsx
158
+ function ColorPickerField(props) {
159
+ const field = useFormField()
160
+ return (
161
+ <ColorPicker
162
+ {...props}
163
+ aria-describedby={field?.helperTextId}
164
+ aria-invalid={field?.state === 'error' || undefined}
165
+ aria-required={field?.required || undefined}
166
+ />
167
+ )
168
+ }
169
+ ```
170
+
171
+ ## Composability
172
+
173
+ - **Explicit props override context** — `<Input state="error">` inside a `<FormField state="helper">` makes only that input look errored. Same for `id` / `htmlFor`.
174
+ - **Don't nest FormFields** — only the outermost context wins. Some a11y wiring silently breaks.
175
+ - **Label-to-control pairing auto-wires inside FormField** via the shared `inputId`. Outside FormField, set `htmlFor` + `id` explicitly.
176
+ - **For non-auto-wired controls** (Checkbox, Radio, Switch) where the visible label sits beside the control, you can still drop the `htmlFor` if you put the control inside FormField with no other Inputs — `inputId` resolves on the Checkbox via its `id` prop the same way.
177
+
178
+ See `foundations/color.md` for state colors, `foundations/spacing.md` for inter-field spacing (`ds-05` default).
179
+
180
+ ## Rules
181
+
182
+ - One FormField per logical field. Never nest FormFields.
183
+ - For error display, drive `FormField state` from your validation state — every consuming control updates together.
184
+ - For Select / Checkbox / Radio / Switch, set the control's error visual manually (`color="error"` on SelectTrigger) — they don't auto-consume FormField state, only ids and a11y attrs.
185
+ - Don't use the removed `getFormFieldA11y()` helper — use the `useFormField()` hook.
186
+ - Inside FormField, `<FormHelperText>` reads `state` + id from context — don't pass them again unless intentionally overriding.
187
+ - Use `<Stack gap="ds-05">` to space fields vertically. Don't reach for `ds-04` or `ds-06` — see `foundations/spacing.md`.
188
+ - `<Label required>` only renders the asterisk. The `aria-required` flag comes from `<FormField required>`.
189
+ - You can omit `id` on `<Input>` and `htmlFor` on `<Label>` inside a `<FormField>` — both resolve from FormField's `inputId`. Set them explicitly only when overriding the auto-generated id.
@@ -0,0 +1,152 @@
1
+ # Icon
2
+
3
+ Wrapper around Tabler icon components. Auto-sizes via `IconProvider` context.
4
+
5
+ ```tsx
6
+ import { Icon } from '@devalok/shilp-sutra/ui'
7
+ import { IconPlus, IconCheck, IconX } from '@tabler/icons-react'
8
+ ```
9
+
10
+ For the broader icon system (which icons exist, how to register custom ones, the IconProvider tree), see `foundations/icons.md`. This guide covers the `<Icon>` component itself.
11
+
12
+ ## When to use
13
+
14
+ - Any inline SVG icon — inside Button slots, IconButton, Badge slots, table cells, list rows.
15
+ - Need an interactive icon button? Use `<IconButton>`, not Icon wrapped in `<button>`.
16
+ - Need a static cluster of related icons? Use `<IconGroup>`.
17
+ - Need a status indicator? Use `<StatusDot>` or `<Badge dot>` — they're optimized for that.
18
+
19
+ Tabler-only. Don't mix icon libraries — see `foundations/icons.md` for the rationale.
20
+
21
+ ## Props
22
+
23
+ | Prop | Type | Notes |
24
+ |---|---|---|
25
+ | `icon` | `ForwardRefExoticComponent` (REQUIRED) | Tabler icon component (or any ForwardRef SVG icon matching the Tabler shape). |
26
+ | `size` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'2xl'` | Reads from `IconContext` if not set. |
27
+ | `stroke` | `'light'\|'regular'\|'bold'` | Reads from `IconContext` if not set. |
28
+ | `label` | `string` | Accessible label — sets `role="img"`, `aria-label`, and `<title>`. Without it, icon is `aria-hidden="true"`. |
29
+ | `animate` | `'spin'\|'pulse'\|'bounce'\|'draw'\|'none'` \| `{ rotate?, scale? }` | Static when undefined. |
30
+ | `state` | `'idle'\|'loading'\|'success'\|'error'` | State machine — overrides `animate` when both set. |
31
+ | `className` | `string` | |
32
+
33
+ ## Size scale
34
+
35
+ | Size | Pixel | Default use |
36
+ |---|---|---|
37
+ | `xs` | 14 | Inside `xs` buttons / badges, dense table cells. |
38
+ | `sm` | 16 | Inside `sm` buttons / badges, standard inline. |
39
+ | `md` (default) | 18 | Inside `md` buttons / inputs. |
40
+ | `lg` | 20 | Inside `lg` buttons / large inputs. |
41
+ | `xl` | 24 | Standalone icons in card headers. |
42
+ | `2xl` | 32 | Hero / illustration accents. |
43
+
44
+ Stroke weight varies by size — smaller icons use lighter strokes for clarity. Override via `stroke` only when the visual weight feels off.
45
+
46
+ ## Accessibility
47
+
48
+ | Use | Result |
49
+ |---|---|
50
+ | No `label` (inside a labeled Button / IconButton) | `aria-hidden="true"` — screen readers skip. |
51
+ | With `label` (standalone) | `role="img"` + `aria-label` + SVG `<title>`. |
52
+
53
+ Inside `<IconButton aria-label="Edit">` the icon is decorative — don't set `label` on the Icon.
54
+
55
+ For standalone icons (status indicators, decorative bullets that convey meaning), pass `label`.
56
+
57
+ ## Examples
58
+
59
+ **Inside a Button:**
60
+ ```tsx
61
+ <Button startIcon={IconPlus}>New project</Button>
62
+ ```
63
+
64
+ `startIcon` accepts the bare Tabler component — Button wraps it in `<Icon>` and provides size via IconProvider. You don't need to wrap manually.
65
+
66
+ **Inside an IconButton (requires `<Icon>` wrapper):**
67
+ ```tsx
68
+ <IconButton
69
+ icon={<Icon icon={IconEdit} />}
70
+ variant="ghost"
71
+ aria-label="Edit item"
72
+ />
73
+ ```
74
+
75
+ IconButton's `icon` prop expects a React element, so wrap in `<Icon icon={...} />`. Size flows from the button via IconProvider.
76
+
77
+ **Standalone with label (a11y):**
78
+ ```tsx
79
+ <Icon icon={IconAlertCircle} label="Warning" size="lg" />
80
+ ```
81
+
82
+ Renders `role="img"` with `aria-label="Warning"` — screen readers announce it.
83
+
84
+ **Loading spinner via state:**
85
+ ```tsx
86
+ <Icon icon={IconRefresh} state="loading" />
87
+ ```
88
+
89
+ `state="loading"` renders a bare Spinner regardless of the `icon` prop. Use for inline loading affordances.
90
+
91
+ **Success / error feedback:**
92
+ ```tsx
93
+ <Icon icon={IconCheck} state="success" />
94
+ <Icon icon={IconX} state="error" />
95
+ ```
96
+
97
+ `state="success"` / `state="error"` render animated checkmark / cross via Framer Motion. Respects `prefers-reduced-motion`.
98
+
99
+ **Path-draw animation (check / X / circle-check only):**
100
+ ```tsx
101
+ <Icon icon={IconCheck} animate="draw" />
102
+ ```
103
+
104
+ Draws the stroke progressively (0.35s easeOut). Works with `IconCheck`, `IconX`, and `IconCircleCheck` only — other icons fall back to static render.
105
+
106
+ **Spin animation:**
107
+ ```tsx
108
+ <Icon icon={IconLoader} animate="spin" />
109
+ ```
110
+
111
+ For an actual loading state, prefer `state="loading"` — it renders the dedicated Spinner.
112
+
113
+ **Inside a row, auto-sized from input:**
114
+ ```tsx
115
+ <Input
116
+ size="lg"
117
+ startSection={<Icon icon={IconSearch} />} {/* size: 'md' from IconProvider */}
118
+ placeholder="Search"
119
+ />
120
+ ```
121
+
122
+ Input's IconProvider sets size by input size: `xs` / `sm` input → `sm` icon, `md` / `lg` input → `md` icon. Don't pass `size` on the nested Icon.
123
+
124
+ **Status badge with custom-colored icon:**
125
+ ```tsx
126
+ <Stack direction="horizontal" gap="ds-02" align="center">
127
+ <Icon icon={IconCircleFilled} size="xs" className="text-success-9" />
128
+ <Text variant="body-sm">Online</Text>
129
+ </Stack>
130
+ ```
131
+
132
+ For most status displays prefer `<StatusDot>` — purpose-built. Use this pattern only when you need a specific icon shape.
133
+
134
+ ## Composability
135
+
136
+ - **IconProvider cascade:** Button, IconButton, Badge, Input (start/endSection), NumberInput, IconGroup all wrap children in an `IconProvider`. Nested `<Icon>` reads size + stroke automatically. Don't pass `size` on those nested Icons.
137
+ - **Explicit props override context.** When you genuinely need a different size, pass `size` on the Icon — it wins.
138
+ - **State overrides animate.** When both are set, `state` wins. `state="loading"` renders a Spinner; `state="success"` / `state="error"` render path-drawn animations.
139
+ - **Reduced motion respected.** All animations fall back to static render under `prefers-reduced-motion: reduce`.
140
+
141
+ See `foundations/icons.md` for the icon-system overview (Tabler-only rationale, IconProvider tree, custom icons), `foundations/motion.md` for animation tokens.
142
+
143
+ ## Rules
144
+
145
+ - Use Tabler icons only. See `foundations/icons.md` for the rationale and registry.
146
+ - Inside Button / IconButton / Badge slots, don't pass `size` on the nested Icon — IconProvider sets it.
147
+ - For standalone icons that convey meaning (not in a labeled button), pass `label` — without it, screen readers skip the icon.
148
+ - Inside `<IconButton aria-label="...">`, the Icon is decorative — don't set `label` on the Icon (would duplicate the announcement).
149
+ - For loading affordances, use `state="loading"` — it renders the dedicated Spinner.
150
+ - `animate="draw"` only works for `IconCheck`, `IconX`, `IconCircleCheck`. Other icons silently fall back to static.
151
+ - Don't wrap Icon in `<button>` — use `<IconButton>`.
152
+ - Stroke weight scales with size — only override `stroke` when the default truly feels off, not as a stylistic preference.