@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,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.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Table
|
|
2
|
+
|
|
3
|
+
Server-safe semantic wrappers around `<table>`. For static / presentational tables.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableFooter,
|
|
11
|
+
TableRow,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableCell,
|
|
14
|
+
TableCaption,
|
|
15
|
+
} from '@devalok/shilp-sutra/ui/table'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- Static or small data displays where you control every row and cell.
|
|
21
|
+
- Marketing / pricing comparison tables.
|
|
22
|
+
- Documentation tables (API references, prop tables).
|
|
23
|
+
- Server-rendered tables (RSC) — Table and sub-components are server-safe.
|
|
24
|
+
- Need sorting / filtering / pagination / selection / virtualization? Use `<DataTable>` from `@devalok/shilp-sutra/ui/data-table` — out of scope for this guide.
|
|
25
|
+
|
|
26
|
+
## Compound shape
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Table (<table>)
|
|
30
|
+
TableCaption (<caption>) ← optional summary for screen readers
|
|
31
|
+
TableHeader (<thead>)
|
|
32
|
+
TableRow (<tr>)
|
|
33
|
+
TableHead (<th scope="col">)
|
|
34
|
+
TableBody (<tbody>)
|
|
35
|
+
TableRow (<tr>)
|
|
36
|
+
TableCell (<td>)
|
|
37
|
+
TableFooter (<tfoot>)
|
|
38
|
+
TableRow
|
|
39
|
+
TableCell
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each component is a thin semantic wrapper — no props beyond standard HTML attributes plus `className`.
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
**Standard:**
|
|
47
|
+
```tsx
|
|
48
|
+
<Table>
|
|
49
|
+
<TableHeader>
|
|
50
|
+
<TableRow>
|
|
51
|
+
<TableHead>Name</TableHead>
|
|
52
|
+
<TableHead>Status</TableHead>
|
|
53
|
+
<TableHead>Owner</TableHead>
|
|
54
|
+
<TableHead>Updated</TableHead>
|
|
55
|
+
</TableRow>
|
|
56
|
+
</TableHeader>
|
|
57
|
+
<TableBody>
|
|
58
|
+
{projects.map((p) => (
|
|
59
|
+
<TableRow key={p.id}>
|
|
60
|
+
<TableCell>{p.name}</TableCell>
|
|
61
|
+
<TableCell>
|
|
62
|
+
<Badge color={p.status === 'active' ? 'success' : 'neutral'}>
|
|
63
|
+
{p.status}
|
|
64
|
+
</Badge>
|
|
65
|
+
</TableCell>
|
|
66
|
+
<TableCell>
|
|
67
|
+
<Stack direction="horizontal" gap="ds-02" align="center">
|
|
68
|
+
<Avatar size="xs" src={p.owner.avatar} alt={p.owner.name} />
|
|
69
|
+
<Text variant="body-sm">{p.owner.name}</Text>
|
|
70
|
+
</Stack>
|
|
71
|
+
</TableCell>
|
|
72
|
+
<TableCell>
|
|
73
|
+
<Text variant="body-sm" className="text-fg-muted">
|
|
74
|
+
{formatDate(p.updatedAt)}
|
|
75
|
+
</Text>
|
|
76
|
+
</TableCell>
|
|
77
|
+
</TableRow>
|
|
78
|
+
))}
|
|
79
|
+
</TableBody>
|
|
80
|
+
</Table>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**With caption + footer:**
|
|
84
|
+
```tsx
|
|
85
|
+
<Table>
|
|
86
|
+
<TableCaption>Q4 revenue by region.</TableCaption>
|
|
87
|
+
<TableHeader>
|
|
88
|
+
<TableRow>
|
|
89
|
+
<TableHead>Region</TableHead>
|
|
90
|
+
<TableHead>Revenue</TableHead>
|
|
91
|
+
</TableRow>
|
|
92
|
+
</TableHeader>
|
|
93
|
+
<TableBody>
|
|
94
|
+
<TableRow><TableCell>NA</TableCell><TableCell>$1.2M</TableCell></TableRow>
|
|
95
|
+
<TableRow><TableCell>EU</TableCell><TableCell>$0.8M</TableCell></TableRow>
|
|
96
|
+
<TableRow><TableCell>APAC</TableCell><TableCell>$0.4M</TableCell></TableRow>
|
|
97
|
+
</TableBody>
|
|
98
|
+
<TableFooter>
|
|
99
|
+
<TableRow>
|
|
100
|
+
<TableCell><Text variant="label-sm">Total</Text></TableCell>
|
|
101
|
+
<TableCell><Text variant="label-sm">$2.4M</Text></TableCell>
|
|
102
|
+
</TableRow>
|
|
103
|
+
</TableFooter>
|
|
104
|
+
</Table>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Row actions (IconButton in last cell):**
|
|
108
|
+
```tsx
|
|
109
|
+
<Table>
|
|
110
|
+
<TableHeader>
|
|
111
|
+
<TableRow>
|
|
112
|
+
<TableHead>File</TableHead>
|
|
113
|
+
<TableHead>Size</TableHead>
|
|
114
|
+
<TableHead className="w-12" />
|
|
115
|
+
</TableRow>
|
|
116
|
+
</TableHeader>
|
|
117
|
+
<TableBody>
|
|
118
|
+
{files.map((f) => (
|
|
119
|
+
<TableRow key={f.id}>
|
|
120
|
+
<TableCell>{f.name}</TableCell>
|
|
121
|
+
<TableCell>{formatFileSize(f.size)}</TableCell>
|
|
122
|
+
<TableCell>
|
|
123
|
+
<DropdownMenu>
|
|
124
|
+
<DropdownMenuTrigger asChild>
|
|
125
|
+
<IconButton icon={<Icon icon={IconDots} />} variant="ghost" size="sm" aria-label="Actions" />
|
|
126
|
+
</DropdownMenuTrigger>
|
|
127
|
+
<DropdownMenuContent>
|
|
128
|
+
<DropdownMenuItem onSelect={() => download(f)}>Download</DropdownMenuItem>
|
|
129
|
+
<DropdownMenuSeparator />
|
|
130
|
+
<DropdownMenuItem onSelect={() => remove(f)}>Delete</DropdownMenuItem>
|
|
131
|
+
</DropdownMenuContent>
|
|
132
|
+
</DropdownMenu>
|
|
133
|
+
</TableCell>
|
|
134
|
+
</TableRow>
|
|
135
|
+
))}
|
|
136
|
+
</TableBody>
|
|
137
|
+
</Table>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Inside a Card:**
|
|
141
|
+
```tsx
|
|
142
|
+
<Card>
|
|
143
|
+
<CardHeader>
|
|
144
|
+
<CardTitle>Team members</CardTitle>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
<CardContent>
|
|
147
|
+
<Table>
|
|
148
|
+
<TableHeader>
|
|
149
|
+
<TableRow>
|
|
150
|
+
<TableHead>Member</TableHead>
|
|
151
|
+
<TableHead>Role</TableHead>
|
|
152
|
+
</TableRow>
|
|
153
|
+
</TableHeader>
|
|
154
|
+
<TableBody>
|
|
155
|
+
{members.map((m) => (
|
|
156
|
+
<TableRow key={m.id}>
|
|
157
|
+
<TableCell>{m.name}</TableCell>
|
|
158
|
+
<TableCell><Badge color="neutral">{m.role}</Badge></TableCell>
|
|
159
|
+
</TableRow>
|
|
160
|
+
))}
|
|
161
|
+
</TableBody>
|
|
162
|
+
</Table>
|
|
163
|
+
</CardContent>
|
|
164
|
+
</Card>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
When inside a `<Card>`, the Table's surrounding padding comes from `CardContent`. Don't add extra padding on the Table.
|
|
168
|
+
|
|
169
|
+
**Server-rendered table (RSC):**
|
|
170
|
+
```tsx
|
|
171
|
+
// app/projects/page.tsx — no 'use client'
|
|
172
|
+
export default async function ProjectsPage() {
|
|
173
|
+
const projects = await db.projects.findMany()
|
|
174
|
+
return (
|
|
175
|
+
<Table>
|
|
176
|
+
<TableHeader>
|
|
177
|
+
<TableRow>
|
|
178
|
+
<TableHead>Name</TableHead>
|
|
179
|
+
<TableHead>Status</TableHead>
|
|
180
|
+
</TableRow>
|
|
181
|
+
</TableHeader>
|
|
182
|
+
<TableBody>
|
|
183
|
+
{projects.map((p) => (
|
|
184
|
+
<TableRow key={p.id}>
|
|
185
|
+
<TableCell>{p.name}</TableCell>
|
|
186
|
+
<TableCell><Badge>{p.status}</Badge></TableCell>
|
|
187
|
+
</TableRow>
|
|
188
|
+
))}
|
|
189
|
+
</TableBody>
|
|
190
|
+
</Table>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Table + Badge (server-safe via Badge.Group context — verify per use) + Avatar all render in RSC trees. Skip client components.
|
|
196
|
+
|
|
197
|
+
## Composability
|
|
198
|
+
|
|
199
|
+
- **Server-safe:** Table and its sub-components are pure HTML semantic wrappers. No state, no context. Use in RSC trees without `'use client'`.
|
|
200
|
+
- **TableHead scope:** Headers automatically get `scope="col"` for screen-reader navigation. Don't override it.
|
|
201
|
+
- **Composes with primitives:** Drop `<Badge>`, `<Avatar>`, `<IconButton>`, `<StatusDot>` inside cells. Check each component's server-safety if you need RSC compatibility.
|
|
202
|
+
- **TableCaption:** Renders as HTML `<caption>` — screen readers announce it before content. Use it for any non-trivial table.
|
|
203
|
+
|
|
204
|
+
See `foundations/typography.md` for the body / label variants inside cells, `foundations/surfaces.md` for table-in-card surface guidance.
|
|
205
|
+
|
|
206
|
+
## Rules
|
|
207
|
+
|
|
208
|
+
- For anything with sorting / filtering / pagination / selection / virtualization, use `<DataTable>` from `/ui/data-table`. Don't rebuild that machinery on bare Table.
|
|
209
|
+
- Always wrap header cells in `<TableHead>` (renders `<th>`). Don't use `<TableCell>` (`<td>`) in headers — breaks screen-reader column scope.
|
|
210
|
+
- Use `<TableCaption>` for any table that's not self-evident. Renders the HTML `<caption>` which screen readers announce.
|
|
211
|
+
- Inside Card, drop the Table directly in `<CardContent>` — don't add wrapper divs that fight the card's padding cascade.
|
|
212
|
+
- For wide tables on mobile, wrap in an `overflow-x-auto` container. Don't try to make a Table responsive via column stacking — switch to a card list on small viewports.
|
|
213
|
+
- Don't style cell text with raw Tailwind palette utilities. Use `text-fg-muted` from `foundations/color.md`.
|
|
214
|
+
- Compose with Badge for status, Avatar for users, IconButton for row actions. Don't invent new primitives per table.
|
|
215
|
+
- Keep TableCell content single-line where possible — multi-line cells make scanning hard. Use `<Stack>` only when each row genuinely needs two lines.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
Switch between sibling views inside a single region. Not for top-level navigation.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@devalok/shilp-sutra/ui/tabs'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- Sub-sections of a single page / panel that share context (Overview / Activity / Settings on a project).
|
|
12
|
+
- Filtered views over the same dataset (All / Mine / Archived).
|
|
13
|
+
- Need URL-driven routes per tab? Wire `value` / `onValueChange` to router state.
|
|
14
|
+
- Top-level app navigation? Use `<Sidebar>` / `<TopBar>`, not Tabs.
|
|
15
|
+
- Multi-step flows? Use `<Stepper>` or a wizard pattern.
|
|
16
|
+
|
|
17
|
+
## Compound shape
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Tabs (root — value, defaultValue, onValueChange)
|
|
21
|
+
TabsList (variant, size, orientation)
|
|
22
|
+
TabsTrigger (value) ← inherits variant/size/orientation from TabsList
|
|
23
|
+
TabsContent (value) ← rendered inline (not portalled)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## TabsList props
|
|
27
|
+
|
|
28
|
+
| Prop | Type | Notes |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `variant` | `'line'\|'contained'` | Default `line`. |
|
|
31
|
+
| `size` | `'sm'\|'md'\|'lg'` | Default `md`. |
|
|
32
|
+
| `orientation` | `'horizontal'\|'vertical'` | Default `horizontal`. Vertical also changes keyboard nav to ArrowUp/Down. |
|
|
33
|
+
| `color` | `'accent'\|'neutral'` | Affects the line-variant active indicator. |
|
|
34
|
+
|
|
35
|
+
## Variants
|
|
36
|
+
|
|
37
|
+
| Variant | When |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `line` (default) | Underline active indicator. Most common — pairs with section headings. |
|
|
40
|
+
| `contained` | Pill background per active trigger. Use inside cards or compact toolbars. |
|
|
41
|
+
|
|
42
|
+
## Root state props (Radix passthrough)
|
|
43
|
+
|
|
44
|
+
| Prop | Type | Notes |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `value` | `string` | Controlled active tab. |
|
|
47
|
+
| `defaultValue` | `string` | Uncontrolled initial tab. |
|
|
48
|
+
| `onValueChange` | `(value: string) => void` | Fires on tab change. |
|
|
49
|
+
|
|
50
|
+
## TabsTrigger / TabsContent
|
|
51
|
+
|
|
52
|
+
Both require a `value: string` prop. The values must match between a trigger and its content.
|
|
53
|
+
|
|
54
|
+
`TabsTrigger` reads `variant` / `size` / `orientation` from `TabsList` via context. Override per-trigger if needed, but normally don't.
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
**Standard line tabs:**
|
|
59
|
+
```tsx
|
|
60
|
+
<Tabs defaultValue="overview">
|
|
61
|
+
<TabsList>
|
|
62
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
63
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
64
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
65
|
+
</TabsList>
|
|
66
|
+
<TabsContent value="overview">
|
|
67
|
+
<ProjectOverview />
|
|
68
|
+
</TabsContent>
|
|
69
|
+
<TabsContent value="activity">
|
|
70
|
+
<ActivityFeed />
|
|
71
|
+
</TabsContent>
|
|
72
|
+
<TabsContent value="settings">
|
|
73
|
+
<ProjectSettings />
|
|
74
|
+
</TabsContent>
|
|
75
|
+
</Tabs>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Contained variant inside a card:**
|
|
79
|
+
```tsx
|
|
80
|
+
<Card>
|
|
81
|
+
<CardContent>
|
|
82
|
+
<Tabs defaultValue="day">
|
|
83
|
+
<TabsList variant="contained" size="sm">
|
|
84
|
+
<TabsTrigger value="day">Day</TabsTrigger>
|
|
85
|
+
<TabsTrigger value="week">Week</TabsTrigger>
|
|
86
|
+
<TabsTrigger value="month">Month</TabsTrigger>
|
|
87
|
+
</TabsList>
|
|
88
|
+
<TabsContent value="day"><Chart range="day" /></TabsContent>
|
|
89
|
+
<TabsContent value="week"><Chart range="week" /></TabsContent>
|
|
90
|
+
<TabsContent value="month"><Chart range="month" /></TabsContent>
|
|
91
|
+
</Tabs>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Vertical orientation (settings-style):**
|
|
97
|
+
```tsx
|
|
98
|
+
<Tabs defaultValue="account" orientation="vertical">
|
|
99
|
+
<Stack direction="horizontal" gap="ds-07" align="start">
|
|
100
|
+
<TabsList orientation="vertical">
|
|
101
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
102
|
+
<TabsTrigger value="billing">Billing</TabsTrigger>
|
|
103
|
+
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
104
|
+
</TabsList>
|
|
105
|
+
<div className="flex-1">
|
|
106
|
+
<TabsContent value="account"><AccountForm /></TabsContent>
|
|
107
|
+
<TabsContent value="billing"><BillingForm /></TabsContent>
|
|
108
|
+
<TabsContent value="notifications"><NotificationsForm /></TabsContent>
|
|
109
|
+
</div>
|
|
110
|
+
</Stack>
|
|
111
|
+
</Tabs>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**With icons + badges:**
|
|
115
|
+
```tsx
|
|
116
|
+
<Tabs defaultValue="inbox">
|
|
117
|
+
<TabsList>
|
|
118
|
+
<TabsTrigger value="inbox">
|
|
119
|
+
<Icon icon={IconInbox} /> Inbox
|
|
120
|
+
<Badge size="xs" color="accent">12</Badge>
|
|
121
|
+
</TabsTrigger>
|
|
122
|
+
<TabsTrigger value="sent">
|
|
123
|
+
<Icon icon={IconSend} /> Sent
|
|
124
|
+
</TabsTrigger>
|
|
125
|
+
</TabsList>
|
|
126
|
+
<TabsContent value="inbox"><InboxList /></TabsContent>
|
|
127
|
+
<TabsContent value="sent"><SentList /></TabsContent>
|
|
128
|
+
</Tabs>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Router-driven (Next.js App Router):**
|
|
132
|
+
```tsx
|
|
133
|
+
'use client'
|
|
134
|
+
const router = useRouter()
|
|
135
|
+
const pathname = usePathname()
|
|
136
|
+
const tab = pathname.split('/').pop() ?? 'overview'
|
|
137
|
+
|
|
138
|
+
<Tabs value={tab} onValueChange={(v) => router.push(`/projects/${id}/${v}`)}>
|
|
139
|
+
<TabsList>
|
|
140
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
141
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
142
|
+
</TabsList>
|
|
143
|
+
</Tabs>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Composability
|
|
147
|
+
|
|
148
|
+
- **Context cascade:** `TabsList` propagates `variant` / `size` / `orientation` to every child `TabsTrigger`. Don't repeat those props on each trigger.
|
|
149
|
+
- **Inline content:** `TabsContent` renders inline (not portalled). Container-scoped queries in tests work.
|
|
150
|
+
- **Keyboard:** Roving tabindex via Radix — ArrowLeft/Right (horizontal) or ArrowUp/Down (vertical). Home / End jump to first / last. Don't re-implement.
|
|
151
|
+
|
|
152
|
+
See `foundations/spacing.md` for the gap between TabsList and TabsContent, `foundations/icons.md` for icon sizing inside triggers.
|
|
153
|
+
|
|
154
|
+
## Rules
|
|
155
|
+
|
|
156
|
+
- Put `variant` / `size` / `orientation` on `TabsList`, NOT on `Tabs` root or `TabsTrigger`.
|
|
157
|
+
- Every `TabsTrigger` and `TabsContent` needs a `value` — the values must match.
|
|
158
|
+
- For top-level app navigation use Sidebar / TopBar, not Tabs.
|
|
159
|
+
- Don't stack two Tabs inside the same region — pick one. Nested tabs confuse keyboard nav and section structure.
|
|
160
|
+
- For router-bound tabs, keep `value` controlled — don't mix `defaultValue` with router-driven URLs.
|
|
161
|
+
- For 5+ tabs that overflow on mobile, consider a Select dropdown on small viewports or wrap in a horizontally scrollable container.
|
|
162
|
+
- Icons in TabsTrigger don't auto-size via IconProvider — set explicit `<Icon icon={...} size="sm" />` when the trigger feels off.
|