premium-ds 0.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.js +6 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +13 -0
- package/dist/avatar-group.js +3 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +3 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.js +3 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.d.ts +20 -0
- package/dist/button.js +3 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.d.ts +25 -0
- package/dist/checkbox.js +3 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2OWHZ4JT.js +36 -0
- package/dist/chunk-2OWHZ4JT.js.map +1 -0
- package/dist/chunk-34SIXSYL.js +64 -0
- package/dist/chunk-34SIXSYL.js.map +1 -0
- package/dist/chunk-37O2ZXD6.js +55 -0
- package/dist/chunk-37O2ZXD6.js.map +1 -0
- package/dist/chunk-4AZL76UJ.js +89 -0
- package/dist/chunk-4AZL76UJ.js.map +1 -0
- package/dist/chunk-4HSCN5TZ.js +86 -0
- package/dist/chunk-4HSCN5TZ.js.map +1 -0
- package/dist/chunk-5DDOOT33.js +258 -0
- package/dist/chunk-5DDOOT33.js.map +1 -0
- package/dist/chunk-5FVHWIMY.js +117 -0
- package/dist/chunk-5FVHWIMY.js.map +1 -0
- package/dist/chunk-5K6KRJGX.js +147 -0
- package/dist/chunk-5K6KRJGX.js.map +1 -0
- package/dist/chunk-5PQMQBQC.js +74 -0
- package/dist/chunk-5PQMQBQC.js.map +1 -0
- package/dist/chunk-7OCTVQ7C.js +95 -0
- package/dist/chunk-7OCTVQ7C.js.map +1 -0
- package/dist/chunk-7OPMOET7.js +39 -0
- package/dist/chunk-7OPMOET7.js.map +1 -0
- package/dist/chunk-BXXS7YRC.js +270 -0
- package/dist/chunk-BXXS7YRC.js.map +1 -0
- package/dist/chunk-CV2Q4YXX.js +272 -0
- package/dist/chunk-CV2Q4YXX.js.map +1 -0
- package/dist/chunk-EIMMDWIW.js +282 -0
- package/dist/chunk-EIMMDWIW.js.map +1 -0
- package/dist/chunk-EZ2CWTBE.js +230 -0
- package/dist/chunk-EZ2CWTBE.js.map +1 -0
- package/dist/chunk-FGHDG3Y4.js +89 -0
- package/dist/chunk-FGHDG3Y4.js.map +1 -0
- package/dist/chunk-FPP2XLKX.js +127 -0
- package/dist/chunk-FPP2XLKX.js.map +1 -0
- package/dist/chunk-G6OY35DI.js +295 -0
- package/dist/chunk-G6OY35DI.js.map +1 -0
- package/dist/chunk-H6KWJNOE.js +65 -0
- package/dist/chunk-H6KWJNOE.js.map +1 -0
- package/dist/chunk-HGILYGY3.js +45 -0
- package/dist/chunk-HGILYGY3.js.map +1 -0
- package/dist/chunk-I3BCB4Z5.js +88 -0
- package/dist/chunk-I3BCB4Z5.js.map +1 -0
- package/dist/chunk-KBWNUUWM.js +582 -0
- package/dist/chunk-KBWNUUWM.js.map +1 -0
- package/dist/chunk-KN7JFAZ6.js +113 -0
- package/dist/chunk-KN7JFAZ6.js.map +1 -0
- package/dist/chunk-MEF7PI6U.js +16 -0
- package/dist/chunk-MEF7PI6U.js.map +1 -0
- package/dist/chunk-NKGMQL6I.js +310 -0
- package/dist/chunk-NKGMQL6I.js.map +1 -0
- package/dist/chunk-NMFQRGLL.js +127 -0
- package/dist/chunk-NMFQRGLL.js.map +1 -0
- package/dist/chunk-OUBWD6CX.js +433 -0
- package/dist/chunk-OUBWD6CX.js.map +1 -0
- package/dist/chunk-PFNXVBLU.js +96 -0
- package/dist/chunk-PFNXVBLU.js.map +1 -0
- package/dist/chunk-PUPZ4HME.js +165 -0
- package/dist/chunk-PUPZ4HME.js.map +1 -0
- package/dist/chunk-QFS52OK5.js +690 -0
- package/dist/chunk-QFS52OK5.js.map +1 -0
- package/dist/chunk-QNC6O3PG.js +45 -0
- package/dist/chunk-QNC6O3PG.js.map +1 -0
- package/dist/chunk-QUHOXWBK.js +82 -0
- package/dist/chunk-QUHOXWBK.js.map +1 -0
- package/dist/chunk-UIQGSTBJ.js +106 -0
- package/dist/chunk-UIQGSTBJ.js.map +1 -0
- package/dist/chunk-UJQKVP6V.js +193 -0
- package/dist/chunk-UJQKVP6V.js.map +1 -0
- package/dist/chunk-VVPGEAC6.js +11 -0
- package/dist/chunk-VVPGEAC6.js.map +1 -0
- package/dist/chunk-XA3T5KWA.js +58 -0
- package/dist/chunk-XA3T5KWA.js.map +1 -0
- package/dist/chunk-YSHJHSJM.js +19 -0
- package/dist/chunk-YSHJHSJM.js.map +1 -0
- package/dist/chunk-YVHOAVSM.js +182 -0
- package/dist/chunk-YVHOAVSM.js.map +1 -0
- package/dist/collapse.d.ts +16 -0
- package/dist/collapse.js +3 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +11 -0
- package/dist/count-badge.js +4 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +39 -0
- package/dist/date-field.js +8 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-range-field.d.ts +30 -0
- package/dist/date-range-field.js +8 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +28 -0
- package/dist/datetime-field.js +10 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.d.ts +26 -0
- package/dist/dialog.js +7 -0
- package/dist/dialog.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/motion-tokens.d.ts +29 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +25 -0
- package/dist/multi-select.js +7 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +24 -0
- package/dist/number-field.js +4 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +20 -0
- package/dist/otp-field.js +3 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/overlay.js +4 -0
- package/dist/overlay.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.js +5 -0
- package/dist/pagination.js.map +1 -0
- package/dist/radio-group.d.ts +46 -0
- package/dist/radio-group.js +6 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select-core-SAyS-8w0.d.ts +16 -0
- package/dist/select.d.ts +27 -0
- package/dist/select.js +7 -0
- package/dist/select.js.map +1 -0
- package/dist/status-badge.d.ts +17 -0
- package/dist/status-badge.js +5 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.d.ts +65 -0
- package/dist/table.js +5 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.d.ts +44 -0
- package/dist/tabs.js +5 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.d.ts +28 -0
- package/dist/tag.js +5 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +30 -0
- package/dist/text-field.js +6 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.d.ts +33 -0
- package/dist/textarea.js +5 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +27 -0
- package/dist/time-field.js +6 -0
- package/dist/time-field.js.map +1 -0
- package/dist/toast-store.d.ts +75 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.d.ts +3 -0
- package/dist/toast.js +6 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +24 -0
- package/dist/toggle-tag.js +4 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.d.ts +21 -0
- package/dist/toggle.js +3 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.d.ts +27 -0
- package/dist/tooltip.js +4 -0
- package/dist/tooltip.js.map +1 -0
- package/llms.txt +165 -0
- package/package.json +205 -0
- package/src/components/alert/Alert.tsx +118 -0
- package/src/components/alert/alert.css +136 -0
- package/src/components/avatar/Avatar.tsx +128 -0
- package/src/components/avatar/AvatarGroup.tsx +50 -0
- package/src/components/avatar/avatar.css +200 -0
- package/src/components/badge/Badge.tsx +66 -0
- package/src/components/badge/CountBadge.tsx +46 -0
- package/src/components/badge/StatusBadge.tsx +132 -0
- package/src/components/badge/badge.css +243 -0
- package/src/components/button/Button.tsx +68 -0
- package/src/components/button/button.css +222 -0
- package/src/components/checkbox/Checkbox.tsx +90 -0
- package/src/components/checkbox/checkbox.css +179 -0
- package/src/components/date-picker/DateField.tsx +362 -0
- package/src/components/date-picker/DateRangeField.tsx +533 -0
- package/src/components/date-picker/DateTimeField.tsx +177 -0
- package/src/components/date-picker/TimeField.tsx +100 -0
- package/src/components/date-picker/date-picker.css +591 -0
- package/src/components/date-picker/date-utils.ts +55 -0
- package/src/components/date-picker/field-shell.tsx +78 -0
- package/src/components/date-picker/glide-pill.tsx +81 -0
- package/src/components/date-picker/time-core.tsx +305 -0
- package/src/components/dialog/Dialog.tsx +181 -0
- package/src/components/dialog/dialog.css +170 -0
- package/src/components/glass/glass.css +100 -0
- package/src/components/icon/Icon.tsx +76 -0
- package/src/components/icon/IconSlot.tsx +11 -0
- package/src/components/icon/icon.css +33 -0
- package/src/components/input/NumberField.tsx +117 -0
- package/src/components/input/OtpField.tsx +118 -0
- package/src/components/input/TextField.tsx +123 -0
- package/src/components/input/input.css +335 -0
- package/src/components/motion/Collapse.tsx +33 -0
- package/src/components/motion/collapse.css +41 -0
- package/src/components/overlay/Overlay.tsx +239 -0
- package/src/components/overlay/overlay-core.tsx +565 -0
- package/src/components/overlay/overlay.css +119 -0
- package/src/components/overlay/sheet-drag.tsx +146 -0
- package/src/components/pagination/Pagination.tsx +140 -0
- package/src/components/pagination/pagination.css +48 -0
- package/src/components/radio-group/RadioGroup.tsx +182 -0
- package/src/components/radio-group/radio-group.css +277 -0
- package/src/components/select/MultiSelect.tsx +251 -0
- package/src/components/select/Select.tsx +235 -0
- package/src/components/select/select-core.tsx +417 -0
- package/src/components/select/select.css +386 -0
- package/src/components/table/Table.tsx +433 -0
- package/src/components/table/table.css +348 -0
- package/src/components/tabs/Tabs.tsx +371 -0
- package/src/components/tabs/tabs.css +228 -0
- package/src/components/tag/Tag.tsx +145 -0
- package/src/components/tag/ToggleTag.tsx +125 -0
- package/src/components/tag/tag.css +248 -0
- package/src/components/textarea/Textarea.tsx +197 -0
- package/src/components/textarea/textarea.css +219 -0
- package/src/components/toast/Toast.tsx +349 -0
- package/src/components/toast/toast-store.ts +266 -0
- package/src/components/toast/toast.css +233 -0
- package/src/components/toggle/Toggle.tsx +94 -0
- package/src/components/toggle/toggle.css +152 -0
- package/src/components/tooltip/Tooltip.tsx +365 -0
- package/src/components/tooltip/tooltip.css +86 -0
- package/src/index.ts +42 -0
- package/src/styles.css +39 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +56 -0
- package/src/tokens/elevation.css +20 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +21 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-tokens.ts +72 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +75 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +54 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/* Table - opinionated data table: columns + rows in; owns sort, selection, sticky header, pinned column, motion. */
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { motion } from 'motion/react';
|
|
7
|
+
import { UIMotion } from '../../tokens/motion-tokens';
|
|
8
|
+
import { Icon } from '../icon/Icon';
|
|
9
|
+
|
|
10
|
+
const { useState, useRef, useEffect, useMemo } = React;
|
|
11
|
+
|
|
12
|
+
export type TableAlign = 'start' | 'end' | 'center';
|
|
13
|
+
export type TableHideBelow = 'sm' | 'md';
|
|
14
|
+
|
|
15
|
+
export interface TableColumn<Row = any> {
|
|
16
|
+
/** Unique column id; also the default data accessor (row[key]). */
|
|
17
|
+
key: string;
|
|
18
|
+
label?: React.ReactNode;
|
|
19
|
+
/** Cell + header alignment. Default 'start'. */
|
|
20
|
+
align?: TableAlign;
|
|
21
|
+
/** Mono + tabular numerals (timestamps, counts, IDs - section E). */
|
|
22
|
+
mono?: boolean;
|
|
23
|
+
/** Identity emphasis: strong text, medium weight. */
|
|
24
|
+
strong?: boolean;
|
|
25
|
+
/** The one greedy column - absorbs slack width. Others size to content. */
|
|
26
|
+
grow?: boolean;
|
|
27
|
+
/** Header becomes a sort control. Sort is local; asc - desc, no third state. */
|
|
28
|
+
sortable?: boolean;
|
|
29
|
+
/** Sort accessor: a row property name, or a function. Defaults to row[key]. */
|
|
30
|
+
sortBy?: string | ((row: Row) => unknown);
|
|
31
|
+
/** Collapse this column below a container breakpoint (30rem / 42rem). */
|
|
32
|
+
hideBelow?: TableHideBelow;
|
|
33
|
+
/** Custom cell renderer. Defaults to row[key]. */
|
|
34
|
+
render?: (row: Row) => React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TableSort {
|
|
38
|
+
key: string;
|
|
39
|
+
dir: 'asc' | 'desc';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TableProps<Row = any> {
|
|
43
|
+
columns: TableColumn<Row>[];
|
|
44
|
+
rows: Row[];
|
|
45
|
+
/** Row identity property. Default 'id'. Must be unique and stable. */
|
|
46
|
+
rowKey?: string;
|
|
47
|
+
/** aria-label for the <table>. */
|
|
48
|
+
label?: string;
|
|
49
|
+
|
|
50
|
+
/** Checkbox column + bulk bar. Selection is a Set of row keys, owned here. */
|
|
51
|
+
selectable?: boolean;
|
|
52
|
+
onSelectionChange?: (keys: Array<string | number>) => void;
|
|
53
|
+
/** Rendered in the bulk bar between the count and the built-in Clear. */
|
|
54
|
+
bulkActions?: (keys: Array<string | number>, clear: () => void) => React.ReactNode;
|
|
55
|
+
/** Per-row checkbox aria-label. Default 'Select row'. */
|
|
56
|
+
selectionLabel?: (row: Row) => string;
|
|
57
|
+
|
|
58
|
+
/** Initial sort; sorting stays local and uncontrolled after that. */
|
|
59
|
+
defaultSort?: TableSort | null;
|
|
60
|
+
/** Notification only - fires after the local sort updates. */
|
|
61
|
+
onSortChange?: (sort: TableSort) => void;
|
|
62
|
+
|
|
63
|
+
/** Row rhythm. Default 'cozy' (46px rows); 'compact' is 38px. */
|
|
64
|
+
density?: 'cozy' | 'compact';
|
|
65
|
+
/** Pin checkbox + first column under horizontal overflow. Default true. */
|
|
66
|
+
pinFirst?: boolean;
|
|
67
|
+
|
|
68
|
+
/** Rows recede and go inert (list-fetch convention). */
|
|
69
|
+
loading?: boolean;
|
|
70
|
+
/** Shown when rows is empty and not loading. Default 'Nothing to show'. */
|
|
71
|
+
empty?: React.ReactNode;
|
|
72
|
+
/** Footer strip - pagination, summaries. */
|
|
73
|
+
footer?: React.ReactNode;
|
|
74
|
+
|
|
75
|
+
/** Makes rows clickable (checkbox cell excluded). */
|
|
76
|
+
onRowClick?: (row: Row) => void;
|
|
77
|
+
/** Size the table here - the internal scroller absorbs the constraint. */
|
|
78
|
+
className?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* numbers compare numerically, else natural-order text; null/undefined sink to the end */
|
|
82
|
+
function tblCompare(a: unknown, b: unknown) {
|
|
83
|
+
if (a == null && b == null) return 0;
|
|
84
|
+
if (a == null) return 1;
|
|
85
|
+
if (b == null) return -1;
|
|
86
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
87
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface TblCheckboxProps {
|
|
91
|
+
checked?: boolean;
|
|
92
|
+
indeterminate?: boolean;
|
|
93
|
+
ariaLabel?: string;
|
|
94
|
+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
95
|
+
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* the Checkbox primitive's exact DOM minus label - its CSS state machine paints it */
|
|
99
|
+
function TblCheckbox({ checked, indeterminate, ariaLabel, onChange, onClick }: TblCheckboxProps) {
|
|
100
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (ref.current) ref.current.indeterminate = !!indeterminate;
|
|
103
|
+
}, [indeterminate, checked]);
|
|
104
|
+
return (
|
|
105
|
+
<label className="cbx tbl__cbx">
|
|
106
|
+
<input
|
|
107
|
+
ref={ref}
|
|
108
|
+
type="checkbox"
|
|
109
|
+
className="cbx__input"
|
|
110
|
+
checked={checked}
|
|
111
|
+
aria-label={ariaLabel}
|
|
112
|
+
onChange={onChange}
|
|
113
|
+
onClick={onClick}
|
|
114
|
+
/>
|
|
115
|
+
<span className="cbx__box" aria-hidden="true">
|
|
116
|
+
<svg className="cbx__mark" viewBox="0 0 16 16" fill="none">
|
|
117
|
+
<path className="cbx__tick" d="M3.5 8.5 L6.75 11.5 L12.5 4.75"></path>
|
|
118
|
+
</svg>
|
|
119
|
+
<span className="cbx__dash"></span>
|
|
120
|
+
</span>
|
|
121
|
+
</label>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* count odometer - reuses badge.css's .odo vocabulary: each digit clips a 0-9 strip */
|
|
126
|
+
const TBL_DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
127
|
+
function TblOdo({ value }: { value: number }) {
|
|
128
|
+
const str = String(value);
|
|
129
|
+
return (
|
|
130
|
+
<span className="odo" role="text" aria-label={str}>
|
|
131
|
+
{str.split('').map((ch, i) =>
|
|
132
|
+
/\d/.test(ch) ? (
|
|
133
|
+
<span key={i} className="odo__col" aria-hidden="true">
|
|
134
|
+
<span className={'odo__strip odo__strip--' + ch}>
|
|
135
|
+
{TBL_DIGITS.map((d) => (
|
|
136
|
+
<span key={d}>{d}</span>
|
|
137
|
+
))}
|
|
138
|
+
</span>
|
|
139
|
+
</span>
|
|
140
|
+
) : (
|
|
141
|
+
<span key={i} className="odo__fixed" aria-hidden="true">
|
|
142
|
+
{ch}
|
|
143
|
+
</span>
|
|
144
|
+
),
|
|
145
|
+
)}
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function Table<Row = any>({
|
|
151
|
+
columns = [],
|
|
152
|
+
rows = [],
|
|
153
|
+
rowKey = 'id',
|
|
154
|
+
label,
|
|
155
|
+
selectable = false,
|
|
156
|
+
onSelectionChange,
|
|
157
|
+
bulkActions,
|
|
158
|
+
selectionLabel,
|
|
159
|
+
defaultSort = null,
|
|
160
|
+
onSortChange,
|
|
161
|
+
density = 'cozy',
|
|
162
|
+
pinFirst = true,
|
|
163
|
+
loading = false,
|
|
164
|
+
empty,
|
|
165
|
+
footer,
|
|
166
|
+
onRowClick,
|
|
167
|
+
className = '',
|
|
168
|
+
}: TableProps<Row>) {
|
|
169
|
+
const [sort, setSort] = useState<TableSort | null>(defaultSort);
|
|
170
|
+
const [selected, setSelected] = useState<Set<string | number>>(() => new Set());
|
|
171
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
172
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
173
|
+
const lastIdxRef = useRef(-1); // anchor for shift-click ranges
|
|
174
|
+
const shiftRef = useRef(false); // shift held on the click that fired change
|
|
175
|
+
const lastCountRef = useRef(0); // freezes the bulk count during its exit fade
|
|
176
|
+
|
|
177
|
+
const keyOf = (row: Row): string | number => (row as Record<string, string | number>)[rowKey];
|
|
178
|
+
|
|
179
|
+
const sortedRows = useMemo(() => {
|
|
180
|
+
if (!sort) return rows;
|
|
181
|
+
const col = columns.find((c) => c.key === sort.key);
|
|
182
|
+
if (!col) return rows;
|
|
183
|
+
const get =
|
|
184
|
+
typeof col.sortBy === 'function'
|
|
185
|
+
? col.sortBy
|
|
186
|
+
: col.sortBy
|
|
187
|
+
? (r: Row) => (r as Record<string, unknown>)[col.sortBy as string]
|
|
188
|
+
: (r: Row) => (r as Record<string, unknown>)[col.key];
|
|
189
|
+
const dir = sort.dir === 'desc' ? -1 : 1;
|
|
190
|
+
return rows.slice().sort((a, b) => dir * tblCompare(get(a), get(b)));
|
|
191
|
+
}, [rows, columns, sort]);
|
|
192
|
+
|
|
193
|
+
function cycleSort(col: TableColumn<Row>) {
|
|
194
|
+
const next: TableSort =
|
|
195
|
+
sort && sort.key === col.key
|
|
196
|
+
? { key: col.key, dir: sort.dir === 'asc' ? 'desc' : 'asc' }
|
|
197
|
+
: { key: col.key, dir: 'asc' };
|
|
198
|
+
setSort(next);
|
|
199
|
+
if (onSortChange) onSortChange(next);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const allSelected = sortedRows.length > 0 && sortedRows.every((r) => selected.has(keyOf(r)));
|
|
203
|
+
const someSelected = selected.size > 0 && !allSelected;
|
|
204
|
+
|
|
205
|
+
function commitSelection(next: Set<string | number>) {
|
|
206
|
+
setSelected(next);
|
|
207
|
+
if (onSelectionChange) onSelectionChange(Array.from(next));
|
|
208
|
+
}
|
|
209
|
+
function clearSelection() {
|
|
210
|
+
lastIdxRef.current = -1;
|
|
211
|
+
commitSelection(new Set());
|
|
212
|
+
}
|
|
213
|
+
function toggleAll() {
|
|
214
|
+
const next = new Set<string | number>();
|
|
215
|
+
if (!allSelected) sortedRows.forEach((r) => next.add(keyOf(r)));
|
|
216
|
+
commitSelection(next);
|
|
217
|
+
}
|
|
218
|
+
function toggleRow(idx: number) {
|
|
219
|
+
const next = new Set(selected);
|
|
220
|
+
const on = !next.has(keyOf(sortedRows[idx]));
|
|
221
|
+
if (shiftRef.current && lastIdxRef.current >= 0 && lastIdxRef.current !== idx) {
|
|
222
|
+
const lo = Math.min(lastIdxRef.current, idx);
|
|
223
|
+
const hi = Math.max(lastIdxRef.current, idx);
|
|
224
|
+
for (let i = lo; i <= hi; i++) {
|
|
225
|
+
const k = keyOf(sortedRows[i]);
|
|
226
|
+
if (on) next.add(k);
|
|
227
|
+
else next.delete(k);
|
|
228
|
+
}
|
|
229
|
+
} else if (on) {
|
|
230
|
+
next.add(keyOf(sortedRows[idx]));
|
|
231
|
+
} else {
|
|
232
|
+
next.delete(keyOf(sortedRows[idx]));
|
|
233
|
+
}
|
|
234
|
+
lastIdxRef.current = idx;
|
|
235
|
+
shiftRef.current = false;
|
|
236
|
+
commitSelection(next);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* live scroll - data attrs that CSS reads for header elevation, pin cast, edge fades */
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const el = scrollRef.current;
|
|
242
|
+
const w = wrapRef.current;
|
|
243
|
+
if (!el || !w) return;
|
|
244
|
+
const set = (attr: string, on: boolean) => {
|
|
245
|
+
if (on) w.setAttribute(attr, '');
|
|
246
|
+
else w.removeAttribute(attr);
|
|
247
|
+
};
|
|
248
|
+
const sync = () => {
|
|
249
|
+
set('data-scrolled', el.scrollTop > 0);
|
|
250
|
+
set('data-x-back', el.scrollLeft > 1);
|
|
251
|
+
set('data-x-more', el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
|
|
252
|
+
};
|
|
253
|
+
sync();
|
|
254
|
+
el.addEventListener('scroll', sync, { passive: true });
|
|
255
|
+
const ro = new ResizeObserver(sync);
|
|
256
|
+
ro.observe(el);
|
|
257
|
+
if (el.firstElementChild) ro.observe(el.firstElementChild);
|
|
258
|
+
return () => {
|
|
259
|
+
el.removeEventListener('scroll', sync);
|
|
260
|
+
ro.disconnect();
|
|
261
|
+
};
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
function cellMods(col: TableColumn<Row>, i: number) {
|
|
265
|
+
const a: string[] = [];
|
|
266
|
+
if (col.align === 'end') a.push('tbl__cell--end');
|
|
267
|
+
if (col.align === 'center') a.push('tbl__cell--center');
|
|
268
|
+
if (col.grow) a.push('tbl__cell--grow');
|
|
269
|
+
if (col.hideBelow) a.push('tbl__cell--hide-' + col.hideBelow);
|
|
270
|
+
if (pinFirst && i === 0) {
|
|
271
|
+
a.push('tbl__cell--pin', 'tbl__cell--pinEdge');
|
|
272
|
+
if (selectable) a.push('tbl__cell--pinOff');
|
|
273
|
+
}
|
|
274
|
+
return a;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const t = UIMotion.t;
|
|
278
|
+
const colCount = columns.length + (selectable ? 1 : 0);
|
|
279
|
+
const showEmpty = !loading && sortedRows.length === 0;
|
|
280
|
+
const checkCellCls = 'tbl__cell--check' + (pinFirst ? ' tbl__cell--pin' : '');
|
|
281
|
+
|
|
282
|
+
/* one persistent node, CSS open/close - AnimatePresence ghosted here; count frozen so exit doesn't roll to 0 */
|
|
283
|
+
const bulkOpen = selectable && selected.size > 0;
|
|
284
|
+
if (selected.size > 0) lastCountRef.current = selected.size;
|
|
285
|
+
|
|
286
|
+
const rootCls = [
|
|
287
|
+
'tbl',
|
|
288
|
+
density === 'compact' ? 'tbl--compact' : '',
|
|
289
|
+
pinFirst ? 'tbl--pin' : '',
|
|
290
|
+
loading ? 'tbl--loading' : '',
|
|
291
|
+
onRowClick ? 'tbl--clickable' : '',
|
|
292
|
+
className,
|
|
293
|
+
]
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.join(' ');
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div ref={wrapRef} className={rootCls}>
|
|
299
|
+
{selectable ? (
|
|
300
|
+
<div
|
|
301
|
+
className="tbl__bulk"
|
|
302
|
+
data-open={bulkOpen ? '' : undefined}
|
|
303
|
+
aria-hidden={bulkOpen ? undefined : true}
|
|
304
|
+
role="toolbar"
|
|
305
|
+
aria-label="Selection actions"
|
|
306
|
+
>
|
|
307
|
+
<TblCheckbox
|
|
308
|
+
checked={allSelected}
|
|
309
|
+
indeterminate={someSelected}
|
|
310
|
+
ariaLabel="Select all rows"
|
|
311
|
+
onChange={toggleAll}
|
|
312
|
+
/>
|
|
313
|
+
<span className="tbl__bulkCount" aria-live="polite" aria-atomic="true">
|
|
314
|
+
<TblOdo value={bulkOpen ? selected.size : lastCountRef.current} />
|
|
315
|
+
<span> selected</span>
|
|
316
|
+
</span>
|
|
317
|
+
<span className="tbl__bulkSpacer"></span>
|
|
318
|
+
{bulkActions ? bulkActions(Array.from(selected), clearSelection) : null}
|
|
319
|
+
<button type="button" className="btn btn--ghost btn--sm" onClick={clearSelection}>
|
|
320
|
+
Clear
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
) : null}
|
|
324
|
+
|
|
325
|
+
<div className="tbl__viewport">
|
|
326
|
+
<div className="tbl__scroll" ref={scrollRef}>
|
|
327
|
+
<table className="tbl__table" aria-label={label} aria-busy={loading || undefined}>
|
|
328
|
+
<thead>
|
|
329
|
+
<tr>
|
|
330
|
+
{selectable ? (
|
|
331
|
+
<th scope="col" className={checkCellCls}>
|
|
332
|
+
<TblCheckbox
|
|
333
|
+
checked={allSelected}
|
|
334
|
+
indeterminate={someSelected}
|
|
335
|
+
ariaLabel="Select all rows"
|
|
336
|
+
onChange={toggleAll}
|
|
337
|
+
/>
|
|
338
|
+
</th>
|
|
339
|
+
) : null}
|
|
340
|
+
{columns.map((c, i) => {
|
|
341
|
+
const active = sort && sort.key === c.key;
|
|
342
|
+
const cls = (c.sortable ? ['tbl__cell--sortable'] : []).concat(cellMods(c, i));
|
|
343
|
+
return (
|
|
344
|
+
<th
|
|
345
|
+
key={c.key}
|
|
346
|
+
scope="col"
|
|
347
|
+
className={cls.join(' ')}
|
|
348
|
+
aria-sort={
|
|
349
|
+
active ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined
|
|
350
|
+
}
|
|
351
|
+
>
|
|
352
|
+
{c.sortable ? (
|
|
353
|
+
<button type="button" className="tbl__sort" onClick={() => cycleSort(c)}>
|
|
354
|
+
<span className="tbl__sortLbl">{c.label}</span>
|
|
355
|
+
<span
|
|
356
|
+
className={
|
|
357
|
+
'tbl__sortIcon' +
|
|
358
|
+
(active ? ' is-on' : '') +
|
|
359
|
+
(active && sort.dir === 'desc' ? ' is-desc' : '')
|
|
360
|
+
}
|
|
361
|
+
aria-hidden="true"
|
|
362
|
+
>
|
|
363
|
+
<Icon name="arrow-up" size="sm" />
|
|
364
|
+
</span>
|
|
365
|
+
</button>
|
|
366
|
+
) : c.label != null ? (
|
|
367
|
+
<span className="tbl__hLbl">{c.label}</span>
|
|
368
|
+
) : null}
|
|
369
|
+
</th>
|
|
370
|
+
);
|
|
371
|
+
})}
|
|
372
|
+
</tr>
|
|
373
|
+
</thead>
|
|
374
|
+
<tbody>
|
|
375
|
+
{showEmpty ? (
|
|
376
|
+
<tr>
|
|
377
|
+
<td colSpan={colCount} className="tbl__emptyCell">
|
|
378
|
+
{empty != null ? empty : 'Nothing to show'}
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
) : (
|
|
382
|
+
sortedRows.map((row, idx) => {
|
|
383
|
+
const k = keyOf(row);
|
|
384
|
+
const isSel = selected.has(k);
|
|
385
|
+
return (
|
|
386
|
+
<motion.tr
|
|
387
|
+
key={k}
|
|
388
|
+
layout="position"
|
|
389
|
+
transition={t.layout}
|
|
390
|
+
className="tbl__row"
|
|
391
|
+
data-selected={isSel ? '' : undefined}
|
|
392
|
+
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
393
|
+
>
|
|
394
|
+
{selectable ? (
|
|
395
|
+
<td className={checkCellCls} onClick={(e) => e.stopPropagation()}>
|
|
396
|
+
<TblCheckbox
|
|
397
|
+
checked={isSel}
|
|
398
|
+
ariaLabel={selectionLabel ? selectionLabel(row) : 'Select row'}
|
|
399
|
+
onClick={(e) => {
|
|
400
|
+
shiftRef.current = e.shiftKey;
|
|
401
|
+
}}
|
|
402
|
+
onChange={() => toggleRow(idx)}
|
|
403
|
+
/>
|
|
404
|
+
</td>
|
|
405
|
+
) : null}
|
|
406
|
+
{columns.map((c, i) => (
|
|
407
|
+
<td
|
|
408
|
+
key={c.key}
|
|
409
|
+
className={cellMods(c, i)
|
|
410
|
+
.concat(c.mono ? ['tbl__cell--mono'] : [])
|
|
411
|
+
.concat(c.strong ? ['tbl__cell--strong'] : [])
|
|
412
|
+
.join(' ')}
|
|
413
|
+
>
|
|
414
|
+
{c.render
|
|
415
|
+
? c.render(row)
|
|
416
|
+
: (row as Record<string, React.ReactNode>)[c.key]}
|
|
417
|
+
</td>
|
|
418
|
+
))}
|
|
419
|
+
</motion.tr>
|
|
420
|
+
);
|
|
421
|
+
})
|
|
422
|
+
)}
|
|
423
|
+
</tbody>
|
|
424
|
+
</table>
|
|
425
|
+
</div>
|
|
426
|
+
<div className="tbl__edge tbl__edge--l" aria-hidden="true"></div>
|
|
427
|
+
<div className="tbl__edge tbl__edge--r" aria-hidden="true"></div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{footer ? <div className="tbl__foot">{footer}</div> : null}
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|