@apliteni/apliteni-ui 0.26.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/react/README.md +111 -2
  4. package/react/dist/index.css +11 -13
  5. package/react/dist/index.d.ts +111 -4
  6. package/react/dist/index.js +811 -167
  7. package/src/components/back.js +57 -0
  8. package/src/components/command-palette.js +597 -0
  9. package/src/components/confirm.js +3 -2
  10. package/src/components/drawer.js +31 -2
  11. package/src/components/dropdown.js +192 -17
  12. package/src/components/feedback.js +2 -2
  13. package/src/components/index.js +5 -2
  14. package/src/components/loading.js +3 -2
  15. package/src/components/nav.js +15 -7
  16. package/src/components/overlay.js +67 -14
  17. package/src/components/pagination.js +344 -0
  18. package/src/components/shell.js +11 -2
  19. package/src/components/tabs.js +7 -1
  20. package/src/components/tooltip.js +237 -0
  21. package/src/components/topbar.js +9 -4
  22. package/src/index.css +4 -0
  23. package/src/index.js +4 -0
  24. package/src/inline.js +8 -0
  25. package/src/motion.js +43 -2
  26. package/src/styles/back.css +42 -0
  27. package/src/styles/badge.css +5 -7
  28. package/src/styles/base.css +8 -7
  29. package/src/styles/button.css +14 -0
  30. package/src/styles/card.css +12 -8
  31. package/src/styles/code.css +3 -4
  32. package/src/styles/command-palette.css +321 -0
  33. package/src/styles/confirm.css +11 -11
  34. package/src/styles/drawer.css +57 -15
  35. package/src/styles/dropdown.css +70 -11
  36. package/src/styles/feedback.css +2 -0
  37. package/src/styles/footer.css +3 -4
  38. package/src/styles/input.css +7 -1
  39. package/src/styles/layout.css +3 -2
  40. package/src/styles/loading.css +5 -0
  41. package/src/styles/nav.css +12 -8
  42. package/src/styles/pagination.css +124 -0
  43. package/src/styles/success.css +3 -2
  44. package/src/styles/table.css +4 -5
  45. package/src/styles/tabs.css +3 -0
  46. package/src/styles/tooltip.css +65 -0
  47. package/src/styles/topbar.css +3 -4
  48. package/src/tokens/tokens.css +14 -8
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.26.0",
3
+ "version": "0.30.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -25,11 +25,36 @@ 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`, `Drawer`, `CommandPalette`, `Button`, `Badge`, `Card`, `Icon`.
33
+
34
+ `CommandPalette` renders the kit's `commandPalette()` markup, class for class, and imports the
35
+ kit's ranking rather than repeating it — so a palette a server rendered and the same palette
36
+ after a keystroke put the same row first. Three differences from the vanilla one. Two are
37
+ because a React host already owns the state the wiring would: it has no Cmd+K binding, since
38
+ `open` is the host's prop to set from whatever key it wants; and a destructive row names an
39
+ `onConfirm` callback rather than the id of a confirm dialog, which is refused the same way — a
40
+ `danger` row with neither renders disabled. `rank={false}` hands the query back through
41
+ `onQueryChange` for a palette a server feeds.
42
+
43
+ The third is not a design decision and is going: **one Escape closes a `Modal` and the palette
44
+ under it.** The React palette and the React `Modal` each register their own listener on the
45
+ document, and `inert` does not stop a document listener, so the question and the surface it was
46
+ asked about close together. The vanilla pair does not do this — every kit overlay is on one
47
+ stack there, and Escape closes the top one only. The React half joins that shared dialog stack
48
+ when this branch is rebased onto the drawer branch, which is where `dialog.ts` lands; until then,
49
+ open a `Modal` from a palette row knowing both will close.
50
+
51
+ `Pagination` renders the kit's `pagination()` markup, class for class, so its styles come from
52
+ `@apliteni/apliteni-ui/css` rather than from this bundle. One deliberate difference: it takes no
53
+ `href`, because a React pager reports through `onPageChange` rather than navigating. Use the
54
+ vanilla factory where the steps have to be real links. `PAGE_SIZES` and
55
+ `DEFAULT_PAGE_SIZE` are exported here too — the scale is the kit's, so no call site writes
56
+ either number. They are declared in this package's own types, and `PAGE_SIZES` is a
57
+ `readonly number[]`: pass it to `pageSizes`, but do not add sizes to it.
33
58
 
34
59
  ## What the Modal does with focus
35
60
 
@@ -39,6 +64,37 @@ controls, disabled controls, controls inside a closed disclosure and elements wi
39
64
  negative tabindex are skipped. Tab and Shift+Tab wrap at the ends of the same list.
40
65
  Escape and a click on the scrim dismiss the dialog and return focus to its opener.
41
66
 
67
+ The Modal fades in and out: the scrim fades and the panel rises a few pixels. After `open`
68
+ turns false it stays mounted until that transition ends, then unmounts. While it leaves,
69
+ focus is already back on the opener and the dialog takes no clicks.
70
+
71
+ ## Drawer
72
+
73
+ A panel that slides in from an edge of the screen over a scrim. It renders the vanilla
74
+ `drawer()` markup, class for class, so it looks and moves like the kit's drawer, and like
75
+ `Pagination` its styles come from `@apliteni/apliteni-ui/css` rather than from this bundle.
76
+ Group what goes inside it the way `drawerSection()` does — a heading over a `<dl>` of label
77
+ and value rows — rather than in cards; the rules are on Guidelines / Drawers.
78
+
79
+ ```tsx
80
+ <Drawer open={open} title="Transaction" onClose={() => setOpen(false)}
81
+ side="right" size="md" footer={<Button onClick={save}>Save</Button>}>
82
+
83
+ </Drawer>
84
+ ```
85
+
86
+ `side` is `right` (default), `left`, `top` or `bottom`. `size` is `sm`, `md` (default) or
87
+ `lg`, measured along the slide. `closeLabel` names the close button (default "Close").
88
+ Focus, Escape, the scrim, Tab and the return of focus behave as the Modal's do. Like the
89
+ Modal, it stays mounted until its exit slide ends. It is portalled to `document.body`.
90
+
91
+ React Modals, Drawers and CommandPalettes share one stack. When one is open over another,
92
+ only the top one takes Escape and Tab, and closing it hands focus back to the one below —
93
+ so a destructive palette row that opens a Modal takes one Escape to answer, not one that
94
+ closes both. The vanilla `drawer()`, `confirm()` and `commandPalette()` keep a separate
95
+ stack that this one cannot see, so do not open a React dialog and a vanilla overlay over
96
+ each other on the same page.
97
+
42
98
  ## Work on them
43
99
 
44
100
  From the repo root — one `npm install` covers the workspace:
@@ -93,3 +149,56 @@ a new array, including when `key` is `undefined`.
93
149
  Choose controlled or uncontrolled once per table. Passing `sort` for a while and then
94
150
  dropping it is not supported: the table falls back to the sort state it started with, not to
95
151
  the one it was last given.
152
+
153
+ ### Tables paged by a server
154
+
155
+ `page` and `onPageChange` make pagination controlled, the same way `sort` does — and the same
156
+ rule applies: choose one mode per table and keep it. Given a `page`, the table renders `rows`
157
+ exactly as handed to it and never slices or re-orders them; the range comes from `page`,
158
+ `pageSize` and `total`:
159
+
160
+ ```tsx
161
+ <DataTable columns={columns} rows={pageOfRows} selectable={false}
162
+ page={page} total={total} pageSize={size} onPageChange={fetchPage}
163
+ pageSizes={PAGE_SIZES} onPageSizeChange={setSize} loading={loading} />
164
+ ```
165
+
166
+ `total` is required with `page`: it is the row count of the whole result, not of `rows`. Pass
167
+ `total={null}` for a result whose size is not known, and then `hasMore` is required too — the
168
+ pager offers Prev and Next alone, because no other control can be computed without a last
169
+ page. Leave both out and the call does not type-check: a pager told nothing can only draw two
170
+ dead buttons.
171
+
172
+ Without `pageSize`, a controlled table takes the page size from `rows.length`, the page it was
173
+ handed. Pass `pageSize` whenever the last page can be shorter than the rest.
174
+
175
+ A controlled table never sorts the rows it is handed. Changing the sort asks its owner for page
176
+ 1 through `onPageChange`, which is the most a controlled table can do about it. Keep `sort`
177
+ controlled too, so the headers can say which column the server ordered by. Without it the
178
+ headers still report a press through `onSortChange`, but no column announces `aria-sort` or
179
+ draws a direction, because the table does not know the server's order.
180
+
181
+ Omit `page` to keep the table's own paging: it slices `rows` in memory and the total is
182
+ `rows.length`. `pageSizes` offers a size control in either mode — without a `pageSize` prop the
183
+ table remembers the size the reader picked, with one it reports the choice through
184
+ `onPageSizeChange` and shows what it is given. A table that owns its page returns the reader to
185
+ page 1 when the size changes. A controlled table only calls `onPageSizeChange`, once, and
186
+ leaves the page to its owner: a new size means page 1, so fetch page 1 at that size. It does
187
+ not also call `onPageChange(1)`, because that second call would carry the old size.
188
+
189
+ `pageSize` is read the way the pager reads it, so the rows and the range always agree: a
190
+ fraction is truncated, and `NaN`, zero or a negative number falls back to the default. A value
191
+ taken from a URL, such as `Number(params.get('size'))`, is safe to pass as it is.
192
+
193
+ `pager={false}` renders no pager at all, for a surface that supplies its own. One page of
194
+ content renders none either: the pager keeps GOV.UK's rule that pagination for a single page is
195
+ not shown, and with a size control on offer it keeps the row count and that control alone.
196
+
197
+ Focus stays on the step the reader pressed, so they can press it again. At an end that step is
198
+ disabled, and a browser drops focus from a disabled control to the page body. So once the new
199
+ page has arrived, the pager moves focus to the nearest step that can still move, never into
200
+ the rows. It moves focus only after a press in the pager, and never while `loading`.
201
+ Clearing the page-jump box, or typing into it and then pressing a step, does not change the
202
+ page.
203
+
204
+ `pageSize` defaults to `DEFAULT_PAGE_SIZE` (100). **Breaking:** it used to default to 4.
@@ -25,7 +25,12 @@
25
25
  backdrop-filter: blur(2px);
26
26
  display: grid;
27
27
  place-items: center;
28
- z-index: 50;
28
+ z-index: calc(var(--z-overlay) + 1);
29
+ opacity: 0;
30
+ transition: opacity var(--dur-med) var(--ease);
31
+ }
32
+ .rx-scrim.is-open {
33
+ opacity: 1;
29
34
  }
30
35
  .rx-modal {
31
36
  width: min(480px, 92vw);
@@ -34,6 +39,11 @@
34
39
  border-radius: var(--radius-xl);
35
40
  box-shadow: var(--shadow-lg);
36
41
  overflow: hidden;
42
+ transform: translateY(8px);
43
+ transition: transform var(--dur-med) var(--ease);
44
+ }
45
+ .rx-scrim.is-open .rx-modal {
46
+ transform: none;
37
47
  }
38
48
  .rx-modal__head {
39
49
  display: flex;
@@ -80,15 +90,3 @@
80
90
  .rx-caret {
81
91
  margin-left: 2px;
82
92
  }
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
- }
@@ -23,9 +23,11 @@ declare function Badge({ variant, children }: {
23
23
  children: ReactNode;
24
24
  }): react.JSX.Element;
25
25
 
26
- declare function Card({ title, sub, children }: {
26
+ type Level = 2 | 3 | 4 | 5 | 6;
27
+ declare function Card({ title, sub, level, children }: {
27
28
  title?: ReactNode;
28
29
  sub?: ReactNode;
30
+ level?: Level;
29
31
  children?: ReactNode;
30
32
  }): react.JSX.Element;
31
33
 
@@ -38,6 +40,63 @@ type ModalProps = {
38
40
  };
39
41
  declare function Modal({ open, title, onClose, footer, children }: ModalProps): react.ReactPortal | null;
40
42
 
43
+ type DrawerProps = {
44
+ open: boolean;
45
+ title: string;
46
+ onClose: () => void;
47
+ side?: 'right' | 'left' | 'top' | 'bottom';
48
+ size?: 'sm' | 'md' | 'lg';
49
+ footer?: ReactNode;
50
+ children?: ReactNode;
51
+ closeLabel?: string;
52
+ };
53
+ declare function Drawer({ open, title, onClose, side, size, footer, children, closeLabel, }: DrawerProps): react.ReactPortal | null;
54
+
55
+ type CommandItem = {
56
+ id: string;
57
+ label: string;
58
+ description?: string;
59
+ icon?: string;
60
+ /** Aliases a reader might type instead of the label. */
61
+ keywords?: string[];
62
+ /** Drawn as <kbd>, never read out: the row already says what it does. */
63
+ shortcut?: string | string[];
64
+ badge?: string;
65
+ /** A row that goes somewhere. Cmd or Ctrl held on Enter opens it in a tab. */
66
+ href?: string;
67
+ /** A row that destroys something. It must carry `onConfirm`, or it is disabled. */
68
+ danger?: boolean;
69
+ /** What a destructive row opens instead of running. */
70
+ onConfirm?: () => void;
71
+ disabled?: boolean;
72
+ };
73
+ type CommandGroup = {
74
+ label?: string;
75
+ items: CommandItem[];
76
+ };
77
+ type CommandPaletteProps = {
78
+ open: boolean;
79
+ onClose: () => void;
80
+ groups: CommandGroup[];
81
+ /** What the reader chose. `newTab` is Cmd or Ctrl held, for a row with an href. */
82
+ onSelect?: (item: CommandItem, meta: {
83
+ newTab: boolean;
84
+ }) => void;
85
+ label?: string;
86
+ placeholder?: string;
87
+ empty?: string;
88
+ density?: 'compact' | 'roomy';
89
+ hint?: boolean;
90
+ /**
91
+ * false → the caller ranks. `groups` is rendered in the order it is given and
92
+ * `onQueryChange` is where the query goes — the shape a palette fed by a
93
+ * server takes, where the rest of the results are not on this page to rank.
94
+ */
95
+ rank?: boolean;
96
+ onQueryChange?: (query: string) => void;
97
+ };
98
+ declare function CommandPalette({ open, onClose, groups, onSelect, label, placeholder, empty, density, hint, rank, onQueryChange, }: CommandPaletteProps): react.ReactPortal | null;
99
+
41
100
  type Column<T> = {
42
101
  key: keyof T & string;
43
102
  label: string;
@@ -60,11 +119,39 @@ type SelectionProps = {
60
119
  onToggle: (name: string) => void;
61
120
  onTogglePage: (names: string[]) => void;
62
121
  };
122
+ type PagerProps = {
123
+ page?: never;
124
+ onPageChange?: (page: number) => void;
125
+ total?: never;
126
+ hasMore?: never;
127
+ } | {
128
+ page: number;
129
+ onPageChange: (page: number) => void;
130
+ total: number;
131
+ hasMore?: boolean;
132
+ } | {
133
+ page: number;
134
+ onPageChange: (page: number) => void;
135
+ total: null;
136
+ hasMore: boolean;
137
+ };
63
138
  type DataTableProps<T> = {
64
139
  columns: Column<T>[];
65
140
  rows: T[];
66
141
  pageSize?: number;
67
- } & SelectionProps & ({
142
+ pageSizes?: readonly number[] | null;
143
+ onPageSizeChange?: (size: number) => void;
144
+ /** `false` renders no pager at all — for a surface that supplies its own. */
145
+ pager?: boolean;
146
+ /**
147
+ * The pager's accessible name. Two tables on one page otherwise publish two
148
+ * landmarks called "Pagination", and a reader listing the landmarks cannot tell
149
+ * which one moves which table. Axe will not catch it: `landmark-unique` is a
150
+ * best-practice rule and the kit's gate runs only the WCAG A/AA tags.
151
+ */
152
+ pagerLabel?: string;
153
+ loading?: boolean;
154
+ } & SelectionProps & PagerProps & ({
68
155
  sort?: never;
69
156
  onSortChange?: (sort: TableSort<T>) => void;
70
157
  } | {
@@ -74,7 +161,24 @@ type DataTableProps<T> = {
74
161
  declare function sortTableRows<T>(rows: T[], sort: TableSort<T>): T[];
75
162
  declare function DataTable<T extends {
76
163
  name: string;
77
- }>({ columns, rows, pageSize, selectable, selected, onToggle, onTogglePage, sort: controlledSort, onSortChange, }: DataTableProps<T>): react.JSX.Element;
164
+ }>({ 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;
165
+
166
+ type PaginationProps = {
167
+ page?: number;
168
+ pageSize?: number;
169
+ /** Rows in the whole result. `null` — the honest answer for a caller who cannot count. */
170
+ total?: number | null;
171
+ /** Read only when `total` is null: whether a page exists after this one. */
172
+ hasMore?: boolean;
173
+ pageSizes?: readonly number[] | null;
174
+ variant?: 'steps' | 'numbered' | 'jump';
175
+ label?: string;
176
+ loading?: boolean;
177
+ id?: string;
178
+ onPageChange?: (page: number) => void;
179
+ onPageSizeChange?: (size: number) => void;
180
+ };
181
+ declare function Pagination({ page, pageSize, total, hasMore, pageSizes, variant, label, loading, id, onPageChange, onPageSizeChange, }: PaginationProps): react.JSX.Element | null;
78
182
 
79
183
  type SkeletonProps = {
80
184
  /** A count of bars, or explicit widths when a ragged prose edge matters. */
@@ -115,4 +219,7 @@ type DeniedProps = {
115
219
  };
116
220
  declare function Denied({ title, sub, need, icon, className, children, }: DeniedProps): react.JSX.Element;
117
221
 
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 };
222
+ declare const PAGE_SIZES: readonly number[];
223
+ declare const DEFAULT_PAGE_SIZE: number;
224
+
225
+ export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, Denied, type DeniedProps, Drawer, type DrawerProps, Icon, Modal, type ModalProps, PAGE_SIZES, Pagination, type PaginationProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, type TableSort, sortTableRows };