@ngrr/ds 0.1.13 → 0.1.16
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 +11 -155
- package/AI.md +1289 -1285
- package/CLAUDE.md +5 -450
- package/dist/components/atoms/DataTableHeaderCell/DataTableHeaderCell.d.ts +1 -1
- package/dist/components/atoms/Tag/Tag.d.ts +15 -4
- package/dist/components/organisms/AlertDialog/AlertDialog.d.ts +1 -2
- package/dist/ds-nagarro.es.js +13745 -14382
- package/dist/ds-nagarro.umd.js +83 -112
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/AI.md
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
# DS-Nagarro — AI Agent Rules
|
|
2
2
|
|
|
3
|
-
> **Single source of truth for AI agents
|
|
3
|
+
> **Single source of truth for AI agents building apps with `@ngrr/ds`.**
|
|
4
4
|
> Compiled from: `foundations.md`, `ds-guidelines.md`, all component docs in `docs/`, and `accessibility-audit-2026-02-28.md`.
|
|
5
5
|
> When in doubt, defer to the specific component doc file.
|
|
6
|
+
>
|
|
7
|
+
> **This file is for consuming the package.** For building components inside the library itself,
|
|
8
|
+
> read `AGENTS.md` instead.
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
## How to use this file
|
|
10
13
|
|
|
11
|
-
1. **Before
|
|
12
|
-
2. **Before
|
|
13
|
-
3. **
|
|
14
|
-
4. **
|
|
15
|
-
5.
|
|
14
|
+
1. **Before writing any code**, read the [Setup](#setup--mandatory-css-imports) section and implement the required imports and CSS.
|
|
15
|
+
2. **Before building any page**, read the [AppShell](#appshell--mandatory-root-layout), [Navigation rules](#navigation--page-structure-rules), and [Layout rules](#layout--component-behavior-rules).
|
|
16
|
+
3. **Before selecting a component**, check the [Cross-component patterns](#cross-component-patterns) section to determine the correct component from a decision tree.
|
|
17
|
+
4. **Before implementing a component**, read its section in [Component usage rules](#component-usage-rules) for all props, states, tokens, ARIA requirements, and content rules.
|
|
18
|
+
5. **All code must comply** with [Accessibility requirements](#accessibility-requirements) and avoid everything in [What NOT to do](#what-not-to-do).
|
|
19
|
+
6. **All token references** must use CSS custom properties: `var(--token-name)`. Never hardcode values.
|
|
20
|
+
7. Token naming: Figma slash paths → hyphenated CSS vars. `background/interactive/cta/default` → `var(--background-interactive-cta-default)`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Setup — mandatory CSS imports
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// main.tsx — must be first, before any other imports
|
|
28
|
+
import '@ngrr/ds/style.css'
|
|
29
|
+
import '@ngrr/ds/tokens.css'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Required CSS on the host app
|
|
33
|
+
```css
|
|
34
|
+
html, body { margin: 0; height: 100%; }
|
|
35
|
+
#root { min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
36
|
+
```
|
|
16
37
|
|
|
17
38
|
---
|
|
18
39
|
|
|
@@ -38,9 +59,11 @@ Primitives (raw) → Semantic (intent) → Component (usage)
|
|
|
38
59
|
| `--background-*` | `background-color` |
|
|
39
60
|
| `--borders-*` | `border-color`, `outline-color` |
|
|
40
61
|
| `--color-surface-*` | `background-color` (component-level) |
|
|
62
|
+
| `--color-dataviz-*` | all chart/graph colors — never use semantic tokens or hex for dataviz |
|
|
41
63
|
| `--font-size-*`, `--font-weight-*`, `--font-line-height-*` | typography |
|
|
42
64
|
| `--space-*` | `gap`, `margin` (between elements) |
|
|
43
65
|
| `--inset-*` | `padding` (inside components) |
|
|
66
|
+
| `--page-margin-x` | `padding-inline` on all containers |
|
|
44
67
|
| `--radius-*` | `border-radius` |
|
|
45
68
|
| `--border-width-*` | `border-width` |
|
|
46
69
|
| `--effects-elevation-*` | `box-shadow` |
|
|
@@ -84,1605 +107,1741 @@ Always use these exact identifiers. Never use `base`, `normal`, or `rest`.
|
|
|
84
107
|
|
|
85
108
|
---
|
|
86
109
|
|
|
87
|
-
##
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
### Button
|
|
92
|
-
|
|
93
|
-
**Applies to: `Button` (Main, Destructive, Toggle, Vertical)**
|
|
110
|
+
## AppShell — mandatory root layout
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
`AppShell` is the only permitted root layout wrapper. Every page must be inside it. Do not invent wrapper divs or custom layouts.
|
|
113
|
+
```tsx
|
|
114
|
+
import { AppShell, Navbar, Sidebar, Button, SearchBar, Avatar } from '@ngrr/ds';
|
|
115
|
+
// Icons come from lucide-react — install separately: npm install lucide-react
|
|
116
|
+
import { LayoutDashboard, FolderKanban, CheckSquare, Users, FileText, Settings } from 'lucide-react';
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
<AppShell
|
|
119
|
+
header={
|
|
120
|
+
<Navbar
|
|
121
|
+
title="Dashboard"
|
|
122
|
+
rightActions={<Button variant="primary" label="New project" />}
|
|
123
|
+
/>
|
|
124
|
+
}
|
|
125
|
+
sidebar={
|
|
126
|
+
<Sidebar
|
|
127
|
+
selectorProps={{
|
|
128
|
+
label: 'WorkScope',
|
|
129
|
+
avatarProps: {
|
|
130
|
+
size: 'xs',
|
|
131
|
+
variant: 'organization',
|
|
132
|
+
fill: 'initials',
|
|
133
|
+
initials: 'WS',
|
|
134
|
+
name: 'WorkScope',
|
|
135
|
+
},
|
|
136
|
+
}}
|
|
137
|
+
showSearch
|
|
138
|
+
searchSlot={<SearchBar placeholder="Search" />}
|
|
139
|
+
sections={[
|
|
140
|
+
{
|
|
141
|
+
id: 'main',
|
|
142
|
+
items: [
|
|
143
|
+
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} />, selected: true, onClick: () => {} },
|
|
144
|
+
{ id: 'projects', label: 'Projects', icon: <FolderKanban size={16} />, onClick: () => {} },
|
|
145
|
+
{ id: 'tasks', label: 'Tasks', icon: <CheckSquare size={16} />, onClick: () => {} },
|
|
146
|
+
{ id: 'people', label: 'People', icon: <Users size={16} />, onClick: () => {} },
|
|
147
|
+
{ id: 'notes', label: 'Notes', icon: <FileText size={16} />, onClick: () => {} },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
]}
|
|
151
|
+
bottomContent={
|
|
152
|
+
<>
|
|
153
|
+
<SidebarMenuSelector
|
|
154
|
+
variant="full"
|
|
155
|
+
label="Settings"
|
|
156
|
+
showIcon
|
|
157
|
+
icon={<Settings size={16} />}
|
|
158
|
+
onClick={() => {}}
|
|
159
|
+
/>
|
|
160
|
+
<SidebarMenuSelector
|
|
161
|
+
variant="full"
|
|
162
|
+
label="User Name"
|
|
163
|
+
showAvatar
|
|
164
|
+
avatarProps={{ size: 'xs', fill: 'initials', initials: 'UN', name: 'User Name' }}
|
|
165
|
+
onClick={() => {}}
|
|
166
|
+
/>
|
|
167
|
+
</>
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
{/* page content goes here */}
|
|
173
|
+
</AppShell>
|
|
174
|
+
```
|
|
101
175
|
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
176
|
+
**AppShell props:**
|
|
177
|
+
- `header` — `<Navbar />` only
|
|
178
|
+
- `sidebar` — `<Sidebar />` only. Never pass `collapsed` manually — AppShell injects it automatically
|
|
179
|
+
- `children` — main page content
|
|
180
|
+
- `rightPanel` — optional, omit if not needed
|
|
181
|
+
- `footer` — optional, omit if not needed
|
|
106
182
|
|
|
107
|
-
|
|
183
|
+
**Forbidden:**
|
|
184
|
+
- Do not wrap AppShell in any other layout element
|
|
185
|
+
- Do not add sidebar items beyond what is specified
|
|
186
|
+
- Do not invent extra sections, items, or nav elements
|
|
108
187
|
|
|
109
|
-
|
|
110
|
-
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'plain';
|
|
111
|
-
type ButtonSize = 'sm' | 'md';
|
|
112
|
-
// plain is only available in sm
|
|
113
|
-
```
|
|
188
|
+
---
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
190
|
+
### Navbar props
|
|
191
|
+
- `variant` — `'title'` (default) for top-level pages | `'breadcrumbs'` for detail views only
|
|
192
|
+
- `title` — page title, sentence case
|
|
193
|
+
- `rightActions` — primary CTA button, rightmost position
|
|
194
|
+
- `leftActions` — secondary actions on the left, omit if not needed
|
|
195
|
+
- `onBackClick` — only used with `variant="breadcrumbs"`
|
|
196
|
+
- `breadcrumbItems` — required only when `variant="breadcrumbs"`
|
|
120
197
|
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
**Rules:**
|
|
199
|
+
- Never show a back arrow on top-level pages (Dashboard, Projects, Tasks, People, Notes)
|
|
200
|
+
- Back arrow only on detail views using `variant="breadcrumbs"`
|
|
201
|
+
- Primary CTA always in `rightActions`
|
|
123
202
|
|
|
124
|
-
|
|
125
|
-
- Do NOT use `aria-pressed` on Main or Destructive buttons.
|
|
126
|
-
- `Loading` → use `aria-busy="true"` on the button.
|
|
127
|
-
- `Disabled` → never rely on opacity alone. Do not disable Primary as a form validation strategy.
|
|
203
|
+
---
|
|
128
204
|
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
205
|
+
### Sidebar props
|
|
206
|
+
- `sections` — required. Only include sections explicitly specified. Never invent extra items.
|
|
207
|
+
- `selectorProps` — workspace selector at the top. Always set `label` and `avatarProps`.
|
|
208
|
+
- `showSearch` — set to `true` to show the search bar
|
|
209
|
+
- `searchSlot` — pass a `<SearchBar />` component when `showSearch` is true
|
|
210
|
+
- `bottomContent` — use for Settings and User Name, pinned to bottom with a divider. Always include these two items.
|
|
211
|
+
- Never pass `collapsed` — AppShell controls it automatically
|
|
134
212
|
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
|
|
213
|
+
**SidebarMenuItem shape:**
|
|
214
|
+
```ts
|
|
215
|
+
{
|
|
216
|
+
id: string;
|
|
217
|
+
label: string; // sentence case always
|
|
218
|
+
icon?: React.ReactNode; // lucide-react icon at size={16}
|
|
219
|
+
selected?: boolean; // true only for the active page
|
|
220
|
+
onClick?: () => void;
|
|
221
|
+
showBadge?: boolean;
|
|
222
|
+
badgeCount?: number;
|
|
223
|
+
badgeVariant?: BadgeVariant;
|
|
224
|
+
showAvatar?: boolean;
|
|
225
|
+
avatarProps?: AvatarProps;
|
|
226
|
+
}
|
|
138
227
|
```
|
|
139
228
|
|
|
140
|
-
#### Content rules
|
|
141
|
-
- Start with a strong verb: "Save", "Delete", "Export"
|
|
142
|
-
- Specific: "Save changes" not "OK" or "Yes"
|
|
143
|
-
- 1–3 words; 5 maximum
|
|
144
|
-
- Sentence case: "Save changes" not "Save Changes"
|
|
145
|
-
- Destructive labels: name what is destroyed. "Delete account" not "Confirm"
|
|
146
|
-
- Loading labels: present-progressive. "Saving…"
|
|
147
|
-
- Toggle `aria-label`: describes the action, not the state. "Bold" not "Bold is active"
|
|
148
|
-
|
|
149
229
|
---
|
|
150
230
|
|
|
151
|
-
###
|
|
231
|
+
### Hard rules — always follow
|
|
232
|
+
- `AppShell` is always the root — no exceptions
|
|
233
|
+
- `DataTable` must never be wrapped in a `Card`
|
|
234
|
+
- No back arrow on top-level pages: Dashboard, Projects, Tasks, People, Notes
|
|
235
|
+
- CTA hierarchy: Primary (object creation, rightmost in Navbar) → Secondary → Ghost (Export, Share, Edit)
|
|
236
|
+
- All labels and copy: sentence case always ("New project" not "New Project")
|
|
237
|
+
- Icons: from `lucide-react` — install with `npm install lucide-react`
|
|
238
|
+
- Tag variants: `success` = completed, `progress` = in progress, `warning` = at risk, `neutral` = not started
|
|
239
|
+
- Settings and User Name always pinned to sidebar bottom via `bottomContent`
|
|
240
|
+
- Sidebar always full viewport height — guaranteed by AppShell CSS rules above
|
|
152
241
|
|
|
153
|
-
|
|
242
|
+
---
|
|
154
243
|
|
|
155
|
-
|
|
156
|
-
- Represents a person or organization where visual identity aids recognition.
|
|
157
|
-
- Do NOT use for generic system processes or when no identity info is available.
|
|
244
|
+
## Navigation & Page Structure Rules
|
|
158
245
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- Never mix Profile and Organization in the same list without clear reason.
|
|
246
|
+
These rules define how pages are structured, how navigation behaves, and where
|
|
247
|
+
actions are placed. They apply to every screen built with DS-Nagarro components.
|
|
248
|
+
Apply these BEFORE selecting or placing any component.
|
|
163
249
|
|
|
164
|
-
|
|
165
|
-
1. `Picture` — real photo/logo
|
|
166
|
-
2. `Initials` — derived from name
|
|
167
|
-
3. `Icon` — anonymous fallback
|
|
250
|
+
---
|
|
168
251
|
|
|
169
|
-
|
|
252
|
+
### NAV-01: AppShell is mandatory
|
|
253
|
+
Every page MUST use `AppShell` as its root layout component.
|
|
254
|
+
Never build page layout manually with divs.
|
|
255
|
+
✅ `<AppShell header={...} sidebar={...}>...</AppShell>`
|
|
256
|
+
❌ `<div style={{ display: 'flex' }}>...</div>`
|
|
170
257
|
|
|
171
|
-
|
|
172
|
-
`xxs` · `xs` · `sm` · `md` (default) · `lg` · `xl` · `xxl`
|
|
258
|
+
---
|
|
173
259
|
|
|
174
|
-
|
|
260
|
+
### NAV-02: Sidebar structure
|
|
261
|
+
The Sidebar always follows this exact structure — top to bottom:
|
|
262
|
+
1. **Workspace switcher** — top of sidebar
|
|
263
|
+
2. **Main navigation items** — middle (product-specific)
|
|
264
|
+
3. **User profile + Settings** — bottom of sidebar
|
|
175
265
|
|
|
176
|
-
|
|
177
|
-
- Sizes: `xxs`, `xs`, `sm` only
|
|
178
|
-
- Show "+" overflow indicator when list exceeds display limit (typically 3–5)
|
|
179
|
-
- Overflow indicator must include full list in `aria-label`
|
|
180
|
-
- Do not use for >20 members without a "view all" affordance
|
|
266
|
+
Never deviate from this order. Never place navigation items above the workspace switcher.
|
|
181
267
|
|
|
182
|
-
|
|
183
|
-
```tsx
|
|
184
|
-
// Individual: meaningful avatar
|
|
185
|
-
<img alt="Ana Costa" aria-label="Ana Costa" />
|
|
268
|
+
---
|
|
186
269
|
|
|
187
|
-
|
|
188
|
-
|
|
270
|
+
### NAV-03: Back arrows — top-level pages
|
|
271
|
+
NEVER show back/forward navigation arrows in the Navbar on top-level section pages.
|
|
272
|
+
Top-level pages are direct sidebar navigation items (e.g. Users, Reports, Pipeline).
|
|
273
|
+
✅ Users page → no back arrow
|
|
274
|
+
✅ Reports page → no back arrow
|
|
275
|
+
❌ Users page → back arrow shown
|
|
189
276
|
|
|
190
|
-
|
|
191
|
-
<div aria-label="Assigned to: Ana Costa, João Silva, and 2 others">
|
|
192
|
-
```
|
|
277
|
+
---
|
|
193
278
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
279
|
+
### NAV-04: Back arrows — detail views
|
|
280
|
+
ONLY show back arrow in Navbar when the user has navigated INTO a detail view
|
|
281
|
+
(i.e. a record, sub-page, or child of a top-level section).
|
|
282
|
+
✅ Alice Johnson's profile page → show back arrow
|
|
283
|
+
✅ Report detail page → show back arrow
|
|
284
|
+
❌ Top-level Users list → no back arrow
|
|
199
285
|
|
|
200
286
|
---
|
|
201
287
|
|
|
202
|
-
###
|
|
288
|
+
### NAV-05: Navbar title — two modes
|
|
203
289
|
|
|
204
|
-
**
|
|
290
|
+
**Mode 1 — Top-level page:**
|
|
291
|
+
Show plain section title in the Navbar. No breadcrumbs.
|
|
292
|
+
✅ Navbar: "Users"
|
|
293
|
+
✅ Navbar: "Reports"
|
|
205
294
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- Label is text/category → use Tag
|
|
212
|
-
- User can select/dismiss it → use Chip
|
|
213
|
-
- Requires user response → use Toast or alert banner
|
|
214
|
-
- Count is zero → **hide the badge**
|
|
215
|
-
|
|
216
|
-
#### Variants
|
|
217
|
-
| Variant | Use |
|
|
218
|
-
|---|---|
|
|
219
|
-
| `Highlight soft` | Low urgency — new content, informational count |
|
|
220
|
-
| `Highlight strong` | Moderate urgency — pending actions, unread items |
|
|
221
|
-
| `Informative` | Neutral — generic counts |
|
|
222
|
-
| `Critical` | Urgent — errors, failures. **Use sparingly.** |
|
|
223
|
-
|
|
224
|
-
#### Sizes
|
|
225
|
-
- `sm`: default — icons, avatars, compact items
|
|
226
|
-
- `lg`: larger host elements
|
|
227
|
-
- `Dot`: presence-only signal. `Highlight strong` and `Critical` only.
|
|
228
|
-
|
|
229
|
-
#### Count display rules
|
|
230
|
-
- ≤99: show exact count
|
|
231
|
-
- >99: show "99+"
|
|
232
|
-
- 0: **remove the badge entirely**
|
|
295
|
+
**Mode 2 — Detail view (2+ levels deep):**
|
|
296
|
+
Show large `Breadcrumbs` component in the Navbar INSTEAD of a plain title.
|
|
297
|
+
The breadcrumb IS the title — do not show both.
|
|
298
|
+
The current page name (rightmost item) is bold and non-interactive.
|
|
299
|
+
Parent items are interactive links.
|
|
233
300
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<button aria-label="Messages, 3 unread" />
|
|
238
|
-
// Dynamic count changes
|
|
239
|
-
<div aria-live="polite" /> // only for meaningful threshold transitions
|
|
240
|
-
```
|
|
301
|
+
✅ Navbar breadcrumb: "Users > **Alice Johnson**"
|
|
302
|
+
❌ Navbar title: "Alice Johnson" + separate breadcrumb below it
|
|
303
|
+
❌ Navbar title: "Users" when inside Alice Johnson's page
|
|
241
304
|
|
|
242
305
|
---
|
|
243
306
|
|
|
244
|
-
###
|
|
245
|
-
|
|
246
|
-
**Applies to: `Tag`**
|
|
247
|
-
|
|
248
|
-
#### When to use
|
|
249
|
-
- Non-interactive categorical label or status on content items.
|
|
250
|
-
- Status: "Active", "Archived", "Pending review"
|
|
251
|
-
- Category: "Design", "Bug", "High"
|
|
252
|
-
|
|
253
|
-
#### Do NOT use when
|
|
254
|
-
- User can interact with it → use Chip
|
|
255
|
-
- It's a numeric count → use Badge
|
|
256
|
-
- Urgent system condition → use Toast
|
|
307
|
+
### NAV-06: Breadcrumb variants — two levels
|
|
257
308
|
|
|
258
|
-
|
|
259
|
-
| Variant | Semantic meaning |
|
|
260
|
-
|---|---|
|
|
261
|
-
| `Highlight` | Notable, not urgent |
|
|
262
|
-
| `Warning` | Caution — needs awareness soon |
|
|
263
|
-
| `Error` | Something is wrong or blocked |
|
|
264
|
-
| `Success` | Completed, approved, healthy |
|
|
265
|
-
| `Neutral` | Purely categorical, no status weight |
|
|
309
|
+
There are two breadcrumb sizes and they serve different purposes:
|
|
266
310
|
|
|
267
|
-
|
|
311
|
+
| Variant | Where | Purpose |
|
|
312
|
+
|---|---|---|
|
|
313
|
+
| **Large** (`BreadcrumbsController`) | Navbar | Main navigation hierarchy — replaces page title on detail views |
|
|
314
|
+
| **Small** (`Breadcrumbs`) | Inside page content | Sub-section hierarchy within a detail page |
|
|
268
315
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
- `sm`: dense layouts, multiple tags per item
|
|
316
|
+
NEVER use small breadcrumbs for main navigation.
|
|
317
|
+
NEVER use large breadcrumbs inside content areas.
|
|
272
318
|
|
|
273
|
-
|
|
319
|
+
✅ Navbar: large breadcrumb "Users > Alice Johnson"
|
|
320
|
+
✅ Inside content: small breadcrumb "Permissions > Edit role"
|
|
321
|
+
❌ Navbar: small breadcrumb
|
|
322
|
+
❌ Content area: large breadcrumb
|
|
274
323
|
|
|
275
|
-
|
|
324
|
+
---
|
|
276
325
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
326
|
+
### NAV-07: Breadcrumb format
|
|
327
|
+
Format: `[Parent section] > [Current page]`
|
|
328
|
+
- Parent items: interactive links
|
|
329
|
+
- Current page (rightmost): non-interactive, visually bold
|
|
330
|
+
- Do NOT always prepend root/Home — start from the immediate meaningful parent
|
|
331
|
+
- Never truncate the current page item
|
|
281
332
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<span id="tag-status" role="status">Error</span>
|
|
287
|
-
```
|
|
333
|
+
✅ "Users > Alice Johnson"
|
|
334
|
+
✅ "Reports > Q4 2025"
|
|
335
|
+
❌ "Home > Users > Alice Johnson" (unnecessary root)
|
|
336
|
+
❌ "Alice Johnson" alone (missing parent context)
|
|
288
337
|
|
|
289
338
|
---
|
|
290
339
|
|
|
291
|
-
###
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
#### When to use
|
|
296
|
-
- User needs to select, activate, or dismiss a discrete value.
|
|
297
|
-
- Filter controls, multi-value input, toggleable options.
|
|
340
|
+
### NAV-08: Primary CTA placement — top-level pages
|
|
341
|
+
The primary action for a section always sits in the Navbar, to the right of the title.
|
|
342
|
+
NEVER place the primary CTA inside a Card header or content area on a top-level page.
|
|
298
343
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
- Mutually exclusive form selection → use Radio
|
|
302
|
-
- Multi-option form selection → use Checkbox
|
|
303
|
-
- Settings toggle → use Switcher
|
|
344
|
+
Primary actions are object-creation actions:
|
|
345
|
+
"New user", "New deal", "Create report", "New email"
|
|
304
346
|
|
|
305
|
-
|
|
306
|
-
-
|
|
307
|
-
- `sm`: compact layouts
|
|
347
|
+
✅ Navbar: "Users" + [New user — Primary button]
|
|
348
|
+
❌ Card header: [New user — Primary button] on a top-level page
|
|
308
349
|
|
|
309
|
-
|
|
350
|
+
---
|
|
310
351
|
|
|
311
|
-
|
|
312
|
-
|
|
352
|
+
### NAV-09: CTA hierarchy in Navbar
|
|
353
|
+
When a page has multiple actions, use this hierarchy in the Navbar (right side):
|
|
313
354
|
|
|
314
|
-
|
|
355
|
+
| Action type | Button variant | Example |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| Primary object creation | `Primary` | "New deal", "New user" |
|
|
358
|
+
| Important but not primary | `Secondary` | "Import", "Publish" |
|
|
359
|
+
| Utility / contextual actions | `Ghost` | "Share", "Edit", "Export" |
|
|
315
360
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
- `Tab` → move between chips
|
|
361
|
+
Order right-to-left by importance: Ghost → Secondary → Primary
|
|
362
|
+
(Primary is always the rightmost — trailing edge)
|
|
319
363
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<div role="group" aria-label="Filter by status">
|
|
324
|
-
<button role="checkbox" aria-checked="true">Active</button>
|
|
325
|
-
<button role="checkbox" aria-checked="false">Closed</button>
|
|
326
|
-
</div>
|
|
364
|
+
✅ [Export — Ghost] [Share — Ghost] [Edit — Secondary] [New user — Primary]
|
|
365
|
+
❌ [New user — Primary] [Share — Ghost] (wrong order)
|
|
366
|
+
❌ Two Primary buttons in the same Navbar
|
|
327
367
|
|
|
328
|
-
|
|
329
|
-
<div role="radiogroup" aria-label="Filter by status">
|
|
330
|
-
<button role="radio" aria-checked="true">Active</button>
|
|
331
|
-
</div>
|
|
368
|
+
---
|
|
332
369
|
|
|
333
|
-
|
|
334
|
-
|
|
370
|
+
### NAV-10: Child section CTAs
|
|
371
|
+
When a detail page has sub-sections with their own primary actions,
|
|
372
|
+
those actions belong at the sub-section level — NOT in the top Navbar.
|
|
335
373
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
374
|
+
✅ Alice Johnson page → Navbar: [Save changes — Primary]
|
|
375
|
+
✅ Alice Johnson > Permissions tab → tab-level: [Add permission — Primary]
|
|
376
|
+
❌ Alice Johnson page → Navbar has both "Save changes" AND "Add permission"
|
|
339
377
|
|
|
340
378
|
---
|
|
341
379
|
|
|
342
|
-
###
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
- Visual divider between content sections that are related but distinct.
|
|
380
|
+
### NAV-11: Section headers inside content
|
|
381
|
+
Use the `SectionHeader` component to divide content sections within a page.
|
|
382
|
+
Section headers are standalone dividers — they do NOT need to be inside a Card.
|
|
383
|
+
Description line is optional — use it when the section needs clarification,
|
|
384
|
+
omit it when the title is self-explanatory.
|
|
348
385
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
386
|
+
✅ SectionHeader "Personal info" (no description needed)
|
|
387
|
+
✅ SectionHeader "Permissions" + description "Control what this user can access"
|
|
388
|
+
❌ Wrapping every section in a Card just to have a title
|
|
389
|
+
❌ Using plain `<h2>` or `<h3>` instead of SectionHeader component
|
|
353
390
|
|
|
354
|
-
|
|
355
|
-
- `Horizontal`: stacked content, spans full container width
|
|
356
|
-
- `Vertical`: side-by-side content, spans full container height
|
|
391
|
+
---
|
|
357
392
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<hr /> // or <div role="separator" />
|
|
393
|
+
### NAV-12: Page title and section header casing
|
|
394
|
+
ALL page titles, Navbar titles, breadcrumb items, and section headers use sentence case.
|
|
395
|
+
NEVER title case.
|
|
362
396
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
```
|
|
397
|
+
✅ "Personal info", "New user", "Alice Johnson", "Save changes"
|
|
398
|
+
❌ "Personal Info", "New User", "Save Changes"
|
|
366
399
|
|
|
367
400
|
---
|
|
368
401
|
|
|
369
|
-
###
|
|
370
|
-
|
|
371
|
-
|
|
402
|
+
### NAV-13: Cards vs sections
|
|
403
|
+
Not every content group needs a Card.
|
|
404
|
+
Use a Card when the content is a distinct, self-contained unit that benefits
|
|
405
|
+
from visual elevation or grouping (e.g. a data table, a form block, a summary panel).
|
|
406
|
+
Use SectionHeader + flat content when sections are part of a continuous page flow.
|
|
372
407
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
408
|
+
✅ DataTable inside a Card
|
|
409
|
+
✅ SectionHeader "Personal info" → flat form fields below (no Card)
|
|
410
|
+
❌ Every section wrapped in a Card by default
|
|
376
411
|
|
|
377
|
-
|
|
378
|
-
- More than 2 states → use Radio or Select
|
|
379
|
-
- Multiple independent options → use Checkbox
|
|
380
|
-
- Requires confirmation before taking effect → use Checkbox
|
|
381
|
-
- Compact toolbar → use Toggle button
|
|
382
|
-
- Choosing between UI views/modes → use Segment control or Tabs
|
|
412
|
+
---
|
|
383
413
|
|
|
384
|
-
|
|
385
|
-
| Component | Effect | Context |
|
|
386
|
-
|---|---|---|
|
|
387
|
-
| **Switcher** | Immediate | Settings with visible label |
|
|
388
|
-
| **Checkbox** | Staged (on submit) | Forms |
|
|
389
|
-
| **Toggle button** | Immediate | Icon-only toolbar |
|
|
414
|
+
## Layout & Component Behavior Rules
|
|
390
415
|
|
|
391
|
-
|
|
392
|
-
- `md`: default
|
|
393
|
-
- `sm`: dense layouts
|
|
416
|
+
These rules apply globally. Apply them before building any screen or component.
|
|
394
417
|
|
|
395
|
-
|
|
418
|
+
---
|
|
396
419
|
|
|
397
|
-
|
|
398
|
-
|
|
420
|
+
### LCB-01: AppShell scroll — only the content area scrolls
|
|
421
|
+
The sidebar and navbar are always fixed. Only the main content area scrolls.
|
|
422
|
+
Never apply scroll to the full page or the AppShell root.
|
|
399
423
|
|
|
400
|
-
|
|
424
|
+
✅ `<AppShell>` — sidebar and navbar fixed; content area overflows and scrolls independently
|
|
425
|
+
❌ Full page scrolls, taking the sidebar and navbar with it
|
|
401
426
|
|
|
402
|
-
|
|
403
|
-
- Always pair with a visible label. **Never use a switcher without a label.**
|
|
404
|
-
- When `Selected=True` + `Disabled=True`: always explain why it's locked.
|
|
405
|
-
- If toggle has latency, show loading indicator alongside — don't leave in ambiguous state.
|
|
427
|
+
---
|
|
406
428
|
|
|
407
|
-
|
|
408
|
-
- `
|
|
429
|
+
### LCB-02: Data visualisation — always use dataviz tokens
|
|
430
|
+
All charts, graphs, and data visualisations must use `--color-dataviz-*` tokens from `tokens.css` for all colors (series, axes, labels, backgrounds).
|
|
431
|
+
Never use hardcoded hex values or generic semantic tokens for dataviz color.
|
|
409
432
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Disabled
|
|
414
|
-
aria-disabled="true" // keep in reading order
|
|
415
|
-
```
|
|
433
|
+
✅ `fill: var(--color-dataviz-1)`
|
|
434
|
+
❌ `fill: #0F766E`
|
|
435
|
+
❌ `fill: var(--background-accent)`
|
|
416
436
|
|
|
417
437
|
---
|
|
418
438
|
|
|
419
|
-
###
|
|
420
|
-
|
|
421
|
-
|
|
439
|
+
### LCB-03: Horizontal spacing — always use `--page-margin-x`
|
|
440
|
+
All horizontal spacing in the content area must use `var(--page-margin-x)`.
|
|
441
|
+
This applies to every container without exception: pages, cards, modals, drawers, tables, lists, and form sections.
|
|
442
|
+
Never introduce independent `padding-left`, `padding-right`, `margin-left`, or `margin-right` values that deviate from this token.
|
|
422
443
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
444
|
+
✅ `padding-inline: var(--page-margin-x)` on content areas, modals, drawers, cards
|
|
445
|
+
❌ `padding: 24px` or `margin: 0 16px` hardcoded on any container
|
|
446
|
+
❌ Different horizontal spacing values across containers causing misalignment
|
|
426
447
|
|
|
427
|
-
|
|
428
|
-
- Only one can be selected → use Radio
|
|
429
|
-
- Effect is immediate → use Switcher
|
|
430
|
-
- Compact token-like pattern → use Chip
|
|
431
|
-
- Single toggle for a setting → use Switcher
|
|
448
|
+
---
|
|
432
449
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
450
|
+
### LCB-04: Card internal padding — content must never touch card edges
|
|
451
|
+
Every card must apply internal padding using the correct inset token.
|
|
452
|
+
Content must never touch the card border.
|
|
436
453
|
|
|
437
|
-
|
|
454
|
+
✅ `padding: var(--inset-large)` inside every card
|
|
455
|
+
❌ Card content flush with card edges
|
|
438
456
|
|
|
439
|
-
|
|
440
|
-
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
457
|
+
---
|
|
441
458
|
|
|
442
|
-
|
|
459
|
+
### LCB-05: Icons — always use Lucide, never system icons
|
|
460
|
+
The only permitted icon library is Lucide (`lucide-react`).
|
|
461
|
+
Never use browser/OS-native icons, emoji, or icons from any other library.
|
|
462
|
+
This applies everywhere: tables, buttons, inputs, menus, empty states, and all other components.
|
|
443
463
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
464
|
+
✅ `import { ArrowUpDown } from 'lucide-react'`
|
|
465
|
+
❌ System sort icon (↕) in table column headers
|
|
466
|
+
❌ Any icon not from the Lucide library
|
|
447
467
|
|
|
448
|
-
|
|
449
|
-
```tsx
|
|
450
|
-
<fieldset>
|
|
451
|
-
<legend>Notification preferences</legend>
|
|
452
|
-
<label>
|
|
453
|
-
<input type="checkbox" aria-checked="true" /> Send weekly summary
|
|
454
|
-
</label>
|
|
455
|
-
</fieldset>
|
|
468
|
+
---
|
|
456
469
|
|
|
457
|
-
|
|
458
|
-
|
|
470
|
+
### LCB-06: List items — always stretch to full container width
|
|
471
|
+
List items inside any container (modal, drawer, card, page section) must stretch to fill the full available width.
|
|
472
|
+
Never let list items float at an arbitrary width or leave unexplained whitespace to the right.
|
|
459
473
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
```
|
|
474
|
+
✅ List item `width: 100%` of container (minus `--page-margin-x` padding)
|
|
475
|
+
❌ List items sized to content, leaving empty space on the right
|
|
463
476
|
|
|
464
477
|
---
|
|
465
478
|
|
|
466
|
-
###
|
|
479
|
+
### LCB-07: Placeholder content — never leave it in place
|
|
480
|
+
Component slots for icons, help text, and hint icons must never contain placeholder values.
|
|
481
|
+
Either replace with real, meaningful content or hide the slot entirely.
|
|
467
482
|
|
|
468
|
-
|
|
483
|
+
This applies to:
|
|
484
|
+
- **Icons** inside inputs, selects, and buttons → replace with a relevant Lucide icon, or hide
|
|
485
|
+
- **Help text** → replace with genuinely useful guidance, or hide
|
|
486
|
+
- **Help/hint icons** → replace with meaningful tooltip content, or hide
|
|
469
487
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
✅ Help text: "We'll use this to send you login notifications."
|
|
489
|
+
✅ Icon slot hidden when no meaningful icon applies
|
|
490
|
+
❌ Help text reads "Help text"
|
|
491
|
+
❌ Placeholder diamond icon left inside a Select component
|
|
474
492
|
|
|
475
|
-
|
|
476
|
-
- Multiple can be selected → use Checkbox
|
|
477
|
-
- More than 6–7 options → use Select
|
|
478
|
-
- Immediate effect → use Switcher or Segment control
|
|
479
|
-
- Single on/off option → use Checkbox or Switcher
|
|
493
|
+
---
|
|
480
494
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
495
|
+
### LCB-08: Required fields — use "Optional" label, not asterisk
|
|
496
|
+
Every form field is mandatory by default.
|
|
497
|
+
Never use a red asterisk (`*`) to mark required fields.
|
|
498
|
+
Only mark optional fields, using an "Optional" label styled in `var(--text-tertiary)`, placed inline after the field label.
|
|
484
499
|
|
|
485
|
-
|
|
500
|
+
✅ `Team <span style="color: var(--text-tertiary)">Optional</span>`
|
|
501
|
+
✅ Fields with no qualifier → assumed mandatory
|
|
502
|
+
❌ `First name *` with a red asterisk
|
|
503
|
+
❌ `aria-required="true"` shown visually as an asterisk
|
|
486
504
|
|
|
487
|
-
|
|
488
|
-
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
505
|
+
---
|
|
489
506
|
|
|
490
|
-
|
|
507
|
+
### LCB-09: Form CTA — disabled until mandatory fields have a value
|
|
508
|
+
The primary submit button in any form (modal, drawer, page) must be disabled until all mandatory fields contain a value.
|
|
509
|
+
On submit, validate all fields and display all errors simultaneously.
|
|
510
|
+
Do not validate in real time as the user types — only on submit attempt.
|
|
491
511
|
|
|
492
|
-
|
|
493
|
-
- Click radio **or its label** to select (full row is hit target).
|
|
494
|
-
- Selecting one deselects all others in the group.
|
|
495
|
-
- Keyboard: `Arrow keys` move selection within group. `Tab` exits group.
|
|
512
|
+
Exceptions where real-time validation is acceptable: password strength, character count limits, search/filter fields, OTP.
|
|
496
513
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
<label>
|
|
501
|
-
<input type="radio" aria-checked="true" /> Option A
|
|
502
|
-
</label>
|
|
503
|
-
</div>
|
|
504
|
-
// Arrow key navigation must move both focus and selection together
|
|
505
|
-
```
|
|
514
|
+
✅ Primary CTA disabled → user fills all mandatory fields → button enables → submit → validate all
|
|
515
|
+
❌ Primary CTA active on an empty form
|
|
516
|
+
❌ Inline errors appearing as the user types in a standard form field
|
|
506
517
|
|
|
507
518
|
---
|
|
508
519
|
|
|
509
|
-
###
|
|
510
|
-
|
|
511
|
-
|
|
520
|
+
### LCB-10: Form submit keyboard shortcut — always show `⌘↵` on primary CTA
|
|
521
|
+
The primary action button in every form must display the `⌘↵` keyboard shortcut hint using the `Shortcut` component.
|
|
522
|
+
`Cmd+Enter` is the standard submit shortcut across all forms.
|
|
512
523
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
- Each section is substantial enough to be its own named panel.
|
|
516
|
-
- 2–7 tabs.
|
|
524
|
+
✅ `<Button variant="primary">Add user <Shortcut>⌘↵</Shortcut></Button>`
|
|
525
|
+
❌ Primary form button with no keyboard shortcut hint
|
|
517
526
|
|
|
518
|
-
|
|
519
|
-
- Sections are sequential steps → use stepper
|
|
520
|
-
- Fewer than 2 tabs
|
|
521
|
-
- More than 6–7 tabs → use sidebar nav or Select
|
|
522
|
-
- Goal is filtering same content → use Chip
|
|
527
|
+
---
|
|
523
528
|
|
|
524
|
-
|
|
525
|
-
| | Segment control | Tab |
|
|
526
|
-
|---|---|---|
|
|
527
|
-
| What changes | How content is rendered | The content itself |
|
|
528
|
-
| Architecture | Display preference | Part of information architecture |
|
|
529
|
-
| URL typically | Does not update | Updates (sub-routes) |
|
|
529
|
+
### Tag semantic color mapping
|
|
530
530
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
- All tabs in a group must be the same type.
|
|
534
|
-
- Tab labels: nouns only. No verbs. "Overview" not "View overview".
|
|
535
|
-
- Disabled tabs: explain via tooltip. Remove if always disabled for all users.
|
|
536
|
-
- Content panel must be directly below/adjacent to tabs — never disconnected.
|
|
537
|
-
- Never nest tabs within tabs.
|
|
531
|
+
Always match Tag variant to the semantic meaning of the value. Never use the
|
|
532
|
+
same variant for all tags in a list.
|
|
538
533
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
534
|
+
| Value meaning | Tag variant |
|
|
535
|
+
|---|---|
|
|
536
|
+
| Done, active, on track, success, approved, complete | success |
|
|
537
|
+
| At risk, pending, in review, in progress, waiting | warning |
|
|
538
|
+
| Behind, blocked, failed, rejected, overdue, error | error |
|
|
539
|
+
| Draft, inactive, unknown, archived, neutral | neutral |
|
|
540
|
+
| Informational, new, upcoming | info |
|
|
544
541
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
<div role="tablist">
|
|
548
|
-
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabIndex={0}>Overview</button>
|
|
549
|
-
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabIndex={-1}>Activity</button>
|
|
550
|
-
</div>
|
|
551
|
-
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
|
|
552
|
-
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>
|
|
553
|
-
// Only selected tab has tabIndex=0. All others tabIndex=-1.
|
|
554
|
-
// Inactive panels: hidden or aria-hidden="true"
|
|
555
|
-
```
|
|
542
|
+
When in doubt, ask: is this good, bad, cautionary, or neutral?
|
|
543
|
+
Pick the variant that matches that meaning.
|
|
556
544
|
|
|
557
545
|
---
|
|
558
546
|
|
|
559
|
-
###
|
|
547
|
+
### Page margin application
|
|
560
548
|
|
|
561
|
-
|
|
549
|
+
The content area must always have horizontal breathing room. Apply
|
|
550
|
+
`var(--page-margin-x)` as `padding-inline` on the direct child of the content area —
|
|
551
|
+
not on individual components.
|
|
562
552
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
-
|
|
566
|
-
-
|
|
553
|
+
```css
|
|
554
|
+
/* CORRECT */
|
|
555
|
+
.page-content {
|
|
556
|
+
padding-inline: var(--page-margin-x);
|
|
557
|
+
}
|
|
567
558
|
|
|
568
|
-
|
|
569
|
-
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
- Stackable filters → use Chips
|
|
559
|
+
/* WRONG */
|
|
560
|
+
.page-content {
|
|
561
|
+
padding: 0; /* flush to edges */
|
|
562
|
+
}
|
|
563
|
+
```
|
|
574
564
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
- All selectors must be equal width.
|
|
578
|
-
- Max 5 options.
|
|
579
|
-
- Never use as navigation.
|
|
565
|
+
This applies to every page without exception: tables, cards, charts, lists,
|
|
566
|
+
empty states. Nothing should ever touch the left or right edge of the content area.
|
|
580
567
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<button role="radio" aria-checked="true">List</button>
|
|
585
|
-
<button role="radio" aria-checked="false">Grid</button>
|
|
586
|
-
</div>
|
|
587
|
-
// Arrow key navigation moves focus and selection together
|
|
588
|
-
```
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Component Usage Rules
|
|
589
571
|
|
|
590
572
|
---
|
|
591
573
|
|
|
592
|
-
###
|
|
574
|
+
### Button
|
|
593
575
|
|
|
594
|
-
**Applies to: `
|
|
576
|
+
**Applies to: `Button` (Main, Destructive, Toggle, Vertical)**
|
|
595
577
|
|
|
596
578
|
#### When to use
|
|
597
|
-
- Clear hierarchy of at least 3 levels.
|
|
598
|
-
- Users navigate between levels frequently.
|
|
599
579
|
|
|
600
|
-
|
|
601
|
-
-
|
|
602
|
-
-
|
|
603
|
-
-
|
|
580
|
+
- **Main button**: default for actions. Not destructive, not icon-only, not vertical.
|
|
581
|
+
- **Destructive button**: irreversible or harmful actions (delete, discard). Same variants as Main but danger color.
|
|
582
|
+
- **Toggle button**: persistent on/off state in toolbars (Bold, Italic, view mode). NOT a one-off action trigger.
|
|
583
|
+
- **Vertical button**: icon + label stacked, in bottom toolbars only.
|
|
604
584
|
|
|
605
|
-
####
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
| `Current` | Current location — not a link, not interactive |
|
|
610
|
-
| `Ellipsis` | Collapsed middle levels — expandable on click |
|
|
585
|
+
#### Do NOT use a button when
|
|
586
|
+
- Action navigates to a page → use a link
|
|
587
|
+
- Action toggles a setting → use Switcher
|
|
588
|
+
- Action selects from a set → use Segment control or Tab
|
|
611
589
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
590
|
+
#### Props and variants
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'plain';
|
|
594
|
+
type ButtonSize = 'sm' | 'md';
|
|
595
|
+
// plain is only available in sm
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Hierarchy rules
|
|
599
|
+
- **One Primary per view.** Never two Primary buttons side by side unless forced mutually exclusive choice.
|
|
600
|
+
- **Secondary**: supports Primary or equal-weight alternative.
|
|
601
|
+
- **Ghost**: functional/utility (filter, sort, copy). Not a "quieter Primary".
|
|
602
|
+
- **Plain**: dismissive, space-constrained, `sm` only. "Cancel", "Skip".
|
|
603
|
+
|
|
604
|
+
#### States
|
|
605
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled` · `Loading` · `Selected` *(Toggle/Vertical only)*
|
|
606
|
+
|
|
607
|
+
- `Selected` is a boolean prop. Use `aria-pressed="true/false"` for Toggle/Vertical.
|
|
608
|
+
- Do NOT use `aria-pressed` on Main or Destructive buttons.
|
|
609
|
+
- `Loading` → use `aria-busy="true"` on the button.
|
|
610
|
+
- `Disabled` → never rely on opacity alone. Do not disable Primary as a form validation strategy.
|
|
611
|
+
|
|
612
|
+
#### Placement
|
|
613
|
+
- Button group order left-to-right: Ghost/Plain → Secondary → **Primary** (most important is trailing).
|
|
614
|
+
- Destructive confirmation: Destructive right, "Cancel" left.
|
|
615
|
+
- Toggle buttons → toolbars only.
|
|
616
|
+
- Vertical buttons → bottom toolbars/action panels only.
|
|
615
617
|
|
|
616
618
|
#### ARIA
|
|
617
619
|
```tsx
|
|
618
|
-
<
|
|
619
|
-
<ol>
|
|
620
|
-
<li><a href="/home">Home</a></li>
|
|
621
|
-
<li><a href="/projects">Projects</a></li>
|
|
622
|
-
<li><span aria-current="page">Q4 Campaign</span></li>
|
|
623
|
-
</ol>
|
|
624
|
-
</nav>
|
|
625
|
-
// Ellipsis
|
|
626
|
-
<button aria-label="Show more breadcrumbs">…</button>
|
|
627
|
-
// Separators — decorative, must not be read aloud
|
|
628
|
-
<span aria-hidden="true">/</span>
|
|
620
|
+
<button type="button" disabled={disabled} aria-pressed={selected} aria-busy={loading} aria-label="...for icon-only">
|
|
629
621
|
```
|
|
630
622
|
|
|
623
|
+
#### Content rules
|
|
624
|
+
- Start with a strong verb: "Save", "Delete", "Export"
|
|
625
|
+
- Specific: "Save changes" not "OK" or "Yes"
|
|
626
|
+
- 1–3 words; 5 maximum
|
|
627
|
+
- Sentence case: "Save changes" not "Save Changes"
|
|
628
|
+
- Destructive labels: name what is destroyed. "Delete account" not "Confirm"
|
|
629
|
+
- Loading labels: present-progressive. "Saving…"
|
|
630
|
+
- Toggle `aria-label`: describes the action, not the state. "Bold" not "Bold is active"
|
|
631
|
+
|
|
631
632
|
---
|
|
632
633
|
|
|
633
|
-
###
|
|
634
|
+
### Avatar / AvatarGroup
|
|
634
635
|
|
|
635
|
-
**Applies to: `
|
|
636
|
+
**Applies to: `Avatar`, `AvatarGroup`**
|
|
636
637
|
|
|
637
638
|
#### When to use
|
|
638
|
-
-
|
|
639
|
-
-
|
|
639
|
+
- Represents a person or organization where visual identity aids recognition.
|
|
640
|
+
- Do NOT use for generic system processes or when no identity info is available.
|
|
640
641
|
|
|
641
|
-
####
|
|
642
|
-
-
|
|
643
|
-
-
|
|
644
|
-
-
|
|
642
|
+
#### Variants
|
|
643
|
+
- `Profile`: individual person
|
|
644
|
+
- `Organization`: company/team
|
|
645
|
+
- Never mix Profile and Organization in the same list without clear reason.
|
|
645
646
|
|
|
646
|
-
####
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
647
|
+
#### Fill priority (always use highest fidelity available)
|
|
648
|
+
1. `Picture` — real photo/logo
|
|
649
|
+
2. `Initials` — derived from name
|
|
650
|
+
3. `Icon` — anonymous fallback
|
|
651
|
+
|
|
652
|
+
Never show Icon when Initials are derivable.
|
|
653
|
+
|
|
654
|
+
#### Sizes (Avatar: 7-step scale)
|
|
655
|
+
`xxs` · `xs` · `sm` · `md` (default) · `lg` · `xl` · `xxl`
|
|
656
|
+
|
|
657
|
+
Use consistent sizes within a list. Never mix sizes in a collection.
|
|
658
|
+
|
|
659
|
+
#### AvatarGroup
|
|
660
|
+
- Sizes: `xxs`, `xs`, `sm` only
|
|
661
|
+
- Show "+" overflow indicator when list exceeds display limit (typically 3–5)
|
|
662
|
+
- Overflow indicator must include full list in `aria-label`
|
|
663
|
+
- Do not use for >20 members without a "view all" affordance
|
|
653
664
|
|
|
654
665
|
#### ARIA
|
|
655
666
|
```tsx
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
667
|
+
// Individual: meaningful avatar
|
|
668
|
+
<img alt="Ana Costa" aria-label="Ana Costa" />
|
|
669
|
+
|
|
670
|
+
// Icon fill, no identity
|
|
671
|
+
<span aria-hidden="true" />
|
|
672
|
+
|
|
673
|
+
// AvatarGroup
|
|
674
|
+
<div aria-label="Assigned to: Ana Costa, João Silva, and 2 others">
|
|
664
675
|
```
|
|
665
676
|
|
|
677
|
+
#### Content rules — Initials
|
|
678
|
+
- Person: first + last initial. "Ana Costa" → "AC"
|
|
679
|
+
- Single name: first two letters. "Cher" → "CH"
|
|
680
|
+
- Organization: first two letters or first letter of each word (max 2). "Design Studio" → "DS"
|
|
681
|
+
- Always uppercase. Max 2 characters.
|
|
682
|
+
|
|
666
683
|
---
|
|
667
684
|
|
|
668
|
-
###
|
|
685
|
+
### Badge
|
|
669
686
|
|
|
670
|
-
**Applies to: `
|
|
687
|
+
**Applies to: `Badge`**
|
|
671
688
|
|
|
672
689
|
#### When to use
|
|
673
|
-
-
|
|
674
|
-
-
|
|
690
|
+
- Attach to another element to surface a count or status signal.
|
|
691
|
+
- Notification count on a navigation icon, pending actions, critical status.
|
|
675
692
|
|
|
676
693
|
#### Do NOT use when
|
|
677
|
-
-
|
|
678
|
-
-
|
|
679
|
-
-
|
|
680
|
-
-
|
|
681
|
-
|
|
682
|
-
#### Types
|
|
683
|
-
- **Inline toast**: single message, brief confirmation.
|
|
684
|
-
- **Rich toast**: title + body + optional action. Use only when additional space is genuinely needed.
|
|
694
|
+
- Label is text/category → use Tag
|
|
695
|
+
- User can select/dismiss it → use Chip
|
|
696
|
+
- Requires user response → use Toast or alert banner
|
|
697
|
+
- Count is zero → **hide the badge**
|
|
685
698
|
|
|
686
|
-
####
|
|
687
|
-
|
|
|
699
|
+
#### Variants
|
|
700
|
+
| Variant | Use |
|
|
688
701
|
|---|---|
|
|
689
|
-
| `
|
|
690
|
-
| `
|
|
691
|
-
| `
|
|
692
|
-
| `
|
|
702
|
+
| `Highlight soft` | Low urgency — new content, informational count |
|
|
703
|
+
| `Highlight strong` | Moderate urgency — pending actions, unread items |
|
|
704
|
+
| `Informative` | Neutral — generic counts |
|
|
705
|
+
| `Critical` | Urgent — errors, failures. **Use sparingly.** |
|
|
693
706
|
|
|
694
|
-
####
|
|
695
|
-
- `
|
|
696
|
-
- `
|
|
697
|
-
- `
|
|
698
|
-
- Rich toast with action: persist until user acts or dismisses
|
|
707
|
+
#### Sizes
|
|
708
|
+
- `sm`: default — icons, avatars, compact items
|
|
709
|
+
- `lg`: larger host elements
|
|
710
|
+
- `Dot`: presence-only signal. `Highlight strong` and `Critical` only.
|
|
699
711
|
|
|
700
|
-
|
|
701
|
-
|
|
712
|
+
#### Count display rules
|
|
713
|
+
- ≤99: show exact count
|
|
714
|
+
- >99: show "99+"
|
|
715
|
+
- 0: **remove the badge entirely**
|
|
702
716
|
|
|
703
717
|
#### ARIA
|
|
704
718
|
```tsx
|
|
705
|
-
//
|
|
706
|
-
<
|
|
707
|
-
|
|
708
|
-
//
|
|
709
|
-
<div aria-live="assertive" role="alert">Couldn't save changes.</div>
|
|
710
|
-
|
|
711
|
-
// Dismiss button
|
|
712
|
-
<button aria-label="Dismiss notification">×</button>
|
|
719
|
+
// Host element's accessible name must include the count
|
|
720
|
+
<button aria-label="Messages, 3 unread" />
|
|
721
|
+
// Dynamic count changes
|
|
722
|
+
<div aria-live="polite" /> // only for meaningful threshold transitions
|
|
713
723
|
```
|
|
714
724
|
|
|
715
725
|
---
|
|
716
726
|
|
|
717
|
-
###
|
|
727
|
+
### Tag
|
|
718
728
|
|
|
719
|
-
**Applies to: `
|
|
729
|
+
**Applies to: `Tag`**
|
|
720
730
|
|
|
721
731
|
#### When to use
|
|
722
|
-
-
|
|
723
|
-
-
|
|
732
|
+
- Non-interactive categorical label or status on content items.
|
|
733
|
+
- Status: "Active", "Archived", "Pending review"
|
|
734
|
+
- Category: "Design", "Bug", "High"
|
|
724
735
|
|
|
725
736
|
#### Do NOT use when
|
|
726
|
-
-
|
|
727
|
-
-
|
|
728
|
-
-
|
|
729
|
-
- Trigger is disabled → use visible text explanation instead
|
|
730
|
-
- Touch device as primary platform → ensure info is available another way
|
|
737
|
+
- User can interact with it → use Chip
|
|
738
|
+
- It's a numeric count → use Badge
|
|
739
|
+
- Urgent system condition → use Toast
|
|
731
740
|
|
|
732
|
-
####
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
741
|
+
#### Variants
|
|
742
|
+
| Variant | Semantic meaning |
|
|
743
|
+
|---|---|
|
|
744
|
+
| `Highlight` | Notable, not urgent |
|
|
745
|
+
| `Warning` | Caution — needs awareness soon |
|
|
746
|
+
| `Error` | Something is wrong or blocked |
|
|
747
|
+
| `Success` | Completed, approved, healthy |
|
|
748
|
+
| `Neutral` | Purely categorical, no status weight |
|
|
749
|
+
|
|
750
|
+
Use the variant matching semantic meaning, not preferred color.
|
|
751
|
+
|
|
752
|
+
#### Sizes
|
|
753
|
+
- `md`: default
|
|
754
|
+
- `sm`: dense layouts, multiple tags per item
|
|
755
|
+
|
|
756
|
+
Use consistent sizes within a list. Never mix on the same row.
|
|
757
|
+
|
|
758
|
+
#### Tags have NO interaction states. Not clickable. Never.
|
|
759
|
+
|
|
760
|
+
#### Placement
|
|
761
|
+
- Show max 3 tags per item.
|
|
762
|
+
- Order: severity first (Error → Warning → Success), then category.
|
|
763
|
+
- Do not wrap — truncate and show full value in tooltip if space is insufficient.
|
|
737
764
|
|
|
738
765
|
#### ARIA
|
|
739
766
|
```tsx
|
|
740
|
-
|
|
741
|
-
<
|
|
742
|
-
|
|
767
|
+
// Include tag content in accessible name of parent
|
|
768
|
+
<li aria-describedby="tag-status">
|
|
769
|
+
<span id="tag-status" role="status">Error</span>
|
|
743
770
|
```
|
|
744
771
|
|
|
745
772
|
---
|
|
746
773
|
|
|
747
|
-
###
|
|
774
|
+
### Chip / ChipGroup
|
|
748
775
|
|
|
749
|
-
**Applies to: `
|
|
776
|
+
**Applies to: `Chip`, `ChipGroup`**
|
|
750
777
|
|
|
751
|
-
#### When to use
|
|
752
|
-
-
|
|
753
|
-
-
|
|
778
|
+
#### When to use
|
|
779
|
+
- User needs to select, activate, or dismiss a discrete value.
|
|
780
|
+
- Filter controls, multi-value input, toggleable options.
|
|
754
781
|
|
|
755
782
|
#### Do NOT use when
|
|
756
|
-
-
|
|
757
|
-
-
|
|
758
|
-
-
|
|
759
|
-
-
|
|
783
|
+
- Purely informational → use Tag
|
|
784
|
+
- Mutually exclusive form selection → use Radio
|
|
785
|
+
- Multi-option form selection → use Checkbox
|
|
786
|
+
- Settings toggle → use Switcher
|
|
760
787
|
|
|
761
|
-
####
|
|
762
|
-
-
|
|
763
|
-
-
|
|
764
|
-
- **Complex menu**: grouped, with section headings, scrollable
|
|
788
|
+
#### Sizes
|
|
789
|
+
- `md`: default
|
|
790
|
+
- `sm`: compact layouts
|
|
765
791
|
|
|
766
|
-
|
|
767
|
-
- Opens on **click/tap** only. Never on hover.
|
|
768
|
-
- Dismissal: click outside, `Escape`, trigger toggle, or completing action.
|
|
769
|
-
- Do not nest popovers inside other popovers.
|
|
770
|
-
- For menus: selecting an item closes popover automatically.
|
|
771
|
-
- For container with unsaved form input: do NOT auto-close on outside click.
|
|
792
|
+
Never mix `md` and `sm` in the same chip group.
|
|
772
793
|
|
|
773
|
-
####
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
-
|
|
794
|
+
#### States
|
|
795
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled` · `Selected` (boolean)
|
|
796
|
+
|
|
797
|
+
`Selected` combines freely with `Hover`, `Pressed`, `Focused`.
|
|
798
|
+
|
|
799
|
+
#### Keyboard interaction
|
|
800
|
+
- `Space` or `Enter` → toggle
|
|
801
|
+
- `Tab` → move between chips
|
|
780
802
|
|
|
781
803
|
#### ARIA
|
|
782
804
|
```tsx
|
|
783
|
-
//
|
|
784
|
-
<
|
|
805
|
+
// Multi-select filter group
|
|
806
|
+
<div role="group" aria-label="Filter by status">
|
|
807
|
+
<button role="checkbox" aria-checked="true">Active</button>
|
|
808
|
+
<button role="checkbox" aria-checked="false">Closed</button>
|
|
809
|
+
</div>
|
|
785
810
|
|
|
786
|
-
//
|
|
787
|
-
<div role="
|
|
788
|
-
<button role="
|
|
789
|
-
<button role="menuitem">Delete</button>
|
|
811
|
+
// Single-select group
|
|
812
|
+
<div role="radiogroup" aria-label="Filter by status">
|
|
813
|
+
<button role="radio" aria-checked="true">Active</button>
|
|
790
814
|
</div>
|
|
791
815
|
|
|
792
|
-
//
|
|
793
|
-
<
|
|
816
|
+
// Dismiss button on chip
|
|
817
|
+
<button aria-label="Remove Active" />
|
|
794
818
|
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
// Escape: closes and returns focus
|
|
798
|
-
// Arrow keys: navigate between menuitem elements
|
|
819
|
+
// Disabled chip
|
|
820
|
+
aria-disabled="true" // keep in reading order
|
|
799
821
|
```
|
|
800
822
|
|
|
801
823
|
---
|
|
802
824
|
|
|
803
|
-
###
|
|
825
|
+
### Separator
|
|
804
826
|
|
|
805
|
-
**Applies to: `
|
|
827
|
+
**Applies to: `Separator`**
|
|
806
828
|
|
|
807
829
|
#### When to use
|
|
808
|
-
-
|
|
809
|
-
- Confirmations, compact forms, critical warnings, media previews.
|
|
830
|
+
- Visual divider between content sections that are related but distinct.
|
|
810
831
|
|
|
811
832
|
#### Do NOT use when
|
|
812
|
-
-
|
|
813
|
-
-
|
|
814
|
-
-
|
|
815
|
-
- User needs to reference background content → use Drawer
|
|
816
|
-
|
|
817
|
-
#### Rules
|
|
818
|
-
- Every modal must have a **title**.
|
|
819
|
-
- Every modal must have an **explicit dismiss** (× in header + cancel in footer).
|
|
820
|
-
- **One primary action only.** Two equal actions → Secondary + Secondary, not two Primary.
|
|
821
|
-
- Never open a modal from within a modal.
|
|
822
|
-
- Never auto-open on page load.
|
|
823
|
-
- `Escape` must close unless accidental dismissal would cause irreversible data loss.
|
|
824
|
-
- Background page scroll must be disabled while modal is open.
|
|
833
|
+
- Adequate spacing already separates sections
|
|
834
|
+
- Sections are already separated by color/card borders
|
|
835
|
+
- Purpose is purely decorative
|
|
825
836
|
|
|
826
|
-
####
|
|
827
|
-
-
|
|
828
|
-
-
|
|
829
|
-
- Focus is **trapped** within modal while open.
|
|
830
|
-
- Focus returns to triggering element on close.
|
|
837
|
+
#### Directions
|
|
838
|
+
- `Horizontal`: stacked content, spans full container width
|
|
839
|
+
- `Vertical`: side-by-side content, spans full container height
|
|
831
840
|
|
|
832
841
|
#### ARIA
|
|
833
842
|
```tsx
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
843
|
+
// Structural separator
|
|
844
|
+
<hr /> // or <div role="separator" />
|
|
845
|
+
|
|
846
|
+
// Decorative only
|
|
847
|
+
<div aria-hidden="true" />
|
|
839
848
|
```
|
|
840
849
|
|
|
841
850
|
---
|
|
842
851
|
|
|
843
|
-
###
|
|
844
|
-
|
|
845
|
-
**Applies to: all input components**
|
|
852
|
+
### Switcher
|
|
846
853
|
|
|
847
|
-
|
|
848
|
-
```
|
|
849
|
-
[Label] ← always required (visible or aria-label)
|
|
850
|
-
[Control] ← must have visible boundary
|
|
851
|
-
[Helper text] ← optional; replace with real content or hide — never leave placeholder text
|
|
852
|
-
[Error message] ← replaces helper text in error state
|
|
853
|
-
```
|
|
854
|
+
**Applies to: `Switcher`**
|
|
854
855
|
|
|
855
|
-
####
|
|
856
|
-
|
|
856
|
+
#### When to use
|
|
857
|
+
- Single binary setting, **effect is immediate** (no confirmation step).
|
|
858
|
+
- Enabling features, notification settings, preferences.
|
|
857
859
|
|
|
858
|
-
|
|
859
|
-
-
|
|
860
|
-
-
|
|
860
|
+
#### Do NOT use when
|
|
861
|
+
- More than 2 states → use Radio or Select
|
|
862
|
+
- Multiple independent options → use Checkbox
|
|
863
|
+
- Requires confirmation before taking effect → use Checkbox
|
|
864
|
+
- Compact toolbar → use Toggle button
|
|
865
|
+
- Choosing between UI views/modes → use Segment control or Tabs
|
|
861
866
|
|
|
862
|
-
####
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
867
|
+
#### Decision: Switcher vs Checkbox vs Toggle button
|
|
868
|
+
| Component | Effect | Context |
|
|
869
|
+
|---|---|---|
|
|
870
|
+
| **Switcher** | Immediate | Settings with visible label |
|
|
871
|
+
| **Checkbox** | Staged (on submit) | Forms |
|
|
872
|
+
| **Toggle button** | Immediate | Icon-only toolbar |
|
|
868
873
|
|
|
869
|
-
####
|
|
870
|
-
-
|
|
871
|
-
-
|
|
872
|
-
- Left-of-control only in Horizontal input layouts.
|
|
873
|
-
- Never use placeholder text as label substitute.
|
|
874
|
-
- Label describes what the field contains: "Email address" not "Enter your email address".
|
|
875
|
-
- Every field is mandatory by default. Only mark optional fields with an "Optional" label in `var(--text-tertiary)` placed inline after the label text. Never use asterisks.
|
|
876
|
-
- Add `aria-required="true"` on all mandatory inputs regardless of visual treatment.
|
|
874
|
+
#### Sizes
|
|
875
|
+
- `md`: default
|
|
876
|
+
- `sm`: dense layouts
|
|
877
877
|
|
|
878
|
-
|
|
879
|
-
```tsx
|
|
880
|
-
<label htmlFor="email">Email address</label>
|
|
881
|
-
<input
|
|
882
|
-
id="email"
|
|
883
|
-
aria-describedby="email-helper email-error"
|
|
884
|
-
aria-invalid={hasError}
|
|
885
|
-
aria-required={required}
|
|
886
|
-
disabled={disabled}
|
|
887
|
-
/>
|
|
888
|
-
<p id="email-helper">We'll only use this to send receipts.</p>
|
|
889
|
-
<p id="email-error" aria-live="polite">Email address is invalid.</p>
|
|
890
|
-
```
|
|
878
|
+
Never mix sizes within a settings group.
|
|
891
879
|
|
|
892
|
-
|
|
880
|
+
#### States
|
|
881
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
893
882
|
|
|
894
|
-
|
|
883
|
+
`Selected` and `Disabled` are independent booleans.
|
|
895
884
|
|
|
896
|
-
|
|
885
|
+
#### Rules
|
|
886
|
+
- Always pair with a visible label. **Never use a switcher without a label.**
|
|
887
|
+
- When `Selected=True` + `Disabled=True`: always explain why it's locked.
|
|
888
|
+
- If toggle has latency, show loading indicator alongside — don't leave in ambiguous state.
|
|
897
889
|
|
|
898
|
-
|
|
890
|
+
#### Keyboard
|
|
891
|
+
- `Space` → toggle
|
|
899
892
|
|
|
900
|
-
|
|
893
|
+
#### ARIA
|
|
894
|
+
```tsx
|
|
895
|
+
<button role="switch" aria-checked={isOn} aria-labelledby="label-id" />
|
|
896
|
+
// Disabled
|
|
897
|
+
aria-disabled="true" // keep in reading order
|
|
898
|
+
```
|
|
901
899
|
|
|
902
900
|
---
|
|
903
901
|
|
|
904
|
-
###
|
|
902
|
+
### Checkbox
|
|
905
903
|
|
|
906
|
-
**Applies to: `
|
|
904
|
+
**Applies to: `Checkbox`**
|
|
907
905
|
|
|
908
|
-
|
|
906
|
+
#### When to use
|
|
907
|
+
- User selects any number of independent options (zero to all).
|
|
908
|
+
- Selection takes effect only after **explicit confirmation** (submit button).
|
|
909
909
|
|
|
910
|
-
|
|
911
|
-
-
|
|
912
|
-
-
|
|
910
|
+
#### Do NOT use when
|
|
911
|
+
- Only one can be selected → use Radio
|
|
912
|
+
- Effect is immediate → use Switcher
|
|
913
|
+
- Compact token-like pattern → use Chip
|
|
914
|
+
- Single toggle for a setting → use Switcher
|
|
913
915
|
|
|
914
|
-
|
|
916
|
+
#### Sizes
|
|
917
|
+
- `md`: default
|
|
918
|
+
- `sm`: dense layouts
|
|
915
919
|
|
|
916
|
-
|
|
920
|
+
Never mix sizes in a single form group.
|
|
917
921
|
|
|
918
|
-
|
|
922
|
+
#### States
|
|
923
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
919
924
|
|
|
920
|
-
|
|
925
|
+
**Indeterminate state**: when a parent checkbox controls sub-items with partial selection.
|
|
921
926
|
|
|
922
|
-
|
|
923
|
-
- **
|
|
924
|
-
-
|
|
925
|
-
- `Error` state is for system errors (service unavailable) — NOT for "no results".
|
|
926
|
-
- "No results" → show empty state in results area, not an input error.
|
|
927
|
+
#### Interaction
|
|
928
|
+
- Click on checkbox **or its label** toggles selection (full row is hit target).
|
|
929
|
+
- State changes are staged — not immediate.
|
|
927
930
|
|
|
931
|
+
#### ARIA
|
|
928
932
|
```tsx
|
|
929
|
-
<
|
|
930
|
-
<
|
|
933
|
+
<fieldset>
|
|
934
|
+
<legend>Notification preferences</legend>
|
|
935
|
+
<label>
|
|
936
|
+
<input type="checkbox" aria-checked="true" /> Send weekly summary
|
|
937
|
+
</label>
|
|
938
|
+
</fieldset>
|
|
939
|
+
|
|
940
|
+
// Indeterminate
|
|
941
|
+
aria-checked="mixed"
|
|
942
|
+
|
|
943
|
+
// Disabled — keep in reading order
|
|
944
|
+
aria-disabled="true"
|
|
931
945
|
```
|
|
932
946
|
|
|
933
947
|
---
|
|
934
948
|
|
|
935
|
-
###
|
|
949
|
+
### Radio
|
|
936
950
|
|
|
937
|
-
**Applies to: `
|
|
951
|
+
**Applies to: `Radio`**
|
|
938
952
|
|
|
939
|
-
|
|
940
|
-
-
|
|
941
|
-
-
|
|
942
|
-
-
|
|
953
|
+
#### When to use
|
|
954
|
+
- Exactly one option from a mutually exclusive set.
|
|
955
|
+
- All options visible simultaneously (2–6 items).
|
|
956
|
+
- Effect takes place after **explicit confirmation**.
|
|
943
957
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
958
|
+
#### Do NOT use when
|
|
959
|
+
- Multiple can be selected → use Checkbox
|
|
960
|
+
- More than 6–7 options → use Select
|
|
961
|
+
- Immediate effect → use Switcher or Segment control
|
|
962
|
+
- Single on/off option → use Checkbox or Switcher
|
|
949
963
|
|
|
950
|
-
|
|
964
|
+
#### Sizes
|
|
965
|
+
- `md`: default
|
|
966
|
+
- `sm`: dense layouts
|
|
951
967
|
|
|
952
|
-
|
|
968
|
+
Never mix sizes within a radio group.
|
|
953
969
|
|
|
954
|
-
|
|
970
|
+
#### States
|
|
971
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
955
972
|
|
|
956
|
-
|
|
957
|
-
- Auto-submit: when last field filled (fixed length), trigger verification automatically.
|
|
958
|
-
- Paste handling: full code must populate all fields at once.
|
|
959
|
-
- Backspace: moves focus to previous field and clears it.
|
|
973
|
+
No indeterminate state for Radio. A group always has 0–1 selected.
|
|
960
974
|
|
|
961
|
-
|
|
975
|
+
#### Interaction
|
|
976
|
+
- Click radio **or its label** to select (full row is hit target).
|
|
977
|
+
- Selecting one deselects all others in the group.
|
|
978
|
+
- Keyboard: `Arrow keys` move selection within group. `Tab` exits group.
|
|
962
979
|
|
|
980
|
+
#### ARIA
|
|
963
981
|
```tsx
|
|
964
|
-
<div role="
|
|
965
|
-
<
|
|
966
|
-
|
|
967
|
-
|
|
982
|
+
<div role="radiogroup" aria-labelledby="group-label">
|
|
983
|
+
<label>
|
|
984
|
+
<input type="radio" aria-checked="true" /> Option A
|
|
985
|
+
</label>
|
|
968
986
|
</div>
|
|
969
|
-
//
|
|
970
|
-
// Correct/Error states: must be announced
|
|
987
|
+
// Arrow key navigation must move both focus and selection together
|
|
971
988
|
```
|
|
972
989
|
|
|
973
990
|
---
|
|
974
991
|
|
|
975
|
-
###
|
|
992
|
+
### Tab / CustomViewTab
|
|
976
993
|
|
|
977
|
-
**Applies to: `
|
|
994
|
+
**Applies to: `Tab` (Navigation, Custom View)**
|
|
978
995
|
|
|
979
|
-
|
|
980
|
-
-
|
|
981
|
-
-
|
|
982
|
-
-
|
|
996
|
+
#### When to use
|
|
997
|
+
- Parallel, mutually exclusive content sections on the same page.
|
|
998
|
+
- Each section is substantial enough to be its own named panel.
|
|
999
|
+
- 2–7 tabs.
|
|
983
1000
|
|
|
984
|
-
|
|
1001
|
+
#### Do NOT use when
|
|
1002
|
+
- Sections are sequential steps → use stepper
|
|
1003
|
+
- Fewer than 2 tabs
|
|
1004
|
+
- More than 6–7 tabs → use sidebar nav or Select
|
|
1005
|
+
- Goal is filtering same content → use Chip
|
|
985
1006
|
|
|
986
|
-
|
|
1007
|
+
#### Tab vs Segment control
|
|
1008
|
+
| | Segment control | Tab |
|
|
1009
|
+
|---|---|---|
|
|
1010
|
+
| What changes | How content is rendered | The content itself |
|
|
1011
|
+
| Architecture | Display preference | Part of information architecture |
|
|
1012
|
+
| URL typically | Does not update | Updates (sub-routes) |
|
|
987
1013
|
|
|
988
|
-
|
|
1014
|
+
#### Rules
|
|
1015
|
+
- **One tab must always be selected.** No valid unselected state.
|
|
1016
|
+
- All tabs in a group must be the same type.
|
|
1017
|
+
- Tab labels: nouns only. No verbs. "Overview" not "View overview".
|
|
1018
|
+
- Disabled tabs: explain via tooltip. Remove if always disabled for all users.
|
|
1019
|
+
- Content panel must be directly below/adjacent to tabs — never disconnected.
|
|
1020
|
+
- Never nest tabs within tabs.
|
|
989
1021
|
|
|
990
|
-
|
|
991
|
-
-
|
|
992
|
-
-
|
|
993
|
-
-
|
|
1022
|
+
#### Keyboard
|
|
1023
|
+
- `Tab` → focus tab row
|
|
1024
|
+
- `←` `→` → move between tabs
|
|
1025
|
+
- `Enter` / `Space` → select focused tab
|
|
1026
|
+
- `Tab` → move into content panel
|
|
994
1027
|
|
|
1028
|
+
#### ARIA
|
|
995
1029
|
```tsx
|
|
996
|
-
<div role="
|
|
997
|
-
<
|
|
998
|
-
<
|
|
1030
|
+
<div role="tablist">
|
|
1031
|
+
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabIndex={0}>Overview</button>
|
|
1032
|
+
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabIndex={-1}>Activity</button>
|
|
999
1033
|
</div>
|
|
1034
|
+
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
|
|
1035
|
+
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>
|
|
1036
|
+
// Only selected tab has tabIndex=0. All others tabIndex=-1.
|
|
1037
|
+
// Inactive panels: hidden or aria-hidden="true"
|
|
1000
1038
|
```
|
|
1001
1039
|
|
|
1002
1040
|
---
|
|
1003
1041
|
|
|
1004
|
-
###
|
|
1042
|
+
### SegmentControl
|
|
1005
1043
|
|
|
1006
|
-
**Applies to: `
|
|
1044
|
+
**Applies to: `SegmentControl`, `SegmentControlSelector`**
|
|
1007
1045
|
|
|
1008
|
-
|
|
1009
|
-
-
|
|
1010
|
-
-
|
|
1011
|
-
-
|
|
1046
|
+
#### When to use
|
|
1047
|
+
- Switch between 2–5 mutually exclusive views or modes.
|
|
1048
|
+
- Effect is **immediate** — no content panels, no confirmation.
|
|
1049
|
+
- "List", "Grid", "Map" / "Day", "Week", "Month"
|
|
1012
1050
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1051
|
+
#### Do NOT use when
|
|
1052
|
+
- More than 5 options → use Select or Tabs
|
|
1053
|
+
- Options have distinct content panels → use Tabs
|
|
1054
|
+
- Requires confirmation → use Radio
|
|
1055
|
+
- Binary setting → use Switcher
|
|
1056
|
+
- Stackable filters → use Chips
|
|
1018
1057
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1058
|
+
#### Rules
|
|
1059
|
+
- **One selector always active.** No valid unselected state.
|
|
1060
|
+
- All selectors must be equal width.
|
|
1061
|
+
- Max 5 options.
|
|
1062
|
+
- Never use as navigation.
|
|
1063
|
+
|
|
1064
|
+
#### ARIA
|
|
1065
|
+
```tsx
|
|
1066
|
+
<div role="group" aria-label="View mode">
|
|
1067
|
+
<button role="radio" aria-checked="true">List</button>
|
|
1068
|
+
<button role="radio" aria-checked="false">Grid</button>
|
|
1023
1069
|
</div>
|
|
1070
|
+
// Arrow key navigation moves focus and selection together
|
|
1024
1071
|
```
|
|
1025
1072
|
|
|
1026
1073
|
---
|
|
1027
1074
|
|
|
1028
|
-
###
|
|
1075
|
+
### Breadcrumbs / BreadcrumbItem
|
|
1029
1076
|
|
|
1030
|
-
**Applies to: `
|
|
1077
|
+
**Applies to: `Breadcrumbs`, `BreadcrumbItem`**
|
|
1031
1078
|
|
|
1032
|
-
|
|
1033
|
-
-
|
|
1034
|
-
-
|
|
1079
|
+
#### When to use
|
|
1080
|
+
- Clear hierarchy of at least 3 levels.
|
|
1081
|
+
- Users navigate between levels frequently.
|
|
1082
|
+
|
|
1083
|
+
#### Do NOT use when
|
|
1084
|
+
- Hierarchy is flat (≤2 levels)
|
|
1085
|
+
- Single-level navigation pattern
|
|
1086
|
+
- User arrived via deep link and path is not meaningful
|
|
1087
|
+
|
|
1088
|
+
#### Item types
|
|
1089
|
+
| Type | Description |
|
|
1090
|
+
|---|---|
|
|
1091
|
+
| `Previous` | Ancestor level — interactive link |
|
|
1092
|
+
| `Current` | Current location — not a link, not interactive |
|
|
1093
|
+
| `Ellipsis` | Collapsed middle levels — expandable on click |
|
|
1094
|
+
|
|
1095
|
+
- **Last item is always `Current`.** Must not be a link.
|
|
1096
|
+
- Never truncate `Current` item.
|
|
1097
|
+
- When trail overflows: always keep root (leftmost) and current (rightmost).
|
|
1035
1098
|
|
|
1099
|
+
#### ARIA
|
|
1036
1100
|
```tsx
|
|
1037
|
-
<
|
|
1038
|
-
<
|
|
1039
|
-
<
|
|
1040
|
-
|
|
1041
|
-
</
|
|
1042
|
-
|
|
1101
|
+
<nav aria-label="Breadcrumb">
|
|
1102
|
+
<ol>
|
|
1103
|
+
<li><a href="/home">Home</a></li>
|
|
1104
|
+
<li><a href="/projects">Projects</a></li>
|
|
1105
|
+
<li><span aria-current="page">Q4 Campaign</span></li>
|
|
1106
|
+
</ol>
|
|
1107
|
+
</nav>
|
|
1108
|
+
// Ellipsis
|
|
1109
|
+
<button aria-label="Show more breadcrumbs">…</button>
|
|
1110
|
+
// Separators — decorative, must not be read aloud
|
|
1111
|
+
<span aria-hidden="true">/</span>
|
|
1043
1112
|
```
|
|
1044
1113
|
|
|
1045
1114
|
---
|
|
1046
1115
|
|
|
1047
|
-
###
|
|
1048
|
-
|
|
1049
|
-
**Applies to: `HorizontalInput`**
|
|
1050
|
-
|
|
1051
|
-
- Label left, control right — layout wrapper only.
|
|
1052
|
-
- Use in dense settings panels, property editors.
|
|
1053
|
-
- Do NOT use in standard forms, mobile layouts, or with long labels.
|
|
1054
|
-
- Label must not wrap — keep to 3–4 words maximum.
|
|
1055
|
-
- Consistent label widths within a group.
|
|
1116
|
+
### Pagination / PageSelector
|
|
1056
1117
|
|
|
1057
|
-
|
|
1118
|
+
**Applies to: `Pagination`, `PageSelector`**
|
|
1058
1119
|
|
|
1059
|
-
|
|
1120
|
+
#### When to use
|
|
1121
|
+
- Large dataset divided into pages where user benefits from explicit navigation.
|
|
1122
|
+
- Data tables, search results, record lists.
|
|
1060
1123
|
|
|
1061
|
-
|
|
1124
|
+
#### Do NOT use when
|
|
1125
|
+
- Dataset fits in one view → show everything
|
|
1126
|
+
- Primary interaction is continuous browsing → use infinite scroll
|
|
1127
|
+
- Within a compact panel → use load-more button
|
|
1062
1128
|
|
|
1063
|
-
|
|
1064
|
-
-
|
|
1065
|
-
- Always show
|
|
1066
|
-
-
|
|
1067
|
-
-
|
|
1129
|
+
#### Rules
|
|
1130
|
+
- Always show current page as `Selected`.
|
|
1131
|
+
- Always show first and last page numbers.
|
|
1132
|
+
- Collapse middle ranges with ellipsis; keep current page ± 1–2 neighbors.
|
|
1133
|
+
- Previous/Next arrows: **disable** at boundaries, never hide.
|
|
1134
|
+
- Show total record count: "Showing 41–60 of 243 results".
|
|
1135
|
+
- After page navigation: move focus to top of updated content area.
|
|
1068
1136
|
|
|
1137
|
+
#### ARIA
|
|
1069
1138
|
```tsx
|
|
1070
|
-
<
|
|
1071
|
-
<
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
// Page
|
|
1078
|
-
// Home/End: jump to min/max
|
|
1139
|
+
<nav aria-label="Pagination">
|
|
1140
|
+
<button aria-label="Go to previous page" aria-disabled="true">←</button>
|
|
1141
|
+
<button aria-current="page">3</button>
|
|
1142
|
+
<button>4</button>
|
|
1143
|
+
<button aria-label="More pages">…</button>
|
|
1144
|
+
<button aria-label="Go to next page">→</button>
|
|
1145
|
+
</nav>
|
|
1146
|
+
// Page change: use aria-live region to announce update
|
|
1079
1147
|
```
|
|
1080
1148
|
|
|
1081
1149
|
---
|
|
1082
1150
|
|
|
1083
|
-
###
|
|
1151
|
+
### Toast (Inline, Rich)
|
|
1084
1152
|
|
|
1085
|
-
**Applies to: `
|
|
1153
|
+
**Applies to: `Toast`**
|
|
1086
1154
|
|
|
1087
|
-
|
|
1088
|
-
-
|
|
1089
|
-
-
|
|
1090
|
-
- Show upload progress after file accepted.
|
|
1155
|
+
#### When to use
|
|
1156
|
+
- Confirm a completed action or surface a system event without interrupting the current task.
|
|
1157
|
+
- Non-blocking. Temporary.
|
|
1091
1158
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
```
|
|
1159
|
+
#### Do NOT use when
|
|
1160
|
+
- Critical, requires action before continuing → use modal or alert banner
|
|
1161
|
+
- Related to a specific form field → use inline validation
|
|
1162
|
+
- Complex, multiple actions needed → use Rich toast or reconsider modal
|
|
1163
|
+
- Unsolicited marketing/promotional content → never appropriate
|
|
1098
1164
|
|
|
1099
|
-
|
|
1165
|
+
#### Types
|
|
1166
|
+
- **Inline toast**: single message, brief confirmation.
|
|
1167
|
+
- **Rich toast**: title + body + optional action. Use only when additional space is genuinely needed.
|
|
1100
1168
|
|
|
1101
|
-
|
|
1169
|
+
#### Severity
|
|
1170
|
+
| Severity | Use |
|
|
1171
|
+
|---|---|
|
|
1172
|
+
| `Default` | Neutral confirmations, background tasks |
|
|
1173
|
+
| `Success` | Most common — action completed as expected |
|
|
1174
|
+
| `Warning` | Completed with caveat, system condition |
|
|
1175
|
+
| `Error` | Genuine failures only — **persists until dismissed** |
|
|
1102
1176
|
|
|
1103
|
-
|
|
1177
|
+
#### Auto-dismiss durations
|
|
1178
|
+
- `Default` / `Success`: 4–5 seconds
|
|
1179
|
+
- `Warning`: 6–8 seconds
|
|
1180
|
+
- `Error`: persistent or 8–10 seconds minimum
|
|
1181
|
+
- Rich toast with action: persist until user acts or dismisses
|
|
1104
1182
|
|
|
1105
|
-
|
|
1106
|
-
-
|
|
1107
|
-
- Validate each value before converting to chip.
|
|
1108
|
-
- Backspace on empty field removes last chip.
|
|
1183
|
+
Always provide a manual dismiss (×) button.
|
|
1184
|
+
Pause auto-dismiss on hover (desktop).
|
|
1109
1185
|
|
|
1186
|
+
#### ARIA
|
|
1110
1187
|
```tsx
|
|
1111
|
-
//
|
|
1112
|
-
<
|
|
1113
|
-
|
|
1114
|
-
|
|
1188
|
+
// Success, warning, default
|
|
1189
|
+
<div aria-live="polite" role="status">Changes saved.</div>
|
|
1190
|
+
|
|
1191
|
+
// Error
|
|
1192
|
+
<div aria-live="assertive" role="alert">Couldn't save changes.</div>
|
|
1193
|
+
|
|
1194
|
+
// Dismiss button
|
|
1195
|
+
<button aria-label="Dismiss notification">×</button>
|
|
1115
1196
|
```
|
|
1116
1197
|
|
|
1117
1198
|
---
|
|
1118
1199
|
|
|
1119
|
-
###
|
|
1200
|
+
### Tooltip
|
|
1120
1201
|
|
|
1121
|
-
**Applies to: `
|
|
1202
|
+
**Applies to: `Tooltip`**
|
|
1122
1203
|
|
|
1123
|
-
|
|
1124
|
-
-
|
|
1204
|
+
#### When to use
|
|
1205
|
+
- Brief supplementary context — hover or keyboard focus only.
|
|
1206
|
+
- Label icon-only buttons, reveal truncated text, show keyboard shortcuts.
|
|
1125
1207
|
|
|
1126
|
-
####
|
|
1127
|
-
-
|
|
1128
|
-
-
|
|
1208
|
+
#### Do NOT use when
|
|
1209
|
+
- Critical info user must see → place inline
|
|
1210
|
+
- Content longer than one sentence → use Popover
|
|
1211
|
+
- Content has interactive elements → use Popover
|
|
1212
|
+
- Trigger is disabled → use visible text explanation instead
|
|
1213
|
+
- Touch device as primary platform → ensure info is available another way
|
|
1129
1214
|
|
|
1130
1215
|
#### Rules
|
|
1131
|
-
-
|
|
1132
|
-
-
|
|
1133
|
-
-
|
|
1134
|
-
-
|
|
1216
|
+
- Plain text only. No icons, buttons, or links.
|
|
1217
|
+
- One sentence maximum.
|
|
1218
|
+
- Appears on hover (100–200ms delay) and keyboard focus. Never on click.
|
|
1219
|
+
- Dismiss: cursor leaves, focus moves, or `Escape`.
|
|
1135
1220
|
|
|
1221
|
+
#### ARIA
|
|
1136
1222
|
```tsx
|
|
1137
|
-
<
|
|
1138
|
-
<div role="
|
|
1139
|
-
|
|
1140
|
-
<div role="gridcell" aria-label="Monday, 15 January 2026" aria-selected="true" tabIndex={0} />
|
|
1141
|
-
<div role="gridcell" aria-label="Tuesday, 16 January 2026" aria-disabled="true" tabIndex={-1} />
|
|
1142
|
-
</div>
|
|
1143
|
-
<button aria-label="Previous month">‹</button>
|
|
1144
|
-
<button aria-label="Next month">›</button>
|
|
1145
|
-
</div>
|
|
1146
|
-
// Arrow keys: move between days
|
|
1147
|
-
// Enter: select day
|
|
1148
|
-
// Page Up/Down: navigate months
|
|
1149
|
-
// Home/End: first/last day of month
|
|
1150
|
-
// Single tabbable day with roving focus — not one tabstop per cell
|
|
1223
|
+
<button aria-label="Export as CSV" aria-describedby="tooltip-export">Export</button>
|
|
1224
|
+
<div role="tooltip" id="tooltip-export">Export table as CSV</div>
|
|
1225
|
+
// Tooltip text and aria-label must match for icon-only buttons
|
|
1151
1226
|
```
|
|
1152
1227
|
|
|
1153
1228
|
---
|
|
1154
1229
|
|
|
1155
|
-
###
|
|
1156
|
-
|
|
1157
|
-
**Applies to: `Spinner`**
|
|
1230
|
+
### Popover (Container, Simple Menu, Complex Menu)
|
|
1158
1231
|
|
|
1159
|
-
|
|
1160
|
-
- Sizes: `sm` (inline button) · `lg` (panels) · `xl` (page sections) · `xxl` (full-page)
|
|
1161
|
-
- Always pair with a label when standalone: "Loading…", "Saving…"
|
|
1162
|
-
- Do NOT use multiple spinners simultaneously — consolidate into one.
|
|
1232
|
+
**Applies to: `Popover`**
|
|
1163
1233
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
</div>
|
|
1168
|
-
// Completion: aria-live="polite"
|
|
1169
|
-
```
|
|
1234
|
+
#### When to use
|
|
1235
|
+
- Contextual rich content anchored to an element. User clicks to open.
|
|
1236
|
+
- Definitions, action menus, date picker triggers, quick-edit panels.
|
|
1170
1237
|
|
|
1171
|
-
|
|
1238
|
+
#### Do NOT use when
|
|
1239
|
+
- Single short sentence, no interaction → use Tooltip
|
|
1240
|
+
- Demands full attention, blocks UI → use Modal
|
|
1241
|
+
- Extended task the user interacts with over time → use Drawer
|
|
1242
|
+
- Navigation menu → use dedicated nav component
|
|
1172
1243
|
|
|
1173
|
-
|
|
1244
|
+
#### Types
|
|
1245
|
+
- **Container**: free-form rich content
|
|
1246
|
+
- **Simple menu**: flat list, 2–6 items, no grouping
|
|
1247
|
+
- **Complex menu**: grouped, with section headings, scrollable
|
|
1174
1248
|
|
|
1175
|
-
|
|
1249
|
+
#### Rules
|
|
1250
|
+
- Opens on **click/tap** only. Never on hover.
|
|
1251
|
+
- Dismissal: click outside, `Escape`, trigger toggle, or completing action.
|
|
1252
|
+
- Do not nest popovers inside other popovers.
|
|
1253
|
+
- For menus: selecting an item closes popover automatically.
|
|
1254
|
+
- For container with unsaved form input: do NOT auto-close on outside click.
|
|
1176
1255
|
|
|
1177
|
-
|
|
1178
|
-
-
|
|
1179
|
-
-
|
|
1180
|
-
-
|
|
1256
|
+
#### Positioning contract (shared `PopoverWrapper`)
|
|
1257
|
+
- Default placement: below trigger
|
|
1258
|
+
- Gap: `var(--space-tiny)`
|
|
1259
|
+
- Flip: only when not enough space below
|
|
1260
|
+
- Viewport safe margin: `var(--space-medium)` on all edges
|
|
1261
|
+
- Width: match trigger width, minimum 192px
|
|
1262
|
+
- Never re-implement per-component popover math — reuse `PopoverWrapper`
|
|
1181
1263
|
|
|
1264
|
+
#### ARIA
|
|
1182
1265
|
```tsx
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1266
|
+
// Trigger
|
|
1267
|
+
<button aria-haspopup="true" aria-expanded={isOpen}>Options</button>
|
|
1268
|
+
|
|
1269
|
+
// Menu type
|
|
1270
|
+
<div role="menu">
|
|
1271
|
+
<button role="menuitem">Edit</button>
|
|
1272
|
+
<button role="menuitem">Delete</button>
|
|
1186
1273
|
</div>
|
|
1187
|
-
// When content ready: aria-busy="false"
|
|
1188
|
-
```
|
|
1189
1274
|
|
|
1190
|
-
|
|
1275
|
+
// Container type
|
|
1276
|
+
<div role="dialog" aria-label="Filter options">...</div>
|
|
1191
1277
|
|
|
1192
|
-
|
|
1278
|
+
// On open: focus moves INTO popover (first interactive element)
|
|
1279
|
+
// On close: focus returns to trigger element
|
|
1280
|
+
// Escape: closes and returns focus
|
|
1281
|
+
// Arrow keys: navigate between menuitem elements
|
|
1282
|
+
```
|
|
1193
1283
|
|
|
1194
|
-
|
|
1284
|
+
---
|
|
1195
1285
|
|
|
1196
|
-
|
|
1197
|
-
- `ProgressBar`: measured progress (file upload, onboarding). Always show percentage or step count alongside bar.
|
|
1286
|
+
### Modal
|
|
1198
1287
|
|
|
1199
|
-
|
|
1200
|
-
<div role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={60}
|
|
1201
|
-
aria-label="Uploading file" />
|
|
1202
|
-
// Completion: aria-live="polite"
|
|
1203
|
-
```
|
|
1288
|
+
**Applies to: `Modal`**
|
|
1204
1289
|
|
|
1205
|
-
|
|
1290
|
+
#### When to use
|
|
1291
|
+
- User must complete a task or make a decision before continuing.
|
|
1292
|
+
- Confirmations, compact forms, critical warnings, media previews.
|
|
1206
1293
|
|
|
1207
|
-
|
|
1294
|
+
#### Do NOT use when
|
|
1295
|
+
- Informational only → use Toast or inline message
|
|
1296
|
+
- Long, multi-step, complex task → use full page or Drawer
|
|
1297
|
+
- Contextually anchored to an element → use Popover
|
|
1298
|
+
- User needs to reference background content → use Drawer
|
|
1208
1299
|
|
|
1209
|
-
|
|
1300
|
+
#### Rules
|
|
1301
|
+
- Every modal must have a **title**.
|
|
1302
|
+
- Every modal must have an **explicit dismiss** (× in header + cancel in footer).
|
|
1303
|
+
- **One primary action only.** Two equal actions → Secondary + Secondary, not two Primary.
|
|
1304
|
+
- Never open a modal from within a modal.
|
|
1305
|
+
- Never auto-open on page load.
|
|
1306
|
+
- `Escape` must close unless accidental dismissal would cause irreversible data loss.
|
|
1307
|
+
- Background page scroll must be disabled while modal is open.
|
|
1210
1308
|
|
|
1211
|
-
|
|
1212
|
-
-
|
|
1213
|
-
-
|
|
1309
|
+
#### Interaction
|
|
1310
|
+
- Opening: explicit user action only.
|
|
1311
|
+
- Focus moves INTO modal on open (first interactive element or container).
|
|
1312
|
+
- Focus is **trapped** within modal while open.
|
|
1313
|
+
- Focus returns to triggering element on close.
|
|
1214
1314
|
|
|
1315
|
+
#### ARIA
|
|
1215
1316
|
```tsx
|
|
1216
|
-
|
|
1217
|
-
<
|
|
1317
|
+
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
|
|
1318
|
+
<h2 id="modal-title">Delete project</h2>
|
|
1319
|
+
<p id="modal-desc">This will permanently delete the project and all its data. This cannot be undone.</p>
|
|
1320
|
+
<div aria-hidden="true" class="overlay" /> {/* backdrop is aria-hidden */}
|
|
1321
|
+
</div>
|
|
1218
1322
|
```
|
|
1219
1323
|
|
|
1220
1324
|
---
|
|
1221
1325
|
|
|
1222
|
-
###
|
|
1223
|
-
|
|
1224
|
-
**Applies to: `Shortcut`**
|
|
1326
|
+
### Input — shared anatomy
|
|
1225
1327
|
|
|
1226
|
-
|
|
1227
|
-
- Sizes: `sm` (tooltips/menus) · `md` (standalone/help panels)
|
|
1228
|
-
- Colors: `Default` (light BG) · `Inverted` (dark BG)
|
|
1229
|
-
- Use platform-standard notation. Detect platform — do not mix Mac/Windows symbols.
|
|
1230
|
-
- Keep to keys only: `⌘S` not `⌘ Save`
|
|
1328
|
+
**Applies to: all input components**
|
|
1231
1329
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1330
|
+
#### Anatomy
|
|
1331
|
+
```
|
|
1332
|
+
[Label] ← always required (visible or aria-label)
|
|
1333
|
+
[Control] ← must have visible boundary
|
|
1334
|
+
[Helper text] ← optional; replace with real content or hide — never leave placeholder text
|
|
1335
|
+
[Error message] ← replaces helper text in error state
|
|
1235
1336
|
```
|
|
1236
1337
|
|
|
1237
|
-
|
|
1338
|
+
#### States
|
|
1339
|
+
`Empty` · `Hover` · `Active` · `Filled` · `Focused` · `Error` · `Disabled`
|
|
1238
1340
|
|
|
1239
|
-
|
|
1341
|
+
- `Empty` ≠ `Filled`: never conflate. `Empty` has no value, `Filled` always does.
|
|
1342
|
+
- `Active` ≠ `Focused`: Active = typing (cursor present). Focused = keyboard focus without typing.
|
|
1343
|
+
- `Disabled`: use native `disabled` attribute or `aria-disabled="true"`. Not tab-focusable.
|
|
1240
1344
|
|
|
1241
|
-
|
|
1345
|
+
#### Validation timing
|
|
1346
|
+
- **Default**: validate on **blur** (leaving the field).
|
|
1347
|
+
- **Real-time** only for: password strength, character count limits, search/filter fields, OTP.
|
|
1348
|
+
- On form submit: show **all errors at once**, scroll to and focus the first errored field.
|
|
1349
|
+
- Once shown, re-validate in real time as user corrects. Remove error when value becomes valid.
|
|
1350
|
+
- Never hide error on blur alone — only when value is actually valid.
|
|
1242
1351
|
|
|
1243
|
-
|
|
1352
|
+
#### Label rules
|
|
1353
|
+
- Required for all inputs.
|
|
1354
|
+
- Visible label above control is default and preferred.
|
|
1355
|
+
- Left-of-control only in Horizontal input layouts.
|
|
1356
|
+
- Never use placeholder text as label substitute.
|
|
1357
|
+
- Label describes what the field contains: "Email address" not "Enter your email address".
|
|
1358
|
+
- Every field is mandatory by default. Only mark optional fields with an "Optional" label in `var(--text-tertiary)` placed inline after the label text. Never use asterisks.
|
|
1359
|
+
- Add `aria-required="true"` on all mandatory inputs regardless of visual treatment.
|
|
1244
1360
|
|
|
1361
|
+
#### ARIA — all inputs
|
|
1362
|
+
```tsx
|
|
1363
|
+
<label htmlFor="email">Email address</label>
|
|
1364
|
+
<input
|
|
1365
|
+
id="email"
|
|
1366
|
+
aria-describedby="email-helper email-error"
|
|
1367
|
+
aria-invalid={hasError}
|
|
1368
|
+
aria-required={required}
|
|
1369
|
+
disabled={disabled}
|
|
1370
|
+
/>
|
|
1371
|
+
<p id="email-helper">We'll only use this to send receipts.</p>
|
|
1372
|
+
<p id="email-error" aria-live="polite">Email address is invalid.</p>
|
|
1245
1373
|
```
|
|
1246
|
-
Does selection require explicit confirmation?
|
|
1247
|
-
Yes → Radio (regardless of option count)
|
|
1248
|
-
No → How many options?
|
|
1249
|
-
2–3 → Segment control
|
|
1250
|
-
4+ → Select
|
|
1251
1374
|
|
|
1252
|
-
|
|
1253
|
-
Yes → add search box inside dropdown
|
|
1254
|
-
```
|
|
1375
|
+
---
|
|
1255
1376
|
|
|
1256
|
-
###
|
|
1377
|
+
### TextInput
|
|
1257
1378
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
4+ → Select dropdown + Choice list (Checkbox variant)
|
|
1379
|
+
**Applies to: `TextInput`**
|
|
1380
|
+
|
|
1381
|
+
Single-line free-form text. Use when no more specific input type applies.
|
|
1262
1382
|
|
|
1263
|
-
|
|
1264
|
-
→ Autocomplete multi
|
|
1265
|
-
```
|
|
1383
|
+
Do NOT use for multi-line → TextArea | numbers → NumberInput | passwords → PasswordInput | phone → PhoneInput | search → SearchInput | predefined list → Select.
|
|
1266
1384
|
|
|
1267
|
-
|
|
1385
|
+
---
|
|
1268
1386
|
|
|
1269
|
-
|
|
1270
|
-
Context?
|
|
1271
|
-
Toolbar, icon-only, immediate → Toggle button (+ mandatory Tooltip)
|
|
1272
|
-
Form, deferred, confirmed on submit → Checkbox
|
|
1273
|
-
Settings, labeled, immediate → Switcher
|
|
1274
|
-
```
|
|
1387
|
+
### TextArea
|
|
1275
1388
|
|
|
1276
|
-
|
|
1389
|
+
**Applies to: `TextArea`**
|
|
1277
1390
|
|
|
1278
|
-
|
|
1279
|
-
How many filter values?
|
|
1280
|
-
1 → Toggle button
|
|
1281
|
-
2–5 → Chips in a horizontal bar
|
|
1282
|
-
6+ → "Filters" button → dropdown (6–12) or drawer (13+)
|
|
1391
|
+
Multi-line free-form text.
|
|
1283
1392
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1393
|
+
- Minimum height: 3 lines
|
|
1394
|
+
- Allow vertical resize (desktop)
|
|
1395
|
+
- Show character count when limit exists: "120 characters remaining" not "380/500"
|
|
1286
1396
|
|
|
1287
|
-
|
|
1397
|
+
---
|
|
1288
1398
|
|
|
1289
|
-
|
|
1290
|
-
Content type?
|
|
1291
|
-
Data tables → Pagination
|
|
1292
|
-
Feeds → Infinite scroll
|
|
1399
|
+
### SearchInput
|
|
1293
1400
|
|
|
1294
|
-
|
|
1295
|
-
```
|
|
1401
|
+
**Applies to: `SearchInput`**
|
|
1296
1402
|
|
|
1297
|
-
|
|
1403
|
+
Search and filter interactions.
|
|
1298
1404
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1405
|
+
- **Bordered**: standard, full border
|
|
1406
|
+
- **Borderless**: inline in toolbars/headers
|
|
1407
|
+
- Always include clear (×) button when field has value.
|
|
1408
|
+
- `Error` state is for system errors (service unavailable) — NOT for "no results".
|
|
1409
|
+
- "No results" → show empty state in results area, not an input error.
|
|
1303
1410
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
Section/card within page → Skeleton or Spinner (lg)
|
|
1308
|
-
Inline within a button → Spinner (sm), button disabled
|
|
1309
|
-
Background (user not waiting) → No indicator; Toast on completion
|
|
1411
|
+
```tsx
|
|
1412
|
+
<input type="search" />
|
|
1413
|
+
<button aria-label="Clear search">×</button>
|
|
1310
1414
|
```
|
|
1311
1415
|
|
|
1312
|
-
|
|
1416
|
+
---
|
|
1313
1417
|
|
|
1314
|
-
|
|
1315
|
-
User must act before continuing?
|
|
1316
|
-
Yes:
|
|
1317
|
-
Critical/destructive confirmation → Alert dialog
|
|
1318
|
-
Other task or form → Modal
|
|
1319
|
-
No:
|
|
1320
|
-
Content anchored to an element?
|
|
1321
|
-
Yes → Popover (rich) or Tooltip (plain text, 1 line)
|
|
1322
|
-
No → Toast (notification) or Drawer (extended task)
|
|
1323
|
-
```
|
|
1418
|
+
### PasswordInput
|
|
1324
1419
|
|
|
1325
|
-
|
|
1420
|
+
**Applies to: `PasswordInput`**
|
|
1326
1421
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1422
|
+
- Always include show/hide toggle.
|
|
1423
|
+
- Show strength indicator + requirements list for new passwords.
|
|
1424
|
+
- Validate confirm password on blur of confirm field only.
|
|
1425
|
+
- `Revealed` state: type switches from `password` to `text`.
|
|
1331
1426
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
Error (failed to load) → See Error pattern; Retry CTA
|
|
1427
|
+
```tsx
|
|
1428
|
+
<input type="password" autocomplete="current-password" />
|
|
1429
|
+
// or new-password for registration
|
|
1430
|
+
<button aria-label="Show password" /> // toggles to "Hide password"
|
|
1337
1431
|
```
|
|
1338
1432
|
|
|
1339
|
-
|
|
1433
|
+
---
|
|
1340
1434
|
|
|
1341
|
-
|
|
1342
|
-
Error scope?
|
|
1343
|
-
Single form field → Inline validation message
|
|
1344
|
-
Section within a page → Error pattern (inline, embedded)
|
|
1345
|
-
Entire page/flow blocked → Error screen (full-screen replacement)
|
|
1346
|
-
```
|
|
1435
|
+
### OTPInput
|
|
1347
1436
|
|
|
1348
|
-
|
|
1437
|
+
**Applies to: `OTPInput`**
|
|
1349
1438
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
```
|
|
1439
|
+
- Auto-advance: entering a character moves focus to next field.
|
|
1440
|
+
- Auto-submit: when last field filled (fixed length), trigger verification automatically.
|
|
1441
|
+
- Paste handling: full code must populate all fields at once.
|
|
1442
|
+
- Backspace: moves focus to previous field and clears it.
|
|
1355
1443
|
|
|
1356
|
-
|
|
1444
|
+
States: `Empty` · `Partially filled` · `Verifying` · `Correct` · `Error` · `Disabled`
|
|
1357
1445
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1446
|
+
```tsx
|
|
1447
|
+
<div role="group" aria-label="Enter 6-digit verification code">
|
|
1448
|
+
<input aria-label="Digit 1 of 6" />
|
|
1449
|
+
<input aria-label="Digit 2 of 6" />
|
|
1450
|
+
...
|
|
1451
|
+
</div>
|
|
1452
|
+
// Verifying state: aria-live announcement
|
|
1453
|
+
// Correct/Error states: must be announced
|
|
1454
|
+
```
|
|
1362
1455
|
|
|
1363
1456
|
---
|
|
1364
1457
|
|
|
1365
|
-
|
|
1458
|
+
### NumberInput
|
|
1366
1459
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1460
|
+
**Applies to: `NumberInput`**
|
|
1461
|
+
|
|
1462
|
+
- Always define and enforce min/max where applicable.
|
|
1463
|
+
- Stepper buttons disable at boundaries.
|
|
1464
|
+
- Always allow direct keyboard entry (steppers are not the only input method).
|
|
1465
|
+
- Do NOT use for currency, ranges (use Slider), or large numeric IDs.
|
|
1370
1466
|
|
|
1371
1467
|
---
|
|
1372
1468
|
|
|
1373
|
-
###
|
|
1374
|
-
Every page MUST use `AppShell` as its root layout component.
|
|
1375
|
-
Never build page layout manually with divs.
|
|
1376
|
-
✅ `<AppShell header={...} sidebar={...}>...</AppShell>`
|
|
1377
|
-
❌ `<div style={{ display: 'flex' }}>...</div>`
|
|
1469
|
+
### PhoneInput
|
|
1378
1470
|
|
|
1379
|
-
|
|
1471
|
+
**Applies to: `PhoneInput`**
|
|
1380
1472
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
3. **User profile + Settings** — bottom of sidebar
|
|
1473
|
+
- Always use `PhoneInput` for international phone numbers (not a plain TextInput).
|
|
1474
|
+
- Pre-select user's locale as default country code.
|
|
1475
|
+
- Allow paste of full international number — auto-populate country selector and number field.
|
|
1476
|
+
- Uses `PopoverWrapper` for dropdown (do not re-implement positioning).
|
|
1386
1477
|
|
|
1387
|
-
|
|
1478
|
+
```tsx
|
|
1479
|
+
<div role="group" aria-label="Phone number">
|
|
1480
|
+
<select aria-label="Country code" autocomplete="tel-country-code" />
|
|
1481
|
+
<input aria-label="Phone number" autocomplete="tel" />
|
|
1482
|
+
</div>
|
|
1483
|
+
```
|
|
1388
1484
|
|
|
1389
1485
|
---
|
|
1390
1486
|
|
|
1391
|
-
###
|
|
1392
|
-
NEVER show back/forward navigation arrows in the Navbar on top-level section pages.
|
|
1393
|
-
Top-level pages are direct sidebar navigation items (e.g. Users, Reports, Pipeline).
|
|
1394
|
-
✅ Users page → no back arrow
|
|
1395
|
-
✅ Reports page → no back arrow
|
|
1396
|
-
❌ Users page → back arrow shown
|
|
1487
|
+
### Select
|
|
1397
1488
|
|
|
1398
|
-
|
|
1489
|
+
**Applies to: `Select`**
|
|
1399
1490
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1491
|
+
- Use for single-value selection from a fixed list when 7+ options or space-constrained.
|
|
1492
|
+
- Do NOT use for ≤6 options with space → use Radio instead.
|
|
1493
|
+
- Always include a placeholder option that is NOT a valid value.
|
|
1494
|
+
- Add search/filter within dropdown for 20+ options.
|
|
1495
|
+
|
|
1496
|
+
```tsx
|
|
1497
|
+
// Native when possible
|
|
1498
|
+
<select aria-required={required} aria-invalid={hasError}>
|
|
1499
|
+
<option value="">Select country</option>
|
|
1500
|
+
</select>
|
|
1501
|
+
|
|
1502
|
+
// Custom implementation
|
|
1503
|
+
<div role="combobox" aria-expanded={open} aria-haspopup="listbox" aria-controls="list">
|
|
1504
|
+
<div id="list" role="listbox">
|
|
1505
|
+
<div role="option" aria-selected="true">Portugal</div>
|
|
1506
|
+
</div>
|
|
1507
|
+
```
|
|
1406
1508
|
|
|
1407
1509
|
---
|
|
1408
1510
|
|
|
1409
|
-
###
|
|
1511
|
+
### AutocompleteMulti
|
|
1410
1512
|
|
|
1411
|
-
**
|
|
1412
|
-
Show plain section title in the Navbar. No breadcrumbs.
|
|
1413
|
-
✅ Navbar: "Users"
|
|
1414
|
-
✅ Navbar: "Reports"
|
|
1513
|
+
**Applies to: `AutocompleteMulti`**
|
|
1415
1514
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
The current page name (rightmost item) is bold and non-interactive.
|
|
1420
|
-
Parent items are interactive links.
|
|
1515
|
+
- Search-and-select multiple values from large/dynamic lists.
|
|
1516
|
+
- Each selected value becomes a chip inside the input.
|
|
1517
|
+
- Backspace on empty field removes last chip.
|
|
1421
1518
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1519
|
+
```tsx
|
|
1520
|
+
<div role="combobox" aria-multiselectable="true" aria-expanded={open}>
|
|
1521
|
+
<button aria-label="Remove Portugal">×</button>
|
|
1522
|
+
<div role="listbox">
|
|
1523
|
+
<div role="option" aria-selected="true">Portugal</div>
|
|
1524
|
+
</div>
|
|
1525
|
+
// Chip add/remove: aria-live="polite"
|
|
1526
|
+
```
|
|
1425
1527
|
|
|
1426
1528
|
---
|
|
1427
1529
|
|
|
1428
|
-
###
|
|
1530
|
+
### HorizontalInput
|
|
1429
1531
|
|
|
1430
|
-
|
|
1532
|
+
**Applies to: `HorizontalInput`**
|
|
1431
1533
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1534
|
+
- Label left, control right — layout wrapper only.
|
|
1535
|
+
- Use in dense settings panels, property editors.
|
|
1536
|
+
- Do NOT use in standard forms, mobile layouts, or with long labels.
|
|
1537
|
+
- Label must not wrap — keep to 3–4 words maximum.
|
|
1538
|
+
- Consistent label widths within a group.
|
|
1436
1539
|
|
|
1437
|
-
|
|
1438
|
-
NEVER use large breadcrumbs inside content areas.
|
|
1540
|
+
---
|
|
1439
1541
|
|
|
1440
|
-
|
|
1441
|
-
✅ Inside content: small breadcrumb "Permissions > Edit role"
|
|
1442
|
-
❌ Navbar: small breadcrumb
|
|
1443
|
-
❌ Content area: large breadcrumb
|
|
1542
|
+
### Slider
|
|
1444
1543
|
|
|
1445
|
-
|
|
1544
|
+
**Applies to: `Slider`**
|
|
1446
1545
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
-
|
|
1450
|
-
-
|
|
1451
|
-
-
|
|
1452
|
-
- Never truncate the current page item
|
|
1546
|
+
- `Max value` type: one handle
|
|
1547
|
+
- `Range` type: two handles — prevent handles from crossing
|
|
1548
|
+
- Always show current value alongside handle (tooltip or persistent label).
|
|
1549
|
+
- Display min/max labels at track ends.
|
|
1550
|
+
- Pair with NumberInput when precision matters.
|
|
1453
1551
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1552
|
+
```tsx
|
|
1553
|
+
<div aria-label="Price range" aria-labelledby="slider-label">
|
|
1554
|
+
<input role="slider"
|
|
1555
|
+
aria-valuenow={120} aria-valuemin={0} aria-valuemax={500}
|
|
1556
|
+
aria-valuetext="€120"
|
|
1557
|
+
aria-label="Minimum price" />
|
|
1558
|
+
</div>
|
|
1559
|
+
// Arrow keys: ±1 step
|
|
1560
|
+
// Page Up/Down: larger increment
|
|
1561
|
+
// Home/End: jump to min/max
|
|
1562
|
+
```
|
|
1458
1563
|
|
|
1459
1564
|
---
|
|
1460
1565
|
|
|
1461
|
-
###
|
|
1462
|
-
The primary action for a section always sits in the Navbar, to the right of the title.
|
|
1463
|
-
NEVER place the primary CTA inside a Card header or content area on a top-level page.
|
|
1566
|
+
### UploadArea
|
|
1464
1567
|
|
|
1465
|
-
|
|
1466
|
-
"New user", "New deal", "Create report", "New email"
|
|
1568
|
+
**Applies to: `UploadArea`**
|
|
1467
1569
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1570
|
+
- Always provide click-to-browse fallback (not all users can drag-drop).
|
|
1571
|
+
- State accepted file types and size limits in Empty state.
|
|
1572
|
+
- Reject invalid file types immediately on drop with clear error.
|
|
1573
|
+
- Show upload progress after file accepted.
|
|
1574
|
+
|
|
1575
|
+
```tsx
|
|
1576
|
+
<div role="button" aria-label="Upload files — click or drag and drop"
|
|
1577
|
+
aria-describedby="upload-constraints" tabIndex={0}>
|
|
1578
|
+
// Enter/Space when focused: opens system file picker
|
|
1579
|
+
// Dropping state: aria-live announcement
|
|
1580
|
+
```
|
|
1470
1581
|
|
|
1471
1582
|
---
|
|
1472
1583
|
|
|
1473
|
-
###
|
|
1474
|
-
When a page has multiple actions, use this hierarchy in the Navbar (right side):
|
|
1584
|
+
### ChipsInput
|
|
1475
1585
|
|
|
1476
|
-
|
|
1477
|
-
|---|---|---|
|
|
1478
|
-
| Primary object creation | `Primary` | "New deal", "New user" |
|
|
1479
|
-
| Important but not primary | `Secondary` | "Import", "Publish" |
|
|
1480
|
-
| Utility / contextual actions | `Ghost` | "Share", "Edit", "Export" |
|
|
1586
|
+
**Applies to: `ChipsInput`**
|
|
1481
1587
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1588
|
+
- Free-form entry where each value becomes a chip.
|
|
1589
|
+
- Confirm with `Enter` (or comma/Tab as defined per product).
|
|
1590
|
+
- Validate each value before converting to chip.
|
|
1591
|
+
- Backspace on empty field removes last chip.
|
|
1484
1592
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1593
|
+
```tsx
|
|
1594
|
+
// Each chip remove button
|
|
1595
|
+
<button aria-label="Remove javascript">×</button>
|
|
1596
|
+
// Chip additions/removals
|
|
1597
|
+
aria-live="polite"
|
|
1598
|
+
```
|
|
1488
1599
|
|
|
1489
1600
|
---
|
|
1490
1601
|
|
|
1491
|
-
###
|
|
1492
|
-
When a detail page has sub-sections with their own primary actions,
|
|
1493
|
-
those actions belong at the sub-section level — NOT in the top Navbar.
|
|
1602
|
+
### DatePicker / DateRangePicker
|
|
1494
1603
|
|
|
1495
|
-
|
|
1496
|
-
✅ Alice Johnson > Permissions tab → tab-level: [Add permission — Primary]
|
|
1497
|
-
❌ Alice Johnson page → Navbar has both "Save changes" AND "Add permission"
|
|
1604
|
+
**Applies to: `DateTimePicker`**
|
|
1498
1605
|
|
|
1499
|
-
|
|
1606
|
+
- `Single date`: selects one date
|
|
1607
|
+
- `Date range`: start + end date — start cannot be after end
|
|
1500
1608
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
Description line is optional — use it when the section needs clarification,
|
|
1505
|
-
omit it when the title is self-explanatory.
|
|
1609
|
+
#### Display modes
|
|
1610
|
+
- **Standalone**: always visible
|
|
1611
|
+
- **Popover**: triggered by clicking input field (uses `PopoverWrapper`)
|
|
1506
1612
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1613
|
+
#### Rules
|
|
1614
|
+
- Show human-readable format in trigger input: "15 Jan 2026" not "2026-01-15"
|
|
1615
|
+
- Communicate date constraints before user opens calendar, not just inside it
|
|
1616
|
+
- For range: show both months side by side when range spans months
|
|
1617
|
+
- Do NOT pre-select today for historical date entry (e.g. date of birth)
|
|
1511
1618
|
|
|
1512
|
-
|
|
1619
|
+
```tsx
|
|
1620
|
+
<input aria-haspopup="dialog" aria-expanded={open} />
|
|
1621
|
+
<div role="dialog" aria-label="Choose date range">
|
|
1622
|
+
<div role="grid">
|
|
1623
|
+
<div role="gridcell" aria-label="Monday, 15 January 2026" aria-selected="true" tabIndex={0} />
|
|
1624
|
+
<div role="gridcell" aria-label="Tuesday, 16 January 2026" aria-disabled="true" tabIndex={-1} />
|
|
1625
|
+
</div>
|
|
1626
|
+
<button aria-label="Previous month">‹</button>
|
|
1627
|
+
<button aria-label="Next month">›</button>
|
|
1628
|
+
</div>
|
|
1629
|
+
// Arrow keys: move between days
|
|
1630
|
+
// Enter: select day
|
|
1631
|
+
// Page Up/Down: navigate months
|
|
1632
|
+
// Home/End: first/last day of month
|
|
1633
|
+
// Single tabbable day with roving focus — not one tabstop per cell
|
|
1634
|
+
```
|
|
1513
1635
|
|
|
1514
|
-
|
|
1515
|
-
ALL page titles, Navbar titles, breadcrumb items, and section headers use sentence case.
|
|
1516
|
-
NEVER title case.
|
|
1636
|
+
---
|
|
1517
1637
|
|
|
1518
|
-
|
|
1519
|
-
❌ "Personal Info", "New User", "Save Changes"
|
|
1638
|
+
### Spinner
|
|
1520
1639
|
|
|
1521
|
-
|
|
1640
|
+
**Applies to: `Spinner`**
|
|
1522
1641
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
Use SectionHeader + flat content when sections are part of a continuous page flow.
|
|
1642
|
+
- Use when operation is in progress and duration is unknown.
|
|
1643
|
+
- Sizes: `sm` (inline button) · `lg` (panels) · `xl` (page sections) · `xxl` (full-page)
|
|
1644
|
+
- Always pair with a label when standalone: "Loading…", "Saving…"
|
|
1645
|
+
- Do NOT use multiple spinners simultaneously — consolidate into one.
|
|
1528
1646
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1647
|
+
```tsx
|
|
1648
|
+
<div role="status" aria-label="Loading results">
|
|
1649
|
+
<svg aria-hidden="true" />
|
|
1650
|
+
</div>
|
|
1651
|
+
// Completion: aria-live="polite"
|
|
1652
|
+
```
|
|
1532
1653
|
|
|
1533
1654
|
---
|
|
1534
1655
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
These rules apply globally. Apply them before building any screen or component.
|
|
1656
|
+
### SkeletonLoading
|
|
1538
1657
|
|
|
1539
|
-
|
|
1658
|
+
**Applies to: `SkeletonLoading`**
|
|
1540
1659
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1660
|
+
- Use when content takes >~300ms to load and shape is known.
|
|
1661
|
+
- Types: `Avatar` · `Pill` · `Image` · `Card`
|
|
1662
|
+
- Always animate (shimmer/pulse) — never show static skeleton shapes.
|
|
1663
|
+
- Replace all skeletons simultaneously when content is ready — no piecemeal replacement.
|
|
1544
1664
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1665
|
+
```tsx
|
|
1666
|
+
<div aria-busy="true" aria-label="Loading">
|
|
1667
|
+
<div aria-hidden="true" class="skeleton-avatar" />
|
|
1668
|
+
<div aria-hidden="true" class="skeleton-pill" />
|
|
1669
|
+
</div>
|
|
1670
|
+
// When content ready: aria-busy="false"
|
|
1671
|
+
```
|
|
1547
1672
|
|
|
1548
1673
|
---
|
|
1549
1674
|
|
|
1550
|
-
###
|
|
1551
|
-
All charts, graphs, and data visualisations must use `--color-dataviz-*` tokens from `tokens.css` for all colors (series, axes, labels, backgrounds).
|
|
1552
|
-
Never use hardcoded hex values or generic semantic tokens for dataviz color.
|
|
1553
|
-
|
|
1554
|
-
✅ `fill: var(--color-dataviz-1)`
|
|
1555
|
-
❌ `fill: #0F766E`
|
|
1556
|
-
❌ `fill: var(--background-accent)`
|
|
1675
|
+
### ProgressBar / LoadingBar
|
|
1557
1676
|
|
|
1558
|
-
|
|
1677
|
+
**Applies to: `ProgressBar`, `LoadingBar`**
|
|
1559
1678
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
This applies to every container without exception: pages, cards, modals, drawers, tables, lists, and form sections.
|
|
1563
|
-
Never introduce independent `padding-left`, `padding-right`, `margin-left`, or `margin-right` values that deviate from this token.
|
|
1679
|
+
- `LoadingBar`: page-level navigation only, top of viewport. Never inline.
|
|
1680
|
+
- `ProgressBar`: measured progress (file upload, onboarding). Always show percentage or step count alongside bar.
|
|
1564
1681
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1682
|
+
```tsx
|
|
1683
|
+
<div role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={60}
|
|
1684
|
+
aria-label="Uploading file" />
|
|
1685
|
+
// Completion: aria-live="polite"
|
|
1686
|
+
```
|
|
1568
1687
|
|
|
1569
1688
|
---
|
|
1570
1689
|
|
|
1571
|
-
###
|
|
1572
|
-
Every card must apply internal padding using the correct inset token.
|
|
1573
|
-
Content must never touch the card border.
|
|
1690
|
+
### Scrollbar
|
|
1574
1691
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1692
|
+
**Applies to: `Scrollbar`**
|
|
1693
|
+
|
|
1694
|
+
- Never hide scrollbar entirely in scrollable areas.
|
|
1695
|
+
- Do not use both vertical and horizontal scrollbars on same container unless content is truly 2D.
|
|
1696
|
+
- Custom scrollbars are visual overlays only — underlying scroll must remain keyboard accessible.
|
|
1697
|
+
|
|
1698
|
+
```tsx
|
|
1699
|
+
// Scrollable container
|
|
1700
|
+
<div tabIndex={0} /> // allows keyboard focus and arrow key scrolling
|
|
1701
|
+
```
|
|
1577
1702
|
|
|
1578
1703
|
---
|
|
1579
1704
|
|
|
1580
|
-
###
|
|
1581
|
-
The only permitted icon library is Lucide (`lucide-react`).
|
|
1582
|
-
Never use browser/OS-native icons, emoji, or icons from any other library.
|
|
1583
|
-
This applies everywhere: tables, buttons, inputs, menus, empty states, and all other components.
|
|
1705
|
+
### Shortcut
|
|
1584
1706
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1707
|
+
**Applies to: `Shortcut`**
|
|
1708
|
+
|
|
1709
|
+
- Non-interactive display element only.
|
|
1710
|
+
- Sizes: `sm` (tooltips/menus) · `md` (standalone/help panels)
|
|
1711
|
+
- Colors: `Default` (light BG) · `Inverted` (dark BG)
|
|
1712
|
+
- Use platform-standard notation. Detect platform — do not mix Mac/Windows symbols.
|
|
1713
|
+
- Keep to keys only: `⌘S` not `⌘ Save`
|
|
1714
|
+
|
|
1715
|
+
```tsx
|
|
1716
|
+
// Key symbols not reliably announced by screen readers
|
|
1717
|
+
<kbd aria-label="Command S">⌘S</kbd>
|
|
1718
|
+
```
|
|
1588
1719
|
|
|
1589
1720
|
---
|
|
1590
1721
|
|
|
1591
|
-
|
|
1592
|
-
List items inside any container (modal, drawer, card, page section) must stretch to fill the full available width.
|
|
1593
|
-
Never let list items float at an arbitrary width or leave unexplained whitespace to the right.
|
|
1722
|
+
## Cross-component patterns
|
|
1594
1723
|
|
|
1595
|
-
|
|
1596
|
-
❌ List items sized to content, leaving empty space on the right
|
|
1724
|
+
These patterns are **decision rules**. Apply before selecting a component.
|
|
1597
1725
|
|
|
1598
|
-
|
|
1726
|
+
### Pattern 1 — Single select
|
|
1599
1727
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1728
|
+
```
|
|
1729
|
+
Does selection require explicit confirmation?
|
|
1730
|
+
Yes → Radio (regardless of option count)
|
|
1731
|
+
No → How many options?
|
|
1732
|
+
2–3 → Segment control
|
|
1733
|
+
4+ → Select
|
|
1603
1734
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
- **Help/hint icons** → replace with meaningful tooltip content, or hide
|
|
1735
|
+
Select has 20+ items?
|
|
1736
|
+
Yes → add search box inside dropdown
|
|
1737
|
+
```
|
|
1608
1738
|
|
|
1609
|
-
|
|
1610
|
-
✅ Icon slot hidden when no meaningful icon applies
|
|
1611
|
-
❌ Help text reads "Help text"
|
|
1612
|
-
❌ Placeholder diamond icon left inside a Select component
|
|
1739
|
+
### Pattern 2 — Multi select
|
|
1613
1740
|
|
|
1614
|
-
|
|
1741
|
+
```
|
|
1742
|
+
How many options?
|
|
1743
|
+
Up to 3 → Chips (shown upfront, immediately toggleable)
|
|
1744
|
+
4+ → Select dropdown + Choice list (Checkbox variant)
|
|
1615
1745
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1746
|
+
Option set is large (20+), dynamic, or needs typing to find?
|
|
1747
|
+
→ Autocomplete multi
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
### Pattern 3 — Binary (on/off)
|
|
1751
|
+
|
|
1752
|
+
```
|
|
1753
|
+
Context?
|
|
1754
|
+
Toolbar, icon-only, immediate → Toggle button (+ mandatory Tooltip)
|
|
1755
|
+
Form, deferred, confirmed on submit → Checkbox
|
|
1756
|
+
Settings, labeled, immediate → Switcher
|
|
1757
|
+
```
|
|
1620
1758
|
|
|
1621
|
-
|
|
1622
|
-
✅ Fields with no qualifier → assumed mandatory
|
|
1623
|
-
❌ `First name *` with a red asterisk
|
|
1624
|
-
❌ `aria-required="true"` shown visually as an asterisk
|
|
1759
|
+
### Pattern 4 — Filter bar
|
|
1625
1760
|
|
|
1626
|
-
|
|
1761
|
+
```
|
|
1762
|
+
How many filter values?
|
|
1763
|
+
1 → Toggle button
|
|
1764
|
+
2–5 → Chips in a horizontal bar
|
|
1765
|
+
6+ → "Filters" button → dropdown (6–12) or drawer (13+)
|
|
1627
1766
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
On submit, validate all fields and display all errors simultaneously.
|
|
1631
|
-
Do not validate in real time as the user types — only on submit attempt.
|
|
1767
|
+
Always: show active state, provide "Clear all" action when any filter active
|
|
1768
|
+
```
|
|
1632
1769
|
|
|
1633
|
-
|
|
1770
|
+
### Pattern 5 — Pagination vs Infinite scroll
|
|
1634
1771
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1772
|
+
```
|
|
1773
|
+
Content type?
|
|
1774
|
+
Data tables → Pagination
|
|
1775
|
+
Feeds → Infinite scroll
|
|
1638
1776
|
|
|
1639
|
-
|
|
1777
|
+
Never mix patterns within the same view.
|
|
1778
|
+
```
|
|
1640
1779
|
|
|
1641
|
-
###
|
|
1642
|
-
The primary action button in every form must display the `⌘↵` keyboard shortcut hint using the `Shortcut` component.
|
|
1643
|
-
`Cmd+Enter` is the standard submit shortcut across all forms.
|
|
1780
|
+
### Pattern 6 — Loading state
|
|
1644
1781
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1782
|
+
```
|
|
1783
|
+
Shape of loading content known?
|
|
1784
|
+
Yes → Skeleton loading
|
|
1785
|
+
No → Spinner
|
|
1647
1786
|
|
|
1648
|
-
|
|
1787
|
+
Scope?
|
|
1788
|
+
Full page navigation → Loading bar
|
|
1789
|
+
Full page/panel content → Skeleton or Spinner (xl/xxl)
|
|
1790
|
+
Section/card within page → Skeleton or Spinner (lg)
|
|
1791
|
+
Inline within a button → Spinner (sm), button disabled
|
|
1792
|
+
Background (user not waiting) → No indicator; Toast on completion
|
|
1793
|
+
```
|
|
1649
1794
|
|
|
1650
|
-
###
|
|
1795
|
+
### Pattern 7 — Overlay
|
|
1651
1796
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1797
|
+
```
|
|
1798
|
+
User must act before continuing?
|
|
1799
|
+
Yes:
|
|
1800
|
+
Critical/destructive confirmation → Alert dialog
|
|
1801
|
+
Other task or form → Modal
|
|
1802
|
+
No:
|
|
1803
|
+
Content anchored to an element?
|
|
1804
|
+
Yes → Popover (rich) or Tooltip (plain text, 1 line)
|
|
1805
|
+
No → Toast (notification) or Drawer (extended task)
|
|
1806
|
+
```
|
|
1654
1807
|
|
|
1655
|
-
|
|
1656
|
-
|---------------------------|--------------|
|
|
1657
|
-
| Done, active, on track, success, approved, complete | success |
|
|
1658
|
-
| At risk, pending, in review, in progress, waiting | warning |
|
|
1659
|
-
| Behind, blocked, failed, rejected, overdue, error | error |
|
|
1660
|
-
| Draft, inactive, unknown, archived, neutral | neutral |
|
|
1661
|
-
| Informational, new, upcoming | info |
|
|
1808
|
+
### Pattern 8 — Empty state
|
|
1662
1809
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1810
|
+
```
|
|
1811
|
+
Context size?
|
|
1812
|
+
Primary page content area → lg (illustration + title + description + CTA)
|
|
1813
|
+
Card/panel/section → sm (icon + title + description + CTA)
|
|
1665
1814
|
|
|
1666
|
-
|
|
1815
|
+
Cause?
|
|
1816
|
+
First use (nothing created yet) → Invite. CTA to create first item.
|
|
1817
|
+
Filtered empty (no results) → "No results for '[query]'" + Clear filters CTA
|
|
1818
|
+
Permission restricted → Explain restriction + contact admin
|
|
1819
|
+
Error (failed to load) → See Error pattern; Retry CTA
|
|
1820
|
+
```
|
|
1667
1821
|
|
|
1668
|
-
###
|
|
1822
|
+
### Pattern 9 — Error
|
|
1669
1823
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1824
|
+
```
|
|
1825
|
+
Error scope?
|
|
1826
|
+
Single form field → Inline validation message
|
|
1827
|
+
Section within a page → Error pattern (inline, embedded)
|
|
1828
|
+
Entire page/flow blocked → Error screen (full-screen replacement)
|
|
1829
|
+
```
|
|
1673
1830
|
|
|
1674
|
-
|
|
1675
|
-
.page-content {
|
|
1676
|
-
padding-inline: var(--page-margin-x);
|
|
1677
|
-
}
|
|
1831
|
+
### Pattern 10 — Success feedback
|
|
1678
1832
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1833
|
+
```
|
|
1834
|
+
Action significance?
|
|
1835
|
+
Routine (save, update, delete) → Toast (Success, auto-dismiss 4–5s)
|
|
1836
|
+
Significant milestone/flow end → Success screen (Celebration or Confirmation)
|
|
1837
|
+
```
|
|
1683
1838
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1839
|
+
### Pattern 11 — Tabs vs Segment control (use both tests)
|
|
1840
|
+
|
|
1841
|
+
1. Does each option have a distinct content panel? → **Tabs**
|
|
1842
|
+
2. Does switching change how the same content is rendered? → **Segment control**
|
|
1843
|
+
3. Could each option exist as its own sub-page/route? → **Tabs**
|
|
1844
|
+
4. Is this a display preference, not an architectural division? → **Segment control**
|
|
1686
1845
|
|
|
1687
1846
|
---
|
|
1688
1847
|
|
|
@@ -1972,160 +2131,5 @@ export const DisabledVariant: Story = {
|
|
|
1972
2131
|
|
|
1973
2132
|
---
|
|
1974
2133
|
|
|
1975
|
-
*
|
|
1976
|
-
*Source files: `docs/foundations.md`, `docs/ds-guidelines.md`, and all component docs in `docs/`.*
|
|
1977
|
-
|
|
1978
|
-
---
|
|
1979
|
-
|
|
1980
|
-
---
|
|
1981
|
-
|
|
1982
|
-
## Consuming the Package — Layout & Navigation
|
|
1983
|
-
|
|
1984
|
-
> This section is for AI agents building apps with `@ngrr/ds`, not for building the library itself.
|
|
1985
|
-
|
|
1986
|
-
### Setup — mandatory CSS imports
|
|
1987
|
-
```tsx
|
|
1988
|
-
// main.tsx — must be first, before any other imports
|
|
1989
|
-
import '@ngrr/ds/style.css'
|
|
1990
|
-
import '@ngrr/ds/tokens.css'
|
|
1991
|
-
```
|
|
1992
|
-
|
|
1993
|
-
### Required CSS on the host app
|
|
1994
|
-
```css
|
|
1995
|
-
html, body { margin: 0; height: 100%; }
|
|
1996
|
-
#root { min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
1997
|
-
```
|
|
1998
|
-
|
|
1999
|
-
---
|
|
2000
|
-
|
|
2001
|
-
### AppShell — mandatory root layout
|
|
2002
|
-
|
|
2003
|
-
`AppShell` is the only permitted root layout wrapper. Every page must be inside it. Do not invent wrapper divs or custom layouts.
|
|
2004
|
-
```tsx
|
|
2005
|
-
import { AppShell, Navbar, Sidebar, Button, SearchBar, Avatar } from '@ngrr/ds';
|
|
2006
|
-
// Icons come from lucide-react — install separately: npm install lucide-react
|
|
2007
|
-
import { LayoutDashboard, FolderKanban, CheckSquare, Users, FileText, Settings } from 'lucide-react';
|
|
2008
|
-
|
|
2009
|
-
<AppShell
|
|
2010
|
-
header={
|
|
2011
|
-
<Navbar
|
|
2012
|
-
title="Dashboard"
|
|
2013
|
-
rightActions={<Button variant="primary" label="New project" />}
|
|
2014
|
-
/>
|
|
2015
|
-
}
|
|
2016
|
-
sidebar={
|
|
2017
|
-
<Sidebar
|
|
2018
|
-
selectorProps={{
|
|
2019
|
-
label: 'WorkScope',
|
|
2020
|
-
avatarProps: {
|
|
2021
|
-
size: 'xs',
|
|
2022
|
-
variant: 'organization',
|
|
2023
|
-
fill: 'initials',
|
|
2024
|
-
initials: 'WS',
|
|
2025
|
-
name: 'WorkScope',
|
|
2026
|
-
},
|
|
2027
|
-
}}
|
|
2028
|
-
showSearch
|
|
2029
|
-
searchSlot={<SearchBar placeholder="Search" />}
|
|
2030
|
-
sections={[
|
|
2031
|
-
{
|
|
2032
|
-
id: 'main',
|
|
2033
|
-
items: [
|
|
2034
|
-
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} />, selected: true, onClick: () => {} },
|
|
2035
|
-
{ id: 'projects', label: 'Projects', icon: <FolderKanban size={16} />, onClick: () => {} },
|
|
2036
|
-
{ id: 'tasks', label: 'Tasks', icon: <CheckSquare size={16} />, onClick: () => {} },
|
|
2037
|
-
{ id: 'people', label: 'People', icon: <Users size={16} />, onClick: () => {} },
|
|
2038
|
-
{ id: 'notes', label: 'Notes', icon: <FileText size={16} />, onClick: () => {} },
|
|
2039
|
-
],
|
|
2040
|
-
},
|
|
2041
|
-
]}
|
|
2042
|
-
bottomContent={
|
|
2043
|
-
<>
|
|
2044
|
-
<SidebarMenuSelector
|
|
2045
|
-
variant="full"
|
|
2046
|
-
label="Settings"
|
|
2047
|
-
showIcon
|
|
2048
|
-
icon={<Settings size={16} />}
|
|
2049
|
-
onClick={() => {}}
|
|
2050
|
-
/>
|
|
2051
|
-
<SidebarMenuSelector
|
|
2052
|
-
variant="full"
|
|
2053
|
-
label="User Name"
|
|
2054
|
-
showAvatar
|
|
2055
|
-
avatarProps={{ size: 'xs', fill: 'initials', initials: 'UN', name: 'User Name' }}
|
|
2056
|
-
onClick={() => {}}
|
|
2057
|
-
/>
|
|
2058
|
-
</>
|
|
2059
|
-
}
|
|
2060
|
-
/>
|
|
2061
|
-
}
|
|
2062
|
-
>
|
|
2063
|
-
{/* page content goes here */}
|
|
2064
|
-
</AppShell>
|
|
2065
|
-
```
|
|
2066
|
-
|
|
2067
|
-
**AppShell props:**
|
|
2068
|
-
- `header` — `<Navbar />` only
|
|
2069
|
-
- `sidebar` — `<Sidebar />` only. Never pass `collapsed` manually — AppShell injects it automatically
|
|
2070
|
-
- `children` — main page content
|
|
2071
|
-
- `rightPanel` — optional, omit if not needed
|
|
2072
|
-
- `footer` — optional, omit if not needed
|
|
2073
|
-
|
|
2074
|
-
**Forbidden:**
|
|
2075
|
-
- Do not wrap AppShell in any other layout element
|
|
2076
|
-
- Do not add sidebar items beyond what is specified
|
|
2077
|
-
- Do not invent extra sections, items, or nav elements
|
|
2078
|
-
|
|
2079
|
-
---
|
|
2080
|
-
|
|
2081
|
-
### Navbar props
|
|
2082
|
-
- `variant` — `'title'` (default) for top-level pages | `'breadcrumbs'` for detail views only
|
|
2083
|
-
- `title` — page title, sentence case
|
|
2084
|
-
- `rightActions` — primary CTA button, rightmost position
|
|
2085
|
-
- `leftActions` — secondary actions on the left, omit if not needed
|
|
2086
|
-
- `onBackClick` — only used with `variant="breadcrumbs"`
|
|
2087
|
-
- `breadcrumbItems` — required only when `variant="breadcrumbs"`
|
|
2088
|
-
|
|
2089
|
-
**Rules:**
|
|
2090
|
-
- Never show a back arrow on top-level pages (Dashboard, Projects, Tasks, People, Notes)
|
|
2091
|
-
- Back arrow only on detail views using `variant="breadcrumbs"`
|
|
2092
|
-
- Primary CTA always in `rightActions`
|
|
2093
|
-
|
|
2094
|
-
---
|
|
2095
|
-
|
|
2096
|
-
### Sidebar props
|
|
2097
|
-
- `sections` — required. Only include sections explicitly specified. Never invent extra items.
|
|
2098
|
-
- `selectorProps` — workspace selector at the top. Always set `label` and `avatarProps`.
|
|
2099
|
-
- `showSearch` — set to `true` to show the search bar
|
|
2100
|
-
- `searchSlot` — pass a `<SearchBar />` component when `showSearch` is true
|
|
2101
|
-
- `bottomContent` — use for Settings and User Name, pinned to bottom with a divider. Always include these two items.
|
|
2102
|
-
- Never pass `collapsed` — AppShell controls it automatically
|
|
2103
|
-
|
|
2104
|
-
**SidebarMenuItem shape:**
|
|
2105
|
-
```ts
|
|
2106
|
-
{
|
|
2107
|
-
id: string;
|
|
2108
|
-
label: string; // sentence case always
|
|
2109
|
-
icon?: React.ReactNode; // lucide-react icon at size={16}
|
|
2110
|
-
selected?: boolean; // true only for the active page
|
|
2111
|
-
onClick?: () => void;
|
|
2112
|
-
showBadge?: boolean;
|
|
2113
|
-
badgeCount?: number;
|
|
2114
|
-
badgeVariant?: BadgeVariant;
|
|
2115
|
-
showAvatar?: boolean;
|
|
2116
|
-
avatarProps?: AvatarProps;
|
|
2117
|
-
}
|
|
2118
|
-
```
|
|
2119
|
-
|
|
2120
|
-
---
|
|
2121
|
-
|
|
2122
|
-
### Hard rules — always follow
|
|
2123
|
-
- `AppShell` is always the root — no exceptions
|
|
2124
|
-
- `DataTable` must never be wrapped in a `Card`
|
|
2125
|
-
- No back arrow on top-level pages: Dashboard, Projects, Tasks, People, Notes
|
|
2126
|
-
- CTA hierarchy: Primary (object creation, rightmost in Navbar) → Secondary → Ghost (Export, Share, Edit)
|
|
2127
|
-
- All labels and copy: sentence case always ("New project" not "New Project")
|
|
2128
|
-
- Icons: from `lucide-react` — install with `npm install lucide-react`
|
|
2129
|
-
- Tag variants: `success` = completed, `progress` = in progress, `warning` = at risk, `neutral` = not started
|
|
2130
|
-
- Settings and User Name always pinned to sidebar bottom via `bottomContent`
|
|
2131
|
-
- Sidebar always full viewport height — guaranteed by AppShell CSS rules above
|
|
2134
|
+
*Single source of truth for consuming DS-Nagarro. Last updated: 2026-03-16.*
|
|
2135
|
+
*Source files: `docs/foundations.md`, `docs/ds-guidelines.md`, and all component docs in `docs/`.*
|