@nxgt/material 3.48.0 → 3.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +3 -3
- package/dist/{async-slice-DlvKCKAa.js → async-slice-3loEtzUY.js} +2 -2
- package/dist/{button-yaltbQmC.js → button-BRUaHD8z.js} +81 -112
- package/dist/charts.js +144 -144
- package/dist/{chip-BqWbbr1b.js → chip-DneTrtNj.js} +56 -56
- package/dist/{code-editor-C9qBNoYb.js → code-editor-R_nPN_-d.js} +308 -308
- package/dist/code-editor.js +47 -47
- package/dist/{code-language-items-DAHtfQ7W.js → code-language-items-BKEUhQlF.js} +2 -2
- package/dist/components/components.css +1 -0
- package/dist/components/ui/actions/action-card/action-card-indicator.css +48 -0
- package/dist/components/ui/actions/action-card/action-card-modes.css +4 -1
- package/dist/components/ui/actions/action-card/action-card-tile.css +141 -0
- package/dist/components/ui/actions/action-card/action-card.css +33 -3
- package/dist/{components-Ox6FHmpx.js → components-D6xFLcoE.js} +17646 -17567
- package/dist/components.js +26 -27
- package/dist/{dialog-CCCcnTIx.js → dialog-S7dLsYRZ.js} +16 -16
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-Df8785nt.js → dropdown-menu-XLlxfDgq.js} +271 -272
- package/dist/editor.js +1028 -1028
- package/dist/{empty-state-DcF4TBzi.js → empty-state-Dsf6R-Zo.js} +37 -37
- package/dist/{escape-owner-C4W_sOAU.js → escape-owner-B9eCwjLw.js} +1 -1
- package/dist/{field-blur-CJHzVSh9.js → field-blur-5uRDFHvB.js} +1 -1
- package/dist/{field-blur-context-Cft9s7Hg.js → field-blur-context-Cwt4TuBq.js} +1 -1
- package/dist/hooks.js +4 -4
- package/dist/{hover-card-C0fw2kLK.js → hover-card-PHUgRSjJ.js} +24 -24
- package/dist/{i18n-provider-C0fB0sEL.js → i18n-provider-DPM_ALQ4.js} +1 -1
- package/dist/i18n.js +5 -5
- package/dist/{icon-badge-aJ22mKGh.js → icon-badge-Dh6-FNOF.js} +3 -3
- package/dist/{icon-button-CN9r5TNE.js → icon-button-DSC2-IAG.js} +118 -86
- package/dist/lib/components/ui/actions/action-card/action-card-semantics.d.ts +25 -2
- package/dist/lib/components/ui/actions/action-card/action-card.d.ts +18 -2
- package/dist/lib/components/ui/actions/action-card/partials/action-card-icon.d.ts +6 -0
- package/dist/lib/components/ui/actions/action-card/partials/action-card-meta.d.ts +15 -0
- package/dist/lib/components/ui/actions/action-card/partials/action-card-status.d.ts +6 -0
- package/dist/lib/components/ui/actions/action-card/partials/action-card-text.d.ts +20 -0
- package/dist/lib/components/ui/actions/action-card/partials/action-card-title-button.d.ts +14 -0
- package/dist/lib/components/ui/actions/action-card/partials/action-card-trail.d.ts +6 -0
- package/dist/lib/components/ui/actions/action-card/use-action-card-names.d.ts +22 -0
- package/dist/lib/components/ui/actions/action-card/use-reveal.d.ts +23 -0
- package/dist/lib/components/ui/actions/button/button.variants.d.ts +1 -1
- package/dist/lib/components/ui/actions/icon-button/icon-button-busy.d.ts +22 -0
- package/dist/lib/components/ui/actions/icon-button/icon-button-shape.d.ts +2 -0
- package/dist/lib/components/ui/actions/icon-button/icon-button.d.ts +7 -0
- package/dist/lib/components/ui/actions/icon-button/icon-button.variants.d.ts +1 -1
- package/dist/lib/components/ui/collections/filter/bar/partials/filter-quick-toggle.d.ts +3 -2
- package/dist/lib/components/ui/collections/filter/sections/filter-sections-classes.d.ts +7 -1
- package/dist/lib/components/ui/collections/page-footer/page-footer.d.ts +6 -1
- package/dist/lib/components/ui/collections/row-menu/row-menu-trigger.d.ts +5 -2
- package/dist/lib/components/ui/collections/row-menu/row-menu.d.ts +1 -1
- package/dist/lib/components/ui/collections/server-table/index.d.ts +1 -1
- package/dist/lib/components/ui/collections/server-table/partials/server-table-empty-chips.d.ts +5 -3
- package/dist/lib/components/ui/collections/server-table/types-paging.d.ts +17 -0
- package/dist/lib/components/ui/collections/server-table/types.d.ts +3 -10
- package/dist/lib/components/ui/dates/duration-field/partials/use-duration-box.d.ts +1 -1
- package/dist/lib/components/ui/display/badge/badge.d.ts +1 -1
- package/dist/lib/components/ui/display/day-separator/day-separator.d.ts +1 -1
- package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-body.d.ts +5 -3
- package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-request-error.d.ts +15 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/step-up-dialog.d.ts +6 -3
- package/dist/lib/components/ui/feedback/step-up-dialog/types.d.ts +12 -1
- package/dist/lib/components/ui/feedback/step-up-dialog/use-retry-focus.d.ts +27 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/use-retry.d.ts +11 -0
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/fields/slider/split-slider-props.d.ts +20 -20
- package/dist/lib/components/ui/fields/slider/use-slider-control.d.ts +20 -20
- package/dist/lib/components/ui/fields/verification-code/use-settled-focus.d.ts +10 -0
- package/dist/lib/components/ui/layout/app-shell/route-transition.d.ts +1 -1
- package/dist/lib/components/ui/media/carousel/use-carousel-engine.d.ts +1 -1
- package/dist/lib/components/ui/media/media-player/engine/use-queue-actions.d.ts +1 -1
- package/dist/lib/components/ui/media/pdf-reader/pdf-reader.slice.d.ts +1 -1
- package/dist/lib/components/ui/selection/choice-group/use-choice-group-field.d.ts +34 -34
- package/dist/lib/components/ui/widgets/chat/chat-message.variants.d.ts +2 -2
- package/dist/lib/components/ui/widgets/event-calendar/use-event-calendar.d.ts +1 -1
- package/dist/lib/i18n/resources/en/en.d.ts +1 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +2 -1
- package/dist/lib/i18n/resources/fr/fr.d.ts +1 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +2 -1
- package/dist/lib/i18n/resources/resources.d.ts +2 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +43 -44
- package/dist/map.js +79 -79
- package/dist/material.css +1 -1
- package/dist/nxgt-theme-ivgNca3z.js +37 -0
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +2 -2
- package/dist/{public-zVb_Nq_D.js → public-vxiijIJF.js} +82 -82
- package/dist/{rich-code-block-DMWZZJma.js → rich-code-block-BwvCXPqq.js} +52 -52
- package/dist/{sortable-provider-DstbgMVf.js → sortable-provider-D2pAFWQB.js} +218 -218
- package/dist/{tooltip-CkttDIab.js → spinner-BpVwI1ps.js} +503 -278
- package/dist/{stored-language-IaL1Ftn_.js → stored-language-Beyj2ult.js} +4 -2
- package/dist/theme-picker.js +45 -45
- package/dist/{theme-store-CSl51uNH.js → theme-store-BdPp43cV.js} +13 -13
- package/dist/{theme-toggle-CGW2_K54.js → theme-toggle-BTYRPkP4.js} +16 -16
- package/dist/themes.js +17 -17
- package/dist/{toggle-group-BMJNcFAC.js → toggle-group-BkH23Jk4.js} +11 -11
- package/dist/{toolbar-items-CsLGJMEJ.js → toolbar-items-CIMo4F9q.js} +421 -421
- package/dist/{translate-material-v9u-e7Kd.js → translate-material-DEUlaL_1.js} +2 -2
- package/dist/{translator-Dk4_qE6U.js → translator-CBgIb3zP.js} +1 -1
- package/dist/{use-chat-composer-Brcb5f7r.js → use-chat-composer-D8UAsX0f.js} +6228 -5999
- package/dist/{button-tones-BwuVvAmd.js → use-click-lock-BZYtTmVt.js} +37 -7
- package/dist/{use-heading-spy-DjKixjLm.js → use-heading-spy-C5LXtA8Z.js} +1 -1
- package/dist/{use-material-translation-DOkTaWK_.js → use-material-translation-DWWeeg-B.js} +2 -2
- package/dist/{use-theme-DMcyJ52N.js → use-theme-BCLireTd.js} +1 -1
- package/docs/components/buttons-and-actions.md +64 -5
- package/docs/components/data-table-filter-and-lists.md +30 -10
- package/docs/components/feedback-and-status.md +22 -3
- package/docs/components/form-fields.md +7 -0
- package/docs/guides/cards-and-tiles.md +60 -0
- package/docs/guides/server-lists-url.md +3 -2
- package/docs/roadmap.md +11 -0
- package/lib/components/components.css +1 -0
- package/lib/components/ui/actions/action-card/action-card-indicator.css +48 -0
- package/lib/components/ui/actions/action-card/action-card-modes.css +4 -1
- package/lib/components/ui/actions/action-card/action-card-tile.css +141 -0
- package/lib/components/ui/actions/action-card/action-card.css +33 -3
- package/package.json +1 -1
- package/dist/nxgt-theme-ZS945iMP.js +0 -37
- package/dist/spinner-C4CgDEYm.js +0 -229
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { createContext as e } from "react";
|
|
1
|
+
import { createContext as e, useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
2
2
|
//#region lib/components/ui/primitives/icon/icon-context.ts
|
|
3
|
-
var
|
|
3
|
+
var a = e(!1);
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region lib/components/ui/actions/button/busy-click-capture.ts
|
|
6
|
+
function o(e, t, n) {
|
|
7
|
+
return (r) => {
|
|
8
|
+
if (e || t.isLocked()) {
|
|
9
|
+
r.preventDefault(), r.stopPropagation();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
t.lock(), n?.(r);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region lib/components/ui/actions/button/button-tones.ts
|
|
17
|
+
var s = {
|
|
4
18
|
primary: "text-primary-text",
|
|
5
19
|
secondary: "text-secondary",
|
|
6
20
|
success: "text-success dark:text-[color-mix(in_oklab,var(--color-success)_85%,white)]",
|
|
@@ -8,7 +22,7 @@ var t = e(!1), n = {
|
|
|
8
22
|
warning: "text-[color-mix(in_oklab,var(--color-warning)_85%,black)] dark:text-[color-mix(in_oklab,var(--color-warning)_85%,white)]",
|
|
9
23
|
error: "text-[color-mix(in_oklab,var(--color-error)_85%,black)] dark:text-[color-mix(in_oklab,var(--color-error)_70%,white)]",
|
|
10
24
|
default: "text-foreground"
|
|
11
|
-
},
|
|
25
|
+
}, c = {
|
|
12
26
|
primary: "[&_svg]:data-[slot=icon]:text-primary-text",
|
|
13
27
|
secondary: "[&_svg]:data-[slot=icon]:text-secondary",
|
|
14
28
|
success: "[&_svg]:data-[slot=icon]:text-success dark:[&_svg]:data-[slot=icon]:text-[color-mix(in_oklab,var(--color-success)_85%,white)]",
|
|
@@ -16,7 +30,7 @@ var t = e(!1), n = {
|
|
|
16
30
|
warning: "[&_svg]:data-[slot=icon]:text-[color-mix(in_oklab,var(--color-warning)_85%,black)] dark:[&_svg]:data-[slot=icon]:text-[color-mix(in_oklab,var(--color-warning)_85%,white)]",
|
|
17
31
|
error: "[&_svg]:data-[slot=icon]:text-[color-mix(in_oklab,var(--color-error)_85%,black)] dark:[&_svg]:data-[slot=icon]:text-[color-mix(in_oklab,var(--color-error)_70%,white)]",
|
|
18
32
|
default: "[&_svg]:data-[slot=icon]:text-foreground"
|
|
19
|
-
},
|
|
33
|
+
}, l = {
|
|
20
34
|
primary: "[&_svg]:stroke-primary-text",
|
|
21
35
|
secondary: "[&_svg]:stroke-secondary",
|
|
22
36
|
success: "[&_svg]:stroke-success dark:[&_svg]:stroke-[color-mix(in_oklab,var(--color-success)_85%,white)]",
|
|
@@ -24,7 +38,7 @@ var t = e(!1), n = {
|
|
|
24
38
|
warning: "[&_svg]:stroke-[color-mix(in_oklab,var(--color-warning)_85%,black)] dark:[&_svg]:stroke-[color-mix(in_oklab,var(--color-warning)_85%,white)]",
|
|
25
39
|
error: "[&_svg]:stroke-[color-mix(in_oklab,var(--color-error)_85%,black)] dark:[&_svg]:stroke-[color-mix(in_oklab,var(--color-error)_70%,white)]",
|
|
26
40
|
default: "[&_svg]:stroke-foreground"
|
|
27
|
-
},
|
|
41
|
+
}, u = {
|
|
28
42
|
primary: "[--morph-on:var(--color-primary)] [--morph-on-fg:var(--color-primary-foreground)] [--btn-focus:var(--color-primary-text)] [--btn-line:var(--color-primary)]",
|
|
29
43
|
secondary: "[--morph-on:var(--color-secondary)] dark:[--morph-on:color-mix(in_oklab,var(--color-secondary)_70%,var(--color-background))] [--morph-on-fg:var(--color-secondary-foreground)] [--btn-focus:var(--color-secondary)] [--btn-line:var(--color-secondary)]",
|
|
30
44
|
success: "[--morph-on:var(--color-success)] dark:[--morph-on:color-mix(in_oklab,var(--color-success)_60%,var(--color-background))] [--morph-on-fg:var(--color-success-foreground)] [--btn-focus:var(--color-success)] dark:[--btn-focus:color-mix(in_oklab,var(--color-success)_85%,white)] [--btn-line:var(--color-success)]",
|
|
@@ -32,6 +46,22 @@ var t = e(!1), n = {
|
|
|
32
46
|
warning: "[--morph-on:var(--color-warning)] dark:[--morph-on:color-mix(in_oklab,var(--color-warning)_60%,var(--color-background))] [--morph-on-fg:var(--color-warning-foreground)] [--btn-focus:color-mix(in_oklab,var(--color-warning)_85%,black)] dark:[--btn-focus:color-mix(in_oklab,var(--color-warning)_85%,white)] [--btn-line:var(--color-warning)]",
|
|
33
47
|
error: "[--morph-on:var(--color-error)] [--morph-on-fg:var(--color-error-foreground)] [--btn-focus:color-mix(in_oklab,var(--color-error)_85%,black)] dark:[--btn-focus:color-mix(in_oklab,var(--color-error)_70%,white)] [--btn-line:var(--color-error)]",
|
|
34
48
|
default: "[--morph-on:var(--color-foreground)] [--morph-on-fg:var(--color-background)] [--btn-focus:var(--color-foreground)] [--btn-line:var(--color-foreground)]"
|
|
35
|
-
};
|
|
49
|
+
}, d = 1e3;
|
|
50
|
+
function f(e, a) {
|
|
51
|
+
let o = r(!1), s = r(!1), c = r(void 0), [l, u] = i(!1), f = t(() => {
|
|
52
|
+
o.current = !1, s.current = !1, clearTimeout(c.current), u(!1);
|
|
53
|
+
}, []), p = t(() => {
|
|
54
|
+
e && (o.current = !0, u(!0), clearTimeout(c.current), c.current = setTimeout(() => {
|
|
55
|
+
s.current || f();
|
|
56
|
+
}, d));
|
|
57
|
+
}, [e, f]);
|
|
58
|
+
return n(() => {
|
|
59
|
+
o.current && (a ? s.current = !0 : s.current && f());
|
|
60
|
+
}, [a, f]), n(() => () => clearTimeout(c.current), []), {
|
|
61
|
+
locked: l,
|
|
62
|
+
lock: p,
|
|
63
|
+
isLocked: t(() => o.current, [])
|
|
64
|
+
};
|
|
65
|
+
}
|
|
36
66
|
//#endregion
|
|
37
|
-
export {
|
|
67
|
+
export { u as a, s as i, c as n, o, l as r, a as s, f as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as e } from "./i18n-provider-
|
|
1
|
+
import { r as e } from "./i18n-provider-DPM_ALQ4.js";
|
|
2
2
|
import { _ as t, g as n, h as r, n as i, v as a, y as o } from "./number.utils-BG_eEt5g.js";
|
|
3
3
|
import { useCallback as s, useEffect as c, useMemo as l, useRef as u, useState as d } from "react";
|
|
4
4
|
import { busySettler as f, busySettler as p, createSubmitGuard as m, createSubmitGuard as h } from "@nxgt/material-core/submit-guard";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as e } from "./i18n-provider-
|
|
2
|
-
import { t } from "./translate-material-
|
|
1
|
+
import { r as e } from "./i18n-provider-DPM_ALQ4.js";
|
|
2
|
+
import { t } from "./translate-material-DEUlaL_1.js";
|
|
3
3
|
import { useCallback as n } from "react";
|
|
4
4
|
//#region lib/i18n/use-material-translation.ts
|
|
5
5
|
function r() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./use-ssr-media-query-C6M5m8sf.js";
|
|
2
|
-
import { i as t, n, o as r, r as i, s as a, t as o } from "./theme-store-
|
|
2
|
+
import { i as t, n, o as r, r as i, s as a, t as o } from "./theme-store-BdPp43cV.js";
|
|
3
3
|
import { a as s } from "./theme-context-Cofk6vgs.js";
|
|
4
4
|
import { useCallback as c, useEffect as l, useState as u, useSyncExternalStore as d } from "react";
|
|
5
5
|
import { PRESET_SCHEMES as f } from "@nxgt/material-core/theme/boot";
|
|
@@ -7,7 +7,7 @@ and language switches. Everything is imported from `@nxgt/material/components`.
|
|
|
7
7
|
| | Component | What it does |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| <img src="../previews/buttons-actions-button.webp" width="96" alt=""> | [Button](#button) | Five variants in seven colours, six sizes, a round or square shape and a selected state, with a loading state |
|
|
10
|
-
| <img src="../previews/buttons-actions-iconbutton.webp" width="96" alt=""> | [IconButton](#iconbutton) | Icon-only button in five sizes and three widths, with an optional tooltip |
|
|
10
|
+
| <img src="../previews/buttons-actions-iconbutton.webp" width="96" alt=""> | [IconButton](#iconbutton) | Icon-only button in five sizes and three widths, with an optional tooltip and a loading state |
|
|
11
11
|
| <img src="../previews/buttons-actions-buttongroup.webp" width="96" alt=""> | [ButtonGroup](#buttongroup) | Joins adjacent buttons into one control; the expressive look widens the pressed one |
|
|
12
12
|
| <img src="../previews/buttons-actions-splitbutton.webp" width="96" alt=""> | [SplitButton](#splitbutton) | Main action plus a menu of related ones, Material 3 Expressive |
|
|
13
13
|
| <img src="../previews/buttons-actions-fab.webp" width="96" alt=""> | [Fab](#fab) | The screen's main action, floating; extended with its label, folds on scroll |
|
|
@@ -20,7 +20,7 @@ and language switches. Everything is imported from `@nxgt/material/components`.
|
|
|
20
20
|
| <img src="../previews/buttons-actions-loadmorebutton.webp" width="96" alt=""> | [LoadMoreButton](#loadmorebutton) | "Load more" for cursor pagination |
|
|
21
21
|
| <img src="../previews/buttons-actions-scrolltotop.webp" width="96" alt=""> | [ScrollToTop](#scrolltotop) | Appears past a scroll threshold |
|
|
22
22
|
| <img src="../previews/buttons-actions-scrolltolatestbutton.webp" width="96" alt=""> | [ScrollToLatestButton](#scrolltolatestbutton) | "Jump to latest" over a scroll box, counting what arrived |
|
|
23
|
-
| <img src="../previews/buttons-actions-actioncard.webp" width="96" alt=""> | [ActionCard](#actioncard) | Clickable card, a button (`aria-pressed`) when it chooses or reacts, with an active indicator |
|
|
23
|
+
| <img src="../previews/buttons-actions-actioncard.webp" width="96" alt=""> | [ActionCard](#actioncard) | Clickable card, a button (`aria-pressed`) when it chooses or reacts, with an active indicator; `status`, `meta`, a trailing `action` and a `tile` variant |
|
|
24
24
|
| <img src="../previews/buttons-actions-themetoggle.webp" width="96" alt=""> | [ThemeToggle](#themetoggle) | Light / dark / system switch |
|
|
25
25
|
| <img src="../previews/buttons-actions-languageswitcher.webp" width="96" alt=""> | [LanguageSwitcher](#languageswitcher) | Menu of the `I18nProvider` languages |
|
|
26
26
|
|
|
@@ -180,6 +180,7 @@ import { Pencil, Trash2 } from 'lucide-react';
|
|
|
180
180
|
| `morph` | `'round' \| 'pill' \| 'none'` | `'pill'` | what a **toggle** (`aria-pressed` set) does when pressed: `pill` fills with its colour and widens by 14 px into a pill, `round` fills and stays a circle, `none` keeps the look your classes give it. Not set on a button without `aria-pressed`, one with a `shape`, or a Toolbar item (the Toolbar's own look) |
|
|
181
181
|
| `badge` | `number` | — | a count on the top end corner (top left in a right-to-left page): "99+" past 99, nothing at 0 or unset. It takes no room, so the box and the 44 px hit area are unchanged. It is **decorative** (`aria-hidden`): put the number in the accessible name yourself, e.g. `aria-label="Publish 2 items"` |
|
|
182
182
|
| `badgeTone` | `'primary' \| 'error' \| 'neutral'` | `'primary'` | the badge's colour: a count is not an alert, so `primary`; `error` for a notification; `neutral` is the page's text colour on its background |
|
|
183
|
+
| `loading` | `boolean` | `false` | busy, as `Button`'s: `aria-busy` and `aria-disabled`, a spinner in place of the icon (the button keeps its size) and every click blocked. It stays focusable (no native `disabled`), so the focus is not lost while the work runs |
|
|
183
184
|
|
|
184
185
|
**A count badge** (`badge`) is a primary pill (`badgeTone="error"` is the menu-item badge's red), ringed in the page background so it reads over any variant, and sits half outside the corner, over the neighbours' gap (leave 4 px; a `Toolbar` clips 4 px outside its items, so leave room there too). It does not dim when the button is disabled. It never names itself: `<IconButton badge={2} aria-label="Publish 2 items">`. `Button` has no `badge`: its label can be cut with an ellipsis and an `asChild` one clips, so compose a `CountBadge` around it.
|
|
185
186
|
|
|
@@ -1063,9 +1064,12 @@ import { Banknote, CreditCard } from 'lucide-react';
|
|
|
1063
1064
|
| `active` | `boolean` | — | the chosen look: a primary container with its edge, a primary title, the icon's circle morphs to a filled cookie and the indicator fills; also `aria-pressed` |
|
|
1064
1065
|
| `onClick` | `(event) => void` | — | makes the card a button |
|
|
1065
1066
|
| `disabled` | `boolean` | `false` | `aria-disabled`, out of the Tab order, no click and no key, muted look |
|
|
1066
|
-
| `variant` | `'sm' \| 'md'` | `'sm'` | one row,
|
|
1067
|
-
| `withIndicator` | `boolean` | `true` | the radio-like ring that fills with a check |
|
|
1067
|
+
| `variant` | `'sm' \| 'md' \| 'tile'` | `'sm'` | one row, icon and indicator above the text, or a compact [tile](#actioncard-tile) |
|
|
1068
|
+
| `withIndicator` | `boolean` | `true` (`false` for `tile`) | the radio-like ring that fills with a check |
|
|
1068
1069
|
| `title`, `description`, `icon` | `ReactNode` | — | the content |
|
|
1070
|
+
| `status` | `ReactNode` | — | a line under the title; a string or number becomes a soft `Badge`, a node is shown as given |
|
|
1071
|
+
| `meta` | `ReactNode` | — | a secondary line, one line with an ellipsis |
|
|
1072
|
+
| `action` | `ReactNode` | — | a trailing control outside the press surface, see [tile](#actioncard-tile); `title` is then required, a `role` moves to the title button; the card's `disabled` does not reach it, disable its control too |
|
|
1069
1073
|
|
|
1070
1074
|
A card with `active`, `onClick`, `disabled` or your own `role` is `role="button"` (unless you set another): it takes the Tab stop, and
|
|
1071
1075
|
Enter and Space press it. `active` sets `aria-pressed`; pass your own `role`
|
|
@@ -1079,9 +1083,64 @@ the card, forced colours in system colours, and no motion under reduced motion.
|
|
|
1079
1083
|
For a card choice bound to a form value, see `ChoiceCardGroup` on
|
|
1080
1084
|
[Selection & choice](selection-and-choice.md).
|
|
1081
1085
|
|
|
1086
|
+
### ActionCard tile
|
|
1087
|
+
|
|
1088
|
+
`variant="tile"` is a compact grid cell for boards: no minimum width (it fills
|
|
1089
|
+
its grid track), the title and the `action` on the first row, then `status`,
|
|
1090
|
+
`description` and `meta`, and no indicator unless `withIndicator`. `status` and
|
|
1091
|
+
`meta` work on every variant.
|
|
1092
|
+
|
|
1093
|
+
```tsx
|
|
1094
|
+
import {
|
|
1095
|
+
ActionCard,
|
|
1096
|
+
Badge,
|
|
1097
|
+
DropdownMenuRadioGroup,
|
|
1098
|
+
DropdownMenuRadioItem,
|
|
1099
|
+
RowMenu,
|
|
1100
|
+
} from '@nxgt/material/components';
|
|
1101
|
+
|
|
1102
|
+
<ActionCard
|
|
1103
|
+
variant="tile"
|
|
1104
|
+
active={selected === unit.id}
|
|
1105
|
+
onClick={() => setSelected(unit.id)}
|
|
1106
|
+
title={unit.name}
|
|
1107
|
+
status={<Badge variant="soft" tone="success">Free</Badge>}
|
|
1108
|
+
meta="Zone B, second floor"
|
|
1109
|
+
action={
|
|
1110
|
+
<RowMenu
|
|
1111
|
+
label={unit.name}
|
|
1112
|
+
triggerProps={{ 'aria-label': `Change status of ${unit.name}` }}
|
|
1113
|
+
>
|
|
1114
|
+
<DropdownMenuRadioGroup value={unit.status} onValueChange={setStatus}>
|
|
1115
|
+
<DropdownMenuRadioItem value="free">Free</DropdownMenuRadioItem>
|
|
1116
|
+
<DropdownMenuRadioItem value="occupied">Occupied</DropdownMenuRadioItem>
|
|
1117
|
+
</DropdownMenuRadioGroup>
|
|
1118
|
+
</RowMenu>
|
|
1119
|
+
}
|
|
1120
|
+
/>;
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
With an `action`, a card that chooses or reacts is a plain `div` and its
|
|
1124
|
+
**title is the one button**, stretched over the card, so the whole card presses
|
|
1125
|
+
it and the action never nests inside it (no `nested-interactive`). The button
|
|
1126
|
+
is named after the title, the status and the meta together ("Unit 21 Free Zone
|
|
1127
|
+
B"); the action keeps its own name. The card is one Tab stop and the action a
|
|
1128
|
+
second; Enter and Space press the button. `disabled` disables the button
|
|
1129
|
+
natively; disable the action's control too. **`title` is required with
|
|
1130
|
+
`action`**: it is the button's text (without one the button is still there, named
|
|
1131
|
+
after the status and the meta line, and development builds warn). The
|
|
1132
|
+
`description` is the button's description (`aria-describedby`). A `role`
|
|
1133
|
+
(`switch`, `checkbox`, `radio`) and its `aria-checked` move to the button, where
|
|
1134
|
+
they are valid; the root stays a plain `div` and takes no `aria-checked` or
|
|
1135
|
+
`aria-label`. Hover and press answer only the title button (its stretched area covers the
|
|
1136
|
+
card, padding included), not the action. The meta line shows its full text in a tooltip, on hover or
|
|
1137
|
+
keyboard focus of the title button, only while it is clipped (Escape dismisses
|
|
1138
|
+
it; moving the pointer across the line does not). Without `action` the DOM is as
|
|
1139
|
+
before. Nothing is added for a Vue port yet (React first).
|
|
1140
|
+
|
|
1082
1141
|
The root is `data-slot="action-card"` (it was the `Card` primitive's `data-slot="card"`): style it with `className`, not through `Card` classes or a `[data-slot=card]` selector.
|
|
1083
1142
|
|
|
1084
|
-
**Storybook:** `Buttons & actions/ActionCard`, `Buttons & actions/ActionCard/Look` · **Vue:** `ActionCard`, with the earlier look and no button role yet (#1195)
|
|
1143
|
+
A whole board built on it, with sections and an occupancy table, is the `Guides/Recipes/Status board` recipe. **Storybook:** `Buttons & actions/ActionCard`, `Buttons & actions/ActionCard/Look`, `Buttons & actions/ActionCard/Tile`, `Buttons & actions/ActionCard/Checks` · **Vue:** `ActionCard`, with the earlier look and no button role yet (#1195)
|
|
1085
1144
|
|
|
1086
1145
|
---
|
|
1087
1146
|
|
|
@@ -15,9 +15,9 @@ either can be used alone.
|
|
|
15
15
|
| --- | --- | --- |
|
|
16
16
|
| <img src="../previews/data-table-filter-lists-datatable.webp" width="96" alt=""> | [DataTable](#datatable) | TanStack Table v9 with sorting, selection, expansion and pagination, client-side or one server page at a time |
|
|
17
17
|
| <img src="../previews/data-table-filter-lists-servertable.webp" width="96" alt=""> | [ServerTable](#servertable) | A server-mode `DataTable` driven by the URL, with the Filter in its top band (a title and a description, or neither): sort, cursor paging, the empty page with a scene, an action and removable chips, one count announced |
|
|
18
|
-
| <img src="../previews/data-table-filter-lists-rowmenu.webp" width="96" alt=""> | [RowMenu](#rowmenu) | A row's "..." button named after its row ("Actions for Ada"), its menu aligned to the end with the destructive item last, and `rowMenuColumn` to drop both in a table |
|
|
18
|
+
| <img src="../previews/data-table-filter-lists-rowmenu.webp" width="96" alt=""> | [RowMenu](#rowmenu) | A row's "..." button named after its row ("Actions for Ada"), its menu aligned to the end with the destructive item last, a loading state while an action runs, and `rowMenuColumn` to drop both in a table |
|
|
19
19
|
| <img src="../previews/data-table-filter-lists-listempty.webp" width="96" alt=""> | [ListEmpty](#listempty) | A list's empty page: nothing yet, with a scene and an action, or nothing matches and the way out |
|
|
20
|
-
| <img src="../previews/data-table-filter-lists-pagefooter.webp" width="96" alt=""> | [PageFooter](#pagefooter) | The page footer of a feed that is not a table (cards, a news feed) under a FilterBar: the range and Previous / Next as links from the URL's cursor trail, `pageControls="icon"`; no `totalCount` for a feed with no total (no range, Previous / Next only) |
|
|
20
|
+
| <img src="../previews/data-table-filter-lists-pagefooter.webp" width="96" alt=""> | [PageFooter](#pagefooter) | The page footer of a feed that is not a table (cards, a news feed) under a FilterBar: the range and Previous / Next as links from the URL's cursor trail, `pageControls="icon"`; no `totalCount` for a feed with no total (no range, Previous / Next only); `hideOnSinglePage` hides it on a single page |
|
|
21
21
|
| <img src="../previews/data-table-filter-lists-datagrid.webp" width="96" alt=""> | [DataGrid](#datagrid) | An editable grid: spreadsheet keys, paste, a fill handle with series, undo, totals, saving with versions and conflicts (lazy ones fetch theirs on "Keep mine"), `onRefuse` for a refused paste or fill, row selection (`enableRowSelection`), and resizable, hideable and reorderable columns (`columnControls`, `columnLayout`, `DataGridColumnsMenu`) |
|
|
22
22
|
| <img src="../previews/data-table-filter-lists-daygroupedlist.webp" width="96" alt=""> | [DayGroupedList](#daygroupedlist) | A list split by day under sticky Today / Yesterday / Earlier headings |
|
|
23
23
|
| <img src="../previews/data-table-filter-lists-filter.webp" width="96" alt=""> | [Filter](#filter) | Schema-driven filters with chips, presets and persistence |
|
|
@@ -818,8 +818,8 @@ export default function Students() {
|
|
|
818
818
|
- **Nothing matches.** The empty page is [`ListEmpty`](#listempty), with no
|
|
819
819
|
header row and no footer. With `filter`, it lists the user's filters as
|
|
820
820
|
removable chips (the band's row keeps the locked ones only, so none is
|
|
821
|
-
listed twice), then "Clear all"
|
|
822
|
-
removing one moves the focus to the next chip, then "Clear all", then the
|
|
821
|
+
listed twice), then "Clear all" (worded by `empty.clear`) whenever a chip or a search
|
|
822
|
+
remains, a single chip included, the locked conditions staying; removing one moves the focus to the next chip, then "Clear all", then the
|
|
823
823
|
search. When rows come back while the focus was on the empty page, it
|
|
824
824
|
goes to the search; rows that come back on their own leave the focus
|
|
825
825
|
alone. The empty page stays while the next answer loads (`data-busy`).
|
|
@@ -995,6 +995,7 @@ Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
|
|
|
995
995
|
| `actions` | `ReactNode` | — | the page's actions, at the band's end |
|
|
996
996
|
| `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') |
|
|
997
997
|
| `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) |
|
|
998
|
+
| `hideOnSinglePage` | `boolean` | `false` | hides the footer's range and Previous / Next when the result is one page (no previous, no next), in the table, cards and tiles layouts; the count is still announced by the live region but no longer shown, so put it in `description` or keep the footer. With `bulkActions` the footer stays for "N rows selected", without its pager |
|
|
998
999
|
| `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. With a cards or tiles list on a phone, neither applies: [the two-row header](#the-phone-header-of-a-list-in-cards-or-tiles) puts the actions on its second row |
|
|
999
1000
|
| `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 |
|
|
1000
1001
|
| `sorting` | `createSorting(…)` | — | headers sorted from the URL |
|
|
@@ -1168,13 +1169,25 @@ and `ref`, so it is the child of a `DropdownMenuTrigger asChild`.
|
|
|
1168
1169
|
| Prop | Type | Default | Effect |
|
|
1169
1170
|
| --- | --- | --- | --- |
|
|
1170
1171
|
| `label` (`RowMenu`, `RowMenuTrigger`) | `string` | — | the row's name; the button's name is "Actions for {label}" (translated). An `aria-label` of your own wins |
|
|
1172
|
+
| `loading` (`RowMenuTrigger`, `triggerProps.loading` of `RowMenu`) | `boolean` | `false` | an action chosen from this row's menu is running: `aria-busy`, a spinner in place of the dots (the button keeps its size), and the menu does not open again until it is over. The button stays focusable and keeps the focus. As `Button`'s `loading` |
|
|
1171
1173
|
| `items` (`RowMenu`) | `{ label, icon?, onSelect?, disabled?, destructive?, id? }[]` | `[]` | the actions; `destructive` ones are drawn in the error colour and moved last, after a separator |
|
|
1172
|
-
| `children` (`RowMenu`) | `ReactNode` | — | menu content of your own, above `items` (put a destructive item last yourself) |
|
|
1174
|
+
| `children` (`RowMenu`) | `ReactNode` | — | menu content of your own, above `items` (put a destructive item last yourself); its menu items are 44px high on a coarse pointer too |
|
|
1173
1175
|
| `triggerProps` (`RowMenu`) | button props | — | `disabled`, `className`, `data-*` on the button |
|
|
1174
1176
|
| `open`, `onOpenChange` (`RowMenu`) | as `DropdownMenu` | — | controlled state |
|
|
1175
1177
|
| `label`, `items`, `menu` (`rowMenuColumn`) | `(row) => string` / `(row) => RowMenuItem[]` / `(row) => ReactNode` | — | the row's name, its actions as data, or its menu content as nodes |
|
|
1176
1178
|
| `id`, `className` (`rowMenuColumn`) | `string` | `'actions'`, `'w-12'` | the column's id and its cell classes |
|
|
1177
1179
|
|
|
1180
|
+
While an action runs, set `loading` on its row's button. The button keeps its size and
|
|
1181
|
+
the focus, so the focus does not fall to the page when the menu closes:
|
|
1182
|
+
|
|
1183
|
+
```tsx
|
|
1184
|
+
<RowMenu
|
|
1185
|
+
label={person.name}
|
|
1186
|
+
items={[{ label: 'Archive', onSelect: () => archive.mutate(person.id) }]}
|
|
1187
|
+
triggerProps={{ loading: archive.isPending && archive.variables === person.id }}
|
|
1188
|
+
/>
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1178
1191
|
Focus returns to the button when the menu closes, and when a dialog opened from
|
|
1179
1192
|
an item (a `ConfirmDialog`, a `Dialog`, a `Sheet`) closes: the item is gone by
|
|
1180
1193
|
then, so the dialog hands the focus back to the menu's button.
|
|
@@ -1249,6 +1262,12 @@ A cursor feed whose API answers `pageInfo` and the items but no total (a news or
|
|
|
1249
1262
|
<PageFooter pageInfo={pageInfo} params={params} pageSize={20} count={items.length} pageControls="icon" />
|
|
1250
1263
|
```
|
|
1251
1264
|
|
|
1265
|
+
A list that fits one page shows "1–5 of 5" by default. `hideOnSinglePage` renders nothing when there is no previous and no next page (`ServerTable` takes the same prop, in every layout). The range is gone from the screen then: tell the count elsewhere, a heading or a status line, or keep the footer.
|
|
1266
|
+
|
|
1267
|
+
```tsx
|
|
1268
|
+
<PageFooter pageInfo={pageInfo} totalCount={5} params={params} pageSize={20} count={5} hideOnSinglePage />
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1252
1271
|
| Prop | Type | Default | Effect |
|
|
1253
1272
|
| --- | --- | --- | --- |
|
|
1254
1273
|
| `params` | `URLSearchParams` | — | the page's search params: the trail is read from them and every other param is kept in the hrefs |
|
|
@@ -1259,6 +1278,7 @@ A cursor feed whose API answers `pageInfo` and the items but no total (a news or
|
|
|
1259
1278
|
| `pageHref` | `(params) => string` | ``?${params}`` | a page's href |
|
|
1260
1279
|
| `pagingNames` | `{ after?, trail? }` | `after`, `trail` | the cursor's search param names |
|
|
1261
1280
|
| `pageControls` | `'labelled' \| 'icon'` | `'labelled'` | icon-only controls with a tooltip; 44x44 boxes on a coarse pointer |
|
|
1281
|
+
| `hideOnSinglePage` | `boolean` | `false` | renders nothing when there is no previous and no next page; the range is then not shown, so tell the count elsewhere (a heading, a status line) or keep the footer |
|
|
1262
1282
|
| `live` | `boolean` | `true` | `false` renders the range as plain text instead of a polite live region |
|
|
1263
1283
|
| `disabled` | `boolean` | `false` | makes Previous and Next inert while the feed loads |
|
|
1264
1284
|
| `linkComponent` | `DataTableLinkComponent` | `<a>` | your router's link; it must forward `className` and `ref` |
|
|
@@ -1484,7 +1504,7 @@ filter values become one form value. See [forms](../guides/forms.md).
|
|
|
1484
1504
|
`variant="bar"` is a query-driven bar over
|
|
1485
1505
|
`@nxgt/material-core/filter`. So far: the search; the quick filters, a group
|
|
1486
1506
|
named "Quick filters" (a dashed "+ Status" pill that opens the field's
|
|
1487
|
-
editor, or a toggle for a switch or checkbox field); "Add filter", a menu
|
|
1507
|
+
editor, or a toggle chip for a switch or checkbox field: its label alone while off, the chip's check mark once on); "Add filter", a menu
|
|
1488
1508
|
that picks a field, then an operator (skipped when the field has one), then
|
|
1489
1509
|
its value, applied as it changes, Done (`aria-disabled`, with its reason described, while the value cannot be read) returning the focus to "Add filter"
|
|
1490
1510
|
and announcing the filter ("Level is CE1, 3 results"), Escape or a click
|
|
@@ -1492,7 +1512,7 @@ elsewhere dropping it; the active filters as chips in a list named "Active
|
|
|
1492
1512
|
filters" (each opens its editor, titled with its field and operator: select,
|
|
1493
1513
|
multi-select, radio group, chips, text, switch, checkbox, autocomplete, date, date range, time, time range, a custom field the page renders; "is empty" takes
|
|
1494
1514
|
no value); the page's locked conditions (a lock, no ✕); "Clear all" from two
|
|
1495
|
-
filters; live apply and one polite live region telling the settled count.
|
|
1515
|
+
filters (on the empty page, from one); live apply and one polite live region telling the settled count.
|
|
1496
1516
|
`/` focuses the search and `F` opens "Add filter" (never while typing, with
|
|
1497
1517
|
a modifier, or while a menu or dialog is open; on a phone, `F` opens the
|
|
1498
1518
|
sheet). Below 768px, a "Filters (N)" button opens the phone sheet (below). That button is `FilterSheetTrigger`, exported: to place it elsewhere, render `<FilterSheetTrigger />` inside `ServerTable` (in its `actions`, or anywhere else in its tree) and hide the bar's own with `filter.sheetTrigger: false`; `Filter` takes no children, and outside a filter scope the trigger throws (see the troubleshooting page). `filter.variant: 'facets'` keeps its own. It opens the one sheet the bar mounts, `F` still opens it, and the focus comes back to the button that opened it.
|
|
@@ -1884,7 +1904,7 @@ a word, never colour alone): a chip, a quick pill, a scope pill (on a phone
|
|
|
1884
1904
|
too), a quick switch or checkbox toggle, and the default chip. It is described "Invalid: <message>"; its
|
|
1885
1905
|
editor and its facets section show the message under the title; and the
|
|
1886
1906
|
polite live region tells it ("Period: The period cannot exceed 366 days."),
|
|
1887
|
-
once, after the count, also when the page opens with it; the next count replaces it. A dashed control (
|
|
1907
|
+
once, after the count, also when the page opens with it; the next count replaces it. A dashed control (the default chip) keeps its dashes
|
|
1888
1908
|
and takes the error tone on its line and text. Only a field of the model that the page does not lock shows
|
|
1889
1909
|
one. The page clears it when the condition changes, by passing no error. The
|
|
1890
1910
|
words are `material.filter.error.*`; `fieldErrors(errors, model, locked)` in
|
|
@@ -1948,7 +1968,7 @@ field's, once and after the count: "Search: Not a valid item." and the
|
|
|
1948
1968
|
search or the query changes, by passing nothing, as it does for `errors`.
|
|
1949
1969
|
|
|
1950
1970
|
When the loader answers one flat record, `splitFilterErrors(refusal, model)`
|
|
1951
|
-
from `@nxgt/material
|
|
1971
|
+
(from `@nxgt/material/components`, usable in a loader) splits it: the key equal to the model's
|
|
1952
1972
|
search param becomes `searchError`, the key `FILTER_FORM_ERROR_KEY`
|
|
1953
1973
|
(`'$form'`, never a field id) becomes `formError`, and the rest stays in
|
|
1954
1974
|
`errors`. Messages are trimmed and an empty one is dropped:
|
|
@@ -1971,7 +1991,7 @@ search param, so `q` is free for a field. `model.search.enabled` tells.
|
|
|
1971
1991
|
|
|
1972
1992
|
Fields declared `quick: true` in the model (or listed in `quick`) show as
|
|
1973
1993
|
pills beside the search, before any chip: a dashed "+ Level" pill opens the
|
|
1974
|
-
field's editor, and a switch or checkbox field is a toggle.
|
|
1994
|
+
field's editor, and a switch or checkbox field is a toggle chip (`aria-pressed`): its label alone, not dashed and with no plus while off, the chip's check mark once on.
|
|
1975
1995
|
|
|
1976
1996
|
```tsx
|
|
1977
1997
|
<Filter
|
|
@@ -418,20 +418,39 @@ export function ConfirmItsYou({
|
|
|
418
418
|
| `via` | `'email' \| 'secondFactor'` | — | how the code was issued; left out, the code is still being requested: no field, a status line. Changing it starts the form again (typed value, recovery switch) |
|
|
419
419
|
| `destination` | `ReactNode` | — | where an e-mailed code went; named in the description |
|
|
420
420
|
| `requestError` | `ReactNode` | — | the request for a code failed: an alert in place of the field |
|
|
421
|
+
| `onRetry` | `() => unknown` | — | with `requestError`, the alert holds a "Retry" button (`labels.retry`, "Réessayer" in French) that calls it. While the returned promise is pending the button is busy and keeps the focus. Clear `requestError` and set `via` and the code field takes the focus; set a new `requestError` and the focus stays on "Retry" and the new text is announced. Left out, there is no button |
|
|
421
422
|
| `attemptsLeft`, `locked`, `error`, `retryAfter`, `pending` | as `VerificationCode` | — | the answer to the last code (`error="throttled"` with `retryAfter` seconds disables "Resend the code" for that long and never locks; the footer is unchanged). No tries left: an e-mailed code reads "expired, ask for a new code"; an app or recovery code shows the locked alert and writes the page-wide lock |
|
|
422
423
|
| `length`, `allowRecovery` | `number`, `boolean` | `6`, per `via` | the slots in the code field, and "Use a recovery code" (on for `via="secondFactor"`), passed to the `VerificationCode` |
|
|
424
|
+
| `autoFocus` | `boolean` | — | the code field, not the title, takes the focus on open (passed to the `VerificationCode`) |
|
|
423
425
|
| `onSubmit`, `onResend`, `resent` | as `VerificationCode` | — | your calls; "Resend the code" sits alone on its own line |
|
|
424
426
|
| `confirming`, `confirmedLabel` | `boolean`, `ReactNode` | — | the code was accepted and you re-run the action: the field goes, a status line shows, Cancel is disabled and Escape does nothing until you close it |
|
|
425
427
|
| `title`, `description` | `ReactNode` | built-in | the title ("Confirm it's you") and the line under it, which follows `via` |
|
|
426
|
-
| `labels`, `icons` | partial objects | built-in | every string of the dialog (`title`, `description`, `descriptionApp`, `descriptionEmail`, `descriptionEmailTo`, `requesting`, `cancel`, `confirmed`) and of `VerificationCode` (`incomplete`, `incompleteRecovery` among them) |
|
|
428
|
+
| `labels`, `icons` | partial objects | built-in | every string of the dialog (`title`, `description`, `descriptionApp`, `descriptionEmail`, `descriptionEmailTo`, `requesting`, `cancel`, `confirmed`, `retry`) and of `VerificationCode` (`incomplete`, `incompleteRecovery` among them) |
|
|
429
|
+
|
|
430
|
+
**Retrying a failed request.** Give `onRetry` and the alert offers a button
|
|
431
|
+
instead of leaving the user to cancel and start over:
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
<StepUpDialog
|
|
435
|
+
open
|
|
436
|
+
onCancel={close}
|
|
437
|
+
requestError={step.requestError} // "We could not send a code."
|
|
438
|
+
onRetry={async () => {
|
|
439
|
+
// clear requestError and set via on success; set a new requestError on failure
|
|
440
|
+
await requestCode();
|
|
441
|
+
}}
|
|
442
|
+
onSubmit={(code, { kind }) => check(code, kind)}
|
|
443
|
+
/>
|
|
444
|
+
```
|
|
427
445
|
|
|
428
446
|
Confirm is a footer button that submits the form. Pressed with too few digits
|
|
429
447
|
(or an empty recovery code) it focuses the field and says "Enter all 6 digits."
|
|
430
448
|
instead of calling `onSubmit`.
|
|
431
449
|
|
|
432
450
|
**Focus.** The title takes the focus on open and whenever the field goes away
|
|
433
|
-
(a code being requested, a code accepted)
|
|
434
|
-
on
|
|
451
|
+
(a code being requested, a code accepted), unless `autoFocus` sends it to the
|
|
452
|
+
field on open; "Retry" keeps it while a retry runs and when the request fails again, and the code field takes it once the request succeeds; the field takes it after a refusal and once a resend settles
|
|
453
|
+
(answered or throttled), when "Resend" held the focus as it started; on close it goes back to the button that opened the dialog. A locked code
|
|
435
454
|
leaves Cancel usable.
|
|
436
455
|
|
|
437
456
|
**On a phone** the footer stays one row, Cancel on the left and Confirm on the
|
|
@@ -792,6 +792,7 @@ export function SecondStep({ answer, pending, send }: Props) {
|
|
|
792
792
|
| `onResend`, `resent` | `() => unknown`, `boolean` | — | "Resend the code" alone on its own line (an e-mailed code), "New code" / "Try again" once a code expired; `resent` says a new code was sent |
|
|
793
793
|
| `allowRecovery` | `boolean` | `via === 'secondFactor'` | "Use a recovery code", which swaps the field for a text field and back |
|
|
794
794
|
| `length` | `number` | `6` | slots in the code field |
|
|
795
|
+
| `autoFocus` | `boolean` | — | puts the focus in the code field on mount |
|
|
795
796
|
| `id` | `string` | generated | the form's id; the buttons of `actions="none"` point at it with `form={id}`. Other `<form>` props pass through |
|
|
796
797
|
| `actions` | `'inline' \| 'none'` | `'inline'` | `'none'` leaves the buttons to you; point them at the form with `form={id}` |
|
|
797
798
|
| `submitLabel`, `secondaryAction` | `ReactNode` | "Confirm" | the button, and what sits beside it (Cancel), which stays usable when locked |
|
|
@@ -836,6 +837,12 @@ is described by the line, and the live region leaves it out. The words are
|
|
|
836
837
|
`labels.throttled`, `throttledMinutes(n)` and `throttledSeconds(n)` (`throttledMinutesApp(n)` and `throttledSecondsApp(n)` for `via="secondFactor"`). On the
|
|
837
838
|
server nothing waits and no timer runs.
|
|
838
839
|
|
|
840
|
+
**After a resend settles** (answered with or without a throttle), the focus
|
|
841
|
+
returns to the code field when "Resend" held it, or the page did, as the resend
|
|
842
|
+
started (a host that sets `pending` disables the button, which drops the focus
|
|
843
|
+
to the page): WCAG 2.4.3. A focus the user moved elsewhere in the meantime is
|
|
844
|
+
left alone.
|
|
845
|
+
|
|
839
846
|
**With no tries left** (`attemptsLeft === 0`) the form reads the answer by what
|
|
840
847
|
the code was: an e-mailed code shows "expired, ask for a new code" (nothing is
|
|
841
848
|
held against it), and an app or recovery code shows the locked alert, because
|
|
@@ -240,6 +240,66 @@ navigation to learn.
|
|
|
240
240
|
|
|
241
241
|
A `linkComponent` must forward `onClick` (with `className`): it is how `onRowClick` runs before the card's link navigates. A click that opens the link elsewhere (Ctrl, Cmd, Shift, Alt, the middle button) only opens it: `onRowClick` does not run. A title that is a button (`onRowClick` and no href) runs it on every click.
|
|
242
242
|
|
|
243
|
+
## Status tiles and boards
|
|
244
|
+
|
|
245
|
+
A board of resources (beds, rooms, vehicles) is not a list of records: it is a
|
|
246
|
+
grid of compact cells you read at a glance and change in place. Use
|
|
247
|
+
[`ActionCard variant="tile"`](../components/buttons-and-actions.md#actioncard-tile)
|
|
248
|
+
in a `ResponsiveGrid minItem="xs"`, one grid per section under its own `h2`:
|
|
249
|
+
|
|
250
|
+
- **`status`** is a soft `Badge` with its word and an icon, so a status is
|
|
251
|
+
never told by colour alone.
|
|
252
|
+
- **`meta`** is one line, the occupant only while occupied; a long one is
|
|
253
|
+
clipped and shown in full in a tooltip.
|
|
254
|
+
- **`action`** is a `RowMenu` with a `DropdownMenuRadioGroup` of statuses,
|
|
255
|
+
named "Change status of {name}". The title is the one button (it opens the
|
|
256
|
+
detail), so the card and its menu are two tab stops and nothing nests.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import {
|
|
260
|
+
ActionCard,
|
|
261
|
+
Badge,
|
|
262
|
+
DropdownMenuRadioGroup,
|
|
263
|
+
DropdownMenuRadioItem,
|
|
264
|
+
ResponsiveGrid,
|
|
265
|
+
RowMenu,
|
|
266
|
+
} from '@nxgt/material/components';
|
|
267
|
+
|
|
268
|
+
<ResponsiveGrid minItem="xs">
|
|
269
|
+
<ActionCard
|
|
270
|
+
variant="tile"
|
|
271
|
+
title="Bed A1"
|
|
272
|
+
status={<Badge variant="soft" tone="warning" icon="user">Occupied</Badge>}
|
|
273
|
+
meta="Amara Diallo"
|
|
274
|
+
onClick={() => openDetail('a1')}
|
|
275
|
+
action={
|
|
276
|
+
<RowMenu
|
|
277
|
+
label="Bed A1"
|
|
278
|
+
triggerProps={{ 'aria-label': 'Change status of Bed A1' }}
|
|
279
|
+
>
|
|
280
|
+
<DropdownMenuRadioGroup value={status} onValueChange={setStatus}>
|
|
281
|
+
<DropdownMenuRadioItem value="free">Free</DropdownMenuRadioItem>
|
|
282
|
+
<DropdownMenuRadioItem value="occupied">Occupied</DropdownMenuRadioItem>
|
|
283
|
+
<DropdownMenuRadioItem value="cleaning">Cleaning</DropdownMenuRadioItem>
|
|
284
|
+
</DropdownMenuRadioGroup>
|
|
285
|
+
</RowMenu>
|
|
286
|
+
}
|
|
287
|
+
/>
|
|
288
|
+
</ResponsiveGrid>;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`title` is required with `action`: it is the card's button, named after the
|
|
292
|
+
title, the status and the meta line.
|
|
293
|
+
|
|
294
|
+
For the figures of the whole board, put a `StatCardGroup` above a `DataTable`
|
|
295
|
+
with one row per section, one numeric column per status (they add up to the
|
|
296
|
+
total) (`meta: { numeric: true }`) and a
|
|
297
|
+
rate cell (a `Progress` named "{section} occupancy" beside the visible
|
|
298
|
+
percent); `layout={{ phone: 'tiles', wide: 'table' }}` turns the rows into
|
|
299
|
+
tiles on a phone. Both screens, tested at 360 and 1440 px, light and dark, are
|
|
300
|
+
the `Guides/Recipes/Status board` story, and the Claude Code plugin's
|
|
301
|
+
`screen-recipes` skill ships their code.
|
|
302
|
+
|
|
243
303
|
## Troubleshooting
|
|
244
304
|
|
|
245
305
|
**The sort control is missing, or an option reads like the column id.** The control needs a column that can be sorted (an accessor, `enableSorting` not `false`); an option is the column's `meta.label`, else a string `header`. Set `meta.label`.
|
|
@@ -6,7 +6,8 @@ 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
8
|
It uses the URL codec (`defineFilterModel`, `parseFilterQuery`,
|
|
9
|
-
`serializeFilterQuery
|
|
9
|
+
`serializeFilterQuery`, plus `effectiveConditions` and `splitFilterErrors` for
|
|
10
|
+
a loader), which `@nxgt/material/components` re-exports (it is
|
|
10
11
|
also in `@nxgt/material-core/filter`, so the examples below work with either
|
|
11
12
|
import) and React Router's
|
|
12
13
|
framework mode. Any router works the same way: the codec takes and returns
|
|
@@ -71,7 +72,7 @@ a condition with no readable value left is still dropped.
|
|
|
71
72
|
|
|
72
73
|
```ts
|
|
73
74
|
// app/routes/students.tsx
|
|
74
|
-
import { effectiveConditions, parseFilterQuery } from '@nxgt/material
|
|
75
|
+
import { effectiveConditions, parseFilterQuery } from '@nxgt/material/components';
|
|
75
76
|
import type { Route } from './+types/students';
|
|
76
77
|
import { STUDENT_FILTERS } from './students.filters';
|
|
77
78
|
import { LOCKED } from './students.locked';
|
package/docs/roadmap.md
CHANGED
|
@@ -330,6 +330,17 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
330
330
|
|
|
331
331
|
## Shipped
|
|
332
332
|
|
|
333
|
+
- **A status board of tiles** (3.49.0) — `ActionCard` gains `status` (a soft badge line),
|
|
334
|
+
`meta` (one line, its tooltip only while clipped), a trailing `action` (a
|
|
335
|
+
`RowMenu`, an `IconButton`) outside the press surface, and `variant="tile"`
|
|
336
|
+
for grids of compact cells. With an `action` the title is the card's one
|
|
337
|
+
button, so the card and its menu are two tab stops and nothing nests. The
|
|
338
|
+
`Guides/Recipes/Status board` recipe (tiles in sections with a menu that
|
|
339
|
+
changes the status, and an occupancy table with a rate per section) shows
|
|
340
|
+
the whole pattern, with its code in the plugin's `screen-recipes` skill.
|
|
341
|
+
React only for now; the Vue gap is a `vue` issue.
|
|
342
|
+
([ActionCard tile](components/buttons-and-actions.md#actioncard-tile))
|
|
343
|
+
|
|
333
344
|
- **A refused field says why** (3.48.0) — the filter bar shows the words of a
|
|
334
345
|
refused field under it, in one error `Alert`: "Period: choose 366 days or
|
|
335
346
|
fewer.", after `formError`, so a refused, empty list no longer shows a red
|
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
@import "./ui/feedback/toast/toast-tokens.css";
|
|
192
192
|
@import "./ui/actions/action-card/action-card.css";
|
|
193
193
|
@import "./ui/actions/action-card/action-card-indicator.css";
|
|
194
|
+
@import "./ui/actions/action-card/action-card-tile.css";
|
|
194
195
|
@import "./ui/actions/action-card/action-card-modes.css";
|
|
195
196
|
@import "./ui/media/galleries/gallery-image-lists.css";
|
|
196
197
|
/* Gallery: the View Transition from a tile to its lightbox. */
|
|
@@ -37,4 +37,52 @@
|
|
|
37
37
|
.nxgt-action-card[data-active] .nxgt-action-card-indicator svg {
|
|
38
38
|
stroke-dashoffset: 0;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/* The indicator takes its own column, after the action, only when shown. */
|
|
42
|
+
.nxgt-action-card[data-size="tile"]:has(> .nxgt-action-card-indicator) {
|
|
43
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
44
|
+
grid-template-areas: "title ind" "status status" "desc desc" "meta meta";
|
|
45
|
+
}
|
|
46
|
+
.nxgt-action-card[data-size="tile"][data-icon]:has(
|
|
47
|
+
> .nxgt-action-card-indicator
|
|
48
|
+
) {
|
|
49
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
50
|
+
grid-template-areas:
|
|
51
|
+
"icon title ind"
|
|
52
|
+
". status status"
|
|
53
|
+
". desc desc"
|
|
54
|
+
". meta meta";
|
|
55
|
+
}
|
|
56
|
+
.nxgt-action-card[data-size="tile"][data-has-action]:has(
|
|
57
|
+
> .nxgt-action-card-indicator
|
|
58
|
+
) {
|
|
59
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
60
|
+
grid-template-areas:
|
|
61
|
+
"title act ind"
|
|
62
|
+
"status status status"
|
|
63
|
+
"desc desc desc"
|
|
64
|
+
"meta meta meta";
|
|
65
|
+
}
|
|
66
|
+
.nxgt-action-card[data-size="tile"][data-has-action][data-icon]:has(
|
|
67
|
+
> .nxgt-action-card-indicator
|
|
68
|
+
) {
|
|
69
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
70
|
+
grid-template-areas:
|
|
71
|
+
"icon title act ind"
|
|
72
|
+
". status status status"
|
|
73
|
+
". desc desc desc"
|
|
74
|
+
". meta meta meta";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* The indicator is centred on the title row (36px high). */
|
|
78
|
+
.nxgt-action-card[data-size="tile"] .nxgt-action-card-indicator {
|
|
79
|
+
align-self: start;
|
|
80
|
+
margin-block: 5px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* The action is no longer at the card's edge: no optical offset. */
|
|
84
|
+
.nxgt-action-card[data-size="tile"]:has(> .nxgt-action-card-indicator)
|
|
85
|
+
.nxgt-action-card-action {
|
|
86
|
+
margin-inline-end: 0;
|
|
87
|
+
}
|
|
40
88
|
}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
.nxgt-action-card * {
|
|
8
8
|
transition: none;
|
|
9
9
|
}
|
|
10
|
-
.nxgt-action-card[data-interactive]:not([data-disabled]):active
|
|
10
|
+
.nxgt-action-card[data-interactive]:not([data-disabled]):active,
|
|
11
|
+
.nxgt-action-card[data-has-action][data-interactive]:not(
|
|
12
|
+
[data-disabled]
|
|
13
|
+
):has(.nxgt-action-card-button:active) {
|
|
11
14
|
scale: none;
|
|
12
15
|
--ac-r: 12px;
|
|
13
16
|
}
|