@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.
@@ -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; a hub's place (a module, with
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–706
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–598
14
- - Step 7: Register routes · lines 599–609
15
- - Step 8: Add Playwright specs for all three screens · lines 610–688
16
- - Step 9: Register the nav entry and route names · lines 689–703
17
- - Step 10: `npm run verify`. · lines 704–706
18
- - [What not to copy](#what-not-to-copy) · lines 708–734
19
- - [After a commit, in the backend](#after-a-commit-in-the-backend) · lines 736–753
20
- - [Deleting](#deleting) · lines 755–784
21
- - [Fields only a script writes](#fields-only-a-script-writes) · lines 786–807
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 65–84
7
- - [1. Plain text](#1-plain-text) · lines 86–93
8
- - [2. Icon + label](#2-icon--label) · lines 95–118
9
- - [3. Icon in a badge](#3-icon-in-a-badge) · lines 120–133
10
- - [4. Icon only, label in a tooltip](#4-icon-only-label-in-a-tooltip) · lines 135–212
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 160–212
12
- - [5. Enum → tone-mapped badge](#5-enum--tone-mapped-badge) · lines 214–227
13
- - [6. Multi-value enum, clustered](#6-multi-value-enum-clustered) · lines 229–241
14
- - [7. Link as an outline button](#7-link-as-an-outline-button) · lines 243–264
15
- - [8. Date, relative](#8-date-relative) · lines 266–303
16
- - [9. Ordinal scale (bounded integer)](#9-ordinal-scale-bounded-integer) · lines 305–353
17
- - [Two variants the package draws: a step bar and a star](#two-variants-the-package-draws-a-step-bar-and-a-star) · lines 323–353
18
- - [10. Summary in the cell, detail in a popover](#10-summary-in-the-cell-detail-in-a-popover) · lines 355–405
19
- - [A second example: what a row depends on](#a-second-example-what-a-row-depends-on) · lines 373–405
20
- - [11. Number or currency — Unproven](#11-number-or-currency--unproven) · lines 407–447
21
- - [12. Boolean](#12-boolean) · lines 449–479
22
- - [13. Progress bar — Unproven](#13-progress-bar--unproven) · lines 481–517
23
- - [The compact form: `ProgressRing`](#the-compact-form-progressring) · lines 501–517
24
- - [14. Avatar and name — Unproven](#14-avatar-and-name--unproven) · lines 519–536
25
- - [15. Yes/no you flip in the row](#15-yesno-you-flip-in-the-row) · lines 538–647
26
- - [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 564–647
27
- - [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 649–684
28
- - [17. Quick action on the view](#17-quick-action-on-the-view) · lines 686–759
29
- - [18. Editable value](#18-editable-value) · lines 761–1207
30
- - [A list of sub-items](#a-list-of-sub-items) · lines 1023–1108
31
- - [In a table's rows](#in-a-tables-rows) · lines 1110–1195
32
- - [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1197–1207
33
- - [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1209–1275
34
- - [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1277–1341
35
- - [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1343–1408
36
- - [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1410–1438
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
- - **Not built:** collapsing groups, grouping by more than one key.
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 32–60
7
- - [The three badge styles](#the-three-badge-styles) · lines 62–79
8
- - [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 81–110
9
- - [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 112–125
10
- - [Logos and brand marks](#logos-and-brand-marks) · lines 127–171
11
- - [Categorical colour](#categorical-colour) · lines 173–243
12
- - [Typography](#typography) · lines 245–327
13
- - [Written text (notes)](#written-text-notes) · lines 276–327
14
- - [Density](#density) · lines 329–386
15
- - [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 388–466
16
- - [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 468–526
17
- - [Notices](#notices) · lines 528–556
18
- - [A value over time](#a-value-over-time) · lines 558–591
19
- - [A list as a board](#a-list-as-a-board) · lines 593–644
20
- - [A list as cards](#a-list-as-cards) · lines 646–692
21
- - [Picking at random](#picking-at-random) · lines 694–725
22
- - [Filling in what's missing](#filling-in-whats-missing) · lines 727–777
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; its checks run axe and the token and glyph checks in
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 and `groupBy` headings
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"), from the group's
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;