@ngrr/ds 0.1.12 → 0.1.15

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