@nxgt/material 3.54.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 +22 -0
- package/README.md +8 -3
- package/dist/{async-slice-DDSsekfm.js → async-slice-CORlM_wu.js} +2 -2
- package/dist/{button-Ct8DSLVv.js → button-CdpVEza0.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-BzoNsd-W.js → chip-C0yNYz9m.js} +2 -2
- package/dist/{code-editor-DsLQeiZc.js → code-editor-Bwzp2038.js} +6 -6
- package/dist/code-editor.js +5 -5
- package/dist/{code-language-items-4zXlIEZ0.js → code-language-items-nP-pF7yH.js} +2 -2
- package/dist/components/components.css +8 -0
- package/dist/components/ui/display/seat-ring/seat-ring-index.css +125 -0
- package/dist/components/ui/display/seat-ring/seat-ring-ring.css +87 -0
- package/dist/components/ui/display/seat-ring/seat-ring.css +139 -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-DFG2mgsM.js → components-Dl2hVfzt.js} +16273 -15482
- package/dist/components.js +26 -26
- package/dist/{dialog-DwPmwmA3.js → dialog-BVgsN9Ch.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-CDN6Nqx6.js → dropdown-menu-C8PBmMIa.js} +1 -1
- package/dist/editor.js +9 -9
- package/dist/{empty-state-Bb9W9did.js → empty-state-C5wfbbW9.js} +2 -2
- package/dist/{escape-owner-DI_lg2ID.js → escape-owner-CYnfySUl.js} +1 -1
- package/dist/{field-blur-BcJH8H9B.js → field-blur-Nid7-bt_.js} +1 -1
- package/dist/{field-blur-context-Dg1nwWOV.js → field-blur-context-DxdIuHAY.js} +1 -1
- package/dist/hooks.js +28 -28
- package/dist/{hover-card-Qse4gnop.js → hover-card-C_9jiwp1.js} +2 -2
- package/dist/{i18n-provider-DlVPS58N.js → i18n-provider-CLVZK6tQ.js} +1 -1
- package/dist/i18n.js +5 -5
- package/dist/{icon-badge-DVPBQOKe.js → icon-badge-Dcjg_SKJ.js} +3 -3
- package/dist/{icon-button-C3wDxsh6.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 +3 -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/seat-ring/index.d.ts +2 -0
- package/dist/lib/components/ui/display/seat-ring/partials/empty-seat.d.ts +5 -0
- package/dist/lib/components/ui/display/seat-ring/partials/seat-body.d.ts +9 -0
- package/dist/lib/components/ui/display/seat-ring/partials/seat-item.d.ts +17 -0
- package/dist/lib/components/ui/display/seat-ring/seat-ring.d.ts +9 -0
- package/dist/lib/components/ui/display/seat-ring/seat-ring.utils.d.ts +9 -0
- package/dist/lib/components/ui/display/seat-ring/types.d.ts +67 -0
- package/dist/lib/components/ui/display/seat-ring/use-seat-ring.d.ts +12 -0
- 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 +34 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +34 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +34 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +34 -0
- package/dist/lib/i18n/resources/resources.d.ts +68 -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-BoVE0ffh.js → public-BtKAthSs.js} +9 -9
- package/dist/{rich-code-block-DAUM7Yh_.js → rich-code-block-BZYZ6ewr.js} +5 -5
- package/dist/{sortable-provider-BLXsGE_0.js → sortable-provider-hUCwt69K.js} +2 -2
- package/dist/{spinner-Rg4PnBPH.js → spinner-BmUXTzvJ.js} +1 -1
- package/dist/{stored-language-B1lJMPbR.js → stored-language-_OGPBCn0.js} +64 -0
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-jVdxF_mc.js → theme-toggle-DdvEafno.js} +1 -1
- package/dist/{toolbar-items-BFY_hns5.js → toolbar-items-DKfY_6AG.js} +4 -4
- package/dist/{translate-material-q-JNxP1m.js → translate-material-DzBMPErh.js} +2 -2
- package/dist/{translator-BU1o3_l9.js → translator-BIVSiq4X.js} +1 -1
- package/dist/{use-chat-composer-DGbDuIUa.js → use-chat-composer-DCQW2EJ8.js} +16 -16
- package/dist/{use-heading-spy-CP63sAcS.js → use-heading-spy-Cr2C_Px8.js} +32 -5
- package/dist/{use-material-translation-nNF8fUXz.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 +212 -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-seatring.webp +0 -0
- package/docs/previews/data-display-secretcard.webp +0 -0
- package/docs/previews/data-display-votetally.webp +0 -0
- package/lib/components/components.css +8 -0
- package/lib/components/ui/display/seat-ring/seat-ring-index.css +125 -0
- package/lib/components/ui/display/seat-ring/seat-ring-ring.css +87 -0
- package/lib/components/ui/display/seat-ring/seat-ring.css +139 -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
|
@@ -8,6 +8,8 @@ from `@nxgt/material/components`.
|
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| <img src="../previews/data-display-avatar.webp" width="96" alt=""> | [Avatar](#avatar) | Image with a fallback and an optional online / offline dot |
|
|
10
10
|
| <img src="../previews/data-display-avatargroup.webp" width="96" alt=""> | [AvatarGroup](#avatargroup) | Overlapping avatars with a `+N` overflow |
|
|
11
|
+
| <img src="../previews/data-display-seatring.webp" width="96" alt=""> | [SeatRing](#seatring) | Participants as seats around a circle or in a grid, optionally pickable |
|
|
12
|
+
| <img src="../previews/data-display-secretcard.webp" width="96" alt=""> | [SecretCard](#secretcard) | A card whose secret is not rendered until it is revealed |
|
|
11
13
|
| <img src="../previews/data-display-shape.webp" width="96" alt=""> | [Shape](#shape) | Material 3 Expressive's shapes as a backdrop, an icon container or a mask for a photo |
|
|
12
14
|
| <img src="../previews/data-display-badge.webp" width="96" alt=""> | [Badge](#badge) | Status pill, 20 px, in seven tones, soft or outlined, with an optional decorative `icon` before the words |
|
|
13
15
|
| <img src="../previews/data-display-countbadge.webp" width="96" alt=""> | [CountBadge](#countbadge) | Count in the corner of its children |
|
|
@@ -27,6 +29,7 @@ from `@nxgt/material/components`.
|
|
|
27
29
|
| <img src="../previews/data-display-timeline.webp" width="96" alt=""> | [Timeline](#timeline) | Vertical event timeline: shaped markers, cards, alternating sides, day groups (`DaySeparator` headings, words from `material.day-separator.*`), relative times; `layout="auto"` needs a parent with a definite width (`rail` in a `w-fit` parent) |
|
|
28
30
|
| <img src="../previews/data-display-changehistory.webp" width="96" alt=""> | [ChangeHistory](#changehistory) | History of the changes made to anything, as a rail or as cards |
|
|
29
31
|
| <img src="../previews/data-display-zonedtime.webp" width="96" alt=""> | [ZonedTime](#zonedtime) | A moment written in a given time zone, identical on the server and the client |
|
|
32
|
+
| <img src="../previews/data-display-votetally.webp" width="96" alt=""> | [VoteTally](#votetally) | A live ranked tally of a vote, with counts, shares and voters |
|
|
30
33
|
| <img src="../previews/data-display-eventchip.webp" width="96" alt=""> | [EventChip](#eventchip) | Calendar event pill, soft, solid or outlined |
|
|
31
34
|
| <img src="../previews/data-display-codeblock.webp" width="96" alt=""> | [CodeBlock](#codeblock) | Code, JSON tree and tabbed samples |
|
|
32
35
|
| <img src="../previews/data-display-markdown.webp" width="96" alt=""> | [Markdown](#markdown) | GFM with math and mermaid |
|
|
@@ -127,6 +130,144 @@ array (`people.map(...)`); a single wrapper component or fragment counts as one.
|
|
|
127
130
|
|
|
128
131
|
---
|
|
129
132
|
|
|
133
|
+
## SeatRing
|
|
134
|
+
|
|
135
|
+

|
|
136
|
+
|
|
137
|
+
Participants as seats: around a circle when there is room, in a grid on a
|
|
138
|
+
phone. A seating plan, a round-table meeting, a party-game lobby. Each seat has
|
|
139
|
+
an avatar, a name, a status written in words and a badge; with `selectionMode`
|
|
140
|
+
it is a listbox to pick one or several seats from.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { SeatRing } from '@nxgt/material/components';
|
|
144
|
+
|
|
145
|
+
<SeatRing
|
|
146
|
+
aria-label="Players"
|
|
147
|
+
seats={[
|
|
148
|
+
{ id: 'ada', label: 'Ada Example', status: { label: 'Ready', tone: 'success' }, badge: 'Host' },
|
|
149
|
+
{ id: 'bo', label: 'Bo Sample', status: { label: 'Away', tone: 'warning' } },
|
|
150
|
+
{ id: 'free', label: 'Free', empty: true },
|
|
151
|
+
]}
|
|
152
|
+
highlightedId="ada"
|
|
153
|
+
centerSlot={<p>Round 2</p>}
|
|
154
|
+
selectionMode="single"
|
|
155
|
+
value={target}
|
|
156
|
+
onValueChange={setTarget}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Default | Notes |
|
|
161
|
+
| --- | --- | --- | --- |
|
|
162
|
+
| `seats` | `Seat[]` | required | `{ id, label, avatar?: { src?, fallback? }, status?: { label, tone? }, badge?, disabled?, empty? }`; clockwise from the top |
|
|
163
|
+
| `layout` | `'ring' \| 'grid'` | `'ring'` | `ring` falls back to the grid in a narrow box and past the seat limit (10 seats) |
|
|
164
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | seat width 4.75, 6 or 8rem; ring from a box of 32, 38 or 46rem; at most 10 seats on the circle |
|
|
165
|
+
| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | `none`: a list; otherwise a listbox |
|
|
166
|
+
| `value`, `defaultValue`, `onValueChange` | `string \| null` (single), `string[]` (multiple) | — | controlled or not |
|
|
167
|
+
| `highlightedId` | `string` | — | emphasised seat (whose turn it is); `aria-current="true"` on it, in a list and in a listbox |
|
|
168
|
+
| `renderAvatar`, `renderStatus`, `renderBadge` | `(seat) => ReactNode` | — | replace the default avatar, status line or badge |
|
|
169
|
+
| `renderEmptySeat` | `(seat) => ReactNode` | dashed circle and "Empty seat" | content of an empty seat |
|
|
170
|
+
| `centerSlot` | `ReactNode` | — | middle of the ring (a timer, a title); above the seats in the grid |
|
|
171
|
+
| `aria-label` / `aria-labelledby` | `string` | — | names the list or the listbox: always give one |
|
|
172
|
+
|
|
173
|
+
The layout follows the component's own width (a container query), not the
|
|
174
|
+
window, so it also collapses in a narrow column or a dialog. The grid is two columns
|
|
175
|
+
at 30rem or less and auto-fill above it, with a 44px minimum seat height. The circle is CSS only, so server markup has
|
|
176
|
+
no `style` attribute and a strict CSP is fine.
|
|
177
|
+
|
|
178
|
+
**Accessibility**
|
|
179
|
+
|
|
180
|
+
- Not selectable: a `list` of `listitem`s. Selectable: a `listbox` of `option`s
|
|
181
|
+
(`aria-multiselectable` in `multiple`).
|
|
182
|
+
- A roving tab stop sits on the chosen seat, else the first taken one. Arrow
|
|
183
|
+
keys, Home and End move the focus (an empty seat is skipped); Space or Enter
|
|
184
|
+
picks, and pressing the chosen seat again clears it.
|
|
185
|
+
- A seat is named by its label and its status text, never by colour alone.
|
|
186
|
+
- In a listbox, empty and `disabled` seats are `aria-disabled`, and a disabled
|
|
187
|
+
seat can be reached but not picked (in a list, `disabled` only dims the seat).
|
|
188
|
+
- Put no button in a selectable seat: interactive content (an "Invite" action
|
|
189
|
+
in `renderEmptySeat`) belongs to a non-selectable ring.
|
|
190
|
+
- Seat changes are not animated beyond a colour fade, which stops under
|
|
191
|
+
`prefers-reduced-motion`.
|
|
192
|
+
|
|
193
|
+
**Storybook:** `Data display/SeatRing` · **Vue:** not yet (#1931)
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## SecretCard
|
|
198
|
+
|
|
199
|
+

|
|
200
|
+
|
|
201
|
+
A card that holds a secret and shows it only after a deliberate tap or click: a
|
|
202
|
+
private role in a party game, a one-time code, a recovery key, a card to turn
|
|
203
|
+
over. It is hidden by default, can be hidden again, and can hide itself after a
|
|
204
|
+
delay. The secret (`back`) is **not rendered** while hidden, so it is not in the
|
|
205
|
+
markup, not read by a screen reader and not copied; it is not merely covered by CSS.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { Button, SecretCard } from '@nxgt/material/components';
|
|
209
|
+
|
|
210
|
+
<SecretCard
|
|
211
|
+
title="Your role"
|
|
212
|
+
description="Look alone, then hide it."
|
|
213
|
+
back={<p>Night watch</p>}
|
|
214
|
+
autoHideMs={8000}
|
|
215
|
+
actions={<Button variant="outlined">Next player</Button>}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Notes |
|
|
220
|
+
| --- | --- | --- | --- |
|
|
221
|
+
| `title` | `ReactNode` | required | the name, always shown (a heading, `h3`) |
|
|
222
|
+
| `back` | `ReactNode` | required | the secret, rendered only while revealed |
|
|
223
|
+
| `description` | `ReactNode` | - | a hint, shown only while hidden |
|
|
224
|
+
| `front` | `ReactNode` | neutral pattern | the hidden face; put no secret in it |
|
|
225
|
+
| `icon` | `ReactNode` | - | decorative, before the title |
|
|
226
|
+
| `tone` | `Tone` | - | `data-tone`: a named tone or an app's |
|
|
227
|
+
| `actions` | `ReactNode` | - | buttons beside the reveal button |
|
|
228
|
+
| `revealed` + `onRevealedChange` | `boolean` + `(revealed) => void` | - | controlled |
|
|
229
|
+
| `defaultRevealed`, `onRevealedChange` | `boolean`, `(revealed) => void` | `false` | uncontrolled; never together with `revealed` |
|
|
230
|
+
| `autoHideMs` | `number` | - | hides again after this long shown; also calls `onRevealedChange(false)` |
|
|
231
|
+
| `disabled` | `boolean` | `false` | the card cannot be revealed; a revealed card can always be hidden |
|
|
232
|
+
| `labels` | `Partial<{ reveal, revealed, hidden }>` | `material.secret-card.*` | the button's name ("Show secret") and the announcements |
|
|
233
|
+
|
|
234
|
+
Controlled, with your own words:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const [open, setOpen] = useState(false);
|
|
238
|
+
|
|
239
|
+
<SecretCard
|
|
240
|
+
title="Recovery key"
|
|
241
|
+
back={<code>K7QF-92LM</code>}
|
|
242
|
+
revealed={open}
|
|
243
|
+
onRevealedChange={setOpen}
|
|
244
|
+
labels={{ reveal: 'Show the key', revealed: 'Key shown', hidden: 'Key hidden' }}
|
|
245
|
+
/>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Accessibility**
|
|
249
|
+
|
|
250
|
+
- The reveal is a real `button`, a disclosure: one name ("Show secret"; the
|
|
251
|
+
icon changes) and `aria-expanded` for the state, with `aria-controls` on the
|
|
252
|
+
face. Only a click, a tap, Enter or Space reveals: no hover, so a scroll
|
|
253
|
+
never reveals it.
|
|
254
|
+
- A click is carried by `aria-expanded`, so nothing is announced for it. A
|
|
255
|
+
change a click does not explain (`autoHideMs`, the parent) is said through a
|
|
256
|
+
polite live region ("Secret revealed", "Secret hidden"). The message never
|
|
257
|
+
holds the secret, and nothing is said on the first render.
|
|
258
|
+
- When the face that held focus is swapped (a focusable `back` on auto-hide, a
|
|
259
|
+
focusable `front` on reveal), focus moves to the reveal button.
|
|
260
|
+
- `disabled` stops a reveal only: a revealed card can always be hidden.
|
|
261
|
+
- The flip runs only after a change (not on first render or hydration) and
|
|
262
|
+
without `prefers-reduced-motion`; with it the face changes at once.
|
|
263
|
+
- The card is full width up to 28rem, the button is at least 44px high, and
|
|
264
|
+
the layout follows `dir` (RTL) and dark mode.
|
|
265
|
+
- Keep `front` and `description` free of the secret: they are always in the page.
|
|
266
|
+
|
|
267
|
+
**Storybook:** `Data display/SecretCard` · **Vue:** not yet (#1938)
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
130
271
|
## Shape
|
|
131
272
|
|
|
132
273
|

|
|
@@ -352,7 +493,7 @@ import { Link } from 'react-router';
|
|
|
352
493
|
| `items` | `PeriodCardItem[]` | — | `id`, `title`, `start`, `end` (`Date`, ISO string or timestamp, read as local days; `'2026-09-01'` is that day, not UTC), and optionally `state`, `description`, `badges`, `footer`, `href` |
|
|
353
494
|
| `today` | `DayInput \| false` | now | the day that tells the current period, by calendar day, both ends included; `false` turns the detection off |
|
|
354
495
|
| `state` (per item) | `'past' \| 'current' \| 'upcoming'` | from `today` | an explicit state wins over the dates |
|
|
355
|
-
| `linkComponent` | `LinkComponent` | `<a>` | the title's link, as the app shell's |
|
|
496
|
+
| `linkComponent` | `LinkComponent` | `<a>` | the title's link, as the app shell's. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
356
497
|
| `emptyState` | `ReactNode` | — | shown instead of the list when `items` is empty |
|
|
357
498
|
| `headingLevel` | `2 \| 3 \| 4` | `3` | the level of each card's heading |
|
|
358
499
|
| `dateFormat` | `Intl.DateTimeFormatOptions` | day, short month, year | how the range reads, through `formatRange` |
|
|
@@ -817,7 +958,7 @@ A tile with an `href` ends with a discreet chevron after `trailing`: decorative
|
|
|
817
958
|
| --- | --- | --- | --- |
|
|
818
959
|
| `href` | `string` | — | renders an `<a>`, with a chevron by default |
|
|
819
960
|
| `chevron` | `boolean` | `true` with an `href`, else `false` | the decorative chevron after `trailing` |
|
|
820
|
-
| `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref` |
|
|
961
|
+
| `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref`. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
821
962
|
| `onClick` | `MouseEventHandler` | — | renders a `<button>` |
|
|
822
963
|
| `selected` | `boolean` | `false` | the current row: drawn selected; a link gets `aria-current="page"` (your own `aria-current` replaces it), a button `aria-pressed` |
|
|
823
964
|
| `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chips`, `chevron` and `linkComponent` are refused beside it by type |
|
|
@@ -1166,6 +1307,75 @@ the same way; pass the app's zone from the server so every screen agrees.
|
|
|
1166
1307
|
|
|
1167
1308
|
---
|
|
1168
1309
|
|
|
1310
|
+
## VoteTally
|
|
1311
|
+
|
|
1312
|
+

|
|
1313
|
+
|
|
1314
|
+
A live ranked tally of a vote: a poll, a decision in a meeting, an election, a
|
|
1315
|
+
party-game ballot. One row per option with its count and share written as text
|
|
1316
|
+
("Ada Example, 3 votes, 60%"), a bar beneath, and who voted for it. The leader,
|
|
1317
|
+
a tie and the viewer's own vote are marked in words. Counts can change while
|
|
1318
|
+
the page is open.
|
|
1319
|
+
|
|
1320
|
+
```tsx
|
|
1321
|
+
import { VoteTally } from '@nxgt/material/components';
|
|
1322
|
+
|
|
1323
|
+
<VoteTally
|
|
1324
|
+
aria-label="Best day"
|
|
1325
|
+
options={[
|
|
1326
|
+
{ id: 'mon', label: 'Monday', count: 5, voters: [{ id: 'ada', label: 'Ada Example' }] },
|
|
1327
|
+
{ id: 'tue', label: 'Tuesday', count: 2 },
|
|
1328
|
+
]}
|
|
1329
|
+
selectedId="tue"
|
|
1330
|
+
abstainCount={1}
|
|
1331
|
+
/>
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
| Prop | Type | Default | Notes |
|
|
1335
|
+
| --- | --- | --- | --- |
|
|
1336
|
+
| `options` | `VoteOption[]` | required | `{ id, label, count, voters?: { id, label, avatar? }[], avatar? }` |
|
|
1337
|
+
| `total` | `number` | sum of counts | base of the shares; abstentions are never in it. `0` or less falls back to the sum. A `total` below the sum caps each share at 100 |
|
|
1338
|
+
| `sort` | `'count' \| 'none'` | `'count'` | highest first; equal counts keep their order and share a rank |
|
|
1339
|
+
| `selectedId` | `string` | — | the viewer's vote: "Your vote" badge and the primary fill |
|
|
1340
|
+
| `showLeader` | `boolean` | `true` | "Leader" on a single top option, "Tie" on each of several; no arbitrary leader. Off: no mark, no emphasis, and the status says the votes cast instead of who leads |
|
|
1341
|
+
| `showVoters` | `'none' \| 'avatars' \| 'names'` | `'avatars'` | avatars, or a line of names |
|
|
1342
|
+
| `maxVoters` | `number` | `5` | voters shown before "+N"; names mode ends with "+N more" |
|
|
1343
|
+
| `abstainCount` | `number` | — | shown under the bars ("2 abstentions") and added to the announcement |
|
|
1344
|
+
| `animated` | `boolean` | `true` | the bar's width eases when a count changes (only the width), never under `prefers-reduced-motion` |
|
|
1345
|
+
| `emptyLabel` | `ReactNode` | "No votes yet" | shown while no option has a vote, rows (if any) stay beneath it |
|
|
1346
|
+
| `announceDelay` | `number` | `1000` | ms a change waits before it is announced |
|
|
1347
|
+
| `renderOption`, `renderVoters` | `(row) => ReactNode` | — | replace a row's label cell or voters; `row` has `option`, `rank`, `percent`, `leader`, `tied`, `selected` |
|
|
1348
|
+
| `header`, `footer` | `ReactNode` | — | above and below the rows |
|
|
1349
|
+
| `aria-label` / `aria-labelledby` | `string` | — | names the list: always give one |
|
|
1350
|
+
| `className`, other `div` props | — | — | go to the root `div` |
|
|
1351
|
+
|
|
1352
|
+
**Shares.** Percentages are whole numbers. When `total` is the sum of the
|
|
1353
|
+
counts they add up to 100 (largest remainder: 1, 1 and 1 vote read 34%, 33%,
|
|
1354
|
+
33%). With a larger `total` (voters still to come) each share is rounded on its
|
|
1355
|
+
own, so they add up to less.
|
|
1356
|
+
|
|
1357
|
+
**Accessibility**
|
|
1358
|
+
|
|
1359
|
+
- A `list` of `listitem`s. Each row's text is real: the label, then
|
|
1360
|
+
"3 votes" and "60%". The bars are `aria-hidden`.
|
|
1361
|
+
- Leader, tie and "Your vote" are words in badges, never colour alone.
|
|
1362
|
+
- A visually hidden polite status says where the vote stands ("Monday leads
|
|
1363
|
+
with 5 votes", "Tie between Ada and Bo, 3 votes each"), and any
|
|
1364
|
+
abstentions, once changes stop for
|
|
1365
|
+
`announceDelay`. It never says the page as loaded, and a burst of updates is
|
|
1366
|
+
one announcement.
|
|
1367
|
+
- Voters are a named group (`Voters for Monday`) of avatars each named by the
|
|
1368
|
+
voter, or a line of names.
|
|
1369
|
+
- One column at any width: on a phone the count wraps under the label, long
|
|
1370
|
+
labels wrap, and a bar keeps its own line so changing counts do not move rows.
|
|
1371
|
+
It mirrors in right-to-left, and the bars follow the theme in dark mode.
|
|
1372
|
+
- Bar widths are SVG attributes, so server markup has no `style` attribute and
|
|
1373
|
+
a strict CSP is fine.
|
|
1374
|
+
|
|
1375
|
+
**Storybook:** `Data display/VoteTally` · **Vue:** not yet (#1940)
|
|
1376
|
+
|
|
1377
|
+
---
|
|
1378
|
+
|
|
1169
1379
|
## EventChip
|
|
1170
1380
|
|
|
1171
1381
|

|
|
@@ -125,7 +125,7 @@ table enables, for typing your own column defs.
|
|
|
125
125
|
| `footer` | `false \| (table) => ReactNode` | the pagination | replaces the footer, or removes it |
|
|
126
126
|
| `getRowHref` | `(row) => string \| undefined` | — | the first data cell links to the row's page; a click elsewhere on the row follows it |
|
|
127
127
|
| `onRowClick` | `(row, event) => void` | — | a click on the row (not on a control); alone (without `getRowHref`), it makes the row focusable and Enter calls it |
|
|
128
|
-
| `linkComponent` | `ComponentType<{ href, className?, children?, rel?, 'aria-label'?, onClick? }>` | `<a>` | the link for `getRowHref` and the page links: wrap your router's. It must forward `className` and `ref` (the past-end page's "Back to the first page" is a `Button asChild` over it) |
|
|
128
|
+
| `linkComponent` | `ComponentType<{ href, className?, children?, rel?, 'aria-label'?, onClick? }>` | `<a>` | the link for `getRowHref` and the page links: wrap your router's. It must forward `className` and `ref` (the past-end page's "Back to the first page" is a `Button asChild` over it). Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
129
129
|
| `reserveActions` | `boolean` | `true` | with `actions` and a `select` column: keep the toolbar's 68px band while nothing is selected, so the bar does not push the rows down; `false` keeps none and the bar arrives on the first selection (`ServerTable` passes `false`) |
|
|
130
130
|
| `focusable` | `boolean` | `true` with `actions`, else `false` | the `<table>` (or the list of cards or tiles) takes the focus by script (`tabindex="-1"`, never a tab stop): where the focus goes once the control that had it is gone. When the selection is cleared and the focused control went with it (a bulk action's button), the table takes the focus instead of the page's body ([focus after a bulk action](#focus-after-a-bulk-action)); `false` turns both off |
|
|
131
131
|
| `tableRef` | `Ref<HTMLTableElement>` | — | the `<table>`, to focus it yourself (with `focusable`); `controls.focusTable()` does the same from a toolbar slot. It stays `null` while the layout is a list (`cards`, `tiles`, or the phone side of `{ phone, wide }`): use `focusRef` |
|
|
@@ -1258,7 +1258,7 @@ import { ListEmpty } from '@nxgt/material/components';
|
|
|
1258
1258
|
| `clear` | `string` | "Clear search and filters" | the way out's label (`material.list-empty.clear`) |
|
|
1259
1259
|
| `onClear` | `() => void` | — | the way out is a button |
|
|
1260
1260
|
| `clearHref` | `string` | — | the way out is a link (without `onClear`) |
|
|
1261
|
-
| `linkComponent` | `DataTableLinkComponent` | `<a>` | the link for `clearHref` (a router's); it must forward `className` and `ref`, since the button is `asChild` over it |
|
|
1261
|
+
| `linkComponent` | `DataTableLinkComponent` | `<a>` | the link for `clearHref` (a router's); it must forward `className` and `ref`, since the button is `asChild` over it. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
1262
1262
|
| `headingLevel` | `2`–`6` | `2` | the title is a heading of this level, so the page keeps a landmark when the list empties; the look does not change |
|
|
1263
1263
|
| `busy` | `boolean` | `false` | the next answer is loading: the page stays, marked `data-busy` (no `aria-busy`: the element has no role) |
|
|
1264
1264
|
|
|
@@ -1314,7 +1314,7 @@ A list that fits one page shows "1–5 of 5" by default. `hideOnSinglePage` rend
|
|
|
1314
1314
|
| `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 |
|
|
1315
1315
|
| `live` | `boolean` | `true` | `false` renders the range as plain text instead of a polite live region |
|
|
1316
1316
|
| `disabled` | `boolean` | `false` | makes Previous and Next inert while the feed loads |
|
|
1317
|
-
| `linkComponent` | `DataTableLinkComponent` | `<a>` | your router's link; it must forward `className` and `ref` |
|
|
1317
|
+
| `linkComponent` | `DataTableLinkComponent` | `<a>` | your router's link; it must forward `className` and `ref`. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
|
|
1318
1318
|
|
|
1319
1319
|
**Storybook:** `Data table, filter & lists/PageFooter`
|
|
1320
1320
|
|
|
@@ -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
|
|
Binary file
|
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
@import "./ui/display/period-cards/period-cards.css";
|
|
135
135
|
@import "./ui/layout/sidebar/sidebar-tokens.css";
|
|
136
136
|
@import "./ui/display/avatar/avatar.css";
|
|
137
|
+
/* SeatRing: seats as a grid or on a circle by its own width. */
|
|
138
|
+
@import "./ui/display/seat-ring/seat-ring.css";
|
|
139
|
+
@import "./ui/display/seat-ring/seat-ring-index.css";
|
|
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";
|
|
137
145
|
/* Alert and Banner span the row in the form grids (#1497). */
|
|
138
146
|
@import "./ui/feedback/alert/alert.css";
|
|
139
147
|
@import "./ui/display/stat-card/stat-card.css";
|