@ngrr/ds 0.1.29 → 0.1.32

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