@attrus-ds/core 1.0.13 → 1.0.17
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/css/components/buttons.css +46 -15
- package/dist/css/components/callout.css +45 -8
- package/dist/css/components/combobox.css +36 -23
- package/dist/css/components/inputs.css +55 -22
- package/dist/css/components/overlays.css +1 -1
- package/dist/css/components/primitives.css +68 -0
- package/dist/css/components/sidebar.css +33 -0
- package/dist/css/components/smart-search.css +56 -23
- package/dist/css/components/status-pills.css +6 -6
- package/dist/css/components/table.css +602 -36
- package/dist/css/components/tabs.css +1 -0
- package/dist/css/components/toggle-chip.css +2 -2
- package/dist/css/components/topbar.css +180 -20
- package/dist/css/tokens.css +50 -10
- package/dist/react/Button/Button.d.ts +10 -2
- package/dist/react/Button/Button.tsx +12 -4
- package/dist/react/Combobox/Combobox.d.ts +5 -0
- package/dist/react/Combobox/Combobox.tsx +26 -4
- package/dist/react/DataTable/DataTable.d.ts +91 -0
- package/dist/react/DataTable/DataTable.tsx +413 -35
- package/dist/react/SearchPill/SearchPill.d.ts +3 -0
- package/dist/react/SearchPill/SearchPill.tsx +6 -1
- package/dist/react/SmartSearch/SmartSearch.d.ts +4 -0
- package/dist/react/SmartSearch/SmartSearch.tsx +14 -1
- package/package.json +3 -2
|
@@ -13,6 +13,9 @@ export interface DataTableColumn<T> {
|
|
|
13
13
|
numeric?: boolean;
|
|
14
14
|
/** Monospace identifier (.dt-id). */
|
|
15
15
|
id?: boolean;
|
|
16
|
+
/** Strike this cell's figure through — an annulled amount inside a row that is
|
|
17
|
+
still live. A fully cancelled row uses `rowState: 'muted'` instead. */
|
|
18
|
+
isVoid?: (row: T) => boolean;
|
|
16
19
|
/** Sortable header (.dt-th-sort with chevron). */
|
|
17
20
|
sortable?: boolean;
|
|
18
21
|
/** Cell renderer — defaults to String(row[key]). */
|
|
@@ -66,8 +69,73 @@ export interface DataTableProps<T> {
|
|
|
66
69
|
export declare function DataTable<T>(props: DataTableProps<T>): React.ReactElement;
|
|
67
70
|
export declare const DTCellStack: React.FC<{ pri: React.ReactNode; sec?: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>>;
|
|
68
71
|
export declare const DTCellLead: React.FC<{ lead: React.ReactNode; pri: React.ReactNode; sec?: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>>;
|
|
72
|
+
export interface DTBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
73
|
+
children?: React.ReactNode;
|
|
74
|
+
tone?: 'success' | 'warning' | 'danger' | 'info';
|
|
75
|
+
/** After the label instead of before it. */
|
|
76
|
+
trailing?: boolean;
|
|
77
|
+
size?: 'sm' | 'md';
|
|
78
|
+
}
|
|
79
|
+
export declare const DTBadge: React.FC<DTBadgeProps>;
|
|
80
|
+
|
|
81
|
+
export interface DTDeltaProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
82
|
+
children?: React.ReactNode;
|
|
83
|
+
direction: 'up' | 'down' | 'flat';
|
|
84
|
+
/** Screen-reader phrasing, e.g. "up 12.4% versus last month". */
|
|
85
|
+
label?: string;
|
|
86
|
+
}
|
|
87
|
+
export declare const DTDelta: React.FC<DTDeltaProps>;
|
|
88
|
+
|
|
69
89
|
export declare const DTActions: React.FC<React.HTMLAttributes<HTMLDivElement>>;
|
|
70
90
|
|
|
91
|
+
/** Lead cell of a subrow — connector glyph, optional mini icon tile, label.
|
|
92
|
+
`parentName` renders a visually-hidden "Item of <parent>:" prefix, since
|
|
93
|
+
native table semantics carry no hierarchy. */
|
|
94
|
+
export interface DTGroupDividerProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
95
|
+
label: React.ReactNode;
|
|
96
|
+
colSpan: number;
|
|
97
|
+
meta?: React.ReactNode;
|
|
98
|
+
}
|
|
99
|
+
export declare function DTGroupDivider(props: DTGroupDividerProps): React.ReactElement;
|
|
100
|
+
|
|
101
|
+
export interface DTDetailProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
102
|
+
colSpan: number;
|
|
103
|
+
open?: boolean;
|
|
104
|
+
title?: React.ReactNode;
|
|
105
|
+
children?: React.ReactNode;
|
|
106
|
+
}
|
|
107
|
+
export declare function DTDetail(props: DTDetailProps): React.ReactElement;
|
|
108
|
+
|
|
109
|
+
export declare const DTToolbar: React.FC<{
|
|
110
|
+
lead?: React.ReactNode;
|
|
111
|
+
filters?: React.ReactNode;
|
|
112
|
+
actions?: React.ReactNode;
|
|
113
|
+
/** Selection state — same bar, preserved height. */
|
|
114
|
+
selecting?: boolean;
|
|
115
|
+
size?: 'md' | 'sm';
|
|
116
|
+
} & React.HTMLAttributes<HTMLDivElement>>;
|
|
117
|
+
|
|
118
|
+
export declare const DTDetailCard: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>>;
|
|
119
|
+
|
|
120
|
+
export declare const DTSubrowLead: React.FC<{
|
|
121
|
+
label: React.ReactNode;
|
|
122
|
+
icon?: React.ReactNode;
|
|
123
|
+
parentName?: string;
|
|
124
|
+
} & React.HTMLAttributes<HTMLDivElement>>;
|
|
125
|
+
|
|
126
|
+
/** Disclosure control for a collapsible subrow group — goes in the parent's
|
|
127
|
+
lead cell. `controls` must match the subrow's id. */
|
|
128
|
+
export declare const DTSubrowToggle: React.FC<{
|
|
129
|
+
expanded: boolean;
|
|
130
|
+
onToggle: () => void;
|
|
131
|
+
controls: string;
|
|
132
|
+
count?: number;
|
|
133
|
+
label?: string;
|
|
134
|
+
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-expanded'>>;
|
|
135
|
+
|
|
136
|
+
/** Animated wrapper for a collapsible subrow's cell content (grid 0fr → 1fr). */
|
|
137
|
+
export declare const DTSubrowCell: React.FC<React.HTMLAttributes<HTMLDivElement>>;
|
|
138
|
+
|
|
71
139
|
export interface DTPaginationProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
72
140
|
/** Left-side mono range text (e.g. "1–10 of 42"). */
|
|
73
141
|
range?: React.ReactNode;
|
|
@@ -75,7 +143,30 @@ export interface DTPaginationProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
75
143
|
page: number;
|
|
76
144
|
pages: number;
|
|
77
145
|
onPage?: (page: number) => void;
|
|
146
|
+
/** Rows-per-page choices, rendered as a segmented pill (short fixed sets only
|
|
147
|
+
— a <select> hides the options behind a tap). Rendering this implies the
|
|
148
|
+
caller re-fetches and resets to page 1. */
|
|
149
|
+
pageSizes?: number[];
|
|
150
|
+
pageSize?: number;
|
|
151
|
+
onPageSize?: (size: number) => void;
|
|
78
152
|
}
|
|
153
|
+
export interface DTParentRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
154
|
+
children?: React.ReactNode;
|
|
155
|
+
/** subrows share the parent's columns; a detail panel does not. */
|
|
156
|
+
kind?: 'subrows' | 'detail';
|
|
157
|
+
open?: boolean;
|
|
158
|
+
onToggle?: () => void;
|
|
159
|
+
controls?: string;
|
|
160
|
+
}
|
|
161
|
+
export declare const DTId: React.FC<React.HTMLAttributes<HTMLSpanElement>>;
|
|
162
|
+
export declare const DTCheckCell: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>>;
|
|
163
|
+
|
|
164
|
+
export declare const DTSubrowMeta: React.FC<{ tag?: React.ReactNode; status?: React.ReactNode; count?: React.ReactNode }>;
|
|
165
|
+
export declare const DTDetailGrid: React.FC<React.HTMLAttributes<HTMLDivElement>>;
|
|
166
|
+
export declare const DTScrollY: React.FC<React.HTMLAttributes<HTMLDivElement>>;
|
|
167
|
+
|
|
168
|
+
export declare const DTParentRow: React.FC<DTParentRowProps>;
|
|
169
|
+
|
|
79
170
|
export declare const DTPagination: React.FC<DTPaginationProps>;
|
|
80
171
|
|
|
81
172
|
export default DataTable;
|
|
@@ -10,6 +10,13 @@ import * as React from 'react';
|
|
|
10
10
|
* data-label automatically.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/* Is this slot content already the canonical bar? Compared by displayName rather
|
|
14
|
+
than identity: DTPagination is declared after DataTable, and a name also
|
|
15
|
+
survives a caller re-exporting the component. */
|
|
16
|
+
function isOwn(node: React.ReactNode, name: string): boolean {
|
|
17
|
+
return React.isValidElement(node) && (node.type as { displayName?: string })?.displayName === name;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export type SortDir = 'asc' | 'desc';
|
|
14
21
|
export type DataTableSize = 'sm' | 'md' | 'lg';
|
|
15
22
|
export type RowState = 'muted' | 'error' | undefined;
|
|
@@ -23,6 +30,11 @@ export interface DataTableColumn<T> {
|
|
|
23
30
|
numeric?: boolean;
|
|
24
31
|
/** Monospace identifier (.dt-id). */
|
|
25
32
|
id?: boolean;
|
|
33
|
+
/** Strike this cell's figure through: the amount is annulled (cancelled,
|
|
34
|
+
reversed, voided) while the ROW itself is still live. A fully cancelled row
|
|
35
|
+
uses `rowState: 'muted'`, which strikes its numeric cells for free. Only
|
|
36
|
+
meaningful on a numeric column — striking a name reads as damage. */
|
|
37
|
+
isVoid?: (row: T) => boolean;
|
|
26
38
|
/** Sortable header (.dt-th-sort with chevron). */
|
|
27
39
|
sortable?: boolean;
|
|
28
40
|
/** Cell renderer — defaults to String(row[key]). */
|
|
@@ -48,7 +60,7 @@ export interface DataTableProps<T> {
|
|
|
48
60
|
loading?: boolean;
|
|
49
61
|
/** How many skeleton rows. Default 4. */
|
|
50
62
|
loadingRows?: number;
|
|
51
|
-
/** sm | md (default) | lg — .dt
|
|
63
|
+
/** sm | md (default) | lg — .dt-sm / .dt-lg. */
|
|
52
64
|
size?: DataTableSize;
|
|
53
65
|
zebra?: boolean;
|
|
54
66
|
sticky?: boolean;
|
|
@@ -71,13 +83,30 @@ export interface DataTableProps<T> {
|
|
|
71
83
|
onSort?: (key: string) => void;
|
|
72
84
|
className?: string;
|
|
73
85
|
style?: React.CSSProperties;
|
|
86
|
+
/** Row scale. */
|
|
87
|
+
size?: 'sm' | 'md' | 'lg';
|
|
88
|
+
/** Vertical rules between columns — for dense numeric grids only. */
|
|
89
|
+
bordered?: boolean;
|
|
90
|
+
/** Alternating row tint. Prefer plain rows; zebra competes with row states. */
|
|
91
|
+
zebra?: boolean;
|
|
92
|
+
/** Sticky header. Requires a bounded height, so wrap in DTScrollY. */
|
|
93
|
+
sticky?: boolean;
|
|
74
94
|
}
|
|
75
95
|
|
|
96
|
+
/* Two arrows stacked in a clipped slot; `.chev-track` slides between them so the
|
|
97
|
+
direction change keeps an arrow pointing the whole way. A single chevron
|
|
98
|
+
rotated 180° also reads, but it spins through horizontal — a frame where the
|
|
99
|
+
glyph points at neither value. */
|
|
76
100
|
const Chev: React.FC = () => (
|
|
77
101
|
<span className="chev" aria-hidden="true">
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
102
|
+
<span className="chev-track">
|
|
103
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
104
|
+
<path d="m18 15-6-6-6 6" />
|
|
105
|
+
</svg>
|
|
106
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
107
|
+
<path d="m6 9 6 6 6-6" />
|
|
108
|
+
</svg>
|
|
109
|
+
</span>
|
|
81
110
|
</span>
|
|
82
111
|
);
|
|
83
112
|
|
|
@@ -121,12 +150,10 @@ export function DataTable<T>({
|
|
|
121
150
|
}: DataTableProps<T>): React.ReactElement {
|
|
122
151
|
const tableCls = [
|
|
123
152
|
'dt',
|
|
124
|
-
size
|
|
125
|
-
|
|
126
|
-
zebra ? 'dt
|
|
127
|
-
sticky ? 'dt
|
|
128
|
-
bordered ? 'dt--bordered' : '',
|
|
129
|
-
onInverse ? 'on-inverse' : '',
|
|
153
|
+
size ? 'dt-' + size : '',
|
|
154
|
+
bordered ? 'dt-bordered' : '',
|
|
155
|
+
zebra ? 'dt-zebra' : '',
|
|
156
|
+
sticky ? 'dt-sticky' : '',
|
|
130
157
|
]
|
|
131
158
|
.filter(Boolean)
|
|
132
159
|
.join(' ');
|
|
@@ -146,6 +173,11 @@ export function DataTable<T>({
|
|
|
146
173
|
</th>
|
|
147
174
|
) : null}
|
|
148
175
|
{columns.map((c) => {
|
|
176
|
+
/* The control is a <button>, not the <th> with onClick: a click
|
|
177
|
+
handler on a header cell is unreachable by keyboard and announces
|
|
178
|
+
nothing, and sorting is the one table action a keyboard user needs
|
|
179
|
+
most. The th keeps aria-sort (the ARIA home for sort state) and
|
|
180
|
+
`.dt-num`; the button carries `.dt-th-sort` and the chevron. */
|
|
149
181
|
const sortCls = c.sortable
|
|
150
182
|
? ['dt-th-sort', sortKey === c.key && sortDir === 'asc' ? 'is-asc' : '', sortKey === c.key && sortDir === 'desc' ? 'is-desc' : '']
|
|
151
183
|
.filter(Boolean)
|
|
@@ -154,12 +186,17 @@ export function DataTable<T>({
|
|
|
154
186
|
return (
|
|
155
187
|
<th
|
|
156
188
|
key={c.key}
|
|
157
|
-
className={
|
|
158
|
-
|
|
159
|
-
aria-sort={sortKey === c.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
189
|
+
className={c.numeric ? 'dt-num' : undefined}
|
|
190
|
+
aria-sort={c.sortable ? (sortKey === c.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none') : undefined}
|
|
160
191
|
>
|
|
161
|
-
{c.
|
|
162
|
-
|
|
192
|
+
{c.sortable ? (
|
|
193
|
+
<button type="button" className={sortCls} onClick={onSort ? () => onSort(c.key) : undefined}>
|
|
194
|
+
{c.header}
|
|
195
|
+
<Chev />
|
|
196
|
+
</button>
|
|
197
|
+
) : (
|
|
198
|
+
c.header
|
|
199
|
+
)}
|
|
163
200
|
</th>
|
|
164
201
|
);
|
|
165
202
|
})}
|
|
@@ -214,7 +251,7 @@ export function DataTable<T>({
|
|
|
214
251
|
{columns.map((c) => (
|
|
215
252
|
<td
|
|
216
253
|
key={c.key}
|
|
217
|
-
className={[c.numeric ? 'dt-num' : '', c.id ? 'dt-id' : ''].filter(Boolean).join(' ') || undefined}
|
|
254
|
+
className={[c.numeric ? 'dt-num' : '', c.id ? 'dt-id' : '', c.isVoid && c.isVoid(row) ? 'is-void' : ''].filter(Boolean).join(' ') || undefined}
|
|
218
255
|
data-label={typeof c.header === 'string' ? c.header : undefined}
|
|
219
256
|
>
|
|
220
257
|
{c.render ? c.render(row) : String((row as Record<string, unknown>)[c.key] ?? '')}
|
|
@@ -234,9 +271,16 @@ export function DataTable<T>({
|
|
|
234
271
|
|
|
235
272
|
return (
|
|
236
273
|
<div className={['dt-wrap', className || ''].filter(Boolean).join(' ')} style={style}>
|
|
237
|
-
{
|
|
274
|
+
{/* Same rule as the pagination slot: DTToolbar already IS a .dt-toolbar,
|
|
275
|
+
so wrapping it would nest two bars and the inner one would shrink-wrap. */}
|
|
276
|
+
{toolbar ? (isOwn(toolbar, 'DTToolbar') ? toolbar : <div className="dt-toolbar">{toolbar}</div>) : null}
|
|
238
277
|
{inner}
|
|
239
|
-
{
|
|
278
|
+
{/* The slot does NOT wrap: DTPagination already IS a .dt-pagination, and
|
|
279
|
+
wrapping it produced a 958px footer holding a 438px footer — the inner
|
|
280
|
+
one shrink-wrapped and sat left, so the pager looked bunched even though
|
|
281
|
+
its own margin-left:auto was working. Raw children still get the frame,
|
|
282
|
+
which is what the slot is for. */}
|
|
283
|
+
{pagination ? (isOwn(pagination, 'DTPagination') ? pagination : <div className="dt-pagination">{pagination}</div>) : null}
|
|
240
284
|
</div>
|
|
241
285
|
);
|
|
242
286
|
}
|
|
@@ -266,8 +310,187 @@ export const DTCellLead: React.FC<{ lead: React.ReactNode; pri: React.ReactNode;
|
|
|
266
310
|
</div>
|
|
267
311
|
);
|
|
268
312
|
|
|
269
|
-
/**
|
|
270
|
-
|
|
313
|
+
/** Group separator — a full-width overline that labels the rows beneath it.
|
|
314
|
+
It is a LABEL, not data: it carries no row state, no hover, and spans every
|
|
315
|
+
column, so `colSpan` must match the table's column count. */
|
|
316
|
+
export interface DTGroupDividerProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
317
|
+
label: React.ReactNode;
|
|
318
|
+
colSpan: number;
|
|
319
|
+
/** Optional right-aligned aggregate ("4 payouts · −$18,750.00"). */
|
|
320
|
+
meta?: React.ReactNode;
|
|
321
|
+
}
|
|
322
|
+
export function DTGroupDivider(props: DTGroupDividerProps): React.ReactElement {
|
|
323
|
+
const { label, colSpan, meta, className, ...rest } = props;
|
|
324
|
+
return (
|
|
325
|
+
<tr className={['dt-row-divider', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
326
|
+
<td colSpan={colSpan}>
|
|
327
|
+
{label}
|
|
328
|
+
{meta != null ? <span className="dt-row-divider-meta">{meta}</span> : null}
|
|
329
|
+
</td>
|
|
330
|
+
</tr>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Detail panel — a child row that keeps its OWN columns instead of borrowing
|
|
335
|
+
the parent's. Use when the child data shares no schema with the row above
|
|
336
|
+
(an audit trail under a payment); use a subrow when it does. The parent must
|
|
337
|
+
carry `has-detail` so its bottom border is dropped and the pair reads as
|
|
338
|
+
one block. */
|
|
339
|
+
export interface DTDetailProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
340
|
+
colSpan: number;
|
|
341
|
+
open?: boolean;
|
|
342
|
+
title?: React.ReactNode;
|
|
343
|
+
children?: React.ReactNode;
|
|
344
|
+
}
|
|
345
|
+
export function DTDetail(props: DTDetailProps): React.ReactElement {
|
|
346
|
+
const { colSpan, open, title, children, className, ...rest } = props;
|
|
347
|
+
return (
|
|
348
|
+
<tr className={['dt-detail', open ? 'is-open' : '', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
349
|
+
<td colSpan={colSpan}>
|
|
350
|
+
<div className="dt-detail-body">
|
|
351
|
+
{title != null ? <div className="dt-detail-title">{title}</div> : null}
|
|
352
|
+
{children}
|
|
353
|
+
</div>
|
|
354
|
+
</td>
|
|
355
|
+
</tr>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Table chrome — the bar above the data. Three zones with fixed roles: lead
|
|
360
|
+
carries identity, filters the middle, actions cluster right via
|
|
361
|
+
margin-left:auto (never a flex:1 spacer, which collapses the lead).
|
|
362
|
+
`selecting` is the same bar in another state, not a second bar — the height
|
|
363
|
+
is preserved so the table does not jump when the first row is ticked. */
|
|
364
|
+
export interface DTToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
365
|
+
lead?: React.ReactNode;
|
|
366
|
+
filters?: React.ReactNode;
|
|
367
|
+
actions?: React.ReactNode;
|
|
368
|
+
selecting?: boolean;
|
|
369
|
+
size?: 'md' | 'sm';
|
|
370
|
+
}
|
|
371
|
+
export function DTToolbar(props: DTToolbarProps): React.ReactElement {
|
|
372
|
+
const { lead, filters, actions, selecting, size, className, ...rest } = props;
|
|
373
|
+
const cls = ['dt-toolbar', size === 'sm' ? 'dt-toolbar-sm' : '', selecting ? 'is-selecting' : '', className || ''].filter(Boolean).join(' ');
|
|
374
|
+
return (
|
|
375
|
+
<div className={cls} {...rest}>
|
|
376
|
+
{lead != null ? <div className="dt-toolbar-lead">{lead}</div> : null}
|
|
377
|
+
{filters != null ? <div className="dt-toolbar-filters">{filters}</div> : null}
|
|
378
|
+
{actions != null ? <div className="dt-toolbar-actions">{actions}</div> : null}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Clickable key-value card inside a detail panel. */
|
|
384
|
+
export function DTDetailCard(props: React.ButtonHTMLAttributes<HTMLButtonElement>): React.ReactElement {
|
|
385
|
+
const { children, className, ...rest } = props;
|
|
386
|
+
return <button type="button" className={['dt-detail-card', className || ''].filter(Boolean).join(' ')} {...rest}>{children}</button>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Lead cell of a subrow — connector glyph, optional mini icon tile, label.
|
|
390
|
+
The glyph is a CSS ::before so it is neither copied with the text nor
|
|
391
|
+
announced as a character. `parentName` renders a visually-hidden prefix:
|
|
392
|
+
native table semantics carry no hierarchy, so the relationship is stated in
|
|
393
|
+
CONTENT rather than by claiming role="treegrid" without its keyboard model. */
|
|
394
|
+
export const DTSubrowLead: React.FC<{
|
|
395
|
+
label: React.ReactNode;
|
|
396
|
+
icon?: React.ReactNode;
|
|
397
|
+
parentName?: string;
|
|
398
|
+
} & React.HTMLAttributes<HTMLDivElement>> = ({ label, icon, parentName, className, ...rest }) => (
|
|
399
|
+
<div className={['dt-subrow-lead', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
400
|
+
{icon != null ? <span className="ic-mini">{icon}</span> : null}
|
|
401
|
+
<span className="txt">
|
|
402
|
+
{parentName ? <span className="sr-only">{'Item of ' + parentName + ': '}</span> : null}
|
|
403
|
+
{label}
|
|
404
|
+
</span>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
/** Disclosure control for a collapsible subrow group — put it in the parent's
|
|
409
|
+
lead cell. Wire `controls` to the subrow's id so `aria-controls` resolves. */
|
|
410
|
+
export const DTSubrowToggle: React.FC<{
|
|
411
|
+
expanded: boolean;
|
|
412
|
+
onToggle: () => void;
|
|
413
|
+
controls: string;
|
|
414
|
+
/** Announced as "Show 2 items" / "Hide 2 items". */
|
|
415
|
+
count?: number;
|
|
416
|
+
label?: string;
|
|
417
|
+
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-expanded'>> = ({
|
|
418
|
+
expanded, onToggle, controls, count, label, className, ...rest
|
|
419
|
+
}) => (
|
|
420
|
+
<button
|
|
421
|
+
type="button"
|
|
422
|
+
className={['dt-subrow-toggle', className || ''].filter(Boolean).join(' ')}
|
|
423
|
+
aria-expanded={expanded}
|
|
424
|
+
aria-controls={controls}
|
|
425
|
+
aria-label={(expanded ? 'Hide' : 'Show') + ' ' + (count != null ? count + ' ' : '') + (label || 'linked items')}
|
|
426
|
+
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
|
427
|
+
{...rest}
|
|
428
|
+
>
|
|
429
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
430
|
+
<polyline points="9 18 15 12 9 6" />
|
|
431
|
+
</svg>
|
|
432
|
+
</button>
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
/** Animated wrapper for a collapsible subrow's cell content. A <tr> cannot be
|
|
436
|
+
animated and display:grid on a <td> breaks column alignment, so the height
|
|
437
|
+
transition lives here: a grid going 0fr -> 1fr, which needs no magic pixel
|
|
438
|
+
value and therefore survives a two-line subrow on mobile. */
|
|
439
|
+
export const DTSubrowCell: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className, ...rest }) => (
|
|
440
|
+
<div className="dt-subrow-inner" {...rest}>
|
|
441
|
+
<div className={['dt-subrow-cell', className || ''].filter(Boolean).join(' ')}>{children}</div>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
/** Round tone badge inside a cell — `.ic-round`.
|
|
446
|
+
Leading answers "what kind of row is this" and is scanned DOWN the column, so
|
|
447
|
+
keep one icon per kind. Trailing annotates the label itself ("verified", "has
|
|
448
|
+
a note") and hugs the text rather than floating to the cell edge, where it
|
|
449
|
+
would read as another column. Reach for a Pill when the state needs a word;
|
|
450
|
+
a badge only when the icon is unambiguous on its own. */
|
|
451
|
+
export interface DTBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
452
|
+
children?: React.ReactNode;
|
|
453
|
+
tone?: 'success' | 'warning' | 'danger' | 'info';
|
|
454
|
+
/** After the label instead of before it. */
|
|
455
|
+
trailing?: boolean;
|
|
456
|
+
size?: 'sm' | 'md';
|
|
457
|
+
}
|
|
458
|
+
export const DTBadge: React.FC<DTBadgeProps> = ({ children, tone, trailing, size = 'md', className, ...rest }) => (
|
|
459
|
+
<span
|
|
460
|
+
className={['ic-round', tone ? 'tone-' + tone : '', trailing ? 'is-trailing' : '', size === 'sm' ? 'is-sm' : '', className || ''].filter(Boolean).join(' ')}
|
|
461
|
+
aria-hidden="true"
|
|
462
|
+
{...rest}
|
|
463
|
+
>
|
|
464
|
+
{children}
|
|
465
|
+
</span>
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
/** Trend delta inside a cell — `.stat-delta`.
|
|
469
|
+
Use when the COMPARISON is the point; when the amount is the point, use
|
|
470
|
+
`.dt-num` and put the delta in its own column. Direction is carried by the
|
|
471
|
+
arrow and the sign, never by colour alone — and `flat` is worth rendering,
|
|
472
|
+
because an empty cell reads as missing data rather than "unchanged". Down is
|
|
473
|
+
not automatically bad: label the polarity when it is ambiguous. */
|
|
474
|
+
export interface DTDeltaProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
475
|
+
children?: React.ReactNode;
|
|
476
|
+
direction: 'up' | 'down' | 'flat';
|
|
477
|
+
/** Screen-reader phrasing, e.g. "up 12.4% versus last month". */
|
|
478
|
+
label?: string;
|
|
479
|
+
}
|
|
480
|
+
export const DTDelta: React.FC<DTDeltaProps> = ({ children, direction, label, className, ...rest }) => {
|
|
481
|
+
const path = direction === 'up' ? 'm3 17 6-6 4 4 8-8' : direction === 'down' ? 'm3 7 6 6 4-4 8 8' : 'M5 12h14';
|
|
482
|
+
return (
|
|
483
|
+
<span className={['stat-delta', 'stat-delta-' + direction, className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
484
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
485
|
+
<path d={path} />
|
|
486
|
+
</svg>
|
|
487
|
+
{label ? <span className="sr-only">{label}</span> : null}
|
|
488
|
+
{children}
|
|
489
|
+
</span>
|
|
490
|
+
);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** Trailing actions cluster — `.dt-actions` (ghost icon buttons). */export const DTActions: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...rest }) => (
|
|
271
494
|
<div className={['dt-actions', className || ''].filter(Boolean).join(' ')} {...rest} />
|
|
272
495
|
);
|
|
273
496
|
|
|
@@ -279,30 +502,185 @@ export interface DTPaginationProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
279
502
|
/** Total pages. */
|
|
280
503
|
pages: number;
|
|
281
504
|
onPage?: (page: number) => void;
|
|
505
|
+
/** Rows-per-page choices. Rendering this control implies the caller re-fetches
|
|
506
|
+
and resets to page 1 — the component owns neither. */
|
|
507
|
+
pageSizes?: number[];
|
|
508
|
+
pageSize?: number;
|
|
509
|
+
onPageSize?: (size: number) => void;
|
|
510
|
+
}
|
|
511
|
+
/* Which page numbers to print. Always the first and last step plus the current
|
|
512
|
+
one and its neighbours; an ellipsis stands in for each gap. Under 8 pages
|
|
513
|
+
everything fits, so no gap is worth the visual noise. */
|
|
514
|
+
function pageWindow(page: number, pages: number): Array<number | '…'> {
|
|
515
|
+
if (pages <= 7) return Array.from({ length: pages }, (_, n) => n + 1);
|
|
516
|
+
const out: Array<number | '…'> = [1];
|
|
517
|
+
const from = Math.max(2, page - 1);
|
|
518
|
+
const to = Math.min(pages - 1, page + 1);
|
|
519
|
+
if (from > 2) out.push('…');
|
|
520
|
+
for (let p = from; p <= to; p++) out.push(p);
|
|
521
|
+
if (to < pages - 1) out.push('…');
|
|
522
|
+
out.push(pages);
|
|
523
|
+
return out;
|
|
282
524
|
}
|
|
525
|
+
|
|
526
|
+
/* Rows-per-page. The sliding white pill is a single .tab-indicator positioned by
|
|
527
|
+
custom properties, so SOMETHING has to measure the selected tab and write them:
|
|
528
|
+
the markup alone renders the indicator parked at zero width, which looks like a
|
|
529
|
+
dead control even though the clicks work. Re-measured on value change and on
|
|
530
|
+
resize, since the tab widths depend on the digits ("25" vs "100"). */
|
|
531
|
+
const PageSizeTabs: React.FC<{
|
|
532
|
+
sizes: number[];
|
|
533
|
+
value?: number;
|
|
534
|
+
onChange?: (n: number) => void;
|
|
535
|
+
}> = ({ sizes, value, onChange }) => {
|
|
536
|
+
const groupRef = React.useRef<HTMLSpanElement>(null);
|
|
537
|
+
|
|
538
|
+
React.useEffect(() => {
|
|
539
|
+
const place = () => {
|
|
540
|
+
const group = groupRef.current;
|
|
541
|
+
if (!group) return;
|
|
542
|
+
const active = group.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
543
|
+
if (!active) return;
|
|
544
|
+
group.style.setProperty('--ind-x', active.offsetLeft + 'px');
|
|
545
|
+
group.style.setProperty('--ind-w', active.offsetWidth + 'px');
|
|
546
|
+
};
|
|
547
|
+
place();
|
|
548
|
+
/* Fonts land after first paint and change every tab's width, so a single
|
|
549
|
+
measurement at mount puts the pill in the wrong place. */
|
|
550
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(place).catch(() => {});
|
|
551
|
+
window.addEventListener('resize', place);
|
|
552
|
+
return () => window.removeEventListener('resize', place);
|
|
553
|
+
}, [value, sizes.join(',')]);
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<span className="dt-pagination-size">
|
|
557
|
+
Rows
|
|
558
|
+
<span ref={groupRef} className="tabs tabs-pill tabs-sm is-animated" role="tablist" aria-label="Rows per page">
|
|
559
|
+
<span className="tab-indicator" aria-hidden="true" />
|
|
560
|
+
{sizes.map((n) => (
|
|
561
|
+
<button
|
|
562
|
+
key={n}
|
|
563
|
+
type="button"
|
|
564
|
+
role="tab"
|
|
565
|
+
className="tab"
|
|
566
|
+
aria-selected={n === value}
|
|
567
|
+
onClick={() => onChange && onChange(n)}
|
|
568
|
+
>
|
|
569
|
+
{n}
|
|
570
|
+
</button>
|
|
571
|
+
))}
|
|
572
|
+
</span>
|
|
573
|
+
</span>
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
283
577
|
/** Canonical pagination row — `.range` + `.pager` with `.page-btn`s (prev · numbered · next).
|
|
284
578
|
Pass as the DataTable `pagination` slot. */
|
|
285
|
-
export const DTPagination: React.FC<DTPaginationProps> = ({ range, page, pages, onPage, className, ...rest }) => (
|
|
286
|
-
<div className={className
|
|
287
|
-
{range != null ? <span className="range">{range}</span> : null}
|
|
288
|
-
|
|
579
|
+
export const DTPagination: React.FC<DTPaginationProps> = ({ range, page, pages, onPage, pageSizes, pageSize, onPageSize, className, ...rest }) => (
|
|
580
|
+
<div className={['dt-pagination', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
581
|
+
{range != null ? <span className="dt-pagination-range">{range}</span> : null}
|
|
582
|
+
{pageSizes && pageSizes.length ? (
|
|
583
|
+
<PageSizeTabs sizes={pageSizes} value={pageSize} onChange={onPageSize} />
|
|
584
|
+
) : null}
|
|
585
|
+
<nav className="dt-pagination-nav" aria-label="Pagination">
|
|
586
|
+
{/* `.page-btn` is the canonical control the stylesheet defines: a bordered
|
|
587
|
+
box with a square minimum and mono figures, current step in accent. The
|
|
588
|
+
previous markup reached for `.btn.btn-ghost` instead, so the pager
|
|
589
|
+
rendered as borderless pills and the boxed style the DS ships went
|
|
590
|
+
unused — a class with rules and no callers. */}
|
|
289
591
|
<button type="button" className="page-btn" disabled={page <= 1} onClick={() => onPage && onPage(page - 1)} aria-label="Previous page">
|
|
290
592
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>
|
|
291
593
|
</button>
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
594
|
+
{/* Windowed: first, last, and the neighbours of the current step, with an
|
|
595
|
+
ellipsis for the gap. Printing every page turns a 40-page table's footer
|
|
596
|
+
into a wall of numbers nobody scans. */}
|
|
597
|
+
{pageWindow(page, pages).map((p, i) =>
|
|
598
|
+
p === '…' ? (
|
|
599
|
+
<span key={'gap' + i} className="page-gap" aria-hidden="true">…</span>
|
|
600
|
+
) : (
|
|
601
|
+
<button
|
|
602
|
+
key={p}
|
|
603
|
+
type="button"
|
|
604
|
+
className={p === page ? 'page-btn is-current' : 'page-btn'}
|
|
605
|
+
aria-current={p === page ? 'page' : undefined}
|
|
606
|
+
aria-label={'Page ' + p}
|
|
607
|
+
onClick={() => onPage && onPage(p as number)}
|
|
608
|
+
>
|
|
609
|
+
{p}
|
|
610
|
+
</button>
|
|
611
|
+
)
|
|
612
|
+
)}
|
|
303
613
|
<button type="button" className="page-btn" disabled={page >= pages} onClick={() => onPage && onPage(page + 1)} aria-label="Next page">
|
|
304
614
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
|
|
305
615
|
</button>
|
|
306
616
|
</nav>
|
|
307
617
|
</div>
|
|
308
618
|
);
|
|
619
|
+
|
|
620
|
+
/** Cell utilities. `noTab` turns OFF tabular figures — for IDs and codes,
|
|
621
|
+
where fixed-width digits look mechanical; `check` is the selection gutter,
|
|
622
|
+
a fixed 40px so the first data column starts at the same x either way. */
|
|
623
|
+
export const DTId: React.FC<React.HTMLAttributes<HTMLSpanElement>> = ({ children, className, ...rest }) => (
|
|
624
|
+
<span className={['dt-id', 'dt-no-tab', className || ''].filter(Boolean).join(' ')} {...rest}>{children}</span>
|
|
625
|
+
);
|
|
626
|
+
export const DTCheckCell: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = ({ children, className, ...rest }) => (
|
|
627
|
+
<td className={['dt-check', className || ''].filter(Boolean).join(' ')} {...rest}>{children}</td>
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
/** Trailing chips on a subrow: what KIND of child it is, and its state.
|
|
631
|
+
Kept separate from the label so the two never merge into one string — an
|
|
632
|
+
agent reading the row can tell the taxonomy from the status. */
|
|
633
|
+
export const DTSubrowMeta: React.FC<{ tag?: React.ReactNode; status?: React.ReactNode; count?: React.ReactNode }> = ({ tag, status, count }) => (
|
|
634
|
+
<React.Fragment>
|
|
635
|
+
{tag != null ? <span className="dt-subrow-tag">{tag}</span> : null}
|
|
636
|
+
{status != null ? <span className="dt-subrow-status">{status}</span> : null}
|
|
637
|
+
{count != null ? <span className="dt-subrow-count">{count}</span> : null}
|
|
638
|
+
</React.Fragment>
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
/** Key–value grid inside a detail panel — for ONE record's attributes. Use a
|
|
642
|
+
nested table instead when the panel holds many records of one type. */
|
|
643
|
+
export const DTDetailGrid: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className, ...rest }) => (
|
|
644
|
+
<div className={['dt-detail-grid', className || ''].filter(Boolean).join(' ')} {...rest}>{children}</div>
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
/** Vertical scroller for a sticky-header table. Belongs inside .dt-wrap so the
|
|
648
|
+
wrap keeps ownership of the radius and the border. */
|
|
649
|
+
export const DTScrollY: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className, style, ...rest }) => (
|
|
650
|
+
<div className={['dt-scroll-y', className || ''].filter(Boolean).join(' ')} style={style} {...rest}>{children}</div>
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
/** Parent of a subrow or detail group. It exists because the FUSION is the
|
|
654
|
+
parent's job: `has-subrows` drops the parent's bottom border so the pair
|
|
655
|
+
reads as one block, and without it a caller gets an indented child floating
|
|
656
|
+
under a fully-ruled row — the exact defect the docs warn against. */
|
|
657
|
+
export interface DTParentRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
658
|
+
children?: React.ReactNode;
|
|
659
|
+
/** The group's kind: subrows share the parent's columns, a detail panel does not. */
|
|
660
|
+
kind?: 'subrows' | 'detail';
|
|
661
|
+
open?: boolean;
|
|
662
|
+
/** Whole-row disclosure — adds role/tabindex and the pointer affordance. */
|
|
663
|
+
onToggle?: () => void;
|
|
664
|
+
controls?: string;
|
|
665
|
+
}
|
|
666
|
+
export const DTParentRow: React.FC<DTParentRowProps> = ({ children, kind = 'subrows', open, onToggle, controls, className, ...rest }) => (
|
|
667
|
+
<tr
|
|
668
|
+
className={[kind === 'detail' ? 'has-detail' : 'has-subrows', onToggle ? 'is-collapsible is-clickable' : '', open ? 'is-open' : '', className || ''].filter(Boolean).join(' ')}
|
|
669
|
+
role={onToggle ? 'button' : undefined}
|
|
670
|
+
tabIndex={onToggle ? 0 : undefined}
|
|
671
|
+
aria-expanded={onToggle ? !!open : undefined}
|
|
672
|
+
aria-controls={controls}
|
|
673
|
+
onClick={onToggle}
|
|
674
|
+
onKeyDown={onToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } } : undefined}
|
|
675
|
+
{...rest}
|
|
676
|
+
>
|
|
677
|
+
{children}
|
|
678
|
+
</tr>
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
/* Slot markers, set after both declarations exist. DataTable reads these to tell
|
|
682
|
+
"already the canonical bar" from "raw children that still need the frame" —
|
|
683
|
+
assigning them earlier put DTPagination in its temporal dead zone and took the
|
|
684
|
+
whole module down with it. */
|
|
685
|
+
DTToolbar.displayName = 'DTToolbar';
|
|
686
|
+
DTPagination.displayName = 'DTPagination';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
export interface SearchPillProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
4
|
+
/** Field height: 'md' (40px, default) or 'sm' (32px). Named fieldSize because
|
|
5
|
+
the native input `size` attribute means character count, not height. */
|
|
6
|
+
fieldSize?: 'sm' | 'md';
|
|
4
7
|
/** Keyboard hint rendered as a .kbd chip (e.g. "⌘K"). */
|
|
5
8
|
shortcut?: string;
|
|
6
9
|
/** Leading icon — defaults to the canonical search glyph. */
|