@nxgt/material 3.55.0 → 3.56.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 +16 -0
- package/README.md +7 -3
- package/dist/{async-slice-yhXT9cml.js → async-slice-CORlM_wu.js} +2 -2
- package/dist/{button-CD7f4KFL.js → button-CdpVEza0.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-Cl7Wm8Pn.js → chip-C0yNYz9m.js} +2 -2
- package/dist/{code-editor-tOhXy6Qo.js → code-editor-Bwzp2038.js} +6 -6
- package/dist/code-editor.js +5 -5
- package/dist/{code-language-items-BxlY6onk.js → code-language-items-nP-pF7yH.js} +2 -2
- package/dist/components/components.css +4 -0
- package/dist/components/ui/display/secret-card/secret-card.css +65 -0
- package/dist/components/ui/display/vote-tally/vote-tally.css +94 -0
- package/dist/{components-B58ZWZYR.js → components-Dl2hVfzt.js} +16817 -16226
- package/dist/components.js +26 -26
- package/dist/{dialog-BzfHcRo6.js → dialog-BVgsN9Ch.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-DUZF4xAJ.js → dropdown-menu-C8PBmMIa.js} +1 -1
- package/dist/editor.js +9 -9
- package/dist/{empty-state-D9o2id1p.js → empty-state-C5wfbbW9.js} +2 -2
- package/dist/{escape-owner-BYs2_7T5.js → escape-owner-CYnfySUl.js} +1 -1
- package/dist/{field-blur-r1tHwqdF.js → field-blur-Nid7-bt_.js} +1 -1
- package/dist/{field-blur-context-ZsK8OTNg.js → field-blur-context-DxdIuHAY.js} +1 -1
- package/dist/hooks.js +28 -28
- package/dist/{hover-card-NfF8taLj.js → hover-card-C_9jiwp1.js} +2 -2
- package/dist/{i18n-provider-TQAEPlJ5.js → i18n-provider-CLVZK6tQ.js} +1 -1
- package/dist/i18n.js +5 -5
- package/dist/{icon-badge-BlaY_a-P.js → icon-badge-Dcjg_SKJ.js} +3 -3
- package/dist/{icon-button-B8tdlP6R.js → icon-button-Bo-VuZ2n.js} +1 -1
- package/dist/lib/components/ui/actions/link/link.d.ts +8 -0
- package/dist/lib/components/ui/collections/data-table/partials/data-table-page-control.d.ts +1 -1
- package/dist/lib/components/ui/collections/data-table/partials/data-table-row.d.ts +1 -1
- package/dist/lib/components/ui/collections/data-table/partials/layouts/data-table-card.d.ts +1 -1
- package/dist/lib/components/ui/collections/server-table/partials/server-table-past-end.d.ts +1 -1
- package/dist/lib/components/ui/display/index.d.ts +2 -0
- package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +1 -1
- package/dist/lib/components/ui/display/period-cards/period-cards.d.ts +1 -1
- package/dist/lib/components/ui/display/secret-card/index.d.ts +2 -0
- package/dist/lib/components/ui/display/secret-card/secret-card.d.ts +11 -0
- package/dist/lib/components/ui/display/secret-card/types.d.ts +49 -0
- package/dist/lib/components/ui/display/secret-card/use-secret-card.d.ts +20 -0
- package/dist/lib/components/ui/display/vote-tally/announcer.d.ts +11 -0
- package/dist/lib/components/ui/display/vote-tally/index.d.ts +2 -0
- package/dist/lib/components/ui/display/vote-tally/partials/tally-bar.d.ts +8 -0
- package/dist/lib/components/ui/display/vote-tally/partials/tally-row.d.ts +9 -0
- package/dist/lib/components/ui/display/vote-tally/partials/tally-voters.d.ts +9 -0
- package/dist/lib/components/ui/display/vote-tally/types.d.ts +73 -0
- package/dist/lib/components/ui/display/vote-tally/use-announcement.d.ts +7 -0
- package/dist/lib/components/ui/display/vote-tally/use-tally-summary.d.ts +3 -0
- package/dist/lib/components/ui/display/vote-tally/vote-tally.d.ts +10 -0
- package/dist/lib/components/ui/display/vote-tally/vote-tally.utils.d.ts +18 -0
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/layout/account-menu/account-menu.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/app-sidebar-icon-item.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/app-sidebar-label-item.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/app-sidebar.d.ts +1 -1
- package/dist/lib/components/ui/layout/pager/pager.d.ts +1 -1
- package/dist/lib/components/ui/layout/see-also/see-also.d.ts +1 -1
- package/dist/lib/components/ui/layout/toc/toc.d.ts +1 -1
- package/dist/lib/components/ui/primitives/index.d.ts +1 -0
- package/dist/lib/components/ui/primitives/link/index.d.ts +2 -0
- package/dist/lib/components/ui/primitives/link/link-component-provider.d.ts +26 -0
- package/dist/lib/components/ui/widgets/education/index.d.ts +1 -0
- package/dist/lib/components/ui/widgets/education/navigator/index.d.ts +2 -0
- package/dist/lib/components/ui/widgets/education/navigator/partials/navigator-item.d.ts +10 -0
- package/dist/lib/components/ui/widgets/education/navigator/question-navigator.d.ts +27 -0
- package/dist/lib/components/ui/widgets/education/questions/index.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/partials/question-media.d.ts +5 -0
- package/dist/lib/components/ui/widgets/education/questions/partials/question-verdict.d.ts +12 -0
- package/dist/lib/components/ui/widgets/education/questions/question-options.d.ts +2 -2
- package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/types.d.ts +33 -1
- package/dist/lib/hooks/index.d.ts +1 -0
- package/dist/lib/hooks/use-countdown.d.ts +18 -0
- package/dist/lib/i18n/resources/en/en.d.ts +31 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +31 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +31 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +31 -0
- package/dist/lib/i18n/resources/resources.d.ts +62 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +43 -43
- package/dist/map.js +8 -8
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +2 -2
- package/dist/{public-tWgM204K.js → public-BtKAthSs.js} +9 -9
- package/dist/{rich-code-block-0OcsqURY.js → rich-code-block-BZYZ6ewr.js} +5 -5
- package/dist/{sortable-provider-DzFAlPU_.js → sortable-provider-hUCwt69K.js} +2 -2
- package/dist/{spinner-GKZcQmrR.js → spinner-BmUXTzvJ.js} +1 -1
- package/dist/{stored-language-B39mABN7.js → stored-language-_OGPBCn0.js} +62 -0
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-C8idk_jQ.js → theme-toggle-DdvEafno.js} +1 -1
- package/dist/{toolbar-items-BrJI-BCs.js → toolbar-items-DKfY_6AG.js} +4 -4
- package/dist/{translate-material-hUA-UAP6.js → translate-material-DzBMPErh.js} +2 -2
- package/dist/{translator-1ql2sxrx.js → translator-BIVSiq4X.js} +1 -1
- package/dist/{use-chat-composer-DQpb8ZXV.js → use-chat-composer-DCQW2EJ8.js} +16 -16
- package/dist/{use-heading-spy-vlPOI1Aj.js → use-heading-spy-Cr2C_Px8.js} +32 -5
- package/dist/{use-material-translation-B1p5y3s9.js → use-material-translation-C_bAxfvp.js} +2 -2
- package/docs/components/README.md +1 -1
- package/docs/components/buttons-and-actions.md +1 -0
- package/docs/components/data-display.md +147 -2
- package/docs/components/data-table-filter-and-lists.md +3 -3
- package/docs/components/feedback-and-status.md +5 -3
- package/docs/components/navigation-and-layout.md +4 -4
- package/docs/components/primitives.md +62 -0
- package/docs/components/rich-widgets.md +51 -0
- package/docs/components/selection-and-choice.md +1 -1
- package/docs/guides/app-shell.md +2 -2
- package/docs/guides/helpers.md +40 -0
- package/docs/previews/data-display-secretcard.webp +0 -0
- package/docs/previews/data-display-votetally.webp +0 -0
- package/lib/components/components.css +4 -0
- package/lib/components/ui/display/secret-card/secret-card.css +65 -0
- package/lib/components/ui/display/vote-tally/vote-tally.css +94 -0
- package/package.json +1 -1
- package/dist/lib/components/ui/feedback/countdown-timer/use-countdown.d.ts +0 -6
|
@@ -760,7 +760,9 @@ import { CountdownTimer } from '@nxgt/material/components';
|
|
|
760
760
|
It ticks on the client only: the server renders the full time. The clock is a
|
|
761
761
|
`role="timer"`; a polite live region speaks only when a threshold is crossed
|
|
762
762
|
("1:00 left") and when the time is up, never every second. `formatClock` and
|
|
763
|
-
`countdownTone` are exported too
|
|
763
|
+
`countdownTone` are exported too, and so is the clock itself as the
|
|
764
|
+
[`useCountdown`](../guides/helpers.md#usecountdown) hook (`@nxgt/material/hooks`),
|
|
765
|
+
for a button that waits without a ring.
|
|
764
766
|
|
|
765
767
|
**Storybook:** `Feedback & status/CountdownTimer` · **Vue:** React only for now (#890)
|
|
766
768
|
|
|
@@ -1210,7 +1212,7 @@ function invite(answer: (id: string, yes: boolean) => void): NotificationItem {
|
|
|
1210
1212
|
| `footer` | `ReactNode` | — | under the list: "View all", settings… |
|
|
1211
1213
|
| `fullScreenOnMobile` | `boolean` | `true` | a phone gets a full-screen sheet instead of the popover |
|
|
1212
1214
|
| `toastOnArrival` | `boolean` | `false` | shows a toast for each new item (needs a `Toaster`) |
|
|
1213
|
-
| `linkComponent` | `LinkComponent` | native `<a>` | renders each row's `href` through your router's link, so a click navigates without a page load: `linkComponent={({ href, ...p }) => <Link to={href} {...p} />}` (spread every prop, as for `AppShell`) |
|
|
1215
|
+
| `linkComponent` | `LinkComponent` | native `<a>` | renders each row's `href` through your router's link, so a click navigates without a page load: `linkComponent={({ href, ...p }) => <Link to={href} {...p} />}` (spread every prop, as for `AppShell`). Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
1214
1216
|
| `error` / `onRetry` | `ReactNode \| boolean` / `() => unknown` | — | the feed could not be read: the list is replaced by an error line (`true` for the default words, a node for your own message) and, with `onRetry`, a Retry button (a returned promise keeps it loading). The empty state is not shown; counts still come from `items` / `count` |
|
|
1215
1217
|
|
|
1216
1218
|
A row is interactive only when a click does something: it has an `href`, or
|
|
@@ -1284,7 +1286,7 @@ import { NotificationRow } from '@nxgt/material/components';
|
|
|
1284
1286
|
| `createdAt` / `now` | `Date \| string \| number` | — | the time, counted from `now` (the clock when unset) |
|
|
1285
1287
|
| `unread` | `boolean` | `false` | tint, bold, dot and edge |
|
|
1286
1288
|
| `href` / `onOpen` | — | — | the row as a link or a button |
|
|
1287
|
-
| `linkComponent` | `LinkComponent` | native `<a>` | renders the `href` link through your router's link (`Notifications` passes its own down) |
|
|
1289
|
+
| `linkComponent` | `LinkComponent` | native `<a>` | renders the `href` link through your router's link (`Notifications` passes its own down). Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
1288
1290
|
| `onMarkRead` / `onDismiss` | `() => void` | — | the quick actions (mark read only while unread) |
|
|
1289
1291
|
| `actions` | `NotificationAction[]` | — | inline buttons; the first is filled, the rest tonal (`emphasis` overrides) |
|
|
1290
1292
|
| `density` | `'comfortable' \| 'compact'` | `'comfortable'` | spacing, avatar size, one-line description |
|
|
@@ -66,7 +66,7 @@ phone's menu sheet pad clear of `env(safe-area-inset-*)`, as classes; see the
|
|
|
66
66
|
| Prop | Type | Default | Effect |
|
|
67
67
|
| --- | --- | --- | --- |
|
|
68
68
|
| `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon`, `footer`, `display` (`'icon'`: a round icon-only button, the label as tooltip and name; see the [app shell guide](../guides/app-shell.md)), `badge` (`true` a dot, a number a count up to "99+"), `badgeLabel` (see [Badges](../guides/app-shell.md#badges)) |
|
|
69
|
-
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href`; see the [app shell guide](../guides/app-shell.md) |
|
|
69
|
+
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href`; see the [app shell guide](../guides/app-shell.md). Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
70
70
|
| `logo`, `name` | `AppLogo` (`ReactNode \| string \| AppLogoImage`, `AppLogoImage` = `{ src, alt? }`), `string` | — | the app's logo alone in the collapsed rail, named by `name` (also its tooltip); logo and name side by side in the phone's menu sheet |
|
|
71
71
|
| `context` | `ReactNode` | — | a line at the top of the phone's menu sheet, above the brand: the current tenant's name; `name` stays the app's brand; `AppHeader` and `AppSidebar` take it too (see the [app shell guide](../guides/app-shell.md#the-brand-logo-and-name)) |
|
|
72
72
|
| `brand` | `ReactNode` | — | replaces the sidebar header, in the rail and the menu sheet; wins over `logo` and `name` |
|
|
@@ -148,7 +148,7 @@ import { AccountMenu, ActivityLayout } from '@nxgt/material/components';
|
|
|
148
148
|
| `secondary` | `ReactNode` | — | a second line under the name, the e-mail say |
|
|
149
149
|
| `src` | `string` | — | the avatar's picture; the initials until it loads, or without one |
|
|
150
150
|
| `items` | `AccountMenuItem[]` | `[]` | `{ label: ReactNode; url: string; icon?: IconName }`: links between the header and Sign out; `label` already translated, `icon` a [Font Awesome](https://fontawesome.com/icons) symbol id of the duotone sprite (see [Icon](primitives.md#icon)) |
|
|
151
|
-
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | the link each item renders, as in the app shell |
|
|
151
|
+
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | the link each item renders, as in the app shell. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
152
152
|
| `onSignOut` | `() => void` | — | Sign out was chosen; the menu stays open, so `signingOut` shows |
|
|
153
153
|
| `signOutLabel` | `ReactNode` | "Sign out" | `material.account-menu.sign-out` |
|
|
154
154
|
| `signingOut` | `boolean` | `false` | Sign out is busy (`aria-busy`, a spinner) and ignores a second selection |
|
|
@@ -588,7 +588,7 @@ import { Pager } from '@nxgt/material/components';
|
|
|
588
588
|
| `previous`, `next` | `{ title; href; description? }` | — | either may be omitted |
|
|
589
589
|
| `shortcuts` | `boolean` | `false` | shows `[` / `]` key hints (the `Doc` shell handles the keys) |
|
|
590
590
|
| `previousLabel`, `nextLabel` | `string` | translated | small labels |
|
|
591
|
-
| `linkComponent` | `ComponentType<{ href; className?; children? }>` | `<a>` | your router's link |
|
|
591
|
+
| `linkComponent` | `ComponentType<{ href; className?; children? }>` | `<a>` | your router's link. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
592
592
|
|
|
593
593
|
External `href`s open in a new tab.
|
|
594
594
|
|
|
@@ -617,7 +617,7 @@ import { SeeAlso } from '@nxgt/material/components';
|
|
|
617
617
|
| --- | --- | --- | --- |
|
|
618
618
|
| `items` | `{ id?; title; href }[]` | — | external links get the outbound icon |
|
|
619
619
|
| `label` | `string` | translated "See also" | heading |
|
|
620
|
-
| `linkComponent` | `ComponentType<{ href; className?; children? }>` | `<a>` | your router's link |
|
|
620
|
+
| `linkComponent` | `ComponentType<{ href; className?; children? }>` | `<a>` | your router's link. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
621
621
|
|
|
622
622
|
**Storybook:** `Navigation & layout/SeeAlso` · **Vue:** `SeeAlso`
|
|
623
623
|
|
|
@@ -28,6 +28,7 @@ other pages are built from these.
|
|
|
28
28
|
| <img src="../previews/primitives-togglegroup.webp" width="96" alt=""> | [ToggleGroup](#togglegroup) | Connected, segmented or separate toggle group |
|
|
29
29
|
| | [Activity](#activity) | React `<Activity>` switched by a boolean |
|
|
30
30
|
| | [NonceProvider](#nonceprovider) | Gives the page's CSP nonce to every component below |
|
|
31
|
+
| | [LinkComponentProvider](#linkcomponentprovider) | Sets, once, the link component of every component that renders a link |
|
|
31
32
|
|
|
32
33
|
## Accordion
|
|
33
34
|
|
|
@@ -960,6 +961,67 @@ import { Activity } from '@nxgt/material/components';
|
|
|
960
961
|
|
|
961
962
|
**Storybook:** — · **Vue:** `Activity`
|
|
962
963
|
|
|
964
|
+
## LinkComponentProvider
|
|
965
|
+
|
|
966
|
+
No preview: it draws nothing of its own, it hands the app's link component down.
|
|
967
|
+
|
|
968
|
+
Sets once the link the components below render their links with: `LinkButton`
|
|
969
|
+
(with `IconLinkButton` and `ResponsiveLinkButton`), the sidebar and app shell,
|
|
970
|
+
`AccountMenu`, `Pager`, `SeeAlso`, `Toc`, `PeriodCards`, `NotificationRow`,
|
|
971
|
+
`Doc` (which then skips its `onNavigate`, as for its own `linkComponent`),
|
|
972
|
+
`ListTile`, `DataTable`, `ServerTable`, `PageFooter`, `ListEmpty`, `Switcher`,
|
|
973
|
+
`Notifications`, `Breadcrumb` and `Pagination` links, `LinkCard` (internal addresses only; an external one stays a
|
|
974
|
+
plain `<a>`) and `QuestionNavigator`'s link variant.
|
|
975
|
+
|
|
976
|
+
Not wired, by design: `ActionCard`'s `titleLink` and `Markdown`'s links are
|
|
977
|
+
elements or content you supply, `SkipLink` and `PaginationBar` are in-page
|
|
978
|
+
(`#`) anchors, `ContactLinks` renders `mailto:` and `tel:` links, and the
|
|
979
|
+
`FileList` and `FilePreview` download links, `EnrolmentSecret`'s `otpauth:`
|
|
980
|
+
link and the editor's action card (`target="_blank"`) are not navigation. A component's own `linkComponent` prop still wins. Without the provider
|
|
981
|
+
and without the prop, each keeps its own default, so nothing changes until you
|
|
982
|
+
render it.
|
|
983
|
+
|
|
984
|
+
```tsx
|
|
985
|
+
import {
|
|
986
|
+
LinkComponentProvider,
|
|
987
|
+
type LinkComponentProps,
|
|
988
|
+
} from '@nxgt/material/components';
|
|
989
|
+
import { Link } from 'react-router';
|
|
990
|
+
|
|
991
|
+
// Spread every prop onto the element; a function component receives `ref`
|
|
992
|
+
// as a prop in React 19, so spreading forwards it.
|
|
993
|
+
const RouterLink = ({ href, ...props }: LinkComponentProps) => (
|
|
994
|
+
<Link to={href} {...props} />
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
<LinkComponentProvider component={RouterLink}>
|
|
998
|
+
<App />
|
|
999
|
+
</LinkComponentProvider>;
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
| Prop | Type | Default | Effect |
|
|
1003
|
+
| --- | --- | --- | --- |
|
|
1004
|
+
| `component` | `LinkComponent` | — | the default link: `ComponentType<LinkComponentProps>`, where `LinkComponentProps` is the anchor's props plus a required `href` |
|
|
1005
|
+
|
|
1006
|
+
`LinkComponent` and `LinkComponentProps` are exported from
|
|
1007
|
+
`@nxgt/material/components`, and `useLinkComponent(own?)` returns the link a
|
|
1008
|
+
component of your own should render (`own`, else the provider's, else
|
|
1009
|
+
`undefined`). `LinkButton` and its siblings take a router `to`: with a link
|
|
1010
|
+
component they hand it over as `href`, with `target` and `download`;
|
|
1011
|
+
`replace`, `state`, `reloadDocument` and `preventScrollReset` belong to React
|
|
1012
|
+
Router's `Link` and are not passed. They also take `linkComponent` themselves.
|
|
1013
|
+
|
|
1014
|
+
```tsx
|
|
1015
|
+
import { useLinkComponent } from '@nxgt/material/components';
|
|
1016
|
+
|
|
1017
|
+
function MyCrumb({ href, children }: { href: string; children: string }) {
|
|
1018
|
+
const Link = useLinkComponent() ?? 'a';
|
|
1019
|
+
return <Link href={href}>{children}</Link>;
|
|
1020
|
+
}
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
**Storybook:** — · **Vue:** —
|
|
1024
|
+
|
|
963
1025
|
## NonceProvider
|
|
964
1026
|
|
|
965
1027
|
No preview: it draws nothing of its own, it hands the page's Content Security
|
|
@@ -2412,7 +2412,58 @@ French bundle — see [i18n](../guides/i18n.md).
|
|
|
2412
2412
|
| `onComplete` | `(result: QuizResult) => void` | — | `{ correct, total, percent, answers, timedOut }`, once per results screen |
|
|
2413
2413
|
| `onAnswersChange` / `onIndexChange` | callbacks | — | follow progress |
|
|
2414
2414
|
| `QuestionCard` `revealed` | `boolean` | `false` | marks right / wrong and shows the verdict and `explanation` |
|
|
2415
|
+
| `QuestionCard` `verdict` | `'correct' \| 'incorrect' \| 'pending'` | — | the server's verdict, shown as given (word and icon, never colour alone); reveals the card. `question.correct` is optional and only highlights the right choices. Without it, `revealed` compares in the browser as before |
|
|
2416
|
+
| `QuestionCard` `points` / `feedback` | `{ awarded, possible }` / `string` | — | points earned and a grader's remark, in the verdict banner (with `verdict`) |
|
|
2417
|
+
| `Question` `media` | `{ src, alt?, kind?, type?, name? }` | — | an image (default), audio (`kind: 'audio'` or `type: 'audio/*'`, an `AudioPlayer`) or a document link (any other `type`, a `FilePreview`); `name` (else `alt`) is the text alternative |
|
|
2415
2418
|
| `QuestionCard` `value` / `onValueChange` | `QuestionAnswer` | — | controlled answer |
|
|
2416
2419
|
| `QuestionCard` `variant` / `density` / `showShortcuts` / `number` / `showFeedback` | — | `'card'`, `'comfortable'`, `false`, —, `true` | framed or plain, spacing, key badges, "Question 2", the verdict banner |
|
|
2417
2420
|
|
|
2421
|
+
A server-graded answer, shown as given (no comparison in the browser):
|
|
2422
|
+
|
|
2423
|
+
```tsx
|
|
2424
|
+
<QuestionCard
|
|
2425
|
+
question={{ id: 'q1', kind: 'text', prompt: 'Explain X' }}
|
|
2426
|
+
value="my answer"
|
|
2427
|
+
verdict="incorrect" // or 'correct' | 'pending'
|
|
2428
|
+
points={{ awarded: 2, possible: 3 }}
|
|
2429
|
+
feedback="Good, but cite a source"
|
|
2430
|
+
/>
|
|
2431
|
+
```
|
|
2432
|
+
|
|
2418
2433
|
**Storybook:** `Rich widgets/Quiz`, `Rich widgets/QuestionCard` · **Vue:** `Quiz`, `QuestionCard` (the modes, results, number kind and shortcuts are React only for now, #890)
|
|
2434
|
+
|
|
2435
|
+
### QuestionNavigator
|
|
2436
|
+
|
|
2437
|
+
The numbered questions of a quiz or exam player: jump to one, see the current
|
|
2438
|
+
one and which are answered or flagged. Import it from
|
|
2439
|
+
`@nxgt/material/widgets/education`.
|
|
2440
|
+
|
|
2441
|
+
```tsx
|
|
2442
|
+
<QuestionNavigator
|
|
2443
|
+
count={20}
|
|
2444
|
+
current={3}
|
|
2445
|
+
answered={[1, 2]} // or a Set
|
|
2446
|
+
flagged={[2]}
|
|
2447
|
+
onSelect={(n) => go(n)}
|
|
2448
|
+
/>
|
|
2449
|
+
// URL-driven player: <QuestionNavigator count={20} current={3} getHref={(n) => `/quiz/${n}`} />
|
|
2450
|
+
```
|
|
2451
|
+
|
|
2452
|
+
| Prop | Type | Default | Notes |
|
|
2453
|
+
| --- | --- | --- | --- |
|
|
2454
|
+
| `count` | `number` | — | how many questions |
|
|
2455
|
+
| `current` | `number` | — | the current question, **from 1**; gets `aria-current="step"` |
|
|
2456
|
+
| `answered` / `flagged` | `number[] \| Set<number>` | — | question numbers, from 1 |
|
|
2457
|
+
| `onSelect` | `(n: number) => void` | — | each number is a button |
|
|
2458
|
+
| `getHref` | `(n: number) => string` | — | each number is a link; wins over `onSelect`. With neither, the numbers are plain marks |
|
|
2459
|
+
| `label` | `string` | `"Questions"` | the navigation's name |
|
|
2460
|
+
|
|
2461
|
+
Each target is at least 44 x 44 px. Answered shows a check and flagged a flag,
|
|
2462
|
+
and both are named in the accessible label ("Question 2, answered, flagged,
|
|
2463
|
+
current"), never by colour alone. With many questions it is **one row that
|
|
2464
|
+
scrolls sideways and keeps the current number in view** (rather than a wrapping
|
|
2465
|
+
grid): its height stays constant at 20 or 100 questions on a 360px phone, so it
|
|
2466
|
+
can sit above the question without pushing it down. Labels live under
|
|
2467
|
+
`material.question-navigator.*` (`label`, `item`, `answered`, `unanswered`, `flagged`, `current`).
|
|
2468
|
+
|
|
2469
|
+
**Storybook:** `Rich widgets/QuestionNavigator` · **Vue:** not yet (React first, #1948)
|
|
@@ -1351,7 +1351,7 @@ To put a Switcher at the start of a page header, pass it as [EntityHeader](data-
|
|
|
1351
1351
|
| `searchable` | `boolean` | from 8 options (always, expressive) | the search field of the panel; `true` also replaces the menu with the panel |
|
|
1352
1352
|
| `recent` | `string[]` | — | up to 3 option ids shown as tiles (expressive) |
|
|
1353
1353
|
| `getHref` | `(item) => string \| undefined` | — | link mode; falls back to `item.href` |
|
|
1354
|
-
| `linkComponent` | `LinkComponent` | `<a>` | your router's link: takes `href`, spreads every prop, forwards `ref` |
|
|
1354
|
+
| `linkComponent` | `LinkComponent` | `<a>` | your router's link: takes `href`, spreads every prop, forwards `ref`. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
1355
1355
|
| `footer` | `{ label?, onSelect?, href? }` | — | a "Manage…" button (a link with `href`) at the end of the panel; `label` defaults to `material.switcher.manage` |
|
|
1356
1356
|
| `emptyLabel` | `ReactNode` | `material.switcher.empty` | shown when the search matches nothing |
|
|
1357
1357
|
| `sheet` | `boolean` | on a phone | `true` always opens a sheet, `false` never |
|
package/docs/guides/app-shell.md
CHANGED
|
@@ -198,7 +198,7 @@ partitionMenu(items); // { content, footer }
|
|
|
198
198
|
| `context` | `ReactNode` | — | A line at the top of the phone's menu sheet, above the brand: the current tenant's name, already translated (see [The brand](#the-brand-logo-and-name)). `name` stays the app's brand; the collapsed rail shows no context |
|
|
199
199
|
| `brand` | `ReactNode` | — | Replaces the sidebar header entirely, in the rail and in the menu sheet; wins over `logo` and `name` |
|
|
200
200
|
| `avatar` | `ReactNode` | — | End of the header. A `string` is used as the image URL of an `Avatar`; anything else is rendered as-is |
|
|
201
|
-
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href` (see below) |
|
|
201
|
+
| `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href` (see below). Defaults to the [`LinkComponentProvider`](../components/primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
202
202
|
| `headerTitle` | `ReactNode` | — | The header's leading title: the context the app is in (the active organisation's name), right after the phone's menu button, truncated when long; when it is cut off, its full text is the `title` tooltip (#629) |
|
|
203
203
|
| `titleAfter` | `ReactNode` | — | A node right after the page's title in the app bar, outside its `h1` (a record's `Switcher`); wins over a route's `handle.titleAfter` (see [A slot after the title](#a-slot-after-the-title)) |
|
|
204
204
|
| `headerContext` | `ReactNode` | — | A small muted context line (the tenant's name) after the page's title and its slot, from `md` up: outside the `h1`, truncated, its full text the tooltip once cut off. Not shown on a phone, where the menu sheet's `context` carries it (see [A context line](#a-context-line-after-the-title)) |
|
|
@@ -231,7 +231,7 @@ hydration on a server-rendered page, opens in a new tab on middle-click, and
|
|
|
231
231
|
is announced as a link, named by its label alone ("Identities": the icon beside
|
|
232
232
|
it is decorative); the current page's entry carries `aria-current="page"`.
|
|
233
233
|
By default it is a React Router `Link` (client-side navigation, the router's
|
|
234
|
-
`basename`), so leaving `linkComponent` out never reloads the page. Pass it only
|
|
234
|
+
`basename`), so leaving `linkComponent` out never reloads the page; a [`LinkComponentProvider`](../components/primitives.md#linkcomponentprovider) above the shell sets another default once. Pass it only
|
|
235
235
|
for another router or a custom link: a component taking
|
|
236
236
|
`href`. It must spread **every other prop it receives** onto the element it
|
|
237
237
|
renders (`className`, `aria-current`, the tooltip's handlers, `data-state`) and
|
package/docs/guides/helpers.md
CHANGED
|
@@ -137,6 +137,7 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
|
|
|
137
137
|
| `useAllowedTransitions(status, transitions, configs)` | Status-machine actions (icon, colour, label, dialog copy) allowed from the current status. |
|
|
138
138
|
| `useFocusOnMount<T>(focus = true)` | A ref whose element takes focus once mounted, and again whenever `focus` turns true: the heading of what replaced the content the user acted on. See [below](#usefocusonmount). |
|
|
139
139
|
| `useScrollAway(ref?, { threshold? })` | True while the reader scrolls down (by `threshold`, 24 px, since the scroll last turned); false scrolling up, at the top or at the end. Folds an extended `Fab`. Without `ref`, the window; false on the server. See [Fab](../components/buttons-and-actions.md#fab). |
|
|
140
|
+
| `useCountdown(duration, { paused?, onExpire?, key? })` | Seconds left of `duration` (the clock `CountdownTimer` draws), for a button or a message that waits: ticks on the client only, `duration` on the server and first render. A new `duration` **or a new `key`** starts over, so the same `retryAfter` twice restarts it. See [below](#usecountdown). |
|
|
140
141
|
| `useSubmitOnce(busy?)` | One submit in flight: wraps a submit handler so a double click or a second Enter is dropped until the request ends (the handler's promise, or `busy` back to false). See [below](#usesubmitonce). |
|
|
141
142
|
| `createSubmitGuard()`, `busySettler(guard)` | The framework-free guard behind `useSubmitOnce` (`@nxgt/material-core/submit-guard`): `{ once(run, { settleSync? }), settle(run?), pendingRun() }`, and the function that settles it from a busy flag. |
|
|
142
143
|
|
|
@@ -204,6 +205,45 @@ export function Sent({ email }: { email: string }) {
|
|
|
204
205
|
`useFocusOnMount(open)` focuses again whenever `open` turns true, for content
|
|
205
206
|
that stays mounted and is only shown. Storybook: `Guides/Hooks/useFocusOnMount`.
|
|
206
207
|
|
|
208
|
+
### `useCountdown`
|
|
209
|
+
|
|
210
|
+
The clock behind [`CountdownTimer`](../components/feedback-and-status.md#countdowntimer),
|
|
211
|
+
without the ring: the seconds left, for a submit button that stays disabled
|
|
212
|
+
while a rate limit (a 429 `retryAfter`) runs out.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { useState } from 'react';
|
|
216
|
+
import { Button } from '@nxgt/material/components';
|
|
217
|
+
import { useCountdown } from '@nxgt/material/hooks';
|
|
218
|
+
|
|
219
|
+
export function SignInButton({ signIn }: { signIn: () => Promise<{ retryAfter?: number }> }) {
|
|
220
|
+
const [wait, setWait] = useState({ seconds: 0, refusals: 0 });
|
|
221
|
+
const left = useCountdown(wait.seconds, { key: wait.refusals });
|
|
222
|
+
return (
|
|
223
|
+
<Button
|
|
224
|
+
disabled={left > 0}
|
|
225
|
+
onClick={async () => {
|
|
226
|
+
const { retryAfter } = await signIn();
|
|
227
|
+
if (retryAfter) setWait((w) => ({ seconds: retryAfter, refusals: w.refusals + 1 }));
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{left > 0 ? `Try again in ${Math.ceil(left)} s` : 'Sign in'}
|
|
231
|
+
</Button>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
| Option | Default | Effect |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| `paused` | `false` | holds the clock where it is; it carries on from there |
|
|
239
|
+
| `onExpire` | none | called once, when it reaches zero |
|
|
240
|
+
| `key` | none (`string \| number`) | a new value starts the clock over from `duration`, even for the same duration: without it, a second refusal that answers `retryAfter: 30` again would not restart anything |
|
|
241
|
+
|
|
242
|
+
It returns a number of seconds, fractional (it ticks every 250 ms): use
|
|
243
|
+
`Math.ceil` to show it, `formatClock` (from `@nxgt/material/components`) for
|
|
244
|
+
`m:ss`. The options type is exported as `UseCountdownOptions`. A `duration` of `0` is
|
|
245
|
+
already expired and never calls `onExpire`.
|
|
246
|
+
|
|
207
247
|
### `useSubmitOnce`
|
|
208
248
|
|
|
209
249
|
A disabled or `loading` button only stops clicks once it has rendered, and
|
|
Binary file
|
|
Binary file
|
|
@@ -138,6 +138,10 @@
|
|
|
138
138
|
@import "./ui/display/seat-ring/seat-ring.css";
|
|
139
139
|
@import "./ui/display/seat-ring/seat-ring-index.css";
|
|
140
140
|
@import "./ui/display/seat-ring/seat-ring-ring.css";
|
|
141
|
+
/* VoteTally: ranked bars with counts, shares and voters. */
|
|
142
|
+
@import "./ui/display/vote-tally/vote-tally.css";
|
|
143
|
+
/* SecretCard: a face that is not rendered until it is revealed. */
|
|
144
|
+
@import "./ui/display/secret-card/secret-card.css";
|
|
141
145
|
/* Alert and Banner span the row in the form grids (#1497). */
|
|
142
146
|
@import "./ui/feedback/alert/alert.css";
|
|
143
147
|
@import "./ui/display/stat-card/stat-card.css";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* SecretCard: a card whose secret face is not rendered until it is revealed.
|
|
2
|
+
* It carries `data-tone`, so the tone's tokens resolve on it. The hidden face
|
|
3
|
+
* is a quiet diagonal pattern in the tone's ring colour. The card's border is
|
|
4
|
+
* that colour too (set by a utility class on the Card, which wins over the
|
|
5
|
+
* Card's own). A change flips the face in, never on mount, and only when the person has not asked for reduced motion. The card is
|
|
6
|
+
* full width up to a reading width and keeps the reveal button a 44px target
|
|
7
|
+
* on a phone. */
|
|
8
|
+
@layer components {
|
|
9
|
+
.nxgt-secret-card {
|
|
10
|
+
width: 100%;
|
|
11
|
+
max-width: 28rem;
|
|
12
|
+
margin-inline: auto;
|
|
13
|
+
--secret-card-line: var(--nxgt-tone-ring, var(--border));
|
|
14
|
+
border-color: var(--secret-card-line);
|
|
15
|
+
perspective: 60rem;
|
|
16
|
+
}
|
|
17
|
+
.nxgt-secret-card [data-slot="secret-card-title"] {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 0.5rem;
|
|
21
|
+
color: var(--nxgt-tone-text, inherit);
|
|
22
|
+
}
|
|
23
|
+
.nxgt-secret-card [data-slot="secret-card-pattern"] {
|
|
24
|
+
min-height: 6rem;
|
|
25
|
+
border-radius: 0.5rem;
|
|
26
|
+
border: 1px dashed var(--secret-card-line);
|
|
27
|
+
background-image: repeating-linear-gradient(
|
|
28
|
+
45deg,
|
|
29
|
+
color-mix(in oklab, var(--secret-card-line) 25%, transparent) 0 0.5rem,
|
|
30
|
+
transparent 0.5rem 1rem
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
.nxgt-secret-card [data-slot="secret-card-back"] {
|
|
34
|
+
min-height: 6rem;
|
|
35
|
+
display: grid;
|
|
36
|
+
place-items: center;
|
|
37
|
+
text-align: center;
|
|
38
|
+
overflow-wrap: anywhere;
|
|
39
|
+
font-size: 1.25rem;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
user-select: text;
|
|
42
|
+
}
|
|
43
|
+
.nxgt-secret-card [data-slot="secret-card-actions"] {
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
gap: 0.5rem;
|
|
46
|
+
}
|
|
47
|
+
.nxgt-secret-card [data-slot="secret-card-actions"] > button {
|
|
48
|
+
min-height: 44px;
|
|
49
|
+
}
|
|
50
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
51
|
+
.nxgt-secret-card [data-slot="secret-card-face"][data-changed] {
|
|
52
|
+
animation: nxgt-secret-flip 0.35s ease-out;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
@keyframes nxgt-secret-flip {
|
|
56
|
+
from {
|
|
57
|
+
opacity: 0;
|
|
58
|
+
transform: rotateY(90deg);
|
|
59
|
+
}
|
|
60
|
+
to {
|
|
61
|
+
opacity: 1;
|
|
62
|
+
transform: rotateY(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* VoteTally: ranked rows, label and "n votes, x%" as text, a decorative bar
|
|
2
|
+
* beneath. The bar is an SVG whose rect width is an attribute (no `style`,
|
|
3
|
+
* strict CSP). Leader and tie take the primary fill and a word; the others a
|
|
4
|
+
* quieter one. Layout is one column at any width: the stat wraps under the
|
|
5
|
+
* label on a phone, the bar keeps its own line, so counts changing never move
|
|
6
|
+
* a row. Bars ease only without reduced motion. */
|
|
7
|
+
@layer components {
|
|
8
|
+
.nxgt-vote-tally {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 0.75rem;
|
|
12
|
+
min-inline-size: 0;
|
|
13
|
+
}
|
|
14
|
+
.nxgt-vote-tally [data-slot="vote-tally-list"] {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 0.875rem;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
list-style: none;
|
|
21
|
+
}
|
|
22
|
+
.nxgt-vote-tally [data-slot="vote-tally-row"] {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 0.375rem;
|
|
26
|
+
min-inline-size: 0;
|
|
27
|
+
}
|
|
28
|
+
.nxgt-vote-tally [data-slot="vote-tally-head"] {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-wrap: wrap;
|
|
31
|
+
align-items: baseline;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
column-gap: 0.75rem;
|
|
34
|
+
row-gap: 0.125rem;
|
|
35
|
+
}
|
|
36
|
+
.nxgt-vote-tally [data-slot="vote-tally-name"] {
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 0.375rem 0.5rem;
|
|
41
|
+
min-inline-size: 0;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
overflow-wrap: anywhere;
|
|
44
|
+
}
|
|
45
|
+
.nxgt-vote-tally [data-slot="vote-tally-marks"] {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: 0.25rem;
|
|
49
|
+
}
|
|
50
|
+
.nxgt-vote-tally [data-slot="vote-tally-stat"] {
|
|
51
|
+
color: var(--muted-foreground);
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
font-variant-numeric: tabular-nums;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
.nxgt-vote-tally [data-slot="vote-tally-bar"] {
|
|
57
|
+
display: block;
|
|
58
|
+
block-size: 0.625rem;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
border-radius: 9999px;
|
|
61
|
+
background: var(--muted);
|
|
62
|
+
}
|
|
63
|
+
.nxgt-vote-tally [data-slot="vote-tally-bar"] svg {
|
|
64
|
+
display: block;
|
|
65
|
+
inline-size: 100%;
|
|
66
|
+
block-size: 100%;
|
|
67
|
+
}
|
|
68
|
+
.nxgt-vote-tally:dir(rtl) [data-slot="vote-tally-bar"] svg {
|
|
69
|
+
transform: scaleX(-1);
|
|
70
|
+
}
|
|
71
|
+
.nxgt-vote-tally [data-slot="vote-tally-fill"] {
|
|
72
|
+
fill: color-mix(in oklab, var(--muted-foreground) 70%, var(--background));
|
|
73
|
+
}
|
|
74
|
+
.nxgt-vote-tally [data-leader] [data-slot="vote-tally-fill"],
|
|
75
|
+
.nxgt-vote-tally [data-selected] [data-slot="vote-tally-fill"] {
|
|
76
|
+
fill: var(--primary-indicator, var(--primary));
|
|
77
|
+
}
|
|
78
|
+
.nxgt-vote-tally [data-slot="vote-tally-voters"] {
|
|
79
|
+
margin: 0;
|
|
80
|
+
color: var(--muted-foreground);
|
|
81
|
+
font-size: 0.8125rem;
|
|
82
|
+
}
|
|
83
|
+
.nxgt-vote-tally [data-slot="vote-tally-empty"],
|
|
84
|
+
.nxgt-vote-tally [data-slot="vote-tally-abstain"] {
|
|
85
|
+
margin: 0;
|
|
86
|
+
color: var(--muted-foreground);
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
}
|
|
89
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
90
|
+
.nxgt-vote-tally[data-animated] [data-slot="vote-tally-fill"] {
|
|
91
|
+
transition: width 400ms cubic-bezier(0.2, 0, 0, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seconds left of `duration`, ticking on the client only (the server and the
|
|
3
|
-
* first render show the full time). Pausing keeps what is left; a new
|
|
4
|
-
* `duration` starts over. `onExpire` runs once, when it reaches zero.
|
|
5
|
-
*/
|
|
6
|
-
export declare function useCountdown(duration: number, paused: boolean, onExpire?: () => void): number;
|