@apliteni/apliteni-ui 0.25.3 → 0.27.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/README.md CHANGED
@@ -107,7 +107,7 @@ import { tokensCss, topbarCss, cssText } from '@apliteni/apliteni-ui/inline';
107
107
 
108
108
  ## React components (stateful surfaces)
109
109
 
110
- `DataTable`, `Modal`, `Button`, `Badge`, `Card` and `Icon` — same `.ui-*` classes,
110
+ `DataTable`, `Pagination`, `Modal`, `Button`, `Badge`, `Card` and `Icon` — same `.ui-*` classes,
111
111
  same tokens, TypeScript types included. They ship as a **subpath of this package**,
112
112
  not as a package of their own: one install, one version, one pin.
113
113
 
@@ -126,7 +126,7 @@ its tree.
126
126
 
127
127
  ```tsx
128
128
  import '@apliteni/apliteni-ui/css'; // kit tokens + .ui-* classes
129
- import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, pager)
129
+ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, sort control)
130
130
  import { DataTable, Modal } from '@apliteni/apliteni-ui/react';
131
131
  ```
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.25.3",
3
+ "version": "0.27.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -25,11 +25,19 @@ The kit declares no dependency on `react` or `react-dom`, so install them yourse
25
25
 
26
26
  ```tsx
27
27
  import '@apliteni/apliteni-ui/css'; // kit tokens + .ui-* classes
28
- import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, pager)
28
+ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, sort control)
29
29
  import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
30
30
  ```
31
31
 
32
- Components: `DataTable`, `Modal`, `Button`, `Badge`, `Card`, `Icon`.
32
+ Components: `DataTable`, `Pagination`, `Modal`, `Button`, `Badge`, `Card`, `Icon`.
33
+
34
+ `Pagination` renders the kit's `pagination()` markup, class for class, so its styles come from
35
+ `@apliteni/apliteni-ui/css` rather than from this bundle. One deliberate difference: it takes no
36
+ `href`, because a React pager reports through `onPageChange` rather than navigating. Use the
37
+ vanilla factory where the steps have to be real links. `PAGE_SIZES` and
38
+ `DEFAULT_PAGE_SIZE` are exported here too — the scale is the kit's, so no call site writes
39
+ either number. They are declared in this package's own types, and `PAGE_SIZES` is a
40
+ `readonly number[]`: pass it to `pageSizes`, but do not add sizes to it.
33
41
 
34
42
  ## What the Modal does with focus
35
43
 
@@ -64,3 +72,85 @@ The bare `@apliteni/apliteni-ui` specifier in this source resolves to the kit it
64
72
  once installed. In the repo there is no copy to resolve to, so `kit-alias.ts` points
65
73
  vitest and Storybook straight at `../src/` — which is why the class-name parity tests
66
74
  now compare against the working tree rather than the last published release.
75
+
76
+ ### Tables with another presentation of the same rows
77
+
78
+ Pass `selectable={false}` to omit selection controls and their callbacks. Existing callers
79
+ that supply selection callbacks retain the checkbox column by default.
80
+
81
+ `sort` and `onSortChange` make sorting controlled. Both presentations can use the exported
82
+ `sortTableRows` helper, so initial order, stable ties and later changes agree:
83
+
84
+ ```tsx
85
+ const [sort, setSort] = useState<TableSort<Row>>({ key: 'name', dir: -1 });
86
+ const ordered = sortTableRows(rows, sort);
87
+
88
+ <DataTable columns={columns} rows={rows} selectable={false}
89
+ sort={sort} onSortChange={setSort} />
90
+ ```
91
+
92
+ Render the sibling list from `ordered`. Omit `sort` to keep the table's own state;
93
+ `onSortChange` can also observe that uncontrolled state. Set `key: undefined` to preserve
94
+ input order. Import `TableSort` and `sortTableRows` from `@apliteni/apliteni-ui/react`.
95
+
96
+ Changing the sort returns the table to its first page. The comparator uses JavaScript
97
+ `<` and `>`; use consistently typed, comparable values in sortable columns. Ordering of
98
+ mixed types, missing values and `NaN` is not guaranteed. `sortTableRows` always returns
99
+ a new array, including when `key` is `undefined`.
100
+
101
+ Choose controlled or uncontrolled once per table. Passing `sort` for a while and then
102
+ dropping it is not supported: the table falls back to the sort state it started with, not to
103
+ the one it was last given.
104
+
105
+ ### Tables paged by a server
106
+
107
+ `page` and `onPageChange` make pagination controlled, the same way `sort` does — and the same
108
+ rule applies: choose one mode per table and keep it. Given a `page`, the table renders `rows`
109
+ exactly as handed to it and never slices or re-orders them; the range comes from `page`,
110
+ `pageSize` and `total`:
111
+
112
+ ```tsx
113
+ <DataTable columns={columns} rows={pageOfRows} selectable={false}
114
+ page={page} total={total} pageSize={size} onPageChange={fetchPage}
115
+ pageSizes={PAGE_SIZES} onPageSizeChange={setSize} loading={loading} />
116
+ ```
117
+
118
+ `total` is required with `page`: it is the row count of the whole result, not of `rows`. Pass
119
+ `total={null}` for a result whose size is not known, and then `hasMore` is required too — the
120
+ pager offers Prev and Next alone, because no other control can be computed without a last
121
+ page. Leave both out and the call does not type-check: a pager told nothing can only draw two
122
+ dead buttons.
123
+
124
+ Without `pageSize`, a controlled table takes the page size from `rows.length`, the page it was
125
+ handed. Pass `pageSize` whenever the last page can be shorter than the rest.
126
+
127
+ A controlled table never sorts the rows it is handed. Changing the sort asks its owner for page
128
+ 1 through `onPageChange`, which is the most a controlled table can do about it. Keep `sort`
129
+ controlled too, so the headers can say which column the server ordered by. Without it the
130
+ headers still report a press through `onSortChange`, but no column announces `aria-sort` or
131
+ draws a direction, because the table does not know the server's order.
132
+
133
+ Omit `page` to keep the table's own paging: it slices `rows` in memory and the total is
134
+ `rows.length`. `pageSizes` offers a size control in either mode — without a `pageSize` prop the
135
+ table remembers the size the reader picked, with one it reports the choice through
136
+ `onPageSizeChange` and shows what it is given. A table that owns its page returns the reader to
137
+ page 1 when the size changes. A controlled table only calls `onPageSizeChange`, once, and
138
+ leaves the page to its owner: a new size means page 1, so fetch page 1 at that size. It does
139
+ not also call `onPageChange(1)`, because that second call would carry the old size.
140
+
141
+ `pageSize` is read the way the pager reads it, so the rows and the range always agree: a
142
+ fraction is truncated, and `NaN`, zero or a negative number falls back to the default. A value
143
+ taken from a URL, such as `Number(params.get('size'))`, is safe to pass as it is.
144
+
145
+ `pager={false}` renders no pager at all, for a surface that supplies its own. One page of
146
+ content renders none either: the pager keeps GOV.UK's rule that pagination for a single page is
147
+ not shown, and with a size control on offer it keeps the row count and that control alone.
148
+
149
+ Focus stays on the step the reader pressed, so they can press it again. At an end that step is
150
+ disabled, and a browser drops focus from a disabled control to the page body. So once the new
151
+ page has arrived, the pager moves focus to the nearest step that can still move, never into
152
+ the rows. It moves focus only after a press in the pager, and never while `loading`.
153
+ Clearing the page-jump box, or typing into it and then pressing a step, does not change the
154
+ page.
155
+
156
+ `pageSize` defaults to `DEFAULT_PAGE_SIZE` (100). **Breaking:** it used to default to 4.
@@ -80,15 +80,3 @@
80
80
  .rx-caret {
81
81
  margin-left: 2px;
82
82
  }
83
- .rx-pager {
84
- display: flex;
85
- align-items: center;
86
- gap: 8px;
87
- justify-content: flex-end;
88
- margin-top: 16px;
89
- }
90
- .rx-pager__info {
91
- color: var(--muted);
92
- font-size: var(--text-sm);
93
- margin-right: auto;
94
- }
@@ -45,17 +45,81 @@ 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 PagerProps = {
64
+ page?: never;
65
+ onPageChange?: (page: number) => void;
66
+ total?: never;
67
+ hasMore?: never;
68
+ } | {
69
+ page: number;
70
+ onPageChange: (page: number) => void;
71
+ total: number;
72
+ hasMore?: boolean;
73
+ } | {
74
+ page: number;
75
+ onPageChange: (page: number) => void;
76
+ total: null;
77
+ hasMore: boolean;
78
+ };
79
+ type DataTableProps<T> = {
80
+ columns: Column<T>[];
81
+ rows: T[];
82
+ pageSize?: number;
83
+ pageSizes?: readonly number[] | null;
84
+ onPageSizeChange?: (size: number) => void;
85
+ /** `false` renders no pager at all — for a surface that supplies its own. */
86
+ pager?: boolean;
87
+ /**
88
+ * The pager's accessible name. Two tables on one page otherwise publish two
89
+ * landmarks called "Pagination", and a reader listing the landmarks cannot tell
90
+ * which one moves which table. Axe will not catch it: `landmark-unique` is a
91
+ * best-practice rule and the kit's gate runs only the WCAG A/AA tags.
92
+ */
93
+ pagerLabel?: string;
94
+ loading?: boolean;
95
+ } & SelectionProps & PagerProps & ({
96
+ sort?: never;
97
+ onSortChange?: (sort: TableSort<T>) => void;
98
+ } | {
99
+ sort: TableSort<T>;
100
+ onSortChange: (sort: TableSort<T>) => void;
101
+ });
102
+ declare function sortTableRows<T>(rows: T[], sort: TableSort<T>): T[];
56
103
  declare function DataTable<T extends {
57
104
  name: string;
58
- }>({ columns, rows, pageSize, selected, onToggle, onTogglePage, }: DataTableProps<T>): react.JSX.Element;
105
+ }>({ columns, rows, pageSize, pageSizes, onPageSizeChange, pager, pagerLabel, loading, selectable, selected, onToggle, onTogglePage, sort: controlledSort, onSortChange, page: controlledPage, onPageChange, total, hasMore, }: DataTableProps<T>): react.JSX.Element;
106
+
107
+ type PaginationProps = {
108
+ page?: number;
109
+ pageSize?: number;
110
+ /** Rows in the whole result. `null` — the honest answer for a caller who cannot count. */
111
+ total?: number | null;
112
+ /** Read only when `total` is null: whether a page exists after this one. */
113
+ hasMore?: boolean;
114
+ pageSizes?: readonly number[] | null;
115
+ variant?: 'steps' | 'numbered' | 'jump';
116
+ label?: string;
117
+ loading?: boolean;
118
+ id?: string;
119
+ onPageChange?: (page: number) => void;
120
+ onPageSizeChange?: (size: number) => void;
121
+ };
122
+ declare function Pagination({ page, pageSize, total, hasMore, pageSizes, variant, label, loading, id, onPageChange, onPageSizeChange, }: PaginationProps): react.JSX.Element | null;
59
123
 
60
124
  type SkeletonProps = {
61
125
  /** A count of bars, or explicit widths when a ragged prose edge matters. */
@@ -96,4 +160,7 @@ type DeniedProps = {
96
160
  };
97
161
  declare function Denied({ title, sub, need, icon, className, children, }: DeniedProps): react.JSX.Element;
98
162
 
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 };
163
+ declare const PAGE_SIZES: readonly number[];
164
+ declare const DEFAULT_PAGE_SIZE: number;
165
+
166
+ export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, Denied, type DeniedProps, Icon, Modal, type ModalProps, PAGE_SIZES, Pagination, type PaginationProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, type TableSort, sortTableRows };