@devalok/shilp-sutra 0.41.0 → 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.
@@ -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.
@@ -0,0 +1,148 @@
1
+ # Select
2
+
3
+ Single-choice picker from a short fixed list. Use instead of `<select>`.
4
+
5
+ ```tsx
6
+ import {
7
+ Select,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ SelectContent,
11
+ SelectGroup,
12
+ SelectLabel,
13
+ SelectItem,
14
+ SelectSeparator,
15
+ } from '@devalok/shilp-sutra/ui/select'
16
+ ```
17
+
18
+ ## When to use
19
+
20
+ - Fixed list under ~15 items, no search needed (status, priority, role).
21
+ - Need typeahead / search across many options? Use `<Combobox>`.
22
+ - Free-text with suggestions? Use `<Autocomplete>`.
23
+ - Multi-select? Use `<MultiSelect>` or `<Combobox multiple>`.
24
+ - Yes/no toggle? Use `<Switch>` or `<RadioGroup>` with 2 options.
25
+
26
+ ## Compound shape
27
+
28
+ ```
29
+ Select (root — value, onValueChange, defaultValue)
30
+ SelectTrigger ← variant / color / size go HERE
31
+ SelectValue (placeholder)
32
+ SelectContent
33
+ SelectGroup (optional)
34
+ SelectLabel ← non-interactive section header
35
+ SelectItem (value) ← REQUIRED value, unique
36
+ SelectSeparator
37
+ ```
38
+
39
+ ## SelectTrigger props
40
+
41
+ | Prop | Type | Notes |
42
+ |---|---|---|
43
+ | `variant` | `'default'\|'outline'\|'ghost'` | Default `default`. |
44
+ | `color` | `'default'\|'error'\|'success'\|'warning'` | Default `default`. `error` sets `aria-invalid`. |
45
+ | `size` | `'xs'\|'sm'\|'md'\|'lg'` | Default `md`. |
46
+
47
+ Styling lives on the **Trigger**, not on `Select` root. Setting `<Select size="lg">` does nothing — TypeScript won't catch it.
48
+
49
+ ## Root state props (Radix passthrough)
50
+
51
+ | Prop | Type | Notes |
52
+ |---|---|---|
53
+ | `value` | `string` | Controlled value. |
54
+ | `onValueChange` | `(value: string) => void` | Fires on select. |
55
+ | `defaultValue` | `string` | Uncontrolled. |
56
+ | `open` | `boolean` | Controlled open state. |
57
+ | `onOpenChange` | `(open: boolean) => void` | |
58
+
59
+ ## Examples
60
+
61
+ **Standard:**
62
+ ```tsx
63
+ <Select onValueChange={setStatus}>
64
+ <SelectTrigger>
65
+ <SelectValue placeholder="Status" />
66
+ </SelectTrigger>
67
+ <SelectContent>
68
+ <SelectItem value="todo">To do</SelectItem>
69
+ <SelectItem value="doing">In progress</SelectItem>
70
+ <SelectItem value="done">Done</SelectItem>
71
+ </SelectContent>
72
+ </Select>
73
+ ```
74
+
75
+ **With grouped items and a separator:**
76
+ ```tsx
77
+ <Select value={assignee} onValueChange={setAssignee}>
78
+ <SelectTrigger size="sm">
79
+ <SelectValue placeholder="Assignee" />
80
+ </SelectTrigger>
81
+ <SelectContent>
82
+ <SelectGroup>
83
+ <SelectLabel>Team</SelectLabel>
84
+ <SelectItem value="alice">Alice</SelectItem>
85
+ <SelectItem value="bob">Bob</SelectItem>
86
+ </SelectGroup>
87
+ <SelectSeparator />
88
+ <SelectGroup>
89
+ <SelectLabel>External</SelectLabel>
90
+ <SelectItem value="contractor-1">Contractor 1</SelectItem>
91
+ </SelectGroup>
92
+ </SelectContent>
93
+ </Select>
94
+ ```
95
+
96
+ **Inside a FormField (manual error wiring):**
97
+ ```tsx
98
+ <FormField state={errors.role ? 'error' : 'helper'}>
99
+ <Label htmlFor="role">Role</Label>
100
+ <Select value={role} onValueChange={setRole}>
101
+ <SelectTrigger id="role" color={errors.role ? 'error' : 'default'}>
102
+ <SelectValue placeholder="Choose a role" />
103
+ </SelectTrigger>
104
+ <SelectContent>
105
+ <SelectItem value="admin">Admin</SelectItem>
106
+ <SelectItem value="member">Member</SelectItem>
107
+ <SelectItem value="viewer">Viewer</SelectItem>
108
+ </SelectContent>
109
+ </Select>
110
+ {errors.role && <FormHelperText>{errors.role}</FormHelperText>}
111
+ </FormField>
112
+ ```
113
+
114
+ Select doesn't auto-consume FormField context (unlike Input / Textarea). Set `color="error"` on `SelectTrigger` manually.
115
+
116
+ **Ghost variant in a toolbar:**
117
+ ```tsx
118
+ <Select value={sort} onValueChange={setSort} defaultValue="recent">
119
+ <SelectTrigger variant="ghost" size="sm">
120
+ <SelectValue />
121
+ </SelectTrigger>
122
+ <SelectContent>
123
+ <SelectItem value="recent">Most recent</SelectItem>
124
+ <SelectItem value="oldest">Oldest</SelectItem>
125
+ <SelectItem value="az">A → Z</SelectItem>
126
+ </SelectContent>
127
+ </Select>
128
+ ```
129
+
130
+ ## Composability
131
+
132
+ - **Radix Select** underneath — `value` / `onValueChange` / `defaultValue` / `open` / `onOpenChange` standard state.
133
+ - **Portal + z-popover (1400):** SelectContent portals to body, stacks above Dialog / Sheet / other overlays.
134
+ - **SelectItem requires a unique `value`** — duplicates produce undefined selection behavior.
135
+ - **FormField integration is manual** — wire `color="error"` on SelectTrigger from your validation state.
136
+
137
+ See `foundations/surfaces.md` for the overlay surface, `foundations/color.md` for state colors.
138
+
139
+ ## Rules
140
+
141
+ - Put `variant` / `color` / `size` on `SelectTrigger`, NOT on `Select` root.
142
+ - Every `SelectItem` needs a unique `value` prop.
143
+ - For lists over ~15 items or when users will scan for a term, switch to `<Combobox>` — Select has no typeahead.
144
+ - Set `color="error"` on `SelectTrigger` for validation failures — Select doesn't auto-consume FormField state.
145
+ - Don't use Select for multi-value capture — use `<MultiSelect>` or `<Combobox multiple>`.
146
+ - Always render `<SelectValue placeholder="..." />` — without it, the trigger renders empty before any selection.
147
+ - Group related items with `<SelectGroup>` + `<SelectLabel>` — flat lists over 8 items get hard to scan.
148
+ - Don't customize the dropdown surface — overlays use `surface-1` per `foundations/surfaces.md`.
@@ -0,0 +1,165 @@
1
+ # Stack
2
+
3
+ Flexbox layout primitive. Use instead of `<div className="flex flex-col gap-4">` everywhere.
4
+
5
+ ```tsx
6
+ import { Stack } from '@devalok/shilp-sutra/ui/stack'
7
+ ```
8
+
9
+ ## When to use
10
+
11
+ - Stacking elements vertically or horizontally with consistent gap.
12
+ - Any time you'd write `flex` + `gap-*` + `items-*` + `justify-*` on a div.
13
+ - Centering + width-capping a page section? Use `<Container>`, then a `<Stack>` inside.
14
+ - Grid layouts (rows × columns)? Use a Tailwind `grid` div directly — Stack is for one-axis layouts.
15
+
16
+ Server-safe — no hydration, no context.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Notes |
21
+ |---|---|---|
22
+ | `direction` | `'vertical'\|'horizontal'\|'row'\|'column'` | Default `vertical`. `row` = `horizontal`, `column` = `vertical` (aliases). |
23
+ | `gap` | `'ds-01'..'ds-13'` \| `0..13` | Design-system spacing token. Numbers map 1:1 to `ds-0N`. |
24
+ | `align` | `'start'\|'center'\|'end'\|'stretch'\|'baseline'` | Cross-axis alignment (`align-items`). |
25
+ | `justify` | `'start'\|'center'\|'end'\|'between'\|'around'\|'evenly'` | Main-axis alignment (`justify-content`). |
26
+ | `wrap` | `boolean` | Enables `flex-wrap`. |
27
+ | `as` | `ElementType` | Default `'div'`. Polymorphic. |
28
+ | `className` | `string` | For overrides — don't reach for raw `flex-*` utilities. |
29
+
30
+ ## Gap cadence
31
+
32
+ Default to the 3-tier cadence from `foundations/spacing.md`:
33
+
34
+ | Gap | Use |
35
+ |---|---|
36
+ | `ds-03` (8 px) | Related items inside a group (icon + label, button row). |
37
+ | `ds-05` (16 px) | Grouped sections within a card / form. |
38
+ | `ds-07` (32 px) | Page sections / major regions. |
39
+
40
+ Don't reach for every adjacent token. If `ds-04` "feels right," it usually means a parent's padding or the section relationship needs a rethink.
41
+
42
+ ## Examples
43
+
44
+ **Vertical form layout:**
45
+ ```tsx
46
+ <Stack gap="ds-05">
47
+ <FormField>
48
+ <Label htmlFor="name">Name</Label>
49
+ <Input id="name" />
50
+ </FormField>
51
+ <FormField>
52
+ <Label htmlFor="email">Email</Label>
53
+ <Input id="email" />
54
+ </FormField>
55
+ </Stack>
56
+ ```
57
+
58
+ **Horizontal toolbar:**
59
+ ```tsx
60
+ <Stack direction="horizontal" gap="ds-03" align="center">
61
+ <Button variant="soft" startIcon={IconFilter}>Filter</Button>
62
+ <Button variant="soft" startIcon={IconSortDescending}>Sort</Button>
63
+ <Separator orientation="vertical" className="h-6" />
64
+ <Button variant="solid" startIcon={IconPlus}>New</Button>
65
+ </Stack>
66
+ ```
67
+
68
+ **Avatar + label cluster:**
69
+ ```tsx
70
+ <Stack direction="horizontal" gap="ds-03" align="center">
71
+ <Avatar size="sm" src={user.avatar} alt={user.name} />
72
+ <Stack gap="ds-01">
73
+ <Text variant="label-plain-sm">{user.name}</Text>
74
+ <Text variant="body-xs" className="text-fg-muted">{user.role}</Text>
75
+ </Stack>
76
+ </Stack>
77
+ ```
78
+
79
+ **Page-section spacing:**
80
+ ```tsx
81
+ <Container>
82
+ <Stack gap="ds-07">
83
+ <PageHeader title="Projects" description="A workspace for everything you ship." />
84
+ <Stack gap="ds-05">
85
+ <SectionHeader title="Active" />
86
+ <ProjectGrid projects={active} />
87
+ </Stack>
88
+ <Stack gap="ds-05">
89
+ <SectionHeader title="Archived" />
90
+ <ProjectGrid projects={archived} />
91
+ </Stack>
92
+ </Stack>
93
+ </Container>
94
+ ```
95
+
96
+ **Wrap for tag cluster:**
97
+ ```tsx
98
+ <Stack direction="horizontal" gap="ds-02" wrap>
99
+ {tags.map((tag) => <Badge key={tag} color="neutral">{tag}</Badge>)}
100
+ </Stack>
101
+ ```
102
+
103
+ For tag clusters specifically, prefer `<Badge.Group>` — it handles overflow with `+N` automatically.
104
+
105
+ **Justify-between (label + action):**
106
+ ```tsx
107
+ <Stack direction="horizontal" gap="ds-04" align="center" justify="between">
108
+ <Text variant="heading-md">Team</Text>
109
+ <Button variant="soft" startIcon={IconPlus} size="sm">Invite</Button>
110
+ </Stack>
111
+ ```
112
+
113
+ **Polymorphic — semantic list:**
114
+ ```tsx
115
+ <Stack as="ul" gap="ds-03">
116
+ {items.map((item) => (
117
+ <Stack as="li" key={item.id} direction="horizontal" gap="ds-03" align="center">
118
+ <Icon icon={IconCheck} />
119
+ <Text variant="body-sm">{item.label}</Text>
120
+ </Stack>
121
+ ))}
122
+ </Stack>
123
+ ```
124
+
125
+ **Numeric gap shortcut:**
126
+ ```tsx
127
+ <Stack gap={5}> {/* same as gap="ds-05" */}
128
+
129
+ </Stack>
130
+ ```
131
+
132
+ **Inside Card (no extra padding):**
133
+ ```tsx
134
+ <Card>
135
+ <CardContent>
136
+ <Stack gap="ds-04">
137
+ <Text variant="label-sm" className="text-fg-muted">REVENUE</Text>
138
+ <Text variant="heading-xl">$2.4M</Text>
139
+ <Text variant="body-sm" className="text-fg-muted">+18% YoY</Text>
140
+ </Stack>
141
+ </CardContent>
142
+ </Card>
143
+ ```
144
+
145
+ CardContent already supplies the outer padding. Stack just spaces the children.
146
+
147
+ ## Composability
148
+
149
+ - **Server-safe.** Nothing to hydrate. Use in RSC trees.
150
+ - **Polymorphic via `as`** — `<Stack as="ul">`, `<Stack as="section">`, `<Stack as="nav">`. Inherits flex behavior on the chosen element.
151
+ - **Compose with Container:** `<Container><Stack>...</Stack></Container>`. Container centers + caps width; Stack arranges children.
152
+ - **No responsive direction prop** — for `flex-col md:flex-row` use a plain div with Tailwind utilities directly, or render two Stacks with display toggling.
153
+
154
+ See `foundations/spacing.md` for the gap cadence, `foundations/surfaces.md` for layout-on-surfaces context.
155
+
156
+ ## Rules
157
+
158
+ - Default to the `ds-03 / ds-05 / ds-07` cadence. Reach for other tokens only with a deliberate reason.
159
+ - Use Stack everywhere you'd write `flex` + `gap` on a div. Don't mix Stack and raw flex utilities in the same tree.
160
+ - For grid (2D) layouts, use a plain `grid` div. Stack is one-axis only.
161
+ - For responsive direction changes, drop to a plain div with Tailwind responsive flex utilities. Stack doesn't have a responsive direction prop.
162
+ - Don't add padding to a Stack — wrap it in a Container or Card. Padding lives on containers, not layout primitives.
163
+ - For tag clusters with potential overflow, use `<Badge.Group>` instead of `<Stack wrap>` — it handles `+N` collapsing.
164
+ - Numeric `gap={4}` and string `gap="ds-04"` are equivalent. Pick a convention per file and stick with it.
165
+ - `direction="row"` and `direction="column"` are aliases for `horizontal` / `vertical`. Pick one naming pair per codebase.