@nxgt/material 3.13.1 → 3.15.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 (31) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/dist/components/ui/display/chip/chip.css +12 -2
  4. package/dist/{components-BhGc4Bj7.js → components-j0Iv8MZi.js} +9036 -9017
  5. package/dist/components.js +25 -25
  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/collections/server-table/index.d.ts +1 -0
  10. package/dist/lib/components/ui/display/chip/chip.d.ts +1 -1
  11. package/dist/lib/components/ui/display/chip/chip.variants.d.ts +5 -2
  12. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog-rules.d.ts +17 -0
  13. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog.d.ts +10 -2
  14. package/dist/lib/components/ui/overlays/command/command.d.ts +7 -1
  15. package/dist/lib/components/ui/overlays/dialog/dialog-variants.d.ts +1 -1
  16. package/dist/main.js +42 -42
  17. package/dist/map.js +2 -2
  18. package/dist/material.css +1 -1
  19. package/dist/pdf-reader.js +1 -1
  20. package/dist/{public-D_Gz7wgr.js → public-Bj3rGSfS.js} +1 -1
  21. package/dist/{use-chat-composer-P4GxmCHa.js → use-chat-composer-CoXmJCkr.js} +7 -7
  22. package/docs/components/data-display.md +2 -2
  23. package/docs/components/data-table-filter-and-lists.md +1 -0
  24. package/docs/components/feedback-and-status.md +4 -0
  25. package/docs/components/overlays-and-menus.md +2 -1
  26. package/docs/components/primitives.md +2 -0
  27. package/docs/components/selection-and-choice.md +3 -1
  28. package/docs/guides/server-lists-url.md +15 -1
  29. package/docs/troubleshooting.md +41 -0
  30. package/lib/components/ui/display/chip/chip.css +12 -2
  31. package/package.json +2 -2
@@ -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 |
@@ -679,6 +679,7 @@ type Student = { id: string; name: string; level: string };
679
679
  const PAGE_SIZE = 20;
680
680
  // Module scope: the loader and the page read the same URL the same way.
681
681
  const sorting = createSorting({ name: 'name', level: 'level' }, { id: 'name', desc: false });
682
+ // A GraphQL API with an ASC / DESC enum: add { direction: (d) => d.toUpperCase() as 'ASC' | 'DESC' } as the third argument.
682
683
  const YEAR = [{ field: 'year', op: 'is', values: ['2026'], reason: 'This school year' }] as const;
683
684
 
684
685
  export async function loader({ request }: Route.LoaderArgs) {
@@ -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
@@ -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) |
@@ -5,7 +5,10 @@ it and fetches the rows, the page shows the `Filter` bar with the same query,
5
5
  and an edit in the bar writes the URL back. A shared link, a reload and the
6
6
  browser's Back button all land on the same list.
7
7
 
8
- It uses the URL codec of `@nxgt/material-core/filter` and React Router's
8
+ It uses the URL codec (`defineFilterModel`, `parseFilterQuery`,
9
+ `serializeFilterQuery`), which `@nxgt/material/components` re-exports (it is
10
+ also in `@nxgt/material-core/filter`, so the examples below work with either
11
+ import) and React Router's
9
12
  framework mode. Any router works the same way: the codec takes and returns
10
13
  `URLSearchParams`. If you render the list with
11
14
  [`ServerTable`](../components/data-table-filter-and-lists.md#servertable), it
@@ -58,6 +61,12 @@ track and an integer beyond `Number.MAX_SAFE_INTEGER`, and it keeps the first
58
61
  readable condition of a field. Whatever a
59
62
  visitor types into the address bar, you get a valid `FilterQuery`.
60
63
 
64
+ A list condition with one unknown value (`?status=a&status=zzz`, a stale or
65
+ hand-edited link) is dropped whole. To keep the values it reads and drop only
66
+ the unknown one, pass `keepKnownValues: true`
67
+ (`parseFilterQuery(params, STUDENT_FILTERS, { locked, keepKnownValues: true })`);
68
+ a condition with no readable value left is still dropped.
69
+
61
70
  ```ts
62
71
  // app/routes/students.tsx
63
72
  import { effectiveConditions, parseFilterQuery } from '@nxgt/material-core/filter';
@@ -142,6 +151,11 @@ export default function Students({ loaderData }: Route.ComponentProps) {
142
151
  re-encodes them (`f=level%3Ain%3ACE1%2CCE2`), which parses back the same. To
143
152
  compare a URL you wrote with the one the router hands back, use
144
153
  `paramsKey(a) === paramsKey(b)`.
154
+ - **Standard encoding.** If your list URLs are already written the
155
+ `URLSearchParams` way (`status=a%2Cb`), pass `encoding: 'standard'` to
156
+ `serializeFilterQuery` (and to `tableFilters` / `withoutFilters`): the params
157
+ returned are a plain `URLSearchParams`, so `toString()` encodes `:` and `,`.
158
+ Both forms parse back the same.
145
159
  - **Dates and times** stay relative in the URL (`f=term:between:this-month`),
146
160
  so a bookmark keeps meaning "this month".
147
161
 
@@ -120,6 +120,7 @@ words in your error.
120
120
  - [The list loads again when the phone filter sheet closes](#the-list-loads-again-when-the-phone-filter-sheet-closes)
121
121
  - [`FilterSheetTrigger must be used inside <Filter> or <ServerTable filter>`](#filtersheettrigger-must-be-used-inside-filter-or-servertable-filter)
122
122
  - [A condition in the URL is missing from the filter](#a-condition-in-the-url-is-missing-from-the-filter)
123
+ - [The filter URL is not encoded like URLSearchParams](#the-filter-url-is-not-encoded-like-urlsearchparams)
123
124
  - [A refused or reset edit stays in the filter bar](#a-refused-or-reset-edit-stays-in-the-filter-bar)
124
125
  - [`defineFilterModel: field "level" cannot use "gt" on a select field`](#definefiltermodel-field-level-cannot-use-gt-on-a-select-field)
125
126
  - [`@nxgt/material-core/filter: unknown time zone "…", using the runtime's`](#nxgtmaterial-corefilter-unknown-time-zone--using-the-runtimes)
@@ -1974,6 +1975,26 @@ a few frames after the alert has gone) and look it up by selector rather
1974
1975
  than by role: the closing alert still hides the page from the accessibility
1975
1976
  tree.
1976
1977
 
1978
+ ### A `ConfirmDialog` keeps offering a refused action
1979
+
1980
+ When `onConfirm` throws, the dialog shows the error and the confirm button
1981
+ stays active, so the user can retry. If a retry cannot succeed (the item is
1982
+ still referenced), lock it:
1983
+
1984
+ ```tsx
1985
+ <ConfirmDialog variant="destructive" lockOnRefusal onConfirm={remove} … />
1986
+ ```
1987
+
1988
+ Cancel stays enabled. `confirmDisabled` is the switch for your own condition.
1989
+
1990
+ ### Fields in a two-column row sit at different heights
1991
+
1992
+ A field is a grid of a label and a control. In earlier versions its rows stretched when a
1993
+ neighbour in the same row was taller (a helper or error text under it), so the
1994
+ shorter field's control sank by half the difference. Fields now keep their rows
1995
+ at the top (`content-start`); upgrade, and drop any `mt-*` or `items-end`
1996
+ workaround. A `DateField`'s tonal fill is deliberate and not a misalignment.
1997
+
1977
1998
  ## Navigation
1978
1999
 
1979
2000
  ### A `ListTile` link shows two chevrons
@@ -2233,6 +2254,26 @@ const query = parseFilterQuery(params, STUDENT_FILTERS, { locked: LOCKED });
2233
2254
  const asWritten = parseFilterQuery(params, STUDENT_FILTERS);
2234
2255
  ```
2235
2256
 
2257
+ A list condition (`in`, a `repeat` param) with one unknown value
2258
+ (`?status=a&status=zzz`) is dropped whole by default. To keep `a` and drop only
2259
+ `zzz`, parse with `{ keepKnownValues: true }`.
2260
+
2261
+ ### The filter URL is not encoded like URLSearchParams
2262
+
2263
+ **When:** links or tests of a list that already published its URLs with
2264
+ `URLSearchParams` (`status=a%2Cb`) now differ in text from what
2265
+ `serializeFilterQuery(...).toString()` or `tableFilters(...).withoutFilters(...)`
2266
+ writes.
2267
+ **Why:** their params leave `:` and `,` raw (legal in a query) so the URL reads
2268
+ well; `URLSearchParams` encodes them. Both parse back the same.
2269
+ **Fix:** pass `encoding: 'standard'` (to `serializeFilterQuery`, or in the
2270
+ options of `withoutFilters` / `tableFilters`) for a plain `URLSearchParams`:
2271
+
2272
+ ```ts
2273
+ serializeFilterQuery(query, MODEL, { encoding: 'standard' }).toString();
2274
+ // status=a&status=b&f=level%3Ain%3ACE1%2CCE2
2275
+ ```
2276
+
2236
2277
  ### `?archived=0` now shows every row
2237
2278
 
2238
2279
  **When:** a stored link or bookmark with a `style: 'flag'` alias at `0`
@@ -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.13.1",
3
+ "version": "3.15.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -357,7 +357,7 @@
357
357
  "docs:previews": "rm -rf node_modules/.cache/nxgt-preview-free/missing && NXGT_PREVIEW_FREE=1 vitest run --project previews && bun .storybook/preview-free-report.ts"
358
358
  },
359
359
  "dependencies": {
360
- "@nxgt/material-core": "^0.30.1",
360
+ "@nxgt/material-core": "^0.31.0",
361
361
  "@base-ui/react": "^1.6.0",
362
362
  "@dnd-kit/core": "^6.3.1",
363
363
  "@dnd-kit/sortable": "^10.0.0",