@nxgt/material 3.24.1 → 3.25.1

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 (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -5
  3. package/dist/{components-CbuWrY21.js → components-wLKi88Su.js} +9441 -9204
  4. package/dist/components.js +23 -23
  5. package/dist/lib/components/ui/collections/filter/bar/filter-bar.d.ts +1 -1
  6. package/dist/lib/components/ui/collections/filter/internal.d.ts +1 -0
  7. package/dist/lib/components/ui/collections/filter/query/types.d.ts +11 -0
  8. package/dist/lib/components/ui/collections/filter/sections/filter-band-classes.d.ts +5 -0
  9. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-classes.d.ts +3 -4
  10. package/dist/lib/components/ui/display/description/description.d.ts +26 -3
  11. package/dist/lib/components/ui/display/description/partials/description-plain.d.ts +6 -0
  12. package/dist/lib/components/ui/display/description-list/description-list.context.d.ts +15 -0
  13. package/dist/lib/components/ui/display/description-list/description-list.d.ts +35 -0
  14. package/dist/lib/components/ui/display/description-list/description-list.utils.d.ts +17 -0
  15. package/dist/lib/components/ui/display/description-list/index.d.ts +3 -0
  16. package/dist/lib/components/ui/display/description-list/partials/description-list-group.d.ts +13 -0
  17. package/dist/lib/components/ui/display/index.d.ts +1 -0
  18. package/dist/lib/components/ui/display/list-tile/list-tile-dense.sample.d.ts +25 -0
  19. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +26 -1
  20. package/dist/lib/components/ui/display/list-tile/partials/list-tile-actions.d.ts +8 -0
  21. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +4 -1
  22. package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +5 -1
  23. package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +1 -1
  24. package/dist/lib/components/ui/display/list-tile/partials/list-tile-toggle.d.ts +1 -1
  25. package/dist/lib/components/ui/display/stat-card/stat-card-group.d.ts +11 -3
  26. package/dist/lib/components/ui/layout/app-shell/activity-chrome.d.ts +7 -0
  27. package/dist/lib/components/ui/layout/index.d.ts +1 -0
  28. package/dist/lib/components/ui/layout/responsive-grid/index.d.ts +1 -0
  29. package/dist/lib/components/ui/layout/responsive-grid/min-item.d.ts +13 -0
  30. package/dist/lib/components/ui/layout/responsive-grid/responsive-grid.d.ts +20 -1
  31. package/dist/lib/components/ui/layout/responsive-grid/responsive-grid.utils.d.ts +20 -0
  32. package/dist/lib/components/ui/layout/section-columns/index.d.ts +2 -0
  33. package/dist/lib/components/ui/layout/section-columns/section-columns.d.ts +25 -0
  34. package/dist/lib/components/ui/layout/section-columns/section-columns.utils.d.ts +9 -0
  35. package/dist/main.js +40 -40
  36. package/dist/material.css +1 -1
  37. package/docs/components/data-display.md +121 -8
  38. package/docs/components/data-table-filter-and-lists.md +20 -2
  39. package/docs/components/navigation-and-layout.md +63 -5
  40. package/docs/guides/app-shell.md +45 -1
  41. package/docs/previews/data-display-descriptionlist.webp +0 -0
  42. package/docs/previews/navigation-layout-sectioncolumns.webp +0 -0
  43. package/docs/roadmap.md +14 -0
  44. package/docs/troubleshooting.md +14 -0
  45. package/package.json +1 -1
@@ -20,8 +20,9 @@ from `@nxgt/material/components`.
20
20
  | <img src="../previews/data-display-statusindicator.webp" width="96" alt=""> | [StatusIndicator](#statusindicator) | Coloured status dot with a label |
21
21
  | <img src="../previews/data-display-summarydata.webp" width="96" alt=""> | [SummaryData](#summarydata) | Label / value pairs as a grid or inline |
22
22
  | <img src="../previews/data-display-description.webp" width="96" alt=""> | [Description](#description) | One read-only label / value pair |
23
+ | <img src="../previews/data-display-descriptionlist.webp" width="96" alt=""> | [DescriptionList](#descriptionlist) | A dense auto-fill grid of label / value pairs in a `dl`, with a panels variant |
23
24
  | <img src="../previews/data-display-entityheader.webp" width="96" alt=""> | [EntityHeader](#entityheader) | Page header with status, metadata and actions |
24
- | <img src="../previews/data-display-listtile.webp" width="96" alt=""> | [ListTile](#listtile) | Row with leading / trailing slots and row actions |
25
+ | <img src="../previews/data-display-listtile.webp" width="96" alt=""> | [ListTile](#listtile) | Row with leading / trailing slots, row actions, chips and a divided variant |
25
26
  | <img src="../previews/data-display-accordioncard.webp" width="96" alt=""> | [AccordionCard](#accordioncard) | Accordion rendered from `items` |
26
27
  | <img src="../previews/data-display-timeline.webp" width="96" alt=""> | [Timeline](#timeline) | Vertical event timeline with relative times |
27
28
  | <img src="../previews/data-display-eventchip.webp" width="96" alt=""> | [EventChip](#eventchip) | Calendar event pill, soft, solid or outlined |
@@ -456,6 +457,18 @@ import { StatCard } from '@nxgt/material/components';
456
457
 
457
458
  **`StatCardGroup`: an even grid of compact tiles.** Four hugging tiles leave an orphan at 360 px. Wrap the `size="sm"` tiles in a `StatCardGroup`: while the group is narrower than `md` (28 rem, its own width, not the window's) the tiles are two equal columns, 2×2 for four (an odd last tile takes the whole row); from there they are one row of tiles that hug their content, wrapping if they must. It takes the props of a `div`; the root is `data-slot="stat-card-group"` and `StatCardGroupProps` is exported. `EntityHeader`'s `stats` use it.
458
459
 
460
+ **Beside a title, in a flex row.** The group is a size container, which counts for nothing in a flex row: it used to collapse to a few pixels, its tiles a character wide. It now keeps at least 16 rem (or its parent's width, if less) and grows into the row's free room, so it needs no `flex-1`, `min-w-*` or `basis-*` from you; in a block parent nothing changes. `align` places the tiles once the group is wider than they are: `'start'` (default) or `'end'`, against the group's end edge, without a selector into its children (`data-align` marks it). In a column (as `EntityHeader`'s body) pass `className="grow-0"`, since growing there would be in height. To let the group shrink below the 16 rem floor in a flex row, pass `className="min-w-0"`.
461
+
462
+ ```tsx
463
+ <div className="flex flex-wrap items-center gap-4">
464
+ <h2>Class 6B</h2>
465
+ <StatCardGroup align="end">
466
+ <StatCard size="sm" label="Students" value={22} />
467
+ <StatCard size="sm" label="Classes" value={6} />
468
+ </StatCardGroup>
469
+ </div>
470
+ ```
471
+
459
472
  ```tsx
460
473
  import { StatCard, StatCardGroup } from '@nxgt/material/components';
461
474
 
@@ -471,7 +484,7 @@ Parts: the root carries `data-slot="stat-card"`, `data-variant` and, when a tone
471
484
 
472
485
  In `filled`, the delta and its trend arrow take the on-colour too (the arrow and the sign still say up or down). The outline's share is `--stat-card-ring-share` (`68%`, or `--nxgt-tone-ring-share` for every ring); it draws the tonal card's border, the filled card's edge in a calm zone and the hairline round the icon chip. Known limit: the dark theme's primary `filled` card has an edge of about 2.3:1 on a `Card` and 2.5:1 on the page (its text is 7:1); a `tonal` card avoids it. See [App tones](../guides/theming.md#app-tones-on-shape-fab-avatar-statcard-and-the-metric-cards).
473
486
 
474
- **Storybook:** `Data display/StatCard/Group` (`GroupOnPhone`, `GroupOnDesktop`), `Data display/StatCard/Tones` · **Vue:** `StatCard` (`StatCardGroup` is React only for now)
487
+ **Storybook:** `Data display/StatCard/Group` (`GroupOnPhone`, `GroupOnDesktop`), `Data display/StatCard/Group in a flex row`, `Data display/StatCard/Tones` · **Vue:** `StatCard` (`StatCardGroup` is React only for now)
475
488
 
476
489
  ---
477
490
 
@@ -620,15 +633,77 @@ import { Description } from '@nxgt/material/components';
620
633
  </div>
621
634
  ```
622
635
 
623
- `label` and `value` are its only props; the rest go to the `div`. A falsy
624
- `value` renders nothing, so optional fields need no guard. The label is a small
636
+ `label` and `value` are its main props; the rest go to the `div`. A `value` of `null`, `undefined`, `false` or `''` renders nothing (`0` is shown), so optional fields need no guard. Inside a `variant="panels"` list a `Description` must sit in a `DescriptionList.Group`; otherwise it renders its stand-alone markup. The label is a small
625
637
  muted caption above the value (not a title). `Description` takes one cell of
626
- the grid it is in; pass `className="col-start-full"` for a full-width row.
638
+ the grid it is in; pass `className="col-start-full"` for a full-width row. It takes `lines` anywhere; inside a [`DescriptionList`](#descriptionlist) also `span` and `labelPosition`.
627
639
 
628
640
  **Storybook:** `Data display/Description` · **Vue:** `Description`
629
641
 
630
642
  ---
631
643
 
644
+ ## DescriptionList
645
+
646
+ ![DescriptionList](../previews/data-display-descriptionlist.webp)
647
+
648
+ A record's fields as a dense grid of `Description`s in a `dl`: label on top, hairlines, as many columns as fit.
649
+
650
+ ```tsx
651
+ import { Description, DescriptionList } from '@nxgt/material/components';
652
+
653
+ <DescriptionList meta="Added on 1 Mar 2026 · Modified on 2 Oct 2026">
654
+ <Description label="Full name" value={patient.name} />
655
+ <Description label="Birth date" value={patient.birthDate} />
656
+ <Description label="Address" lines={patient.addressLines} span="full" />
657
+ <Description label="Phone" value={patient.phone} />
658
+ </DescriptionList>
659
+ ```
660
+
661
+ | Prop | Type | Default | Notes |
662
+ | --- | --- | --- | --- |
663
+ | `minItem` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` or a CSS length | `'md'` (14rem) | the narrowest a field gets: 10, 12, 14, 18 or 22rem |
664
+ | `maxColumns` | `1`–`6` | `4` (`panels`: `3`) | the most columns |
665
+ | `meta` | `ReactNode` | — | a muted line under the list |
666
+ | `variant` | `'default' \| 'panels'` | `'default'` | `panels`: tonal groups |
667
+
668
+ Columns are `auto-fill`, so three fields keep narrow, left-aligned columns on a
669
+ wide screen instead of stretching, and there is one column on a phone. The
670
+ layout is preset classes and CSS variables, so it works under a strict CSP; a
671
+ CSS-length `minItem` sets one variable in a `style` attribute, so prefer a
672
+ preset there. Fit three fields or thirty alike.
673
+
674
+ `Description` takes two more props inside a list. `span` is `'full'` (the whole
675
+ row) or a number of columns (used once that many fit; at most `maxColumns`, and
676
+ 4; with a CSS-length `minItem` the thresholds are those of `md`). `labelPosition` is `'top'` or `'inline'` (label beside the value). A full-width
677
+ field uses dense packing: the fields after it fill the cells before it, so they
678
+ may be drawn above it while the DOM and reading order stay the source order.
679
+ Put the full-width field where that reads fine, or last. A multi-line value
680
+ (an address) takes `lines={[…]}` or `children`, never a string to split. Entries of `lines` that are `null`, `false` or `""` are skipped, and a `Description` whose `lines` are all empty and that has no `value` renders nothing.
681
+
682
+ ```tsx
683
+ <DescriptionList variant="panels">
684
+ <DescriptionList.Group title="Contact">
685
+ <Description label="Address" lines={['12 Sample Street', '75001 Exampleville']} />
686
+ <Description label="Phone" value={patient.phone} />
687
+ </DescriptionList.Group>
688
+ <DescriptionList.Group title="Coverage" headingLevel={2}>
689
+ <Description label="Insurer" value={patient.insurer} />
690
+ </DescriptionList.Group>
691
+ </DescriptionList>
692
+ ```
693
+
694
+ `variant="panels"` draws one tonal panel per `DescriptionList.Group` (a `div` with `role="group"`,
695
+ not a landmark, named by its `title`, an `h3` unless `headingLevel` says otherwise, over its
696
+ own `dl`) in a grid of at least 19rem and at most 3 columns. The label sits
697
+ beside the value from the panel's own width (`@xs`, 20rem); `labelPosition`
698
+ pins it. `title` is the heading and `headingLevel` (2 to 6, default 3) its level. `span` has no effect in a panel, and `maxColumns` is clamped to 1 to 6. In the default variant a `Group` only passes its fields through. In `panels`, put the fields in `DescriptionList.Group`s: a `Description` straight in a `panels` list (outside a Group) has no `dl` to sit in, so it falls back to its stand-alone markup.
699
+
700
+ Each pair is a `div` holding a `dt` and a `dd`, valid inside the `dl`; a
701
+ `Description` outside a `DescriptionList` keeps its plain markup.
702
+
703
+ **Storybook:** `Data display/DescriptionList` · **Vue:** later ([#1614](https://github.com/softistx/nxgt-material/issues/1614))
704
+
705
+ ---
706
+
632
707
  ## EntityHeader
633
708
 
634
709
  ![EntityHeader](../previews/data-display-entityheader.webp)
@@ -736,10 +811,13 @@ A tile with an `href` ends with a discreet chevron after `trailing`: decorative
736
811
  | `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref` |
737
812
  | `onClick` | `MouseEventHandler` | — | renders a `<button>` |
738
813
  | `selected` | `boolean` | `false` | the current row: drawn selected; a link gets `aria-current="page"` (your own `aria-current` replaces it), a button `aria-pressed` |
739
- | `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chevron` and `linkComponent` are refused beside it by type |
740
- | `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense navigation row |
814
+ | `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chips`, `chevron` and `linkComponent` are refused beside it by type |
815
+ | `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense row (with `chips`, the record row, see below) |
741
816
  | `leading`, `trailing`, `subtitle` | `ReactNode` | — | slots; `trailing` sits inside the link, so it holds decoration only |
742
817
  | `actions` | `ReactNode` | — | buttons at the end of the tile, outside the link, vertically centred (`data-slot="item-actions"`) |
818
+ | `chips` | `ReactNode` | — | tags after the text, in the compact record row (see below); inside the link, so decoration only; part of the row's accessible name, keep labels short |
819
+ | `variant` | `'tonal' \| 'divided'` | `'tonal'` | `divided`: flat rows with no tonal surface, a hover tint and the record-row layout; the hairline comes with `as="li"` |
820
+ | `as` | `'li'` | — | renders the tile inside a `<li>`, for a `<ul>` you own |
743
821
  | `disabled` | `boolean` | — | disables the link or button, and the `actions` with it |
744
822
 
745
823
  With a router, pass `linkComponent` so a click navigates on the client instead of reloading the page (React Router's `Link` takes `to`, so wrap it):
@@ -776,6 +854,41 @@ export function SettingsRow() {
776
854
  }
777
855
  ```
778
856
 
857
+ **A compact record row.** For a long list of records (patients, students), `chips` adds tags to the row and the tile lays itself out by its own width (a container query: the tile measures itself, so put it in a block, grid or `w-full` flex parent, not a box sized by its content):
858
+
859
+ | Tile width | Layout |
860
+ | --- | --- |
861
+ | under 28rem (`@md`) | title, subtitle under it, chips under the subtitle; `actions` at the end |
862
+ | 28rem and up | the subtitle sits on the title's line |
863
+ | 36rem (`@xl`) and up | the chips sit after the text, before the chevron and the `actions` |
864
+
865
+ `size="sm"` is the dense size, and `actions` stay last at every width. The chips sit inside the link (or button), so they are decoration, never buttons, and they form part of the row's accessible name: keep the chip labels short. On a coarse pointer, the buttons of `actions` at `size="sm"` keep the 44px hit area of an `IconButton` and are spaced so a neighbour never takes it. The 44px spacing applies to the record row (a tile with `chips` or `variant="divided"`); a tile without them renders the markup it always did. A switch row takes `variant` but not `chips`.
866
+
867
+ ```tsx
868
+ import { MoreHorizontal } from 'lucide-react';
869
+ import { Avatar, Chip, IconButton, ListTile } from '@nxgt/material/components';
870
+
871
+ <ul aria-label="Patients" className="m-0 grid list-none gap-1 p-0">
872
+ <ListTile
873
+ as="li"
874
+ size="sm"
875
+ href="/patients/204-118"
876
+ title="Amara Okafor"
877
+ subtitle="54 y · MRN 204-118"
878
+ leading={<Avatar className="size-7 text-xs" tone="primary" fallback="AO" />}
879
+ chips={[
880
+ <Chip key="a" size="sm" color="error" label="Penicillin allergy" />,
881
+ <Chip key="d" size="sm" color="warning" label="Type 2 diabetes" />,
882
+ ]}
883
+ actions={<IconButton size="sm" aria-label="More for Amara Okafor"><MoreHorizontal /></IconButton>}
884
+ />
885
+ </ul>
886
+ ```
887
+
888
+ `variant="divided"` is the flat version for a denser page: no tonal surface, a hover tint and a hairline under each row. Give the `<ul>` no gap (`m-0 list-none p-0`); the hairline is drawn on the `<li>` of an `as="li"` tile, and the last row has none.
889
+
890
+ **A list container: `as="li"`.** There is no `ListTileGroup`: `as="li"` on the tile and a `<ul>` of your own is the smaller API (the `<ul>` carries your `aria-label`, spacing and width, and the tile stays free of a context, so it still renders on the server alone). The `<li>` wraps the tile's own element, since a link or a button cannot be the `<li>`.
891
+
779
892
  **A selected link, for list-detail navigation.** Give `selected` from the router's location: the open row is a link (a shareable URL), `aria-current="page"` and drawn selected. Pass `aria-current="true"` when the selection is not a page.
780
893
 
781
894
  ```tsx
@@ -805,7 +918,7 @@ See also [A `ListTile` link reloads the whole page](../troubleshooting.md#a-list
805
918
 
806
919
  The root carries `data-slot="item-detail"` (a tile that is a button or a plain row too).
807
920
 
808
- **Storybook:** `Data display/ListTile`, `Data display/ListTile links`, `Data display/ListTile current` · **Vue:** `ListTile`
921
+ **Storybook:** `Data display/ListTile`, `Data display/ListTile links`, `Data display/ListTile current`, `Data display/ListTile/Compact record row` · **Vue:** `ListTile`
809
922
 
810
923
  ---
811
924
 
@@ -883,7 +883,7 @@ Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
883
883
  | `title` | `ReactNode` | — | the band's title (a string is an `<h2>`). Leave it out when the page's `h1` is elsewhere (the app bar's): the band then has no heading |
884
884
  | `description` | `ReactNode` | — | a muted line under the `title`, or alone above the band's controls without one; the `actions` stay at the band's end |
885
885
  | `actions` | `ReactNode` | — | the page's actions, at the band's end |
886
- | `bandDensity` | `'comfortable' \| 'compact'` | `'comfortable'` | `'compact'` halves the room above the band (16px to 8px) and under it (12px to 6px), for a list under a sticky app bar and a breadcrumb; the controls keep their size (the Filter's own `density` is the controls') |
886
+ | `bandDensity` | `'comfortable' \| 'compact'` | `'comfortable'` | (the same prop as [`Filter`](#a-compact-band-for-a-list-inside-a-page-section)'s bar) `'compact'` halves the room above the band (16px to 8px) and under it (12px to 6px), for a list under a sticky app bar and a breadcrumb; the controls keep their size (the Filter's own `density` is the controls') |
887
887
  | `pageControls` | `'labelled' \| 'icon'` | `'labelled'` | the footer's Previous / Next: `'icon'` is icon-only, with a tooltip and the names "Previous page" / "Next page" (`DataTable`'s default is unchanged); on a coarse pointer the icon controls are 44x44 boxes (still painted 32px) |
888
888
  | `bandActions` | `'stacked' \| 'inline'` | `'stacked'` | with `filter`, where `actions` sit below 768px: `'stacked'` gives them a row above the search; `'inline'` keeps them on the search's row (the search takes the room left), for a lone icon action such as a "+". An action wider than 3.5rem wraps under the search. From 768px both are the same |
889
889
  | `filterRef` | `Ref<ServerTableFilterHandle>` | — | with `filter`: `filterRef.current.focus()` focuses the Filter's first control, the search, else the first quick filter, "Add filter" or "Filters (N)" the width shows |
@@ -1251,10 +1251,28 @@ Its props are `FilterQueryProps`, its `variant` a `FilterQueryVariant`
1251
1251
  overload per kind, so `<Filter variant="bar" />` does not compile).
1252
1252
  `query` / `defaultQuery` + `onQueryChange` carry the user's query, never a
1253
1253
  locked condition; `density` (a `FilterDensity`: `'comfortable'` or
1254
- `'compact'`), `label`, `searchLabel`, `searchPlaceholder`, `liveDelayMs`
1254
+ `'compact'`, the controls' size and the gap between them), `bandDensity`
1255
+ (`'comfortable'` or `'compact'`, the bar's own vertical room: see below), `label`, `searchLabel`, `searchPlaceholder`, `liveDelayMs`
1255
1256
  (200 ms), `searchApply` and `searchMinLength` tune it. A locked chip is a focusable group named by its text ("Year is 2026-2027"), with "Locked: <reason>" as its description. The other variants
1256
1257
  and `FilterProps` are unchanged.
1257
1258
 
1259
+ #### A compact band for a list inside a page section
1260
+
1261
+ `bandDensity="compact"` is `ServerTable`'s prop, with the same values and the
1262
+ same spacing, on a standalone `Filter variant="bar"`: the bar takes 6px back
1263
+ under it (a `-mb-1.5`, the class `ServerTable`'s band uses), so a short list
1264
+ filtered with `matchQuery` inside a page section does not spend the room of a
1265
+ full page. The controls keep their size: `density` is theirs, and the two are
1266
+ independent. The default is unchanged, and a compact bar carries
1267
+ `data-band-density="compact"`. `variant="bar"` only; a `ServerTable` sets its
1268
+ own band with its own `bandDensity`.
1269
+
1270
+ ```tsx
1271
+ <Filter variant="bar" bandDensity="compact" density="compact" model={model} query={query} onQueryChange={setQuery} />
1272
+ ```
1273
+
1274
+ **Storybook:** `Data table, filter & lists/Filter/Bar/Band density`.
1275
+
1258
1276
  #### The search: apply on Enter, a minimum length
1259
1277
 
1260
1278
  By default the search is live: the text reaches `onQueryChange` (and a
@@ -14,7 +14,8 @@ documentation-site shell. Everything is imported from
14
14
  | <img src="../previews/navigation-layout-topappbar.webp" width="96" alt=""> | [TopAppBar, BottomAppBar](#topappbar) | Top and bottom bars |
15
15
  | <img src="../previews/navigation-layout-breadcrumb.webp" width="96" alt=""> | [Breadcrumb](#breadcrumb) | Breadcrumb primitives |
16
16
  | <img src="../previews/navigation-layout-listdetailslayout.webp" width="96" alt=""> | [ListDetailsLayout, PaneLayout, ThreadLayout](#listdetailslayout) | Page layouts that adapt to phones |
17
- | <img src="../previews/navigation-layout-responsivegrid.webp" width="96" alt=""> | [ResponsiveGrid](#responsivegrid) | Container-query grid, 1 to 5 columns |
17
+ | <img src="../previews/navigation-layout-responsivegrid.webp" width="96" alt=""> | [ResponsiveGrid](#responsivegrid) | Container-query grid, 1 to 5 columns, or `auto-fill` columns by a minimum tile width (`minItem`) |
18
+ | <img src="../previews/navigation-layout-sectioncolumns.webp" width="96" alt=""> | [SectionColumns](#sectioncolumns) | Short page sections side by side from a viewport breakpoint, stacked below it |
18
19
  | <img src="../previews/navigation-layout-resizable.webp" width="96" alt=""> | [ResizableGroup](#resizablegroup) | Resizable panels |
19
20
  | <img src="../previews/navigation-layout-skiplink.webp" width="96" alt=""> | [SkipLink](#skiplink) | Skip-to-content link |
20
21
  | <img src="../previews/navigation-layout-pager.webp" width="96" alt=""> | [Pager](#pager) | Previous / next cards |
@@ -442,7 +443,7 @@ app bar.
442
443
 
443
444
  ![ResponsiveGrid](../previews/navigation-layout-responsivegrid.webp)
444
445
 
445
- Container-query grid that grows from 1 to 5 columns.
446
+ Tiles in a grid that follows its own width: stepped columns from 1 to 5, or `auto-fill` columns by a minimum tile width.
446
447
 
447
448
  ```tsx
448
449
  import { ResponsiveGrid, StatCard } from '@nxgt/material/components';
@@ -452,12 +453,69 @@ import { ResponsiveGrid, StatCard } from '@nxgt/material/components';
452
453
  <StatCard label="Orders" value="1,284" />
453
454
  <StatCard label="Customers" value="812" />
454
455
  </ResponsiveGrid>
456
+
457
+ // A board of small tiles: as many 12rem columns as fit, 1rem apart.
458
+ <ResponsiveGrid minItem="sm" gap="md">
459
+ {rooms.map((room) => <RoomTile key={room.id} room={room} />)}
460
+ </ResponsiveGrid>
455
461
  ```
456
462
 
457
- No props of its own. Columns follow the grid's own width, not the window's:
458
- 2 from `@lg`, 3 from `@2xl`, 4 from `@4xl`, 5 from `@6xl`.
463
+ | Prop | Type | Default | Effect |
464
+ | --- | --- | --- | --- |
465
+ | `minItem` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| string` | none | Fill the row with tiles at least this wide: a preset (`xs` 10rem, `sm` 12rem, `md` 14rem, `lg` 18rem, `xl` 22rem, the same as `DescriptionList`) or a CSS length (`'9rem'`). Without it the columns step by width |
466
+ | `gap` | `'sm' \| 'md' \| 'lg'` | `'sm'` | The space between tiles: 0.5rem, 1rem, 1.5rem |
467
+
468
+ Without `minItem`, columns follow the grid's own width, not the window's: 2
469
+ from `@lg`, 3 from `@2xl`, 4 from `@4xl`, 5 from `@6xl`. With it, the tracks
470
+ are `repeat(auto-fill, minmax(min(<minItem>, 100%), 1fr))`: a few tiles keep
471
+ their narrow width instead of stretching, and a wide screen gains columns
472
+ (a 1920 px box holds eight `md` tiles). A preset is a class that sets a CSS
473
+ variable; a custom length sets that variable in a `style` attribute (the
474
+ variable only, never a grid style), so prefer a preset under a strict CSP
475
+ (`style-src` without `'unsafe-inline'`). The `ResponsiveGrid` props are the
476
+ `div`'s plus these two.
477
+
478
+ **Storybook:** `Navigation & layout/ResponsiveGrid` · **Vue:** `ResponsiveGrid` (without `minItem` and `gap`)
479
+
480
+ ---
481
+
482
+ ## SectionColumns
483
+
484
+ ![SectionColumns](../previews/navigation-layout-sectioncolumns.webp)
485
+
486
+ Short sections of a page side by side from a viewport breakpoint, stacked in source order below it: a record's contact beside its identifiers, its related people beside its allergies.
487
+
488
+ ```tsx
489
+ import { SectionColumns } from '@nxgt/material/components';
490
+
491
+ <SectionColumns from="2xl">
492
+ <section aria-labelledby="contact">
493
+ <h2 id="contact">Contact</h2>
494
+ …
495
+ </section>
496
+ <section aria-labelledby="identifiers">
497
+ <h2 id="identifiers">Identifiers</h2>
498
+ …
499
+ </section>
500
+ </SectionColumns>
501
+ ```
459
502
 
460
- **Storybook:** `Navigation & layout/ResponsiveGrid` · **Vue:** `ResponsiveGrid`
503
+ | Prop | Type | Default | Effect |
504
+ | --- | --- | --- | --- |
505
+ | `from` | `'lg' \| 'xl' \| '2xl'` | `'2xl'` | The viewport breakpoint the sections sit side by side from: 1024, 1280 or 1536 px. Below it they stack, in source order |
506
+ | `columns` | `2 \| 3` | `2` | Equal columns from `from`. A third column wants `2xl`: from `lg` it leaves each section about 20rem on a laptop |
507
+
508
+ The columns are equal (`minmax(0, 1fr)`, so a wide child cannot push the page
509
+ sideways) and the children keep their own height (`items-start`): a short
510
+ section is not stretched to its neighbour's. The gap is 1rem. It is a plain
511
+ `div`, **not a landmark**: each child keeps its own heading (a `section` with
512
+ `aria-labelledby`, or a heading in a `div`), and it adds no region of its own.
513
+ It follows the **viewport**, not its container: for tiles that follow their
514
+ box use [ResponsiveGrid](#responsivegrid) with `minItem`, and for one side
515
+ panel beside a page use `ActivityContent`'s `aside`. It takes the `div`'s
516
+ props, so `className` adds to it.
517
+
518
+ **Storybook:** `Navigation & layout/SectionColumns` · **Vue:** later ([#1614](https://github.com/softistx/nxgt-material/issues/1614))
461
519
 
462
520
  ---
463
521
 
@@ -508,7 +508,8 @@ The layout paints `bg-ground` (the `--ground` token, see [Page ground](./theming
508
508
  on its root, so the content, the gutters and the space between the chrome and
509
509
  the content are tinted, with or without a `backdrop`, in light and dark. The
510
510
  sidebar and the app bar stay on `--background`: flat rectangles with no radius
511
- and no shadow (translucent over a backdrop, about 90 % without one), so the edge
511
+ and no shadow (translucent over a backdrop, about 90 % without one; the sidebar's
512
+ fill runs the page's whole height, out to the frame's edges), so the edge
512
513
  between chrome and content is the change of colour. The cards, and the
513
514
  `ActivityContent` header card (`bg-card`), stand on the ground with their
514
515
  hairline and soft shadow. With `scroll="container"` the same ground stays behind
@@ -888,6 +889,49 @@ decides the layout.
888
889
  - **Width token.** `--nxgt-aside-width` changes `asideWidth="md"` (20rem) app-wide;
889
890
  any CSS length goes in `asideWidth` for one screen.
890
891
 
892
+ ## Dense pages on large screens
893
+
894
+ On a 1440 or 1920 px screen a page should show more, not stretch the same four
895
+ fields wider. Four components do it together, each measuring its own space, and
896
+ the `Dense detail page` recipe (`Guides/Recipes` in Storybook, `screen-recipes`
897
+ in the plugin) assembles them for any record: a patient, a student, a project.
898
+
899
+ | The block is | Use | Behaviour |
900
+ | --- | --- | --- |
901
+ | a record's fields | [`DescriptionList`](../components/data-display.md#descriptionlist) | `auto-fill` columns of at least `minItem` (14rem), at most `maxColumns` (4); one column on a phone; `variant="panels"` for titled groups |
902
+ | short sections of a page | [`SectionColumns`](../components/navigation-and-layout.md#sectioncolumns) | side by side from `from` (`2xl`, 1536px, by default), stacked in source order below it |
903
+ | a board of small tiles | [`ResponsiveGrid minItem`](../components/navigation-and-layout.md#responsivegrid) | more columns as the box widens, no stretching |
904
+ | the rows of a long list | [`ListTile`](../components/data-display.md#listtile) `size="sm" as="li"` with `chips` | subtitle on the title's line from 28rem, chips after the text from 36rem |
905
+ | the filter above such a list | [`Filter`](../components/data-table-filter-and-lists.md#filter) `variant="bar" bandDensity="compact"` | 6px less under the bar; `density` stays the controls' size |
906
+
907
+ ```tsx
908
+ <ActivityContent hideHeader>
909
+ <EntityHeader title={record.name} stats={stats} metadata={facts} />
910
+ <DescriptionList meta="Added on … · Modified on …">
911
+ {fields.map((f) => <Description key={f.label} label={f.label} value={f.value} />)}
912
+ </DescriptionList>
913
+ <SectionColumns>
914
+ <section aria-labelledby="visits">
915
+ <h2 id="visits">Visits</h2>
916
+ <Filter variant="bar" bandDensity="compact" model={model} query={query} onQueryChange={setQuery} />
917
+ <ul className="m-0 grid list-none gap-1 p-0">
918
+ {rows.map((r) => <ListTile key={r.id} as="li" size="sm" title={r.title} chips={<Chip size="sm" label={r.kind} />} href={r.href} />)}
919
+ </ul>
920
+ </section>
921
+ <section aria-labelledby="notes">…</section>
922
+ </SectionColumns>
923
+ </ActivityContent>
924
+ ```
925
+
926
+ - **One `h1`**: the `EntityHeader`'s title, or the shell's `handle.title` (then
927
+ leave `title` out). Each section keeps its own `h2`; `SectionColumns` is a
928
+ plain `div`, not a landmark.
929
+ - **Viewport against container**: `SectionColumns` and `asideFrom` follow the
930
+ window, while `DescriptionList`, `ListTile` and `ResponsiveGrid` follow their
931
+ own box, so the same list works in a column, a dialog or a side panel.
932
+ - **Check it** at 360, 768, 1440 and 1920 px, light and dark: no sideways
933
+ overflow, and no column of empty space beside a short list.
934
+
891
935
  ## Breadcrumbs
892
936
 
893
937
  `AppBreadcrumb` (rendered by `ActivityLayout`) starts with a home icon linking
package/docs/roadmap.md CHANGED
@@ -6,6 +6,20 @@ version they shipped in, and planned entries may name the major they target.
6
6
 
7
7
  ## Now
8
8
 
9
+ - **Dense pages on large screens** — a record's page shows more on a 1440 or
10
+ 1920 px screen instead of stretching the same fields. `DescriptionList` lays
11
+ a record's fields in `auto-fill` columns (`minItem`, `maxColumns`, a `meta`
12
+ line, `span`, `lines`, and `variant="panels"` for titled groups);
13
+ `SectionColumns` puts short sections side by side from a breakpoint (1536 px
14
+ by default) and stacks them below it; `ResponsiveGrid` takes `minItem` and
15
+ `gap` for boards of tiles; `ListTile` becomes a compact record row with
16
+ `chips`, `variant="divided"` and `as="li"`; `Filter` takes `bandDensity` like
17
+ `ServerTable`; `StatCardGroup` no longer collapses in a flex row and takes
18
+ `align`. The `Dense detail page` recipe puts them together for any subject,
19
+ tested at 360, 768, 1440 and 1920 px. React only for now; the Vue gap is
20
+ tracked in issue #1614.
21
+ ([App shell guide](guides/app-shell.md#dense-pages-on-large-screens))
22
+
9
23
  - **Vector drawings in code** (in the next prerelease) — `@nxgt/material/draw`
10
24
  draws a scene of plain data, or JSX primitives (`Rect`, `Path`, `Text`...), as
11
25
  SVG that follows the theme: colours are Material 3 roles and tones, with no
@@ -712,6 +712,20 @@ Vite and React Router apps use:
712
712
 
713
713
  ## Styles & assets
714
714
 
715
+ ### The backdrop shapes disappeared between the cards (3.22+)
716
+
717
+ **When:** an `ActivityLayout` with `backdrop={<Background preset="shapes" ... />}`
718
+ on a page whose cards sit close together. On 3.20 shapes showed in the gaps and
719
+ around the cards; now only the open areas (below the last card, beside a short
720
+ list) hold shapes.
721
+ **Why:** not the page ground (`--ground` is painted by the layout root, under the
722
+ backdrop layer). Since 3.22 a glyph whose box meets a surface, a text or a
723
+ control, within 8px, is dropped whole (the keep-clear pass), so a gap narrower
724
+ than a glyph shows none.
725
+ **Fix:** none needed, it is the documented behaviour (see [the drop rule](./guides/backgrounds.md) in
726
+ the backgrounds guide). To see more shapes, leave open space in the page, or draw
727
+ the backdrop in a layout that has no content over it.
728
+
715
729
  ### The page and the cards look the same
716
730
 
717
731
  **When:** a screen under `ActivityLayout`, after the 3.24 upgrade or on an older
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.24.1",
3
+ "version": "3.25.1",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"