@nxgt/material 3.14.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/components/ui/display/chip/chip.css +12 -2
  4. package/dist/{components-BOwj2IuR.js → components-BOF9frx3.js} +1893 -1774
  5. package/dist/components.js +5 -5
  6. package/dist/{dialog-rOMjlKJ7.js → dialog-YHZJjDrI.js} +1 -0
  7. package/dist/editor.js +3 -3
  8. package/dist/{icon-badge-_tY1MwU9.js → icon-badge-C0TGvF9o.js} +2 -2
  9. package/dist/lib/components/ui/display/chip/chip.d.ts +1 -1
  10. package/dist/lib/components/ui/display/chip/chip.variants.d.ts +5 -2
  11. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog-rules.d.ts +17 -0
  12. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog.d.ts +10 -2
  13. package/dist/lib/components/ui/layout/app-shell/activity-content-aside.d.ts +22 -0
  14. package/dist/lib/components/ui/layout/app-shell/activity-content-aside.types.d.ts +36 -0
  15. package/dist/lib/components/ui/layout/app-shell/activity-content.d.ts +15 -2
  16. package/dist/lib/components/ui/layout/app-shell/index.d.ts +1 -0
  17. package/dist/lib/components/ui/overlays/command/command.d.ts +7 -1
  18. package/dist/lib/components/ui/overlays/dialog/dialog-variants.d.ts +1 -1
  19. package/dist/main.js +5 -5
  20. package/dist/map.js +2 -2
  21. package/dist/material.css +1 -1
  22. package/dist/pdf-reader.js +1 -1
  23. package/dist/{public-D_Gz7wgr.js → public-Bj3rGSfS.js} +1 -1
  24. package/dist/{use-chat-composer-P4GxmCHa.js → use-chat-composer-CoXmJCkr.js} +7 -7
  25. package/docs/components/data-display.md +2 -2
  26. package/docs/components/feedback-and-status.md +4 -0
  27. package/docs/components/navigation-and-layout.md +3 -0
  28. package/docs/components/overlays-and-menus.md +2 -1
  29. package/docs/components/primitives.md +2 -0
  30. package/docs/components/selection-and-choice.md +3 -1
  31. package/docs/guides/app-shell.md +52 -0
  32. package/docs/guides/theming.md +1 -0
  33. package/docs/troubleshooting.md +44 -0
  34. package/lib/components/ui/display/chip/chip.css +12 -2
  35. package/package.json +1 -1
@@ -1,3 +1,3 @@
1
- import { t as e } from "./public-D_Gz7wgr.js";
1
+ import { t as e } from "./public-Bj3rGSfS.js";
2
2
  import { configurePdfReader as t } from "@nxgt/material-core/pdf-reader";
3
3
  export { e as PdfReader, t as configurePdfReader };
@@ -6,7 +6,7 @@ import { c as i, i as a, l as o, n as s, o as c, s as l } from "./dist-lmHmz_bu.
6
6
  import { a as u, o as d } from "./tooltip-CkttDIab.js";
7
7
  import { i as f } from "./dist-C4wtXCjf.js";
8
8
  import { r as p, t as m } from "./in-dialog-Vwkyp-M5.js";
9
- import { C as h, D as g, E as _, O as v, S as y, T as b, _ as x, f as S, g as ee, k as te, r as ne, t as C, u as re, w } from "./dialog-rOMjlKJ7.js";
9
+ import { C as h, D as g, E as _, O as v, S as y, T as b, _ as x, f as S, g as ee, k as te, r as ne, t as C, u as re, w } from "./dialog-YHZJjDrI.js";
10
10
  import { c as T, d as ie, f as E, i as ae, l as oe, n as se, o as D, t as ce, u as O } from "./dropdown-menu-BEyP-A0J.js";
11
11
  import { S as le, a as k, d as A, g as ue, h as de, i as fe, m as pe, n as j, r as me, t as M, u as he, w as ge } from "./toolbar-items-CVQBYp4K.js";
12
12
  import { t as N } from "./use-material-translation-DGeVp-qP.js";
@@ -7,7 +7,7 @@ import { a as o, c as s, i as c, l, n as u, o as d, r as f, s as p, t as m } fro
7
7
  import { _ as h, a as g, g as _, h as v, m as y, o as b, p as x, t as S, v as C, y as w } from "./tooltip-CkttDIab.js";
8
8
  import { a as T, i as E, n as D, r as O, t as k } from "./dist-C4wtXCjf.js";
9
9
  import { i as A, n as ee, t as j } from "./in-dialog-Vwkyp-M5.js";
10
- import { A as M, C as N, D as te, E as ne, O as P, S as re, T as ie, _ as F, b as ae, f as oe, g as se, k as I, l as ce, m as le, n as ue, o as de, r as fe, s as pe, t as me, u as he, v as ge, w as _e, x as ve, y as ye } from "./dialog-rOMjlKJ7.js";
10
+ import { A as M, C as N, D as te, E as ne, O as P, S as re, T as ie, _ as F, b as ae, f as oe, g as se, k as I, l as ce, m as le, n as ue, o as de, r as fe, s as pe, t as me, u as he, v as ge, w as _e, x as ve, y as ye } from "./dialog-YHZJjDrI.js";
11
11
  import { A as be, C as xe, D as Se, E as Ce, O as we, S as Te, T as Ee, _ as De, a as Oe, b as ke, d as Ae, f as je, g as Me, h as Ne, i as Pe, k as Fe, l as Ie, m as Le, n as Re, o as ze, p as Be, r as Ve, s as He, t as Ue, u as We, v as Ge, w as Ke, x as qe, y as Je } from "./floating-ui.dom-KkZSW8_E.js";
12
12
  import { D as Ye, E as Xe, N as Ze, O as Qe, c as $e, d as et, f as tt, i as nt, k as rt, m as it, n as at, o as ot, p as st, t as ct, u as lt } from "./dropdown-menu-BEyP-A0J.js";
13
13
  import { a as ut, i as dt, n as ft } from "./field-blur-context-Cft9s7Hg.js";
@@ -36,7 +36,7 @@ import { n as an } from "./use-slice-reducer-BGkeGtML.js";
36
36
  import { t as on } from "./browser-only-DQ94lWF8.js";
37
37
  import { t as sn } from "./compose-refs-DZ0iIVF-.js";
38
38
  import { n as cn, r as ln, t as un } from "./chip-BZu-bvAJ.js";
39
- import { n as dn, t as fn } from "./icon-badge-_tY1MwU9.js";
39
+ import { n as dn, t as fn } from "./icon-badge-C0TGvF9o.js";
40
40
  import { r as pn } from "./use-csp-nonce-ZKs9k-ov.js";
41
41
  import { t as mn } from "./empty-state-ZtqqpX0S.js";
42
42
  import { t as hn } from "./skeleton-DSdtr2qc.js";
@@ -1324,7 +1324,7 @@ function xu({ className: e, ...t }) {
1324
1324
  function Su({ className: e, ...t }) {
1325
1325
  return /* @__PURE__ */ G("kbd", {
1326
1326
  "data-slot": "kbd",
1327
- className: n("pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1 font-mono text-[10px] font-medium text-muted-foreground", e),
1327
+ className: n("pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1 font-mono text-[10px] font-medium text-muted-foreground", "in-[[data-slot=button][data-variant=filled]]:border-current/40 in-[[data-slot=button][data-variant=filled]]:bg-current/15 in-[[data-slot=button][data-variant=filled]]:text-inherit", e),
1328
1328
  ...t
1329
1329
  });
1330
1330
  }
@@ -7969,7 +7969,7 @@ function Hy({ slot: e, values: t, getOptionValue: r = (e) => e.value, getOptionL
7969
7969
  ref: o,
7970
7970
  "data-slot": e ?? "autocomplete-field",
7971
7971
  "data-mode": a.mode,
7972
- className: n("grid w-full gap-1.5", a.className),
7972
+ className: n("grid w-full content-start gap-1.5", a.className),
7973
7973
  children: [
7974
7974
  /* @__PURE__ */ G(dt, {
7975
7975
  id: c,
@@ -8432,7 +8432,7 @@ function ub({ group: e, name: t, slot: r, className: i, contentClassName: a, inl
8432
8432
  return /* @__PURE__ */ K("div", {
8433
8433
  "data-slot": r,
8434
8434
  "data-pw": t ?? r,
8435
- className: n("grid gap-1 w-full", i),
8435
+ className: n("grid content-start gap-1 w-full", i),
8436
8436
  ...l,
8437
8437
  ...d,
8438
8438
  ref: u,
@@ -9542,7 +9542,7 @@ function kx({ error: e, helperText: t, label: r, className: i, placeholder: a, m
9542
9542
  return /* @__PURE__ */ K("div", {
9543
9543
  ref: m,
9544
9544
  "data-slot": "textarea-field",
9545
- className: n("grid w-full gap-1.5", i),
9545
+ className: n("grid w-full content-start gap-1.5", i),
9546
9546
  children: [
9547
9547
  r && /* @__PURE__ */ G(dt, {
9548
9548
  withAsterisk: p.required,
@@ -12073,7 +12073,7 @@ function GC({ id: e, value: t, defaultValue: r = "", onChange: i, onValueChange:
12073
12073
  return /* @__PURE__ */ K("div", {
12074
12074
  "data-slot": "search-field",
12075
12075
  "data-pw": m ?? "search-field",
12076
- className: n("grid w-full gap-1.5", h),
12076
+ className: n("grid w-full content-start gap-1.5", h),
12077
12077
  children: [
12078
12078
  l && /* @__PURE__ */ G(dt, {
12079
12079
  htmlFor: ee,
@@ -368,7 +368,7 @@ reads as a status (a stronger container of the tone, the label in the tone's
368
368
  text colour, 4.5:1 or more on the page and in a card; the `outlined` hairline
369
369
  and the `tag` dot still read `--chip-tone`, not the tone channel); `tag`
370
370
  a smaller tag led by a colour dot; `dashed` a muted "add" pill. 28px tall,
371
- 24px with `size="sm"`. Selected (`active`, or `aria-pressed` on a toggle
371
+ 24px with `size="sm"`, 44px with `size="lg"`. Selected (`active`, or `aria-pressed` on a toggle
372
372
  chip) takes a primary tint and a primary edge, and a pressed toggle with
373
373
  nothing leading draws a check. The label is truncated as one piece; the ✕
374
374
  and `trailing` stay whole.
@@ -397,7 +397,7 @@ import { Avatar, Chip } from '@nxgt/material/components';
397
397
  | `variant` | `'outlined' \| 'tonal' \| 'filled' \| 'tag' \| 'dashed'` | `'outlined'` | the look: hairline, soft (`tonal` and `filled`), tag, or a muted dashed "add" pill that turns solid once `active` or pressed. `ghost` (soft) and `link` (hairline) are still accepted, deprecated |
398
398
  | `active` | `boolean` | — | selected: primary tint and edge |
399
399
  | `aria-pressed` | `boolean` | — | a toggle chip: selected while pressed, with a check when nothing leads |
400
- | `size` | `'default' \| 'sm'` | `'default'` | 28px or 24px; every target stays at least 24px |
400
+ | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | 28px, 24px, or `lg`, a 44px box at every pointer (the touch target); every target stays at least 24px |
401
401
  | `color` | `'default' \| 'primary' \| 'secondary' \| 'info' \| 'success' \| 'warning' \| 'error'` | neutral | tone: the edge (outlined), the fill and label (tonal, `filled`: a status look) or the dot and tint (tag). A `tag` without `color` (or with `default`) draws no dot |
402
402
  | `onDismiss` | `() => void` | — | adds the ✕ button, named "Remove {label}" (`material.chip.dismiss-label`), `{label}` being the text `label` shows, a node's included; "Remove" when it shows none; the ✕ is drawn 18px (16px small) with a 24px target |
403
403
  | `dismissLabel` | `string \| ((label: string) => string)` | — | names the ✕ instead of "Remove {label}": a string as is, or a function of the text the chip shows |
@@ -222,8 +222,12 @@ function DeleteProject({ onDelete }: { onDelete: () => Promise<void> }) {
222
222
  | `onConfirm` | `() => void \| Promise<void>` | required | awaited with the dialog open and both buttons disabled; closes on success; on a throw or rejection the dialog stays open and shows the error's message, or the translated `material.confirm-dialog.failed` ("The action failed.") when it has none |
223
223
  | `variant` | `'default' \| 'destructive'` | `'default'` | `destructive` paints the confirm button `error` |
224
224
  | `loading` | `boolean` | `false` | disables both buttons and shows the translated `material.confirm-dialog.processing` label, as a pending `onConfirm` does |
225
+ | `confirmDisabled` | `boolean` | `false` | disables the confirm button, for example while an error of yours stands |
226
+ | `lockOnRefusal` | `boolean` | `false` | once `onConfirm` has refused (thrown), the confirm button stays disabled while its error shows, so a retry that cannot succeed is not invited; Cancel stays enabled. The error clears when the dialog closes |
225
227
  | `confirmLabel` / `cancelLabel` | `string` | "Confirm" / "Cancel", translated (`material.confirm-dialog.confirm` / `.cancel`) | button text |
226
228
 
229
+ On a phone (below 640 px) the footer stacks. A `destructive` dialog puts Cancel first and the destructive action last; a default one keeps the action on top.
230
+
227
231
  `ConfirmDialog` opens with its Cancel focused, so Enter cancels. On closing
228
232
  it hands the focus back to what opened it, or to the menu's trigger when
229
233
  that was a `DropdownMenuItem` (gone by then); `Dialog`, `Sheet` and
@@ -81,6 +81,9 @@ phone's menu sheet pad clear of `env(safe-area-inset-*)`, as classes; see the
81
81
  | `menuOpen` / `onMenuOpenChange` | `boolean` / `(open: boolean) => void` | — | control the phone's menu sheet; it still closes by itself when a link in it is followed or the location changes (see the [app shell guide](../guides/app-shell.md#activitylayout)) |
82
82
  | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | `ActivityContent`: the title's heading level |
83
83
  | `inset` | `boolean` | `false` | `ActivityContent`: pads the content 1rem on each side. Off, it lines up with the app bar and the cards |
84
+ | `width` | `'full' \| 'form' \| 'reading'` | `'full'` | `ActivityContent`: caps the main column at 48rem (`'form'`) or 72ch (`'reading'`), the side panel right beside it |
85
+ | `aside`, `asideLabel` / `asideLabelledBy` | `ReactNode`, `string` | — | `ActivityContent`: a side panel, a named `complementary` landmark, beside the content from 1280px (CSS only), stacked below; sticky under the app bar |
86
+ | `asideSide` / `asideWidth` / `asideFrom` / `asideSticky` / `asideStack` | `'start' \| 'end'` / `'sm' \| 'md' \| 'lg' \| string` / `'lg' \| 'xl' \| '2xl'` / `boolean` / `'before' \| 'after'` | `'end'` / `'md'` / `'xl'` / `true` / `'after'` | `ActivityContent`: the panel's side, width (16, 20, 24rem), breakpoint, stickiness and stacking order (see the [app shell guide](../guides/app-shell.md#a-side-panel-and-a-readable-width)) |
84
87
  | `align` | `'start' \| 'center'` | `'start'` | `ActivityLayout`, `AppSidebar`: the collapsed rail's entries sit right under the brand, or `'center'` for centred in the height below it |
85
88
  | `backdrop` | `ReactNode \| false` | — | `ActivityLayout`: an exception to the theme's background (drawn by default; Lavande's is the built-in gradient and blobs); `false` draws a plain page. A node is a decorative background behind the frame: a `Background` preset from `@nxgt/material/backgrounds`. Rendered in an `inert`, `aria-hidden` layer as wide as the content column and one viewport high, fixed in the viewport while the content scrolls; the chrome (app bar, breadcrumb, tabs strip) is lightly translucent and frosted over it (80%, 87% for the sidebar, breadcrumb and tabs strip, 92% and no blur under `prefers-reduced-transparency` or without `backdrop-filter`), and the lists, tables and cards stay opaque; `--nxgt-backdrop-chrome` sets the chrome's fill and `--nxgt-backdrop-surface` the lists'. See [Use a background preset behind the shell](../guides/app-shell.md#use-a-background-preset-behind-the-shell) |
86
89
  | `routeTransition` | `boolean \| Omit<ViewTransitionProps, 'children' \| 'ref'>` | `true` | `ActivityLayout`: React's `<ViewTransition>` around the content region, so a navigation cross-fades it. `false` renders none (your app has its own boundary); an object passes `default` / `update` classes, `name`, callbacks through. See [Route transitions](../guides/app-shell.md#route-transitions) |
@@ -131,7 +131,7 @@ export function Terms() {
131
131
  | `asChild` (`DialogTrigger`) | `boolean` | **`true`** | the trigger is its child — pass one element, usually a `Button` |
132
132
  | `variant`, `color` (`DialogClose`) | as `IconButton` | `'ghost'` | a top-right X button, named "Close" (`material.buttons.close`, translated) while it shows only the icon; text children or an `aria-label` name it instead, and icon-only children leave it unnamed; children replace the X |
133
133
  | `size` (`DialogContent`) | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | its width from 640 px up: 384, 512, 672, 896 px, or the screen less 2rem a side |
134
- | `mobilePresentation` (`DialogContent`) | `'dialog' \| 'bottom-sheet' \| 'fullscreen'` | `'dialog'` | below 640 px: a centred dialog, a sheet that springs up from the bottom edge (rounded top corners), or the whole screen |
134
+ | `mobilePresentation` (`DialogContent`) | `'dialog' \| 'bottom-sheet' \| 'top' \| 'fullscreen'` | `'dialog'` | below 640 px: a centred dialog, a sheet that springs up from the bottom edge (rounded top corners), a dialog pinned under the top edge, or the whole screen |
135
135
  | `scrim` (`DialogContent`, `SheetContent`, `DrawerContent`) | `'dim' \| 'blur'` | `'dim'` | the scrim behind; `blur` also blurs the page a little |
136
136
 
137
137
  The look lives in the stylesheet (`nxgt-dialog`, `nxgt-scrim`), under the
@@ -678,6 +678,7 @@ children and they replace that, as before.
678
678
  | `alwaysRender` (`CommandSeparator`) | `boolean` | `false` | the separator is purely visual (`role="presentation"`, hidden from assistive technology) and hidden while the user searches; `true` keeps it visible then |
679
679
  | `shouldFilter` (`Command`) | `boolean` | `true` | `false` when you filter the items yourself (server search) |
680
680
  | `title` / `description` (`CommandDialog`) | `string` | `"Command Palette"` / `"Search for a command to run..."` (translated) | the dialog's title and description, inside the dialog and visually hidden |
681
+ | `mobilePresentation` (`CommandDialog`) | `'dialog' \| 'top' \| 'fullscreen'` | `'top'` | below 640 px: pinned under the top edge (so the input and the results stay above the on-screen keyboard), the whole screen, or the centred modal it was before |
681
682
  | `open`, `onOpenChange` (`CommandDialog`) | as `Dialog` | — | `CommandDialog` takes every `Dialog` prop |
682
683
 
683
684
  `CommandDialog` does not bind a keyboard shortcut; listen for ⌘K yourself and
@@ -526,6 +526,8 @@ import { Kbd, KbdShortcut } from '@nxgt/material/components';
526
526
  | --- | --- | --- | --- |
527
527
  | `keys` (`KbdShortcut`) | `string[]` | — (required) | one `Kbd` per key; `mod`, `shift` and `alt` become `⌘ ⇧ ⌥` on Apple platforms, `Ctrl Shift Alt` elsewhere |
528
528
 
529
+ Inside a filled `Button` the key, its border and the `+` between keys take the button's own text colour, so the shortcut keeps the contrast of the fill.
530
+
529
531
  `Kbd` takes the props of `<kbd>`. `shortcutGlyph(key)` gives one glyph on its own.
530
532
 
531
533
  **Storybook:** `Primitives/Kbd` · **Vue:** `Kbd`, `KbdShortcut`
@@ -328,6 +328,8 @@ when nothing matches. Options with a `group` are listed under its header
328
328
  with their count, filtered within it (a group with no match goes); a
329
329
  `description` is a muted line under the label, in the list only.
330
330
 
331
+ Once an option is picked, a round "Clear" button (`material.combobox.clear`, "Effacer" in French) appears at the end of the box, also in `ComboboxFormField`. It is a real button, named and exposed to assistive technology (`getByRole('button', { name: 'Clear' })`), described by the field's label, and kept out of the tab order like the other clearable fields: the keyboard clears with Backspace or by selecting the text and deleting it.
332
+
331
333
  ```tsx
332
334
  import { ComboboxField } from '@nxgt/material/components';
333
335
  import { useState } from 'react';
@@ -785,7 +787,7 @@ export function Topics() {
785
787
  | `deselectable` | `boolean` | `false` | `single` only: pressing the chosen chip clears it |
786
788
  | `max` / `min` | `number` | — | `multiple` only: `max` blocks the other chips once reached and speaks "{count} of {max} chosen"; `min` is a hint only |
787
789
  | `options` | `{ value; label; icon?; count?; description?; disabled?; disabledReason? }[]` | — | the chips; `count`: see [option counts](#option-counts) |
788
- | `size` | `'default' \| 'sm'` | `'default'` | the chip's size (28px or 24px) |
790
+ | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | the chip's size: 28px, 24px, or `lg`, a 44px box at every pointer (a coarse pointer already gets a 44px box at any size) |
789
791
  | `name` | `string` | — | put on every chip (`ChipGroupFormField` passes its field `name`), so `document.getElementsByName(name)` finds the group's chips in order |
790
792
  | `label`, `helperText`, `error`, `required`, `disabled`, `id` | | — | the question and its help or error text, the required mark, disabling every chip, the group's `id` |
791
793
  | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | wrapping row or a column (each chip as wide as its label); in single mode it sets the `radiogroup`'s `aria-orientation` (ARIA allows none on a `group`), and every arrow key moves in both axes either way (the chips wrap) |
@@ -799,6 +799,15 @@ export function BoardShell({ items }: { items: MenuItem[] }) {
799
799
  | `hideHeader` | `boolean` | `false` | No header card |
800
800
  | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | The title's element: the screen's heading, `'h2'` under a heading of yours, `'div'` for none |
801
801
  | `inset` | `boolean` | `false` | Pads the content 1rem on each side. Off, it lines up with the app bar and the cards |
802
+ | `width` | `'full'` \| `'form'` \| `'reading'` | `'full'` | Caps the main column: `'form'` 48rem, `'reading'` 72ch. The header card keeps the full width |
803
+ | `aside` | `ReactNode` | — | A side panel: beside the content from `asideFrom`, stacked below it |
804
+ | `asideLabel` / `asideLabelledBy` | `string` | — | The panel's accessible name (`aria-label`), or the id of a heading inside it (`aria-labelledby`). One of them is needed |
805
+ | `asideSide` | `'start'` \| `'end'` | `'end'` | Which side of the content the panel sits on, wide |
806
+ | `asideWidth` | `'sm'` \| `'md'` \| `'lg'` \| a CSS length | `'md'` | 16, 20 or 24rem; `'md'` reads the `--nxgt-aside-width` token (20rem) |
807
+ | `asideFrom` | `'lg'` \| `'xl'` \| `'2xl'` | `'xl'` | The viewport width (1024, 1280 or 1536px) from which the panel is beside the content |
808
+ | `asideSticky` | `boolean` | `true` | Keeps the panel under the app bar while the page scrolls; it scrolls inside itself when taller than the window |
809
+ | `asideStack` | `'before'` \| `'after'` | `'after'` | Where the panel goes below `asideFrom`, in the DOM and on screen |
810
+ | `asideClassName` | `string` | — | Classes for the panel |
802
811
 
803
812
  ```tsx
804
813
  import { ActivityContent, Button } from '@nxgt/material/components';
@@ -819,6 +828,49 @@ export function InvoicesPage() {
819
828
  The header card wraps: on a narrow screen the actions go under the title
820
829
  instead of squeezing it, and a long title wraps rather than scrolling.
821
830
 
831
+ ### A side panel and a readable width
832
+
833
+ A record's alerts and key facts, a dashboard's counts, a form's help: pass them
834
+ as `aside`. From `asideFrom` (`'xl'`, a 1280px window) the panel sits beside the
835
+ main column and sticks under the app bar; below it, the panel stacks after the
836
+ content (`asideStack="before"` puts it first). The switch is CSS only: the
837
+ server's HTML and a phone's first paint are already right, and no JavaScript
838
+ decides the layout.
839
+
840
+ ```tsx
841
+ <ActivityContent
842
+ title="New visit"
843
+ width="form"
844
+ aside={<DuplicateHints />}
845
+ asideLabel="Possible duplicates"
846
+ >
847
+ <VisitForm />
848
+ </ActivityContent>
849
+ ```
850
+
851
+ - **Landmark.** The panel is an `<aside>`, a `complementary` landmark: name it
852
+ with `asideLabel`, or with `asideLabelledBy` (the id of its heading). Its
853
+ content is the panel's own; give it one `h2` when it has a heading.
854
+ - **Sticky, in every scroll mode.** With `scroll="document"` the panel sticks
855
+ under the app bar (the bar's gutter, its `--nxgt-header-height` and 0.5rem);
856
+ with `scroll="container"` it sticks to the top of the content region. It is as
857
+ tall as the window at most and scrolls inside itself, and becomes focusable
858
+ (`tabindex="0"`) only while it does, so a keyboard reaches what overflows. A
859
+ `scroll="document"` page needs no `overflow` on any ancestor of the screen
860
+ (see [the troubleshooting entry](../troubleshooting.md#the-side-panel-does-not-stick)).
861
+ - **A capped column sits beside its panel.** `width="form"` or `"reading"` caps
862
+ the main column and the pair stays start-aligned: at 1920px the panel is 1rem
863
+ from the fields, not at the window's far edge. The header card above keeps
864
+ the full width. `width` also works without a panel.
865
+ - **The breakpoint is the window's.** `asideFrom` follows the viewport, not the
866
+ content column, which the sidebar and the gutters narrow a little: pick `'lg'`
867
+ for a panel on a smaller window, `'2xl'` to keep it stacked on all but the
868
+ widest.
869
+ - **A size container.** With a panel, the main column is an `@container`, so a
870
+ `ResponsiveGrid` inside it follows the column's width.
871
+ - **Width token.** `--nxgt-aside-width` changes `asideWidth="md"` (20rem) app-wide;
872
+ any CSS length goes in `asideWidth` for one screen.
873
+
822
874
  ## Breadcrumbs
823
875
 
824
876
  `AppBreadcrumb` (rendered by `ActivityLayout`) starts with a home icon linking
@@ -625,6 +625,7 @@ centre line from `md` (48rem) up:
625
625
  | Token | Default | Reads |
626
626
  | --- | --- | --- |
627
627
  | `--nxgt-header-height` | `4rem` | the app bar (all widths) and the rail's header (`md` and up) |
628
+ | `--nxgt-aside-width` | `20rem` | an `ActivityContent` side panel with `asideWidth="md"` (the default) |
628
629
 
629
630
  ```css
630
631
  :root {
@@ -1338,6 +1338,30 @@ top (`align="start"`).
1338
1338
  <ActivityLayout items={items} align="center">…</ActivityLayout>
1339
1339
  ```
1340
1340
 
1341
+ ### The side panel does not stick
1342
+
1343
+ **When:** an `ActivityContent` `aside` scrolls away with the page instead of
1344
+ staying under the app bar, or it is not beside the content at 1280px.
1345
+ **Why:** `position: sticky` sticks inside its nearest scrolling ancestor, and
1346
+ any ancestor with an `overflow` other than `visible` (`overflow-hidden`,
1347
+ `overflow-auto`, `overflow-clip`; a `PaneLayout`'s root has `overflow-hidden`)
1348
+ becomes that scroller, which does not scroll, so the panel never moves. A
1349
+ wrapper of yours around the screen, or an `ActivityLayout` with
1350
+ `scroll="container"` where you meant `"document"`, does the same. Below
1351
+ `asideFrom` (1280px by default) the panel is stacked and not sticky on
1352
+ purpose, and the breakpoint is the window's width, not the column's.
1353
+ **Fix:** take the `overflow` off every ancestor between `ActivityLayout` and
1354
+ `ActivityContent`, or move it onto an element inside the screen; use
1355
+ `scroll="document"` (the default). Find the culprit in the console:
1356
+
1357
+ ```js
1358
+ for (let el = document.querySelector('[data-slot="activity-content-aside"]').parentElement; el; el = el.parentElement)
1359
+ if (getComputedStyle(el).overflowY !== 'visible') console.log(el);
1360
+ ```
1361
+
1362
+ With `scroll="container"` the region itself is the scroller, and the panel
1363
+ sticks to its top: that is expected.
1364
+
1341
1365
  ### axe reports `target-size` on a control under a form's sticky actions
1342
1366
 
1343
1367
  **When:** since the form's actions stick to the bottom (see above), axe's
@@ -1975,6 +1999,26 @@ a few frames after the alert has gone) and look it up by selector rather
1975
1999
  than by role: the closing alert still hides the page from the accessibility
1976
2000
  tree.
1977
2001
 
2002
+ ### A `ConfirmDialog` keeps offering a refused action
2003
+
2004
+ When `onConfirm` throws, the dialog shows the error and the confirm button
2005
+ stays active, so the user can retry. If a retry cannot succeed (the item is
2006
+ still referenced), lock it:
2007
+
2008
+ ```tsx
2009
+ <ConfirmDialog variant="destructive" lockOnRefusal onConfirm={remove} … />
2010
+ ```
2011
+
2012
+ Cancel stays enabled. `confirmDisabled` is the switch for your own condition.
2013
+
2014
+ ### Fields in a two-column row sit at different heights
2015
+
2016
+ A field is a grid of a label and a control. In earlier versions its rows stretched when a
2017
+ neighbour in the same row was taller (a helper or error text under it), so the
2018
+ shorter field's control sank by half the difference. Fields now keep their rows
2019
+ at the top (`content-start`); upgrade, and drop any `mt-*` or `items-end`
2020
+ workaround. A `DateField`'s tonal fill is deliberate and not a misalignment.
2021
+
1978
2022
  ## Navigation
1979
2023
 
1980
2024
  ### A `ListTile` link shows two chevrons
@@ -1,6 +1,6 @@
1
1
  /* Chip: what the three looks share (data-look hairline, soft, tag). Sizes:
2
- * default 28px, sm 24px (both at least the 24px target of WCAG 2.5.8). The
3
- * label truncates as one piece; the ✕ is drawn small, its target is 24×24
2
+ * default 28px, sm 24px (both at least the 24px target of WCAG 2.5.8), lg
3
+ * 44px (the touch target). The label truncates as one piece; the ✕ is drawn small, its target is 24×24
4
4
  * through ::after, and it has its own focus ring. */
5
5
  @layer components {
6
6
  .nxgt-chip {
@@ -50,6 +50,16 @@
50
50
  --chip-gap: 0.25rem;
51
51
  --chip-x: 1rem;
52
52
  }
53
+ /* lg: a 44px box at every pointer (the touch target, #1529). */
54
+ .nxgt-chip[data-size="lg"] {
55
+ --chip-h: 2.75rem;
56
+ --chip-px: 1rem;
57
+ --chip-font: 0.9375rem;
58
+ --chip-icon: 1rem;
59
+ --chip-gap: 0.375rem;
60
+ --chip-x: 1.5rem;
61
+ min-width: 2.75rem;
62
+ }
53
63
  button.nxgt-chip,
54
64
  .nxgt-chip > button[data-slot="chip-body"] {
55
65
  cursor: pointer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.14.0",
3
+ "version": "3.16.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"