@cmgfi/clear-ds 1.0.1 → 1.1.1

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/dist/llms.txt ADDED
@@ -0,0 +1,1642 @@
1
+ # @cmgfi/clear-ds — Clear Design System
2
+
3
+ > CMG Financial's official React component library. The UI foundation for CMG's internal loan origination platform. 55+ fully-typed components, a complete CSS custom property token system, and Storybook documentation. Built with React, TypeScript, Vite, and CSS Modules.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # With PrimeIcons (recommended — required for icon-bearing components)
9
+ npm install @cmgfi/clear-ds primeicons
10
+
11
+ # Without PrimeIcons
12
+ npm install @cmgfi/clear-ds
13
+ ```
14
+
15
+ ## Setup (app root, once)
16
+
17
+ ```tsx
18
+ import '@cmgfi/clear-ds/tokens'; // CSS custom property definitions (:root)
19
+ import '@cmgfi/clear-ds/styles'; // compiled component styles
20
+ import 'primeicons/primeicons.css'; // omit if PrimeIcons not installed
21
+ ```
22
+
23
+ ```css
24
+ /* Required: token scale is built on 1rem = 12px */
25
+ html { font-size: 12px; }
26
+ ```
27
+
28
+ ## Basic usage
29
+
30
+ ```tsx
31
+ import { Button, InputText, Modal, DataTable } from '@cmgfi/clear-ds';
32
+
33
+ <Button variant="primary" size="md" onClick={handleSave}>Save application</Button>
34
+ <InputText label="First name" value={value} onChange={e => setValue(e.target.value)} />
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Tokens
40
+
41
+ The token system has two layers: **primitive tokens** (the raw scale) and **semantic tokens** (purpose-named aliases). Always reach for a semantic token first. Use a primitive only when no semantic alias covers your use case.
42
+
43
+ ### How to use tokens
44
+
45
+ ```css
46
+ /* In component CSS or consumer stylesheets */
47
+ .my-heading { color: var(--color-text-heading); }
48
+ .my-card { background: var(--color-surface-default); border: 1px solid var(--color-border-primary); }
49
+ .my-button { background: var(--color-brand-primary); border-radius: var(--radius-md); transition: background var(--transition-normal); }
50
+ .my-input:focus { box-shadow: var(--focus-ring); }
51
+ ```
52
+
53
+ ### Semantic tokens — Text Colors
54
+
55
+ | Token | Value | Use when |
56
+ |---|---|---|
57
+ | `--color-text-heading` | `--navy-800` | Page headings, section titles, panel headers |
58
+ | `--color-text-body` | `--navy-800` | Body copy, descriptions, content paragraphs |
59
+ | `--color-text-label` | `--navy-800` | Form field labels, data column headers |
60
+ | `--color-text-helper` | `--navy-400` | Helper text, subtitles, field descriptions |
61
+ | `--color-text-placeholder` | `--navy-300` | Input placeholder text |
62
+ | `--color-text-disabled` | `--navy-300` | Disabled input and control text |
63
+ | `--color-text-error` | `--red-600` | Validation error messages |
64
+ | `--color-text-link` | `--teal-700` | Hyperlinks, inline interactive text |
65
+ | `--color-text-link-hover` | `--teal-900` | Hovered hyperlinks |
66
+ | `--color-text-inverse` | `--surface-50` | Text on dark or brand-colored surfaces |
67
+
68
+ ### Semantic tokens — Text Sizes
69
+
70
+ Base: `1rem = 12px` (consumer app sets `html { font-size: 12px }`).
71
+
72
+ | Token | Value | Use when |
73
+ |---|---|---|
74
+ | `--font-size-hero` | `48px` | Hero banners, display text |
75
+ | `--font-size-title` | `38px` | Major page titles |
76
+ | `--font-size-section` | `32px` | Section headings |
77
+ | `--font-size-panel` | `20px` | Panel and card headings |
78
+ | `--font-size-subheading` | `18px` | Subheadings, intro paragraphs |
79
+ | `--font-size-label` | `14px` | Form labels, prominent body text |
80
+ | `--font-size-body` | `12px` | Standard body text (= 1rem) |
81
+ | `--font-size-caption` | `10px` | Captions, helper text, badge labels |
82
+ | `--font-weight-bold` | `700` | Headings, strong emphasis |
83
+ | `--font-weight-semibold` | `600` | Labels, medium emphasis |
84
+ | `--font-weight-regular` | `400` | Body copy, field values |
85
+ | `--line-height-heading` | `1.2` | Headings and titles — tight |
86
+ | `--line-height-body` | `1.4` | Body copy and labels — comfortable |
87
+
88
+ Typography utility classes (`.text-h1`–`.text-h6`, `.text-large-bold`, `.text-normal-regular`, etc.) also exist and apply a full set of `font-size`, `font-weight`, and `color` in one class. Use the CSS vars above when you need the value itself; use the classes when you want to apply the full style at once.
89
+
90
+ ### Semantic tokens — Surfaces
91
+
92
+ | Token | Value | Use when |
93
+ |---|---|---|
94
+ | `--color-surface-default` | `--surface-50` | White — base page/card/input background |
95
+ | `--color-surface-secondary` | `--surface-200` | Secondary panels, alternate rows |
96
+ | `--color-surface-elevated` | `--surface-100` | Popovers, dropdowns, cards that float |
97
+ | `--color-surface-hover` | `--surface-200` | Hover background on list/menu items |
98
+ | `--color-surface-disabled` | `--surface-300` | Disabled input/field background |
99
+ | `--color-surface-selected` | `--teal-50` | Selected row or active item background |
100
+ | `--color-surface-overlay` | `--scrim` | Modal and drawer backdrop |
101
+
102
+ ### Semantic tokens — Borders
103
+
104
+ | Token | Value | Use when |
105
+ |---|---|---|
106
+ | `--color-border-primary` | `--navy-100` | Default input, card, and container borders |
107
+ | `--color-border-secondary` | `--navy-200` | Stronger dividers, tab borders |
108
+ | `--color-border-tertiary` | `--surface-500` | Very light internal dividers |
109
+ | `--color-border-brand` | `--teal-100` | Soft brand-colored border |
110
+ | `--color-border-brand-strong` | `--teal-200` | Stronger brand border — hover of soft elements |
111
+ | `--color-border-focus` | `--teal-500` | Focused input border ring |
112
+ | `--color-border-error` | `--red-600` | Error/invalid state border |
113
+
114
+ ### Semantic tokens — Brand
115
+
116
+ Use `--color-brand-*` for any element whose color comes from the CMG brand (teal). This replaces raw `--teal-*` usage in component code.
117
+
118
+ | Token | Value | Use when |
119
+ |---|---|---|
120
+ | `--color-brand-primary` | `--teal-700` | Primary brand color — buttons, links, toggles |
121
+ | `--color-brand-primary-hover` | `--teal-800` | Hover over primary brand elements |
122
+ | `--color-brand-primary-active` | `--teal-900` | Pressed/active primary brand elements |
123
+ | `--color-brand-primary-bg` | `--teal-50` | Tinted background for brand-highlighted areas |
124
+ | `--color-brand-secondary` | `--teal-500` | Secondary brand — focus indicators, data markers |
125
+ | `--color-brand-secondary-hover` | `--teal-600` | Hover over secondary brand elements |
126
+
127
+ ### Semantic tokens — Status
128
+
129
+ Grouped under `--color-status-*` so all status colors are discoverable together.
130
+
131
+ | Token | Value | Use when |
132
+ |---|---|---|
133
+ | `--color-status-error` | `--red-600` | Error text, icons |
134
+ | `--color-status-error-hover` | `--red-700` | Hovered error elements |
135
+ | `--color-status-error-bg` | `--red-50` | Error background tint |
136
+ | `--color-status-error-border` | `--red-600` | Error borders |
137
+ | `--color-status-warning` | `--yellow-600` | Warning text, icons |
138
+ | `--color-status-warning-bg` | `--yellow-50` | Warning background tint |
139
+ | `--color-status-warning-border` | `--yellow-200` | Warning borders |
140
+ | `--color-status-success` | `--green-500` | Success text, icons |
141
+ | `--color-status-success-bg` | `--green-50` | Success background tint |
142
+ | `--color-status-success-border` | `--green-200` | Success borders |
143
+ | `--color-status-info` | `--blue-500` | Info text, icons |
144
+ | `--color-status-info-bg` | `--blue-50` | Info background tint |
145
+ | `--color-status-info-border` | `--blue-200` | Info borders |
146
+
147
+ ### Semantic tokens — Focus, Radius, Motion, Z-Index
148
+
149
+ ```css
150
+ /* Focus rings — use on :focus-visible */
151
+ --focus-ring: 0 0 0 2px var(--surface-50), 0 0 0 6px var(--teal-700)
152
+ --focus-ring-inset: inset 0 0 0 2px var(--teal-700)
153
+
154
+ /* Border radius */
155
+ --radius-sm: 4px /* chips, tags, badges */
156
+ --radius-md: 6px /* inputs, cards, buttons */
157
+ --radius-lg: 8px /* modals, drawers */
158
+ --radius-full: 9999px /* pill buttons, circular avatars */
159
+
160
+ /* Transitions */
161
+ --transition-fast: 80ms ease /* hover color changes */
162
+ --transition-normal: 120ms ease /* standard state changes */
163
+ --transition-slow: 200ms ease /* expand/collapse, panel slides */
164
+
165
+ /* Z-index scale */
166
+ --z-sticky: 100 /* sticky headers */
167
+ --z-dropdown: 200 /* dropdowns, menus */
168
+ --z-overlay: 300 /* modals, drawers */
169
+ --z-notification: 400 /* toasts */
170
+ --z-tooltip: 500 /* tooltips */
171
+ ```
172
+
173
+ ### Primitive tokens (reference)
174
+
175
+ Raw color primitives remain available but should only be used when no semantic alias fits. Color families: `--teal-50`–`--teal-900`, `--navy-50`–`--navy-900`, `--surface-50`–`--surface-900`, `--red-50`–`--red-900`, `--yellow-50`–`--yellow-900`, `--green-50`–`--green-900`, `--blue-50`–`--blue-900`. Spacing: `--spacing-02` through `--spacing-40` (numeric) and `--spacing-xxxs` through `--spacing-xxl` (named). Elevation: `--shadow-depth-1` through `--shadow-depth-4`.
176
+
177
+ ---
178
+
179
+ ## Components
180
+
181
+ ### Button
182
+
183
+ ```tsx
184
+ import { Button } from '@cmgfi/clear-ds';
185
+ import type { ButtonProps, ButtonVariant, ButtonSize } from '@cmgfi/clear-ds';
186
+ ```
187
+
188
+ Props:
189
+ - `variant`: `'primary' | 'secondary' | 'ghost' | 'danger' | 'link'` — default `'primary'`
190
+ - `size`: `'md' | 'sm'` — `md` = 40px height (default), `sm` = 24px height
191
+ - `leadingIcon`: ReactNode — rendered before the label
192
+ - `trailingIcon`: ReactNode — rendered after the label
193
+ - `badge`: `string | number` — red notification badge rendered after the label
194
+ - `disabled`: boolean
195
+ - Extends `React.ButtonHTMLAttributes<HTMLButtonElement>`
196
+
197
+ ```tsx
198
+ <Button variant="primary" size="md">Save application</Button>
199
+ <Button variant="secondary" leadingIcon={<i className="pi pi-plus" />}>Add co-borrower</Button>
200
+ <Button variant="danger">Delete record</Button>
201
+ <Button variant="link">View details</Button>
202
+ <Button variant="primary" disabled leadingIcon={<i className="pi pi-spin pi-spinner" />}>Saving…</Button>
203
+ <Button badge={3} trailingIcon={<i className="pi pi-chevron-down" />}>Actions</Button>
204
+ ```
205
+
206
+ ---
207
+
208
+ ### IconButton
209
+
210
+ ```tsx
211
+ import { IconButton } from '@cmgfi/clear-ds';
212
+ import type { IconButtonProps } from '@cmgfi/clear-ds';
213
+ ```
214
+
215
+ Props:
216
+ - `icon`: ReactNode — the icon to display (e.g. `<i className="pi pi-search" />`)
217
+ - `variant`: `ButtonVariant` — same variants as Button, default `'primary'`
218
+ - `size`: `'md' | 'sm'` — `md` = 40×40px (default), `sm` = 24×24px
219
+ - `aria-label`: string — **required** (no visible text)
220
+ - `disabled`: boolean
221
+ - Extends `React.ButtonHTMLAttributes<HTMLButtonElement>`
222
+
223
+ ```tsx
224
+ <IconButton icon={<i className="pi pi-search" />} aria-label="Search" />
225
+ <IconButton icon={<i className="pi pi-trash" />} aria-label="Delete" variant="danger" />
226
+ <IconButton icon={<i className="pi pi-plus" />} aria-label="Add" size="sm" />
227
+ ```
228
+
229
+ ---
230
+
231
+ ### DropdownButton
232
+
233
+ ```tsx
234
+ import { DropdownButton } from '@cmgfi/clear-ds';
235
+ import type { DropdownButtonProps, DropdownButtonItem } from '@cmgfi/clear-ds';
236
+ ```
237
+
238
+ Props:
239
+ - `label`: string — button label
240
+ - `items`: `DropdownButtonItem[]` — `{ label: string; value: string; icon?: ReactNode; disabled?: boolean; divider?: boolean }`
241
+ - `onSelect`: `(value: string) => void`
242
+ - `variant`: `ButtonVariant` — default `'primary'`
243
+ - `size`: `'md' | 'sm'` — default `'md'`
244
+ - `leadingIcon`: ReactNode — optional icon before the label
245
+ - `disabled`: boolean
246
+
247
+ ```tsx
248
+ const items: DropdownButtonItem[] = [
249
+ { label: 'Edit', value: 'edit', icon: <i className="pi pi-pencil" /> },
250
+ { label: 'Delete', value: 'delete', icon: <i className="pi pi-trash" /> },
251
+ ];
252
+ <DropdownButton label="Actions" items={items} variant="secondary" onSelect={handleSelect} />
253
+ ```
254
+
255
+ ---
256
+
257
+ ### SplitButton
258
+
259
+ ```tsx
260
+ import { SplitButton } from '@cmgfi/clear-ds';
261
+ import type { SplitButtonProps, SplitButtonItem } from '@cmgfi/clear-ds';
262
+ ```
263
+
264
+ Props:
265
+ - `label`: string — main action label
266
+ - `onAction`: `() => void` — main button (left side) click handler
267
+ - `items`: `SplitButtonItem[]` — `{ label: string; value: string; icon?: ReactNode; disabled?: boolean }`
268
+ - `onSelect`: `(value: string) => void` — dropdown item selection handler
269
+ - `variant`: `'primary' | 'secondary'` — default `'primary'`
270
+ - `size`: `'md' | 'sm'` — `md` = 36px height (default), `sm` = 24px
271
+ - `leadingIcon`: ReactNode — optional icon in the main action area
272
+ - `disabled`: boolean
273
+ - `triggerAriaLabel`: string — accessible label for the chevron trigger, default `"More options"`
274
+
275
+ ---
276
+
277
+ ### CloseButton
278
+
279
+ ```tsx
280
+ import { CloseButton } from '@cmgfi/clear-ds';
281
+ import type { CloseButtonProps } from '@cmgfi/clear-ds';
282
+ ```
283
+
284
+ Props:
285
+ - `size`: `'sm' | 'md'` — `sm` = 24×24px (default), `md` = 40×40px
286
+ - `aria-label`: string — defaults to `"Close"`
287
+ - Extends `React.ButtonHTMLAttributes<HTMLButtonElement>`
288
+
289
+ ```tsx
290
+ <CloseButton onClick={onDismiss} />
291
+ <CloseButton aria-label="Close dialog" size="md" onClick={onClose} />
292
+ ```
293
+
294
+ ---
295
+
296
+ ### LightningButton
297
+
298
+ ```tsx
299
+ import { LightningButton } from '@cmgfi/clear-ds';
300
+ import type { LightningButtonProps, LightningButtonItem } from '@cmgfi/clear-ds';
301
+ ```
302
+
303
+ Pill-shaped bolt icon + chevron dropdown. Used as a quick-actions trigger in nav bars and toolbars.
304
+
305
+ Props:
306
+ - `variant`: `'basic' | 'filled'` — `basic` = no container, teal icon (default); `filled` = teal-50 bg / teal border
307
+ - `size`: `'md' | 'sm'` — `md` = 36px tall (default), `sm` = 24px (only meaningful for `filled`)
308
+ - `items`: `LightningButtonItem[]` — `{ label: string; value: string; icon?: ReactNode; disabled?: boolean; divider?: boolean }`
309
+ - `onSelect`: `(value: string) => void` — **required**
310
+ - `disabled`: boolean
311
+ - `aria-label`: string — accessible label (no visible text)
312
+
313
+ ```tsx
314
+ <LightningButton
315
+ variant="filled"
316
+ size="sm"
317
+ items={[{ label: 'Copy loan', value: 'copy' }, { label: 'Export PDF', value: 'export' }]}
318
+ onSelect={handleSelect}
319
+ aria-label="Quick actions"
320
+ />
321
+ ```
322
+
323
+ ---
324
+
325
+ ### InputText
326
+
327
+ ```tsx
328
+ import { InputText } from '@cmgfi/clear-ds';
329
+ import type { InputTextProps } from '@cmgfi/clear-ds';
330
+ ```
331
+
332
+ Props:
333
+ - `label`: string — visible label above the input
334
+ - `required`: boolean — appends a red asterisk to the label
335
+ - `helperText`: string — helper or error text below the input
336
+ - `invalid`: boolean — triggers error/red styling
337
+ - `size`: `'sm' | 'md' | 'lg'` — default `'md'`
338
+ - `prefixIcon`: ReactNode — rendered to the left of the input text
339
+ - `suffixIcon`: ReactNode — rendered to the right of the input text
340
+ - `disabled`: boolean
341
+ - Extends `React.InputHTMLAttributes<HTMLInputElement>` (minus native `size`)
342
+
343
+ ```tsx
344
+ <InputText label="Social Security Number" placeholder="000-00-0000" style={{ width: 240 }} />
345
+ <InputText label="Annual income" required invalid helperText="Must be greater than zero." />
346
+ <InputText label="First name" disabled value="Maria Johnson" />
347
+ <InputText label="Search" prefixIcon={<i className="pi pi-search" />} />
348
+ ```
349
+
350
+ ---
351
+
352
+ ### TextArea
353
+
354
+ ```tsx
355
+ import { TextArea } from '@cmgfi/clear-ds';
356
+ import type { TextAreaProps } from '@cmgfi/clear-ds';
357
+ ```
358
+
359
+ Props:
360
+ - `label`: string
361
+ - `required`: boolean — appends a red asterisk
362
+ - `helperText`: string — turns red when `invalid` is true
363
+ - `invalid`: boolean
364
+ - `rows`: number — default `3`
365
+ - `autoGrow`: boolean — height grows with content instead of scrolling
366
+ - `maxLength`: number (via `TextareaHTMLAttributes`) — shows a live `X/N` character counter
367
+ - `disabled`: boolean
368
+ - Extends `React.TextareaHTMLAttributes<HTMLTextAreaElement>`
369
+
370
+ ```tsx
371
+ <TextArea label="Loan notes" placeholder="Enter notes…" rows={4} />
372
+ <TextArea label="Comments" maxLength={500} />
373
+ <TextArea label="Description" autoGrow />
374
+ <TextArea label="Notes" invalid helperText="This field is required." />
375
+ ```
376
+
377
+ ---
378
+
379
+ ### Checkbox
380
+
381
+ ```tsx
382
+ import { Checkbox } from '@cmgfi/clear-ds';
383
+ import type { CheckboxProps } from '@cmgfi/clear-ds';
384
+ ```
385
+
386
+ Props:
387
+ - `label`: ReactNode — visible label
388
+ - `indeterminate`: boolean — dash state for "select all" patterns
389
+ - `invalid`: boolean — red border and label
390
+ - `helperText`: string — turns red when `invalid`
391
+ - `size`: `'sm' | 'md' | 'lg'` — `sm`=14px, `md`=18px (default), `lg`=22px
392
+ - `checked`, `onChange`, `disabled` — standard input props
393
+ - Extends `React.InputHTMLAttributes<HTMLInputElement>`
394
+
395
+ ```tsx
396
+ <Checkbox label="I agree to the terms" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
397
+ <Checkbox label="Select all" indeterminate />
398
+ <Checkbox label="Accept terms" invalid helperText="You must accept the terms to continue." />
399
+ <Checkbox label="Dense option" size="sm" />
400
+ ```
401
+
402
+ ---
403
+
404
+ ### RadioButton
405
+
406
+ ```tsx
407
+ import { RadioButton } from '@cmgfi/clear-ds';
408
+ import type { RadioButtonProps } from '@cmgfi/clear-ds';
409
+ ```
410
+
411
+ Props:
412
+ - `label`: ReactNode — visible label
413
+ - `invalid`: boolean — red border and label
414
+ - `helperText`: string — turns red when `invalid`
415
+ - `size`: `'sm' | 'md' | 'lg'` — `sm`=14px, `md`=18px (default), `lg`=22px
416
+ - `name`, `value`, `checked`, `onChange`, `disabled` — standard input props
417
+ - Extends `React.InputHTMLAttributes<HTMLInputElement>`
418
+
419
+ ```tsx
420
+ {['Conventional', 'FHA', 'VA'].map(type => (
421
+ <RadioButton key={type} name="loanType" label={type} value={type} checked={loanType === type} onChange={e => setLoanType(e.target.value)} />
422
+ ))}
423
+ ```
424
+
425
+ ---
426
+
427
+ ### Select
428
+
429
+ ```tsx
430
+ import { Select } from '@cmgfi/clear-ds';
431
+ import type { SelectProps, SelectOption } from '@cmgfi/clear-ds';
432
+ ```
433
+
434
+ Props:
435
+ - `options`: `SelectOption[]` — `{ label: string; value: string; disabled?: boolean }`
436
+ - `value`: `string | null` — controlled selected value
437
+ - `onChange`: `(value: string | null) => void`
438
+ - `placeholder`: string
439
+ - `label`: string
440
+ - `required`: boolean
441
+ - `helperText`: string
442
+ - `invalid`: boolean
443
+ - `disabled`: boolean
444
+ - `filter`: boolean — shows a live-search input in the dropdown
445
+ - `filterPlaceholder`: string — default `"Search…"`
446
+ - `size`: `'sm' | 'md' | 'lg'` — default `'md'`
447
+
448
+ ```tsx
449
+ const options: SelectOption[] = [
450
+ { label: 'Conventional', value: 'conventional' },
451
+ { label: 'FHA', value: 'fha' },
452
+ { label: 'VA', value: 'va' },
453
+ ];
454
+ <Select label="Loan type" options={options} value={loanType} onChange={setLoanType} />
455
+ <Select label="Product" options={options} value={val} onChange={setVal} filter required invalid helperText="Required." />
456
+ ```
457
+
458
+ ---
459
+
460
+ ### MultiSelect
461
+
462
+ ```tsx
463
+ import { MultiSelect } from '@cmgfi/clear-ds';
464
+ import type { MultiSelectProps, MultiSelectOption } from '@cmgfi/clear-ds';
465
+ ```
466
+
467
+ Props:
468
+ - `options`: `MultiSelectOption[]` — `{ label: string; value: string; disabled?: boolean }`
469
+ - `value`: `string[]` — controlled selected values
470
+ - `onChange`: `(value: string[]) => void`
471
+ - `placeholder`: string
472
+ - `label`: string
473
+ - `required`: boolean
474
+ - `helperText`: string
475
+ - `invalid`: boolean
476
+ - `disabled`: boolean
477
+ - `filter`: boolean — shows a live-search input (default `true`)
478
+ - `filterPlaceholder`: string — default `"Search…"`
479
+ - `showSelectAll`: boolean — "Select all" checkbox in dropdown header (default `true`)
480
+ - `size`: `'sm' | 'md' | 'lg'` — default `'md'`
481
+
482
+ ```tsx
483
+ <MultiSelect label="Loan types" options={options} value={selected} onChange={setSelected} />
484
+ ```
485
+
486
+ ---
487
+
488
+ ### ListBox
489
+
490
+ ```tsx
491
+ import { ListBox } from '@cmgfi/clear-ds';
492
+ import type { ListBoxProps, ListBoxOption, ListBoxOptionGroup } from '@cmgfi/clear-ds';
493
+ ```
494
+
495
+ Props:
496
+ - `options`: `ListBoxOption[] | ListBoxOptionGroup[]` — flat list or grouped; groups auto-detected when entries contain an `items` array
497
+ - `ListBoxOption`: `{ label: string; value: string; disabled?: boolean }`
498
+ - `ListBoxOptionGroup`: `{ label: string; items: ListBoxOption[] }`
499
+ - `value`: `string | string[] | null` — single or multi-select
500
+ - `onChange`: `(value: string | string[] | null) => void`
501
+ - `multiple`: boolean — enables checkbox multi-select
502
+ - `filter`: boolean — shows a search input above the list
503
+ - `filterPlaceholder`: string — default `"Search…"`
504
+ - `label`: string
505
+ - `required`: boolean
506
+ - `helperText`: string
507
+ - `invalid`: boolean
508
+ - `disabled`: boolean
509
+
510
+ ---
511
+
512
+ ### SelectButton
513
+
514
+ ```tsx
515
+ import { SelectButton } from '@cmgfi/clear-ds';
516
+ import type { SelectButtonProps, SelectButtonOption, SelectButtonBadge } from '@cmgfi/clear-ds';
517
+ ```
518
+
519
+ Props:
520
+ - `options`: `SelectButtonOption[]` — `{ label: string; value: string; count?: number; badge?: SelectButtonBadge; disabled?: boolean }`
521
+ - `SelectButtonBadge`: `{ value: string | number; color: string }`
522
+ - `value`: `string | null` — controlled selected value
523
+ - `onChange`: `(value: string) => void`
524
+ - `variant`: `'group' | 'options'` — `group` = joined toggle row (default); `options` = split button + dropdown
525
+ - `label`: string — optional field label above the button
526
+ - `required`: boolean
527
+ - `disabled`: boolean
528
+ - `placeholder`: string — for the `options` variant when nothing is selected
529
+
530
+ ```tsx
531
+ <SelectButton
532
+ options={[{ label: 'Purchase', value: 'purchase' }, { label: 'Refinance', value: 'refi' }]}
533
+ value={loanPurpose}
534
+ onChange={setLoanPurpose}
535
+ />
536
+ ```
537
+
538
+ ---
539
+
540
+ ### DatePicker
541
+
542
+ ```tsx
543
+ import { DatePicker } from '@cmgfi/clear-ds';
544
+ import type { DatePickerProps } from '@cmgfi/clear-ds';
545
+ ```
546
+
547
+ Props:
548
+ - `value`: `Date | null` — controlled selected date
549
+ - `onChange`: `(date: Date | null) => void`
550
+ - `label`: string
551
+ - `required`: boolean
552
+ - `helperText`: string
553
+ - `invalid`: boolean
554
+ - `disabled`: boolean
555
+ - `placeholder`: string — default `"MM/DD/YYYY"`
556
+ - `minDate`: Date — dates before this are disabled
557
+ - `maxDate`: Date — dates after this are disabled
558
+ - `showWeekNumbers`: boolean — shows ISO week numbers in the calendar
559
+
560
+ ---
561
+
562
+ ### ToggleSwitch
563
+
564
+ ```tsx
565
+ import { ToggleSwitch } from '@cmgfi/clear-ds';
566
+ import type { ToggleSwitchProps } from '@cmgfi/clear-ds';
567
+ ```
568
+
569
+ Props:
570
+ - `label`: ReactNode — label to the right of the switch
571
+ - `helperText`: string — shown below the switch row
572
+ - `checked`, `onChange`, `disabled`, `defaultChecked` — standard input props
573
+ - Extends `React.InputHTMLAttributes<HTMLInputElement>`
574
+
575
+ ```tsx
576
+ <ToggleSwitch label="Enable notifications" checked={on} onChange={e => setOn(e.target.checked)} />
577
+ <ToggleSwitch label="Auto-lock" defaultChecked />
578
+ <ToggleSwitch label="Feature locked" disabled />
579
+ ```
580
+
581
+ ---
582
+
583
+ ### FileUpload
584
+
585
+ ```tsx
586
+ import { FileUpload, DEFAULT_DOC_TYPES } from '@cmgfi/clear-ds';
587
+ import type { FileUploadProps, FileUploadResult } from '@cmgfi/clear-ds';
588
+ ```
589
+
590
+ Props:
591
+ - `onUpload`: `(files: FileUploadResult[]) => void` — **required**; called when the Upload button is clicked with all classified files; `FileUploadResult`: `{ file: File; docType: string }`
592
+ - `onFilesAdded`: `(files: File[]) => void` — called immediately after files are dropped/selected (before classification)
593
+ - `docTypes`: `string[]` — document type options in the per-file dropdown; defaults to `DEFAULT_DOC_TYPES` (8 standard mortgage doc types)
594
+ - `accept`: string — accepted file extensions; defaults to common image, PDF, and Office formats
595
+ - `initialFiles`: `File[]` — pre-populate the staged file queue on mount (for testing/Storybook)
596
+
597
+ ```tsx
598
+ <FileUpload
599
+ accept=".pdf,image/*"
600
+ onFilesAdded={files => console.log('Added:', files)}
601
+ onUpload={results => results.forEach(r => upload(r.file, r.docType))}
602
+ />
603
+ ```
604
+
605
+ ---
606
+
607
+ ### DataTable
608
+
609
+ ```tsx
610
+ import { DataTable } from '@cmgfi/clear-ds';
611
+ import type {
612
+ DataTableProps, DataTableColumn, DataTableGroup, DataTableExpansion,
613
+ SortOrder, SortMeta, SelectionMode, EditMode,
614
+ ColumnFilter, FilterOption, FilterGroup, FilterType, RuleFilterValue
615
+ } from '@cmgfi/clear-ds';
616
+ ```
617
+
618
+ Feature-rich data grid. Props:
619
+ - `data`: `T[] | DataTableGroup<T>[]` — flat rows or pre-grouped data
620
+ - `DataTableGroup<T>`: `{ label: string; data: T[]; defaultExpanded?: boolean }`
621
+ - `columns`: `DataTableColumn<T>[]`
622
+ - `field`: string
623
+ - `header`: ReactNode
624
+ - `sortable`: boolean
625
+ - `filter`: `ColumnFilter` — attach a filter popover
626
+ - `body`: `(row: T, index: number) => ReactNode` — custom cell renderer
627
+ - `editor`: `(row: T, field: string, onChange: (val: unknown) => void) => ReactNode`
628
+ - `footer`: ReactNode or `(data: T[]) => ReactNode`
629
+ - `width`: number | string
630
+ - `align`: `'left' | 'center' | 'right'`
631
+ - `dataKey`: string — unique row identifier field, default `'id'`
632
+ - `selectionMode`: `'none' | 'single' | 'multiple'`
633
+ - `selection`: `T | T[] | null` — controlled selection
634
+ - `onSelectionChange`: `(sel: T | T[] | null) => void`
635
+ - `sortMode`: `'single' | 'multiple'`
636
+ - `sortField`: string
637
+ - `sortOrder`: `SortOrder` (`1 | -1`)
638
+ - `multiSortMeta`: `SortMeta[]`
639
+ - `onSort`: `(e: { field: string; order: SortOrder; multiSortMeta?: SortMeta[] }) => void`
640
+ - `expansion`: `DataTableExpansion<T>` — `{ dataKey: string; childKey?: string; template?: (row: T) => ReactNode; columns?: DataTableColumn<T>[] }`
641
+ - `editMode`: `'row' | 'cell'`
642
+ - `onRowEditSave`: `(updated: T, original: T) => void`
643
+ - `onCellEdit`: `(row: T, field: string, value: unknown) => void`
644
+ - `stripedRows`: boolean
645
+ - `size`: `'sm' | 'md'` — `sm` = compact (6px padding)
646
+ - `showFooter`: boolean
647
+ - `loading`: boolean — shows a skeleton placeholder
648
+ - `emptyMessage`: ReactNode
649
+ - `scrollable`: boolean
650
+ - `scrollHeight`: string
651
+
652
+ ```tsx
653
+ const columns: DataTableColumn<Loan>[] = [
654
+ { field: 'borrowerName', header: 'Borrower', sortable: true },
655
+ { field: 'loanAmount', header: 'Amount', align: 'right', body: row => `$${row.loanAmount.toLocaleString()}` },
656
+ { field: 'status', header: 'Status', body: row => <SeverityChip severity={row.statusSeverity} label={row.status} /> },
657
+ ];
658
+ <DataTable data={loans} columns={columns} selectionMode="multiple" selection={selected} onSelectionChange={setSelected} />
659
+ ```
660
+
661
+ ---
662
+
663
+ ### Paginator
664
+
665
+ ```tsx
666
+ import { Paginator } from '@cmgfi/clear-ds';
667
+ import type { PaginatorProps, PageChangeEvent } from '@cmgfi/clear-ds';
668
+ ```
669
+
670
+ Props:
671
+ - `totalRecords`: number
672
+ - `first`: number — 0-based index of the first record on the current page
673
+ - `rows`: number — records per page
674
+ - `onPageChange`: `(e: PageChangeEvent) => void` — `PageChangeEvent`: `{ first: number; rows: number; page: number }`
675
+ - `template`: `'pages' | 'report' | 'full'` — layout variant (default `'pages'`)
676
+ - `rowsPerPageOptions`: `number[]` — default `[10, 25, 50, 100]`
677
+ - `pageLinkSize`: number — max visible page buttons (default `5`)
678
+
679
+ ```tsx
680
+ <Paginator totalRecords={1240} first={first} rows={25} onPageChange={e => setFirst(e.first)} />
681
+ ```
682
+
683
+ ---
684
+
685
+ ### Picklist
686
+
687
+ ```tsx
688
+ import { Picklist } from '@cmgfi/clear-ds';
689
+ import type { PicklistProps, PicklistItem, PicklistChangeEvent } from '@cmgfi/clear-ds';
690
+ ```
691
+
692
+ Dual-panel transfer control. Props:
693
+ - `sourceItems`: `PicklistItem[]` — `{ id: string | number; label?: string; icon?: string; disabled?: boolean; [key: string]: unknown }`
694
+ - `targetItems`: `PicklistItem[]`
695
+ - `onChange`: `(e: PicklistChangeEvent) => void` — `PicklistChangeEvent`: `{ source: PicklistItem[]; target: PicklistItem[] }`
696
+ - `sourceHeader`: ReactNode
697
+ - `targetHeader`: ReactNode
698
+ - `itemTemplate`: `(item: PicklistItem) => ReactNode` — custom row renderer (after the checkbox)
699
+ - `filter`: boolean — shows a search input in each panel
700
+ - `sourceFilterPlaceholder`: string
701
+ - `targetFilterPlaceholder`: string
702
+ - `showMoveAll`: boolean — show `>>` / `<<` buttons (default `true`)
703
+ - `showCheckbox`: boolean — show per-row checkboxes (default `true`)
704
+
705
+ ---
706
+
707
+ ### Card
708
+
709
+ ```tsx
710
+ import { Card } from '@cmgfi/clear-ds';
711
+ import type { CardProps } from '@cmgfi/clear-ds';
712
+ ```
713
+
714
+ Props:
715
+ - `title`: ReactNode — bold primary title
716
+ - `subTitle`: ReactNode — muted subtitle below the title
717
+ - `footer`: ReactNode — action area at the bottom (typically buttons)
718
+ - `children`: ReactNode — card body
719
+ - `onClick`: `React.MouseEventHandler<HTMLDivElement>` — makes the card interactive with hover shadow lift
720
+
721
+ ```tsx
722
+ <Card title="Loan Summary" subTitle="Application #12345" footer={<Button>View</Button>}>
723
+ <p>Body content goes here.</p>
724
+ </Card>
725
+
726
+ <Card title="Open Application" onClick={() => navigate('/app/123')}>
727
+ <p>Click anywhere to open.</p>
728
+ </Card>
729
+ ```
730
+
731
+ ---
732
+
733
+ ### Accordion
734
+
735
+ ```tsx
736
+ import { Accordion } from '@cmgfi/clear-ds';
737
+ import type { AccordionProps, AccordionItem, AccordionVariant } from '@cmgfi/clear-ds';
738
+ ```
739
+
740
+ Props:
741
+ - `items`: `AccordionItem[]` — `{ id: string; title: string; content: ReactNode; badge?: number; headerAction?: ReactNode; disabled?: boolean }`
742
+ - `openIds`: `string[]` — controlled open panel IDs
743
+ - `onChange`: `(ids: string[]) => void`
744
+ - `variant`: `'simple' | 'page' | 'panel' | 'flyout'` — default `'page'`
745
+ - `simple` — teal-700 bottom border on title
746
+ - `page` — full-width gray `#e4e4e4` header
747
+ - `panel` — compact gray `#f4f4f5` header, 32px, optional badge
748
+ - `flyout` — gray `#f4f4f5` header, 44px, optional badge
749
+ - `multiple`: boolean — allow multiple panels open simultaneously (default `false`)
750
+
751
+ ```tsx
752
+ const [open, setOpen] = useState(['item1']);
753
+ <Accordion
754
+ variant="page"
755
+ items={[
756
+ { id: 'item1', title: 'Borrower Information', content: <BorrowerForm /> },
757
+ { id: 'item2', title: 'Property Details', content: <PropertyForm />, badge: 3 },
758
+ ]}
759
+ openIds={open}
760
+ onChange={setOpen}
761
+ />
762
+ ```
763
+
764
+ ---
765
+
766
+ ### Tabs
767
+
768
+ ```tsx
769
+ import { Tabs } from '@cmgfi/clear-ds';
770
+ import type { TabsProps, TabItem } from '@cmgfi/clear-ds';
771
+ ```
772
+
773
+ Props:
774
+ - `tabs`: `TabItem[]` — `{ id: string; label: string; icon?: ReactNode; count?: number; icon2?: ReactNode; content?: ReactNode; disabled?: boolean }`
775
+ - `activeTab`: string — controlled active tab ID
776
+ - `onChange`: `(id: string) => void`
777
+
778
+ ```tsx
779
+ const [active, setActive] = useState('tab1');
780
+ <Tabs
781
+ tabs={[
782
+ { id: 'tab1', label: 'Overview', content: <p>Overview content</p> },
783
+ { id: 'tab2', label: 'Details', content: <p>Details content</p>, count: 5 },
784
+ { id: 'tab3', label: 'History', content: <p>History</p>, disabled: true },
785
+ ]}
786
+ activeTab={active}
787
+ onChange={setActive}
788
+ />
789
+ ```
790
+
791
+ ---
792
+
793
+ ### BannerTabs
794
+
795
+ ```tsx
796
+ import { BannerTabs } from '@cmgfi/clear-ds';
797
+ import type {
798
+ BannerTabsProps, BannerTabItem, BannerTabStatus,
799
+ BannerTabBadge, BannerTabBadgeGroup, StatusShape, StatusColor, BadgeShape
800
+ } from '@cmgfi/clear-ds';
801
+ ```
802
+
803
+ Scrollable card-shaped loan-workflow tab bar with status indicators. Handles overflow with arrow buttons automatically.
804
+
805
+ Props:
806
+ - `items`: `BannerTabItem[]`
807
+ - `id`: string
808
+ - `label`: string — primary label ("DU", "Loan Summary")
809
+ - `subLabel`: string — secondary label after "/" ("LPA" in "DU / LPA")
810
+ - `subtitle`: string — muted status line ("Not Complete", "Rep. Score 732")
811
+ - `status`: `BannerTabStatus` — `{ shape: 'dot' | 'diamond' | 'square' | 'none'; color: 'green' | 'yellow' | 'red' | 'gray' }`
812
+ - `subStatus`: `BannerTabStatus` — used alongside `subLabel`
813
+ - `locked`: boolean — shows a lock icon, non-interactive
814
+ - `badgeGroups`: `BannerTabBadgeGroup[]` — count badges with `{ badges: BannerTabBadge[] }`; each badge: `{ count: number; tooltip: string; shape?: BadgeShape; color?: string }`
815
+ - `docs`: ReactNode — custom content at the bottom of the card
816
+ - `disabled`: boolean
817
+ - `activeId`: `string | null`
818
+ - `onChange`: `(id: string) => void`
819
+
820
+ ---
821
+
822
+ ### Modal
823
+
824
+ ```tsx
825
+ import { Modal } from '@cmgfi/clear-ds';
826
+ import type { ModalProps } from '@cmgfi/clear-ds';
827
+ ```
828
+
829
+ Props:
830
+ - `isOpen`: boolean
831
+ - `onClose`: `() => void` — called on ✕ click, scrim click, or Escape
832
+ - `title`: string — modal header text
833
+ - `children`: ReactNode — modal body
834
+ - `footer`: ReactNode — typically right-aligned action buttons
835
+ - `scrim`: boolean — dark overlay behind the modal (default `true`)
836
+ - `width`: number — modal width in px (default `510`)
837
+
838
+ ```tsx
839
+ <Modal
840
+ isOpen={isOpen}
841
+ onClose={() => setIsOpen(false)}
842
+ title='Delete "July 2024 Purchase Loan"?'
843
+ footer={
844
+ <>
845
+ <Button variant="secondary" onClick={() => setIsOpen(false)} autoFocus>Cancel</Button>
846
+ <Button variant="danger" onClick={handleDelete}>Delete permanently</Button>
847
+ </>
848
+ }
849
+ >
850
+ <p>This action cannot be undone.</p>
851
+ </Modal>
852
+ ```
853
+
854
+ ---
855
+
856
+ ### Drawer
857
+
858
+ ```tsx
859
+ import { Drawer } from '@cmgfi/clear-ds';
860
+ import type { DrawerProps, DrawerAction } from '@cmgfi/clear-ds';
861
+ ```
862
+
863
+ Full-height sliding panel. Props:
864
+ - `isOpen`: boolean
865
+ - `onClose`: `() => void` — called on ✕ click, scrim click, or Escape
866
+ - `title`: string
867
+ - `subtitle`: ReactNode
868
+ - `children`: ReactNode — scrollable body
869
+ - `primaryAction`: `DrawerAction` — **required**; `{ label: string; onClick: () => void; disabled?: boolean }`
870
+ - `secondaryAction`: `DrawerAction` — optional; rendered left of primary
871
+ - `tertiaryActions`: `DrawerAction[]` — optional; left-aligned ghost buttons
872
+ - `side`: `'left' | 'right'` — default `'right'`
873
+ - `width`: number — default `800`
874
+
875
+ ---
876
+
877
+ ### SidePanel / SidePanelLayout
878
+
879
+ ```tsx
880
+ import { SidePanel, SidePanelLayout } from '@cmgfi/clear-ds';
881
+ import type { SidePanelProps, SidePanelLayoutProps } from '@cmgfi/clear-ds';
882
+ ```
883
+
884
+ `SidePanel` props:
885
+ - `isOpen`: boolean
886
+ - `onClose`: `() => void`
887
+ - `title`: string
888
+ - `subtitle`: ReactNode — below the title (e.g. phone + loan number link)
889
+ - `onSave`: `() => void` — renders a Save button in the header when provided
890
+ - `headerContent`: ReactNode — slot below the title row
891
+ - `width`: number — default `470`
892
+ - `children`: ReactNode — scrollable body
893
+
894
+ `SidePanelLayout` props (layout wrapper that pushes main content left):
895
+ - `open`: boolean — should match `SidePanel`'s `isOpen`
896
+ - `panelWidth`: number — should match `SidePanel`'s `width`, default `470`
897
+ - `children`: ReactNode — the main page content to be shifted
898
+
899
+ ```tsx
900
+ <SidePanelLayout open={panelOpen} panelWidth={470}>
901
+ <main>…page content…</main>
902
+ </SidePanelLayout>
903
+ <SidePanel isOpen={panelOpen} onClose={() => setPanelOpen(false)} title="Ryan Smith">
904
+ …panel body…
905
+ </SidePanel>
906
+ ```
907
+
908
+ ---
909
+
910
+ ### Popup
911
+
912
+ ```tsx
913
+ import { Popup } from '@cmgfi/clear-ds';
914
+ import type { PopupProps } from '@cmgfi/clear-ds';
915
+ ```
916
+
917
+ Positioned overlay anchored to a trigger element, with a CSS arrow pointing at the trigger. Props:
918
+ - `isOpen`: boolean
919
+ - `onClose`: `() => void` — called on ✕ click or scrim click
920
+ - `trigger`: ReactNode — the element the popup is anchored to
921
+ - `title`: string — optional header
922
+ - `children`: ReactNode — popup body
923
+ - `footer`: ReactNode — optional action area
924
+ - `placement`: `'top' | 'bottom'` — which side of the trigger (default `'bottom'`)
925
+ - `align`: `'left' | 'right'` — horizontal alignment relative to the trigger (default `'right'`)
926
+ - `width`: number — default `360`
927
+
928
+ ---
929
+
930
+ ### Tooltip
931
+
932
+ ```tsx
933
+ import { Tooltip } from '@cmgfi/clear-ds';
934
+ import type { TooltipProps, TooltipPlacement } from '@cmgfi/clear-ds';
935
+ ```
936
+
937
+ Props:
938
+ - `content`: ReactNode — tooltip bubble content
939
+ - `children`: ReactNode — the element the tooltip is anchored to
940
+ - `placement`: `'top' | 'bottom' | 'left' | 'right'` — default `'top'`
941
+ - `delay`: number — hover delay in ms (default `200`)
942
+ - `disabled`: boolean — suppresses the tooltip
943
+ - `maxWidth`: number — bubble max-width in px (default `140`)
944
+
945
+ ```tsx
946
+ <Tooltip content="This rate is locked until March 15." placement="top">
947
+ <IconButton icon={<i className="pi pi-info-circle" />} aria-label="Rate lock info" />
948
+ </Tooltip>
949
+ ```
950
+
951
+ ---
952
+
953
+ ### BannerAlert
954
+
955
+ ```tsx
956
+ import { BannerAlert } from '@cmgfi/clear-ds';
957
+ import type { BannerAlertProps, BannerAlertSeverity } from '@cmgfi/clear-ds';
958
+ ```
959
+
960
+ Props:
961
+ - `severity`: `'success' | 'info' | 'warn' | 'error' | 'neutral'`
962
+ - `title`: string — **required**
963
+ - `detail`: string — optional supporting text after the title
964
+ - `closable`: boolean — shows a dismiss ✕ button (default `true`)
965
+ - `onClose`: `() => void`
966
+
967
+ ```tsx
968
+ <BannerAlert severity="error" title="Missing required fields" detail="First name, last name, and SSN are required to continue." />
969
+ <BannerAlert severity="success" title="Application submitted" detail="Assigned to underwriter." closable onClose={handleDismiss} />
970
+ <BannerAlert severity="neutral" title="Rate lock expires in 3 days." closable={false} />
971
+ ```
972
+
973
+ ---
974
+
975
+ ### InlineContainedAlert
976
+
977
+ ```tsx
978
+ import { InlineContainedAlert } from '@cmgfi/clear-ds';
979
+ import type { InlineContainedAlertProps, AlertSeverity } from '@cmgfi/clear-ds';
980
+ ```
981
+
982
+ Bordered inline alert for use inside panels or cards.
983
+
984
+ Props:
985
+ - `severity`: `AlertSeverity` — `'success' | 'info' | 'warn' | 'error'`
986
+ - `message`: string — **required**
987
+ - `closable`: boolean — default `true`
988
+ - `onClose`: `() => void`
989
+
990
+ ---
991
+
992
+ ### InlineAlert
993
+
994
+ ```tsx
995
+ import { InlineAlert } from '@cmgfi/clear-ds';
996
+ import type { InlineAlertProps } from '@cmgfi/clear-ds';
997
+ ```
998
+
999
+ Minimal icon + colored text; no background or border.
1000
+
1001
+ Props:
1002
+ - `severity`: `AlertSeverity` — `'success' | 'info' | 'warn' | 'error'`
1003
+ - `message`: string — **required**
1004
+
1005
+ ---
1006
+
1007
+ ### Toast / Toaster / toast
1008
+
1009
+ ```tsx
1010
+ import { Toast, Toaster, toast } from '@cmgfi/clear-ds';
1011
+ import type { ToastSeverity, ToastLayout, ToastOptions, StaticToastProps } from '@cmgfi/clear-ds';
1012
+ ```
1013
+
1014
+ Mount `<Toaster />` once at the app root. Call `toast.*()` anywhere.
1015
+
1016
+ ```tsx
1017
+ // App root
1018
+ <Toaster />
1019
+
1020
+ // Anywhere in the app — imperative API
1021
+ toast.success('Application saved');
1022
+ toast.error('Upload failed', 'Check your connection.', { duration: 0 }); // 0 = no auto-dismiss
1023
+ toast.warn('Session expiring', 'You will be logged out in 5 minutes.');
1024
+ toast.info('3 items updated');
1025
+ ```
1026
+
1027
+ `toast.success / .info / .warn / .error` signature: `(title: string, detail?: string, opts?: ToastOptions) => void`
1028
+
1029
+ `ToastOptions`:
1030
+ - `duration`: number (ms) — default `5000`; `0` = no auto-dismiss (use for errors)
1031
+ - `layout`: `'horizontal' | 'vertical'` — `horizontal` = title and detail inline (default); `vertical` = stacked
1032
+
1033
+ ---
1034
+
1035
+ ### ProgressBar
1036
+
1037
+ ```tsx
1038
+ import { ProgressBar } from '@cmgfi/clear-ds';
1039
+ import type { ProgressBarProps } from '@cmgfi/clear-ds';
1040
+ ```
1041
+
1042
+ Props:
1043
+ - `value`: number — current progress, clamped to `[0, max]`
1044
+ - `max`: number — default `100`
1045
+ - `error`: boolean — hides the fill and shows `errorMessage` in red
1046
+ - `errorMessage`: string — shown below the track when `error` is true
1047
+ - `aria-label`: string — accessible label describing what is being measured
1048
+
1049
+ ```tsx
1050
+ <ProgressBar value={65} aria-label="Underwriting review" />
1051
+ <ProgressBar value={100} max={100} aria-label="Upload complete" />
1052
+ <ProgressBar value={0} error errorMessage="Upload failed. Please try again." />
1053
+ ```
1054
+
1055
+ ---
1056
+
1057
+ ### ProgressSpinner
1058
+
1059
+ ```tsx
1060
+ import { ProgressSpinner } from '@cmgfi/clear-ds';
1061
+ import type { ProgressSpinnerProps } from '@cmgfi/clear-ds';
1062
+ ```
1063
+
1064
+ SVG gradient loading spinner.
1065
+
1066
+ Props:
1067
+ - `size`: `'sm' | 'md' | 'lg'` — `sm`=20px, `md`=32px (default), `lg`=36px with Clear logo inside
1068
+ - `label`: string — text displayed below the spinner (designed for `lg`)
1069
+ - `theme`: `'light' | 'dark'` — adjusts label text color (default `'light'`)
1070
+ - `aria-label`: string — accessible label (default `"Loading"`)
1071
+
1072
+ ```tsx
1073
+ <ProgressSpinner size="md" aria-label="Loading loan pipeline…" />
1074
+ <ProgressSpinner size="lg" label="Loading…" theme="dark" />
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ### SeverityChip
1080
+
1081
+ ```tsx
1082
+ import { SeverityChip } from '@cmgfi/clear-ds';
1083
+ import type { SeverityChipProps, SeverityChipVariant } from '@cmgfi/clear-ds';
1084
+ ```
1085
+
1086
+ Props:
1087
+ - `label`: string — **required**
1088
+ - `severity`: `'plain' | 'contrast' | 'error' | 'warning' | 'informative' | 'success'`
1089
+ - `dismissible`: boolean — shows a ✕ button (default `true`)
1090
+ - `onDismiss`: `() => void`
1091
+
1092
+ ```tsx
1093
+ <SeverityChip severity="success" label="Approved" />
1094
+ <SeverityChip severity="warning" label="Conditions pending" />
1095
+ <SeverityChip severity="error" label="Denied" dismissible={false} />
1096
+ <SeverityChip severity="plain" label="Draft" onDismiss={handleDismiss} />
1097
+ ```
1098
+
1099
+ ---
1100
+
1101
+ ### MiscChip
1102
+
1103
+ ```tsx
1104
+ import { MiscChip } from '@cmgfi/clear-ds';
1105
+ import type { MiscChipProps, MiscChipColor } from '@cmgfi/clear-ds';
1106
+ ```
1107
+
1108
+ General-purpose color-coded label chip. 12 named colors, all mapping to existing token vars.
1109
+
1110
+ Props:
1111
+ - `label`: string — **required**
1112
+ - `color`: `'red-darkest' | 'red-light' | 'yellow-darkest' | 'yellow-dark' | 'yellow-light' | 'green-darkest' | 'green-dark' | 'green-light' | 'blue-darkest' | 'blue-dark' | 'blue-light' | 'navy'`
1113
+ - `dismissible`: boolean — default `true`
1114
+ - `onDismiss`: `() => void`
1115
+
1116
+ ---
1117
+
1118
+ ### ProfileChip
1119
+
1120
+ ```tsx
1121
+ import { ProfileChip } from '@cmgfi/clear-ds';
1122
+ import type { ProfileChipProps } from '@cmgfi/clear-ds';
1123
+ ```
1124
+
1125
+ Person/user identifier pill.
1126
+
1127
+ Props:
1128
+ - `label`: string — **required**
1129
+ - `leading`: ReactNode — rendered before the label in a 18×18px circular clip (avatar, initials, etc.)
1130
+ - `trailing`: ReactNode — rendered after the label in a 18×18px circular clip
1131
+ - `dismissible`: boolean — default `true`
1132
+ - `onDismiss`: `() => void`
1133
+
1134
+ ```tsx
1135
+ <ProfileChip label="Maria Johnson" />
1136
+ <ProfileChip label="Robert Chen" leading={<img src="/avatars/rchen.jpg" alt="" />} />
1137
+ ```
1138
+
1139
+ ---
1140
+
1141
+ ### AUSChip
1142
+
1143
+ ```tsx
1144
+ import { AUSChip } from '@cmgfi/clear-ds';
1145
+ import type { AUSChipProps, AUSChipColor } from '@cmgfi/clear-ds';
1146
+ ```
1147
+
1148
+ Automated Underwriting System result chip. Non-dismissible.
1149
+
1150
+ Props:
1151
+ - `label`: string — e.g. `'Approve/Eligible'`, `'Refer/Eligible'`
1152
+ - `color`: `'green' | 'red' | 'yellow' | 'grey'`
1153
+
1154
+ ```tsx
1155
+ <AUSChip label="Approve/Eligible" color="green" />
1156
+ <AUSChip label="Refer/Eligible" color="yellow" />
1157
+ ```
1158
+
1159
+ ---
1160
+
1161
+ ### TopBar
1162
+
1163
+ ```tsx
1164
+ import { TopBar } from '@cmgfi/clear-ds';
1165
+ import type { TopBarProps, TopBarNavItem, TopBarMenuItem, TopBarUserProfile } from '@cmgfi/clear-ds';
1166
+ ```
1167
+
1168
+ Desktop application top bar with logo, nav items, search, and profile menu.
1169
+
1170
+ Props:
1171
+ - `navItems`: `TopBarNavItem[]` — `{ id: string; label: string; items?: TopBarMenuItem[]; onSelect?: (value: string) => void; isPrimary?: boolean }`
1172
+ - `isPrimary`: renders the item in teal with a `+` circle icon (for the Create CTA)
1173
+ - `TopBarMenuItem`: `{ label: string; value: string; icon?: ReactNode; disabled?: boolean; divider?: boolean }`
1174
+ - `profile`: `TopBarUserProfile` — `{ name: string; role?: string; initials?: string; menuItems?: TopBarMenuItem[]; onMenuSelect?: (value: string) => void }`
1175
+ - `version`: string — e.g. `"v.1.88.3459"`, shown next to the logo
1176
+ - `onSearch`: `(query: string) => void`
1177
+ - `searchPlaceholder`: string — default `"Search Leads and Loans"`
1178
+
1179
+ ---
1180
+
1181
+ ### TopBarMobile
1182
+
1183
+ ```tsx
1184
+ import { TopBarMobile } from '@cmgfi/clear-ds';
1185
+ import type { TopBarMobileProps, TopBarMobileBorrower } from '@cmgfi/clear-ds';
1186
+ ```
1187
+
1188
+ Mobile top bar. Two modes: standard (Clear logo centered) and URLA (small C logo + borrower name).
1189
+
1190
+ Props:
1191
+ - `profile`: `TopBarUserProfile` — **required**
1192
+ - `borrower`: `TopBarMobileBorrower` — `{ name: string; onInfoClick?: () => void }`; when provided, activates URLA mode
1193
+ - `onMenuOpen`: `() => void`
1194
+ - `onSearchOpen`: `() => void`
1195
+
1196
+ ---
1197
+
1198
+ ### URLATabsNav
1199
+
1200
+ ```tsx
1201
+ import { URLATabsNav } from '@cmgfi/clear-ds';
1202
+ import type {
1203
+ URLATabsNavProps, URLATabsNavApplicantOption, URLATabsNavTab, URLATabsNavMenuItem
1204
+ } from '@cmgfi/clear-ds';
1205
+ ```
1206
+
1207
+ Single-row desktop sub-navigation for the URLA section. Left: applicant selector + optional sort. Right: optional AUS buttons + scrollable tab strip.
1208
+
1209
+ Props:
1210
+ - `applicants`: `URLATabsNavApplicantOption[]` — `{ id: string; label: string }`
1211
+ - `selectedApplicantId`: string
1212
+ - `onApplicantChange`: `(id: string) => void`
1213
+ - `onAddApplicant`: `() => void`
1214
+ - `sortItems`: `URLATabsNavMenuItem[]` — `{ label: string; value: string }`
1215
+ - `onSortSelect`: `(value: string) => void`
1216
+ - `tabs`: `URLATabsNavTab[]` — `{ id: string; label: string; disabled?: boolean }`
1217
+ - `activeTabId`: string
1218
+ - `onTabChange`: `(id: string) => void`
1219
+ - `onNextValidation`: `() => void` — renders a red "Next Validation" button
1220
+ - `nextValidationBadge`: number — count badge on the validation button
1221
+ - `onBackToAUS`: `() => void` — renders a secondary "Back to AUS" button
1222
+
1223
+ ---
1224
+
1225
+ ### URLATabsNavTablet
1226
+
1227
+ ```tsx
1228
+ import { URLATabsNavTablet } from '@cmgfi/clear-ds';
1229
+ import type { URLATabsNavTabletProps } from '@cmgfi/clear-ds';
1230
+ ```
1231
+
1232
+ Two-row tablet variant (~992px). Row 1: borrower name + "Loan Details" | applicant selector + sort | AUS buttons. Row 2: scrollable tab strip.
1233
+
1234
+ Props (extends URLATabsNav props with):
1235
+ - `borrowerName`: string — **required**
1236
+ - `onLoanDetailsClick`: `() => void`
1237
+ - `tabStripBadge`: number — optional red count badge at the far right of the tab strip
1238
+
1239
+ ---
1240
+
1241
+ ### URLATabsNavMobile
1242
+
1243
+ ```tsx
1244
+ import { URLATabsNavMobile } from '@cmgfi/clear-ds';
1245
+ import type { URLATabsNavMobileProps } from '@cmgfi/clear-ds';
1246
+ ```
1247
+
1248
+ Two-row touch-optimized mobile variant (~768px). Row 1: borrower name + section dropdown + sort + Actions + Save. Row 2: AUS pills + applicant selector (44px) + sort button + tab dropdown.
1249
+
1250
+ Props (extends URLATabsNav props with):
1251
+ - `borrowerName`: string — **required**
1252
+ - `onLoanDetailsClick`: `() => void`
1253
+ - `sectionLabel`: string — **required** (current section, e.g. `"URLA"`)
1254
+ - `sectionItems`: `URLATabsNavMenuItem[]`
1255
+ - `onSectionSelect`: `(value: string) => void`
1256
+ - `onSectionSort`: `() => void`
1257
+ - `actionItems`: `URLATabsNavMenuItem[]`
1258
+ - `onActionSelect`: `(value: string) => void`
1259
+ - `onSave`: `() => void`
1260
+
1261
+ ---
1262
+
1263
+ ### LoanBannerNav
1264
+
1265
+ ```tsx
1266
+ import { LoanBannerNav } from '@cmgfi/clear-ds';
1267
+ import type {
1268
+ LoanBannerNavProps, LoanBannerNavAction, LoanBannerNavCondition,
1269
+ LoanBannerNavToolbar, LoanBannerNavBanner, LoanBannerNavCollapsedSection
1270
+ } from '@cmgfi/clear-ds';
1271
+ ```
1272
+
1273
+ Primary loan-file navigation bar (83px tall). Renders BannerTabs in the center and a configurable action zone on the right.
1274
+
1275
+ Props:
1276
+ - `tabs`: `BannerTabItem[]` — **required**
1277
+ - `activeTabId`: string — **required**
1278
+ - `onTabChange`: `(id: string) => void` — **required**
1279
+ - `onMenuToggle`: `() => void` — hamburger button handler
1280
+ - `secondaryActions`: `LoanBannerNavAction[]` — action buttons before the primary
1281
+ - `LoanBannerNavAction`: `{ value: string; label?: string; onClick?: () => void; variant?: ButtonVariant; icon?: ReactNode; badge?: string | number; items?: DropdownButtonItem[]; onSelect?: (value: string) => void; dividerBefore?: boolean; ariaLabel?: string; lightning?: boolean; lightningVariant?: 'basic' | 'filled' }`
1282
+ - `onRefresh`: `() => void` — dedicated refresh icon button
1283
+ - `primaryLabel`: string — primary button label, default `'Save Loan'`
1284
+ - `onPrimary`: `() => void`
1285
+ - `primaryItems`: `SplitButtonItem[]` — when provided, primary renders as a SplitButton
1286
+ - `onPrimarySelect`: `(value: string) => void`
1287
+ - `condition`: `LoanBannerNavCondition` — `{ label: string; onDismiss?: () => void }`
1288
+ - `toolbar`: `LoanBannerNavToolbar` — `{ left?: ReactNode; center?: ReactNode; right?: ReactNode }` — adds a secondary row below
1289
+ - `banner`: `LoanBannerNavBanner` — `{ title: string; detail?: string; severity?: BannerAlertSeverity; closable?: boolean; onClose?: () => void }` — adds a BannerAlert strip below
1290
+ - `collapsed`: boolean — compact single-row mode (hides BannerTabs, shows breadcrumbs)
1291
+ - `collapsedSection`: `LoanBannerNavCollapsedSection` — `{ label: string; subLabel?: string; status?: StatusColor }`
1292
+ - `onCollapsedBack`: `() => void`
1293
+ - `onExpandTabs`: `() => void`
1294
+ - `collapsedQuickItems`: `DropdownButtonItem[]`
1295
+ - `onCollapsedQuickAction`: `(value: string) => void`
1296
+
1297
+ ---
1298
+
1299
+ ### FullNav
1300
+
1301
+ ```tsx
1302
+ import { FullNav } from '@cmgfi/clear-ds';
1303
+ import type { FullNavProps } from '@cmgfi/clear-ds';
1304
+ ```
1305
+
1306
+ Full desktop navigation shell. Stacks TopBar → optional LoanBannerNav → optional URLATabsNav.
1307
+
1308
+ Props:
1309
+ - `topBar`: `TopBarProps` — **required**
1310
+ - `loanBannerNav`: `LoanBannerNavProps` — renders the loan banner below the top bar
1311
+ - `urlATabsNav`: `URLATabsNavProps` — renders the URLA tab nav below the loan banner (requires `loanBannerNav`)
1312
+
1313
+ ```tsx
1314
+ <FullNav
1315
+ topBar={{ version: 'v.1.88.3459', navItems: NAV_ITEMS, profile: PROFILE, onSearch: () => {} }}
1316
+ loanBannerNav={{ tabs: BANNER_TABS, activeTabId: activeTab, onTabChange: setActiveTab, primaryLabel: 'Save Loan', onPrimary: handleSave }}
1317
+ urlATabsNav={{ applicants: APPLICANTS, selectedApplicantId: applicant, onApplicantChange: setApplicant, tabs: URLA_TABS, activeTabId: urlaTab, onTabChange: setUrlaTab }}
1318
+ />
1319
+ ```
1320
+
1321
+ ---
1322
+
1323
+ ### FullNavMobile
1324
+
1325
+ ```tsx
1326
+ import { FullNavMobile } from '@cmgfi/clear-ds';
1327
+ import type { FullNavMobileProps } from '@cmgfi/clear-ds';
1328
+ ```
1329
+
1330
+ Mobile navigation shell. Stacks TopBarMobile → optional URLATabsNavMobile (only when `topBar.borrower` is set).
1331
+
1332
+ Props:
1333
+ - `topBar`: `TopBarMobileProps` — **required**
1334
+ - `urlATabsNavMobile`: `URLATabsNavMobileProps` — renders when `topBar.borrower` is also set
1335
+
1336
+ ---
1337
+
1338
+ ## Token Reference
1339
+
1340
+ All tokens are CSS custom properties. Import `@cmgfi/clear-ds/tokens` in your app root.
1341
+
1342
+ **Base scale: 1rem = 12px** — set `html { font-size: 12px }` in your app.
1343
+
1344
+ ### Colors
1345
+
1346
+ 7 families × 10 shades (-50 to -900):
1347
+
1348
+ ```css
1349
+ /* Teal — brand, primary actions, focus rings */
1350
+ --teal-50 through --teal-900
1351
+ /* key: --teal-500: #32A4AC --teal-600: #138890 --teal-700: #047880 */
1352
+
1353
+ /* Surface — backgrounds */
1354
+ --surface-50: #FFFFFF --surface-100: #FCFCFC --surface-200: #F7F7F7
1355
+ --surface-300: #F4F4F5 --surface-400: #EDEDED --surface-500: #E4E4E4
1356
+ --surface-600: #CFCFCF --surface-700: #A2A2A2 --surface-800: #7D7D7D
1357
+ --surface-900: #606060
1358
+
1359
+ /* Navy — text, borders */
1360
+ --navy-50 through --navy-900
1361
+ /* key: --navy-800: #171D22 --navy-900: #12161A */
1362
+
1363
+ /* Semantic */
1364
+ --color-text: #202020
1365
+ --color-text-secondary: #70777D
1366
+
1367
+ /* Severity */
1368
+ --yellow-* (warning) --blue-* (info) --green-* (success) --red-* (error)
1369
+ ```
1370
+
1371
+ ### Spacing
1372
+
1373
+ ```css
1374
+ /* Numeric scale */
1375
+ --spacing-02: 2px --spacing-04: 4px --spacing-06: 6px
1376
+ --spacing-08: 8px --spacing-12: 12px --spacing-16: 16px
1377
+ --spacing-20: 20px --spacing-24: 24px --spacing-32: 32px --spacing-40: 40px
1378
+
1379
+ /* Named aliases */
1380
+ --spacing-xxxs: 2px --spacing-xxs: 4px --spacing-xs: 6px --spacing-sm: 8px
1381
+ --spacing-md: 12px --spacing-lg: 16px --spacing-xl: 24px --spacing-xxl: 32px
1382
+ ```
1383
+
1384
+ ### Typography
1385
+
1386
+ ```css
1387
+ --font-family: 'Open Sans', sans-serif
1388
+
1389
+ /* Utility classes */
1390
+ .text-h1 .text-h2 .text-h3 .text-h4 .text-h5 .text-h6
1391
+ .text-xl-bold .text-xl-semibold .text-xl-regular
1392
+ .text-large-bold .text-large-semibold .text-large-regular
1393
+ .text-normal-bold .text-normal-semibold .text-normal-regular
1394
+ .text-small-bold .text-small-semibold .text-small-regular
1395
+ ```
1396
+
1397
+ ### Elevation
1398
+
1399
+ ```css
1400
+ --shadow-depth-1: 0 1px 4px rgba(0,0,0,0.12)
1401
+ --shadow-depth-2: 0 2px 8px rgba(0,0,0,0.16)
1402
+ --shadow-depth-3: 0 4px 16px rgba(0,0,0,0.20)
1403
+ --shadow-depth-4: 0 8px 32px rgba(0,0,0,0.24)
1404
+ ```
1405
+
1406
+ ### Icons
1407
+
1408
+ ```css
1409
+ --icon-size-sm: 12px --icon-size-md: 14px --icon-size-lg: 18px --icon-size-xl: 24px
1410
+ ```
1411
+
1412
+ ```tsx
1413
+ <i className="pi pi-check" style={{ fontSize: 'var(--icon-size-lg)' }} />
1414
+ ```
1415
+
1416
+ ### Grid
1417
+
1418
+ ```css
1419
+ --grid-columns-mobile: 4 --grid-gutter-mobile: 16px --grid-margin-mobile: 24px
1420
+ --grid-columns-tablet: 6 --grid-gutter-tablet: 16px --grid-margin-tablet: 48px
1421
+ --grid-columns-desktop: 12 --grid-gutter-desktop: 18px --grid-margin-desktop: 128px
1422
+ --breakpoint-sm: 576px --breakpoint-md: 768px --breakpoint-lg: 1200px
1423
+ ```
1424
+
1425
+ ---
1426
+
1427
+ ## Best Practices
1428
+
1429
+ The full interactive best practices reference is in Storybook under **Best Practices/**. Summary:
1430
+
1431
+ ### Accessibility
1432
+ - Every image needs alt text — `alt=""` for decorative images
1433
+ - All form inputs must have a visible `<label>` — never use placeholder as label
1434
+ - Use `:focus-visible` styles, not `:focus` — keyboard-only focus rings
1435
+ - `aria-live` regions for dynamic content updates
1436
+ - Minimum color contrast 4.5:1 for normal text, 3:1 for large text (WCAG AA)
1437
+ - Icon-only buttons must have `aria-label`
1438
+ - Use semantic HTML — `<button>` for actions, `<a>` for navigation
1439
+
1440
+ ### Color
1441
+ - Color is never the only indicator of state — always pair with text, icon, or shape
1442
+ - Red is for destructive actions and errors only
1443
+ - Teal (`--teal-500/600`) for primary interactive elements; never invent new interactive colors
1444
+ - Never use color tokens outside the system — always reference `var(--token-name)`
1445
+
1446
+ ### Forms
1447
+ - Labels go above fields — never use placeholder text as a label
1448
+ - Group related fields; separate groups with 32px + a section heading
1449
+ - Validate inline, adjacent to the field — not in a distant summary list
1450
+ - Always use a single primary submit action per form section
1451
+
1452
+ ### Error Handling
1453
+ - Display error messages adjacent to the field or action that caused them
1454
+ - Be specific: "SSN must be 9 digits" not "Invalid input"
1455
+ - Page-level `BannerAlert` for system errors; field-level `helperText` + `invalid` for validation
1456
+ - Never auto-dismiss error toasts — users need time to read them
1457
+
1458
+ ### Feedback & Notifications
1459
+ - Always acknowledge completed async actions with a success message
1460
+ - Success toasts: auto-dismiss after 5s (`duration: 5000`)
1461
+ - Error toasts: no auto-dismiss (`duration: 0`)
1462
+ - Use `ProgressBar` for operations with known progress; `ProgressSpinner` for indeterminate
1463
+
1464
+ ### Loading & Async
1465
+ - 0–100ms: no loading indicator needed
1466
+ - 100ms–1s: loading state on the triggering button (disabled + spinner)
1467
+ - 1s+: `ProgressSpinner` or `ProgressBar` in the content area
1468
+ - Skeleton screens feel faster than spinners for predictable-shape content
1469
+ - Optimistic UI: update immediately, roll back on error with a Toast
1470
+
1471
+ ### Destructive Actions
1472
+ - Always confirm before irreversible operations — use Modal with explicit "Cancel" + "Delete permanently"
1473
+ - `autoFocus` the Cancel button in destructive confirmation modals
1474
+ - Label the destructive action specifically: "Delete application" not "Delete"
1475
+ - Use `Button variant="danger"` for destructive primary actions
1476
+
1477
+ ### Microcopy
1478
+ - Button labels: Verb + Noun — "Save application" not "Submit"
1479
+ - Sentence case for all labels — not Title Case, not ALL CAPS
1480
+ - Avoid "Click here" — write descriptive link text
1481
+ - Error messages: what went wrong + how to fix it
1482
+
1483
+ ### Mobile & Responsive
1484
+ - Design for 375px (iPhone SE) as the smallest mobile viewport
1485
+ - Minimum touch target: 44×44px (use `Button md` = 40px for mobile; supplement with padding)
1486
+ - Test at 375px, 768px, 1024px, 1440px
1487
+ - Stack form fields vertically on mobile; never use multi-column form layouts below 768px
1488
+
1489
+ ### Navigation
1490
+ - Users must always know: where they are, where else they can go, and how they got here
1491
+ - Active nav item must be visually obvious — not color alone (add bold/underline/background)
1492
+ - Deep links must work — always reflect navigation state in the URL
1493
+ - Keep navigation consistent across all pages
1494
+
1495
+ ### Performance Perception
1496
+ - Any action >100ms: immediate visual feedback on the triggering element
1497
+ - Any operation >1s: `ProgressSpinner` or `ProgressBar`
1498
+ - Debounce search/filter inputs at 300ms
1499
+ - Lazy-load images below the fold; preload critical above-fold images
1500
+
1501
+ ### Spacing & Layout
1502
+ - Use spacing token scale — never invent arbitrary pixel values
1503
+ - Card padding: `--spacing-16` (compact) or `--spacing-24` (content-heavy)
1504
+ - Form section gaps: `--spacing-32` with a section heading
1505
+ - Group related items within 8px; separate groups by at least 24px
1506
+
1507
+ ### Tables & Data
1508
+ - Right-align numbers; left-align text; center-align icons and status badges
1509
+ - Always show context: "Showing 25 of 1,240 results"
1510
+ - Sticky column headers on tables taller than the viewport
1511
+ - Empty/filtered states must explain what's filtered and offer "Clear filters"
1512
+
1513
+ ### Typography
1514
+ - Body text minimum 14px. Labels and helper text minimum 12px. Never below 12px
1515
+ - Line length: 60–80 characters per line
1516
+ - Line height: 1.4–1.6× for body text; 1.1–1.2× for headings
1517
+ - Never more than 2 typefaces or 4 distinct sizes on one screen
1518
+
1519
+ ### Visual Hierarchy
1520
+ - Every screen has exactly one primary action (one `Button variant="primary"`)
1521
+ - Use maximum 3 levels of visual weight per screen
1522
+ - Primary button is always the rightmost in a button row
1523
+ - Destructive actions are always leftmost (so users encounter Cancel before Delete)
1524
+
1525
+ ---
1526
+
1527
+ ## Common Patterns
1528
+
1529
+ ### Form with validation
1530
+
1531
+ ```tsx
1532
+ const [name, setName] = useState('');
1533
+ const [nameError, setNameError] = useState('');
1534
+
1535
+ function validate() {
1536
+ if (!name) { setNameError('First name is required.'); return false; }
1537
+ return true;
1538
+ }
1539
+
1540
+ <InputText
1541
+ label="First name"
1542
+ required
1543
+ value={name}
1544
+ onChange={e => { setName(e.target.value); setNameError(''); }}
1545
+ invalid={!!nameError}
1546
+ helperText={nameError}
1547
+ />
1548
+ <Button variant="primary" onClick={() => validate() && handleSubmit()}>Save application</Button>
1549
+ ```
1550
+
1551
+ ### Loading button state
1552
+
1553
+ ```tsx
1554
+ const [saving, setSaving] = useState(false);
1555
+
1556
+ async function handleSave() {
1557
+ setSaving(true);
1558
+ try { await saveApplication(); toast.success('Application saved'); }
1559
+ catch { toast.error('Save failed', 'Check your connection.', { duration: 0 }); }
1560
+ finally { setSaving(false); }
1561
+ }
1562
+
1563
+ <Button
1564
+ variant="primary"
1565
+ disabled={saving}
1566
+ leadingIcon={saving ? <i className="pi pi-spin pi-spinner" /> : undefined}
1567
+ onClick={handleSave}
1568
+ >
1569
+ {saving ? 'Saving…' : 'Save application'}
1570
+ </Button>
1571
+ ```
1572
+
1573
+ ### Confirmation modal for destructive action
1574
+
1575
+ ```tsx
1576
+ const [isOpen, setIsOpen] = useState(false);
1577
+
1578
+ <Button variant="danger" size="sm" onClick={() => setIsOpen(true)}>Delete application</Button>
1579
+ <Modal
1580
+ isOpen={isOpen}
1581
+ onClose={() => setIsOpen(false)}
1582
+ title='Delete "July 2024 Purchase Loan"?'
1583
+ footer={
1584
+ <>
1585
+ <Button variant="secondary" onClick={() => setIsOpen(false)} autoFocus>Cancel</Button>
1586
+ <Button variant="danger" onClick={() => { handleDelete(); setIsOpen(false); }}>Delete permanently</Button>
1587
+ </>
1588
+ }
1589
+ >
1590
+ <p style={{ margin: 0, fontSize: 14, color: 'var(--color-text)', lineHeight: 1.6 }}>
1591
+ This action cannot be undone. The loan file and all associated documents will be permanently removed.
1592
+ </p>
1593
+ </Modal>
1594
+ ```
1595
+
1596
+ ### Data table with server-side sort and pagination
1597
+
1598
+ ```tsx
1599
+ const [first, setFirst] = useState(0);
1600
+ const [sortField, setSortField] = useState('borrowerName');
1601
+ const [sortOrder, setSortOrder] = useState<SortOrder>(1);
1602
+
1603
+ <DataTable
1604
+ data={loans}
1605
+ columns={columns}
1606
+ sortField={sortField}
1607
+ sortOrder={sortOrder}
1608
+ onSort={({ field, order }) => { setSortField(field); setSortOrder(order); }}
1609
+ loading={isLoading}
1610
+ emptyMessage="No loans match the current filters."
1611
+ />
1612
+ <Paginator
1613
+ totalRecords={totalCount}
1614
+ first={first}
1615
+ rows={25}
1616
+ onPageChange={e => setFirst(e.first)}
1617
+ />
1618
+ ```
1619
+
1620
+ ### Toast notifications
1621
+
1622
+ ```tsx
1623
+ // App root — mount once
1624
+ <Toaster />
1625
+
1626
+ // After async operations
1627
+ toast.success('Document uploaded', 'Income verification added to file.');
1628
+ toast.error('Upload failed', 'The file exceeds the 25 MB limit.', { duration: 0 });
1629
+ toast.warn('Session expiring', 'You will be logged out in 5 minutes.');
1630
+ ```
1631
+
1632
+ ---
1633
+
1634
+ ## Architecture Notes
1635
+
1636
+ - **CSS Modules** — all component styles are scoped; no class name collisions with your app
1637
+ - **Zero runtime CSS** — no CSS-in-JS; styles are pre-compiled at build time
1638
+ - **Tree-shakeable** — import only the components you use; unused components are dropped by your bundler
1639
+ - **`React.forwardRef` on every component** — all refs forward to the underlying DOM element
1640
+ - **Token vars only in styles** — no hardcoded colors or spacing anywhere in component CSS
1641
+ - **Consumer `className` is always merged**, never replaced — pass any class to any component safely
1642
+ - **Dual output** — `index.mjs` (ESM) and `index.cjs` (CJS); works in Vite, Webpack, Next.js, Create React App