@misoto22/design 0.5.0 → 0.6.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/README.md +124 -0
- package/bin/misoto22-design.mjs +205 -0
- package/dist/agent/Accordion.md +41 -0
- package/dist/agent/Alert.md +42 -0
- package/dist/agent/AppShell.md +40 -0
- package/dist/agent/AreaChart.md +68 -0
- package/dist/agent/Article.md +41 -0
- package/dist/agent/Avatar.md +32 -0
- package/dist/agent/Badge.md +40 -0
- package/dist/agent/BarChart.md +65 -0
- package/dist/agent/BarList.md +40 -0
- package/dist/agent/BigNumber.md +45 -0
- package/dist/agent/BoxPlot.md +58 -0
- package/dist/agent/Breadcrumb.md +32 -0
- package/dist/agent/BulletChart.md +48 -0
- package/dist/agent/Button.md +56 -0
- package/dist/agent/Calendar.md +42 -0
- package/dist/agent/Card.md +49 -0
- package/dist/agent/Checkbox.md +35 -0
- package/dist/agent/Collapsible.md +46 -0
- package/dist/agent/Combobox.md +50 -0
- package/dist/agent/Command.md +93 -0
- package/dist/agent/ComposedChart.md +57 -0
- package/dist/agent/ContextMenu.md +59 -0
- package/dist/agent/DatePicker.md +75 -0
- package/dist/agent/Diagram.md +46 -0
- package/dist/agent/Dialog.md +52 -0
- package/dist/agent/DropdownMenu.md +70 -0
- package/dist/agent/EmptyState.md +37 -0
- package/dist/agent/ErrorState.md +41 -0
- package/dist/agent/Facet.md +82 -0
- package/dist/agent/Field.md +37 -0
- package/dist/agent/FigureBand.md +37 -0
- package/dist/agent/FloatingIconButton.md +38 -0
- package/dist/agent/FunnelChart.md +52 -0
- package/dist/agent/Heatmap.md +53 -0
- package/dist/agent/Histogram.md +63 -0
- package/dist/agent/Input.md +30 -0
- package/dist/agent/Kbd.md +29 -0
- package/dist/agent/LineChart.md +60 -0
- package/dist/agent/LinkArrow.md +29 -0
- package/dist/agent/NativeSelect.md +40 -0
- package/dist/agent/NavItem.md +35 -0
- package/dist/agent/Pagination.md +43 -0
- package/dist/agent/PieChart.md +55 -0
- package/dist/agent/Popover.md +57 -0
- package/dist/agent/Progress.md +33 -0
- package/dist/agent/RadarChart.md +54 -0
- package/dist/agent/RadialChart.md +57 -0
- package/dist/agent/RadioGroup.md +44 -0
- package/dist/agent/SankeyChart.md +61 -0
- package/dist/agent/ScatterChart.md +54 -0
- package/dist/agent/ScrollArea.md +40 -0
- package/dist/agent/SearchableMenu.md +46 -0
- package/dist/agent/Select.md +75 -0
- package/dist/agent/Separator.md +35 -0
- package/dist/agent/Sheet.md +57 -0
- package/dist/agent/Skeleton.md +73 -0
- package/dist/agent/Slider.md +39 -0
- package/dist/agent/Sparkline.md +45 -0
- package/dist/agent/Spinner.md +39 -0
- package/dist/agent/StatusDot.md +36 -0
- package/dist/agent/StatusPill.md +28 -0
- package/dist/agent/Steps.md +44 -0
- package/dist/agent/Switch.md +29 -0
- package/dist/agent/Table.md +85 -0
- package/dist/agent/Tabs.md +48 -0
- package/dist/agent/Tag.md +35 -0
- package/dist/agent/Textarea.md +26 -0
- package/dist/agent/Toast.md +26 -0
- package/dist/agent/ToggleGroup.md +44 -0
- package/dist/agent/Tooltip.md +51 -0
- package/dist/agent/TreemapChart.md +52 -0
- package/dist/agent/WaterfallChart.md +62 -0
- package/dist/agent/catalog.json +1791 -0
- package/dist/agent/index.md +131 -0
- package/dist/charts/AreaChart/AreaChart.d.ts +191 -0
- package/dist/charts/AreaChart/AreaChart.js +480 -0
- package/dist/charts/AreaChart/AreaChart.js.map +1 -0
- package/dist/charts/BarChart/BarChart.d.ts +179 -0
- package/dist/charts/BarChart/BarChart.js +591 -0
- package/dist/charts/BarChart/BarChart.js.map +1 -0
- package/dist/charts/BarList/BarList.d.ts +57 -0
- package/dist/charts/BarList/BarList.js +71 -0
- package/dist/charts/BarList/BarList.js.map +1 -0
- package/dist/charts/BigNumber/BigNumber.d.ts +56 -0
- package/dist/charts/BigNumber/BigNumber.js +31 -0
- package/dist/charts/BigNumber/BigNumber.js.map +1 -0
- package/dist/charts/BoxPlot/BoxPlot.d.ts +177 -0
- package/dist/charts/BoxPlot/BoxPlot.js +495 -0
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -0
- package/dist/charts/BulletChart/BulletChart.d.ts +100 -0
- package/dist/charts/BulletChart/BulletChart.js +162 -0
- package/dist/charts/BulletChart/BulletChart.js.map +1 -0
- package/dist/charts/ComposedChart/ComposedChart.d.ts +171 -0
- package/dist/charts/ComposedChart/ComposedChart.js +667 -0
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -0
- package/dist/charts/Facet/Facet.d.ts +240 -0
- package/dist/charts/Facet/Facet.js +133 -0
- package/dist/charts/Facet/Facet.js.map +1 -0
- package/dist/charts/Facet/panels.d.ts +187 -0
- package/dist/charts/Facet/panels.js +203 -0
- package/dist/charts/Facet/panels.js.map +1 -0
- package/dist/charts/FunnelChart/FunnelChart.d.ts +101 -0
- package/dist/charts/FunnelChart/FunnelChart.js +147 -0
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -0
- package/dist/charts/Heatmap/Heatmap.d.ts +78 -0
- package/dist/charts/Heatmap/Heatmap.js +108 -0
- package/dist/charts/Heatmap/Heatmap.js.map +1 -0
- package/dist/charts/Histogram/Histogram.d.ts +165 -0
- package/dist/charts/Histogram/Histogram.js +350 -0
- package/dist/charts/Histogram/Histogram.js.map +1 -0
- package/dist/charts/LineChart/LineChart.d.ts +167 -0
- package/dist/charts/LineChart/LineChart.js +486 -0
- package/dist/charts/LineChart/LineChart.js.map +1 -0
- package/dist/charts/PieChart/PieChart.d.ts +128 -0
- package/dist/charts/PieChart/PieChart.js +309 -0
- package/dist/charts/PieChart/PieChart.js.map +1 -0
- package/dist/charts/RadarChart/RadarChart.d.ts +123 -0
- package/dist/charts/RadarChart/RadarChart.js +299 -0
- package/dist/charts/RadarChart/RadarChart.js.map +1 -0
- package/dist/charts/RadialChart/RadialChart.d.ts +124 -0
- package/dist/charts/RadialChart/RadialChart.js +257 -0
- package/dist/charts/RadialChart/RadialChart.js.map +1 -0
- package/dist/charts/SankeyChart/SankeyChart.d.ts +137 -0
- package/dist/charts/SankeyChart/SankeyChart.js +501 -0
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -0
- package/dist/charts/ScatterChart/ScatterChart.d.ts +128 -0
- package/dist/charts/ScatterChart/ScatterChart.js +269 -0
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -0
- package/dist/charts/Sparkline/Sparkline.d.ts +53 -0
- package/dist/charts/Sparkline/Sparkline.js +93 -0
- package/dist/charts/Sparkline/Sparkline.js.map +1 -0
- package/dist/charts/TreemapChart/TreemapChart.d.ts +93 -0
- package/dist/charts/TreemapChart/TreemapChart.js +118 -0
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -0
- package/dist/charts/WaterfallChart/WaterfallChart.d.ts +164 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js +319 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -0
- package/dist/charts/index.d.ts +41 -0
- package/dist/charts/index.js +128 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/lib/annotations.d.ts +103 -0
- package/dist/charts/lib/annotations.js +99 -0
- package/dist/charts/lib/annotations.js.map +1 -0
- package/dist/charts/lib/axis.d.ts +18 -0
- package/dist/charts/lib/axis.js +21 -0
- package/dist/charts/lib/axis.js.map +1 -0
- package/dist/charts/lib/background.d.ts +33 -0
- package/dist/charts/lib/background.js +173 -0
- package/dist/charts/lib/background.js.map +1 -0
- package/dist/charts/lib/brush.d.ts +94 -0
- package/dist/charts/lib/brush.js +535 -0
- package/dist/charts/lib/brush.js.map +1 -0
- package/dist/charts/lib/chart.d.ts +94 -0
- package/dist/charts/lib/chart.js +134 -0
- package/dist/charts/lib/chart.js.map +1 -0
- package/dist/charts/lib/dot.d.ts +29 -0
- package/dist/charts/lib/dot.js +78 -0
- package/dist/charts/lib/dot.js.map +1 -0
- package/dist/charts/lib/empty.d.ts +31 -0
- package/dist/charts/lib/empty.js +13 -0
- package/dist/charts/lib/empty.js.map +1 -0
- package/dist/charts/lib/export.d.ts +103 -0
- package/dist/charts/lib/export.js +204 -0
- package/dist/charts/lib/export.js.map +1 -0
- package/dist/charts/lib/figure.d.ts +61 -0
- package/dist/charts/lib/figure.js +59 -0
- package/dist/charts/lib/figure.js.map +1 -0
- package/dist/charts/lib/format.d.ts +46 -0
- package/dist/charts/lib/format.js +56 -0
- package/dist/charts/lib/format.js.map +1 -0
- package/dist/charts/lib/legend.d.ts +45 -0
- package/dist/charts/lib/legend.js +117 -0
- package/dist/charts/lib/legend.js.map +1 -0
- package/dist/charts/lib/loading.d.ts +48 -0
- package/dist/charts/lib/loading.js +105 -0
- package/dist/charts/lib/loading.js.map +1 -0
- package/dist/charts/lib/paint.d.ts +154 -0
- package/dist/charts/lib/paint.js +551 -0
- package/dist/charts/lib/paint.js.map +1 -0
- package/dist/charts/lib/sonify-control.d.ts +163 -0
- package/dist/charts/lib/sonify-control.js +172 -0
- package/dist/charts/lib/sonify-control.js.map +1 -0
- package/dist/charts/lib/sonify.d.ts +248 -0
- package/dist/charts/lib/sonify.js +262 -0
- package/dist/charts/lib/sonify.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +143 -0
- package/dist/charts/lib/toolbar.js +156 -0
- package/dist/charts/lib/toolbar.js.map +1 -0
- package/dist/charts/lib/tooltip.d.ts +59 -0
- package/dist/charts/lib/tooltip.js +135 -0
- package/dist/charts/lib/tooltip.js.map +1 -0
- package/dist/charts/lib/values.d.ts +56 -0
- package/dist/charts/lib/values.js +71 -0
- package/dist/charts/lib/values.js.map +1 -0
- package/dist/charts/lib/zoom.d.ts +139 -0
- package/dist/charts/lib/zoom.js +314 -0
- package/dist/charts/lib/zoom.js.map +1 -0
- package/dist/semantic.css +19 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +50 -0
- package/dist/tokens.css +118 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.json +101 -0
- package/package.json +26 -4
- package/skills/misoto22-design/SKILL.md +171 -0
- package/skills/misoto22-design/evals/claims.json +95 -0
- package/skills/misoto22-design/rules/a11y.md +115 -0
- package/skills/misoto22-design/rules/composition.md +108 -0
- package/skills/misoto22-design/rules/forms.md +128 -0
- package/skills/misoto22-design/rules/naming.md +155 -0
- package/skills/misoto22-design/rules/tokens.md +131 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
The package keeps these promises so a call site does not have to. What is left
|
|
4
|
+
is the handful of things only the call site knows — and every one of them is a
|
|
5
|
+
prop that is easy to leave off and impossible to notice afterwards.
|
|
6
|
+
|
|
7
|
+
## Props the type makes required, and why
|
|
8
|
+
|
|
9
|
+
These are required in TypeScript because forgetting them ships a control that
|
|
10
|
+
cannot be used without sight. Do not satisfy the type with a placeholder.
|
|
11
|
+
|
|
12
|
+
| Prop | What it is |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `Table.caption` | What the table is a table *of*. Announced before the rows. |
|
|
15
|
+
| `Progress.label` | What is progressing. `aria-valuetext` has nothing else to say. |
|
|
16
|
+
| `Avatar.alt` + `Avatar.fallback` | Who it is, and the initials shown when the image fails. |
|
|
17
|
+
| `FloatingIconButton.label` | There is no text in the button at all. |
|
|
18
|
+
| `Select.label` | The trigger is a button; this is its accessible name. |
|
|
19
|
+
| `Combobox.label` | Same. |
|
|
20
|
+
| `SheetContent.title` | A panel with no name drops a screen reader into an unnamed region. |
|
|
21
|
+
| `AccordionItem.title` | The heading that is also the trigger. |
|
|
22
|
+
| `EmptyState.title` | The state's own name. |
|
|
23
|
+
| `Alert.title` | Optional, but an alert with no title is a coloured paragraph. |
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// Incorrect — the type is satisfied and the table is still anonymous
|
|
27
|
+
<Table caption="">
|
|
28
|
+
|
|
29
|
+
// Incorrect — the caption is not a label for the reader, it is the reader's only orientation
|
|
30
|
+
<Table caption="Table">
|
|
31
|
+
|
|
32
|
+
// Correct
|
|
33
|
+
<Table caption="Invoices awaiting payment">
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `showCaption={false}` when the design has no room for a visible caption. The
|
|
37
|
+
caption still reaches assistive tech.
|
|
38
|
+
|
|
39
|
+
## An icon-only control needs a name
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// Incorrect — reads as "button"
|
|
43
|
+
<Button iconOnly><TrashIcon /></Button>
|
|
44
|
+
|
|
45
|
+
// Correct
|
|
46
|
+
<Button iconOnly aria-label="Delete invoice"><TrashIcon /></Button>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The same applies to any `Button` whose children are only an icon, whether or not
|
|
50
|
+
`iconOnly` is set. `FloatingIconButton` cannot get this wrong — its `label` is
|
|
51
|
+
required.
|
|
52
|
+
|
|
53
|
+
## Decoration is `aria-hidden`
|
|
54
|
+
|
|
55
|
+
An icon that repeats the words beside it is noise; an arrow inside a link is
|
|
56
|
+
read out as "north east arrow".
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Incorrect
|
|
60
|
+
<StatusPill tone="success"><CheckIcon /> Available</StatusPill>
|
|
61
|
+
|
|
62
|
+
// Correct
|
|
63
|
+
<StatusPill tone="success"><CheckIcon aria-hidden="true" /> Available</StatusPill>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Colour is never the only carrier
|
|
67
|
+
|
|
68
|
+
Every status tone in the system is doubled by an icon, by the words, or by both.
|
|
69
|
+
Keep that when you compose.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Incorrect — the only difference between these two rows is a hue
|
|
73
|
+
<TD><span className="text-(--danger)">●</span></TD>
|
|
74
|
+
|
|
75
|
+
// Correct
|
|
76
|
+
<TD><StatusPill tone="danger">Overdue</StatusPill></TD>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Do not re-implement a pattern Radix already owns
|
|
80
|
+
|
|
81
|
+
Focus traps, roving tabindex, typeahead, portal placement and scroll locking are
|
|
82
|
+
where a hand-rolled component quietly becomes unusable with a keyboard. `Dialog`,
|
|
83
|
+
`Sheet`, `Popover`, `DropdownMenu`, `ContextMenu`, `Tabs`, `Accordion`,
|
|
84
|
+
`RadioGroup`, `ToggleGroup`, `Select`, `Slider` and `Tooltip` already wrap it.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Incorrect
|
|
88
|
+
<div role="dialog" className="fixed inset-0 …" onKeyDown={handleEscape}>
|
|
89
|
+
|
|
90
|
+
// Correct
|
|
91
|
+
<Dialog><DialogContent title="…">…</DialogContent></Dialog>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Motion is gated
|
|
95
|
+
|
|
96
|
+
Anything animated is behind `motion-safe` and carries `data-m22-animated`, so
|
|
97
|
+
the one reduced-motion rule in the package can reach it. If you animate
|
|
98
|
+
something yourself, do both.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// Correct — the package's keyframes are all prefixed `m22-`
|
|
102
|
+
<div data-m22-animated className="motion-safe:animate-[m22-fade-in_200ms_var(--ease-out-expo)]">
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`data-motion="still"` on any ancestor stops motion in that subtree.
|
|
106
|
+
|
|
107
|
+
## The keyboard contract is documented per component
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx misoto22-design docs Combobox
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
prints the key-by-key contract. It is the part that quietly regresses — a
|
|
114
|
+
wrapper that swallows a key looks identical until someone tries it — so check it
|
|
115
|
+
before you wrap one of these in your own handler.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Composition
|
|
2
|
+
|
|
3
|
+
## `className` adjusts layout, never appearance
|
|
4
|
+
|
|
5
|
+
The caller's `className` is merged last through `cn()`, so it *can* override
|
|
6
|
+
anything. That is what makes the rule necessary rather than enforced.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// Incorrect — re-skins the component, and only in this one place
|
|
10
|
+
<Button className="bg-blue-600 text-white rounded-full px-8 text-lg">
|
|
11
|
+
|
|
12
|
+
// Correct — the appearance is a variant, the layout is the class
|
|
13
|
+
<Button variant="primary" size="lg" className="w-full">
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Legitimate `className`: `w-`, `flex-`, `grid-`, `col-span-`, `m*`, `self-`,
|
|
17
|
+
`order-`. Not legitimate: colour, border, radius, font size, shadow. If the
|
|
18
|
+
appearance you want is not reachable through props and the theme axes, that is a
|
|
19
|
+
gap in the system — say so rather than patching it at one call site.
|
|
20
|
+
|
|
21
|
+
## Use `cn()` for conditional classes
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { cn } from '@misoto22/design'
|
|
25
|
+
|
|
26
|
+
// Incorrect — emits both sides of the conflict; stylesheet order picks a winner
|
|
27
|
+
<div className={`p-6 ${dense ? 'p-2' : ''}`}>
|
|
28
|
+
|
|
29
|
+
// Correct — twMerge resolves by utility group, so p-2 replaces p-6
|
|
30
|
+
<div className={cn('p-6', dense && 'p-2')}>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Card uses its full set
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Incorrect
|
|
37
|
+
<Card><div className="p-6"><h3 className="font-bold">Usage</h3><p>…</p></div></Card>
|
|
38
|
+
|
|
39
|
+
// Correct
|
|
40
|
+
<Card variant="outline">
|
|
41
|
+
<CardHeader><CardTitle>Usage</CardTitle></CardHeader>
|
|
42
|
+
<CardBody>…</CardBody>
|
|
43
|
+
<CardFooter><Button>Manage</Button></CardFooter>
|
|
44
|
+
</Card>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`variant`: `outline` (default) · `plate` · `flat`. The part is `CardBody`, not
|
|
48
|
+
`CardContent` — see [naming.md](./naming.md).
|
|
49
|
+
|
|
50
|
+
## Reach for a component before writing markup
|
|
51
|
+
|
|
52
|
+
| Instead of | Use |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| a styled div with an icon and a message | `Alert` |
|
|
55
|
+
| a centred "nothing here yet" block | `EmptyState` |
|
|
56
|
+
| an error page or panel | `ErrorState` |
|
|
57
|
+
| `<hr>` or `border-t` | `Separator` |
|
|
58
|
+
| a custom `animate-pulse` div | `Skeleton` (+ `SkeletonLine` / `SkeletonText` / `SkeletonPage`) |
|
|
59
|
+
| a styled span for a count or state | `Badge`, or `StatusPill` for a live state |
|
|
60
|
+
| a styled span for a keyboard key | `Kbd` |
|
|
61
|
+
| a hand-built breadcrumb trail | `Breadcrumb` |
|
|
62
|
+
| a hand-built page/sidebar frame | `AppShell` |
|
|
63
|
+
| a hand-built numbered process | `Steps` |
|
|
64
|
+
| long-form prose you did not style yourself | `Article` |
|
|
65
|
+
|
|
66
|
+
## Navigation goes through `asChild`
|
|
67
|
+
|
|
68
|
+
No component in this package imports a router. A control that navigates takes
|
|
69
|
+
its own element from the call site.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Incorrect in a Next.js app — a full page load
|
|
73
|
+
<Button href="/settings">Settings</Button>
|
|
74
|
+
|
|
75
|
+
// Correct
|
|
76
|
+
<Button asChild><Link href="/settings">Settings</Link></Button>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`href` on `Button` renders a plain `<a>`. That is right for an external link and
|
|
80
|
+
wrong for in-app navigation.
|
|
81
|
+
|
|
82
|
+
## Overlays own their own stacking
|
|
83
|
+
|
|
84
|
+
`Dialog`, `Sheet`, `Popover`, `DropdownMenu`, `ContextMenu` and `Tooltip` portal
|
|
85
|
+
themselves and read `--z-overlay` / `--z-dropdown`.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// Incorrect
|
|
89
|
+
<DialogContent title="Edit" className="z-50">
|
|
90
|
+
|
|
91
|
+
// Correct
|
|
92
|
+
<DialogContent title="Edit">
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`TooltipProvider` goes once at the app root; `Toaster` too.
|
|
96
|
+
|
|
97
|
+
## Logical properties, always
|
|
98
|
+
|
|
99
|
+
`ps-` / `pe-` / `ms-` / `me-` / `start-` / `end-` / `text-start` / `border-s`.
|
|
100
|
+
A physical property breaks RTL, and the package is RTL-correct everywhere else.
|
|
101
|
+
|
|
102
|
+
## Server and client
|
|
103
|
+
|
|
104
|
+
The package is compiled per-file, so `'use client'` sits on the components that
|
|
105
|
+
need it. `Button`, `Card`, `Badge`, `Table`, `Alert`, `Separator` and the other
|
|
106
|
+
static primitives stay server-renderable; anything wrapping Radix carries the
|
|
107
|
+
directive itself. Do not add `'use client'` to a page just because it imports
|
|
108
|
+
from here.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
`Field` is the form row. It does the ARIA wiring that a hand-rolled label +
|
|
4
|
+
input + error div silently skips, and skipping it is invisible in a browser and
|
|
5
|
+
total for a screen reader.
|
|
6
|
+
|
|
7
|
+
## A labelled control is a `Field`
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
// Incorrect — the hint is drawn and never announced, and required is decorative
|
|
11
|
+
<div className="space-y-2">
|
|
12
|
+
<label htmlFor="email">Email *</label>
|
|
13
|
+
<Input id="email" />
|
|
14
|
+
<p className="text-xs text-red-500">Enter a valid email.</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
// Correct
|
|
18
|
+
<Field label="Email" required error="Enter a valid email.">
|
|
19
|
+
<Input type="email" />
|
|
20
|
+
</Field>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`Field` generates the id, points the label at it, and sets `aria-describedby`,
|
|
24
|
+
`aria-required` and `aria-invalid` on the control.
|
|
25
|
+
|
|
26
|
+
## Do not repeat what `Field` already sets
|
|
27
|
+
|
|
28
|
+
Passing `error` sets `aria-invalid` on the control for you. Writing `invalid`
|
|
29
|
+
as well is redundant, and writing a red border class is wrong.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// Redundant
|
|
33
|
+
<Field label="Email" error={message}><Input invalid /></Field>
|
|
34
|
+
|
|
35
|
+
// Wrong
|
|
36
|
+
<Field label="Email" error={message}><Input className="border-red-500" /></Field>
|
|
37
|
+
|
|
38
|
+
// Correct
|
|
39
|
+
<Field label="Email" error={message}><Input /></Field>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Reach for `invalid` on the control only when there is no `Field` around it.
|
|
43
|
+
|
|
44
|
+
## `hint` and `error` are one slot
|
|
45
|
+
|
|
46
|
+
They are not two stacked messages. `error` replaces `hint` when present: when a
|
|
47
|
+
field is wrong, the thing to read is what is wrong with it.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Correct — one of the two shows, decided by whether there is an error
|
|
51
|
+
<Field label="Password" hint="At least 12 characters." error={errors.password}>
|
|
52
|
+
<Input type="password" />
|
|
53
|
+
</Field>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## One control per `Field`
|
|
57
|
+
|
|
58
|
+
The wiring clones a **single** element child. Two children, or a wrapper div,
|
|
59
|
+
and nothing is wired — the label points at an id that is on nothing.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// Incorrect — the div is the child, so the input gets no id, no describedby
|
|
63
|
+
<Field label="Amount">
|
|
64
|
+
<div className="flex gap-2"><Input /><Button>Max</Button></div>
|
|
65
|
+
</Field>
|
|
66
|
+
|
|
67
|
+
// Correct — one Field per control; the row is laid out outside it
|
|
68
|
+
<div className="flex items-end gap-2">
|
|
69
|
+
<Field label="Amount" className="flex-1"><Input /></Field>
|
|
70
|
+
<Button>Max</Button>
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## The controls
|
|
75
|
+
|
|
76
|
+
| Need | Component | Note |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| Text, email, number | `Input` | |
|
|
79
|
+
| Multi-line | `Textarea` | |
|
|
80
|
+
| A short list, styled | `Select` | `label` is **required** |
|
|
81
|
+
| A short list, native | `NativeSelect` | Use on mobile-first forms |
|
|
82
|
+
| A long, searchable list | `Combobox` | `options` array, `label` **required** |
|
|
83
|
+
| Boolean, in a form | `Checkbox` | |
|
|
84
|
+
| Boolean, applied immediately | `Switch` | |
|
|
85
|
+
| One of 2–7 visible options | `RadioGroup` + `RadioGroupItem` | |
|
|
86
|
+
| One of 2–7, as a toolbar | `ToggleGroup` + `ToggleGroupItem` | |
|
|
87
|
+
| A number in a range | `Slider` | |
|
|
88
|
+
| A date | `DatePicker` / `DateRangePicker` | |
|
|
89
|
+
|
|
90
|
+
`Select` and `Combobox` take `label` even inside a `Field` — it is the
|
|
91
|
+
accessible name of the trigger, which is a button, not a labellable control.
|
|
92
|
+
|
|
93
|
+
## Custom controls read the shared base
|
|
94
|
+
|
|
95
|
+
Do not re-derive the text-control look. Three exports carry it, and they respond
|
|
96
|
+
to `data-density` and `data-mode` for free.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { CONTROL_BASE, CONTROL_BORDER, cn, isInvalid } from '@misoto22/design'
|
|
100
|
+
|
|
101
|
+
function MoneyInput({ invalid, 'aria-invalid': ariaInvalid, className, ...rest }) {
|
|
102
|
+
const bad = isInvalid(invalid, ariaInvalid)
|
|
103
|
+
return (
|
|
104
|
+
<input
|
|
105
|
+
aria-invalid={bad || undefined}
|
|
106
|
+
className={cn(CONTROL_BASE, bad ? CONTROL_BORDER.invalid : CONTROL_BORDER.resting, className)}
|
|
107
|
+
{...rest}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`isInvalid()` reads both spellings — `invalid` is the ergonomic prop, and
|
|
114
|
+
`aria-invalid` is what a form library sets. A control that honours only one of
|
|
115
|
+
them loses the error styling for half the ecosystem.
|
|
116
|
+
|
|
117
|
+
## Layout
|
|
118
|
+
|
|
119
|
+
`Field` already stacks its own parts. For the gap between fields, use flex with
|
|
120
|
+
`gap`, not margin utilities on the children.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Correct
|
|
124
|
+
<form className="flex flex-col gap-4">
|
|
125
|
+
<Field label="Name"><Input /></Field>
|
|
126
|
+
<Field label="Email"><Input type="email" /></Field>
|
|
127
|
+
</form>
|
|
128
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Naming — where habit produces an import that does not exist
|
|
2
|
+
|
|
3
|
+
Most models have read far more shadcn/ui and Radix than they have read this
|
|
4
|
+
package. Those habits produce identifiers that are not exported here, and the
|
|
5
|
+
failure is a TypeScript error at the import line rather than something subtle.
|
|
6
|
+
|
|
7
|
+
This is the whole divergence list. Everything not on it is spelled the way you
|
|
8
|
+
would guess.
|
|
9
|
+
|
|
10
|
+
## Compound parts renamed
|
|
11
|
+
|
|
12
|
+
| Habit | Here |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `CardContent` | `CardBody` |
|
|
15
|
+
| `TableHeader` | `THead` |
|
|
16
|
+
| `TableBody` | `TBody` |
|
|
17
|
+
| `TableRow` | `TR` |
|
|
18
|
+
| `TableHead` | `TH` |
|
|
19
|
+
| `TableCell` | `TD` |
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Incorrect
|
|
23
|
+
<Card><CardContent>…</CardContent></Card>
|
|
24
|
+
|
|
25
|
+
// Correct
|
|
26
|
+
<Card><CardBody>…</CardBody></Card>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Compounds that are one component with props
|
|
30
|
+
|
|
31
|
+
These have no sub-parts at all. Reaching for `<XTrigger>` / `<XContent>` fails.
|
|
32
|
+
|
|
33
|
+
### Dialog and Sheet — the title is a prop
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Incorrect — DialogTitle and DialogHeader are not exported
|
|
37
|
+
<DialogContent>
|
|
38
|
+
<DialogHeader><DialogTitle>Delete project</DialogTitle></DialogHeader>
|
|
39
|
+
…
|
|
40
|
+
</DialogContent>
|
|
41
|
+
|
|
42
|
+
// Correct
|
|
43
|
+
<DialogContent title="Delete project" description="This cannot be undone.">
|
|
44
|
+
…
|
|
45
|
+
</DialogContent>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`Sheet` is the same shape, and `SheetTitle` does not exist either:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Incorrect
|
|
52
|
+
<SheetContent><SheetTitle>Filters</SheetTitle>…</SheetContent>
|
|
53
|
+
|
|
54
|
+
// Correct
|
|
55
|
+
<SheetContent side="end" title="Filters">…</SheetContent>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`SheetContent.title` is required by the type. `DialogContent.title` is optional
|
|
59
|
+
— Radix demands a title either way, so omitting it renders a visually-hidden
|
|
60
|
+
placeholder rather than warning. Pass it anyway: the placeholder keeps the modal
|
|
61
|
+
legal, not useful. When the design calls for no visible heading, pass the title
|
|
62
|
+
and add `hideTitle`, so the accessible name is a real one.
|
|
63
|
+
|
|
64
|
+
### Tooltip — the content is a prop, and it wraps its trigger
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Incorrect
|
|
68
|
+
<Tooltip>
|
|
69
|
+
<TooltipTrigger asChild><Button /></TooltipTrigger>
|
|
70
|
+
<TooltipContent>Copy</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
|
|
73
|
+
// Correct — put TooltipProvider once at the app root
|
|
74
|
+
<Tooltip content="Copy"><Button iconOnly aria-label="Copy"><CopyIcon /></Button></Tooltip>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Alert and EmptyState — tone and title are props
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Incorrect
|
|
81
|
+
<Alert><AlertTitle>Saved</AlertTitle><AlertDescription>…</AlertDescription></Alert>
|
|
82
|
+
|
|
83
|
+
// Correct
|
|
84
|
+
<Alert tone="success" title="Saved">Your changes are live.</Alert>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`EmptyState` is the same shape: `icon` (a Lucide component, not an element),
|
|
88
|
+
`title`, `description`, `action`.
|
|
89
|
+
|
|
90
|
+
### Accordion — the item carries its own title
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Incorrect
|
|
94
|
+
<AccordionItem value="a">
|
|
95
|
+
<AccordionTrigger>Billing</AccordionTrigger>
|
|
96
|
+
<AccordionContent>…</AccordionContent>
|
|
97
|
+
</AccordionItem>
|
|
98
|
+
|
|
99
|
+
// Correct
|
|
100
|
+
<AccordionItem value="a" title="Billing">…</AccordionItem>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Select — one component, `SelectItem` children
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// Incorrect
|
|
107
|
+
<Select>
|
|
108
|
+
<SelectTrigger><SelectValue placeholder="Choose" /></SelectTrigger>
|
|
109
|
+
<SelectContent><SelectItem value="au">Australia</SelectItem></SelectContent>
|
|
110
|
+
</Select>
|
|
111
|
+
|
|
112
|
+
// Correct — label is required
|
|
113
|
+
<Select label="Region" placeholder="Choose">
|
|
114
|
+
<SelectItem value="au">Australia</SelectItem>
|
|
115
|
+
</Select>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`SelectRoot` is the raw Radix root, exported for the rare case that needs it.
|
|
119
|
+
Reach for `Select` unless you know why you are not.
|
|
120
|
+
|
|
121
|
+
### Combobox — an options array, not children
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// Correct
|
|
125
|
+
<Combobox
|
|
126
|
+
label="Assignee"
|
|
127
|
+
options={[{ value: 'ada', label: 'Ada Lovelace' }]}
|
|
128
|
+
value={value}
|
|
129
|
+
onValueChange={setValue}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Compounds that ARE compounds
|
|
134
|
+
|
|
135
|
+
Spelled as expected, no surprises: `Popover`, `DropdownMenu`, `ContextMenu`,
|
|
136
|
+
`Command`, `Tabs`, `Collapsible`, `RadioGroup`, `ToggleGroup`, `Skeleton`.
|
|
137
|
+
|
|
138
|
+
## Imports that come from here, not from upstream
|
|
139
|
+
|
|
140
|
+
| Habit | Here |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `import { toast } from 'sonner'` | `import { toast } from '@misoto22/design'` |
|
|
143
|
+
| `import { cn } from '@/lib/utils'` | `import { cn } from '@misoto22/design'` |
|
|
144
|
+
|
|
145
|
+
`<Toaster />` goes once at the app root, and it is exported from here too.
|
|
146
|
+
|
|
147
|
+
## When in doubt
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx misoto22-design docs <Component>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That prints the component's real exports, every prop with its type and default,
|
|
154
|
+
and its keyboard contract, read out of the installed source. It is cheaper than
|
|
155
|
+
a wrong import and a retry.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Colour, theming and spacing
|
|
2
|
+
|
|
3
|
+
The system is monochrome by construction: paper ground, near-black mark, and
|
|
4
|
+
status is the only chroma. A raw colour class is not a style choice here, it is
|
|
5
|
+
a hole in the theme — it survives the light/dark swap unchanged and it ignores
|
|
6
|
+
every surface axis.
|
|
7
|
+
|
|
8
|
+
## Never write a raw colour
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
// Incorrect
|
|
12
|
+
<div className="bg-white text-gray-500 border-neutral-200">
|
|
13
|
+
<div className="bg-neutral-900 dark:bg-white">
|
|
14
|
+
<div style={{ color: '#101010' }}>
|
|
15
|
+
|
|
16
|
+
// Correct
|
|
17
|
+
<div className="bg-(--background) text-(--foreground-muted) border-(--border-color)">
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The semantic layer is the one to read. It is shipped as its own entry point
|
|
21
|
+
(`@misoto22/design/semantic.css`) and it is what the components themselves use.
|
|
22
|
+
|
|
23
|
+
| Role | Token |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Page ground | `--background` |
|
|
26
|
+
| One step off the ground | `--background-elevated` |
|
|
27
|
+
| Card ground | `--card-background` |
|
|
28
|
+
| Body text | `--foreground` |
|
|
29
|
+
| Quieter text | `--foreground-muted` |
|
|
30
|
+
| Quietest text that still clears AA | `--secondary-text` |
|
|
31
|
+
| Edge | `--border-color` |
|
|
32
|
+
| Hairline | `--border-subtle` |
|
|
33
|
+
| A chosen state — primary button, checked box, active tab | `--accent` |
|
|
34
|
+
| Text on an accent fill | `--accent-foreground` |
|
|
35
|
+
| Text on an accent wash | `--accent-on-muted` |
|
|
36
|
+
| Status | `--success` `--warning` `--danger` `--info`, each with a `-wash` |
|
|
37
|
+
| Text over a photograph | `--on-dark` |
|
|
38
|
+
|
|
39
|
+
Prefer these over the primitive layer (`--paper`, `--ink`, `--rule`, `--stone`).
|
|
40
|
+
Primitives are values; semantics are roles, and a role can be re-pointed without
|
|
41
|
+
a component changing. `--card-background` is already an alias for a surface that
|
|
42
|
+
was retired — that is the layer doing its job.
|
|
43
|
+
|
|
44
|
+
## Never write a `dark:` variant
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Incorrect — freezes one side of the swap
|
|
48
|
+
<div className="bg-white dark:bg-neutral-900">
|
|
49
|
+
|
|
50
|
+
// Correct — the token already resolves in both
|
|
51
|
+
<div className="bg-(--background)">
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Dark mode is a value swap on the primitives behind `[data-mode="dark"]`. Any
|
|
55
|
+
semantic token re-resolves on its own. A `dark:` class is a second, competing
|
|
56
|
+
theme that will disagree with the first the moment a surface axis is set.
|
|
57
|
+
|
|
58
|
+
## Theme with the seven axes
|
|
59
|
+
|
|
60
|
+
Each is an attribute, each is independent, and each works on **any element** —
|
|
61
|
+
not just `:root`. An unset axis is the default.
|
|
62
|
+
|
|
63
|
+
| Attribute | Values | Unset means |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `data-mode` | `light` `dark` | follows the app |
|
|
66
|
+
| `data-surface` | `warm` `cool` `glass` | paper |
|
|
67
|
+
| `data-radius` | `sharp` `round` | the default ladder |
|
|
68
|
+
| `data-rules` | `quiet` `firm` | hairline |
|
|
69
|
+
| `data-type` | `grotesk` `bookish` | editorial |
|
|
70
|
+
| `data-motion` | `still` `snappy` | calm |
|
|
71
|
+
| `data-density` | `compact` | comfortable |
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// A warm, square, dense panel inside an otherwise default page.
|
|
75
|
+
<section data-surface="warm" data-radius="sharp" data-density="compact">…</section>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
There is **no `data-accent` attribute.** `--accent` is a single CSS custom
|
|
79
|
+
property; re-skin the system by re-pointing it in your own stylesheet, not by
|
|
80
|
+
setting an attribute.
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
/* Correct way to re-accent */
|
|
84
|
+
:root { --accent: var(--ok); --accent-hover: var(--ok-ink); }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Radius, duration and control size come from tokens
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Incorrect
|
|
91
|
+
<div className="rounded-lg duration-150 min-h-11">
|
|
92
|
+
|
|
93
|
+
// Correct
|
|
94
|
+
<div className="rounded-(--radius) duration-(--duration-fast) min-h-(--control-h-md)">
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--control-h-md` is 44px comfortable / 36px compact. Hard-coding `min-h-11`
|
|
98
|
+
means the control never responds to `data-density`, and `min-height` is a floor,
|
|
99
|
+
so it also never shrinks.
|
|
100
|
+
|
|
101
|
+
Durations: `--duration-fast` `--duration-base` `--duration-slow`. Easing:
|
|
102
|
+
`--ease-out-expo`.
|
|
103
|
+
|
|
104
|
+
## No blurred shadow
|
|
105
|
+
|
|
106
|
+
`--shadow`, `--shadow-sm` and `--shadow-lg` all resolve to `none` on purpose.
|
|
107
|
+
The system has no elevation ramp. Depth is a hairline, a change of ground, or
|
|
108
|
+
`--lift` — a hard ink offset with no blur.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Incorrect
|
|
112
|
+
<div className="shadow-lg">
|
|
113
|
+
|
|
114
|
+
// Correct
|
|
115
|
+
<div className="shadow-(--lift)">
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Logical properties, always
|
|
119
|
+
|
|
120
|
+
The package is RTL-correct and a physical property breaks it.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Incorrect
|
|
124
|
+
<div className="pl-4 ml-2 text-left border-l">
|
|
125
|
+
|
|
126
|
+
// Correct
|
|
127
|
+
<div className="ps-4 ms-2 text-start border-s">
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Tailwind has no logical `translate`, so anything sliding along the inline axis
|
|
131
|
+
needs an explicit `rtl:` counterpart.
|