@oprt/react 0.0.0-stage → 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.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -2
  3. package/dist/ActionStatus/ActionStatus.js +104 -0
  4. package/dist/ActionStatus/ActionStatus.manifest.js +64 -0
  5. package/dist/ActionStatus/index.js +7 -0
  6. package/dist/Alert/Alert.js +86 -0
  7. package/dist/Alert/Alert.manifest.js +36 -0
  8. package/dist/Alert/index.js +7 -0
  9. package/dist/AppShell/AppShell.js +103 -0
  10. package/dist/AppShell/AppShell.manifest.js +66 -0
  11. package/dist/AppShell/index.js +9 -0
  12. package/dist/AttachmentPreview/AttachmentPreview.js +145 -0
  13. package/dist/AttachmentPreview/AttachmentPreview.manifest.js +42 -0
  14. package/dist/AttachmentPreview/index.js +9 -0
  15. package/dist/Avatar/Avatar.js +87 -0
  16. package/dist/Avatar/Avatar.manifest.js +25 -0
  17. package/dist/Avatar/index.js +7 -0
  18. package/dist/Badge/Badge.js +124 -0
  19. package/dist/Badge/Badge.manifest.js +41 -0
  20. package/dist/Badge/index.js +7 -0
  21. package/dist/Breadcrumb/Breadcrumb.js +64 -0
  22. package/dist/Breadcrumb/Breadcrumb.manifest.js +28 -0
  23. package/dist/Breadcrumb/index.js +7 -0
  24. package/dist/BulkActionBar/BulkActionBar.js +145 -0
  25. package/dist/BulkActionBar/BulkActionBar.manifest.js +46 -0
  26. package/dist/BulkActionBar/index.js +9 -0
  27. package/dist/BulkActionBar/selection.js +44 -0
  28. package/dist/BulkResults/BulkResults.js +269 -0
  29. package/dist/BulkResults/BulkResults.manifest.js +45 -0
  30. package/dist/BulkResults/index.js +7 -0
  31. package/dist/Button/Button.js +170 -0
  32. package/dist/Button/Button.manifest.js +36 -0
  33. package/dist/Button/index.js +7 -0
  34. package/dist/ButtonGroup/ButtonGroup.js +60 -0
  35. package/dist/ButtonGroup/ButtonGroup.manifest.js +22 -0
  36. package/dist/ButtonGroup/index.js +7 -0
  37. package/dist/Calendar/Calendar.js +106 -0
  38. package/dist/Calendar/Calendar.manifest.js +32 -0
  39. package/dist/Calendar/index.js +7 -0
  40. package/dist/Card/Card.js +70 -0
  41. package/dist/Card/Card.manifest.js +57 -0
  42. package/dist/Card/index.js +16 -0
  43. package/dist/Chart/Chart.js +1200 -0
  44. package/dist/Chart/Chart.manifest.js +110 -0
  45. package/dist/Chart/ChartControls.js +190 -0
  46. package/dist/Chart/geometry.js +217 -0
  47. package/dist/Chart/index.js +9 -0
  48. package/dist/Checkbox/Checkbox.js +166 -0
  49. package/dist/Checkbox/Checkbox.manifest.js +38 -0
  50. package/dist/Checkbox/index.js +7 -0
  51. package/dist/Collapsible/Collapsible.js +177 -0
  52. package/dist/Collapsible/Collapsible.manifest.js +54 -0
  53. package/dist/Collapsible/index.js +12 -0
  54. package/dist/CollectionState/CollectionState.js +68 -0
  55. package/dist/CollectionState/CollectionState.manifest.js +44 -0
  56. package/dist/CollectionState/index.js +7 -0
  57. package/dist/ComboBox/ComboBox.js +221 -0
  58. package/dist/ComboBox/ComboBox.manifest.js +87 -0
  59. package/dist/ComboBox/index.js +7 -0
  60. package/dist/CommandPalette/CommandPalette.js +276 -0
  61. package/dist/CommandPalette/CommandPalette.manifest.js +24 -0
  62. package/dist/CommandPalette/index.js +7 -0
  63. package/dist/ConfirmDialog/ConfirmDialog.js +84 -0
  64. package/dist/ConfirmDialog/ConfirmDialog.manifest.js +32 -0
  65. package/dist/ConfirmDialog/index.js +7 -0
  66. package/dist/ContextMenu/ContextMenu.js +192 -0
  67. package/dist/ContextMenu/ContextMenu.manifest.js +41 -0
  68. package/dist/ContextMenu/index.js +7 -0
  69. package/dist/CoordinateInput/CoordinateInput.js +141 -0
  70. package/dist/CoordinateInput/CoordinateInput.manifest.js +36 -0
  71. package/dist/CoordinateInput/index.js +9 -0
  72. package/dist/CopyButton/CopyButton.js +110 -0
  73. package/dist/CopyButton/CopyButton.manifest.js +35 -0
  74. package/dist/CopyButton/index.js +7 -0
  75. package/dist/DataGrid/DataGrid.js +459 -0
  76. package/dist/DataGrid/DataGrid.manifest.js +135 -0
  77. package/dist/DataGrid/PageControls.js +41 -0
  78. package/dist/DataGrid/index.js +9 -0
  79. package/dist/DataTable/DataTable.js +375 -0
  80. package/dist/DataTable/DataTable.manifest.js +86 -0
  81. package/dist/DataTable/index.js +9 -0
  82. package/dist/DataTable/sort.js +45 -0
  83. package/dist/DataTable/useScrollsSideways.js +61 -0
  84. package/dist/DataTable/useTableWindow.js +185 -0
  85. package/dist/DatePicker/DatePicker.js +187 -0
  86. package/dist/DatePicker/DatePicker.manifest.js +40 -0
  87. package/dist/DatePicker/index.js +7 -0
  88. package/dist/DateRangePicker/DateRangePicker.js +156 -0
  89. package/dist/DateRangePicker/DateRangePicker.manifest.js +41 -0
  90. package/dist/DateRangePicker/index.js +7 -0
  91. package/dist/DateTimePicker/DateTimePicker.js +213 -0
  92. package/dist/DateTimePicker/DateTimePicker.manifest.js +44 -0
  93. package/dist/DateTimePicker/index.js +7 -0
  94. package/dist/DescriptionList/DescriptionList.js +93 -0
  95. package/dist/DescriptionList/DescriptionList.manifest.js +40 -0
  96. package/dist/DescriptionList/index.js +9 -0
  97. package/dist/Dialog/Dialog.js +214 -0
  98. package/dist/Dialog/Dialog.manifest.js +31 -0
  99. package/dist/Dialog/index.js +7 -0
  100. package/dist/Divider/Divider.js +64 -0
  101. package/dist/Divider/Divider.manifest.js +21 -0
  102. package/dist/Divider/index.js +7 -0
  103. package/dist/DropdownMenu/DropdownMenu.js +170 -0
  104. package/dist/DropdownMenu/DropdownMenu.manifest.js +82 -0
  105. package/dist/DropdownMenu/index.js +16 -0
  106. package/dist/EntityPreview/EntityPreview.js +82 -0
  107. package/dist/EntityPreview/EntityPreview.manifest.js +43 -0
  108. package/dist/EntityPreview/index.js +7 -0
  109. package/dist/EventTimeline/EventTimeline.js +249 -0
  110. package/dist/EventTimeline/EventTimeline.manifest.js +57 -0
  111. package/dist/EventTimeline/index.js +7 -0
  112. package/dist/Field/Field.js +81 -0
  113. package/dist/Field/Field.manifest.js +32 -0
  114. package/dist/Field/index.js +9 -0
  115. package/dist/FileUpload/FileUpload.js +215 -0
  116. package/dist/FileUpload/FileUpload.manifest.js +48 -0
  117. package/dist/FileUpload/index.js +7 -0
  118. package/dist/FilterBar/FilterBar.js +173 -0
  119. package/dist/FilterBar/FilterBar.manifest.js +49 -0
  120. package/dist/FilterBar/index.js +7 -0
  121. package/dist/FormErrorSummary/FormErrorSummary.js +70 -0
  122. package/dist/FormErrorSummary/FormErrorSummary.manifest.js +29 -0
  123. package/dist/FormErrorSummary/index.js +9 -0
  124. package/dist/FormSection/FormSection.js +51 -0
  125. package/dist/FormSection/FormSection.manifest.js +30 -0
  126. package/dist/FormSection/index.js +7 -0
  127. package/dist/InlineEdit/InlineEdit.js +261 -0
  128. package/dist/InlineEdit/InlineEdit.manifest.js +40 -0
  129. package/dist/InlineEdit/index.js +7 -0
  130. package/dist/Input/Input.js +126 -0
  131. package/dist/Input/Input.manifest.js +49 -0
  132. package/dist/Input/index.js +7 -0
  133. package/dist/Inspector/Inspector.js +63 -0
  134. package/dist/Inspector/Inspector.manifest.js +35 -0
  135. package/dist/Inspector/index.js +7 -0
  136. package/dist/JobStatus/JobStatus.js +118 -0
  137. package/dist/JobStatus/JobStatus.manifest.js +53 -0
  138. package/dist/JobStatus/index.js +7 -0
  139. package/dist/LayerControl/LayerControl.js +199 -0
  140. package/dist/LayerControl/LayerControl.manifest.js +34 -0
  141. package/dist/LayerControl/index.js +7 -0
  142. package/dist/Link/Link.js +83 -0
  143. package/dist/Link/Link.manifest.js +37 -0
  144. package/dist/Link/index.js +7 -0
  145. package/dist/MapControls/MapControls.js +122 -0
  146. package/dist/MapControls/MapControls.manifest.js +31 -0
  147. package/dist/MapControls/index.js +7 -0
  148. package/dist/MapLegend/MapLegend.js +89 -0
  149. package/dist/MapLegend/MapLegend.manifest.js +29 -0
  150. package/dist/MapLegend/index.js +7 -0
  151. package/dist/MapScale/MapScale.js +96 -0
  152. package/dist/MapScale/MapScale.manifest.js +31 -0
  153. package/dist/MapScale/index.js +7 -0
  154. package/dist/MediaFrame/MediaFrame.js +113 -0
  155. package/dist/MediaFrame/MediaFrame.manifest.js +57 -0
  156. package/dist/MediaFrame/index.js +7 -0
  157. package/dist/MultiSelect/MultiSelect.js +113 -0
  158. package/dist/MultiSelect/MultiSelect.manifest.js +51 -0
  159. package/dist/MultiSelect/index.js +7 -0
  160. package/dist/NumberInput/NumberInput.js +210 -0
  161. package/dist/NumberInput/NumberInput.manifest.js +56 -0
  162. package/dist/NumberInput/index.js +7 -0
  163. package/dist/OperationalStatus/OperationalStatus.js +89 -0
  164. package/dist/OperationalStatus/OperationalStatus.manifest.js +39 -0
  165. package/dist/OperationalStatus/index.js +9 -0
  166. package/dist/PlaybackControls/PlaybackControls.js +289 -0
  167. package/dist/PlaybackControls/PlaybackControls.manifest.js +85 -0
  168. package/dist/PlaybackControls/index.js +7 -0
  169. package/dist/Popover/Popover.js +302 -0
  170. package/dist/Popover/Popover.manifest.js +50 -0
  171. package/dist/Popover/index.js +7 -0
  172. package/dist/Progress/Progress.js +132 -0
  173. package/dist/Progress/Progress.manifest.js +30 -0
  174. package/dist/Progress/index.js +7 -0
  175. package/dist/PropertyFilter/PropertyFilter.js +306 -0
  176. package/dist/PropertyFilter/PropertyFilter.manifest.js +32 -0
  177. package/dist/PropertyFilter/index.js +11 -0
  178. package/dist/PropertyFilter/tokenError.js +17 -0
  179. package/dist/RadioGroup/RadioGroup.js +143 -0
  180. package/dist/RadioGroup/RadioGroup.manifest.js +44 -0
  181. package/dist/RadioGroup/index.js +9 -0
  182. package/dist/Select/Select.js +219 -0
  183. package/dist/Select/Select.manifest.js +77 -0
  184. package/dist/Select/index.js +9 -0
  185. package/dist/Sidebar/Sidebar.js +146 -0
  186. package/dist/Sidebar/Sidebar.manifest.js +72 -0
  187. package/dist/Sidebar/index.js +18 -0
  188. package/dist/Skeleton/Skeleton.js +76 -0
  189. package/dist/Skeleton/Skeleton.manifest.js +30 -0
  190. package/dist/Skeleton/index.js +7 -0
  191. package/dist/Slider/Slider.js +246 -0
  192. package/dist/Slider/Slider.manifest.js +56 -0
  193. package/dist/Slider/index.js +7 -0
  194. package/dist/Sparkline/Sparkline.js +135 -0
  195. package/dist/Sparkline/Sparkline.manifest.js +37 -0
  196. package/dist/Sparkline/index.js +7 -0
  197. package/dist/SplitPane/SplitPane.js +316 -0
  198. package/dist/SplitPane/SplitPane.manifest.js +79 -0
  199. package/dist/SplitPane/index.js +7 -0
  200. package/dist/StatCard/StatCard.js +163 -0
  201. package/dist/StatCard/StatCard.manifest.js +45 -0
  202. package/dist/StatCard/index.js +7 -0
  203. package/dist/Tabs/Tabs.js +181 -0
  204. package/dist/Tabs/Tabs.manifest.js +72 -0
  205. package/dist/Tabs/index.js +16 -0
  206. package/dist/Tabs/useTabsIndicator.js +71 -0
  207. package/dist/Tabs/useTabsScroll.js +123 -0
  208. package/dist/TagGroup/TagGroup.js +142 -0
  209. package/dist/TagGroup/TagGroup.manifest.js +35 -0
  210. package/dist/TagGroup/index.js +7 -0
  211. package/dist/Textarea/Textarea.js +68 -0
  212. package/dist/Textarea/Textarea.manifest.js +25 -0
  213. package/dist/Textarea/index.js +7 -0
  214. package/dist/TimeField/TimeField.js +118 -0
  215. package/dist/TimeField/TimeField.manifest.js +42 -0
  216. package/dist/TimeField/index.js +7 -0
  217. package/dist/TimeRange/TimeRange.js +184 -0
  218. package/dist/TimeRange/TimeRange.manifest.js +48 -0
  219. package/dist/TimeRange/index.js +9 -0
  220. package/dist/Toast/Toast.js +188 -0
  221. package/dist/Toast/Toast.manifest.js +77 -0
  222. package/dist/Toast/index.js +9 -0
  223. package/dist/Toggle/Toggle.js +154 -0
  224. package/dist/Toggle/Toggle.manifest.js +54 -0
  225. package/dist/Toggle/index.js +7 -0
  226. package/dist/Toolbar/Toolbar.js +86 -0
  227. package/dist/Toolbar/Toolbar.manifest.js +70 -0
  228. package/dist/Toolbar/index.js +12 -0
  229. package/dist/Tooltip/Tooltip.js +122 -0
  230. package/dist/Tooltip/Tooltip.manifest.js +27 -0
  231. package/dist/Tooltip/index.js +7 -0
  232. package/dist/TreeView/TreeView.js +129 -0
  233. package/dist/TreeView/TreeView.manifest.js +56 -0
  234. package/dist/TreeView/index.js +7 -0
  235. package/dist/UploadQueue/UploadQueue.js +296 -0
  236. package/dist/UploadQueue/UploadQueue.manifest.js +34 -0
  237. package/dist/UploadQueue/index.js +7 -0
  238. package/dist/_internal/MotionPopover.js +113 -0
  239. package/dist/_internal/aria.js +40 -0
  240. package/dist/_internal/bytes.js +18 -0
  241. package/dist/_internal/controlStyles.js +26 -0
  242. package/dist/_internal/date.js +179 -0
  243. package/dist/_internal/devValidate.js +51 -0
  244. package/dist/_internal/fieldIds.js +50 -0
  245. package/dist/_internal/hydration.js +74 -0
  246. package/dist/_internal/manifest-entry.js +0 -0
  247. package/dist/_internal/mergeRefs.js +68 -0
  248. package/dist/_internal/motion.js +138 -0
  249. package/dist/_internal/navigationLandmark.js +13 -0
  250. package/dist/_internal/numberText.js +176 -0
  251. package/dist/_internal/overlay.js +53 -0
  252. package/dist/_internal/segmentField.js +125 -0
  253. package/dist/_internal/suggestions.js +20 -0
  254. package/dist/_internal/timestamp.js +25 -0
  255. package/dist/_internal/unavailable.js +13 -0
  256. package/dist/_internal/useActionFocus.js +27 -0
  257. package/dist/_internal/useElementWidth.js +37 -0
  258. package/dist/_internal/useIsMounted.js +18 -0
  259. package/dist/_internal/useIsomorphicLayoutEffect.js +8 -0
  260. package/dist/_internal/useRestoreFocusOnRemoval.js +57 -0
  261. package/dist/index.cjs +17811 -0
  262. package/dist/index.d.cts +2330 -0
  263. package/dist/index.d.ts +2330 -0
  264. package/dist/index.js +407 -0
  265. package/dist/version.js +8 -0
  266. package/package.json +80 -3
@@ -0,0 +1,375 @@
1
+ "use client";
2
+
3
+ // src/DataTable/DataTable.tsx
4
+ import { ChevronUp, ChevronDown } from "@oprt/icons";
5
+ import {
6
+ forwardRef,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ import clsx from "clsx";
13
+ import { Badge } from "../Badge/Badge.js";
14
+ import { numberText } from "../_internal/numberText.js";
15
+ import { sortedOrder } from "./sort.js";
16
+ import { useScrollsSideways } from "./useScrollsSideways.js";
17
+ import { useTableWindow } from "./useTableWindow.js";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ var SEMANTIC_BADGE = /* @__PURE__ */ new Set(["success", "warning", "error", "info"]);
20
+ var CELL_CONTROL_SELECTOR = "button, a, input, select, textarea, label, summary, [role='button'], [role='link'], [role='checkbox'], [role='radio'], [role='switch'], [role='combobox'], [role='slider'], [role='textbox'], [tabindex], [contenteditable]:not([contenteditable='false'])";
21
+ function SortGlyph({ dir }) {
22
+ return /* @__PURE__ */ jsxs(
23
+ "span",
24
+ {
25
+ "aria-hidden": "true",
26
+ className: clsx(
27
+ "inline-flex h-oprt-sm w-1.5u shrink-0 flex-col items-center justify-center",
28
+ dir ? "text-oprt-accent" : "text-oprt-text-dim"
29
+ ),
30
+ children: [
31
+ dir !== "desc" && /* @__PURE__ */ jsx(ChevronUp, { size: 12 }),
32
+ dir !== "asc" && /* @__PURE__ */ jsx(ChevronDown, { size: 12 })
33
+ ]
34
+ }
35
+ );
36
+ }
37
+ var SORTABLE_ROWS_UNWINDOWED = 150;
38
+ function isRightAligned(column) {
39
+ return column.align === "right" || (column.as === "number" || column.as === "timestamp") && column.align !== "left";
40
+ }
41
+ var NUMERIC_AUTO = "font-mono tabular-nums whitespace-nowrap";
42
+ var NUMERIC_FIXED = "font-mono tabular-nums";
43
+ function renderCell(column, value, fixedWidth) {
44
+ const numeric = fixedWidth ? NUMERIC_FIXED : NUMERIC_AUTO;
45
+ switch (column.as) {
46
+ case "number":
47
+ if (typeof value === "string" || typeof value === "number")
48
+ return numberText({ value, className: numeric });
49
+ return /* @__PURE__ */ jsx("span", { className: numeric, children: value });
50
+ case "timestamp":
51
+ return /* @__PURE__ */ jsx("span", { className: numeric, children: value });
52
+ case "code":
53
+ return /* @__PURE__ */ jsx("span", { className: "font-mono", children: value });
54
+ case "badge": {
55
+ const key = String(value).toLowerCase();
56
+ if (SEMANTIC_BADGE.has(key)) {
57
+ return /* @__PURE__ */ jsx(
58
+ Badge,
59
+ {
60
+ appearance: "text",
61
+ variant: key,
62
+ indicator: "none",
63
+ children: value
64
+ }
65
+ );
66
+ }
67
+ return /* @__PURE__ */ jsx(Badge, { appearance: "text", variant: "neutral", children: value });
68
+ }
69
+ default:
70
+ if (column.highlight) {
71
+ return /* @__PURE__ */ jsx("span", { className: "text-oprt-accent font-medium", children: value });
72
+ }
73
+ return value;
74
+ }
75
+ }
76
+ var DataTable = forwardRef(
77
+ ({
78
+ columns,
79
+ data,
80
+ sortable = false,
81
+ sortDescriptor,
82
+ dense = false,
83
+ onRowClick,
84
+ getRowId,
85
+ caption,
86
+ emptyMessage = "No results.",
87
+ columnWidths,
88
+ pinnedColumnKeys = [],
89
+ virtualization,
90
+ rowIndexOffset = 0,
91
+ totalRowCount,
92
+ className,
93
+ style,
94
+ onBlurCapture,
95
+ ...props
96
+ }, ref) => {
97
+ const [sort, setSort] = useState(null);
98
+ const externallySorted = sortDescriptor !== void 0;
99
+ const toggleSort = (key) => {
100
+ setSort((prev) => {
101
+ if (!prev || prev.key !== key) return { key, dir: "asc" };
102
+ if (prev.dir === "asc") return { key, dir: "desc" };
103
+ return null;
104
+ });
105
+ };
106
+ const columnSortable = (col) => !externallySorted && sortable && col.sortable !== false;
107
+ const order = useMemo(
108
+ () => !externallySorted && sort ? sortedOrder(
109
+ data.map((row) => row[sort.key]),
110
+ sort.dir === "asc" ? 1 : -1
111
+ ) : null,
112
+ [data, externallySorted, sort]
113
+ );
114
+ const rows = useMemo(
115
+ () => order ? order.map((position) => data[position]) : data,
116
+ [data, order]
117
+ );
118
+ const latestGetRowId = useRef(getRowId);
119
+ latestGetRowId.current = getRowId;
120
+ const withRowIds = getRowId !== void 0;
121
+ const rowKeys = useMemo(() => {
122
+ const read = withRowIds ? latestGetRowId.current : void 0;
123
+ const keys2 = rows.map((row, position) => read?.(row, position) ?? String(position));
124
+ const index = /* @__PURE__ */ new Map();
125
+ keys2.forEach((key, position) => {
126
+ if (!index.has(key)) index.set(key, position);
127
+ });
128
+ return { keys: keys2, index };
129
+ }, [rows, withRowIds]);
130
+ const sortsItself = !externallySorted && sortable && columns.some((column) => column.sortable !== false);
131
+ const outsideEnvelope = sortsItself && !virtualization && data.length > SORTABLE_ROWS_UNWINDOWED;
132
+ const warned = useRef(false);
133
+ useEffect(() => {
134
+ if (process.env.NODE_ENV === "production" || !outsideEnvelope || warned.current) return;
135
+ warned.current = true;
136
+ console.warn(
137
+ JSON.stringify(
138
+ {
139
+ component: "DataTable",
140
+ warning: "OUTSIDE_SIZE_ENVELOPE",
141
+ prop: "data",
142
+ received: `${data.length} rows, sortable, without virtualization`,
143
+ suggestion: `A sortable DataTable that is not windowed stays responsive up to about ${SORTABLE_ROWS_UNWINDOWED} rows: sorting moves every row and the browser lays them all out again. Use DataGrid, which pages or windows its rows, or pass virtualization.`
144
+ },
145
+ null,
146
+ 2
147
+ )
148
+ );
149
+ }, [outsideEnvelope, data.length]);
150
+ const ariaSortFor = (col) => {
151
+ if (externallySorted) {
152
+ if (!sortDescriptor || sortDescriptor.key !== col.key) return void 0;
153
+ return sortDescriptor.direction === "asc" ? "ascending" : "descending";
154
+ }
155
+ if (!columnSortable(col)) return void 0;
156
+ if (!sort || sort.key !== col.key) return "none";
157
+ return sort.dir === "asc" ? "ascending" : "descending";
158
+ };
159
+ const keys = rowKeys.keys;
160
+ const rowWindow = useTableWindow(rowKeys, virtualization);
161
+ const [box, scrolls] = useScrollsSideways(!virtualization);
162
+ const sized = Boolean(columnWidths);
163
+ const widths = columns.map((column) => {
164
+ const width = columnWidths?.[column.key];
165
+ return width !== void 0 && Number.isFinite(width) ? Math.max(24, width) : 160;
166
+ });
167
+ const leftFor = (index) => columns.slice(0, index).reduce(
168
+ (offset, column, i) => offset + (pinnedColumnKeys.includes(column.key) ? widths[i] : 0),
169
+ 0
170
+ );
171
+ const indexed = totalRowCount !== void 0 || Boolean(virtualization) || rowIndexOffset > 0;
172
+ const cellLook = columns.map((col, columnIndex) => {
173
+ const pinned = pinnedColumnKeys.includes(col.key);
174
+ return {
175
+ className: clsx(
176
+ "border-b border-oprt-border px-oprt-cell-inline align-middle [&>button]:align-middle [&>label]:align-middle",
177
+ dense ? "py-oprt-cell-block-dense h-oprt-row-dense" : "py-oprt-cell-block h-oprt-row",
178
+ isRightAligned(col) && "text-right",
179
+ sized && "break-words",
180
+ pinned && "z-10 bg-oprt-surface-1"
181
+ ),
182
+ style: {
183
+ ...pinned ? { position: "sticky", left: leftFor(columnIndex) } : {},
184
+ ...rowWindow.fixed !== void 0 && virtualization ? { height: rowWindow.fixed, paddingTop: 0, paddingBottom: 0 } : {}
185
+ }
186
+ };
187
+ });
188
+ const body = [];
189
+ let lastIndex = 0;
190
+ const spacer = (from, to) => {
191
+ if (to <= from) return;
192
+ body.push(
193
+ /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx(
194
+ "td",
195
+ {
196
+ colSpan: Math.max(columns.length, 1),
197
+ style: {
198
+ height: rowWindow.offsetOf(to) - rowWindow.offsetOf(from),
199
+ padding: 0,
200
+ border: 0
201
+ }
202
+ }
203
+ ) }, `spacer-${from}`)
204
+ );
205
+ };
206
+ const renderRow = (i) => {
207
+ const row = rows[i];
208
+ const rowKey = keys[i];
209
+ const source = order ? order[i] : i;
210
+ const clickable = Boolean(onRowClick);
211
+ body.push(
212
+ /* @__PURE__ */ jsx(
213
+ "tr",
214
+ {
215
+ ref: rowWindow.rowRef(rowKey),
216
+ "aria-rowindex": indexed ? rowIndexOffset + i + 2 : void 0,
217
+ "data-row-id": rowKey,
218
+ onFocusCapture: () => rowWindow.onFocusRow(rowKey, i),
219
+ ...clickable ? {
220
+ onClick: (e) => {
221
+ const control = e.target.closest(CELL_CONTROL_SELECTOR);
222
+ if (e.defaultPrevented || control && e.currentTarget.contains(control)) return;
223
+ onRowClick?.(row, source);
224
+ }
225
+ } : {},
226
+ className: clsx(clickable && "cursor-pointer hover:bg-oprt-surface-2"),
227
+ children: columns.map((col, columnIndex) => {
228
+ const value = col.render ? col.render(row[col.key], row) : row[col.key];
229
+ const rendered = col.render ? value : renderCell(col, value, sized);
230
+ const textValue = col.as !== "badge" && (typeof value === "string" || typeof value === "number");
231
+ const look = cellLook[columnIndex];
232
+ const content = clickable && columnIndex === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
233
+ !textValue && rendered,
234
+ /* @__PURE__ */ jsx(
235
+ "button",
236
+ {
237
+ type: "button",
238
+ className: clsx(
239
+ "inline-flex items-center h-oprt-sm text-oprt-base font-medium hover:underline border border-transparent focus-visible:border-oprt-border-focus focus-visible:shadow-oprt-focus focus-visible:outline-hidden",
240
+ !sized && "whitespace-nowrap",
241
+ !textValue && "ml-1u uppercase tracking-oprt-caps"
242
+ ),
243
+ onClick: () => onRowClick?.(row, source),
244
+ children: textValue ? rendered : `Open row ${rowIndexOffset + i + 1}`
245
+ }
246
+ )
247
+ ] }) : rendered;
248
+ return /* @__PURE__ */ jsx("td", { style: look.style, className: look.className, children: rowWindow.fixed !== void 0 && virtualization ? /* @__PURE__ */ jsx("div", { style: { maxHeight: rowWindow.fixed - 1 }, className: "overflow-hidden", children: content }) : content }, col.key);
249
+ })
250
+ },
251
+ rowKey
252
+ )
253
+ );
254
+ };
255
+ if (rowWindow.indices === null) {
256
+ for (let i = 0; i < rows.length; i++) renderRow(i);
257
+ } else {
258
+ for (const i of rowWindow.indices) {
259
+ spacer(lastIndex, i);
260
+ lastIndex = i + 1;
261
+ renderRow(i);
262
+ }
263
+ spacer(lastIndex, rows.length);
264
+ }
265
+ const table = /* @__PURE__ */ jsxs(
266
+ "table",
267
+ {
268
+ ref,
269
+ className: clsx(
270
+ "w-full border-collapse font-sans text-oprt-base normal-case text-oprt-text",
271
+ className
272
+ ),
273
+ style: {
274
+ ...sized ? { tableLayout: "fixed", width: widths.reduce((sum, width) => sum + width, 0) } : {},
275
+ ...style
276
+ },
277
+ "aria-rowcount": indexed ? (totalRowCount ?? rows.length) + 1 : void 0,
278
+ onBlurCapture: (event) => {
279
+ if (!event.currentTarget.contains(event.relatedTarget))
280
+ rowWindow.clearFocus();
281
+ onBlurCapture?.(event);
282
+ },
283
+ ...props,
284
+ children: [
285
+ caption && /* @__PURE__ */ jsx("caption", { className: "sr-only", children: caption }),
286
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { "aria-rowindex": indexed ? 1 : void 0, children: columns.map((col, columnIndex) => {
287
+ const active = columnSortable(col);
288
+ const dir = externallySorted ? sortDescriptor?.key === col.key ? sortDescriptor.direction : null : sort && sort.key === col.key ? sort.dir : null;
289
+ return /* @__PURE__ */ jsx(
290
+ "th",
291
+ {
292
+ scope: "col",
293
+ "aria-sort": ariaSortFor(col),
294
+ style: {
295
+ ...sized ? { width: widths[columnIndex] } : col.width ? { width: col.width } : {},
296
+ ...pinnedColumnKeys.includes(col.key) ? { position: "sticky", left: leftFor(columnIndex) } : {}
297
+ },
298
+ className: clsx(
299
+ "px-oprt-cell-inline text-oprt-sm font-medium uppercase tracking-oprt-sm text-oprt-text-dim border-b border-oprt-border align-middle",
300
+ !sized && "whitespace-nowrap",
301
+ dense ? "py-oprt-cell-block-dense h-oprt-row-dense" : "py-oprt-cell-block h-oprt-row",
302
+ isRightAligned(col) ? "text-right" : "text-left",
303
+ pinnedColumnKeys.includes(col.key) && "z-10 bg-oprt-surface-1"
304
+ ),
305
+ children: active ? /* @__PURE__ */ jsxs(
306
+ "button",
307
+ {
308
+ type: "button",
309
+ onClick: () => toggleSort(col.key),
310
+ className: "inline-flex items-center gap-1u h-oprt-sm whitespace-nowrap text-oprt-base font-medium uppercase tracking-oprt-sm border border-transparent focus-visible:border-oprt-border-focus focus-visible:shadow-oprt-focus focus-visible:outline-hidden",
311
+ children: [
312
+ col.label,
313
+ /* @__PURE__ */ jsx(SortGlyph, { dir })
314
+ ]
315
+ }
316
+ ) : externallySorted && dir ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1u", children: [
317
+ col.label,
318
+ /* @__PURE__ */ jsx(SortGlyph, { dir })
319
+ ] }) : col.label
320
+ },
321
+ col.key
322
+ );
323
+ }) }) }),
324
+ /* @__PURE__ */ jsxs("tbody", { children: [
325
+ body,
326
+ rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
327
+ "td",
328
+ {
329
+ colSpan: Math.max(columns.length, 1),
330
+ className: "px-2u py-4u text-oprt-text-dim border-b border-oprt-border",
331
+ children: emptyMessage
332
+ }
333
+ ) })
334
+ ] })
335
+ ]
336
+ }
337
+ );
338
+ const regionName = `${caption ?? "Data table"} scroll viewport`;
339
+ return virtualization ? /* @__PURE__ */ jsx(
340
+ "div",
341
+ {
342
+ ref: rowWindow.viewport,
343
+ role: "region",
344
+ "aria-label": regionName,
345
+ tabIndex: 0,
346
+ style: { height: rowWindow.height, overflowAnchor: "none" },
347
+ className: "overflow-auto border border-oprt-border focus-visible:outline-hidden focus-visible:border-oprt-border-focus focus-visible:shadow-oprt-focus",
348
+ onScroll: rowWindow.onScroll,
349
+ children: table
350
+ }
351
+ ) : (
352
+ // Columns that do not fit scroll sideways inside the table's own box, never the page (R6).
353
+ // While they do, the box is a named region and a tab stop, so a keyboard can scroll it; while
354
+ // the table fits it is a plain box that adds no tab stop.
355
+ /* @__PURE__ */ jsx(
356
+ "div",
357
+ {
358
+ ref: box,
359
+ "data-oprt-table-scroll": "",
360
+ className: clsx(
361
+ "max-w-full overflow-x-auto",
362
+ scrolls && "border border-transparent focus-visible:outline-hidden focus-visible:border-oprt-border-focus focus-visible:shadow-oprt-focus"
363
+ ),
364
+ ...scrolls ? { role: "region", "aria-label": regionName, tabIndex: 0 } : {},
365
+ children: table
366
+ }
367
+ )
368
+ );
369
+ }
370
+ );
371
+ DataTable.displayName = "DataTable";
372
+ export {
373
+ DataTable,
374
+ SORTABLE_ROWS_UNWINDOWED
375
+ };
@@ -0,0 +1,86 @@
1
+ // src/DataTable/DataTable.manifest.ts
2
+ var DataTableManifest = {
3
+ DataTable: {
4
+ description: "Data-driven table with sorting, highlighting, and cell rendering.",
5
+ props: {
6
+ columns: {
7
+ type: "array",
8
+ required: true,
9
+ description: "Column descriptors. Each item: { key: string (required), label: string (required), as?: 'text'|'number'|'code'|'timestamp'|'badge' (default 'text'), highlight?: boolean (default false), sortable?: boolean (default inherits table 'sortable'), width?: string (CSS width), align?: 'left'|'right' }. 'number'/'timestamp' cells render mono + right-aligned; with columnWidths a 'number' cell wraps only between digit groups and keeps a unit with its last group; 'badge' maps the value to a text-only Badge (success/warning/error/info use their semantic color, otherwise neutral). Use a custom render for outlined badges or an explicit indicator.",
10
+ items: {
11
+ key: "string (required)",
12
+ label: "string (required)",
13
+ as: "enum [text, number, code, timestamp, badge] (default: text)",
14
+ highlight: "boolean (default: false)",
15
+ sortable: "boolean (default: inherits from table)",
16
+ width: "string",
17
+ align: "enum [left, right]",
18
+ render: "(value, originalRow) => ReactNode; called for mounted cells only"
19
+ }
20
+ },
21
+ data: {
22
+ type: "array",
23
+ required: true,
24
+ description: "Array of row objects (Record<string, unknown>) keyed by column.key."
25
+ },
26
+ sortable: {
27
+ type: "boolean",
28
+ default: false,
29
+ description: "Enables column header sort buttons (cycles ascending \u2192 descending \u2192 unsorted) when sortDescriptor is omitted. Per-column 'sortable: false' opts a column out. Designed for about 150 rows without virtualization: development warns above that; use DataGrid or virtualization for larger result sets."
30
+ },
31
+ sortDescriptor: {
32
+ type: "node",
33
+ description: "Read-only { key: string, direction: 'asc' | 'desc' } metadata for rows already sorted by the caller. The matching header exposes aria-sort and a direction glyph; data order is preserved and internal header sort controls are disabled even when sortable is true. Pass null for externally managed source order. Omit to retain existing internal sortable behavior."
34
+ },
35
+ dense: {
36
+ type: "boolean",
37
+ default: false,
38
+ description: "Compact rows: 32px minimum height with 4px vertical cell padding. Default rows use a 40px minimum and 8px padding. Rows grow with content; explicit virtualization.rowHeight overrides these dimensions."
39
+ },
40
+ onRowClick: {
41
+ type: "function",
42
+ description: "(row, indexInSuppliedData) => void. The index is the row's index in data, also after a header sort. Adds a native action button to the first cell while preserving table row semantics. Text and number cells become the button label; React content remains independent beside an Open row button. Nested controls and editable content do not activate the row. Enter/Space activate the row button."
43
+ },
44
+ getRowId: {
45
+ type: "function",
46
+ description: "(row, index) => string. Stable React key per row; falls back to the index. Keep it pure: IDs are read again only when data changes, not when only the function does."
47
+ },
48
+ caption: {
49
+ type: "string",
50
+ description: "Accessible table caption, visually hidden (sr-only). It also names the table's scroll region, '<caption> scroll viewport': a table whose columns do not fit scrolls sideways inside its own box, which is a keyboard tab stop while it scrolls; the page never widens."
51
+ },
52
+ emptyMessage: {
53
+ type: "string",
54
+ default: "No results.",
55
+ description: "Message spanning all columns when data is empty."
56
+ },
57
+ columnWidths: {
58
+ type: "node",
59
+ description: "Record<columnKey, CSS pixel width> for fixed table layout. Unspecified columns use 160 pixels. DataGrid provides bounded user controls."
60
+ },
61
+ pinnedColumnKeys: {
62
+ type: "array",
63
+ description: "Column keys to keep sticky at the leading edge. Supply pinned columns first in columns, in the desired order, with columnWidths for accurate offsets. DataGrid manages this ordering automatically."
64
+ },
65
+ virtualization: {
66
+ type: "node",
67
+ description: "Optional {height, rowHeight?, estimateRowHeight?, overscan?}. Native rows are windowed in a labeled scrolling region. Omit rowHeight for measured variable heights; fixed rows clip overlong content. Prefer DataGrid for its built-in paginated fallback. Stable getRowId is required for retained focus across data changes."
68
+ },
69
+ rowIndexOffset: {
70
+ type: "number",
71
+ default: 0,
72
+ description: "Zero-based offset into the complete result set. Data rows announce offset + local index + 2, including the header."
73
+ },
74
+ totalRowCount: {
75
+ type: "number",
76
+ description: "Total data rows excluding the header. Native table aria-rowcount includes the header as one additional row."
77
+ }
78
+ },
79
+ composable_with: ["Card"],
80
+ never_inside: ["DataTable"],
81
+ example: '<DataTable\n columns={[\n { key: "service", label: "SERVICE" },\n { key: "latency", label: "LATENCY", as: "number", highlight: true },\n { key: "status", label: "STATUS", as: "badge" }\n ]}\n data={rows}\n sortable\n/>'
82
+ }
83
+ };
84
+ export {
85
+ DataTableManifest
86
+ };
@@ -0,0 +1,9 @@
1
+ // src/DataTable/index.ts
2
+ import {
3
+ DataTable
4
+ } from "./DataTable.js";
5
+ import { DataTableManifest } from "./DataTable.manifest.js";
6
+ export {
7
+ DataTable,
8
+ DataTableManifest
9
+ };
@@ -0,0 +1,45 @@
1
+ // src/DataTable/sort.ts
2
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
3
+ var textOf = (value) => String(value ?? "");
4
+ function compareCellValues(a, b) {
5
+ if (isFiniteNumber(a) && isFiniteNumber(b)) return a - b;
6
+ return textOf(a).localeCompare(textOf(b));
7
+ }
8
+ function sortedOrder(values, direction, compare) {
9
+ const order = Array.from({ length: values.length }, (_, index) => index);
10
+ if (compare) {
11
+ order.sort((i, j) => {
12
+ const result = compare(values[i], values[j]);
13
+ return Number.isFinite(result) ? result * direction : 0;
14
+ });
15
+ return order;
16
+ }
17
+ let numbers = 0;
18
+ for (const value of values) if (isFiniteNumber(value)) numbers++;
19
+ if (numbers === values.length) {
20
+ const numeric = Float64Array.from(values);
21
+ order.sort((i, j) => (numeric[i] - numeric[j]) * direction);
22
+ return order;
23
+ }
24
+ if (numbers === 0) {
25
+ const texts = values.map(textOf);
26
+ const distinct = [...new Set(texts)].sort((a, b) => a.localeCompare(b));
27
+ const ranks = /* @__PURE__ */ new Map();
28
+ let rank = -1;
29
+ let previous;
30
+ for (const text of distinct) {
31
+ if (previous === void 0 || previous.localeCompare(text) !== 0) rank++;
32
+ ranks.set(text, rank);
33
+ previous = text;
34
+ }
35
+ const ranked = Int32Array.from(texts, (text) => ranks.get(text));
36
+ order.sort((i, j) => (ranked[i] - ranked[j]) * direction);
37
+ return order;
38
+ }
39
+ order.sort((i, j) => compareCellValues(values[i], values[j]) * direction);
40
+ return order;
41
+ }
42
+ export {
43
+ compareCellValues,
44
+ sortedOrder
45
+ };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ // src/DataTable/useScrollsSideways.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useIsomorphicLayoutEffect } from "../_internal/useIsomorphicLayoutEffect.js";
6
+ import { createResizeObserver } from "../_internal/useElementWidth.js";
7
+ function useScrollsSideways(enabled = true) {
8
+ const box = useRef(null);
9
+ const followed = useRef(null);
10
+ const [scrolling, setScrolling] = useState(null);
11
+ const answered = useRef(scrolling);
12
+ useIsomorphicLayoutEffect(() => {
13
+ answered.current = scrolling;
14
+ });
15
+ const setBox = useCallback((element) => {
16
+ box.current = element;
17
+ if (element !== null && answered.current !== null && answered.current !== element) {
18
+ setScrolling(null);
19
+ }
20
+ }, []);
21
+ useIsomorphicLayoutEffect(() => {
22
+ const element = enabled ? box.current : null;
23
+ if (followed.current?.element === element) return;
24
+ followed.current?.stop();
25
+ followed.current = null;
26
+ if (!element) return;
27
+ let current = answered.current === element;
28
+ const measure = (focused = element.ownerDocument.activeElement === element) => {
29
+ const scrolls = element.scrollWidth > element.clientWidth + 1 || focused;
30
+ if (scrolls === current) return;
31
+ current = scrolls;
32
+ setScrolling(scrolls ? element : null);
33
+ };
34
+ const blurred = () => measure(false);
35
+ measure();
36
+ element.addEventListener("blur", blurred);
37
+ const observer = createResizeObserver(() => measure());
38
+ if (observer) {
39
+ observer.observe(element);
40
+ for (const child of Array.from(element.children)) observer.observe(child);
41
+ }
42
+ followed.current = {
43
+ element,
44
+ stop: () => {
45
+ element.removeEventListener("blur", blurred);
46
+ observer?.disconnect();
47
+ }
48
+ };
49
+ });
50
+ useEffect(
51
+ () => () => {
52
+ followed.current?.stop();
53
+ followed.current = null;
54
+ },
55
+ []
56
+ );
57
+ return [setBox, enabled && scrolling !== null && scrolling === box.current];
58
+ }
59
+ export {
60
+ useScrollsSideways
61
+ };