@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.
- package/AGENTS.md +23 -1
- package/BREAKING.json +66 -0
- package/BREAKING.schema.json +184 -0
- package/MIGRATION.md +16 -0
- package/docs/recipes/install-next-app-router.md +54 -14
- package/docs/recipes/upgrading.md +19 -0
- package/llms-full.txt +1 -1
- package/llms-quick.txt +3 -1
- package/llms.txt +1 -0
- package/make-kit/Guidelines.md +71 -0
- package/make-kit/components/badge.md +162 -0
- package/make-kit/components/button.md +125 -0
- package/make-kit/components/card.md +147 -0
- package/make-kit/components/dialog.md +167 -0
- package/make-kit/components/dropdown-menu.md +205 -0
- package/make-kit/components/form.md +189 -0
- package/make-kit/components/icon.md +152 -0
- package/make-kit/components/input.md +154 -0
- package/make-kit/components/overview.md +308 -0
- package/make-kit/components/popover.md +201 -0
- package/make-kit/components/select.md +148 -0
- package/make-kit/components/stack.md +165 -0
- package/make-kit/components/table.md +215 -0
- package/make-kit/components/tabs.md +162 -0
- package/make-kit/components/text.md +139 -0
- package/make-kit/components/toast.md +193 -0
- package/make-kit/foundations/color.md +128 -0
- package/make-kit/foundations/dark-mode.md +81 -0
- package/make-kit/foundations/icons.md +107 -0
- package/make-kit/foundations/motion.md +134 -0
- package/make-kit/foundations/radius.md +78 -0
- package/make-kit/foundations/spacing.md +110 -0
- package/make-kit/foundations/surfaces.md +121 -0
- package/make-kit/foundations/typography.md +120 -0
- package/make-kit/setup.md +130 -0
- package/package.json +9 -2
- package/skill/SKILL.md +3 -3
- package/skill/references/components-full.md +1 -1
- package/skill/references/components.md +1 -0
- package/skill/references/setup-next-app-router.md +54 -14
- 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.
|