@zyncat/ui 0.8.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 +133 -0
- package/dist/alert.css +132 -0
- package/dist/alert.d.ts +46 -0
- package/dist/alert.js +80 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +24 -0
- package/dist/avatar-group.js +23 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.css +200 -0
- package/dist/avatar.d.ts +35 -0
- package/dist/avatar.js +67 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.css +182 -0
- package/dist/badge.d.ts +34 -0
- package/dist/badge.js +4 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.css +269 -0
- package/dist/button.d.ts +44 -0
- package/dist/button.js +4 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.css +177 -0
- package/dist/checkbox.d.ts +35 -0
- package/dist/checkbox.js +5 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2ERBBFZS.js +12 -0
- package/dist/chunk-2ERBBFZS.js.map +1 -0
- package/dist/chunk-2HORSLXJ.js +191 -0
- package/dist/chunk-2HORSLXJ.js.map +1 -0
- package/dist/chunk-36WVAQKL.js +40 -0
- package/dist/chunk-36WVAQKL.js.map +1 -0
- package/dist/chunk-3SBCBYB7.js +575 -0
- package/dist/chunk-3SBCBYB7.js.map +1 -0
- package/dist/chunk-4R2GQO43.js +125 -0
- package/dist/chunk-4R2GQO43.js.map +1 -0
- package/dist/chunk-64UU4LGU.js +258 -0
- package/dist/chunk-64UU4LGU.js.map +1 -0
- package/dist/chunk-7SZCJ7WI.js +35 -0
- package/dist/chunk-7SZCJ7WI.js.map +1 -0
- package/dist/chunk-BIDVNLJO.js +38 -0
- package/dist/chunk-BIDVNLJO.js.map +1 -0
- package/dist/chunk-CONSWNIV.js +17 -0
- package/dist/chunk-CONSWNIV.js.map +1 -0
- package/dist/chunk-EQ32F2QJ.js +75 -0
- package/dist/chunk-EQ32F2QJ.js.map +1 -0
- package/dist/chunk-GY7JOPUQ.js +111 -0
- package/dist/chunk-GY7JOPUQ.js.map +1 -0
- package/dist/chunk-I4XQ4PV2.js +30 -0
- package/dist/chunk-I4XQ4PV2.js.map +1 -0
- package/dist/chunk-JSKXZMLH.js +67 -0
- package/dist/chunk-JSKXZMLH.js.map +1 -0
- package/dist/chunk-L3Q3VDIW.js +295 -0
- package/dist/chunk-L3Q3VDIW.js.map +1 -0
- package/dist/chunk-LDC3RIYA.js +62 -0
- package/dist/chunk-LDC3RIYA.js.map +1 -0
- package/dist/chunk-MKIQFK7S.js +43 -0
- package/dist/chunk-MKIQFK7S.js.map +1 -0
- package/dist/chunk-MMRIQNEN.js +20 -0
- package/dist/chunk-MMRIQNEN.js.map +1 -0
- package/dist/chunk-MXOPCFWR.js +59 -0
- package/dist/chunk-MXOPCFWR.js.map +1 -0
- package/dist/chunk-NEF5XAHG.js +17 -0
- package/dist/chunk-NEF5XAHG.js.map +1 -0
- package/dist/chunk-NNXJCLTV.js +22 -0
- package/dist/chunk-NNXJCLTV.js.map +1 -0
- package/dist/chunk-O4N2OBTR.js +94 -0
- package/dist/chunk-O4N2OBTR.js.map +1 -0
- package/dist/chunk-PMECHR3A.js +38 -0
- package/dist/chunk-PMECHR3A.js.map +1 -0
- package/dist/chunk-QDK66S4G.js +44 -0
- package/dist/chunk-QDK66S4G.js.map +1 -0
- package/dist/chunk-QGNC7LH5.js +8 -0
- package/dist/chunk-QGNC7LH5.js.map +1 -0
- package/dist/chunk-QMSJZCK3.js +398 -0
- package/dist/chunk-QMSJZCK3.js.map +1 -0
- package/dist/chunk-S5ZTZANS.js +57 -0
- package/dist/chunk-S5ZTZANS.js.map +1 -0
- package/dist/chunk-S7B34VGN.js +14 -0
- package/dist/chunk-S7B34VGN.js.map +1 -0
- package/dist/chunk-TCUCYFT7.js +25 -0
- package/dist/chunk-TCUCYFT7.js.map +1 -0
- package/dist/chunk-TYHR4VNM.js +193 -0
- package/dist/chunk-TYHR4VNM.js.map +1 -0
- package/dist/chunk-U5QQZC3U.js +56 -0
- package/dist/chunk-U5QQZC3U.js.map +1 -0
- package/dist/chunk-UE63OOVG.js +13 -0
- package/dist/chunk-UE63OOVG.js.map +1 -0
- package/dist/chunk-VTJTGVZZ.js +22 -0
- package/dist/chunk-VTJTGVZZ.js.map +1 -0
- package/dist/chunk-W3SHPKSB.js +49 -0
- package/dist/chunk-W3SHPKSB.js.map +1 -0
- package/dist/chunk-XZAEIQDR.js +188 -0
- package/dist/chunk-XZAEIQDR.js.map +1 -0
- package/dist/chunk-YGGCQNNU.js +92 -0
- package/dist/chunk-YGGCQNNU.js.map +1 -0
- package/dist/collapse.css +88 -0
- package/dist/collapse.d.ts +46 -0
- package/dist/collapse.js +5 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +13 -0
- package/dist/count-badge.js +13 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +25 -0
- package/dist/date-field.js +87 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-picker.css +595 -0
- package/dist/date-range-field.d.ts +33 -0
- package/dist/date-range-field.js +404 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +29 -0
- package/dist/datetime-field.js +155 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.css +180 -0
- package/dist/dialog.d.ts +42 -0
- package/dist/dialog.js +128 -0
- package/dist/dialog.js.map +1 -0
- package/dist/dom-props-C0jrllBw.d.ts +5 -0
- package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
- package/dist/field-shell-Cuda-feL.d.ts +24 -0
- package/dist/glide.d.ts +15 -0
- package/dist/glide.js +5 -0
- package/dist/glide.js.map +1 -0
- package/dist/icon.css +33 -0
- package/dist/input.css +333 -0
- package/dist/mcp.js +359 -0
- package/dist/motion-devtools.css +265 -0
- package/dist/motion-devtools.d.ts +51 -0
- package/dist/motion-devtools.js +354 -0
- package/dist/motion-devtools.js.map +1 -0
- package/dist/motion-scale-Canq4stZ.d.ts +18 -0
- package/dist/motion-tokens.d.ts +28 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +44 -0
- package/dist/multi-select.js +109 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +36 -0
- package/dist/number-field.js +112 -0
- package/dist/number-field.js.map +1 -0
- package/dist/odometer.css +62 -0
- package/dist/otp-field.d.ts +31 -0
- package/dist/otp-field.js +98 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.css +23 -0
- package/dist/pagination.css +43 -0
- package/dist/pagination.d.ts +29 -0
- package/dist/pagination.js +107 -0
- package/dist/pagination.js.map +1 -0
- package/dist/popover.css +69 -0
- package/dist/popover.d.ts +35 -0
- package/dist/popover.js +15 -0
- package/dist/popover.js.map +1 -0
- package/dist/radio-group.css +300 -0
- package/dist/radio-group.d.ts +52 -0
- package/dist/radio-group.js +115 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select.css +416 -0
- package/dist/select.d.ts +49 -0
- package/dist/select.js +99 -0
- package/dist/select.js.map +1 -0
- package/dist/sheet.css +26 -0
- package/dist/sheet.d.ts +31 -0
- package/dist/sheet.js +14 -0
- package/dist/sheet.js.map +1 -0
- package/dist/status-badge.d.ts +19 -0
- package/dist/status-badge.js +72 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.css +348 -0
- package/dist/table.d.ts +75 -0
- package/dist/table.js +262 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.css +208 -0
- package/dist/tabs.d.ts +71 -0
- package/dist/tabs.js +181 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.css +248 -0
- package/dist/tag.d.ts +52 -0
- package/dist/tag.js +72 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +28 -0
- package/dist/text-field.js +69 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.css +219 -0
- package/dist/textarea.d.ts +38 -0
- package/dist/textarea.js +144 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +28 -0
- package/dist/time-field.js +66 -0
- package/dist/time-field.js.map +1 -0
- package/dist/timing-7JA_zQHP.d.ts +27 -0
- package/dist/toast-store.d.ts +86 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.css +257 -0
- package/dist/toast.d.ts +12 -0
- package/dist/toast.js +310 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +34 -0
- package/dist/toggle-tag.js +57 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.css +152 -0
- package/dist/toggle.d.ts +31 -0
- package/dist/toggle.js +75 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.css +86 -0
- package/dist/tooltip.d.ts +30 -0
- package/dist/tooltip.js +268 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/types-SGMShK7R.d.ts +22 -0
- package/llms.txt +196 -0
- package/next.d.mts +7 -0
- package/next.mjs +59 -0
- package/package.json +257 -0
- package/src/components/composites/alert/Alert.tsx +141 -0
- package/src/components/composites/alert/alert.css +132 -0
- package/src/components/composites/date-picker/DateField.tsx +80 -0
- package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
- package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
- package/src/components/composites/date-picker/TimeField.tsx +78 -0
- package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
- package/src/components/composites/date-picker/date-picker.css +595 -0
- package/src/components/composites/date-picker/date-utils.ts +56 -0
- package/src/components/composites/date-picker/field-shell.tsx +85 -0
- package/src/components/composites/date-picker/range-panel.tsx +363 -0
- package/src/components/composites/date-picker/time-core.tsx +290 -0
- package/src/components/composites/date-picker/use-day-focus.ts +35 -0
- package/src/components/composites/dialog/Dialog.tsx +198 -0
- package/src/components/composites/dialog/dialog.css +180 -0
- package/src/components/composites/pagination/Pagination.tsx +131 -0
- package/src/components/composites/pagination/pagination.css +43 -0
- package/src/components/composites/popover/Popover.tsx +141 -0
- package/src/components/composites/popover/popover.css +69 -0
- package/src/components/composites/select/MultiSelect.tsx +133 -0
- package/src/components/composites/select/Select.tsx +122 -0
- package/src/components/composites/select/core/index.ts +4 -0
- package/src/components/composites/select/core/menu.tsx +100 -0
- package/src/components/composites/select/core/panel.tsx +154 -0
- package/src/components/composites/select/core/trigger.tsx +74 -0
- package/src/components/composites/select/core/types.ts +39 -0
- package/src/components/composites/select/core/use-listbox.ts +182 -0
- package/src/components/composites/select/select.css +416 -0
- package/src/components/composites/sheet/Sheet.tsx +135 -0
- package/src/components/composites/sheet/sheet.css +26 -0
- package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
- package/src/components/composites/table/Table.tsx +377 -0
- package/src/components/composites/table/table.css +348 -0
- package/src/components/composites/tabs/Tabs.tsx +278 -0
- package/src/components/composites/tabs/tabs.css +208 -0
- package/src/components/composites/toast/Toast.tsx +380 -0
- package/src/components/composites/toast/toast-store.ts +295 -0
- package/src/components/composites/toast/toast.css +257 -0
- package/src/components/composites/tooltip/Tooltip.tsx +110 -0
- package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
- package/src/components/composites/tooltip/tooltip-store.ts +81 -0
- package/src/components/composites/tooltip/tooltip.css +86 -0
- package/src/components/dev/MotionDevtools.tsx +200 -0
- package/src/components/dev/motion-devtools.css +265 -0
- package/src/components/dev/slowmo-engine.ts +230 -0
- package/src/components/internal/glass/glass.css +100 -0
- package/src/components/internal/glass/glint.ts +9 -0
- package/src/components/internal/hooks/use-controllable.ts +21 -0
- package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
- package/src/components/internal/icon/Icon.tsx +66 -0
- package/src/components/internal/icon/IconSlot.tsx +10 -0
- package/src/components/internal/icon/icon.css +33 -0
- package/src/components/internal/overlay/focus.ts +68 -0
- package/src/components/internal/overlay/layer.tsx +149 -0
- package/src/components/internal/overlay/modal.tsx +141 -0
- package/src/components/internal/overlay/overlay.css +23 -0
- package/src/components/internal/overlay/position.ts +103 -0
- package/src/components/internal/utils/cx.ts +3 -0
- package/src/components/internal/utils/token-px.ts +8 -0
- package/src/components/primitives/avatar/Avatar.tsx +120 -0
- package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
- package/src/components/primitives/avatar/avatar.css +200 -0
- package/src/components/primitives/badge/Badge.tsx +75 -0
- package/src/components/primitives/badge/CountBadge.tsx +20 -0
- package/src/components/primitives/badge/StatusBadge.tsx +111 -0
- package/src/components/primitives/badge/badge.css +182 -0
- package/src/components/primitives/badge/odometer.css +62 -0
- package/src/components/primitives/badge/odometer.tsx +36 -0
- package/src/components/primitives/button/Button.tsx +102 -0
- package/src/components/primitives/button/button.css +269 -0
- package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
- package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
- package/src/components/primitives/checkbox/checkbox.css +177 -0
- package/src/components/primitives/collapse/Collapse.tsx +86 -0
- package/src/components/primitives/collapse/collapse.css +88 -0
- package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
- package/src/components/primitives/input/NumberField.tsx +139 -0
- package/src/components/primitives/input/OtpField.tsx +134 -0
- package/src/components/primitives/input/TextField.tsx +110 -0
- package/src/components/primitives/input/field-chrome.tsx +83 -0
- package/src/components/primitives/input/input.css +333 -0
- package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
- package/src/components/primitives/radio-group/radio-group.css +300 -0
- package/src/components/primitives/tag/Tag.tsx +143 -0
- package/src/components/primitives/tag/ToggleTag.tsx +97 -0
- package/src/components/primitives/tag/tag.css +248 -0
- package/src/components/primitives/textarea/Textarea.tsx +190 -0
- package/src/components/primitives/textarea/textarea.css +219 -0
- package/src/components/primitives/toggle/Toggle.tsx +105 -0
- package/src/components/primitives/toggle/toggle.css +152 -0
- package/src/css.d.ts +1 -0
- package/src/dom-props.ts +1 -0
- package/src/engine/animate.ts +141 -0
- package/src/engine/drag.ts +53 -0
- package/src/engine/ease.ts +155 -0
- package/src/engine/flip.ts +59 -0
- package/src/engine/index.ts +13 -0
- package/src/mcp/server.ts +420 -0
- package/src/motion/element.tsx +53 -0
- package/src/motion/flip.tsx +87 -0
- package/src/motion/glide.tsx +47 -0
- package/src/motion/motion-timing.ts +33 -0
- package/src/motion/presence-context.ts +18 -0
- package/src/motion/presence.tsx +91 -0
- package/src/motion/timing.ts +49 -0
- package/src/motion/use-motion.ts +64 -0
- package/src/styles.css +26 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +58 -0
- package/src/tokens/elevation.css +22 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +20 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-scale.ts +17 -0
- package/src/tokens/motion-tokens.ts +92 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +65 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +52 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import './table.css';
|
|
4
|
+
import { useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from 'react';
|
|
5
|
+
import { Motion } from '../../../motion/element';
|
|
6
|
+
import { UIMotion } from '../../../tokens/motion-tokens';
|
|
7
|
+
import { Icon } from '../../internal/icon/Icon';
|
|
8
|
+
import type { DataAttributes } from '../../../dom-props';
|
|
9
|
+
import { Button } from '../../primitives/button/Button';
|
|
10
|
+
import { Checkbox } from '../../primitives/checkbox/Checkbox';
|
|
11
|
+
import { Odometer } from '../../primitives/badge/odometer';
|
|
12
|
+
import { useScrollEdges } from '../../internal/hooks/use-scroll-edges';
|
|
13
|
+
import { cx } from '../../internal/utils/cx';
|
|
14
|
+
|
|
15
|
+
export type TableAlign = 'start' | 'end' | 'center';
|
|
16
|
+
export type TableHideBelow = 'sm' | 'md';
|
|
17
|
+
|
|
18
|
+
export interface TableColumn<Row = any> {
|
|
19
|
+
/** Unique column id; also the default data accessor (row[key]). */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Header cell content; omit for a header-less column (e.g. an actions column). */
|
|
22
|
+
label?: ReactNode;
|
|
23
|
+
/** Cell + header alignment. Default 'start'. */
|
|
24
|
+
align?: TableAlign;
|
|
25
|
+
/** Mono + tabular numerals (timestamps, counts, IDs - section E). */
|
|
26
|
+
mono?: boolean;
|
|
27
|
+
/** Identity emphasis: strong text, medium weight. */
|
|
28
|
+
strong?: boolean;
|
|
29
|
+
/** The one greedy column - absorbs slack width. Others size to content. */
|
|
30
|
+
grow?: boolean;
|
|
31
|
+
/** Header becomes a sort control. Sort is local; asc - desc, no third state. */
|
|
32
|
+
sortable?: boolean;
|
|
33
|
+
/** Sort accessor: a row property name, or a function. Defaults to row[key]. */
|
|
34
|
+
sortBy?: string | ((row: Row) => unknown);
|
|
35
|
+
/** Collapse this column below a container breakpoint (30rem / 42rem). */
|
|
36
|
+
hideBelow?: TableHideBelow;
|
|
37
|
+
/** Custom cell renderer. Defaults to row[key]. */
|
|
38
|
+
render?: (row: Row) => ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TableSort {
|
|
42
|
+
/** `key` of the column being sorted by. */
|
|
43
|
+
key: string;
|
|
44
|
+
/** Sort direction - `asc` or `desc`. */
|
|
45
|
+
dir: 'asc' | 'desc';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TableProps<Row = any> {
|
|
49
|
+
/** Column defs, rendered left to right. */
|
|
50
|
+
columns: TableColumn<Row>[];
|
|
51
|
+
/** The row data; sorted locally per the active sort before render. */
|
|
52
|
+
rows: Row[];
|
|
53
|
+
/** Row identity property. Default 'id'. Must be unique and stable. */
|
|
54
|
+
rowKey?: string;
|
|
55
|
+
/** Accessible name for the <table>. */
|
|
56
|
+
ariaLabel?: string;
|
|
57
|
+
|
|
58
|
+
/** Checkbox column + bulk bar. Selection is a Set of row keys, owned here. */
|
|
59
|
+
selectable?: boolean;
|
|
60
|
+
/** Fires with the selected row keys after every selection change. */
|
|
61
|
+
onSelectionChange?: (keys: Array<string | number>) => void;
|
|
62
|
+
/** Rendered in the bulk bar between the count and the built-in Clear. */
|
|
63
|
+
bulkActions?: (keys: Array<string | number>, clear: () => void) => ReactNode;
|
|
64
|
+
/** Per-row checkbox aria-label. Default 'Select row'. */
|
|
65
|
+
selectionLabel?: (row: Row) => string;
|
|
66
|
+
|
|
67
|
+
/** Initial sort; sorting stays local and uncontrolled after that. */
|
|
68
|
+
defaultSort?: TableSort | null;
|
|
69
|
+
/** Notification only - fires after the local sort updates. */
|
|
70
|
+
onSortChange?: (sort: TableSort) => void;
|
|
71
|
+
|
|
72
|
+
/** Row rhythm. Default 'cozy' (46px rows); 'compact' is 38px. */
|
|
73
|
+
density?: 'cozy' | 'compact';
|
|
74
|
+
/** Pin checkbox + first column under horizontal overflow. Default true. */
|
|
75
|
+
pinFirst?: boolean;
|
|
76
|
+
|
|
77
|
+
/** Rows recede and go inert (list-fetch convention). */
|
|
78
|
+
loading?: boolean;
|
|
79
|
+
/** Shown when rows is empty and not loading. Default 'Nothing to show'. */
|
|
80
|
+
empty?: ReactNode;
|
|
81
|
+
/** Footer strip - pagination, summaries. */
|
|
82
|
+
footer?: ReactNode;
|
|
83
|
+
|
|
84
|
+
/** Makes rows clickable (checkbox cell excluded). */
|
|
85
|
+
onRowClick?: (row: Row) => void;
|
|
86
|
+
/** Size the table here - the internal scroller absorbs the constraint. */
|
|
87
|
+
className?: string;
|
|
88
|
+
/** Standard <div> attributes (style, data-*, aria-*, ...) forwarded to the table wrapper. */
|
|
89
|
+
htmlProps?: Omit<HTMLAttributes<HTMLDivElement>, 'className'> & DataAttributes;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tblCompare(a: unknown, b: unknown) {
|
|
93
|
+
if (a == null && b == null) return 0;
|
|
94
|
+
if (a == null) return 1;
|
|
95
|
+
if (b == null) return -1;
|
|
96
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
97
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function Table<Row = any>({
|
|
101
|
+
columns = [],
|
|
102
|
+
rows = [],
|
|
103
|
+
rowKey = 'id',
|
|
104
|
+
ariaLabel,
|
|
105
|
+
selectable = false,
|
|
106
|
+
onSelectionChange,
|
|
107
|
+
bulkActions,
|
|
108
|
+
selectionLabel,
|
|
109
|
+
defaultSort = null,
|
|
110
|
+
onSortChange,
|
|
111
|
+
density = 'cozy',
|
|
112
|
+
pinFirst = true,
|
|
113
|
+
loading = false,
|
|
114
|
+
empty,
|
|
115
|
+
footer,
|
|
116
|
+
onRowClick,
|
|
117
|
+
className = '',
|
|
118
|
+
htmlProps,
|
|
119
|
+
}: TableProps<Row>) {
|
|
120
|
+
const [sort, setSort] = useState<TableSort | null>(defaultSort);
|
|
121
|
+
const [selected, setSelected] = useState<Set<string | number>>(() => new Set());
|
|
122
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
123
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
124
|
+
const lastIdxRef = useRef(-1);
|
|
125
|
+
const shiftRef = useRef(false);
|
|
126
|
+
const lastCountRef = useRef(0);
|
|
127
|
+
|
|
128
|
+
const keyOf = (row: Row): string | number => (row as Record<string, string | number>)[rowKey];
|
|
129
|
+
|
|
130
|
+
const sortedRows = useMemo(() => {
|
|
131
|
+
if (!sort) return rows;
|
|
132
|
+
const col = columns.find((c) => c.key === sort.key);
|
|
133
|
+
if (!col) return rows;
|
|
134
|
+
const get =
|
|
135
|
+
typeof col.sortBy === 'function'
|
|
136
|
+
? col.sortBy
|
|
137
|
+
: col.sortBy
|
|
138
|
+
? (r: Row) => (r as Record<string, unknown>)[col.sortBy as string]
|
|
139
|
+
: (r: Row) => (r as Record<string, unknown>)[col.key];
|
|
140
|
+
const dir = sort.dir === 'desc' ? -1 : 1;
|
|
141
|
+
return rows.slice().sort((a, b) => dir * tblCompare(get(a), get(b)));
|
|
142
|
+
}, [rows, columns, sort]);
|
|
143
|
+
|
|
144
|
+
function cycleSort(col: TableColumn<Row>) {
|
|
145
|
+
const next: TableSort =
|
|
146
|
+
sort && sort.key === col.key
|
|
147
|
+
? { key: col.key, dir: sort.dir === 'asc' ? 'desc' : 'asc' }
|
|
148
|
+
: { key: col.key, dir: 'asc' };
|
|
149
|
+
setSort(next);
|
|
150
|
+
if (onSortChange) onSortChange(next);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const allSelected = sortedRows.length > 0 && sortedRows.every((r) => selected.has(keyOf(r)));
|
|
154
|
+
const someSelected = selected.size > 0 && !allSelected;
|
|
155
|
+
|
|
156
|
+
function commitSelection(next: Set<string | number>) {
|
|
157
|
+
setSelected(next);
|
|
158
|
+
if (onSelectionChange) onSelectionChange(Array.from(next));
|
|
159
|
+
}
|
|
160
|
+
function clearSelection() {
|
|
161
|
+
lastIdxRef.current = -1;
|
|
162
|
+
commitSelection(new Set());
|
|
163
|
+
}
|
|
164
|
+
function toggleAll() {
|
|
165
|
+
const next = new Set<string | number>();
|
|
166
|
+
if (!allSelected) sortedRows.forEach((r) => next.add(keyOf(r)));
|
|
167
|
+
commitSelection(next);
|
|
168
|
+
}
|
|
169
|
+
function toggleRow(idx: number) {
|
|
170
|
+
const next = new Set(selected);
|
|
171
|
+
const on = !next.has(keyOf(sortedRows[idx]));
|
|
172
|
+
if (shiftRef.current && lastIdxRef.current >= 0 && lastIdxRef.current !== idx) {
|
|
173
|
+
const lo = Math.min(lastIdxRef.current, idx);
|
|
174
|
+
const hi = Math.max(lastIdxRef.current, idx);
|
|
175
|
+
for (let i = lo; i <= hi; i++) {
|
|
176
|
+
const k = keyOf(sortedRows[i]);
|
|
177
|
+
if (on) next.add(k);
|
|
178
|
+
else next.delete(k);
|
|
179
|
+
}
|
|
180
|
+
} else if (on) {
|
|
181
|
+
next.add(keyOf(sortedRows[idx]));
|
|
182
|
+
} else {
|
|
183
|
+
next.delete(keyOf(sortedRows[idx]));
|
|
184
|
+
}
|
|
185
|
+
lastIdxRef.current = idx;
|
|
186
|
+
shiftRef.current = false;
|
|
187
|
+
commitSelection(next);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
useScrollEdges(
|
|
191
|
+
scrollRef,
|
|
192
|
+
(edges, el) => {
|
|
193
|
+
const w = wrapRef.current;
|
|
194
|
+
if (!w) return;
|
|
195
|
+
const set = (attr: string, on: boolean) => {
|
|
196
|
+
if (on) w.setAttribute(attr, '');
|
|
197
|
+
else w.removeAttribute(attr);
|
|
198
|
+
};
|
|
199
|
+
set('data-scrolled', el.scrollTop > 0);
|
|
200
|
+
set('data-x-back', edges.left);
|
|
201
|
+
set('data-x-more', edges.right);
|
|
202
|
+
},
|
|
203
|
+
{ content: true },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
function cellMods(col: TableColumn<Row>, i: number) {
|
|
207
|
+
const a: string[] = [];
|
|
208
|
+
if (col.align === 'end') a.push('tbl__cell--end');
|
|
209
|
+
if (col.align === 'center') a.push('tbl__cell--center');
|
|
210
|
+
if (col.grow) a.push('tbl__cell--grow');
|
|
211
|
+
if (col.hideBelow) a.push('tbl__cell--hide-' + col.hideBelow);
|
|
212
|
+
if (pinFirst && i === 0) {
|
|
213
|
+
a.push('tbl__cell--pin', 'tbl__cell--pinEdge');
|
|
214
|
+
if (selectable) a.push('tbl__cell--pinOff');
|
|
215
|
+
}
|
|
216
|
+
return a;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const t = UIMotion.t;
|
|
220
|
+
const colCount = columns.length + (selectable ? 1 : 0);
|
|
221
|
+
const showEmpty = !loading && sortedRows.length === 0;
|
|
222
|
+
const checkCellCls = 'tbl__cell--check' + (pinFirst ? ' tbl__cell--pin' : '');
|
|
223
|
+
|
|
224
|
+
const checkSize = density === 'compact' ? 'sm' : 'md';
|
|
225
|
+
const bulkOpen = selectable && selected.size > 0;
|
|
226
|
+
if (selected.size > 0) lastCountRef.current = selected.size;
|
|
227
|
+
|
|
228
|
+
const rootCls = cx(
|
|
229
|
+
'tbl',
|
|
230
|
+
density === 'compact' && 'tbl--compact',
|
|
231
|
+
pinFirst && 'tbl--pin',
|
|
232
|
+
loading && 'tbl--loading',
|
|
233
|
+
onRowClick && 'tbl--clickable',
|
|
234
|
+
className,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div ref={wrapRef} className={rootCls} {...htmlProps}>
|
|
239
|
+
{selectable ? (
|
|
240
|
+
<div
|
|
241
|
+
className="tbl__bulk"
|
|
242
|
+
data-open={bulkOpen ? '' : undefined}
|
|
243
|
+
aria-hidden={bulkOpen ? undefined : true}
|
|
244
|
+
role="toolbar"
|
|
245
|
+
aria-label="Selection actions"
|
|
246
|
+
>
|
|
247
|
+
<Checkbox
|
|
248
|
+
className="tbl__cbx"
|
|
249
|
+
size={checkSize}
|
|
250
|
+
checked={allSelected}
|
|
251
|
+
indeterminate={someSelected}
|
|
252
|
+
htmlProps={{ 'aria-label': 'Select all rows' }}
|
|
253
|
+
onChange={toggleAll}
|
|
254
|
+
/>
|
|
255
|
+
<span className="tbl__bulkCount" aria-live="polite" aria-atomic="true">
|
|
256
|
+
<Odometer value={bulkOpen ? selected.size : lastCountRef.current} />
|
|
257
|
+
<span> selected</span>
|
|
258
|
+
</span>
|
|
259
|
+
<span className="tbl__bulkSpacer"></span>
|
|
260
|
+
{bulkActions ? bulkActions(Array.from(selected), clearSelection) : null}
|
|
261
|
+
<Button variant="ghost" size="sm" onClick={clearSelection}>
|
|
262
|
+
Clear
|
|
263
|
+
</Button>
|
|
264
|
+
</div>
|
|
265
|
+
) : null}
|
|
266
|
+
|
|
267
|
+
<div className="tbl__viewport">
|
|
268
|
+
<div className="tbl__scroll" ref={scrollRef}>
|
|
269
|
+
<table className="tbl__table" aria-label={ariaLabel} aria-busy={loading || undefined}>
|
|
270
|
+
<thead>
|
|
271
|
+
<tr>
|
|
272
|
+
{selectable ? (
|
|
273
|
+
<th scope="col" className={checkCellCls}>
|
|
274
|
+
<Checkbox
|
|
275
|
+
className="tbl__cbx"
|
|
276
|
+
size={checkSize}
|
|
277
|
+
checked={allSelected}
|
|
278
|
+
indeterminate={someSelected}
|
|
279
|
+
htmlProps={{ 'aria-label': 'Select all rows' }}
|
|
280
|
+
onChange={toggleAll}
|
|
281
|
+
/>
|
|
282
|
+
</th>
|
|
283
|
+
) : null}
|
|
284
|
+
{columns.map((c, i) => {
|
|
285
|
+
const active = sort && sort.key === c.key;
|
|
286
|
+
const cls = (c.sortable ? ['tbl__cell--sortable'] : []).concat(cellMods(c, i));
|
|
287
|
+
return (
|
|
288
|
+
<th
|
|
289
|
+
key={c.key}
|
|
290
|
+
scope="col"
|
|
291
|
+
className={cls.join(' ')}
|
|
292
|
+
aria-sort={active ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
293
|
+
>
|
|
294
|
+
{c.sortable ? (
|
|
295
|
+
<button type="button" className="tbl__sort" onClick={() => cycleSort(c)}>
|
|
296
|
+
<span className="tbl__sortLbl">{c.label}</span>
|
|
297
|
+
<span
|
|
298
|
+
className={
|
|
299
|
+
'tbl__sortIcon' +
|
|
300
|
+
(active ? ' is-on' : '') +
|
|
301
|
+
(active && sort.dir === 'desc' ? ' is-desc' : '')
|
|
302
|
+
}
|
|
303
|
+
aria-hidden="true"
|
|
304
|
+
>
|
|
305
|
+
<Icon name="arrow-up" size="sm" />
|
|
306
|
+
</span>
|
|
307
|
+
</button>
|
|
308
|
+
) : c.label != null ? (
|
|
309
|
+
<span className="tbl__hLbl">{c.label}</span>
|
|
310
|
+
) : null}
|
|
311
|
+
</th>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</tr>
|
|
315
|
+
</thead>
|
|
316
|
+
<tbody>
|
|
317
|
+
{showEmpty ? (
|
|
318
|
+
<tr>
|
|
319
|
+
<td colSpan={colCount} className="tbl__emptyCell">
|
|
320
|
+
{empty != null ? empty : 'Nothing to show'}
|
|
321
|
+
</td>
|
|
322
|
+
</tr>
|
|
323
|
+
) : (
|
|
324
|
+
sortedRows.map((row, idx) => {
|
|
325
|
+
const k = keyOf(row);
|
|
326
|
+
const isSel = selected.has(k);
|
|
327
|
+
return (
|
|
328
|
+
<Motion
|
|
329
|
+
as="tr"
|
|
330
|
+
key={k}
|
|
331
|
+
layout={{ timing: t.layout }}
|
|
332
|
+
className="tbl__row"
|
|
333
|
+
data-selected={isSel ? '' : undefined}
|
|
334
|
+
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
335
|
+
>
|
|
336
|
+
{selectable ? (
|
|
337
|
+
<td className={checkCellCls} onClick={(e) => e.stopPropagation()}>
|
|
338
|
+
<Checkbox
|
|
339
|
+
className="tbl__cbx"
|
|
340
|
+
size={checkSize}
|
|
341
|
+
checked={isSel}
|
|
342
|
+
htmlProps={{
|
|
343
|
+
'aria-label': selectionLabel ? selectionLabel(row) : 'Select row',
|
|
344
|
+
onClick: (e) => {
|
|
345
|
+
shiftRef.current = e.shiftKey;
|
|
346
|
+
},
|
|
347
|
+
}}
|
|
348
|
+
onChange={() => toggleRow(idx)}
|
|
349
|
+
/>
|
|
350
|
+
</td>
|
|
351
|
+
) : null}
|
|
352
|
+
{columns.map((c, i) => (
|
|
353
|
+
<td
|
|
354
|
+
key={c.key}
|
|
355
|
+
className={cellMods(c, i)
|
|
356
|
+
.concat(c.mono ? ['tbl__cell--mono'] : [])
|
|
357
|
+
.concat(c.strong ? ['tbl__cell--strong'] : [])
|
|
358
|
+
.join(' ')}
|
|
359
|
+
>
|
|
360
|
+
{c.render ? c.render(row) : (row as Record<string, ReactNode>)[c.key]}
|
|
361
|
+
</td>
|
|
362
|
+
))}
|
|
363
|
+
</Motion>
|
|
364
|
+
);
|
|
365
|
+
})
|
|
366
|
+
)}
|
|
367
|
+
</tbody>
|
|
368
|
+
</table>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="tbl__edge tbl__edge--l" aria-hidden="true"></div>
|
|
371
|
+
<div className="tbl__edge tbl__edge--r" aria-hidden="true"></div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{footer ? <div className="tbl__foot">{footer}</div> : null}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|