division-ds 0.1.2 → 0.2.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 +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +7 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Division DS: instructions for coding agents
|
|
2
2
|
|
|
3
|
-
A React 19 + Tailwind 4 + Radix design system:
|
|
3
|
+
A React 19 + Tailwind 4 + Radix design system: 68 components, tokens in three layers, light and
|
|
4
4
|
dark themes, WCAG 2.2 AA. This file is the entry point. It is short on purpose; everything it
|
|
5
5
|
points to is generated from the code, so it is current.
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ points to is generated from the code, so it is current.
|
|
|
11
11
|
| The list of components, one line each | `public/llms.txt` |
|
|
12
12
|
| One component: props, examples as code, keyboard, do and don't | `public/docs/components/<slug>.md` |
|
|
13
13
|
| Every component's props in one compact file | `public/llms-components.txt` |
|
|
14
|
+
| The whole system in one short file (for a tool that takes one design file) | `public/DESIGN.md` |
|
|
14
15
|
| Which token to use for what | `public/llms-tokens.txt` |
|
|
15
16
|
| Shared helpers: form errors, money, links, theme, streamed text | `public/docs/helpers.md` |
|
|
16
17
|
| The same as data | `public/manifest/components.json`, `public/manifest/tokens.json` |
|
|
@@ -42,10 +43,17 @@ npx division-ds check <files or dirs> (from npm; Node 22 or newer)
|
|
|
42
43
|
node <path-to-division-ds>/scripts/division-ds.mjs check <files or dirs> (the same, from a clone)
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
It runs the colour
|
|
46
|
+
It runs the colour, button-spacing and AI copy gates and checks every Division DS element's props against
|
|
46
47
|
`public/manifest/components.json`: an unknown prop, or a value that is not in the prop's union.
|
|
47
48
|
Exit 1 means a finding; each one says what to write instead. `division-ds rules --target <dir>`
|
|
48
49
|
installs these rules into that project's `AGENTS.md`, `GEMINI.md` and Cursor rules.
|
|
50
|
+
`division-ds skill --target <dir>` installs the skill into `.agents/skills/` (Codex, Cursor) and
|
|
51
|
+
`.claude/skills/` (Claude Code).
|
|
52
|
+
|
|
53
|
+
An agent that supports MCP can have the docs and the same checks as tools instead:
|
|
54
|
+
`claude mcp add division-ds -- npx -y division-ds mcp` (`list_components`, `get_component`,
|
|
55
|
+
`search`, `get_tokens`, `validate_code`, `check_contrast`, `get_add_command`,
|
|
56
|
+
`get_review_checklist`).
|
|
49
57
|
|
|
50
58
|
<!-- rules:start -->
|
|
51
59
|
## Rules
|
|
@@ -175,6 +183,12 @@ element arriving, `anim-draw-on` for a hairline) and the motion tokens (`--durat
|
|
|
175
183
|
hover, `--duration-base` by default, `--duration-slow` for a larger change, `--ease-snap`).
|
|
176
184
|
Anything you animate yourself stops under `prefers-reduced-motion` (`motion-reduce:` in
|
|
177
185
|
Tailwind), and nothing moves on its own: no autoplay.
|
|
186
|
+
|
|
187
|
+
<!-- topic: ai -->
|
|
188
|
+
|
|
189
|
+
**Assistant copy.** A button names the action ("Summarize"), never "AI". Say "response", not
|
|
190
|
+
"answer". No "please" or "!" in assistant labels. The interface says "Assistant"; only its own
|
|
191
|
+
messages say "I".
|
|
178
192
|
<!-- rules:end -->
|
|
179
193
|
|
|
180
194
|
## Changing the system itself
|
package/README.md
CHANGED
|
@@ -16,4 +16,15 @@ npx division-ds rules # writes the Division DS rules into AGENTS.md / CLAUDE
|
|
|
16
16
|
npx division-ds help
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## For agents: the MCP server
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp add division-ds -- npx -y division-ds mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Any MCP client works the same way: the command is `npx -y division-ds mcp`, on stdio. It gives the
|
|
26
|
+
agent eight tools: `list_components`, `get_component`, `search`, `get_tokens`,
|
|
27
|
+
`validate_code` (the checks above, on code the agent just wrote), `check_contrast` (WCAG 2.2 AA,
|
|
28
|
+
both themes), `get_add_command` and `get_review_checklist`.
|
|
29
|
+
|
|
19
30
|
The components, the docs and the agent skill: https://github.com/Victor-Ilioiu/division-ds
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "division-ds",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Checks a project that uses Division DS components
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Checks a project that uses Division DS components (raw colours, invented tokens, touching buttons, unknown props), and serves the docs and the same checks to agents over MCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Victor Ilioiu",
|
|
7
7
|
"homepage": "https://victor-ilioiu.vercel.app/systems/division-ds/",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Accordion
|
|
2
|
+
|
|
3
|
+
> Sections that open and close in place, each under its own heading. You can allow one open at a time or several, and draw them as plain rows split by thin lines or as a panel per section.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Sections people scan by heading and open one or two of: frequently asked questions (FAQ), grouped settings, long specifications.
|
|
6
|
+
- **Not for:** Steps in a sequence: use Stepper. Equal views of the same thing: use Tabs.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/accordion/Accordion.tsx`
|
|
8
|
+
- Rendered docs page: `/components/accordion`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'src/components/molecules/accordion/Accordion'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `type` | `'single' \| 'multiple'` | | Required on the root. single: opening one section closes the other. multiple: any number can be open. |
|
|
21
|
+
| `collapsible` | `boolean` | `false` | single mode only. Lets the open section be closed again by its own trigger. You'll want it almost always. |
|
|
22
|
+
| `variant` | `'plain' \| 'boxed'` | `'plain'` | plain: thin separator lines, for a long run of questions. boxed: a panel per section, for a few sections that each stand alone. |
|
|
23
|
+
| `value (Root)` | `string \| string[]` | | The open section when the page controls it: a string in single mode, an array in multiple mode. |
|
|
24
|
+
| `defaultValue` | `string \| string[]` | | The section or sections open at first, when the accordion keeps track itself (uncontrolled). Set on the root. |
|
|
25
|
+
| `onValueChange` | `(value: string \| string[]) => void` | | Called when a section opens or closes (on the root). The argument has the same shape as type. |
|
|
26
|
+
| `value (Item)` | `string` | | Required. The section’s own value. |
|
|
27
|
+
| `disabled (Item)` | `boolean` | `false` | The section can't be opened or closed. |
|
|
28
|
+
| `level (Trigger)` | `2 \| 3 \| 4 \| 5 \| 6` | `3` | The level of the heading the trigger sits in. Pick one below the heading the accordion appears under. |
|
|
29
|
+
| `forceMount (Content)` | `true` | | Keeps a closed panel in the page's code (the DOM, the browser's live model of the page), so the browser's find-in-page search can reach its content. It then skips the closing animation. |
|
|
30
|
+
| `className` | `string` | | Merged with each part’s classes. On AccordionContent it goes on the inner padded box, not the animated clipping box around it. |
|
|
31
|
+
| `ref` | `Ref<HTMLElement>` | | Passed through to the DOM element. Every part has one; the trigger’s is the button. |
|
|
32
|
+
|
|
33
|
+
## Types and helpers
|
|
34
|
+
|
|
35
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export type AccordionVariant = 'plain' | 'boxed'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
44
|
+
|
|
45
|
+
### Frequently asked questions (FAQ): plain style, one answer open at a time, and the open one can be closed again
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<div className="w-full max-w-lg">
|
|
49
|
+
<Accordion type="single" collapsible>
|
|
50
|
+
<AccordionItem value="shipping">
|
|
51
|
+
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
|
|
52
|
+
<AccordionContent>
|
|
53
|
+
Two to four working days inside the EU. Every parcel leaves the depot with a tracking
|
|
54
|
+
number, sent to you by email the same day.
|
|
55
|
+
</AccordionContent>
|
|
56
|
+
</AccordionItem>
|
|
57
|
+
<AccordionItem value="returns">
|
|
58
|
+
<AccordionTrigger>Can I return a replica?</AccordionTrigger>
|
|
59
|
+
<AccordionContent>
|
|
60
|
+
Within 30 days, unfired and in its original box. Open a return from your order page and
|
|
61
|
+
print the label.
|
|
62
|
+
</AccordionContent>
|
|
63
|
+
</AccordionItem>
|
|
64
|
+
<AccordionItem value="age">
|
|
65
|
+
<AccordionTrigger>Do I need to prove my age?</AccordionTrigger>
|
|
66
|
+
<AccordionContent>
|
|
67
|
+
Yes, once. We check an ID document at the first order of a replica; accessories need no
|
|
68
|
+
check.
|
|
69
|
+
</AccordionContent>
|
|
70
|
+
</AccordionItem>
|
|
71
|
+
<AccordionItem value="custom" disabled>
|
|
72
|
+
<AccordionTrigger>Custom builds (paused)</AccordionTrigger>
|
|
73
|
+
<AccordionContent>The workshop takes no new builds this season.</AccordionContent>
|
|
74
|
+
</AccordionItem>
|
|
75
|
+
</Accordion>
|
|
76
|
+
</div>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Settings sections: boxed style, several open at once, and each section header is a level-2 heading (h2)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
function SettingRow({ id, label, defaultChecked }: { id: string; label: string; defaultChecked?: boolean }) {
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex items-center justify-between gap-4 py-2">
|
|
85
|
+
<label htmlFor={id} className="text-sm text-[var(--color-text-primary)]">
|
|
86
|
+
{label}
|
|
87
|
+
</label>
|
|
88
|
+
<Switch id={id} defaultChecked={defaultChecked} />
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
<div className="w-full max-w-lg">
|
|
94
|
+
<Accordion type="multiple" variant="boxed" defaultValue={['notifications']}>
|
|
95
|
+
<AccordionItem value="notifications">
|
|
96
|
+
<AccordionTrigger level={2}>Notifications</AccordionTrigger>
|
|
97
|
+
<AccordionContent>
|
|
98
|
+
<SettingRow id="acc-set-shipping" label="Shipping updates" defaultChecked />
|
|
99
|
+
<SettingRow id="acc-set-restock" label="Restock alerts" />
|
|
100
|
+
<SettingRow id="acc-set-news" label="Monthly newsletter" />
|
|
101
|
+
</AccordionContent>
|
|
102
|
+
</AccordionItem>
|
|
103
|
+
<AccordionItem value="privacy">
|
|
104
|
+
<AccordionTrigger level={2}>Privacy</AccordionTrigger>
|
|
105
|
+
<AccordionContent>
|
|
106
|
+
<SettingRow id="acc-set-profile" label="Show my squad profile" defaultChecked />
|
|
107
|
+
<SettingRow id="acc-set-history" label="Keep order history" defaultChecked />
|
|
108
|
+
</AccordionContent>
|
|
109
|
+
</AccordionItem>
|
|
110
|
+
<AccordionItem value="display">
|
|
111
|
+
<AccordionTrigger level={2}>Display</AccordionTrigger>
|
|
112
|
+
<AccordionContent>
|
|
113
|
+
<SettingRow id="acc-set-motion" label="Reduce motion" />
|
|
114
|
+
</AccordionContent>
|
|
115
|
+
</AccordionItem>
|
|
116
|
+
</Accordion>
|
|
117
|
+
</div>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Controlled: the page keeps track of which sections are open
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
function ControlledExample() {
|
|
124
|
+
const [open, setOpen] = useState<string[]>(['battery'])
|
|
125
|
+
return (
|
|
126
|
+
<div className="w-full max-w-lg">
|
|
127
|
+
<p className="mb-3 font-mono text-xs uppercase tracking-[0.18em] text-[var(--color-text-secondary)]">
|
|
128
|
+
Open: {open.length ? open.join(', ') : 'none'}
|
|
129
|
+
</p>
|
|
130
|
+
<Accordion type="multiple" value={open} onValueChange={setOpen}>
|
|
131
|
+
<AccordionItem value="battery">
|
|
132
|
+
<AccordionTrigger>Battery</AccordionTrigger>
|
|
133
|
+
<AccordionContent>11.1V LiPo, 1450mAh, with a deans connector.</AccordionContent>
|
|
134
|
+
</AccordionItem>
|
|
135
|
+
<AccordionItem value="hop-up">
|
|
136
|
+
<AccordionTrigger>Hop-up</AccordionTrigger>
|
|
137
|
+
<AccordionContent>Rotary adjustment, 60 degree bucking.</AccordionContent>
|
|
138
|
+
</AccordionItem>
|
|
139
|
+
</Accordion>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
<ControlledExample />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Keyboard
|
|
148
|
+
|
|
149
|
+
| Keys | Result |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `Tab` | Moves between triggers, and into an open section’s content. |
|
|
152
|
+
| `Enter` `Space` | Opens or closes the focused section. |
|
|
153
|
+
| `↓` `↑` | Goes to the next or previous trigger, wrapping round at the ends and skipping disabled ones. |
|
|
154
|
+
| `Home` `End` | Goes to the first or last trigger. |
|
|
155
|
+
|
|
156
|
+
## Do
|
|
157
|
+
|
|
158
|
+
- Use it for content people scan by its headings and open one or two of: an FAQ, a group of settings, long specifications.
|
|
159
|
+
- Write each trigger as the question or the section name, so the closed list reads like a table of contents.
|
|
160
|
+
- Set collapsible in single mode.
|
|
161
|
+
- Use boxed for a few sections that each hold controls, and plain for a long list of text answers.
|
|
162
|
+
|
|
163
|
+
## Don't
|
|
164
|
+
|
|
165
|
+
- Don't hide the one thing everyone needs inside a closed section. Show it.
|
|
166
|
+
- Don't use it for steps in a sequence. That's Stepper.
|
|
167
|
+
- Don't use it to switch between equal views of the same thing. That's Tabs.
|
|
168
|
+
- Don't nest an accordion inside another accordion's content.
|
|
169
|
+
|
|
170
|
+
## Common mistakes
|
|
171
|
+
|
|
172
|
+
Wrong:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<Accordion type="single">
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Right:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Accordion type="single" collapsible>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Without collapsible, a single-mode accordion can never be fully closed again once a section is open. That surprises people.
|
|
185
|
+
|
|
186
|
+
Wrong:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<AccordionTrigger>Shipping</AccordionTrigger> // under an h3 section title
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Right:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<AccordionTrigger level={4}>Shipping</AccordionTrigger>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Every trigger (the clickable section header) sits inside a heading, h3 (a level-3 heading) by default. Set level so the triggers nest one level below the heading they appear under.
|
|
199
|
+
|
|
200
|
+
## Anatomy
|
|
201
|
+
|
|
202
|
+
- **Accordion**: The root. It holds which sections are open and the variant, and every part below reads both.
|
|
203
|
+
- **AccordionItem**: One section. plain: a thin line under it. boxed: its own raised panel with the cut corner. The panel is drawn on a backing layer marked aria-hidden (hidden from screen readers), so the item itself is never clipped.
|
|
204
|
+
- **AccordionTrigger**: A button inside a heading of the level you choose. It uses the display typeface, has a chevron that turns half a revolution when the section is open, and is underlined on hover.
|
|
205
|
+
- **AccordionContent**: A region labelled by its trigger, in secondary text. It grows open over the base duration and shrinks shut faster (M7). When someone has asked their device for reduced motion, it snaps open and shut instead.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# AiLabel
|
|
2
|
+
|
|
3
|
+
> Marks content as made by AI, and on request says why. Given an explanation, the mark is a button that opens a short note right under it, with an optional list of sources. It is a disclosure (a button that shows and hides a region), not a tooltip or a popover: a tooltip can't hold links and never shows on a touch screen, and the system has no popover.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Marking a block of text, a value or a field that an AI wrote, so the person knows to check it.
|
|
6
|
+
- **Not for:** A status or a category: use Badge. A field the AI filled: give the field aiValue, which marks it itself.
|
|
7
|
+
- Atom. Source: `src/components/atoms/ai-label/AiLabel.tsx`
|
|
8
|
+
- Rendered docs page: `/components/ai-label`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AiLabel } from 'src/components/atoms/ai-label/AiLabel'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `label` | `string` | `'Generated by AI'` | The mark's words, and the button's name. Say what the AI made: 'AI summary', 'AI draft'. |
|
|
21
|
+
| `explanation` | `ReactNode` | | Why this is AI content and what to do about it, in a sentence or two: what it was made from, and that it needs checking. Without it the mark is static text and nothing opens. |
|
|
22
|
+
| `sources` | `AiLabelSource[]` | | { label, href } for each thing the content was made from. Listed under the explanation; ignored without one. |
|
|
23
|
+
| `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | How each source link is drawn. Pass your router's link component for sources inside the app. |
|
|
24
|
+
| `className` | `string` | | Merged with the root's classes. When Tailwind utility classes clash, the later one wins. |
|
|
25
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the root div. |
|
|
26
|
+
| `...rest` | `HTMLAttributes<HTMLDivElement>` | | Every native attribute, onto the root div. |
|
|
27
|
+
|
|
28
|
+
## Types and helpers
|
|
29
|
+
|
|
30
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export type AiLabelSource = { label: string; href: string }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
39
|
+
|
|
40
|
+
### Static mark
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<AiLabel />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### With an explanation and sources
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<div className="flex flex-col gap-2 max-w-md">
|
|
50
|
+
<AiLabel
|
|
51
|
+
label="AI summary"
|
|
52
|
+
explanation="Summarised from the three most recent reviews of this replica. It can miss details or get them wrong: verify before you rely on it."
|
|
53
|
+
sources={[
|
|
54
|
+
{ label: 'Review by Ilse Varga', href: '#review-1' },
|
|
55
|
+
{ label: 'Review by Tomas Brandt', href: '#review-2' },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
<p className="text-sm text-[var(--color-text-primary)]">
|
|
59
|
+
Reviewers find the gearbox quiet and the hop-up easy to tune; two mention a loose stock.
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Keyboard
|
|
65
|
+
|
|
66
|
+
| Keys | Result |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `Tab` | Moves focus to the mark when it has an explanation, then into any source links once open. |
|
|
69
|
+
| `Enter` `Space` | Opens or closes the explanation. Focus stays on the button. |
|
|
70
|
+
|
|
71
|
+
## Do
|
|
72
|
+
|
|
73
|
+
- Put it right above or beside the AI content it marks, not in a page header far away.
|
|
74
|
+
- Say in the explanation what the AI used and that the person should check it, the way GitLab's 'Verify before use' does.
|
|
75
|
+
- Name the sources when there are any. Showing where it came from is how a reader decides whether to trust it.
|
|
76
|
+
|
|
77
|
+
## Don't
|
|
78
|
+
|
|
79
|
+
- Don't put it inside a <label>. A button inside a label is read into the field's name; put it beside the label.
|
|
80
|
+
- Don't mark everything. A label on every line is a label on nothing.
|
|
81
|
+
- Don't rely on the colour. The words say AI; ai.text only helps you find them.
|
|
82
|
+
|
|
83
|
+
## Anatomy
|
|
84
|
+
|
|
85
|
+
- **Mark**: A sparkle and the label in uppercase mono, in ai.text. Static text when there is no explanation.
|
|
86
|
+
- **Disclosure button**: With an explanation, the mark is a button with aria-expanded and a chevron that turns when open. Its name is the label.
|
|
87
|
+
- **Explanation**: A region right under the button, edged in ai.border, in text.secondary. Hidden until asked for, then next in the reading order.
|
|
88
|
+
- **Sources**: Optional. A list of links under the explanation, drawn through renderLink.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# AlertDialog
|
|
2
|
+
|
|
3
|
+
> Asks for confirmation before something with real consequences. It shows a question, says what will happen, and offers Cancel and the action. Screen readers announce it as an alert, a click outside won't dismiss it, and focus starts on Cancel. If the action returns a promise (a value that finishes later, such as a server call), the dialog stays open and shows it's busy until that work is done.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Confirming an action that matters and can't be undone, such as a delete.
|
|
6
|
+
- **Not for:** Something that can be undone: just do it and offer Undo in a Toast. A form or a task: use Dialog.
|
|
7
|
+
- Organism. Source: `src/components/organisms/alert-dialog/AlertDialog.tsx`
|
|
8
|
+
- Rendered docs page: `/components/alert-dialog`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogFooter, AlertDialogTrigger } from 'src/components/organisms/alert-dialog/AlertDialog'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `open (AlertDialog)` | `boolean` | | Whether it is open, when your code controls that (controlled mode). |
|
|
21
|
+
| `defaultOpen (AlertDialog)` | `boolean` | `false` | Whether it starts open, when the component tracks its own state (uncontrolled mode). |
|
|
22
|
+
| `onOpenChange (AlertDialog)` | `(open: boolean) => void` | | Called on every open and close. It isn't called when someone tries to close it while the action is busy. |
|
|
23
|
+
| `asChild (Trigger)` | `boolean` | | Uses its single child as the trigger instead of adding its own button. Pass a Button. |
|
|
24
|
+
| `ref (Trigger)` | `Ref<HTMLButtonElement>` | | The trigger button. |
|
|
25
|
+
| `title (Content)` | `string` | | Required. The question, and the name screen readers say. |
|
|
26
|
+
| `description (Content)` | `string` | | Required. What confirming does. Screen readers read it as the accessible description. |
|
|
27
|
+
| `size (Content)` | `'sm' \| 'md'` | `'md'` | Maximum width: 24rem or 32rem (rem is a unit based on the root text size, usually 16 pixels). |
|
|
28
|
+
| `className (Content)` | `string` | | Added to the panel classes. Where two Tailwind utility classes clash, the later one wins. |
|
|
29
|
+
| `ref (Content)` | `Ref<HTMLDivElement>` | | The element with the role alertdialog. |
|
|
30
|
+
| `onPointerDownOutside / onInteractOutside (Content)` | `—` | | Not available: a click outside never dismisses an alert dialog. |
|
|
31
|
+
| `className (Footer)` | `string` | | Added to the button row classes. |
|
|
32
|
+
| `children (Cancel)` | `ReactNode` | `'Cancel'` | The label. Name what the person keeps, for example Keep editing. |
|
|
33
|
+
| `ref (Cancel)` | `Ref<HTMLButtonElement>` | | The Cancel button, which gets focus when the dialog opens. |
|
|
34
|
+
| `tone (Action)` | `'hostile' \| 'default'` | `'hostile'` | hostile (the danger button) for something that can't be undone. default (the primary button) for a confirm that's safe but still matters. |
|
|
35
|
+
| `onClick (Action)` | `(event) => void \| Promise<unknown>` | | Runs on confirm, then closes the dialog. If it returns a promise, the dialog stays open and busy until the promise settles: success closes it, failure keeps it open. Calling event.preventDefault() also keeps it open. |
|
|
36
|
+
| `loading (Action)` | `boolean` | `false` | Marks it busy from outside, for work you track yourself. It holds the dialog open the same way a pending promise does. |
|
|
37
|
+
| `loadingLabel (Action)` | `string` | `'Working'` | Text you can't see that screen readers announce while it's busy. |
|
|
38
|
+
| `ref (Action)` | `Ref<HTMLButtonElement>` | | The action button. |
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
43
|
+
|
|
44
|
+
### Delete while the request is still running: the action button shows it's busy and the dialog waits
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const request = (fail = false) =>
|
|
48
|
+
new Promise<void>((resolve, reject) =>
|
|
49
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
function DeleteExample() {
|
|
53
|
+
const { toast } = useToast()
|
|
54
|
+
return (
|
|
55
|
+
<AlertDialog>
|
|
56
|
+
<AlertDialogTrigger asChild>
|
|
57
|
+
<Button variant="danger">Delete depot</Button>
|
|
58
|
+
</AlertDialogTrigger>
|
|
59
|
+
<AlertDialogContent
|
|
60
|
+
title="Delete Brasov north?"
|
|
61
|
+
description="Its 12 open manifests move to the unassigned queue. The depot cannot be restored."
|
|
62
|
+
>
|
|
63
|
+
<AlertDialogFooter>
|
|
64
|
+
<AlertDialogCancel />
|
|
65
|
+
<AlertDialogAction
|
|
66
|
+
loadingLabel="Deleting depot"
|
|
67
|
+
onClick={() => request().then(() => toast({ tone: 'ok', title: 'Depot deleted' }))}
|
|
68
|
+
>
|
|
69
|
+
Delete depot
|
|
70
|
+
</AlertDialogAction>
|
|
71
|
+
</AlertDialogFooter>
|
|
72
|
+
</AlertDialogContent>
|
|
73
|
+
</AlertDialog>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
<DeleteExample />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### A failed request: the dialog stays open and a toast says why
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const request = (fail = false) =>
|
|
84
|
+
new Promise<void>((resolve, reject) =>
|
|
85
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
function FailureExample() {
|
|
89
|
+
const { toast } = useToast()
|
|
90
|
+
return (
|
|
91
|
+
<AlertDialog>
|
|
92
|
+
<AlertDialogTrigger asChild>
|
|
93
|
+
<Button variant="secondary">Revoke API key</Button>
|
|
94
|
+
</AlertDialogTrigger>
|
|
95
|
+
<AlertDialogContent
|
|
96
|
+
title="Revoke the warehouse key?"
|
|
97
|
+
description="Scripts that use it stop working at once. This demo always fails, to show the dialog staying open."
|
|
98
|
+
size="sm"
|
|
99
|
+
>
|
|
100
|
+
<AlertDialogFooter>
|
|
101
|
+
<AlertDialogCancel>Keep key</AlertDialogCancel>
|
|
102
|
+
<AlertDialogAction
|
|
103
|
+
loadingLabel="Revoking key"
|
|
104
|
+
onClick={() =>
|
|
105
|
+
request(true).catch((error: Error) => {
|
|
106
|
+
toast({ tone: 'hostile', title: 'Key not revoked', description: error.message })
|
|
107
|
+
throw error
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
Revoke key
|
|
112
|
+
</AlertDialogAction>
|
|
113
|
+
</AlertDialogFooter>
|
|
114
|
+
</AlertDialogContent>
|
|
115
|
+
</AlertDialog>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
<FailureExample />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### A safe confirm that still has consequences, with the default tone
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
function DefaultToneExample() {
|
|
126
|
+
return (
|
|
127
|
+
<AlertDialog>
|
|
128
|
+
<AlertDialogTrigger asChild>
|
|
129
|
+
<Button>Publish prices</Button>
|
|
130
|
+
</AlertDialogTrigger>
|
|
131
|
+
<AlertDialogContent
|
|
132
|
+
title="Publish the autumn prices?"
|
|
133
|
+
description="Customers see them on every replica page as soon as you publish."
|
|
134
|
+
size="sm"
|
|
135
|
+
>
|
|
136
|
+
<AlertDialogFooter>
|
|
137
|
+
<AlertDialogCancel>Not yet</AlertDialogCancel>
|
|
138
|
+
<AlertDialogAction tone="default">Publish</AlertDialogAction>
|
|
139
|
+
</AlertDialogFooter>
|
|
140
|
+
</AlertDialogContent>
|
|
141
|
+
</AlertDialog>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
<DefaultToneExample />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Type the name to confirm, for the costliest actions
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
const request = (fail = false) =>
|
|
152
|
+
new Promise<void>((resolve, reject) =>
|
|
153
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const ACCOUNT = 'northfield-supply'
|
|
157
|
+
|
|
158
|
+
function TypeToConfirmExample() {
|
|
159
|
+
const [typed, setTyped] = useState('')
|
|
160
|
+
const matches = typed.trim() === ACCOUNT
|
|
161
|
+
return (
|
|
162
|
+
<AlertDialog onOpenChange={() => setTyped('')}>
|
|
163
|
+
<AlertDialogTrigger asChild>
|
|
164
|
+
<Button variant="danger">Delete account</Button>
|
|
165
|
+
</AlertDialogTrigger>
|
|
166
|
+
<AlertDialogContent
|
|
167
|
+
title="Delete the account?"
|
|
168
|
+
description="Its 212 orders, saved kit lists and team access are removed for good."
|
|
169
|
+
>
|
|
170
|
+
<FormField label="Account name" help={`Type ${ACCOUNT} to enable Delete account.`}>
|
|
171
|
+
<Input
|
|
172
|
+
value={typed}
|
|
173
|
+
onChange={event => setTyped(event.target.value)}
|
|
174
|
+
autoComplete="off"
|
|
175
|
+
spellCheck={false}
|
|
176
|
+
className="font-mono"
|
|
177
|
+
/>
|
|
178
|
+
</FormField>
|
|
179
|
+
<AlertDialogFooter>
|
|
180
|
+
<AlertDialogCancel />
|
|
181
|
+
<AlertDialogAction disabled={!matches} loadingLabel="Deleting account" onClick={() => request()}>
|
|
182
|
+
Delete account
|
|
183
|
+
</AlertDialogAction>
|
|
184
|
+
</AlertDialogFooter>
|
|
185
|
+
</AlertDialogContent>
|
|
186
|
+
</AlertDialog>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
<TypeToConfirmExample />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Keyboard
|
|
194
|
+
|
|
195
|
+
| Keys | Result |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `Tab` | Moves between Cancel, the action and any field, looping around inside the dialog. |
|
|
198
|
+
| `Enter` | Presses the focused button. Focus starts on Cancel, so an accidental Enter cancels. |
|
|
199
|
+
| `Esc` | Cancels and sends focus back to whatever opened it. It waits while the action is busy. |
|
|
200
|
+
|
|
201
|
+
## Do
|
|
202
|
+
|
|
203
|
+
- Write the title as the question, with the thing it's about: 'Delete Brasov north?', not 'Are you sure?'.
|
|
204
|
+
- Say in the description exactly what will be lost or changed, with numbers where you have them.
|
|
205
|
+
- Label the action with the verb and the thing: 'Delete depot', never 'OK' or 'Yes'.
|
|
206
|
+
- Return the promise from onClick for anything that calls a server.
|
|
207
|
+
- For something very costly, add a field asking for the name, and disable the action until it matches (see the last example).
|
|
208
|
+
|
|
209
|
+
## Don't
|
|
210
|
+
|
|
211
|
+
- Don't use it for something that can be undone. Do the thing and offer Undo in a toast.
|
|
212
|
+
- Don't use it for a form or a task. That's a Dialog.
|
|
213
|
+
- Don't add a close button or close it on a click outside. The only choices are Cancel or the action.
|
|
214
|
+
- Don't open one from a dropdown menu item. The item is gone by the time the dialog closes, so focus has nowhere to go back to.
|
|
215
|
+
|
|
216
|
+
## Common mistakes
|
|
217
|
+
|
|
218
|
+
Wrong:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<Dialog>
|
|
222
|
+
<DialogContent title="Delete depot?">
|
|
223
|
+
<DialogFooter>
|
|
224
|
+
<DialogClose asChild><Button variant="secondary">Cancel</Button></DialogClose>
|
|
225
|
+
<Button variant="danger" onClick={remove}>Delete</Button>
|
|
226
|
+
</DialogFooter>
|
|
227
|
+
</DialogContent>
|
|
228
|
+
</Dialog>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Right:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
<AlertDialog>
|
|
235
|
+
<AlertDialogContent title="Delete depot?" description="Its open manifests move to the unassigned queue.">
|
|
236
|
+
<AlertDialogFooter>
|
|
237
|
+
<AlertDialogCancel />
|
|
238
|
+
<AlertDialogAction onClick={remove}>Delete depot</AlertDialogAction>
|
|
239
|
+
</AlertDialogFooter>
|
|
240
|
+
</AlertDialogContent>
|
|
241
|
+
</AlertDialog>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Dialog has the role dialog (the label screen readers use to describe it), closes when someone clicks the scrim behind it, and focuses the first control. A destructive confirm needs the role alertdialog, no dismissal from outside, and focus on Cancel. AlertDialog does all three.
|
|
245
|
+
|
|
246
|
+
Wrong:
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
const [busy, setBusy] = useState(false)
|
|
250
|
+
<AlertDialog open={open} onOpenChange={next => { if (!busy) setOpen(next) }}>
|
|
251
|
+
...
|
|
252
|
+
<AlertDialogAction loading={busy} onClick={async event => {
|
|
253
|
+
event.preventDefault(); setBusy(true); await remove(); setBusy(false); setOpen(false)
|
|
254
|
+
}}>Delete</AlertDialogAction>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Right:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
<AlertDialogAction onClick={() => remove()}>Delete</AlertDialogAction>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Return the promise. The action goes busy, Cancel and Escape wait, and the dialog closes when the work succeeds or stays open when it fails. You don't need any state of your own.
|
|
264
|
+
|
|
265
|
+
Wrong:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<AlertDialogAction onClick={() => remove()}>Delete</AlertDialogAction>
|
|
269
|
+
// remove() rejects, and nothing tells the person
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Right:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
<AlertDialogAction onClick={() => remove().catch(error => {
|
|
276
|
+
toast({ tone: 'hostile', title: 'Not deleted', description: error.message })
|
|
277
|
+
throw error
|
|
278
|
+
})}>Delete</AlertDialogAction>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
When the promise fails, the dialog stays open but shows nothing on its own. Report the failure, then throw the error again so the dialog stays open for a retry or a Cancel.
|
|
282
|
+
|
|
283
|
+
## Anatomy
|
|
284
|
+
|
|
285
|
+
- **Scrim**: The dimmed layer over the page, coloured surface.scrim. The page under it is inert (it can't be clicked or focused) and hidden from screen readers. Unlike Dialog, clicking it does nothing: the person has to choose.
|
|
286
|
+
- **Surface**: The same panel as Dialog: surface.base, a strong hairline border, the corner crosshair and the M7 motion for opening and closing. Its role is alertdialog.
|
|
287
|
+
- **Title**: Required. The question. It's an h2 (a second-level heading) and the dialog's accessible name, the name screen readers say for it.
|
|
288
|
+
- **Description**: Required. What confirming does. Screen readers announce it with the title.
|
|
289
|
+
- **Body**: Optional. A list of what will be lost, or a field to type a name into.
|
|
290
|
+
- **Footer**: Cancel first, then the action. There's no close button.
|