@cmgfi/clear-ds 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +478 -0
- package/dist/llms.txt +1642 -0
- package/dist/tokens/tokens.css +102 -0
- package/package.json +1 -1
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
|