@lablup/ui-common 0.1.0-alpha.20 → 0.1.0-alpha.21

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.
@@ -1 +1 @@
1
- .data-table{border:1px solid var(--token-colorBorderSecondary,var(--token-colorBorder,#d9d9d9));border-radius:var(--token-borderRadiusLG,8px);background-color:var(--token-colorBgContainer,#fff);width:100%;position:relative;overflow-x:auto}.data-table__table{border-collapse:collapse;table-layout:auto;width:100%}.data-table__head{z-index:1;background-color:var(--token-colorFillQuaternary,var(--token-colorBgLayout,#f5f5f5));position:sticky;top:0}.data-table__row{border-bottom:1px solid var(--token-colorBorderSecondary,transparent)}.data-table__row:last-of-type{border-bottom:none}.data-table__row--clickable{cursor:pointer;transition:background-color var(--token-motionDurationFast,.12s) ease}.data-table__row--clickable:hover{background-color:var(--token-colorFillQuaternary,#0000000a)}.data-table__row--clickable:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:-2px}.data-table__cell{padding:var(--token-paddingSM,.5rem) var(--token-paddingMD,1rem);font-size:var(--token-fontSize,.875rem);color:var(--token-colorText,#141414);vertical-align:middle;text-align:start;word-break:break-word}.data-table__cell--align-left{text-align:left}.data-table__cell--align-right{text-align:right}.data-table__cell--align-center{text-align:center}.data-table__cell--head{font-weight:600;font-size:var(--token-fontSizeSM,.75rem);text-transform:uppercase;letter-spacing:.05em;color:var(--token-colorTextSecondary,#595959);background-color:var(--token-colorFillQuaternary,var(--token-colorBgLayout,#f5f5f5));border-bottom:1px solid var(--token-colorBorderSecondary,var(--token-colorBorder,#d9d9d9));-webkit-user-select:none;user-select:none;white-space:nowrap;word-break:normal;overflow-wrap:normal;position:relative}.data-table__head-label{vertical-align:middle;display:inline-block}.data-table__cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.data-table__cell--sortable:hover{background-color:var(--token-colorFillTertiary,#0000000f)}.data-table__cell--sortable:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:-2px}.data-table__cell--sort-active{color:var(--token-colorPrimary,#8b5cf6)}.data-table__sort-icon{vertical-align:middle;color:var(--token-colorTextSecondary,#595959);flex-shrink:0;align-items:center;margin-inline-start:var(--token-marginXXS,4px);line-height:0;display:inline-flex}.data-table__cell--sort-active .data-table__sort-icon{color:var(--token-colorPrimary,#8b5cf6)}.data-table__resize-handle{cursor:col-resize;width:6px;transition:background-color var(--token-motionDurationFast,.12s) ease;background-color:#0000;position:absolute;top:0;bottom:0;right:0}.data-table__resize-handle:hover{background-color:var(--token-colorPrimaryBorder,var(--token-colorPrimary,#8b5cf6));opacity:.5}.data-table__state-cell{padding:var(--token-paddingLG,16px);text-align:center;color:var(--token-colorTextSecondary,#595959)}.data-table__loading{background:linear-gradient(90deg, var(--token-colorFillQuaternary,#fafafa) 25%, var(--token-colorFillTertiary,#f9fafb) 50%, var(--token-colorFillQuaternary,#fafafa) 75%);border-radius:var(--token-borderRadiusSM,.25rem);background-size:200% 100%;width:100%;height:24px;animation:1.4s ease-in-out infinite data-table-shimmer}@keyframes data-table-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (prefers-reduced-motion:reduce){.data-table__loading{animation:none}}
1
+ .data-table{border:1px solid var(--token-colorBorderSecondary,var(--token-colorBorder,#d9d9d9));border-radius:var(--token-borderRadiusLG,8px);background-color:var(--token-colorBgContainer,#fff);width:100%;position:relative;overflow-x:auto}.data-table__table{border-collapse:collapse;table-layout:auto;width:100%}.data-table__head{z-index:1;background-color:var(--token-colorFillQuaternary,var(--token-colorBgLayout,#f5f5f5));position:sticky;top:0}.data-table__row{border-bottom:1px solid var(--token-colorBorderSecondary,transparent)}.data-table__row:last-of-type{border-bottom:none}.data-table__row--clickable{cursor:pointer;transition:background-color var(--token-motionDurationFast,.12s) ease}.data-table__row--clickable:hover{background-color:var(--token-colorFillQuaternary,#0000000a)}.data-table__row--clickable:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:-2px}.data-table__cell{padding:var(--token-paddingSM,.5rem) var(--token-paddingMD,1rem);font-size:var(--token-fontSize,.875rem);color:var(--token-colorText,#141414);vertical-align:middle;text-align:start;word-break:break-word}.data-table__cell--align-left{text-align:left}.data-table__cell--align-right{text-align:right}.data-table__cell--align-center{text-align:center}.data-table__cell--head{font-weight:600;font-size:var(--token-fontSizeSM,.75rem);text-transform:uppercase;letter-spacing:.05em;color:var(--token-colorTextSecondary,#595959);background-color:var(--token-colorFillQuaternary,var(--token-colorBgLayout,#f5f5f5));border-bottom:1px solid var(--token-colorBorderSecondary,var(--token-colorBorder,#d9d9d9));-webkit-user-select:none;user-select:none;white-space:nowrap;word-break:normal;overflow-wrap:normal;position:relative}.data-table__cell--resizable{padding-inline-end:calc(var(--token-paddingMD,1rem) + var(--token-controlHeightSM,2rem))}.data-table__head-label{vertical-align:middle;display:inline-block}.data-table__cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.data-table__cell--sortable:hover{background-color:var(--token-colorFillTertiary,#0000000f)}.data-table__cell--sortable:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:-2px}.data-table__cell--sort-active{color:var(--token-colorPrimary,#8b5cf6)}.data-table__sort-icon{vertical-align:middle;color:var(--token-colorTextSecondary,#595959);flex-shrink:0;align-items:center;margin-inline-start:var(--token-marginXXS,4px);line-height:0;display:inline-flex}.data-table__cell--sort-active .data-table__sort-icon{color:var(--token-colorPrimary,#8b5cf6)}.data-table__resize-handle{width:var(--token-controlHeightSM,2rem);cursor:col-resize;touch-action:none;outline:none;position:absolute;top:0;bottom:0;right:0}.data-table__resize-handle:before{top:var(--token-paddingXS,.25rem);right:calc(50% - 1px);bottom:var(--token-paddingXS,.25rem);content:"";border-radius:var(--token-borderRadiusSM,.25rem);background-color:var(--token-colorTextTertiary,#737373);opacity:1;width:2px;transition:background-color var(--token-motionDurationFast,.12s) ease, opacity var(--token-motionDurationFast,.12s) ease;position:absolute}.data-table__resize-handle:hover:before,.data-table__resize-handle:focus-visible:before{background-color:var(--token-focusRingColor);opacity:1}.data-table__resize-handle:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor);outline-offset:-2px}.data-table__state-cell{padding:var(--token-paddingLG,16px);text-align:center;color:var(--token-colorTextSecondary,#595959)}.data-table__loading{background:linear-gradient(90deg, var(--token-colorFillQuaternary,#fafafa) 25%, var(--token-colorFillTertiary,#f9fafb) 50%, var(--token-colorFillQuaternary,#fafafa) 75%);border-radius:var(--token-borderRadiusSM,.25rem);background-size:200% 100%;width:100%;height:24px;animation:1.4s ease-in-out infinite data-table-shimmer}@keyframes data-table-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (prefers-reduced-motion:reduce){.data-table__loading{animation:none}.data-table__resize-handle:before{transition:none}}
@@ -13,6 +13,14 @@ export interface DataTableColumn<T> {
13
13
  render: (row: T, index: number) => ReactNode;
14
14
  /** Optional minimum column width in pixels. */
15
15
  minWidth?: number;
16
+ /**
17
+ * Optional maximum column width in pixels. Values above this limit are
18
+ * clamped for pointer and keyboard resizing. Resizable columns default to
19
+ * 1200 pixels so every separator exposes a finite ARIA maximum. A
20
+ * `noResize` column keeps its existing unbounded width unless it explicitly
21
+ * supplies this property.
22
+ */
23
+ maxWidth?: number;
16
24
  /** Optional initial width in pixels (only honored on the first paint). */
17
25
  initialWidth?: number;
18
26
  /** When `true`, the column cannot be resized. Defaults to `false`. */
@@ -108,6 +116,18 @@ export interface DataTableProps<T> {
108
116
  className?: string;
109
117
  /** ARIA label for the table. Defaults to "Data table". */
110
118
  ariaLabel?: string;
119
+ /**
120
+ * Produces the accessible name for a resizable column's separator. The
121
+ * default is `Resize <column id> column`; pass a product's translated label
122
+ * when column ids are not suitable for people to hear.
123
+ */
124
+ resizeHandleLabel?: (column: DataTableColumn<T>) => string;
125
+ /**
126
+ * Produces the human-readable current width announced by a resize
127
+ * separator. The default is `<width> pixels`; pass a translated formatter
128
+ * when the host localizes measurements.
129
+ */
130
+ resizeValueText?: (width: number, column: DataTableColumn<T>) => string;
111
131
  /** Test ID for testing harnesses. */
112
132
  testId?: string;
113
133
  /**
@@ -152,6 +172,6 @@ export interface DataTableProps<T> {
152
172
  */
153
173
  onSortChange?: (columnId: string | undefined, direction: SortDirection | undefined) => void;
154
174
  }
155
- declare function DataTableInner<T>({ columns, rows, getRowKey, emptyState, loadingState, loading, columnState, onColumnStateChange, className, ariaLabel, testId, onRowClick, isRowClickable, rowClassName, sortColumnId: controlledSortColumnId, sortDirection: controlledSortDirection, onSortChange, }: DataTableProps<T>): import("react").JSX.Element;
175
+ declare function DataTableInner<T>({ columns, rows, getRowKey, emptyState, loadingState, loading, columnState, onColumnStateChange, className, ariaLabel, resizeHandleLabel, resizeValueText, testId, onRowClick, isRowClickable, rowClassName, sortColumnId: controlledSortColumnId, sortDirection: controlledSortDirection, onSortChange, }: DataTableProps<T>): import("react").JSX.Element;
156
176
  export declare const DataTable: typeof DataTableInner;
157
177
  export default DataTable;
@@ -1,8 +1,8 @@
1
1
  /* empty css */
2
- import { memo as e, useCallback as t, useEffect as n, useMemo as r, useRef as i, useState as a } from "react";
3
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
2
+ import { memo as e, useCallback as t, useEffect as n, useLayoutEffect as r, useMemo as i, useRef as a, useState as o } from "react";
3
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
4
4
  //#region src/components/DataTable/DataTable.tsx
5
- var c = [
5
+ var l = [
6
6
  "a",
7
7
  "button",
8
8
  "input",
@@ -15,16 +15,27 @@ var c = [
15
15
  "[role='link']",
16
16
  "[tabindex]:not([tabindex='-1'])"
17
17
  ].join(",");
18
- function l(e, t) {
18
+ function u(e, t) {
19
19
  if (!(e instanceof Element) || !(t instanceof Element)) return !1;
20
- let n = e.closest(c);
20
+ let n = e.closest(l);
21
21
  return n !== null && n !== t && t.contains(n);
22
22
  }
23
- var u = {
23
+ var ee = {
24
24
  widths: {},
25
25
  visibility: {}
26
- };
27
- function d(e, t) {
26
+ }, te = 16, d = 1200;
27
+ function f(e) {
28
+ let t = Math.max(0, e.minWidth ?? 80), n = e.maxWidth ?? d;
29
+ return {
30
+ min: t,
31
+ max: Math.max(t, Number.isFinite(n) ? Math.max(t, n) : d)
32
+ };
33
+ }
34
+ function p(e, t) {
35
+ let { min: n, max: r } = f(e);
36
+ return Math.min(r, Math.max(n, Math.round(t)));
37
+ }
38
+ function ne(e, t) {
28
39
  let n = e.map((e, t) => ({
29
40
  row: e,
30
41
  i: t
@@ -34,7 +45,7 @@ function d(e, t) {
34
45
  return r === 0 ? e.i - n.i : r;
35
46
  }), n.map(({ row: e }) => e);
36
47
  }
37
- function f(e, t) {
48
+ function re(e, t) {
38
49
  let { sortComparator: n } = e;
39
50
  if (n) return (e, r) => n(e, r, t);
40
51
  let r = e.sortValueAccessor;
@@ -47,213 +58,281 @@ function f(e, t) {
47
58
  return o = typeof i == "number" && typeof a == "number" ? i - a : String(i).localeCompare(String(a)), t === "asc" ? o : -o;
48
59
  } : () => 0;
49
60
  }
50
- function p(e, t, n) {
61
+ function m(e, t, n) {
51
62
  if (e.sortable) return e.id === t ? n === "asc" ? "ascending" : "descending" : "none";
52
63
  }
53
- function m({ direction: e }) {
54
- return e === "none" ? /* @__PURE__ */ o("span", {
64
+ function h({ direction: e }) {
65
+ return e === "none" ? /* @__PURE__ */ s("span", {
55
66
  className: "data-table__sort-icon data-table__sort-icon--none",
56
67
  "aria-hidden": "true",
57
- children: /* @__PURE__ */ s("svg", {
68
+ children: /* @__PURE__ */ c("svg", {
58
69
  width: "10",
59
70
  height: "14",
60
71
  viewBox: "0 0 10 14",
61
72
  fill: "none",
62
73
  xmlns: "http://www.w3.org/2000/svg",
63
- children: [/* @__PURE__ */ o("path", {
74
+ children: [/* @__PURE__ */ s("path", {
64
75
  d: "M5 1L1 5H9L5 1Z",
65
76
  fill: "currentColor",
66
77
  opacity: "0.35"
67
- }), /* @__PURE__ */ o("path", {
78
+ }), /* @__PURE__ */ s("path", {
68
79
  d: "M5 13L9 9H1L5 13Z",
69
80
  fill: "currentColor",
70
81
  opacity: "0.35"
71
82
  })]
72
83
  })
73
- }) : e === "asc" ? /* @__PURE__ */ o("span", {
84
+ }) : e === "asc" ? /* @__PURE__ */ s("span", {
74
85
  className: "data-table__sort-icon data-table__sort-icon--asc",
75
86
  "aria-hidden": "true",
76
- children: /* @__PURE__ */ s("svg", {
87
+ children: /* @__PURE__ */ c("svg", {
77
88
  width: "10",
78
89
  height: "14",
79
90
  viewBox: "0 0 10 14",
80
91
  fill: "none",
81
92
  xmlns: "http://www.w3.org/2000/svg",
82
- children: [/* @__PURE__ */ o("path", {
93
+ children: [/* @__PURE__ */ s("path", {
83
94
  d: "M5 1L1 5H9L5 1Z",
84
95
  fill: "currentColor"
85
- }), /* @__PURE__ */ o("path", {
96
+ }), /* @__PURE__ */ s("path", {
86
97
  d: "M5 13L9 9H1L5 13Z",
87
98
  fill: "currentColor",
88
99
  opacity: "0.25"
89
100
  })]
90
101
  })
91
- }) : /* @__PURE__ */ o("span", {
102
+ }) : /* @__PURE__ */ s("span", {
92
103
  className: "data-table__sort-icon data-table__sort-icon--desc",
93
104
  "aria-hidden": "true",
94
- children: /* @__PURE__ */ s("svg", {
105
+ children: /* @__PURE__ */ c("svg", {
95
106
  width: "10",
96
107
  height: "14",
97
108
  viewBox: "0 0 10 14",
98
109
  fill: "none",
99
110
  xmlns: "http://www.w3.org/2000/svg",
100
- children: [/* @__PURE__ */ o("path", {
111
+ children: [/* @__PURE__ */ s("path", {
101
112
  d: "M5 1L1 5H9L5 1Z",
102
113
  fill: "currentColor",
103
114
  opacity: "0.25"
104
- }), /* @__PURE__ */ o("path", {
115
+ }), /* @__PURE__ */ s("path", {
105
116
  d: "M5 13L9 9H1L5 13Z",
106
117
  fill: "currentColor"
107
118
  })]
108
119
  })
109
120
  });
110
121
  }
111
- function h({ columns: e, rows: c, getRowKey: h, emptyState: g, loadingState: _, loading: v = !1, columnState: y, onColumnStateChange: b, className: x = "", ariaLabel: S = "Data table", testId: C, onRowClick: w, isRowClickable: T, rowClassName: E, sortColumnId: D, sortDirection: O, onSortChange: k }) {
112
- let [A, j] = a(() => y ?? u);
122
+ function g({ columns: e, rows: l, getRowKey: d, emptyState: g, loadingState: _, loading: ie = !1, columnState: v, onColumnStateChange: y, className: b = "", ariaLabel: x = "Data table", resizeHandleLabel: S = (e) => `Resize ${e.id} column`, resizeValueText: C = (e) => `${String(e)} pixels`, testId: w, onRowClick: T, isRowClickable: E, rowClassName: D, sortColumnId: O, sortDirection: k, onSortChange: A }) {
123
+ let [j, M] = o(() => v ?? ee);
113
124
  n(() => {
114
- y && j(y);
115
- }, [y]);
116
- let M = D !== void 0 || O !== void 0, [N, P] = a(void 0), [F, I] = a(void 0), L = M ? D : N, R = M ? O : F, z = t((e) => {
125
+ v && M(v);
126
+ }, [v]);
127
+ let N = O !== void 0 || k !== void 0, [ae, oe] = o(void 0), [P, F] = o(void 0), I = N ? O : ae, L = N ? k : P, R = t((e) => {
117
128
  if (!e.sortable) return;
118
129
  let t, n;
119
- L === e.id ? R === (e.defaultSortDirection ?? "asc") ? (t = e.id, n = R === "asc" ? "desc" : "asc") : (t = void 0, n = void 0) : (t = e.id, n = e.defaultSortDirection ?? "asc"), M || (P(t), I(n)), k?.(t, n);
130
+ I === e.id ? L === (e.defaultSortDirection ?? "asc") ? (t = e.id, n = L === "asc" ? "desc" : "asc") : (t = void 0, n = void 0) : (t = e.id, n = e.defaultSortDirection ?? "asc"), N || (oe(t), F(n)), A?.(t, n);
120
131
  }, [
132
+ I,
121
133
  L,
122
- R,
123
- M,
124
- k
125
- ]), B = r(() => e.filter((e) => e.alwaysVisible ? !0 : A.visibility[e.id] !== !1), [e, A.visibility]), V = t((e) => {
126
- let t = A.widths[e.id], n = typeof t == "number" && t > 0 ? t : e.initialWidth;
127
- return n === void 0 ? void 0 : Math.max(n, e.minWidth ?? 0);
128
- }, [A.widths]), H = i(null), U = t((e, t, n) => {
129
- if (t.noResize) return;
130
- e.preventDefault(), e.stopPropagation(), H.current = {
134
+ N,
135
+ A
136
+ ]), z = i(() => e.filter((e) => e.alwaysVisible ? !0 : j.visibility[e.id] !== !1), [e, j.visibility]), B = t((e) => {
137
+ let t = j.widths[e.id], n = typeof t == "number" && t > 0 ? t : e.initialWidth;
138
+ if (n !== void 0) return e.noResize && e.maxWidth === void 0 ? Math.max(n, e.minWidth ?? 0) : p(e, n);
139
+ }, [j.widths]), V = a(/* @__PURE__ */ new Map()), [H, U] = o({}), W = t((e, t) => {
140
+ let n = p(e, t);
141
+ U((t) => t[e.id] === n ? t : {
142
+ ...t,
143
+ [e.id]: n
144
+ });
145
+ }, []), G = t((e, t) => {
146
+ t ? V.current.set(e, t) : V.current.delete(e);
147
+ }, []);
148
+ r(() => {
149
+ let e = (e, t) => {
150
+ let n = t.getBoundingClientRect().width;
151
+ n > 0 && W(e, n);
152
+ };
153
+ for (let t of z) {
154
+ let n = V.current.get(t.id);
155
+ n && e(t, n);
156
+ }
157
+ if (typeof ResizeObserver > "u") return;
158
+ let t = new ResizeObserver((t) => {
159
+ for (let n of t) {
160
+ let t = n.target.dataset.columnId, r = z.find((e) => e.id === t);
161
+ r && e(r, n.target);
162
+ }
163
+ });
164
+ for (let e of z) {
165
+ let n = V.current.get(e.id);
166
+ n && t.observe(n);
167
+ }
168
+ return () => t.disconnect();
169
+ }, [W, z]);
170
+ let K = a(null), q = t((e, t) => {
171
+ let n = p(e, t);
172
+ M((t) => ({
173
+ ...t,
174
+ widths: {
175
+ ...t.widths,
176
+ [e.id]: n
177
+ }
178
+ })), W(e, n);
179
+ }, [W]), J = t((e) => {
180
+ let t = K.current;
181
+ !t || e !== void 0 && t.pointerId !== e || (K.current = null, t.handle.hasPointerCapture?.(t.pointerId) && t.handle.releasePointerCapture?.(t.pointerId));
182
+ }, []);
183
+ n(() => J, [J]);
184
+ let Y = t((e, t) => {
185
+ let n = e.parentElement?.getBoundingClientRect().width ?? 0, r = H[t.id] ?? B(t) ?? f(t).min;
186
+ return p(t, Math.round(n) || r);
187
+ }, [H, B]), se = t((e, t) => {
188
+ if (t.noResize || !e.isPrimary || e.button !== 0 || K.current) return;
189
+ e.preventDefault(), e.stopPropagation();
190
+ let n = e.currentTarget;
191
+ K.current = {
131
192
  columnId: t.id,
132
193
  startX: e.clientX,
133
- startWidth: n,
134
- minWidth: t.minWidth ?? 80
135
- };
136
- let r = (e) => {
137
- let t = H.current;
138
- if (!t) return;
139
- let n = e.clientX - t.startX, r = Math.max(t.minWidth, t.startWidth + n);
140
- j((e) => ({
141
- ...e,
142
- widths: {
143
- ...e.widths,
144
- [t.columnId]: r
145
- }
146
- }));
147
- }, i = () => {
148
- H.current = null, window.removeEventListener("mousemove", r), window.removeEventListener("mouseup", i);
149
- };
150
- window.addEventListener("mousemove", r), window.addEventListener("mouseup", i);
151
- }, []), W = i(A);
194
+ startWidth: Y(n, t),
195
+ minWidth: f(t).min,
196
+ maxWidth: f(t).max,
197
+ pointerId: e.pointerId,
198
+ handle: n
199
+ }, n.setPointerCapture?.(e.pointerId);
200
+ }, [Y]), ce = t((t) => {
201
+ let n = K.current;
202
+ if (!n || n.pointerId !== t.pointerId) return;
203
+ t.preventDefault(), t.stopPropagation();
204
+ let r = Math.max(n.minWidth, Math.min(n.maxWidth, n.startWidth + t.clientX - n.startX)), i = e.find((e) => e.id === n.columnId);
205
+ i && q(i, r);
206
+ }, [e, q]), X = t((e) => {
207
+ K.current?.pointerId === e.pointerId && (e.preventDefault(), e.stopPropagation(), J(e.pointerId));
208
+ }, [J]), le = t((e, t) => {
209
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight" && e.key !== "Home") return;
210
+ e.preventDefault(), e.stopPropagation();
211
+ let { min: n, max: r } = f(t), i = Y(e.currentTarget, t), a = e.key === "Home" ? n : Math.min(r, Math.max(n, i + (e.key === "ArrowRight" ? te : -16)));
212
+ q(t, a);
213
+ }, [Y, q]), Z = a(j);
152
214
  n(() => {
153
- W.current !== A && (W.current = A, b?.(A));
154
- }, [A, b]);
155
- let G = r(() => {
156
- if (!L || !R) return c;
157
- let t = e.find((e) => e.id === L);
158
- return t?.sortable ? d(c, f(t, R)) : c;
215
+ Z.current !== j && (Z.current = j, y?.(j));
216
+ }, [j, y]);
217
+ let Q = i(() => {
218
+ if (!I || !L) return l;
219
+ let t = e.find((e) => e.id === I);
220
+ return t?.sortable ? ne(l, re(t, L)) : l;
159
221
  }, [
160
- c,
222
+ l,
161
223
  e,
162
- L,
163
- R
164
- ]), K = ["data-table", x].filter(Boolean).join(" ");
165
- return /* @__PURE__ */ o("div", {
166
- className: K,
167
- "data-testid": C,
168
- children: /* @__PURE__ */ s("table", {
224
+ I,
225
+ L
226
+ ]), $ = ["data-table", b].filter(Boolean).join(" ");
227
+ return /* @__PURE__ */ s("div", {
228
+ className: $,
229
+ "data-testid": w,
230
+ children: /* @__PURE__ */ c("table", {
169
231
  className: "data-table__table",
170
- "aria-label": S,
232
+ "aria-label": x,
171
233
  role: "table",
172
- children: [/* @__PURE__ */ o("thead", {
234
+ children: [/* @__PURE__ */ s("thead", {
173
235
  className: "data-table__head",
174
- children: /* @__PURE__ */ o("tr", {
236
+ children: /* @__PURE__ */ s("tr", {
175
237
  className: "data-table__row data-table__row--head",
176
- children: B.map((e) => {
177
- let t = V(e), n = e.sortable && e.id === L;
178
- return /* @__PURE__ */ s("th", {
238
+ children: z.map((e) => {
239
+ let t = B(e), n = p(e, H[e.id] ?? t ?? f(e).min), r = f(e), i = e.sortable && e.id === I;
240
+ return /* @__PURE__ */ c("th", {
241
+ ref: (t) => G(e.id, t),
242
+ "data-column-id": e.id,
179
243
  scope: "col",
180
244
  className: [
181
245
  "data-table__cell",
182
246
  "data-table__cell--head",
247
+ !e.noResize && "data-table__cell--resizable",
183
248
  e.sortable && "data-table__cell--sortable",
184
- n && "data-table__cell--sort-active",
249
+ i && "data-table__cell--sort-active",
185
250
  e.className,
186
251
  e.align && `data-table__cell--align-${e.align}`
187
252
  ].filter(Boolean).join(" "),
188
253
  style: {
189
254
  width: t === void 0 ? void 0 : `${String(t)}px`,
190
- minWidth: e.minWidth
255
+ minWidth: e.minWidth,
256
+ maxWidth: e.noResize ? e.maxWidth : r.max
191
257
  },
192
- "aria-sort": p(e, L, R),
258
+ "aria-sort": m(e, I, L),
193
259
  onClick: e.sortable ? () => {
194
- z(e);
260
+ R(e);
195
261
  } : void 0,
196
262
  onKeyDown: e.sortable ? (t) => {
197
- (t.key === "Enter" || t.key === " ") && (t.preventDefault(), z(e));
263
+ (t.key === "Enter" || t.key === " ") && (t.preventDefault(), R(e));
198
264
  } : void 0,
199
265
  tabIndex: e.sortable ? 0 : void 0,
200
266
  children: [
201
- /* @__PURE__ */ o("span", {
267
+ /* @__PURE__ */ s("span", {
202
268
  className: "data-table__head-label",
203
269
  children: e.header
204
270
  }),
205
- e.sortable && /* @__PURE__ */ o(m, { direction: n && R ? R : "none" }),
206
- !e.noResize && /* @__PURE__ */ o("div", {
271
+ e.sortable && /* @__PURE__ */ s(h, { direction: i && L ? L : "none" }),
272
+ !e.noResize && /* @__PURE__ */ s("div", {
207
273
  role: "separator",
208
274
  "aria-orientation": "vertical",
275
+ "aria-label": S(e),
276
+ "aria-valuemin": r.min,
277
+ "aria-valuemax": r.max,
278
+ "aria-valuenow": n,
279
+ "aria-valuetext": C(n, e),
280
+ tabIndex: 0,
209
281
  className: "data-table__resize-handle",
210
- onMouseDown: (n) => {
211
- U(n, e, t ?? e.minWidth ?? 120);
212
- }
282
+ onClick: (e) => e.stopPropagation(),
283
+ onPointerDown: (t) => se(t, e),
284
+ onPointerMove: ce,
285
+ onPointerUp: X,
286
+ onPointerCancel: X,
287
+ onLostPointerCapture: (e) => J(e.pointerId),
288
+ onKeyDown: (t) => le(t, e)
213
289
  })
214
290
  ]
215
291
  }, e.id);
216
292
  })
217
293
  })
218
- }), /* @__PURE__ */ o("tbody", {
294
+ }), /* @__PURE__ */ s("tbody", {
219
295
  className: "data-table__body",
220
- children: v ? /* @__PURE__ */ o("tr", {
296
+ children: ie ? /* @__PURE__ */ s("tr", {
221
297
  className: "data-table__row data-table__row--state",
222
- children: /* @__PURE__ */ o("td", {
223
- colSpan: B.length || 1,
298
+ children: /* @__PURE__ */ s("td", {
299
+ colSpan: z.length || 1,
224
300
  className: "data-table__state-cell",
225
- children: _ ?? /* @__PURE__ */ o("div", { className: "data-table__loading" })
301
+ children: _ ?? /* @__PURE__ */ s("div", { className: "data-table__loading" })
226
302
  })
227
- }) : G.length === 0 ? /* @__PURE__ */ o("tr", {
303
+ }) : Q.length === 0 ? /* @__PURE__ */ s("tr", {
228
304
  className: "data-table__row data-table__row--state",
229
- children: /* @__PURE__ */ o("td", {
230
- colSpan: B.length || 1,
305
+ children: /* @__PURE__ */ s("td", {
306
+ colSpan: z.length || 1,
231
307
  className: "data-table__state-cell",
232
308
  children: g
233
309
  })
234
- }) : G.map((e, t) => {
235
- let n = h(e, t), r = !!(w && (T?.(e, t) ?? !0)), i = E?.(e, t);
236
- return /* @__PURE__ */ o("tr", {
310
+ }) : Q.map((e, t) => {
311
+ let n = d(e, t), r = !!(T && (E?.(e, t) ?? !0)), i = D?.(e, t);
312
+ return /* @__PURE__ */ s("tr", {
237
313
  className: [
238
314
  "data-table__row",
239
315
  r && "data-table__row--clickable",
240
316
  i
241
317
  ].filter(Boolean).join(" "),
242
318
  onClick: r ? (t) => {
243
- l(t.target, t.currentTarget) || w?.(e);
319
+ u(t.target, t.currentTarget) || T?.(e);
244
320
  } : void 0,
245
321
  onKeyDown: r ? (t) => {
246
- (t.key === "Enter" || t.key === " ") && (l(t.target, t.currentTarget) || (t.preventDefault(), w?.(e)));
322
+ (t.key === "Enter" || t.key === " ") && (u(t.target, t.currentTarget) || (t.preventDefault(), T?.(e)));
247
323
  } : void 0,
248
324
  tabIndex: r ? 0 : void 0,
249
325
  role: r ? "button" : void 0,
250
- children: B.map((n) => /* @__PURE__ */ o("td", {
326
+ children: z.map((n) => /* @__PURE__ */ s("td", {
251
327
  className: [
252
328
  "data-table__cell",
253
329
  n.className,
254
330
  n.align && `data-table__cell--align-${n.align}`
255
331
  ].filter(Boolean).join(" "),
256
- style: { minWidth: n.minWidth },
332
+ style: {
333
+ minWidth: n.minWidth,
334
+ maxWidth: n.noResize ? n.maxWidth : f(n).max
335
+ },
257
336
  children: n.render(e, t)
258
337
  }, n.id))
259
338
  }, n);
@@ -262,9 +341,9 @@ function h({ columns: e, rows: c, getRowKey: h, emptyState: g, loadingState: _,
262
341
  })
263
342
  });
264
343
  }
265
- var g = e(h);
344
+ var _ = e(g);
266
345
  //#endregion
267
- export { g as DataTable, g as default };
346
+ export { _ as DataTable, _ as default };
268
347
 
269
348
  //# sourceMappingURL=DataTable2.js.map
270
349
  import "../../assets/components/DataTable/DataTable.css";
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable2.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * DataTable Component (epic #2730 / issue #2738 · sort: epic #2859 / issue #2861)\n *\n * Generic, type-safe table primitive modeled after the Backend.AI WebUI\n * `BAITable`. Designed so future menus (Models, Sessions, Data, ...)\n * can share one tabular surface instead of inventing a new one each\n * time.\n *\n * Features (V1 — additional capabilities planned for follow-up issues):\n * - Type-safe column definitions with custom cell renderers\n * - Optional column-visibility persistence via `localStorage` when\n * `persistKey` is supplied\n * - Optional resizable columns (drag the right edge of a header cell)\n * with widths persisted alongside visibility\n * - Empty-state slot rendered when `rows` is empty\n * - Loading-state slot rendered when `loading` is `true`\n * - Stable `getRowKey` accessor so rows with the same identifier remain\n * referentially identical across event-driven updates\n *\n * Sorting (V2 — epic #2859 / issue #2861):\n * - Optional per-column sortable flag with sort value accessor or\n * custom comparator\n * - Optional defaultSortDirection per column\n * - Controlled (`sortColumnId` + `sortDirection` + `onSortChange`) and\n * uncontrolled (internal state) modes\n * - Accessible `aria-sort` attributes on header cells\n * - Keyboard activation (Enter / Space) for sortable headers\n * - Stable sort — equal-valued rows retain their original order\n * - Sorting never mutates the original `rows` prop\n *\n * The component is intentionally domain-agnostic — it MUST NOT carry\n * Session, Model, or Agent-specific knowledge. The first consumer is\n * `SessionListTable` (epic #2730 / #2738) but the API is generic.\n */\n\nimport {\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport \"./DataTable.css\";\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/** Direction a column is currently sorted. */\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableColumn<T> {\n /** Unique identifier for the column. Used for visibility + width persistence. */\n id: string;\n /** Header label. */\n header: ReactNode;\n /**\n * Cell renderer. Receives the row and returns a React node. Pure\n * functions are encouraged so memoization stays effective.\n */\n render: (row: T, index: number) => ReactNode;\n /** Optional minimum column width in pixels. */\n minWidth?: number;\n /** Optional initial width in pixels (only honored on the first paint). */\n initialWidth?: number;\n /** When `true`, the column cannot be resized. Defaults to `false`. */\n noResize?: boolean;\n /** When `true`, the column cannot be hidden by the user. Defaults to `false`. */\n alwaysVisible?: boolean;\n /** Extra `class` for the column's `<th>` and `<td>` cells. */\n className?: string;\n /**\n * When provided, the value is exposed via the `align` style on the\n * column's cells. Useful for right-aligned numeric columns.\n */\n align?: \"left\" | \"right\" | \"center\";\n\n // ---- Sorting API (added epic #2859 / issue #2861) -------------------------\n\n /**\n * When `true`, the column header renders a sort affordance and clicking\n * (or pressing Enter / Space) cycles through ascending → descending →\n * unsorted states. Defaults to `false`.\n */\n sortable?: boolean;\n /**\n * Extracts a primitive comparable value from a row for sorting purposes.\n * Used when the default ascending/descending behavior is sufficient.\n * Mutually exclusive with `sortComparator` — if both are provided,\n * `sortComparator` takes precedence.\n *\n * @example\n * ```tsx\n * sortValueAccessor: (row) => row.name.toLowerCase()\n * ```\n */\n sortValueAccessor?: (row: T) => string | number | boolean | null | undefined;\n /**\n * Custom comparator used for sorting. Receives two rows and the active\n * sort direction. Return a negative number if `a` should come first, a\n * positive number if `b` should come first, or `0` if they are equal.\n * The table applies a stable fallback on `0` to preserve original order.\n *\n * @example\n * ```tsx\n * sortComparator: (a, b, dir) =>\n * dir === \"asc\"\n * ? a.createdAt - b.createdAt\n * : b.createdAt - a.createdAt\n * ```\n */\n sortComparator?: (a: T, b: T, direction: SortDirection) => number;\n /**\n * The sort direction applied when this column is first activated.\n * Defaults to `\"asc\"`.\n */\n defaultSortDirection?: SortDirection;\n}\n\nexport interface DataTablePersistedState {\n /** column id → user-overridden width in pixels. */\n widths: Record<string, number>;\n /** column id → visibility flag. Missing keys count as visible. */\n visibility: Record<string, boolean>;\n}\n\n/**\n * The controls a row can contain that are actions in their own right. A click\n * or an Enter on one of these is that control's business, not the row's.\n */\nconst INTERACTIVE_DESCENDANT_SELECTOR = [\n \"a\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"label\",\n \"[contenteditable='true']\",\n \"[role='button']\",\n \"[role='link']\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Whether an event inside a clickable row came from a control nested in it.\n *\n * A row that is itself clickable and contains an Edit button has two actions\n * competing for one click. Without this, both fire: the button edits and the\n * row also navigates away from the thing just edited. The row is the fallback,\n * so anything more specific wins.\n */\nfunction fromInteractiveDescendant(\n target: EventTarget | null,\n currentTarget: EventTarget,\n): boolean {\n if (!(target instanceof Element) || !(currentTarget instanceof Element)) {\n return false;\n }\n const control = target.closest(INTERACTIVE_DESCENDANT_SELECTOR);\n return (\n control !== null && control !== currentTarget && currentTarget.contains(control)\n );\n}\n\nexport interface DataTableProps<T> {\n /** Column definitions, in display order. */\n columns: DataTableColumn<T>[];\n /** Row data. */\n rows: T[];\n /**\n * Stable identifier accessor — used as the React key for each row.\n * Must be deterministic for the lifetime of the row.\n */\n getRowKey: (row: T, index: number) => string;\n /** Render this node when `rows` is empty. */\n emptyState?: ReactNode;\n /** Render this node when `loading` is `true`. */\n loadingState?: ReactNode;\n /** When `true`, the loading slot replaces the table body. */\n loading?: boolean;\n /**\n * Column widths and visibility, controlled by the caller.\n *\n * This component used to take a `persistKey` and write to `localStorage`\n * itself. Storage is a policy decision that belongs to the consumer: where\n * it goes, under which key, whether it is per user or per workspace, and\n * whether it exists at all in a host that has no `localStorage`. A\n * rendering component that answers those questions on its own cannot be\n * reused by a consumer that answers them differently.\n */\n columnState?: DataTablePersistedState;\n /**\n * Called whenever the user resizes a column or toggles its visibility.\n * Pair it with `columnState` to persist wherever the consumer keeps\n * preferences; omit both to get a table that forgets on unmount.\n */\n onColumnStateChange?: (state: DataTablePersistedState) => void;\n /** Extra class for the table's wrapping element. */\n className?: string;\n /** ARIA label for the table. Defaults to \"Data table\". */\n ariaLabel?: string;\n /** Test ID for testing harnesses. */\n testId?: string;\n /**\n * Callback invoked when a row is clicked. When provided, rows render\n * with a clickable affordance (cursor: pointer + keyboard binding).\n */\n onRowClick?: (row: T) => void;\n /**\n * Narrows `onRowClick` to the rows that can actually act on it, for a table\n * that mixes actionable and informational rows. A row this returns `false`\n * for gets no pointer affordance, no tab stop, and no keyboard binding, so it\n * is not announced as a button it cannot be.\n */\n isRowClickable?: (row: T, index: number) => boolean;\n /**\n * Extra class for one row, for flagging a specific row without forking the\n * table: a transient deep-link highlight, a stale entry, a row being removed.\n * A falsy return adds nothing.\n */\n rowClassName?: (row: T, index: number) => string | undefined | false | null;\n\n // ---- Sorting props (added epic #2859 / issue #2861) -----------------------\n\n /**\n * Controlled — the column currently being sorted. Pass `undefined` to\n * indicate no active sort. When this prop is present, the component\n * operates in controlled mode: the caller is responsible for updating\n * the sort state via `onSortChange`.\n */\n sortColumnId?: string;\n /**\n * Controlled — the direction of the active sort. Ignored when\n * `sortColumnId` is `undefined`.\n */\n sortDirection?: SortDirection;\n /**\n * Called when the user activates a sortable column header. In\n * controlled mode the caller must propagate this back as\n * `sortColumnId` / `sortDirection`. In uncontrolled mode this prop is\n * purely observational.\n *\n * @param columnId The column being sorted, or `undefined` when the\n * sort is cleared.\n * @param direction The new direction.\n */\n onSortChange?: (\n columnId: string | undefined,\n direction: SortDirection | undefined,\n ) => void;\n}\n\nconst EMPTY_COLUMN_STATE: DataTablePersistedState = { widths: {}, visibility: {} };\n\n// ============================================================================\n// Sorting helpers\n// ============================================================================\n\n/**\n * Stable sort — rows with equal comparison result keep their original\n * relative order. We achieve stability by augmenting each row with its\n * original index and using that as a tie-breaker.\n *\n * The input array is never mutated.\n */\nfunction stableSort<T>(rows: T[], comparator: (a: T, b: T) => number): T[] {\n const indexed = rows.map((row, i) => ({ row, i }));\n indexed.sort((a, b) => {\n const result = comparator(a.row, b.row);\n return result !== 0 ? result : a.i - b.i;\n });\n return indexed.map(({ row }) => row);\n}\n\n/**\n * Builds a comparator from a `DataTableColumn` + active sort direction.\n * Falls back to string comparison when neither `sortComparator` nor\n * `sortValueAccessor` is provided on the column.\n */\nfunction buildComparator<T>(\n col: DataTableColumn<T>,\n direction: SortDirection,\n): (a: T, b: T) => number {\n const { sortComparator } = col;\n if (sortComparator) {\n return (a, b) => sortComparator(a, b, direction);\n }\n const accessor = col.sortValueAccessor;\n if (accessor) {\n return (a, b) => {\n const av = accessor(a);\n const bv = accessor(b);\n // Nullish values sort last regardless of direction.\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n let cmp: number;\n if (typeof av === \"number\" && typeof bv === \"number\") {\n cmp = av - bv;\n } else {\n cmp = String(av).localeCompare(String(bv));\n }\n return direction === \"asc\" ? cmp : -cmp;\n };\n }\n // No accessor — unsortable in practice (column should not be sortable).\n return () => 0;\n}\n\n/**\n * Returns the `aria-sort` attribute value for a column header cell.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort\n */\nfunction ariaSortValue(\n col: DataTableColumn<unknown>,\n activeSortColumnId: string | undefined,\n activeSortDirection: SortDirection | undefined,\n): \"ascending\" | \"descending\" | \"none\" | undefined {\n if (!col.sortable) return undefined;\n if (col.id === activeSortColumnId) {\n return activeSortDirection === \"asc\" ? \"ascending\" : \"descending\";\n }\n return \"none\";\n}\n\n// Sort indicator icons rendered as inline SVG so there is no external\n// asset dependency. They intentionally reuse design-token colours so they\n// automatically adapt to light / dark themes.\n\nfunction SortIcon({ direction }: { direction: SortDirection | \"none\" }) {\n if (direction === \"none\") {\n // Neutral chevron stack — both arrows in muted colour\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--none\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.35\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.35\" />\n </svg>\n </span>\n );\n }\n if (direction === \"asc\") {\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--asc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.25\" />\n </svg>\n </span>\n );\n }\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--desc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.25\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" />\n </svg>\n </span>\n );\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nfunction DataTableInner<T>({\n columns,\n rows,\n getRowKey,\n emptyState,\n loadingState,\n loading = false,\n columnState,\n onColumnStateChange,\n className = \"\",\n ariaLabel = \"Data table\",\n testId,\n onRowClick,\n isRowClickable,\n rowClassName,\n // Sorting (controlled)\n sortColumnId: controlledSortColumnId,\n sortDirection: controlledSortDirection,\n onSortChange,\n}: DataTableProps<T>) {\n // ---- Column state (widths + visibility) ----------------------------------\n //\n // Held internally so a caller that does not care about persistence gets a\n // working table, and re-seeded whenever the caller supplies a new one.\n const [persisted, setPersisted] = useState<DataTablePersistedState>(\n () => columnState ?? EMPTY_COLUMN_STATE,\n );\n\n useEffect(() => {\n if (columnState) setPersisted(columnState);\n }, [columnState]);\n\n // ---- Sorting state (uncontrolled fallback) -------------------------------\n //\n // Controlled mode is detected by the presence of `sortColumnId` as a prop\n // (the caller explicitly manages the sort column). Using only this prop as\n // the signal (rather than OR-ing with `sortDirection`) avoids a\n // pathological state where `sortDirection` is set but `sortColumnId` is\n // not, which would enter controlled mode with no sort effect.\n //\n // Note: TypeScript cannot distinguish \"prop not passed\" from \"prop passed\n // as undefined\", so we rely on the convention that controlled callers\n // always pass both `sortColumnId` and `sortDirection` together.\n const isControlled =\n controlledSortColumnId !== undefined || controlledSortDirection !== undefined;\n\n const [internalSortColumnId, setInternalSortColumnId] = useState<string | undefined>(\n undefined,\n );\n const [internalSortDirection, setInternalSortDirection] = useState<\n SortDirection | undefined\n >(undefined);\n\n const activeSortColumnId = isControlled\n ? controlledSortColumnId\n : internalSortColumnId;\n const activeSortDirection = isControlled\n ? controlledSortDirection\n : internalSortDirection;\n\n /** Handle a click / keypress on a sortable column header. */\n const handleSortToggle = useCallback(\n (col: DataTableColumn<T>) => {\n if (!col.sortable) return;\n\n let nextColumnId: string | undefined;\n let nextDirection: SortDirection | undefined;\n\n if (activeSortColumnId !== col.id) {\n // Activate this column in its default direction.\n nextColumnId = col.id;\n nextDirection = col.defaultSortDirection ?? \"asc\";\n } else if (activeSortDirection === (col.defaultSortDirection ?? \"asc\")) {\n // Flip to the opposite direction.\n nextColumnId = col.id;\n nextDirection = activeSortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n // Already in the non-default direction — clear the sort.\n nextColumnId = undefined;\n nextDirection = undefined;\n }\n\n if (!isControlled) {\n setInternalSortColumnId(nextColumnId);\n setInternalSortDirection(nextDirection);\n }\n onSortChange?.(nextColumnId, nextDirection);\n },\n [activeSortColumnId, activeSortDirection, isControlled, onSortChange],\n );\n\n // ---- Visibility computation (column always-visible wins) -----------------\n const visibleColumns = useMemo(\n () =>\n columns.filter((col) => {\n if (col.alwaysVisible) return true;\n const flag = persisted.visibility[col.id];\n return flag !== false;\n }),\n [columns, persisted.visibility],\n );\n\n // ---- Width resolution ----------------------------------------------------\n const resolveWidth = useCallback(\n (col: DataTableColumn<T>): number | undefined => {\n const persistedWidth = persisted.widths[col.id];\n const width =\n typeof persistedWidth === \"number\" && persistedWidth > 0\n ? persistedWidth\n : col.initialWidth;\n return width === undefined ? undefined : Math.max(width, col.minWidth ?? 0);\n },\n [persisted.widths],\n );\n\n // ---- Resize handling -----------------------------------------------------\n const resizeRef = useRef<{\n columnId: string;\n startX: number;\n startWidth: number;\n minWidth: number;\n } | null>(null);\n\n const beginResize = useCallback(\n (\n ev: React.MouseEvent<HTMLDivElement>,\n col: DataTableColumn<T>,\n currentWidth: number,\n ) => {\n if (col.noResize) return;\n ev.preventDefault();\n ev.stopPropagation();\n resizeRef.current = {\n columnId: col.id,\n startX: ev.clientX,\n startWidth: currentWidth,\n minWidth: col.minWidth ?? 80,\n };\n\n const onMove = (e: MouseEvent) => {\n const ref = resizeRef.current;\n if (!ref) return;\n const delta = e.clientX - ref.startX;\n const next = Math.max(ref.minWidth, ref.startWidth + delta);\n setPersisted((prev) => ({\n ...prev,\n widths: { ...prev.widths, [ref.columnId]: next },\n }));\n };\n const onUp = () => {\n resizeRef.current = null;\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n };\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp);\n },\n [],\n );\n\n // Report changes so the caller can persist them. Skipped while `persisted`\n // still holds what the caller last handed in, so echoing the callback back\n // through `columnState` does not loop.\n const reported = useRef(persisted);\n useEffect(() => {\n if (reported.current === persisted) return;\n reported.current = persisted;\n onColumnStateChange?.(persisted);\n }, [persisted, onColumnStateChange]);\n\n // ---- Sorted rows --------------------------------------------------------\n const sortedRows = useMemo(() => {\n if (!activeSortColumnId || !activeSortDirection) return rows;\n const col = columns.find((c) => c.id === activeSortColumnId);\n if (!col?.sortable) return rows;\n return stableSort(rows, buildComparator(col, activeSortDirection));\n }, [rows, columns, activeSortColumnId, activeSortDirection]);\n\n // ---- Body rendering ------------------------------------------------------\n const composedClassName = [\"data-table\", className].filter(Boolean).join(\" \");\n\n const renderBody = () => {\n if (loading) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {loadingState ?? <div className=\"data-table__loading\" />}\n </td>\n </tr>\n );\n }\n if (sortedRows.length === 0) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {emptyState}\n </td>\n </tr>\n );\n }\n return sortedRows.map((row, index) => {\n const key = getRowKey(row, index);\n // A row is interactive only if the table has a handler AND this row is\n // not excluded, so the affordance, the tab stop and the role move\n // together. Splitting them would announce a button that does nothing.\n const clickable = Boolean(onRowClick && (isRowClickable?.(row, index) ?? true));\n const extraClass = rowClassName?.(row, index);\n return (\n <tr\n key={key}\n className={[\n \"data-table__row\",\n clickable && \"data-table__row--clickable\",\n extraClass,\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={\n clickable\n ? (e) => {\n if (fromInteractiveDescendant(e.target, e.currentTarget)) return;\n onRowClick?.(row);\n }\n : undefined\n }\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n if (fromInteractiveDescendant(e.target, e.currentTarget)) return;\n e.preventDefault();\n onRowClick?.(row);\n }\n : undefined\n }\n tabIndex={clickable ? 0 : undefined}\n role={clickable ? \"button\" : undefined}\n >\n {visibleColumns.map((col) => (\n <td\n key={col.id}\n className={[\n \"data-table__cell\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={{ minWidth: col.minWidth }}\n >\n {col.render(row, index)}\n </td>\n ))}\n </tr>\n );\n });\n };\n\n return (\n <div className={composedClassName} data-testid={testId}>\n <table className=\"data-table__table\" aria-label={ariaLabel} role=\"table\">\n <thead className=\"data-table__head\">\n <tr className=\"data-table__row data-table__row--head\">\n {visibleColumns.map((col) => {\n const width = resolveWidth(col);\n const isSortActive = col.sortable && col.id === activeSortColumnId;\n\n return (\n <th\n key={col.id}\n scope=\"col\"\n className={[\n \"data-table__cell\",\n \"data-table__cell--head\",\n col.sortable && \"data-table__cell--sortable\",\n isSortActive && \"data-table__cell--sort-active\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={{\n width: width === undefined ? undefined : `${String(width)}px`,\n minWidth: col.minWidth,\n }}\n aria-sort={ariaSortValue(\n col as DataTableColumn<unknown>,\n activeSortColumnId,\n activeSortDirection,\n )}\n onClick={\n col.sortable\n ? () => {\n handleSortToggle(col);\n }\n : undefined\n }\n onKeyDown={\n col.sortable\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleSortToggle(col);\n }\n }\n : undefined\n }\n tabIndex={col.sortable ? 0 : undefined}\n >\n <span className=\"data-table__head-label\">{col.header}</span>\n {col.sortable && (\n <SortIcon\n direction={\n isSortActive && activeSortDirection\n ? activeSortDirection\n : \"none\"\n }\n />\n )}\n {!col.noResize && (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n className=\"data-table__resize-handle\"\n onMouseDown={(e) => {\n beginResize(e, col, width ?? col.minWidth ?? 120);\n }}\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody className=\"data-table__body\">{renderBody()}</tbody>\n </table>\n </div>\n );\n}\n\n// Memoize while preserving the generic signature.\nexport const DataTable = memo(DataTableInner) as typeof DataTableInner;\n\nexport default DataTable;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable2.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * DataTable Component (epic #2730 / issue #2738 · sort: epic #2859 / issue #2861)\n *\n * Generic, type-safe table primitive modeled after the Backend.AI WebUI\n * `BAITable`. Designed so future menus (Models, Sessions, Data, ...)\n * can share one tabular surface instead of inventing a new one each\n * time.\n *\n * Features (V1 — additional capabilities planned for follow-up issues):\n * - Type-safe column definitions with custom cell renderers\n * - Optional column-visibility persistence via `localStorage` when\n * `persistKey` is supplied\n * - Optional resizable columns (drag the right edge of a header cell)\n * with widths persisted alongside visibility\n * - Empty-state slot rendered when `rows` is empty\n * - Loading-state slot rendered when `loading` is `true`\n * - Stable `getRowKey` accessor so rows with the same identifier remain\n * referentially identical across event-driven updates\n *\n * Sorting (V2 — epic #2859 / issue #2861):\n * - Optional per-column sortable flag with sort value accessor or\n * custom comparator\n * - Optional defaultSortDirection per column\n * - Controlled (`sortColumnId` + `sortDirection` + `onSortChange`) and\n * uncontrolled (internal state) modes\n * - Accessible `aria-sort` attributes on header cells\n * - Keyboard activation (Enter / Space) for sortable headers\n * - Stable sort — equal-valued rows retain their original order\n * - Sorting never mutates the original `rows` prop\n *\n * The component is intentionally domain-agnostic — it MUST NOT carry\n * Session, Model, or Agent-specific knowledge. The first consumer is\n * `SessionListTable` (epic #2730 / #2738) but the API is generic.\n */\n\nimport {\n memo,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from \"react\";\nimport \"./DataTable.css\";\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/** Direction a column is currently sorted. */\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableColumn<T> {\n /** Unique identifier for the column. Used for visibility + width persistence. */\n id: string;\n /** Header label. */\n header: ReactNode;\n /**\n * Cell renderer. Receives the row and returns a React node. Pure\n * functions are encouraged so memoization stays effective.\n */\n render: (row: T, index: number) => ReactNode;\n /** Optional minimum column width in pixels. */\n minWidth?: number;\n /**\n * Optional maximum column width in pixels. Values above this limit are\n * clamped for pointer and keyboard resizing. Resizable columns default to\n * 1200 pixels so every separator exposes a finite ARIA maximum. A\n * `noResize` column keeps its existing unbounded width unless it explicitly\n * supplies this property.\n */\n maxWidth?: number;\n /** Optional initial width in pixels (only honored on the first paint). */\n initialWidth?: number;\n /** When `true`, the column cannot be resized. Defaults to `false`. */\n noResize?: boolean;\n /** When `true`, the column cannot be hidden by the user. Defaults to `false`. */\n alwaysVisible?: boolean;\n /** Extra `class` for the column's `<th>` and `<td>` cells. */\n className?: string;\n /**\n * When provided, the value is exposed via the `align` style on the\n * column's cells. Useful for right-aligned numeric columns.\n */\n align?: \"left\" | \"right\" | \"center\";\n\n // ---- Sorting API (added epic #2859 / issue #2861) -------------------------\n\n /**\n * When `true`, the column header renders a sort affordance and clicking\n * (or pressing Enter / Space) cycles through ascending → descending →\n * unsorted states. Defaults to `false`.\n */\n sortable?: boolean;\n /**\n * Extracts a primitive comparable value from a row for sorting purposes.\n * Used when the default ascending/descending behavior is sufficient.\n * Mutually exclusive with `sortComparator` — if both are provided,\n * `sortComparator` takes precedence.\n *\n * @example\n * ```tsx\n * sortValueAccessor: (row) => row.name.toLowerCase()\n * ```\n */\n sortValueAccessor?: (row: T) => string | number | boolean | null | undefined;\n /**\n * Custom comparator used for sorting. Receives two rows and the active\n * sort direction. Return a negative number if `a` should come first, a\n * positive number if `b` should come first, or `0` if they are equal.\n * The table applies a stable fallback on `0` to preserve original order.\n *\n * @example\n * ```tsx\n * sortComparator: (a, b, dir) =>\n * dir === \"asc\"\n * ? a.createdAt - b.createdAt\n * : b.createdAt - a.createdAt\n * ```\n */\n sortComparator?: (a: T, b: T, direction: SortDirection) => number;\n /**\n * The sort direction applied when this column is first activated.\n * Defaults to `\"asc\"`.\n */\n defaultSortDirection?: SortDirection;\n}\n\nexport interface DataTablePersistedState {\n /** column id → user-overridden width in pixels. */\n widths: Record<string, number>;\n /** column id → visibility flag. Missing keys count as visible. */\n visibility: Record<string, boolean>;\n}\n\n/**\n * The controls a row can contain that are actions in their own right. A click\n * or an Enter on one of these is that control's business, not the row's.\n */\nconst INTERACTIVE_DESCENDANT_SELECTOR = [\n \"a\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"label\",\n \"[contenteditable='true']\",\n \"[role='button']\",\n \"[role='link']\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Whether an event inside a clickable row came from a control nested in it.\n *\n * A row that is itself clickable and contains an Edit button has two actions\n * competing for one click. Without this, both fire: the button edits and the\n * row also navigates away from the thing just edited. The row is the fallback,\n * so anything more specific wins.\n */\nfunction fromInteractiveDescendant(\n target: EventTarget | null,\n currentTarget: EventTarget,\n): boolean {\n if (!(target instanceof Element) || !(currentTarget instanceof Element)) {\n return false;\n }\n const control = target.closest(INTERACTIVE_DESCENDANT_SELECTOR);\n return (\n control !== null && control !== currentTarget && currentTarget.contains(control)\n );\n}\n\nexport interface DataTableProps<T> {\n /** Column definitions, in display order. */\n columns: DataTableColumn<T>[];\n /** Row data. */\n rows: T[];\n /**\n * Stable identifier accessor — used as the React key for each row.\n * Must be deterministic for the lifetime of the row.\n */\n getRowKey: (row: T, index: number) => string;\n /** Render this node when `rows` is empty. */\n emptyState?: ReactNode;\n /** Render this node when `loading` is `true`. */\n loadingState?: ReactNode;\n /** When `true`, the loading slot replaces the table body. */\n loading?: boolean;\n /**\n * Column widths and visibility, controlled by the caller.\n *\n * This component used to take a `persistKey` and write to `localStorage`\n * itself. Storage is a policy decision that belongs to the consumer: where\n * it goes, under which key, whether it is per user or per workspace, and\n * whether it exists at all in a host that has no `localStorage`. A\n * rendering component that answers those questions on its own cannot be\n * reused by a consumer that answers them differently.\n */\n columnState?: DataTablePersistedState;\n /**\n * Called whenever the user resizes a column or toggles its visibility.\n * Pair it with `columnState` to persist wherever the consumer keeps\n * preferences; omit both to get a table that forgets on unmount.\n */\n onColumnStateChange?: (state: DataTablePersistedState) => void;\n /** Extra class for the table's wrapping element. */\n className?: string;\n /** ARIA label for the table. Defaults to \"Data table\". */\n ariaLabel?: string;\n /**\n * Produces the accessible name for a resizable column's separator. The\n * default is `Resize <column id> column`; pass a product's translated label\n * when column ids are not suitable for people to hear.\n */\n resizeHandleLabel?: (column: DataTableColumn<T>) => string;\n /**\n * Produces the human-readable current width announced by a resize\n * separator. The default is `<width> pixels`; pass a translated formatter\n * when the host localizes measurements.\n */\n resizeValueText?: (width: number, column: DataTableColumn<T>) => string;\n /** Test ID for testing harnesses. */\n testId?: string;\n /**\n * Callback invoked when a row is clicked. When provided, rows render\n * with a clickable affordance (cursor: pointer + keyboard binding).\n */\n onRowClick?: (row: T) => void;\n /**\n * Narrows `onRowClick` to the rows that can actually act on it, for a table\n * that mixes actionable and informational rows. A row this returns `false`\n * for gets no pointer affordance, no tab stop, and no keyboard binding, so it\n * is not announced as a button it cannot be.\n */\n isRowClickable?: (row: T, index: number) => boolean;\n /**\n * Extra class for one row, for flagging a specific row without forking the\n * table: a transient deep-link highlight, a stale entry, a row being removed.\n * A falsy return adds nothing.\n */\n rowClassName?: (row: T, index: number) => string | undefined | false | null;\n\n // ---- Sorting props (added epic #2859 / issue #2861) -----------------------\n\n /**\n * Controlled — the column currently being sorted. Pass `undefined` to\n * indicate no active sort. When this prop is present, the component\n * operates in controlled mode: the caller is responsible for updating\n * the sort state via `onSortChange`.\n */\n sortColumnId?: string;\n /**\n * Controlled — the direction of the active sort. Ignored when\n * `sortColumnId` is `undefined`.\n */\n sortDirection?: SortDirection;\n /**\n * Called when the user activates a sortable column header. In\n * controlled mode the caller must propagate this back as\n * `sortColumnId` / `sortDirection`. In uncontrolled mode this prop is\n * purely observational.\n *\n * @param columnId The column being sorted, or `undefined` when the\n * sort is cleared.\n * @param direction The new direction.\n */\n onSortChange?: (\n columnId: string | undefined,\n direction: SortDirection | undefined,\n ) => void;\n}\n\nconst EMPTY_COLUMN_STATE: DataTablePersistedState = { widths: {}, visibility: {} };\n\n/** Keyboard increment, in CSS pixels, for a column resize separator. */\nconst KEYBOARD_RESIZE_STEP = 16;\n\n/** Product-neutral finite ceiling for a column without an explicit maximum. */\nconst DEFAULT_MAX_COLUMN_WIDTH = 1200;\n\ninterface ColumnWidthBounds {\n min: number;\n max: number;\n}\n\nfunction columnWidthBounds<T>(column: DataTableColumn<T>): ColumnWidthBounds {\n const min = Math.max(0, column.minWidth ?? 80);\n const requestedMax = column.maxWidth ?? DEFAULT_MAX_COLUMN_WIDTH;\n const max = Number.isFinite(requestedMax)\n ? Math.max(min, requestedMax)\n : DEFAULT_MAX_COLUMN_WIDTH;\n return { min, max: Math.max(min, max) };\n}\n\nfunction clampColumnWidth<T>(column: DataTableColumn<T>, width: number): number {\n const { min, max } = columnWidthBounds(column);\n return Math.min(max, Math.max(min, Math.round(width)));\n}\n\n// ============================================================================\n// Sorting helpers\n// ============================================================================\n\n/**\n * Stable sort — rows with equal comparison result keep their original\n * relative order. We achieve stability by augmenting each row with its\n * original index and using that as a tie-breaker.\n *\n * The input array is never mutated.\n */\nfunction stableSort<T>(rows: T[], comparator: (a: T, b: T) => number): T[] {\n const indexed = rows.map((row, i) => ({ row, i }));\n indexed.sort((a, b) => {\n const result = comparator(a.row, b.row);\n return result !== 0 ? result : a.i - b.i;\n });\n return indexed.map(({ row }) => row);\n}\n\n/**\n * Builds a comparator from a `DataTableColumn` + active sort direction.\n * Falls back to string comparison when neither `sortComparator` nor\n * `sortValueAccessor` is provided on the column.\n */\nfunction buildComparator<T>(\n col: DataTableColumn<T>,\n direction: SortDirection,\n): (a: T, b: T) => number {\n const { sortComparator } = col;\n if (sortComparator) {\n return (a, b) => sortComparator(a, b, direction);\n }\n const accessor = col.sortValueAccessor;\n if (accessor) {\n return (a, b) => {\n const av = accessor(a);\n const bv = accessor(b);\n // Nullish values sort last regardless of direction.\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n let cmp: number;\n if (typeof av === \"number\" && typeof bv === \"number\") {\n cmp = av - bv;\n } else {\n cmp = String(av).localeCompare(String(bv));\n }\n return direction === \"asc\" ? cmp : -cmp;\n };\n }\n // No accessor — unsortable in practice (column should not be sortable).\n return () => 0;\n}\n\n/**\n * Returns the `aria-sort` attribute value for a column header cell.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort\n */\nfunction ariaSortValue(\n col: DataTableColumn<unknown>,\n activeSortColumnId: string | undefined,\n activeSortDirection: SortDirection | undefined,\n): \"ascending\" | \"descending\" | \"none\" | undefined {\n if (!col.sortable) return undefined;\n if (col.id === activeSortColumnId) {\n return activeSortDirection === \"asc\" ? \"ascending\" : \"descending\";\n }\n return \"none\";\n}\n\n// Sort indicator icons rendered as inline SVG so there is no external\n// asset dependency. They intentionally reuse design-token colours so they\n// automatically adapt to light / dark themes.\n\nfunction SortIcon({ direction }: { direction: SortDirection | \"none\" }) {\n if (direction === \"none\") {\n // Neutral chevron stack — both arrows in muted colour\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--none\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.35\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.35\" />\n </svg>\n </span>\n );\n }\n if (direction === \"asc\") {\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--asc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.25\" />\n </svg>\n </span>\n );\n }\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--desc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.25\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" />\n </svg>\n </span>\n );\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nfunction DataTableInner<T>({\n columns,\n rows,\n getRowKey,\n emptyState,\n loadingState,\n loading = false,\n columnState,\n onColumnStateChange,\n className = \"\",\n ariaLabel = \"Data table\",\n resizeHandleLabel = (column) => `Resize ${column.id} column`,\n resizeValueText = (width) => `${String(width)} pixels`,\n testId,\n onRowClick,\n isRowClickable,\n rowClassName,\n // Sorting (controlled)\n sortColumnId: controlledSortColumnId,\n sortDirection: controlledSortDirection,\n onSortChange,\n}: DataTableProps<T>) {\n // ---- Column state (widths + visibility) ----------------------------------\n //\n // Held internally so a caller that does not care about persistence gets a\n // working table, and re-seeded whenever the caller supplies a new one.\n const [persisted, setPersisted] = useState<DataTablePersistedState>(\n () => columnState ?? EMPTY_COLUMN_STATE,\n );\n\n useEffect(() => {\n if (columnState) setPersisted(columnState);\n }, [columnState]);\n\n // ---- Sorting state (uncontrolled fallback) -------------------------------\n //\n // Controlled mode is detected by the presence of `sortColumnId` as a prop\n // (the caller explicitly manages the sort column). Using only this prop as\n // the signal (rather than OR-ing with `sortDirection`) avoids a\n // pathological state where `sortDirection` is set but `sortColumnId` is\n // not, which would enter controlled mode with no sort effect.\n //\n // Note: TypeScript cannot distinguish \"prop not passed\" from \"prop passed\n // as undefined\", so we rely on the convention that controlled callers\n // always pass both `sortColumnId` and `sortDirection` together.\n const isControlled =\n controlledSortColumnId !== undefined || controlledSortDirection !== undefined;\n\n const [internalSortColumnId, setInternalSortColumnId] = useState<string | undefined>(\n undefined,\n );\n const [internalSortDirection, setInternalSortDirection] = useState<\n SortDirection | undefined\n >(undefined);\n\n const activeSortColumnId = isControlled\n ? controlledSortColumnId\n : internalSortColumnId;\n const activeSortDirection = isControlled\n ? controlledSortDirection\n : internalSortDirection;\n\n /** Handle a click / keypress on a sortable column header. */\n const handleSortToggle = useCallback(\n (col: DataTableColumn<T>) => {\n if (!col.sortable) return;\n\n let nextColumnId: string | undefined;\n let nextDirection: SortDirection | undefined;\n\n if (activeSortColumnId !== col.id) {\n // Activate this column in its default direction.\n nextColumnId = col.id;\n nextDirection = col.defaultSortDirection ?? \"asc\";\n } else if (activeSortDirection === (col.defaultSortDirection ?? \"asc\")) {\n // Flip to the opposite direction.\n nextColumnId = col.id;\n nextDirection = activeSortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n // Already in the non-default direction — clear the sort.\n nextColumnId = undefined;\n nextDirection = undefined;\n }\n\n if (!isControlled) {\n setInternalSortColumnId(nextColumnId);\n setInternalSortDirection(nextDirection);\n }\n onSortChange?.(nextColumnId, nextDirection);\n },\n [activeSortColumnId, activeSortDirection, isControlled, onSortChange],\n );\n\n // ---- Visibility computation (column always-visible wins) -----------------\n const visibleColumns = useMemo(\n () =>\n columns.filter((col) => {\n if (col.alwaysVisible) return true;\n const flag = persisted.visibility[col.id];\n return flag !== false;\n }),\n [columns, persisted.visibility],\n );\n\n // ---- Width resolution ----------------------------------------------------\n const resolveWidth = useCallback(\n (col: DataTableColumn<T>): number | undefined => {\n const persistedWidth = persisted.widths[col.id];\n const width =\n typeof persistedWidth === \"number\" && persistedWidth > 0\n ? persistedWidth\n : col.initialWidth;\n if (width === undefined) return undefined;\n if (col.noResize && col.maxWidth === undefined) {\n return Math.max(width, col.minWidth ?? 0);\n }\n return clampColumnWidth(col, width);\n },\n [persisted.widths],\n );\n\n // ---- Resize handling -----------------------------------------------------\n const headerRefs = useRef(new Map<string, HTMLTableCellElement>());\n const [renderedWidths, setRenderedWidths] = useState<Record<string, number>>({});\n\n const updateRenderedWidth = useCallback(\n (column: DataTableColumn<T>, width: number) => {\n const nextWidth = clampColumnWidth(column, width);\n setRenderedWidths((previous) =>\n previous[column.id] === nextWidth\n ? previous\n : { ...previous, [column.id]: nextWidth },\n );\n },\n [],\n );\n\n const setHeaderRef = useCallback(\n (columnId: string, element: HTMLTableCellElement | null) => {\n if (element) headerRefs.current.set(columnId, element);\n else headerRefs.current.delete(columnId);\n },\n [],\n );\n\n useLayoutEffect(() => {\n const updateFromElement = (column: DataTableColumn<T>, element: Element) => {\n const nextWidth = element.getBoundingClientRect().width;\n if (nextWidth > 0) updateRenderedWidth(column, nextWidth);\n };\n\n for (const column of visibleColumns) {\n const header = headerRefs.current.get(column.id);\n if (header) updateFromElement(column, header);\n }\n\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const columnId = (entry.target as HTMLElement).dataset.columnId;\n const column = visibleColumns.find((candidate) => candidate.id === columnId);\n if (column) updateFromElement(column, entry.target);\n }\n });\n for (const column of visibleColumns) {\n const header = headerRefs.current.get(column.id);\n if (header) observer.observe(header);\n }\n return () => observer.disconnect();\n }, [updateRenderedWidth, visibleColumns]);\n\n const resizeRef = useRef<{\n columnId: string;\n startX: number;\n startWidth: number;\n minWidth: number;\n maxWidth: number;\n pointerId: number;\n handle: HTMLDivElement;\n } | null>(null);\n\n const updateColumnWidth = useCallback(\n (column: DataTableColumn<T>, width: number) => {\n const nextWidth = clampColumnWidth(column, width);\n setPersisted((previous) => ({\n ...previous,\n widths: { ...previous.widths, [column.id]: nextWidth },\n }));\n updateRenderedWidth(column, nextWidth);\n },\n [updateRenderedWidth],\n );\n\n const clearResize = useCallback((pointerId?: number) => {\n const resize = resizeRef.current;\n if (!resize || (pointerId !== undefined && resize.pointerId !== pointerId)) return;\n // Clear the state before releasing capture: releasePointerCapture can\n // synchronously dispatch lostpointercapture in some browsers.\n resizeRef.current = null;\n if (resize.handle.hasPointerCapture?.(resize.pointerId)) {\n resize.handle.releasePointerCapture?.(resize.pointerId);\n }\n }, []);\n\n useEffect(() => clearResize, [clearResize]);\n\n const measuredWidth = useCallback(\n (handle: HTMLDivElement, col: DataTableColumn<T>): number => {\n const renderedWidth = handle.parentElement?.getBoundingClientRect().width ?? 0;\n const fallbackWidth =\n renderedWidths[col.id] ?? resolveWidth(col) ?? columnWidthBounds(col).min;\n return clampColumnWidth(col, Math.round(renderedWidth) || fallbackWidth);\n },\n [renderedWidths, resolveWidth],\n );\n\n const beginResize = useCallback(\n (ev: ReactPointerEvent<HTMLDivElement>, col: DataTableColumn<T>) => {\n if (col.noResize || !ev.isPrimary || ev.button !== 0 || resizeRef.current) {\n return;\n }\n ev.preventDefault();\n ev.stopPropagation();\n const handle = ev.currentTarget;\n resizeRef.current = {\n columnId: col.id,\n startX: ev.clientX,\n startWidth: measuredWidth(handle, col),\n minWidth: columnWidthBounds(col).min,\n maxWidth: columnWidthBounds(col).max,\n pointerId: ev.pointerId,\n handle,\n };\n handle.setPointerCapture?.(ev.pointerId);\n },\n [measuredWidth],\n );\n\n const moveResize = useCallback(\n (ev: ReactPointerEvent<HTMLDivElement>) => {\n const resize = resizeRef.current;\n if (!resize || resize.pointerId !== ev.pointerId) return;\n ev.preventDefault();\n ev.stopPropagation();\n const nextWidth = Math.max(\n resize.minWidth,\n Math.min(resize.maxWidth, resize.startWidth + ev.clientX - resize.startX),\n );\n const column = columns.find((candidate) => candidate.id === resize.columnId);\n if (column) updateColumnWidth(column, nextWidth);\n },\n [columns, updateColumnWidth],\n );\n\n const endResize = useCallback(\n (ev: ReactPointerEvent<HTMLDivElement>) => {\n if (resizeRef.current?.pointerId !== ev.pointerId) return;\n ev.preventDefault();\n ev.stopPropagation();\n clearResize(ev.pointerId);\n },\n [clearResize],\n );\n\n const handleResizeKeyDown = useCallback(\n (ev: ReactKeyboardEvent<HTMLDivElement>, col: DataTableColumn<T>) => {\n if (ev.key !== \"ArrowLeft\" && ev.key !== \"ArrowRight\" && ev.key !== \"Home\") {\n return;\n }\n ev.preventDefault();\n ev.stopPropagation();\n const { min: minimum, max: maximum } = columnWidthBounds(col);\n const current = measuredWidth(ev.currentTarget, col);\n const nextWidth =\n ev.key === \"Home\"\n ? minimum\n : Math.min(\n maximum,\n Math.max(\n minimum,\n current +\n (ev.key === \"ArrowRight\"\n ? KEYBOARD_RESIZE_STEP\n : -KEYBOARD_RESIZE_STEP),\n ),\n );\n updateColumnWidth(col, nextWidth);\n },\n [measuredWidth, updateColumnWidth],\n );\n\n // Report changes so the caller can persist them. Skipped while `persisted`\n // still holds what the caller last handed in, so echoing the callback back\n // through `columnState` does not loop.\n const reported = useRef(persisted);\n useEffect(() => {\n if (reported.current === persisted) return;\n reported.current = persisted;\n onColumnStateChange?.(persisted);\n }, [persisted, onColumnStateChange]);\n\n // ---- Sorted rows --------------------------------------------------------\n const sortedRows = useMemo(() => {\n if (!activeSortColumnId || !activeSortDirection) return rows;\n const col = columns.find((c) => c.id === activeSortColumnId);\n if (!col?.sortable) return rows;\n return stableSort(rows, buildComparator(col, activeSortDirection));\n }, [rows, columns, activeSortColumnId, activeSortDirection]);\n\n // ---- Body rendering ------------------------------------------------------\n const composedClassName = [\"data-table\", className].filter(Boolean).join(\" \");\n\n const renderBody = () => {\n if (loading) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {loadingState ?? <div className=\"data-table__loading\" />}\n </td>\n </tr>\n );\n }\n if (sortedRows.length === 0) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {emptyState}\n </td>\n </tr>\n );\n }\n return sortedRows.map((row, index) => {\n const key = getRowKey(row, index);\n // A row is interactive only if the table has a handler AND this row is\n // not excluded, so the affordance, the tab stop and the role move\n // together. Splitting them would announce a button that does nothing.\n const clickable = Boolean(onRowClick && (isRowClickable?.(row, index) ?? true));\n const extraClass = rowClassName?.(row, index);\n return (\n <tr\n key={key}\n className={[\n \"data-table__row\",\n clickable && \"data-table__row--clickable\",\n extraClass,\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={\n clickable\n ? (e) => {\n if (fromInteractiveDescendant(e.target, e.currentTarget)) return;\n onRowClick?.(row);\n }\n : undefined\n }\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n if (fromInteractiveDescendant(e.target, e.currentTarget)) return;\n e.preventDefault();\n onRowClick?.(row);\n }\n : undefined\n }\n tabIndex={clickable ? 0 : undefined}\n role={clickable ? \"button\" : undefined}\n >\n {visibleColumns.map((col) => (\n <td\n key={col.id}\n className={[\n \"data-table__cell\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={{\n minWidth: col.minWidth,\n maxWidth: col.noResize ? col.maxWidth : columnWidthBounds(col).max,\n }}\n >\n {col.render(row, index)}\n </td>\n ))}\n </tr>\n );\n });\n };\n\n return (\n <div className={composedClassName} data-testid={testId}>\n <table className=\"data-table__table\" aria-label={ariaLabel} role=\"table\">\n <thead className=\"data-table__head\">\n <tr className=\"data-table__row data-table__row--head\">\n {visibleColumns.map((col) => {\n const width = resolveWidth(col);\n const currentWidth = clampColumnWidth(\n col,\n renderedWidths[col.id] ?? width ?? columnWidthBounds(col).min,\n );\n const bounds = columnWidthBounds(col);\n const isSortActive = col.sortable && col.id === activeSortColumnId;\n\n return (\n <th\n key={col.id}\n ref={(element) => setHeaderRef(col.id, element)}\n data-column-id={col.id}\n scope=\"col\"\n className={[\n \"data-table__cell\",\n \"data-table__cell--head\",\n !col.noResize && \"data-table__cell--resizable\",\n col.sortable && \"data-table__cell--sortable\",\n isSortActive && \"data-table__cell--sort-active\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={{\n width: width === undefined ? undefined : `${String(width)}px`,\n minWidth: col.minWidth,\n maxWidth: col.noResize ? col.maxWidth : bounds.max,\n }}\n aria-sort={ariaSortValue(\n col as DataTableColumn<unknown>,\n activeSortColumnId,\n activeSortDirection,\n )}\n onClick={\n col.sortable\n ? () => {\n handleSortToggle(col);\n }\n : undefined\n }\n onKeyDown={\n col.sortable\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleSortToggle(col);\n }\n }\n : undefined\n }\n tabIndex={col.sortable ? 0 : undefined}\n >\n <span className=\"data-table__head-label\">{col.header}</span>\n {col.sortable && (\n <SortIcon\n direction={\n isSortActive && activeSortDirection\n ? activeSortDirection\n : \"none\"\n }\n />\n )}\n {!col.noResize && (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label={resizeHandleLabel(col)}\n aria-valuemin={bounds.min}\n aria-valuemax={bounds.max}\n aria-valuenow={currentWidth}\n aria-valuetext={resizeValueText(currentWidth, col)}\n tabIndex={0}\n className=\"data-table__resize-handle\"\n onClick={(event) => event.stopPropagation()}\n onPointerDown={(event) => beginResize(event, col)}\n onPointerMove={moveResize}\n onPointerUp={endResize}\n onPointerCancel={endResize}\n onLostPointerCapture={(event) => clearResize(event.pointerId)}\n onKeyDown={(event) => handleResizeKeyDown(event, col)}\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody className=\"data-table__body\">{renderBody()}</tbody>\n </table>\n </div>\n );\n}\n\n// Memoize while preserving the generic signature.\nexport const DataTable = memo(DataTableInner) as typeof DataTableInner;\n\nexport default DataTable;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lablup/ui-common",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.21",
4
4
  "description": "Shared, product-neutral UI components and design tokens for Lablup products",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lablup Inc.",