@tristan2828/ui-foundation 4.44.0 → 4.46.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/conventions/AGENTS.md +2 -1
- package/conventions/docs/add-an-entity.md +19 -10
- package/conventions/docs/cell-patterns.md +111 -32
- package/conventions/docs/design-language.md +82 -21
- package/dist/components/app/data-table.d.ts +61 -4
- package/dist/components/app/data-table.js +217 -17
- package/dist/components/app/entity-view.d.ts +14 -1
- package/dist/components/app/entity-view.js +5 -3
- package/dist/components/app/key-stats.d.ts +14 -0
- package/dist/components/app/key-stats.js +6 -0
- package/dist/components/app/table-groups.d.ts +12 -0
- package/dist/components/app/table-groups.js +37 -0
- package/dist/components/app/widget-card.d.ts +29 -1
- package/dist/components/app/widget-card.js +61 -5
- package/dist/components/app/widget-layout-sheet.d.ts +25 -0
- package/dist/components/app/widget-layout-sheet.js +55 -0
- package/dist/hooks/table-url-changes.d.ts +7 -0
- package/dist/hooks/table-url-changes.js +1 -0
- package/dist/hooks/use-table-layout.d.ts +1 -0
- package/dist/hooks/use-table-layout.js +2 -1
- package/dist/hooks/use-table-url-state.d.ts +11 -1
- package/dist/hooks/use-table-url-state.js +26 -2
- package/dist/hooks/use-widget-layout.d.ts +45 -0
- package/dist/hooks/use-widget-layout.js +128 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/conventions/AGENTS.md
CHANGED
|
@@ -87,7 +87,8 @@ Every data view handles: loading, empty, error, and success. Use
|
|
|
87
87
|
`<Skeleton>`, `<Empty>`, and the error boundary — `DataTable` already does,
|
|
88
88
|
and `EntityView` (one record) adds not found: a plain 404, never a retry.
|
|
89
89
|
A small widget on a hub page is a `WidgetCard`, which holds all four for
|
|
90
|
-
its own data, its error inside the card
|
|
90
|
+
its own data, its error inside the card (folded, its `summary` follows
|
|
91
|
+
the same states); a hub's place (a module, with
|
|
91
92
|
one live number) is a `ModuleCard` in a `ModuleStrip`, the same way.
|
|
92
93
|
Every screen has one Playwright test per state, forced via MSW overrides
|
|
93
94
|
(`@tristan2828/ui-foundation/testing`).
|
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
6
|
- [The reference files](#the-reference-files) · lines 37–59
|
|
7
|
-
- [Before anything: the entity plan](#before-anything-the-entity-plan) · lines 61–
|
|
7
|
+
- [Before anything: the entity plan](#before-anything-the-entity-plan) · lines 61–715
|
|
8
8
|
- Step 1: Add `<Entity>` to `openapi.yaml` · lines 99–106
|
|
9
9
|
- Step 2: `npm run gen:api` · lines 107–108
|
|
10
10
|
- Step 3: Add gateway tests in `tests/gateway/<entity>.test.ts`, derived from the spec · lines 109–120
|
|
11
11
|
- Step 4: Add MSW handlers in `src/mocks/<entity>.ts` · lines 121–130
|
|
12
12
|
- Step 5: Add a gateway module in `src/api/gateway/<entity>.ts` · lines 131–135
|
|
13
|
-
- Step 6: Copy the reference files, one for one, not just the two screens · lines 136–
|
|
14
|
-
- Step 7: Register routes · lines
|
|
15
|
-
- Step 8: Add Playwright specs for all three screens · lines
|
|
16
|
-
- Step 9: Register the nav entry and route names · lines
|
|
17
|
-
- Step 10: `npm run verify`. · lines
|
|
18
|
-
- [What not to copy](#what-not-to-copy) · lines
|
|
19
|
-
- [After a commit, in the backend](#after-a-commit-in-the-backend) · lines
|
|
20
|
-
- [Deleting](#deleting) · lines
|
|
21
|
-
- [Fields only a script writes](#fields-only-a-script-writes) · lines
|
|
13
|
+
- Step 6: Copy the reference files, one for one, not just the two screens · lines 136–607
|
|
14
|
+
- Step 7: Register routes · lines 608–618
|
|
15
|
+
- Step 8: Add Playwright specs for all three screens · lines 619–697
|
|
16
|
+
- Step 9: Register the nav entry and route names · lines 698–712
|
|
17
|
+
- Step 10: `npm run verify`. · lines 713–715
|
|
18
|
+
- [What not to copy](#what-not-to-copy) · lines 717–743
|
|
19
|
+
- [After a commit, in the backend](#after-a-commit-in-the-backend) · lines 745–762
|
|
20
|
+
- [Deleting](#deleting) · lines 764–793
|
|
21
|
+
- [Fields only a script writes](#fields-only-a-script-writes) · lines 795–816
|
|
22
22
|
|
|
23
23
|
<!-- /index -->
|
|
24
24
|
|
|
@@ -410,6 +410,15 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
410
410
|
view is `rail`, with Details in the rail. The rail's sections come
|
|
411
411
|
first in the page (header, rail, main) at every width, so the
|
|
412
412
|
view's specs find them first too.
|
|
413
|
+
- **A subtitle** (`subtitle`), only when the plan names a line that
|
|
414
|
+
says what the record is for (a goal, a one-line summary): under the
|
|
415
|
+
title in muted body type. When the plan marks it editable, wrap it
|
|
416
|
+
in `<EditableValue label=… edit={editInPlace(…)} layout="inline">`.
|
|
417
|
+
- **Key numbers** (`strip`), only when the plan names a few figures
|
|
418
|
+
read before anything else (done of total, time left, the next
|
|
419
|
+
date): `<KeyStats stats={[{ label, value, hint? }]} />`, under the
|
|
420
|
+
header across the page, so on a phone it comes before the rail and
|
|
421
|
+
the main column. Three to five; more belong in a section.
|
|
413
422
|
- **A notice** (`notice`), only when the plan names a state that
|
|
414
423
|
decides what you do with the record (held, overdue, archived and
|
|
415
424
|
read-only): a `<Notice tone=…>` saying what it means and what to do,
|
|
@@ -3,37 +3,38 @@
|
|
|
3
3
|
<!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
|
-
- [The rules that apply to every cell](#the-rules-that-apply-to-every-cell) · lines
|
|
7
|
-
- [1. Plain text](#1-plain-text) · lines
|
|
8
|
-
- [2. Icon + label](#2-icon--label) · lines
|
|
9
|
-
- [3. Icon in a badge](#3-icon-in-a-badge) · lines
|
|
10
|
-
- [4. Icon only, label in a tooltip](#4-icon-only-label-in-a-tooltip) · lines
|
|
11
|
-
- [Several values: the icons side by side, every name in one tooltip](#several-values-the-icons-side-by-side-every-name-in-one-tooltip) · lines
|
|
12
|
-
- [5. Enum → tone-mapped badge](#5-enum--tone-mapped-badge) · lines
|
|
13
|
-
- [6. Multi-value enum, clustered](#6-multi-value-enum-clustered) · lines
|
|
14
|
-
- [7. Link as an outline button](#7-link-as-an-outline-button) · lines
|
|
15
|
-
- [8. Date, relative](#8-date-relative) · lines
|
|
16
|
-
- [9. Ordinal scale (bounded integer)](#9-ordinal-scale-bounded-integer) · lines
|
|
17
|
-
- [Two variants the package draws: a step bar and a star](#two-variants-the-package-draws-a-step-bar-and-a-star) · lines
|
|
18
|
-
- [10. Summary in the cell, detail in a popover](#10-summary-in-the-cell-detail-in-a-popover) · lines
|
|
19
|
-
- [A second example: what a row depends on](#a-second-example-what-a-row-depends-on) · lines
|
|
20
|
-
- [11. Number or currency — Unproven](#11-number-or-currency--unproven) · lines
|
|
21
|
-
- [12. Boolean](#12-boolean) · lines
|
|
22
|
-
- [13. Progress bar — Unproven](#13-progress-bar--unproven) · lines
|
|
23
|
-
- [The compact form: `ProgressRing`](#the-compact-form-progressring) · lines
|
|
24
|
-
- [14. Avatar and name — Unproven](#14-avatar-and-name--unproven) · lines
|
|
25
|
-
- [15. Yes/no you flip in the row](#15-yesno-you-flip-in-the-row) · lines
|
|
26
|
-
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines
|
|
27
|
-
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines
|
|
28
|
-
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines
|
|
29
|
-
- [18. Editable value](#18-editable-value) · lines
|
|
30
|
-
- [A list of sub-items](#a-list-of-sub-items) · lines
|
|
31
|
-
- [In a table's rows](#in-a-tables-rows) · lines
|
|
32
|
-
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines
|
|
33
|
-
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines
|
|
34
|
-
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines
|
|
35
|
-
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines
|
|
36
|
-
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines
|
|
6
|
+
- [The rules that apply to every cell](#the-rules-that-apply-to-every-cell) · lines 66–85
|
|
7
|
+
- [1. Plain text](#1-plain-text) · lines 87–94
|
|
8
|
+
- [2. Icon + label](#2-icon--label) · lines 96–119
|
|
9
|
+
- [3. Icon in a badge](#3-icon-in-a-badge) · lines 121–134
|
|
10
|
+
- [4. Icon only, label in a tooltip](#4-icon-only-label-in-a-tooltip) · lines 136–213
|
|
11
|
+
- [Several values: the icons side by side, every name in one tooltip](#several-values-the-icons-side-by-side-every-name-in-one-tooltip) · lines 161–213
|
|
12
|
+
- [5. Enum → tone-mapped badge](#5-enum--tone-mapped-badge) · lines 215–228
|
|
13
|
+
- [6. Multi-value enum, clustered](#6-multi-value-enum-clustered) · lines 230–242
|
|
14
|
+
- [7. Link as an outline button](#7-link-as-an-outline-button) · lines 244–265
|
|
15
|
+
- [8. Date, relative](#8-date-relative) · lines 267–304
|
|
16
|
+
- [9. Ordinal scale (bounded integer)](#9-ordinal-scale-bounded-integer) · lines 306–354
|
|
17
|
+
- [Two variants the package draws: a step bar and a star](#two-variants-the-package-draws-a-step-bar-and-a-star) · lines 324–354
|
|
18
|
+
- [10. Summary in the cell, detail in a popover](#10-summary-in-the-cell-detail-in-a-popover) · lines 356–406
|
|
19
|
+
- [A second example: what a row depends on](#a-second-example-what-a-row-depends-on) · lines 374–406
|
|
20
|
+
- [11. Number or currency — Unproven](#11-number-or-currency--unproven) · lines 408–448
|
|
21
|
+
- [12. Boolean](#12-boolean) · lines 450–480
|
|
22
|
+
- [13. Progress bar — Unproven](#13-progress-bar--unproven) · lines 482–518
|
|
23
|
+
- [The compact form: `ProgressRing`](#the-compact-form-progressring) · lines 502–518
|
|
24
|
+
- [14. Avatar and name — Unproven](#14-avatar-and-name--unproven) · lines 520–537
|
|
25
|
+
- [15. Yes/no you flip in the row](#15-yesno-you-flip-in-the-row) · lines 539–648
|
|
26
|
+
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 565–648
|
|
27
|
+
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 650–685
|
|
28
|
+
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 687–760
|
|
29
|
+
- [18. Editable value](#18-editable-value) · lines 762–1208
|
|
30
|
+
- [A list of sub-items](#a-list-of-sub-items) · lines 1024–1109
|
|
31
|
+
- [In a table's rows](#in-a-tables-rows) · lines 1111–1196
|
|
32
|
+
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1198–1208
|
|
33
|
+
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1210–1276
|
|
34
|
+
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1278–1342
|
|
35
|
+
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1344–1412
|
|
36
|
+
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1414–1442
|
|
37
|
+
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines 1444–1518
|
|
37
38
|
|
|
38
39
|
<!-- /index -->
|
|
39
40
|
|
|
@@ -1405,7 +1406,10 @@ const { page, filters, setFilter, setPage } = useTableUrlState(['kind'])
|
|
|
1405
1406
|
- **No rows for a filter** is the table's own empty state, saying which
|
|
1406
1407
|
filter, with a button to take it off. An empty group never renders:
|
|
1407
1408
|
groups come from the rows.
|
|
1408
|
-
- **
|
|
1409
|
+
- **Headings that fold, picked by the reader** are pattern 23's
|
|
1410
|
+
`groupings`: a Group menu over choice fields. `groupBy` stays the
|
|
1411
|
+
app's one fixed grouping (a feed's days). **Not built:** grouping by
|
|
1412
|
+
more than one key.
|
|
1409
1413
|
|
|
1410
1414
|
## 22. A highlighted row, and rows that are done
|
|
1411
1415
|
|
|
@@ -1437,3 +1441,78 @@ current one. `DataTable`'s `highlightRow` returns true for it.
|
|
|
1437
1441
|
the token check in both themes; `e2e/data-table.spec.ts` checks every
|
|
1438
1442
|
cell wears the wash.
|
|
1439
1443
|
|
|
1444
|
+
## 23. A Group menu, and a record read beside the list
|
|
1445
|
+
|
|
1446
|
+
A working list (tasks, chores, a backlog) is read two ways besides its
|
|
1447
|
+
columns: **grouped by one of its choice fields**, picked by the reader
|
|
1448
|
+
(by status to see what's moving, by area to plan a trip round the
|
|
1449
|
+
house), and **one record at a time without leaving the list**, its notes
|
|
1450
|
+
and key facts in a panel beside the rows. `DataTable`'s `groupings` and
|
|
1451
|
+
`detailPanel` (4.46) do both. Raised for an app's task list; the
|
|
1452
|
+
package's Storybook renders it (`app/DataTable grouping`), with axe in
|
|
1453
|
+
both themes and its specs in a real browser.
|
|
1454
|
+
|
|
1455
|
+
```tsx
|
|
1456
|
+
const GROUPINGS: DataTableGrouping<Task>[] = [ // module level
|
|
1457
|
+
{ id: 'status', label: 'Status', options: STATUS_OPTIONS, value: (task) => task.status },
|
|
1458
|
+
{ id: 'categories', label: 'Category', options: CATEGORY_OPTIONS, value: (task) => task.categories, emptyLabel: 'No category' },
|
|
1459
|
+
{ id: 'canDoNow', label: 'Can do now', options: [{ value: 'yes', label: 'Can do now' }, { value: 'no', label: 'Not yet' }],
|
|
1460
|
+
value: (task) => (task.canDoNow ? 'yes' : 'no') },
|
|
1461
|
+
]
|
|
1462
|
+
|
|
1463
|
+
const { grouping, setGrouping, ...table } = useTableUrlState(FILTERS, MULTI, VIEWS, { groupStorageKey: 'my-app:tasks:group' })
|
|
1464
|
+
|
|
1465
|
+
<DataTable
|
|
1466
|
+
groupings={GROUPINGS}
|
|
1467
|
+
grouping={grouping}
|
|
1468
|
+
onGroupingChange={setGrouping}
|
|
1469
|
+
groupSummary={(tasks) => `${tasks.filter(isReady).length} ready · top ${Math.max(...tasks.map((task) => task.score))}`}
|
|
1470
|
+
detailLabel="Task"
|
|
1471
|
+
detailPanel={(task) => <TaskPreview task={task} />} // its own "Open page" link
|
|
1472
|
+
… />
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
- **None is the default**, and the table is exactly as it was until a
|
|
1476
|
+
grouping is picked. The Group menu sits at the end of the toolbar
|
|
1477
|
+
(before a layout switch): None, then each grouping. While grouped a
|
|
1478
|
+
Fold all button joins it, and the menu says which ("Group: Status").
|
|
1479
|
+
- **Only choice fields**: each group is one of the field's options, in
|
|
1480
|
+
the options' order (the order the form lists them), so give the same
|
|
1481
|
+
options list the column and the form use. A multi-value field (an
|
|
1482
|
+
array) shows the row under each of its values; a row with none goes
|
|
1483
|
+
last, under `emptyLabel` ("Not set" unless set). A yes/no field is two
|
|
1484
|
+
options, worded as headings ("Can do now", "Not yet").
|
|
1485
|
+
- **The grouped column is hidden** while grouped (its value is the
|
|
1486
|
+
heading), so a grouping's `id` is its column's id.
|
|
1487
|
+
- **A heading is a button** (`aria-expanded`) that folds its rows away:
|
|
1488
|
+
a chevron, the label, the count, then `groupSummary`'s line, given the
|
|
1489
|
+
group's rows *on this page* and the group itself. Folds reset when the
|
|
1490
|
+
grouping changes.
|
|
1491
|
+
- **Remembered like the layout**: `useTableUrlState`'s `grouping` is
|
|
1492
|
+
`?group=` in the URL, then the one remembered under `groupStorageKey`
|
|
1493
|
+
on this device, then None. A saved view can name a `group` (''
|
|
1494
|
+
for None) to turn one on when pressed; a view that names none keeps
|
|
1495
|
+
the grouping that's on. Grouping doesn't change which rows are shown,
|
|
1496
|
+
so it keeps the page and doesn't count toward the active view.
|
|
1497
|
+
- **It groups the page's rows**: paging pages rows, not groups, so a
|
|
1498
|
+
list that groups wants a page size that holds it (a working list of a
|
|
1499
|
+
few dozen), and a summary that must count a whole group across pages
|
|
1500
|
+
comes from the API. A grouping, while on, takes the place of
|
|
1501
|
+
`groupBy`'s headings.
|
|
1502
|
+
- **The detail panel** opens on a click on a row, or Enter on it (its
|
|
1503
|
+
title link included: a plain click picks the row, a modified one still
|
|
1504
|
+
opens the link in a new tab, and the panel's own "Open page" link
|
|
1505
|
+
leaves). A control in a cell (a button, a switch, an editable value)
|
|
1506
|
+
stays the control's. The picked row is marked (`aria-selected`, the
|
|
1507
|
+
muted fill and a bar in `--primary` at its start); ↑ and ↓ move the pick
|
|
1508
|
+
while the list has focus, and Escape or Close closes it, focus back on
|
|
1509
|
+
the row. On a wide screen (64rem and up) the panel sits beside the
|
|
1510
|
+
table and stays in view as the page scrolls; below that it is a
|
|
1511
|
+
bottom sheet.
|
|
1512
|
+
- **The panel is the app's**: a heading-sized title, a few badges, a
|
|
1513
|
+
short notes preview (a few lines of the record's notes, not the whole
|
|
1514
|
+
editor), and an "Open page" link (a `<Link>` in `text-link`, so it's
|
|
1515
|
+
announced as the link it is). Without `detailPanel` rows behave as they
|
|
1516
|
+
always have: the title link opens the record's page.
|
|
1517
|
+
- **The table layout's alone:** a card layout's cards don't open the
|
|
1518
|
+
panel.
|
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
<!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
|
-
- [Semantic tones](#semantic-tones) · lines
|
|
7
|
-
- [The three badge styles](#the-three-badge-styles) · lines
|
|
8
|
-
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines
|
|
9
|
-
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines
|
|
10
|
-
- [Logos and brand marks](#logos-and-brand-marks) · lines
|
|
11
|
-
- [Categorical colour](#categorical-colour) · lines
|
|
12
|
-
- [Typography](#typography) · lines
|
|
13
|
-
- [Written text (notes)](#written-text-notes) · lines
|
|
14
|
-
- [Density](#density) · lines
|
|
15
|
-
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines
|
|
16
|
-
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines
|
|
17
|
-
- [Notices](#notices) · lines
|
|
18
|
-
- [A value over time](#a-value-over-time) · lines
|
|
19
|
-
- [A list as a board](#a-list-as-a-board) · lines
|
|
20
|
-
- [A list as cards](#a-list-as-cards) · lines
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
6
|
+
- [Semantic tones](#semantic-tones) · lines 33–61
|
|
7
|
+
- [The three badge styles](#the-three-badge-styles) · lines 63–80
|
|
8
|
+
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 82–111
|
|
9
|
+
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 113–126
|
|
10
|
+
- [Logos and brand marks](#logos-and-brand-marks) · lines 128–172
|
|
11
|
+
- [Categorical colour](#categorical-colour) · lines 174–244
|
|
12
|
+
- [Typography](#typography) · lines 246–328
|
|
13
|
+
- [Written text (notes)](#written-text-notes) · lines 277–328
|
|
14
|
+
- [Density](#density) · lines 330–387
|
|
15
|
+
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 389–503
|
|
16
|
+
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 505–563
|
|
17
|
+
- [Notices](#notices) · lines 565–593
|
|
18
|
+
- [A value over time](#a-value-over-time) · lines 595–628
|
|
19
|
+
- [A list as a board](#a-list-as-a-board) · lines 630–681
|
|
20
|
+
- [A list as cards](#a-list-as-cards) · lines 683–729
|
|
21
|
+
- [Key numbers on a record](#key-numbers-on-a-record) · lines 731–753
|
|
22
|
+
- [Picking at random](#picking-at-random) · lines 755–786
|
|
23
|
+
- [Filling in what's missing](#filling-in-whats-missing) · lines 788–838
|
|
23
24
|
|
|
24
25
|
<!-- /index -->
|
|
25
26
|
|
|
@@ -446,7 +447,42 @@ const { data, isLoading, error, refetch } = useReadyTasksQuery() // one page of
|
|
|
446
447
|
(`"<app>:hub.<widget>"`) so a card folded stays folded on that device,
|
|
447
448
|
and `defaultCollapsed` for one that should start folded. The chevron
|
|
448
449
|
at the header's end is named by the title; the header's link stays
|
|
449
|
-
while folded.
|
|
450
|
+
while folded. Give a card that folds a `summary`: one short value its
|
|
451
|
+
header shows while folded (`summary={data?.total}`, `"Due today"`),
|
|
452
|
+
so a hub of folded cards still reads at a glance. It follows the
|
|
453
|
+
card's state: a small skeleton while loading, a muted dash on an error
|
|
454
|
+
(the error itself is in the body, opened), and `emptySummary`
|
|
455
|
+
(`"None"`) or nothing when empty.
|
|
456
|
+
- **One open at a time on a phone:** put the cards in a
|
|
457
|
+
`WidgetCardGroup accordion="mobile"` (its `className` is the grid),
|
|
458
|
+
give each an `id`, and name the one to start open in `defaultOpen`.
|
|
459
|
+
Below the sidebar's breakpoint every card folds, opening one folds the
|
|
460
|
+
others, and a tap anywhere on a header (not its link) toggles it; on a
|
|
461
|
+
desktop the cards keep their own `collapsible`. `accordion` alone does
|
|
462
|
+
it at every width.
|
|
463
|
+
- **Letting someone order the cards:** keep the order in
|
|
464
|
+
`useWidgetLayout('<app>:hub', ids)` (every card's id, in the default
|
|
465
|
+
order), render `layout.visible`, and put a `WidgetLayoutSheet` (its
|
|
466
|
+
Customize button) beside the page's title, with each card's title by id.
|
|
467
|
+
Its rows have Up, Down and a Show switch, with Reset: the same on a
|
|
468
|
+
phone, a desktop and a keyboard (the focus stays on the button
|
|
469
|
+
pressed). The order is kept on the device; a removed id drops out and
|
|
470
|
+
a new one falls in at its default place. To follow the person between
|
|
471
|
+
devices, pass `{ store }`, a `WidgetLayoutStore` over the app's
|
|
472
|
+
backend.
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
const HUB_CARDS = { ready: 'Ready to start', waiting: 'Waiting on', links: 'Saved links' }
|
|
476
|
+
const layout = useWidgetLayout('my-app:hub', Object.keys(HUB_CARDS))
|
|
477
|
+
|
|
478
|
+
<div className="flex items-center justify-between gap-3">
|
|
479
|
+
<h1 className="type-page-title text-foreground">Home</h1>
|
|
480
|
+
<WidgetLayoutSheet layout={layout} titles={HUB_CARDS} />
|
|
481
|
+
</div>
|
|
482
|
+
<WidgetCardGroup accordion="mobile" defaultOpen="ready" className="grid … gap-4">
|
|
483
|
+
{layout.visible.map((id) => <HubCard key={id} id={id} />)}
|
|
484
|
+
</WidgetCardGroup>
|
|
485
|
+
```
|
|
450
486
|
- **A row is one line:** its text is a link (`to` for a route in the
|
|
451
487
|
app, `href` for an external address, which opens in a new tab with the
|
|
452
488
|
external-link mark) or plain text, cut with an ellipsis, the whole of
|
|
@@ -462,7 +498,8 @@ const { data, isLoading, error, refetch } = useReadyTasksQuery() // one page of
|
|
|
462
498
|
many 288px-or-wider columns as fit). Each card is as tall as its rows;
|
|
463
499
|
it doesn't stretch to its tallest neighbour.
|
|
464
500
|
- Storybook's `app/WidgetCard` shows three widgets on a page and every
|
|
465
|
-
one in each state
|
|
501
|
+
one in each state, a folded summary in each state and an accordion,
|
|
502
|
+
and `app/WidgetLayoutSheet` a hub with its Customize sheet; its checks run axe and the token and glyph checks in
|
|
466
503
|
both themes and at `large`.
|
|
467
504
|
|
|
468
505
|
## Module cards (a hub's places)
|
|
@@ -648,8 +685,8 @@ the table's.
|
|
|
648
685
|
`DataTable`'s `layouts` (4.39) draw the same rows as cards beside the
|
|
649
686
|
table: a thumbnail grid for a feed, covers for a collection, a dense line
|
|
650
687
|
each for clearing a backlog. They are layouts of one list, not other
|
|
651
|
-
pages: the toolbar, filters, saved views, paging
|
|
652
|
-
are the table's in every one, and so are its loading, empty and error
|
|
688
|
+
pages: the toolbar, filters, saved views, paging, `groupBy` headings
|
|
689
|
+
and a picked grouping's folding ones (cell pattern 23) are the table's in every one, and so are its loading, empty and error
|
|
653
690
|
states.
|
|
654
691
|
|
|
655
692
|
```tsx
|
|
@@ -691,6 +728,30 @@ const { layout, setLayout } = useTableLayout({
|
|
|
691
728
|
- `totals` and `highlightRow` are the table's alone. Sorting is the
|
|
692
729
|
toolbar's in a card layout (there is no header to click).
|
|
693
730
|
|
|
731
|
+
## Key numbers on a record
|
|
732
|
+
|
|
733
|
+
`KeyStats` (4.46), in `EntityView`'s `strip`: the few figures a record's
|
|
734
|
+
page is read for first (done of total, time left, the next date), in a
|
|
735
|
+
row under the header, across the page at every width.
|
|
736
|
+
|
|
737
|
+
```tsx
|
|
738
|
+
<EntityView
|
|
739
|
+
subtitle={<EditableValue label="Goal" edit={goals} layout="inline">{goal}</EditableValue>}
|
|
740
|
+
strip={<KeyStats stats={[{ label: 'Done', value: '7', hint: 'of 12' }, { label: 'Next step', value: formatPlainDate(next) }]} />}
|
|
741
|
+
… />
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- **A label over its value**: the label in caption type and muted, the
|
|
745
|
+
value in the section title's size and tabular figures, a `hint`
|
|
746
|
+
under it in caption type. One bordered card, the figures side by side,
|
|
747
|
+
wrapping on a phone. A `<dl>`, so each label is read with its value.
|
|
748
|
+
- **An empty value reads as `emptyLabel`** ("Not set"), in body type and
|
|
749
|
+
muted, never a blank or a dash.
|
|
750
|
+
- **Three to five.** It's what the page is for at a glance; anything
|
|
751
|
+
more is a section's label/value rows.
|
|
752
|
+
- **`subtitle`** is the line under the title: what the record is for,
|
|
753
|
+
in muted body type, editable in place like any value.
|
|
754
|
+
|
|
694
755
|
## Picking at random
|
|
695
756
|
|
|
696
757
|
`SpinWheel` (4.42) picks one of a few candidates by chance, for "what do
|
|
@@ -82,10 +82,43 @@ export type DataTableProps<TData extends Record<string, unknown>> = {
|
|
|
82
82
|
*/
|
|
83
83
|
groupBy?: (row: TData) => RowGroup;
|
|
84
84
|
/**
|
|
85
|
-
* What a heading says after its label ("4 · 1 h 12 m"
|
|
86
|
-
* rows on this page
|
|
85
|
+
* What a heading says after its label ("4 · 1 h 12 m", "3 ready · top
|
|
86
|
+
* 42"), from the group's rows on this page, for `groupBy`'s headings and
|
|
87
|
+
* a grouping's alike. `group` says which group it is.
|
|
87
88
|
*/
|
|
88
|
-
groupSummary?: (rows: TData[]) => ReactNode;
|
|
89
|
+
groupSummary?: (rows: TData[], group: RowGroup) => ReactNode;
|
|
90
|
+
/**
|
|
91
|
+
* The fields the rows can be grouped by, picked in a Group menu at the
|
|
92
|
+
* end of the toolbar: None (the default, the table as it is without
|
|
93
|
+
* this), then each of these. Choice fields: each group is one of the
|
|
94
|
+
* field's options, in the options' order. While grouped, the field's
|
|
95
|
+
* column is hidden (its value is the heading), each heading folds its
|
|
96
|
+
* rows away and says how many there are, with `groupSummary` after it,
|
|
97
|
+
* and a Fold all button joins the menu. A grouping, while on, takes the
|
|
98
|
+
* place of `groupBy`. It groups this page's rows: paging still pages
|
|
99
|
+
* rows.
|
|
100
|
+
*/
|
|
101
|
+
groupings?: readonly DataTableGrouping<TData>[];
|
|
102
|
+
/**
|
|
103
|
+
* The grouping on: a grouping's id, or '' for None, from
|
|
104
|
+
* useTableUrlState's `grouping` (the URL's `?group=`, then the one
|
|
105
|
+
* remembered for this table). An id the table doesn't have is None.
|
|
106
|
+
* Left out, the menu keeps its own state, starting on None.
|
|
107
|
+
*/
|
|
108
|
+
grouping?: string;
|
|
109
|
+
onGroupingChange?: (grouping: string) => void;
|
|
110
|
+
/**
|
|
111
|
+
* A record read beside the list without leaving it. Clicking a row (its
|
|
112
|
+
* title link too; a modified click still opens the link) or Enter on it
|
|
113
|
+
* selects it and opens this panel: beside the table on a wide screen
|
|
114
|
+
* (64rem and up), a bottom sheet below that. ↑ and ↓ move the selection
|
|
115
|
+
* while the list has focus; Escape or Close closes it. Everything in
|
|
116
|
+
* the panel is the app's, an "Open page" link included. The table
|
|
117
|
+
* layout's alone: a card layout's cards are as they are.
|
|
118
|
+
*/
|
|
119
|
+
detailPanel?: (row: TData) => ReactNode;
|
|
120
|
+
/** The panel's heading and landmark name: "Details" unless set. */
|
|
121
|
+
detailLabel?: string;
|
|
89
122
|
/**
|
|
90
123
|
* False hides the column header row, for a list-like table whose columns
|
|
91
124
|
* explain themselves (a thumbnail, a title block, actions). Screen readers
|
|
@@ -132,6 +165,30 @@ export type DataTableProps<TData extends Record<string, unknown>> = {
|
|
|
132
165
|
icon?: IconComponent;
|
|
133
166
|
};
|
|
134
167
|
};
|
|
168
|
+
/** A field DataTable's rows can be grouped by (`groupings`). */
|
|
169
|
+
export type DataTableGrouping<TData> = {
|
|
170
|
+
/**
|
|
171
|
+
* The field's column id, hidden while grouped, and the grouping's id in
|
|
172
|
+
* the URL (`?group=status`) and in a saved view's `group`.
|
|
173
|
+
*/
|
|
174
|
+
id: string;
|
|
175
|
+
/** The menu's word for it ("Status"), and the menu button's while it's on ("Group: Status"). */
|
|
176
|
+
label: string;
|
|
177
|
+
/** The field's options, in order: the groups, their order and their headings. */
|
|
178
|
+
options: readonly {
|
|
179
|
+
value: string;
|
|
180
|
+
label: string;
|
|
181
|
+
}[];
|
|
182
|
+
/**
|
|
183
|
+
* The row's value: one option's value, or a list of them for a
|
|
184
|
+
* multi-value field, whose row then shows under each. A value the
|
|
185
|
+
* options don't name gets a group of its own after theirs, headed by
|
|
186
|
+
* the value itself.
|
|
187
|
+
*/
|
|
188
|
+
value: (row: TData) => string | readonly string[] | null | undefined;
|
|
189
|
+
/** The heading over rows with no value, last: "Not set" unless set ("No category"). */
|
|
190
|
+
emptyLabel?: string;
|
|
191
|
+
};
|
|
135
192
|
/** One of DataTable's card layouts (`layouts`). */
|
|
136
193
|
export type DataTableCardLayout<TData> = {
|
|
137
194
|
/** In the URL (`?layout=covers`) and remembered, so keep it short and stable. */
|
|
@@ -167,4 +224,4 @@ export type DataTableTotals = {
|
|
|
167
224
|
*/
|
|
168
225
|
cells?: Partial<Record<string, ReactNode>>;
|
|
169
226
|
};
|
|
170
|
-
export declare function DataTable<TData extends Record<string, unknown>>({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width, groupBy, groupSummary, header: showHeader, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, }: DataTableProps<TData>): import("react").JSX.Element;
|
|
227
|
+
export declare function DataTable<TData extends Record<string, unknown>>({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width, groupBy, groupSummary, header: showHeader, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, groupings, grouping: groupingProp, onGroupingChange, detailPanel, detailLabel, }: DataTableProps<TData>): import("react").JSX.Element;
|