@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,154 @@
1
+ # Input
2
+
3
+ Single-line text entry. Use instead of `<input type="text" | "email" | "url" | "tel" | "search" | "password">`.
4
+
5
+ ```tsx
6
+ import { Input } from '@devalok/shilp-sutra/ui/input'
7
+ ```
8
+
9
+ ## When to use
10
+
11
+ - Any single-line text capture: email, URL, search, name, password, phone.
12
+ - Multi-line input? Use `<Textarea>`.
13
+ - Number-only with steppers? Use `<NumberInput>`.
14
+ - One-time-code / PIN entry? Use `<InputOTP>`.
15
+ - Searchable list with selection? Use `<Combobox>` or `<Autocomplete>`.
16
+
17
+ Wrap in a `<FormField>` for automatic a11y wiring (state, `aria-describedby`, `aria-invalid`, `aria-required`).
18
+
19
+ ## Sizes
20
+
21
+ | Size | Pixel height | When |
22
+ |---|---|---|
23
+ | `xs` | 28 | Toolbar / inline filters. |
24
+ | `sm` | 32 | Dense forms. |
25
+ | `md` (default) | 40 | Standard. |
26
+ | `lg` | 48 | Marketing / spacious layouts. |
27
+
28
+ All sizes use 14 px text — sizes scale height + padding, not font.
29
+
30
+ ## States
31
+
32
+ | State | Visual | Use |
33
+ |---|---|---|
34
+ | `default` (default) | Subtle border. | Resting. |
35
+ | `error` | Red border + `aria-invalid="true"`. | Validation failure. |
36
+ | `warning` | Amber border. | Soft caution (caps lock on, weak password). |
37
+ | `success` | Green border. | Inline confirmation. |
38
+
39
+ ## Props
40
+
41
+ | Prop | Type | Notes |
42
+ |---|---|---|
43
+ | `size` | `'xs'\|'sm'\|'md'\|'lg'` | Default `md`. Excludes the HTML native `size` attribute. |
44
+ | `state` | `'default'\|'error'\|'warning'\|'success'` | Inherits from `<FormField>` context if present. |
45
+ | `startSection` | `ReactNode` | Leading slot — icon (React element) or label string. |
46
+ | `endSection` | `ReactNode` | Trailing slot — icon or label. |
47
+ | `startSectionType` | `'icon'\|'label'` | Auto-inferred from content type. Override only when needed. |
48
+ | `endSectionType` | `'icon'\|'label'` | Auto-inferred. |
49
+ | `startSectionClickable` | `boolean` | Enables pointer events on the section (e.g. clear button in slot). |
50
+ | `endSectionClickable` | `boolean` | Same for trailing slot. |
51
+ | `wrapperClassName` | `string` | Classes on the wrapper div (border, bg, ring live here). |
52
+ | `className` | `string` | Classes on the raw `<input>` element (transparent — text only). |
53
+
54
+ Plus all standard HTML input attributes except the native `size` (renamed to mean visual sizing).
55
+
56
+ ## Section type inference
57
+
58
+ | Content | Renders as |
59
+ |---|---|
60
+ | React element (`<Icon icon={IconMail} />`) | `'icon'` — fixed-width centered cell. |
61
+ | String (`"https://"`, `".00"`) | `'label'` — tinted bg + border separator. |
62
+
63
+ Override via `startSectionType` / `endSectionType` when the inference is wrong.
64
+
65
+ ## Examples
66
+
67
+ **Standard, with leading icon:**
68
+ ```tsx
69
+ <Input
70
+ type="email"
71
+ placeholder="you@example.com"
72
+ startSection={<Icon icon={IconMail} />}
73
+ />
74
+ ```
75
+
76
+ **URL prefix (label section):**
77
+ ```tsx
78
+ <Input
79
+ startSection="https://"
80
+ placeholder="example.com"
81
+ />
82
+ ```
83
+
84
+ **Currency with prefix icon + suffix label:**
85
+ ```tsx
86
+ <Input
87
+ startSection={<Icon icon={IconCurrencyDollar} />}
88
+ endSection=".00"
89
+ placeholder="0"
90
+ inputMode="decimal"
91
+ />
92
+ ```
93
+
94
+ **Clearable search (clickable trailing slot):**
95
+ ```tsx
96
+ <Input
97
+ placeholder="Search projects"
98
+ startSection={<Icon icon={IconSearch} />}
99
+ endSection={
100
+ <button type="button" onClick={() => setValue('')} aria-label="Clear">
101
+ <Icon icon={IconX} />
102
+ </button>
103
+ }
104
+ endSectionClickable
105
+ value={value}
106
+ onChange={(e) => setValue(e.target.value)}
107
+ />
108
+ ```
109
+
110
+ **Inside a FormField (auto-wired a11y):**
111
+ ```tsx
112
+ <FormField state="error">
113
+ <Label htmlFor="email" required>Email</Label>
114
+ <Input id="email" type="email" />
115
+ <FormHelperText>Enter a valid work email.</FormHelperText>
116
+ </FormField>
117
+ ```
118
+
119
+ The Input picks up `state="error"`, `aria-describedby` (linked to FormHelperText), `aria-invalid`, and `aria-required` from FormField context automatically.
120
+
121
+ **Password with reveal toggle:**
122
+ ```tsx
123
+ const [visible, setVisible] = useState(false)
124
+
125
+ <Input
126
+ type={visible ? 'text' : 'password'}
127
+ placeholder="Password"
128
+ endSection={
129
+ <button type="button" onClick={() => setVisible((v) => !v)} aria-label={visible ? 'Hide password' : 'Show password'}>
130
+ <Icon icon={visible ? IconEyeOff : IconEye} />
131
+ </button>
132
+ }
133
+ endSectionClickable
134
+ />
135
+ ```
136
+
137
+ ## Composability
138
+
139
+ - **FormField:** Inside a `<FormField>`, Input auto-reads `state`, `aria-describedby`, `aria-invalid`, `aria-required` from context. Explicit props override.
140
+ - **IconProvider:** Icons in `startSection` / `endSection` are auto-sized via the input's `size`. Don't pass `size` on the nested `<Icon>`.
141
+ - **Container-first architecture:** Border, background, and focus ring live on the wrapper div, not the input element. Style overrides go through `wrapperClassName`, not `className`. The raw `<input>` is transparent.
142
+
143
+ See `foundations/color.md` for state-color tokens, `foundations/icons.md` for the IconProvider cascade.
144
+
145
+ ## Rules
146
+
147
+ - Pair every Input with a `<Label htmlFor="x" />` and matching `<Input id="x" />`. FormField does NOT auto-wire labels.
148
+ - Use `wrapperClassName` for border / bg / ring overrides. `className` only changes the raw input text styling.
149
+ - Don't pass `size` on `<Icon>` inside a section — IconProvider sets it.
150
+ - Don't use the HTML native `size` attribute — it's excluded. Use CSS width or the size prop.
151
+ - For interactive sections (clear button, password toggle), set `startSectionClickable` / `endSectionClickable`. Without it, the slot is `pointer-events-none`.
152
+ - For status / validation, set `state` — it sets `aria-invalid` automatically when `state="error"`.
153
+ - Don't use the removed `startIcon` / `endIcon` props — they were removed in 0.38. Use `startSection` / `endSection`.
154
+ - Prefer FormField wrapping for any input with a label + helper — manual a11y wiring is error-prone.
@@ -0,0 +1,308 @@
1
+ # Components — catalog & decision trees
2
+
3
+ Catalog of every major component. Use this file to pick the right component before reaching for raw HTML. Per-component deep guides live alongside this file.
4
+
5
+ ## Decision tree — actions
6
+
7
+ ```
8
+ User wants to commit / cancel / submit?
9
+ → primary CTA (1 per region) → <Button variant="solid" color="accent">
10
+ → secondary action → <Button variant="soft"> (NOT outline — see preference)
11
+ → tertiary / dismissive → <Button variant="ghost">
12
+ → destructive action → <Button variant="solid" color="error">
13
+ → link-styled action → <Button variant="link">
14
+
15
+ Action has a side-menu / overflow?
16
+ → <SplitButton> — primary + adjacent dropdown trigger.
17
+
18
+ Action is icon-only?
19
+ → <IconButton> — same as Button, square aspect, aria-label required.
20
+
21
+ Group of related actions?
22
+ → <ButtonGroup> — attached or not. Propagates disabled / size.
23
+ ```
24
+
25
+ ## Decision tree — input
26
+
27
+ ```
28
+ Single-line text?
29
+ → <Input> — type="text"/"email"/"password"/"number"/"tel"/"url"/"search"
30
+ → For search specifically: <SearchInput> (has clear button + icon)
31
+ → For OTP code: <InputOTP>
32
+
33
+ Multi-line text?
34
+ → <Textarea>
35
+
36
+ Pick one from a list?
37
+ → static list ≤7 items → <Select>
38
+ → static list >7 items → <Combobox> (searchable)
39
+ → async / large list → <Autocomplete>
40
+ → enum of 2–4 options → <SegmentedControl> (always-visible) or <Tabs> (if scoping a section)
41
+
42
+ Pick many?
43
+ → list with checkboxes → <Checkbox> per item
44
+ → compact pills → <ToggleGroup type="multiple">
45
+ → searchable → <Combobox multiple>
46
+
47
+ Boolean?
48
+ → <Switch> (settings-style on/off) — instant action
49
+ → <Checkbox> (form field) — confirmed-on-submit
50
+ → <Toggle> (button-like toggled state)
51
+
52
+ Range / scalar?
53
+ → <Slider> (single or multi-thumb range)
54
+ → <NumberInput> (precise number with steppers)
55
+
56
+ Date / time?
57
+ → <DatePicker> (single date)
58
+ → <DateRangePicker> (range)
59
+ → <DateTimePicker> (date + time)
60
+
61
+ Color?
62
+ → <ColorInput> (popover swatch + manual hex)
63
+ → <ColorSwatch> (display-only)
64
+
65
+ File?
66
+ → <FileUpload> (drag-drop + click)
67
+ ```
68
+
69
+ ## Decision tree — feedback
70
+
71
+ ```
72
+ Permanent inline message?
73
+ → <Alert> — page-level notice (info / success / warning / error)
74
+ → <Banner> — full-bleed page banner (system status, announcements)
75
+ → <InfoBlock> — within a form / card (helper context)
76
+
77
+ Status of an item?
78
+ → <Badge> — pill label (status / tag)
79
+ → <StatusBadge> — colored dot + label (discriminated union: status="online"/"away"/...)
80
+ → <StatusDot> — just the colored dot
81
+
82
+ Temporary notification?
83
+ → toast.success / error / warning / info (imperative) — must have <Toaster /> mounted
84
+
85
+ Loading / pending?
86
+ → <Spinner> — generic
87
+ → <ProgressRing> — determinate ring
88
+ → <Progress> — linear bar
89
+ → <Skeleton> — content placeholder shimmer
90
+ → <PageSkeletons.*> — pre-built page-level skeletons
91
+ → <GlobalLoading> — app-wide loading overlay
92
+
93
+ Empty state?
94
+ → <EmptyState> — icon + heading + body + action
95
+ ```
96
+
97
+ ## Decision tree — overlays
98
+
99
+ ```
100
+ Confirm a destructive action? → <AlertDialog> or <ConfirmDialog>
101
+ Show a form / detailed flow? → <Dialog> (auto fullScreen on mobile)
102
+ Show a side panel? → <Sheet> (auto bottom-drawer on mobile)
103
+ Show a small floating popup on click? → <Popover>
104
+ Show a small floating popup on hover? → <HoverCard>
105
+ Show a label on hover? → <Tooltip>
106
+ Show a menu from a button? → <DropdownMenu>
107
+ Show a menu from right-click? → <ContextMenu>
108
+ Show a menu in a toolbar? → <Menubar>
109
+ Show a command palette (cmd+K)? → <AppCommandPalette> (shell) or <CommandPalette> (composed)
110
+ ```
111
+
112
+ ## Decision tree — navigation
113
+
114
+ ```
115
+ Top of every page?
116
+ → <TopBar> (shell)
117
+
118
+ Side nav (product)?
119
+ → <AppSidebar> (shell) — collapsible, with sections, footer
120
+
121
+ Tabs within a page?
122
+ → <Tabs> — horizontal default
123
+ → <Tabs orientation="vertical"> — side-nav within a card
124
+
125
+ Pages within a flow?
126
+ → <Stepper> — multi-step wizard, optional clickable
127
+
128
+ Breadcrumb trail?
129
+ → <Breadcrumb>
130
+
131
+ Pagination?
132
+ → <Pagination>
133
+
134
+ Mobile bottom nav?
135
+ → <BottomNavbar>
136
+ ```
137
+
138
+ ## Decision tree — layout
139
+
140
+ ```
141
+ Page wrapper?
142
+ → <Container size="..."> — max-width constraints (sm / md / lg / xl / 2xl / full)
143
+
144
+ Vertical / horizontal stack?
145
+ → <Stack direction="col|row" gap="ds-*">
146
+
147
+ A card / panel?
148
+ → <Card> — surface-raised + shadow-raised
149
+ → <Card variant="..."> — tinted by color prop
150
+
151
+ A grid?
152
+ → use native CSS grid utilities (grid grid-cols-12 gap-ds-05) — no kit primitive for this
153
+
154
+ Aspect-ratio box?
155
+ → <AspectRatio ratio={16/9}>
156
+
157
+ Divider?
158
+ → <Separator> (horizontal default; orientation="vertical")
159
+
160
+ Collapsible section?
161
+ → <Collapsible> (single)
162
+ → <Accordion> (group, single-open or multi-open)
163
+ ```
164
+
165
+ ## Decision tree — data display
166
+
167
+ ```
168
+ Tabular data?
169
+ → <Table> — simple, hand-roll rows/cells
170
+ → <DataTable> — full-featured (sort, paginate, select, filter, export, density, mobile-card)
171
+
172
+ Key-value display?
173
+ → <StatCard> — single big stat
174
+ → 4 StatCards in a row → use <Stack direction="row" gap="ds-05">
175
+
176
+ Activity / event log?
177
+ → <ActivityFeed> — vertical timeline with dots
178
+
179
+ User avatars?
180
+ → <Avatar> — single
181
+ → <AvatarGroup> — stack with overflow
182
+
183
+ A list of selectable members?
184
+ → <MemberPicker>
185
+
186
+ A chart?
187
+ → BarChart / LineChart / AreaChart / PieChart / RadarChart / GaugeChart / Sparkline (from /ui/charts)
188
+
189
+ Code?
190
+ → <Code> inline
191
+ → <MarkdownViewer> for rendered markdown
192
+ ```
193
+
194
+ ## Component catalog by category
195
+
196
+ ### Actions
197
+ | Component | Subpath | Purpose |
198
+ |---|---|---|
199
+ | Button | `/ui/button` | Primary action element. |
200
+ | IconButton | `/ui/icon-button` | Icon-only square button. |
201
+ | ButtonGroup | `/ui/button-group` | Visually attached group. |
202
+ | SplitButton | `/ui/split-button` | Action + dropdown. |
203
+ | OAuthButton | `/ui/oauth-button` | Brand-aware social login button. |
204
+ | Toggle | `/ui/toggle` | Toggled-state button. |
205
+ | ToggleGroup | `/ui/toggle-group` | Group of toggles (single / multiple). |
206
+ | Link | `/ui/link` | Themed anchor. |
207
+
208
+ ### Inputs
209
+ | Component | Subpath |
210
+ |---|---|
211
+ | Input | `/ui/input` |
212
+ | Textarea | `/ui/textarea` |
213
+ | Select | `/ui/select` |
214
+ | Combobox | `/ui/combobox` |
215
+ | Autocomplete | `/ui/autocomplete` |
216
+ | SearchInput | `/ui/search-input` |
217
+ | NumberInput | `/ui/number-input` |
218
+ | InputOTP | `/ui/input-otp` |
219
+ | Checkbox | `/ui/checkbox` |
220
+ | Radio | `/ui/radio` |
221
+ | Switch | `/ui/switch` |
222
+ | Slider | `/ui/slider` |
223
+ | FileUpload | `/ui/file-upload` |
224
+ | ColorInput | `/ui/color-input` |
225
+ | ColorSwatch | `/ui/color-swatch` |
226
+ | DatePicker | `/composed/date-picker` |
227
+ | Label | `/ui/label` |
228
+ | Form / FormField | `/ui/form` |
229
+ | SegmentedControl | `/ui/segmented-control` |
230
+
231
+ ### Overlays
232
+ | Component | Subpath |
233
+ |---|---|
234
+ | Dialog | `/ui/dialog` |
235
+ | AlertDialog | `/ui/alert-dialog` |
236
+ | Sheet | `/ui/sheet` |
237
+ | Popover | `/ui/popover` |
238
+ | HoverCard | `/ui/hover-card` |
239
+ | Tooltip | `/ui/tooltip` |
240
+ | DropdownMenu | `/ui/dropdown-menu` |
241
+ | ContextMenu | `/ui/context-menu` |
242
+ | Menubar | `/ui/menubar` |
243
+
244
+ ### Feedback
245
+ | Component | Subpath |
246
+ |---|---|
247
+ | Alert | `/ui/alert` |
248
+ | Banner | `/ui/banner` |
249
+ | Toast / Toaster | `/ui/toast`, `/ui/toaster` |
250
+ | Spinner | `/ui/spinner` |
251
+ | Progress | `/ui/progress` |
252
+ | ProgressRing | `/ui/progress-ring` |
253
+ | Skeleton | `/ui/skeleton` |
254
+ | Badge | `/ui/badge` |
255
+ | StatusBadge | `/composed/status-badge` |
256
+ | StatusDot | `/ui/status-dot` |
257
+ | EmptyState | `/composed/empty-state` |
258
+
259
+ ### Layout
260
+ | Component | Subpath |
261
+ |---|---|
262
+ | Container | `/ui/container` |
263
+ | Stack | `/ui/stack` |
264
+ | Card | `/ui/card` |
265
+ | AspectRatio | `/ui/aspect-ratio` |
266
+ | Separator | `/ui/separator` |
267
+ | Accordion | `/ui/accordion` |
268
+ | Collapsible | `/ui/collapsible` |
269
+
270
+ ### Navigation
271
+ | Component | Subpath |
272
+ |---|---|
273
+ | Tabs | `/ui/tabs` |
274
+ | Breadcrumb | `/ui/breadcrumb` |
275
+ | Pagination | `/ui/pagination` |
276
+ | Stepper | `/ui/stepper` |
277
+ | NavigationMenu | `/ui/navigation-menu` |
278
+ | Sidebar (AppSidebar) | `/shell/sidebar` |
279
+ | TopBar | `/shell/top-bar` |
280
+ | BottomNavbar | `/shell/bottom-navbar` |
281
+
282
+ ### Data display
283
+ | Component | Subpath |
284
+ |---|---|
285
+ | Table | `/ui/table` |
286
+ | DataTable | `/ui/data-table` |
287
+ | StatCard | `/ui/stat-card` |
288
+ | ActivityFeed | `/composed/activity-feed` |
289
+ | Avatar | `/ui/avatar` |
290
+ | AvatarGroup | `/composed/avatar-group` |
291
+ | Text | `/ui/text` |
292
+ | Code | `/ui/code` |
293
+ | Charts (bar / line / area / pie / radar / gauge / sparkline) | `/ui/charts/*` |
294
+
295
+ ## Common props across components
296
+
297
+ | Prop | Type | Where |
298
+ |---|---|---|
299
+ | `className` | string | All. Use sparingly. |
300
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | Button, Input, Select, Card, Alert, Badge, Tabs, Checkbox, Radio, Slider, etc. Not all sizes exist on every component. |
301
+ | `color` | `'accent' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | Button, Badge, Card, Tabs (subset), Alert. |
302
+ | `variant` | varies | Button (`solid|soft|outline|ghost|link`), Card, Alert, Badge, Select, etc. |
303
+ | `disabled` | boolean | All interactive. Propagates from ButtonGroup. |
304
+ | `loading` | boolean | Button, IconButton, OAuthButton, async-aware components. |
305
+
306
+ ## Per-component guides
307
+
308
+ See `components/{button|card|input|dialog|badge|select|tabs|toast|form|table|dropdown-menu|popover|text|stack|icon}.md` for prop tables, examples, and rules.
@@ -0,0 +1,201 @@
1
+ # Popover
2
+
3
+ Click-triggered floating panel for interactive content anchored to a trigger.
4
+
5
+ ```tsx
6
+ import {
7
+ Popover,
8
+ PopoverTrigger,
9
+ PopoverContent,
10
+ PopoverAnchor,
11
+ } from '@devalok/shilp-sutra/ui/popover'
12
+ ```
13
+
14
+ ## When to use
15
+
16
+ - Interactive panel anchored to a button: filter form, date picker, color picker, share popover.
17
+ - A list of actions? Use `<DropdownMenu>` — keyboard model differs (arrow nav vs. Tab nav).
18
+ - Inert hover label (tooltip)? Use `<Tooltip>`.
19
+ - Rich hover preview (user card, link preview)? Use `<HoverCard>`.
20
+ - Critical full-attention interaction? Use `<Dialog>`.
21
+
22
+ On mobile, Popover auto-promotes to a bottom drawer when it would overflow the viewport — don't rebuild this manually.
23
+
24
+ ## Compound shape
25
+
26
+ ```
27
+ Popover (root — open, onOpenChange, defaultOpen, modal)
28
+ PopoverTrigger ← asChild around your button
29
+ PopoverAnchor ← optional, decouples trigger from positioning origin
30
+ PopoverContent ← portalled, accepts side/align/sideOffset
31
+ ```
32
+
33
+ ## Root state props (Radix passthrough)
34
+
35
+ | Prop | Type | Notes |
36
+ |---|---|---|
37
+ | `open` | `boolean` | Controlled. |
38
+ | `onOpenChange` | `(open: boolean) => void` | |
39
+ | `defaultOpen` | `boolean` | Uncontrolled. |
40
+ | `modal` | `boolean` | Default `false`. Set `true` for backdrop + focus trap (rare — usually use Dialog). |
41
+
42
+ ## PopoverContent positioning
43
+
44
+ | Prop | Type | Notes |
45
+ |---|---|---|
46
+ | `side` | `'top'\|'right'\|'bottom'\|'left'` | Preferred side. Floats to opposite side if it would overflow. |
47
+ | `align` | `'start'\|'center'\|'end'` | Alignment relative to trigger. |
48
+ | `sideOffset` | `number` (px) | Gap between trigger and content. |
49
+ | `collisionPadding` | `number \| { top, right, bottom, left }` | Distance from viewport edges before flipping. |
50
+
51
+ All Floating-UI positioning options pass through via Radix.
52
+
53
+ ## Examples
54
+
55
+ **Filter form:**
56
+ ```tsx
57
+ <Popover>
58
+ <PopoverTrigger asChild>
59
+ <Button variant="soft" startIcon={IconFilter}>Filters</Button>
60
+ </PopoverTrigger>
61
+ <PopoverContent align="start">
62
+ <Stack gap="ds-04">
63
+ <FormField>
64
+ <Label htmlFor="status">Status</Label>
65
+ <Select value={status} onValueChange={setStatus}>
66
+ <SelectTrigger id="status">
67
+ <SelectValue />
68
+ </SelectTrigger>
69
+ <SelectContent>
70
+ <SelectItem value="all">All</SelectItem>
71
+ <SelectItem value="active">Active</SelectItem>
72
+ <SelectItem value="archived">Archived</SelectItem>
73
+ </SelectContent>
74
+ </Select>
75
+ </FormField>
76
+ <Stack direction="horizontal" gap="ds-03" justify="end">
77
+ <Button variant="ghost" size="sm" onClick={reset}>Clear</Button>
78
+ <Button size="sm" onClick={apply}>Apply</Button>
79
+ </Stack>
80
+ </Stack>
81
+ </PopoverContent>
82
+ </Popover>
83
+ ```
84
+
85
+ **Date picker:**
86
+ ```tsx
87
+ <Popover>
88
+ <PopoverTrigger asChild>
89
+ <Button variant="soft" startIcon={IconCalendar}>
90
+ {date ? formatDate(date) : 'Pick a date'}
91
+ </Button>
92
+ </PopoverTrigger>
93
+ <PopoverContent>
94
+ <Calendar value={date} onChange={(d) => { setDate(d); setOpen(false) }} />
95
+ </PopoverContent>
96
+ </Popover>
97
+ ```
98
+
99
+ **Share popover with copy link:**
100
+ ```tsx
101
+ <Popover>
102
+ <PopoverTrigger asChild>
103
+ <Button variant="soft" startIcon={IconShare}>Share</Button>
104
+ </PopoverTrigger>
105
+ <PopoverContent side="bottom" align="end">
106
+ <Stack gap="ds-03">
107
+ <Text variant="label-sm">Share link</Text>
108
+ <Stack direction="horizontal" gap="ds-02">
109
+ <Input value={shareUrl} readOnly className="flex-1" />
110
+ <IconButton
111
+ icon={<Icon icon={copied ? IconCheck : IconCopy} />}
112
+ variant="soft"
113
+ aria-label="Copy link"
114
+ onClick={() => { navigator.clipboard.writeText(shareUrl); setCopied(true) }}
115
+ />
116
+ </Stack>
117
+ </Stack>
118
+ </PopoverContent>
119
+ </Popover>
120
+ ```
121
+
122
+ **Decoupled anchor (trigger ≠ positioning origin):**
123
+ ```tsx
124
+ <Popover>
125
+ <PopoverAnchor>
126
+ <Card>
127
+ <CardHeader>
128
+ <CardTitle>Project</CardTitle>
129
+ <PopoverTrigger asChild>
130
+ <IconButton icon={<Icon icon={IconInfo} />} variant="ghost" size="sm" aria-label="About" />
131
+ </PopoverTrigger>
132
+ </CardHeader>
133
+ </Card>
134
+ </PopoverAnchor>
135
+ <PopoverContent side="top">
136
+ Popover positions relative to the whole Card, not the small info button.
137
+ </PopoverContent>
138
+ </Popover>
139
+ ```
140
+
141
+ **Inside a Dialog (nested overlays work correctly):**
142
+ ```tsx
143
+ <Dialog>
144
+ <DialogContent>
145
+ <DialogHeader>
146
+ <DialogTitle>Schedule meeting</DialogTitle>
147
+ </DialogHeader>
148
+ <Popover>
149
+ <PopoverTrigger asChild>
150
+ <Button variant="soft">Pick a time</Button>
151
+ </PopoverTrigger>
152
+ <PopoverContent>
153
+ <TimePicker value={time} onChange={setTime} />
154
+ </PopoverContent>
155
+ </Popover>
156
+ </DialogContent>
157
+ </Dialog>
158
+ ```
159
+
160
+ Popover uses `z-popover` (1400), above `z-dialog`. Nesting works without z-index fights.
161
+
162
+ **Controlled with custom close behavior:**
163
+ ```tsx
164
+ const [open, setOpen] = useState(false)
165
+
166
+ <Popover open={open} onOpenChange={setOpen}>
167
+ <PopoverTrigger asChild>
168
+ <Button>Open</Button>
169
+ </PopoverTrigger>
170
+ <PopoverContent
171
+ onInteractOutside={(e) => {
172
+ if (hasUnsavedChanges) {
173
+ e.preventDefault()
174
+ confirmClose().then((ok) => ok && setOpen(false))
175
+ }
176
+ }}
177
+ >
178
+ <UnsavedForm />
179
+ </PopoverContent>
180
+ </Popover>
181
+ ```
182
+
183
+ ## Composability
184
+
185
+ - **Built on Radix Popover** — every standard Radix prop passes through.
186
+ - **Portal rendering:** PopoverContent portals to body. CSS `overflow: hidden` / `transform` on ancestors don't clip it.
187
+ - **z-popover (1400):** Above Dialog (`z-dialog`). Nested popovers inside dialogs stack correctly.
188
+ - **PopoverAnchor:** Decouples the visual anchor from the interactive trigger. Useful when the trigger is small (icon button) but the popover should position relative to a larger surrounding element.
189
+
190
+ See `foundations/surfaces.md` for overlay surface, `foundations/motion.md` for the spring open animation, `foundations/spacing.md` for internal popover padding.
191
+
192
+ ## Rules
193
+
194
+ - Use Popover for interactive content (forms, pickers). Use DropdownMenu for action lists — different keyboard model.
195
+ - `<PopoverTrigger asChild>` around any focusable element — usually a Button or IconButton.
196
+ - Set `modal={true}` only when the popover blocks interaction with the rest of the page. Default `false` is correct for filters / pickers.
197
+ - Set explicit `side` + `align` for predictable positioning. Defaults bottom-center, but designs often want `align="end"` for right-anchored triggers.
198
+ - Don't use Popover for tooltips — Tooltip is for inert hover labels. Popover requires explicit click.
199
+ - Don't nest Popover inside Popover. Use a single Popover with stepped content, or close the first before opening the second.
200
+ - For complex form flows that need full attention, switch to Dialog. Popovers shouldn't host multi-step wizards.
201
+ - The internal padding inside `PopoverContent` is preset — don't add wrapper divs with extra padding. Use `<Stack gap>` to space content.