@apliteni/apliteni-ui 0.25.3 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.25.3",
3
+ "version": "0.26.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -64,3 +64,32 @@ The bare `@apliteni/apliteni-ui` specifier in this source resolves to the kit it
64
64
  once installed. In the repo there is no copy to resolve to, so `kit-alias.ts` points
65
65
  vitest and Storybook straight at `../src/` — which is why the class-name parity tests
66
66
  now compare against the working tree rather than the last published release.
67
+
68
+ ### Tables with another presentation of the same rows
69
+
70
+ Pass `selectable={false}` to omit selection controls and their callbacks. Existing callers
71
+ that supply selection callbacks retain the checkbox column by default.
72
+
73
+ `sort` and `onSortChange` make sorting controlled. Both presentations can use the exported
74
+ `sortTableRows` helper, so initial order, stable ties and later changes agree:
75
+
76
+ ```tsx
77
+ const [sort, setSort] = useState<TableSort<Row>>({ key: 'name', dir: -1 });
78
+ const ordered = sortTableRows(rows, sort);
79
+
80
+ <DataTable columns={columns} rows={rows} selectable={false}
81
+ sort={sort} onSortChange={setSort} />
82
+ ```
83
+
84
+ Render the sibling list from `ordered`. Omit `sort` to keep the table's own state;
85
+ `onSortChange` can also observe that uncontrolled state. Set `key: undefined` to preserve
86
+ input order. Import `TableSort` and `sortTableRows` from `@apliteni/apliteni-ui/react`.
87
+
88
+ Changing the sort returns the table to its first page. The comparator uses JavaScript
89
+ `<` and `>`; use consistently typed, comparable values in sortable columns. Ordering of
90
+ mixed types, missing values and `NaN` is not guaranteed. `sortTableRows` always returns
91
+ a new array, including when `key` is `undefined`.
92
+
93
+ Choose controlled or uncontrolled once per table. Passing `sort` for a while and then
94
+ dropping it is not supported: the table falls back to the sort state it started with, not to
95
+ the one it was last given.
@@ -45,17 +45,36 @@ type Column<T> = {
45
45
  sortable?: boolean;
46
46
  render?: (row: T) => ReactNode;
47
47
  };
48
- type DataTableProps<T> = {
49
- columns: Column<T>[];
50
- rows: T[];
51
- pageSize?: number;
48
+ type TableSort<T> = {
49
+ key: (keyof T & string) | undefined;
50
+ dir: 1 | -1;
51
+ };
52
+ type SelectionProps = {
53
+ selectable: false;
54
+ selected?: Set<string>;
55
+ onToggle?: (name: string) => void;
56
+ onTogglePage?: (names: string[]) => void;
57
+ } | {
58
+ selectable?: true;
52
59
  selected: Set<string>;
53
60
  onToggle: (name: string) => void;
54
61
  onTogglePage: (names: string[]) => void;
55
62
  };
63
+ type DataTableProps<T> = {
64
+ columns: Column<T>[];
65
+ rows: T[];
66
+ pageSize?: number;
67
+ } & SelectionProps & ({
68
+ sort?: never;
69
+ onSortChange?: (sort: TableSort<T>) => void;
70
+ } | {
71
+ sort: TableSort<T>;
72
+ onSortChange: (sort: TableSort<T>) => void;
73
+ });
74
+ declare function sortTableRows<T>(rows: T[], sort: TableSort<T>): T[];
56
75
  declare function DataTable<T extends {
57
76
  name: string;
58
- }>({ columns, rows, pageSize, selected, onToggle, onTogglePage, }: DataTableProps<T>): react.JSX.Element;
77
+ }>({ columns, rows, pageSize, selectable, selected, onToggle, onTogglePage, sort: controlledSort, onSortChange, }: DataTableProps<T>): react.JSX.Element;
59
78
 
60
79
  type SkeletonProps = {
61
80
  /** A count of bars, or explicit widths when a ragged prose edge matters. */
@@ -96,4 +115,4 @@ type DeniedProps = {
96
115
  };
97
116
  declare function Denied({ title, sub, need, icon, className, children, }: DeniedProps): react.JSX.Element;
98
117
 
99
- export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, DataTable, type DataTableProps, Denied, type DeniedProps, Icon, Modal, type ModalProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps };
118
+ export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, DataTable, type DataTableProps, Denied, type DeniedProps, Icon, Modal, type ModalProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, type TableSort, sortTableRows };
@@ -172,36 +172,50 @@ function Modal({ open, title, onClose, footer, children }) {
172
172
  // src/DataTable.tsx
173
173
  import { useMemo, useState } from "react";
174
174
  import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
175
+ function sortTableRows(rows, sort) {
176
+ if (sort.key === void 0) return [...rows];
177
+ const key = sort.key;
178
+ return [...rows].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * sort.dir);
179
+ }
175
180
  function DataTable({
176
181
  columns,
177
182
  rows,
178
183
  pageSize = 4,
179
- selected,
180
- onToggle,
181
- onTogglePage
184
+ selectable = true,
185
+ selected = /* @__PURE__ */ new Set(),
186
+ onToggle = () => {
187
+ },
188
+ onTogglePage = () => {
189
+ },
190
+ sort: controlledSort,
191
+ onSortChange
182
192
  }) {
183
- const [sort, setSort] = useState(
193
+ const [localSort, setLocalSort] = useState(
184
194
  { key: columns.find((c) => c.sortable)?.key, dir: -1 }
185
195
  );
196
+ const sort = controlledSort ?? localSort;
186
197
  const [page, setPage] = useState(0);
187
- const sorted = useMemo(() => {
188
- if (!sort.key) return rows;
189
- const k = sort.key;
190
- return [...rows].sort((a, b) => (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * sort.dir);
191
- }, [rows, sort]);
198
+ const [pagedSort, setPagedSort] = useState(sort);
199
+ if (pagedSort.key !== sort.key || pagedSort.dir !== sort.dir) {
200
+ setPagedSort(sort);
201
+ setPage(0);
202
+ }
203
+ const sorted = useMemo(() => sortTableRows(rows, sort), [rows, sort.key, sort.dir]);
192
204
  const pages = Math.max(1, Math.ceil(sorted.length / pageSize));
193
205
  const safePage = Math.min(page, pages - 1);
194
206
  const slice = sorted.slice(safePage * pageSize, safePage * pageSize + pageSize);
195
- const onSort = (k) => {
196
- setSort((s) => s.key === k ? { key: k, dir: s.dir === 1 ? -1 : 1 } : { key: k, dir: -1 });
207
+ const onSort = (key) => {
208
+ const next = sort.key === key ? { key, dir: sort.dir === 1 ? -1 : 1 } : { key, dir: -1 };
209
+ if (controlledSort === void 0) setLocalSort(next);
210
+ onSortChange?.(next);
197
211
  setPage(0);
198
212
  };
199
213
  const caret = (k) => sort.key === k ? sort.dir === 1 ? " \u25B2" : " \u25BC" : " \u2195";
200
- const pageAllOn = slice.length > 0 && slice.every((r) => selected.has(r.name));
214
+ const pageAllOn = selectable && slice.length > 0 && slice.every((r) => selected.has(r.name));
201
215
  return /* @__PURE__ */ jsxs4(Fragment, { children: [
202
216
  /* @__PURE__ */ jsxs4("table", { className: "ui-table ui-table--hover ui-table--zebra", children: [
203
217
  /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
204
- /* @__PURE__ */ jsx6("th", { scope: "col", children: /* @__PURE__ */ jsx6(
218
+ selectable ? /* @__PURE__ */ jsx6("th", { scope: "col", children: /* @__PURE__ */ jsx6(
205
219
  "input",
206
220
  {
207
221
  type: "checkbox",
@@ -209,7 +223,7 @@ function DataTable({
209
223
  "aria-label": "Select all rows on this page",
210
224
  onChange: () => onTogglePage(slice.map((r) => r.name))
211
225
  }
212
- ) }),
226
+ ) }) : null,
213
227
  columns.map((c) => (
214
228
  // The sort control is a real <button> inside the header cell. It used to be
215
229
  // role="button" ON the <th>, which threw away the columnheader role and put
@@ -219,7 +233,7 @@ function DataTable({
219
233
  {
220
234
  scope: "col",
221
235
  className: [c.num && "ui-table__num", c.sortable && "rx-sortable"].filter(Boolean).join(" "),
222
- "aria-sort": c.sortable ? sort.key === c.key ? sort.dir === 1 ? "ascending" : "descending" : "none" : void 0,
236
+ "aria-sort": sort.key === c.key ? sort.dir === 1 ? "ascending" : "descending" : c.sortable ? "none" : void 0,
223
237
  children: c.sortable ? /* @__PURE__ */ jsxs4("button", { type: "button", className: "rx-sort", onClick: () => onSort(c.key), children: [
224
238
  c.label,
225
239
  /* @__PURE__ */ jsx6("span", { className: "rx-caret", "aria-hidden": "true", children: caret(c.key) })
@@ -230,7 +244,7 @@ function DataTable({
230
244
  ))
231
245
  ] }) }),
232
246
  /* @__PURE__ */ jsx6("tbody", { children: slice.map((r) => /* @__PURE__ */ jsxs4("tr", { children: [
233
- /* @__PURE__ */ jsx6("td", { children: /* @__PURE__ */ jsx6(
247
+ selectable ? /* @__PURE__ */ jsx6("td", { children: /* @__PURE__ */ jsx6(
234
248
  "input",
235
249
  {
236
250
  type: "checkbox",
@@ -238,7 +252,7 @@ function DataTable({
238
252
  "aria-label": `Select ${r.name}`,
239
253
  onChange: () => onToggle(r.name)
240
254
  }
241
- ) }),
255
+ ) }) : null,
242
256
  columns.map((c) => /* @__PURE__ */ jsx6("td", { className: c.num ? "ui-table__num" : void 0, children: c.render ? c.render(r) : String(r[c.key]) }, c.key))
243
257
  ] }, r.name)) })
244
258
  ] }),
@@ -357,5 +371,6 @@ export {
357
371
  Icon,
358
372
  Modal,
359
373
  Skeleton,
360
- SkeletonTable
374
+ SkeletonTable,
375
+ sortTableRows
361
376
  };