@hegemonart/get-design-done 1.14.8 → 1.16.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/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +15 -5
- package/CHANGELOG.md +97 -0
- package/README.md +30 -0
- package/SKILL.md +4 -1
- package/agents/a11y-mapper.md +25 -0
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/agents/design-auditor.md +92 -8
- package/agents/design-context-builder.md +6 -0
- package/agents/design-executor.md +5 -2
- package/agents/design-pattern-mapper.md +2 -0
- package/agents/design-verifier.md +11 -0
- package/agents/motion-mapper.md +45 -0
- package/agents/token-mapper.md +36 -0
- package/agents/visual-hierarchy-mapper.md +29 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +16 -2
- package/reference/anti-patterns.md +69 -0
- package/reference/audit-scoring.md +34 -3
- package/reference/brand-voice.md +199 -0
- package/reference/checklists.md +30 -3
- package/reference/components/README.md +90 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/button.md +195 -0
- package/reference/components/card.md +200 -0
- package/reference/components/checkbox.md +207 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/input.md +208 -0
- package/reference/components/label.md +200 -0
- package/reference/components/link.md +193 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/select-combobox.md +219 -0
- package/reference/components/switch.md +194 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/data/google-fonts.csv +51 -0
- package/reference/data/palettes.csv +41 -0
- package/reference/data/styles.csv +39 -0
- package/reference/design-system-guidance.md +177 -0
- package/reference/design-systems-catalog.md +151 -0
- package/reference/framer-motion-patterns.md +411 -0
- package/reference/gestalt.md +219 -0
- package/reference/iconography.md +231 -0
- package/reference/motion.md +102 -0
- package/reference/palette-catalog.md +82 -0
- package/reference/performance.md +304 -0
- package/reference/registry.json +359 -28
- package/reference/registry.schema.json +2 -1
- package/reference/review-format.md +2 -2
- package/reference/style-vocabulary.md +62 -0
- package/reference/surfaces.md +114 -0
- package/reference/typography.md +80 -0
- package/reference/visual-hierarchy-layout.md +306 -0
- package/skills/benchmark/SKILL.md +105 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Card — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Polaris, Carbon, Atlassian, Mantine, shadcn/ui, Ant Design, Fluent 2
|
|
4
|
+
**Wave**: 2 · **Category**: Containers
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A card is a contained surface that groups related information and actions. It is a visual container, not a navigation element by default — only make the entire card clickable when the primary action is navigation and there is a single dominant action. Mixed-content cards with multiple actions should not be entirely clickable. *(Material 3, Polaris, Carbon all agree on this boundary)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌────────────────────────────┐ ← card surface (border/shadow/background)
|
|
18
|
+
│ [Media / Image] │ ← optional, always with alt text
|
|
19
|
+
│────────────────────────────│
|
|
20
|
+
│ Eyebrow / Category │ ← optional; 12px/600 uppercase
|
|
21
|
+
│ Title │ ← primary label; ≥16px
|
|
22
|
+
│ Description │ ← supporting text; 14px/400
|
|
23
|
+
│────────────────────────────│
|
|
24
|
+
│ [Action 1] [Action 2] │ ← optional action area
|
|
25
|
+
└────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Part | Required | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| Container | Yes | `<article>` for standalone content; `<div>` for layout |
|
|
31
|
+
| Title | Yes | Descriptive; h2/h3 depending on hierarchy |
|
|
32
|
+
| Content | Yes | Body text, metadata, or media |
|
|
33
|
+
| Media / image | No | Always provide `alt`; decorative images use `alt=""` |
|
|
34
|
+
| Actions | No | Keep ≤2 primary actions per card |
|
|
35
|
+
| Footer / metadata | No | 12px/400; secondary information |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
| Variant | Description | Systems |
|
|
42
|
+
|---------|-------------|---------|
|
|
43
|
+
| Elevated | Drop shadow; floats above surface | Material 3, shadcn |
|
|
44
|
+
| Outlined | Border, no shadow | Material 3, Carbon, Polaris |
|
|
45
|
+
| Filled | Filled background, no shadow or border | Material 3, Mantine |
|
|
46
|
+
| Clickable / Interactive | Entire card is a link or button | Material 3, Polaris, Carbon |
|
|
47
|
+
| Horizontal | Media left, content right | Carbon, Polaris, Atlassian |
|
|
48
|
+
| Compact | Dense layout; no media | Carbon, Fluent |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥5/18): outlined or elevated; ≤2 actions per card.
|
|
51
|
+
**Diverge**: elevation vs. outline — both are valid; use elevated for content that needs to float (dashboards), outlined for dense lists (tables of cards).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Trigger | Visual | ARIA/HTML |
|
|
58
|
+
|-------|---------|--------|-----------|
|
|
59
|
+
| default | — | Resting surface | — |
|
|
60
|
+
| hover (clickable) | pointer | Shadow deepens or border darkens | — |
|
|
61
|
+
| focus (clickable) | keyboard | 2px focus ring on outer card | — |
|
|
62
|
+
| active (clickable) | mousedown | Scale 0.99 | — |
|
|
63
|
+
| selected | programmatic | Border + background tint | `aria-selected="true"` |
|
|
64
|
+
| loading | async content | Skeleton placeholder | `aria-busy="true"` |
|
|
65
|
+
| disabled | programmatic | 38% opacity | `aria-disabled="true"` |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Sizing & Spacing
|
|
70
|
+
|
|
71
|
+
| Property | Value | Notes |
|
|
72
|
+
|----------|-------|-------|
|
|
73
|
+
| Padding | 16px (sm), 20px (md), 24px (lg) | *(Carbon: 16px default, Material 3: 16px)* |
|
|
74
|
+
| Border radius | Use token; match `reference/surfaces.md` concentric rule | |
|
|
75
|
+
| Media aspect ratio | 16:9 (landscape), 1:1 (square) | `object-fit: cover` |
|
|
76
|
+
| Min width | 240px | Prevents content collapse |
|
|
77
|
+
| Max width | 480px (typical) | Grid controls actual width; max-width is a guideline |
|
|
78
|
+
| Gap between cards | 16px (sm grid), 24px (md grid) | |
|
|
79
|
+
|
|
80
|
+
Cross-link: `reference/surfaces.md` — concentric radius, 3-layer shadow formula, elevation tokens
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Typography
|
|
85
|
+
|
|
86
|
+
- Title: 16–20px/600 depending on card prominence (h2/h3 in DOM hierarchy)
|
|
87
|
+
- Eyebrow: 11px/600 uppercase, muted colour
|
|
88
|
+
- Body: 14px/400
|
|
89
|
+
- Metadata: 12px/400 muted
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Keyboard & Accessibility
|
|
94
|
+
|
|
95
|
+
> **WAI-ARIA role**: No specific card role. Use `<article>` for independent pieces of content; `<li>` in a list of cards; `<div>` for layout grouping.
|
|
96
|
+
|
|
97
|
+
### Clickable Card Rules
|
|
98
|
+
|
|
99
|
+
*Per WAI-ARIA APG link + button patterns — W3C — 2024*
|
|
100
|
+
|
|
101
|
+
| Invocation | Element | Key |
|
|
102
|
+
|------------|---------|-----|
|
|
103
|
+
| Navigate to new page | `<a href>` wrapping or inside card | Enter |
|
|
104
|
+
| Trigger action in context | `<button>` wrapping or inside card | Enter, Space |
|
|
105
|
+
|
|
106
|
+
- **Do not wrap an entire card in `<a>` if it contains other interactive elements** (links, buttons inside) — nested interactive elements are inaccessible by keyboard
|
|
107
|
+
- Use the "card with primary action + secondary actions" pattern: one `<a>` stretched via `::after` pseudo-element to fill the card; secondary action buttons sit above in stacking context
|
|
108
|
+
|
|
109
|
+
### Accessibility Rules
|
|
110
|
+
|
|
111
|
+
- Card with image: always provide `alt`; use `alt=""` for decorative images
|
|
112
|
+
- Clickable card: heading inside card should be the accessible name (via `aria-labelledby` or the stretched-link pattern)
|
|
113
|
+
- Card grid: use `role="list"` on the grid container and `role="listitem"` on each card, or a semantic `<ul>/<li>` structure, so screen readers announce item count
|
|
114
|
+
- Loading skeleton: add `aria-busy="true"` on the card container; remove when content loads
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Motion
|
|
119
|
+
|
|
120
|
+
| Transition | Duration | Easing | Notes |
|
|
121
|
+
|------------|----------|--------|-------|
|
|
122
|
+
| hover shadow | 150ms | ease-out | Elevation increase |
|
|
123
|
+
| press scale | 80ms | ease | 1→0.99 (subtle; card is large) |
|
|
124
|
+
| skeleton shimmer | 1.5s | linear loop | Respect `prefers-reduced-motion` |
|
|
125
|
+
|
|
126
|
+
Cross-link: `reference/motion.md` — Skeleton shimmer pattern, `prefers-reduced-motion`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Do / Don't
|
|
131
|
+
|
|
132
|
+
### Do
|
|
133
|
+
- Use `<article>` when the card is an independent, self-contained piece of content *(Carbon, Polaris)*
|
|
134
|
+
- Keep clickable cards to a single primary action; surface secondary actions as explicit buttons *(Material 3, Polaris)*
|
|
135
|
+
- Use the stretched-link (`::after`) pattern for clickable cards with nested links/buttons *(Carbon, Bootstrap pattern)*
|
|
136
|
+
- Provide `alt` text for all card images, or `alt=""` for decorative images *(WCAG 1.1.1)*
|
|
137
|
+
|
|
138
|
+
### Don't
|
|
139
|
+
- Don't wrap the entire card in `<a>` if it contains other interactive elements *(WAI-ARIA APG)*
|
|
140
|
+
- Don't use `<div>` as a clickable card without `role="button"` or `role="link"` + keyboard handler *(WAI-ARIA APG)*
|
|
141
|
+
- Don't place the entire card title in a plain `<span>` when it could be `<h2>/<h3>` *(Atlassian, Carbon)*
|
|
142
|
+
- Don't use more than 2 primary actions per card — extract to a detail view *(Material 3, Polaris)*
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Anti-patterns Cross-links
|
|
147
|
+
|
|
148
|
+
| Anti-pattern | Entry |
|
|
149
|
+
|--------------|-------|
|
|
150
|
+
| Nested interactive elements in clickable container | `reference/anti-patterns.md` |
|
|
151
|
+
| Missing alt on card media | `reference/anti-patterns.md` |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Benchmark Citations
|
|
156
|
+
|
|
157
|
+
| Claim | Sources |
|
|
158
|
+
|-------|---------|
|
|
159
|
+
| ≤2 actions per card | Material 3, Polaris, Carbon |
|
|
160
|
+
| Stretched-link pattern for nested interactivity | Carbon, Bootstrap |
|
|
161
|
+
| article element for standalone card content | Carbon, Polaris |
|
|
162
|
+
| 16px default padding | Carbon, Material 3 |
|
|
163
|
+
|
|
164
|
+
Full system URLs: `connections/design-corpora.md`
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Grep Signatures
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Entire card wrapped in <a> with nested buttons/links
|
|
172
|
+
grep -rn '<a ' src/ | grep -i 'card' | xargs grep -l 'button\|<a ' 2>/dev/null
|
|
173
|
+
|
|
174
|
+
# Clickable div without role
|
|
175
|
+
grep -rn 'class.*card\|data-testid.*card' src/ | grep 'onClick\|on:click' | grep -v 'role='
|
|
176
|
+
|
|
177
|
+
# Card image without alt
|
|
178
|
+
grep -rn '<img' src/ | grep -i 'card' | grep -v 'alt='
|
|
179
|
+
|
|
180
|
+
# Missing heading hierarchy in card
|
|
181
|
+
grep -rn 'class.*card' src/ | xargs grep -L 'h[1-6]\|aria-label' 2>/dev/null
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Failing Example
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<!-- BAD: entire card is <a> but contains a button — button is inaccessible by keyboard in most AT -->
|
|
190
|
+
<a href="/product/42" class="card">
|
|
191
|
+
<img src="product.jpg" />
|
|
192
|
+
<h3>Product Name</h3>
|
|
193
|
+
<p>Description…</p>
|
|
194
|
+
<button onclick="addToCart(42)">Add to cart</button>
|
|
195
|
+
</a>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why it fails**: Nested `<button>` inside `<a>` is invalid HTML. Keyboard users pressing Tab inside the link may reach the button, but Enter on the button may also trigger the link. Screen reader behavior is unpredictable.
|
|
199
|
+
**Grep detection**: `grep -rn '<a.*href' src/ | xargs grep -l '<button' 2>/dev/null`
|
|
200
|
+
**Fix**: Use the stretched-link pattern — `<h3><a href="/product/42">Product Name</a></h3>` with `::after { position: absolute; inset: 0; }` on the `<a>`, and position the "Add to cart" button above via `position: relative; z-index: 1`.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Checkbox — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Polaris, Ant Design, WAI-ARIA APG, Mantine, Chakra UI, Atlassian
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A checkbox allows the user to select or deselect a binary option, or to represent an indeterminate state (partially selected group). Checkboxes are independent — selecting one does not affect others. Use radio buttons for mutually exclusive choices within a group. Always group related checkboxes in a `<fieldset>` with a `<legend>`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌──┐ Label text
|
|
18
|
+
│✓ │ ← <input type="checkbox" id="x"> (or role="checkbox")
|
|
19
|
+
└──┘ Helper text (opt.)
|
|
20
|
+
Error message (opt.)
|
|
21
|
+
|
|
22
|
+
Group:
|
|
23
|
+
<fieldset>
|
|
24
|
+
<legend>Preferences</legend>
|
|
25
|
+
[checkbox] Option A
|
|
26
|
+
[checkbox] Option B
|
|
27
|
+
[checkbox] Option C (indeterminate)
|
|
28
|
+
</fieldset>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Part | Required | Notes |
|
|
32
|
+
|------|----------|-------|
|
|
33
|
+
| Input / control | Yes | Native `<input type="checkbox">` preferred |
|
|
34
|
+
| Label | Yes | `<label for="id">` — click zone includes label text |
|
|
35
|
+
| Fieldset + legend | Yes (group) | Required when ≥2 related checkboxes |
|
|
36
|
+
| Helper text | No | Below label; `aria-describedby` |
|
|
37
|
+
| Error message | Conditional | Field-level or group-level |
|
|
38
|
+
| Indeterminate indicator | Conditional | Dash/minus mark; set via `.indeterminate = true` (JS) |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Variants
|
|
43
|
+
|
|
44
|
+
| Variant | Description | Systems |
|
|
45
|
+
|---------|-------------|---------|
|
|
46
|
+
| Default | Binary checked / unchecked | All |
|
|
47
|
+
| Indeterminate | Partial selection indicator (parent of group) | Material 3, Carbon, Ant, Mantine |
|
|
48
|
+
| Standalone | Single checkbox (e.g. "I agree to terms") | All |
|
|
49
|
+
| Group | ≥2 checkboxes in `<fieldset>` | All |
|
|
50
|
+
| With description | Label + helper text below | Material 3, Polaris, Carbon |
|
|
51
|
+
|
|
52
|
+
**Norm** (≥6/18): indeterminate state is a visual-only UI state — the underlying `checked` value is still boolean; set via DOM `.indeterminate` property, not HTML attribute.
|
|
53
|
+
**Diverge**: size — Material 3 uses 18px; Carbon 16px; Polaris 16px; Ant 14–16px. 16px is the de-facto norm.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## States
|
|
58
|
+
|
|
59
|
+
| State | Trigger | Visual | ARIA |
|
|
60
|
+
|-------|---------|--------|------|
|
|
61
|
+
| unchecked | default | Empty box | `aria-checked="false"` |
|
|
62
|
+
| checked | user interaction | Checkmark | `aria-checked="true"` |
|
|
63
|
+
| indeterminate | set via JS | Dash / minus | `aria-checked="mixed"` |
|
|
64
|
+
| hover | pointer over | Box border darkens | — |
|
|
65
|
+
| focus | keyboard | 2px focus ring around box | — |
|
|
66
|
+
| disabled unchecked | `disabled` | 38% opacity | `aria-disabled="true"` |
|
|
67
|
+
| disabled checked | `disabled` | 38% opacity + check | `aria-disabled="true"` |
|
|
68
|
+
| error | validation | Red box border | `aria-invalid="true"` |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Sizing & Spacing
|
|
73
|
+
|
|
74
|
+
| Property | Value | Notes |
|
|
75
|
+
|----------|-------|-------|
|
|
76
|
+
| Control size | 16×16px (20px touch target via pseudo-element) | *(Carbon, Polaris, Material 3)* |
|
|
77
|
+
| Gap: control → label | 8px | |
|
|
78
|
+
| Label min click zone | Full row width | Increases tap target |
|
|
79
|
+
| Group item spacing | 8px vertical between items | *(Carbon, Material 3)* |
|
|
80
|
+
| Indentation (nested) | 24px | When showing hierarchical groups |
|
|
81
|
+
|
|
82
|
+
Cross-link: `reference/surfaces.md` — hit-area pseudo-element pattern (44×44px minimum touch target)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Typography
|
|
87
|
+
|
|
88
|
+
- Label: 14px/400; same weight as body — checkboxes are options, not headings
|
|
89
|
+
- Legend: 14px/500 or 12px/600 uppercase — distinguishes group from items
|
|
90
|
+
- Helper/error: 12px/400
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Keyboard & Accessibility
|
|
95
|
+
|
|
96
|
+
> **WAI-ARIA role**: `checkbox` (implicit on `<input type="checkbox">`)
|
|
97
|
+
> **Required attributes**: `aria-checked` (if not native); `aria-describedby` for helper/error
|
|
98
|
+
|
|
99
|
+
### Keyboard Contract
|
|
100
|
+
|
|
101
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ — W3C — 2024*
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| Tab | Moves focus to the checkbox |
|
|
106
|
+
| Space | Toggles the checkbox state (checked / unchecked / indeterminate) |
|
|
107
|
+
|
|
108
|
+
Within a group: Tab moves between checkboxes (not arrow keys — checkboxes are independent).
|
|
109
|
+
|
|
110
|
+
### Accessibility Rules
|
|
111
|
+
|
|
112
|
+
- Label MUST be associated via `<label for="id">` — clicking the label must toggle the checkbox
|
|
113
|
+
- `<fieldset>` + `<legend>` MUST wrap every group of related checkboxes — the legend provides group context to screen readers
|
|
114
|
+
- Indeterminate state MUST be set via JS `.indeterminate = true` — there is no HTML attribute; `aria-checked="mixed"` must be set simultaneously on `role="checkbox"` elements
|
|
115
|
+
- Disabled checkboxes: use native `disabled` attribute for form semantics; `aria-disabled="true"` if the element must remain in tab order (e.g. with explanatory tooltip)
|
|
116
|
+
- Error state: `aria-invalid="true"` on the control; group-level error on the `<fieldset>` via `aria-describedby`
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Motion
|
|
121
|
+
|
|
122
|
+
| Transition | Duration | Easing | Notes |
|
|
123
|
+
|------------|----------|--------|-------|
|
|
124
|
+
| check fill | 120ms | ease-out | SVG path draw or scale from center |
|
|
125
|
+
| indeterminate dash | 120ms | ease | Width animation of dash element |
|
|
126
|
+
| hover border | 80ms | ease | Border colour only |
|
|
127
|
+
|
|
128
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip path animation, show fill instantly
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Do / Don't
|
|
133
|
+
|
|
134
|
+
### Do
|
|
135
|
+
- Use `<fieldset>` + `<legend>` for every group *(WAI-ARIA APG, Carbon, Polaris)*
|
|
136
|
+
- Set indeterminate via `.indeterminate = true` AND `aria-checked="mixed"` *(WAI-ARIA APG, Mantine)*
|
|
137
|
+
- Make the entire label row clickable, not just the box *(Material 3, Carbon, Polaris)*
|
|
138
|
+
- Align label text to the top of the control in multiline label scenarios *(Carbon)*
|
|
139
|
+
|
|
140
|
+
### Don't
|
|
141
|
+
- Don't use checkboxes for mutually exclusive options — use radio buttons *(Material 3, Carbon, Polaris)*
|
|
142
|
+
- Don't use a custom `<div>` checkbox without `role="checkbox"` and keyboard handler *(WAI-ARIA APG)*
|
|
143
|
+
- Don't set `aria-checked="mixed"` via HTML attribute — it must be set dynamically *(WAI-ARIA APG)*
|
|
144
|
+
- Don't rely on colour alone for checked state — always include a visible checkmark *(WCAG 1.4.1)*
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Anti-patterns Cross-links
|
|
149
|
+
|
|
150
|
+
| Anti-pattern | Entry |
|
|
151
|
+
|--------------|-------|
|
|
152
|
+
| Custom checkbox without role | `reference/anti-patterns.md` |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Benchmark Citations
|
|
157
|
+
|
|
158
|
+
| Claim | Sources |
|
|
159
|
+
|-------|---------|
|
|
160
|
+
| Space toggles checkbox | WAI-ARIA APG §3.5 |
|
|
161
|
+
| fieldset+legend required for group | WAI-ARIA APG, Carbon, Polaris |
|
|
162
|
+
| .indeterminate = true (JS only) | WAI-ARIA APG, MDN |
|
|
163
|
+
| 16px control size | Carbon, Polaris, Material 3 |
|
|
164
|
+
|
|
165
|
+
Full system URLs: `connections/design-corpora.md`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Grep Signatures
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Custom checkbox div/span without role="checkbox"
|
|
173
|
+
grep -rn 'class.*checkbox\|type.*checkbox' src/ | grep '<div\|<span' | grep -v 'role='
|
|
174
|
+
|
|
175
|
+
# Missing label association
|
|
176
|
+
grep -rn 'type="checkbox"' src/ | grep -v 'id=\|aria-label'
|
|
177
|
+
|
|
178
|
+
# Group without fieldset
|
|
179
|
+
grep -rn 'checkbox' src/ | grep -v 'fieldset\|role="group"'
|
|
180
|
+
|
|
181
|
+
# Indeterminate set via attribute instead of JS
|
|
182
|
+
grep -rn 'indeterminate' src/ | grep 'setAttribute\|attr(' | grep '"true"'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Failing Example
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<!-- BAD: checkboxes in a group without fieldset/legend — group context lost for screen readers -->
|
|
191
|
+
<div>
|
|
192
|
+
<p>Notification preferences</p>
|
|
193
|
+
<input type="checkbox" id="email"> <label for="email">Email</label>
|
|
194
|
+
<input type="checkbox" id="sms"> <label for="sms">SMS</label>
|
|
195
|
+
</div>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why it fails**: Screen readers announce each option without the group context ("Email — checkbox") — users don't know what these options belong to without reading surrounding text.
|
|
199
|
+
**Grep detection**: `grep -B5 'type="checkbox"' src/ | grep -v 'fieldset\|role="group"'`
|
|
200
|
+
**Fix**:
|
|
201
|
+
```html
|
|
202
|
+
<fieldset>
|
|
203
|
+
<legend>Notification preferences</legend>
|
|
204
|
+
<input type="checkbox" id="email"> <label for="email">Email</label>
|
|
205
|
+
<input type="checkbox" id="sms"> <label for="sms">SMS</label>
|
|
206
|
+
</fieldset>
|
|
207
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Drawer / Sheet — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Polaris (Sheet), Carbon, Atlassian, Mantine, shadcn/ui, Headless UI, Apple HIG
|
|
4
|
+
**Wave**: 2 · **Category**: Containers
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A drawer (or sheet) is a panel that slides in from an edge of the viewport. It is less disruptive than a modal for workflows that benefit from co-existing with the background — detail panels, navigation menus, filter sidebars, multi-step flows. Like a modal, it traps focus and requires Escape to close. Unlike a modal, the backdrop is optional and can be semi-transparent. *(Material 3, Carbon, Polaris all position drawer as less disruptive than modal)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Side drawer (right):
|
|
18
|
+
┌────────────────┬──────────────┐
|
|
19
|
+
│ │ [✕] Title │
|
|
20
|
+
│ Page │──────────────│
|
|
21
|
+
│ content │ Body │
|
|
22
|
+
│ (inert) │ content │
|
|
23
|
+
│ │──────────────│
|
|
24
|
+
│ │ [Actions] │
|
|
25
|
+
└────────────────┴──────────────┘
|
|
26
|
+
|
|
27
|
+
Bottom sheet (mobile):
|
|
28
|
+
┌──────────────────────────────┐
|
|
29
|
+
│ Page content │
|
|
30
|
+
├──────────────────────────────┤ ← handle / drag indicator
|
|
31
|
+
│ Sheet content │ ← slides up; partial height
|
|
32
|
+
└──────────────────────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Part | Required | Notes |
|
|
36
|
+
|------|----------|-------|
|
|
37
|
+
| Panel container | Yes | `role="dialog"` + `aria-modal="true"` |
|
|
38
|
+
| Title | Yes | `id` → `aria-labelledby` on panel |
|
|
39
|
+
| Close button | Yes | Top-right; keyboard accessible |
|
|
40
|
+
| Backdrop | Conditional | Semi-transparent; may click-to-close (configurable) |
|
|
41
|
+
| Drag handle | No | Bottom sheet only; swipe gesture affordance |
|
|
42
|
+
| Scroll container | Conditional | Body scrollable when content exceeds height |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Variants
|
|
47
|
+
|
|
48
|
+
| Variant | Direction | Use case | Systems |
|
|
49
|
+
|---------|-----------|----------|---------|
|
|
50
|
+
| Right side | Slides from right | Detail panels, settings | All |
|
|
51
|
+
| Left side | Slides from left | Navigation menus | Material 3, Carbon |
|
|
52
|
+
| Bottom sheet | Slides from bottom | Mobile actions, filters | Material 3, Apple HIG |
|
|
53
|
+
| Top | Slides from top | Notifications, alerts | Rare; avoid |
|
|
54
|
+
| Full-height | 100vh, pushes content | Persistent navigation | Material 3 |
|
|
55
|
+
| Partial height | 60–80vh, overlays | Mobile bottom sheet | Apple HIG, Material 3 |
|
|
56
|
+
|
|
57
|
+
**Norm** (≥5/18): right-side is default; bottom sheet for mobile.
|
|
58
|
+
**Diverge**: backdrop-click-to-close — same debate as modal; for navigation drawers, backdrop click should close; for form/detail drawers, configurable.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## States
|
|
63
|
+
|
|
64
|
+
Same as Modal/Dialog — see `modal-dialog.md`. Key differences:
|
|
65
|
+
|
|
66
|
+
| State | Drawer-specific |
|
|
67
|
+
|-------|-----------------|
|
|
68
|
+
| open | Slides in from edge; `aria-expanded="true"` on trigger (nav drawer) |
|
|
69
|
+
| closed | Slides out; `aria-expanded="false"` |
|
|
70
|
+
| partial (bottom sheet) | Dragged to partial height; swipe-up to expand |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Sizing & Spacing
|
|
75
|
+
|
|
76
|
+
| Variant | Width / Height | Notes |
|
|
77
|
+
|---------|---------------|-------|
|
|
78
|
+
| Right side | 400–480px (desktop), 100% (mobile) | `min-width: 280px` |
|
|
79
|
+
| Left side (nav) | 240–320px | 256px is common (Carbon, Material 3) |
|
|
80
|
+
| Bottom sheet | 60–100vh | Drag handle at 12px × 36px |
|
|
81
|
+
| Padding | 20–24px | Match modal padding |
|
|
82
|
+
|
|
83
|
+
Cross-link: `reference/surfaces.md` — shadow on drawer edge (unilateral shadow)
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Typography
|
|
88
|
+
|
|
89
|
+
- Title: 16–18px/600
|
|
90
|
+
- Body: 14px/400
|
|
91
|
+
- Section headers within body: 12px/600 uppercase muted
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Keyboard & Accessibility
|
|
96
|
+
|
|
97
|
+
> **WAI-ARIA role**: `dialog` (same as modal — drawer is a type of dialog)
|
|
98
|
+
> **Required attributes**: `aria-modal="true"`, `aria-labelledby` (title id)
|
|
99
|
+
|
|
100
|
+
### Keyboard Contract
|
|
101
|
+
|
|
102
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ — W3C — 2024*
|
|
103
|
+
|
|
104
|
+
Same Tab/Shift+Tab/Escape contract as modal (see `modal-dialog.md`).
|
|
105
|
+
|
|
106
|
+
### Drawer-specific Accessibility Rules
|
|
107
|
+
|
|
108
|
+
- Focus trap: MUST trap focus inside the drawer while open — same as modal
|
|
109
|
+
- On open: focus moves to first focusable element (or close button if no primary action)
|
|
110
|
+
- On close: focus MUST return to the element that triggered the drawer open
|
|
111
|
+
- Navigation drawer (`role="navigation"`): if the drawer IS the main nav, use `role="navigation"` + `aria-label="Main"` instead of `role="dialog"`; different keyboard contract (no focus trap — it IS a landmark)
|
|
112
|
+
- Background `inert`: set `inert` attribute (or equivalent) on background content when drawer is open
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Motion
|
|
117
|
+
|
|
118
|
+
| Transition | Duration | Easing | Notes |
|
|
119
|
+
|------------|----------|--------|-------|
|
|
120
|
+
| Slide in (right) | 250ms | ease-out (cubic-bezier 0.4,0,0.2,1) | |
|
|
121
|
+
| Slide out (right) | 200ms | ease-in | |
|
|
122
|
+
| Bottom sheet expand | 300ms | spring (bounce: 0) | |
|
|
123
|
+
| Backdrop fade | 200ms | ease | opacity 0→0.4 |
|
|
124
|
+
|
|
125
|
+
Swipe-to-close (bottom sheet): detect `pointerup` with velocity + displacement threshold.
|
|
126
|
+
Cross-link: `reference/motion.md` — spring bounce=0, `prefers-reduced-motion` (disable slide, instant toggle)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Do / Don't
|
|
131
|
+
|
|
132
|
+
### Do
|
|
133
|
+
- Trap focus inside the drawer when open — same rule as modal *(WAI-ARIA APG)*
|
|
134
|
+
- Return focus to the trigger element on close *(WAI-ARIA APG, Radix, Mantine)*
|
|
135
|
+
- Use right-side drawer for content-detail panels; left-side for navigation *(Material 3, Carbon)*
|
|
136
|
+
- Support swipe-to-close on bottom sheets for mobile *(Apple HIG, Material 3)*
|
|
137
|
+
|
|
138
|
+
### Don't
|
|
139
|
+
- Don't use `role="navigation"` for content drawers — only for navigation-purpose drawers *(WAI-ARIA APG)*
|
|
140
|
+
- Don't let Tab escape the drawer while it's open *(WAI-ARIA APG)*
|
|
141
|
+
- Don't disable background scroll without setting `overflow:hidden` on body *(all systems)*
|
|
142
|
+
- Don't slide from top for content — top drawers conflict with browser UI and notifications *(Material 3)*
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Anti-patterns Cross-links
|
|
147
|
+
|
|
148
|
+
| Anti-pattern | Entry |
|
|
149
|
+
|--------------|-------|
|
|
150
|
+
| Focus escaping drawer | `reference/anti-patterns.md` |
|
|
151
|
+
| No focus return on close | `reference/anti-patterns.md` |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Benchmark Citations
|
|
156
|
+
|
|
157
|
+
| Claim | Sources |
|
|
158
|
+
|-------|---------|
|
|
159
|
+
| role="dialog" for content drawers | WAI-ARIA APG, Radix |
|
|
160
|
+
| Focus trap required | WAI-ARIA APG |
|
|
161
|
+
| Swipe-to-close on bottom sheet | Apple HIG, Material 3 |
|
|
162
|
+
| 256px left nav width | Carbon, Material 3 |
|
|
163
|
+
| ease-out 250ms slide-in | Material 3 motion spec |
|
|
164
|
+
|
|
165
|
+
Full system URLs: `connections/design-corpora.md`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Grep Signatures
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Drawer without focus trap
|
|
173
|
+
grep -rn 'drawer\|sheet\|sidebar' src/ | grep -L 'FocusTrap\|focus-trap\|inert'
|
|
174
|
+
|
|
175
|
+
# Drawer missing aria-modal
|
|
176
|
+
grep -rn 'class.*drawer\|class.*sheet' src/ | grep 'role="dialog"' | grep -v 'aria-modal'
|
|
177
|
+
|
|
178
|
+
# Background scroll not prevented
|
|
179
|
+
grep -rn 'drawer.*open\|isOpen.*drawer' src/ | grep -v 'overflow\|body\.'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Failing Example
|
|
185
|
+
|
|
186
|
+
```jsx
|
|
187
|
+
// BAD: drawer panel with no focus management — Tab escapes to background
|
|
188
|
+
function Drawer({ isOpen }) {
|
|
189
|
+
return isOpen ? (
|
|
190
|
+
<div className="drawer-panel">
|
|
191
|
+
<button onClick={close}>✕</button>
|
|
192
|
+
<h2>Settings</h2>
|
|
193
|
+
<SettingsForm />
|
|
194
|
+
</div>
|
|
195
|
+
) : null;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Why it fails**: No `role="dialog"`, no `aria-modal`, no focus trap. Tab navigates freely into the background while the drawer is open. Escape does nothing.
|
|
200
|
+
**Grep detection**: `grep -rn 'class.*drawer\|class.*panel' src/ | grep -v 'role=\|aria-modal'`
|
|
201
|
+
**Fix**: Use Radix `<Dialog>` with `data-side` variant, or Vaul (drawer library), which handles focus trap, Escape, portal, and `aria-modal` automatically.
|