@nxgt/material-vue 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{Button-D1uzdbOJ.js → Button-CNbJT5qV.js} +5 -5
  3. package/dist/{HoverCardTrigger-DaM5d4hE.js → HoverCardTrigger-BTAHE6wK.js} +11 -2
  4. package/dist/{camera-DZ5nYW-v.js → camera-l87FB3uW.js} +3 -3
  5. package/dist/components/theme-tokens.css +2 -2
  6. package/dist/components/ui/primitives/label/label.css +14 -21
  7. package/dist/{components-Cteo76NG.js → components-DSl6EpOS.js} +6827 -6809
  8. package/dist/components.js +5 -5
  9. package/dist/dnd.js +1 -1
  10. package/dist/{form-scope-BvdPqk6T.js → form-scope-BTUGjQdY.js} +1 -1
  11. package/dist/{icon-button-D60G5B_Z.js → icon-button-DXXjxBgr.js} +6 -6
  12. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +14 -9
  13. package/dist/lib/components/ui/dates/calendar/calendar-weekday.d.ts +4 -0
  14. package/dist/lib/components/ui/feedback/toast/toast-classes.d.ts +13 -0
  15. package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +3 -2
  16. package/dist/main.js +5 -5
  17. package/dist/map.js +3 -3
  18. package/dist/material-vue.css +1 -1
  19. package/dist/{pdf-outline-sidebar-Bln9F9gF.js → pdf-outline-sidebar-DQ6BCQhR.js} +1 -1
  20. package/dist/{pdf-page-BVtXjnHS.js → pdf-page-cuvGDdM2.js} +1 -1
  21. package/dist/{pdf-page-list-D3eng5h4.js → pdf-page-list-B94WdjV4.js} +2 -2
  22. package/dist/{pdf-reader-CPms5uyj.js → pdf-reader-CEkr3KzC.js} +7 -7
  23. package/dist/{pdf-sidebar-lzBEntPH.js → pdf-sidebar-DJf--6rP.js} +2 -2
  24. package/dist/{pdf-thumbnail-sidebar-DB3PmiGW.js → pdf-thumbnail-sidebar-JLWNyl-p.js} +2 -2
  25. package/dist/{pdf-toolbar-mWlGTWU1.js → pdf-toolbar-B_MDicKL.js} +3 -3
  26. package/dist/pdf.js +1 -1
  27. package/dist/{sortable-provider-Dml11nQY.js → sortable-provider-Cb3tm9Cy.js} +1 -1
  28. package/dist/{stx-player-BpQHnHaa.js → stx-player-DWy055hI.js} +1 -1
  29. package/docs/components/data-table-filter-and-lists.md +15 -6
  30. package/docs/components/date-and-time.md +6 -1
  31. package/docs/components/feedback-and-status.md +17 -2
  32. package/docs/components/primitives.md +7 -6
  33. package/docs/guides/app-shell.md +2 -1
  34. package/docs/previews/date-time-calendar.webp +0 -0
  35. package/docs/previews/date-time-datefield.webp +0 -0
  36. package/docs/previews/date-time-daterangefield.webp +0 -0
  37. package/docs/previews/feedback-status-toast.webp +0 -0
  38. package/docs/roadmap.md +76 -38
  39. package/docs/troubleshooting.md +99 -8
  40. package/lib/components/theme-tokens.css +2 -2
  41. package/lib/components/ui/primitives/label/label.css +14 -21
  42. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { Ef as n } from "./components-Cteo76NG.js";
3
+ import { Ef as n } from "./components-DSl6EpOS.js";
4
4
  import { t as r } from "./use-material-translation-Do6ztdtV.js";
5
5
  import { Fragment as i, computed as a, createBlock as o, createCommentVNode as s, createElementBlock as c, createElementVNode as l, defineComponent as u, normalizeClass as d, normalizeStyle as f, openBlock as p, ref as m, renderList as h, toDisplayString as g, unref as _, withCtx as v } from "vue";
6
6
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-outline-row.vue?vue&type=script&setup=true&lang.ts
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { kp as t } from "./components-Cteo76NG.js";
2
+ import { kp as t } from "./components-DSl6EpOS.js";
3
3
  import { computed as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createElementVNode as o, defineComponent as s, nextTick as c, normalizeClass as l, normalizeStyle as u, onBeforeUnmount as d, openBlock as f, ref as p, shallowRef as m, unref as h, watch as g } from "vue";
4
4
  import { DEFAULT_PAGE_SIZE as _, escapeHtml as v, findTextMatches as y, highlightItemHtml as b, placeholderSize as x, rotatedSize as S } from "@nxgt/material-core/pdf-viewer";
5
5
  //#region lib/components/ui/media/pdf-viewer/pdf-note-icons.ts
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
- import { Ef as t } from "./components-Cteo76NG.js";
3
- import { t as n } from "./pdf-page-BVtXjnHS.js";
2
+ import { Ef as t } from "./components-DSl6EpOS.js";
3
+ import { t as n } from "./pdf-page-cuvGDdM2.js";
4
4
  import { Fragment as r, createBlock as i, createElementBlock as a, createElementVNode as o, defineComponent as s, openBlock as c, renderList as l, withCtx as u } from "vue";
5
5
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-page-list.vue?vue&type=script&setup=true&lang.ts
6
6
  var d = { class: "flex flex-col items-center gap-4 px-4 py-4" }, f = /*@__PURE__*/ s({
@@ -1,12 +1,12 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { On as t } from "./components-Cteo76NG.js";
2
+ import { On as t } from "./components-DSl6EpOS.js";
3
3
  import { t as n } from "./use-material-translation-Do6ztdtV.js";
4
- import { t as r } from "./Button-D1uzdbOJ.js";
5
- import { o as i } from "./HoverCardTrigger-DaM5d4hE.js";
6
- import { n as a } from "./pdf-page-BVtXjnHS.js";
7
- import { t as o } from "./pdf-page-list-D3eng5h4.js";
8
- import { t as s } from "./pdf-sidebar-lzBEntPH.js";
9
- import { t as c } from "./pdf-toolbar-mWlGTWU1.js";
4
+ import { t as r } from "./Button-CNbJT5qV.js";
5
+ import { o as i } from "./HoverCardTrigger-BTAHE6wK.js";
6
+ import { n as a } from "./pdf-page-cuvGDdM2.js";
7
+ import { t as o } from "./pdf-page-list-B94WdjV4.js";
8
+ import { t as s } from "./pdf-sidebar-DJf--6rP.js";
9
+ import { t as c } from "./pdf-toolbar-B_MDicKL.js";
10
10
  import { computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createTextVNode as m, createVNode as h, defineComponent as g, nextTick as _, normalizeClass as v, onBeforeUnmount as y, onMounted as b, openBlock as x, reactive as S, ref as C, shallowRef as w, toDisplayString as T, toRef as E, unref as D, useId as O, watch as k, withCtx as A } from "vue";
11
11
  import { AlertCircle as j, FileX2 as M, KeyRound as N, Loader2 as P } from "@lucide/vue";
12
12
  import { pdfSourceKey as F, toPdfjsParams as I } from "@nxgt/material-core/pdf-source";
@@ -1,8 +1,8 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
3
  import { t as n } from "./use-material-translation-Do6ztdtV.js";
4
- import { t as r } from "./pdf-outline-sidebar-Bln9F9gF.js";
5
- import { t as i } from "./pdf-thumbnail-sidebar-DB3PmiGW.js";
4
+ import { t as r } from "./pdf-outline-sidebar-DQ6BCQhR.js";
5
+ import { t as i } from "./pdf-thumbnail-sidebar-JLWNyl-p.js";
6
6
  import { createElementBlock as a, createElementVNode as o, createVNode as s, defineComponent as c, normalizeClass as l, openBlock as u, toDisplayString as d, unref as f, useId as p, useTemplateRef as m } from "vue";
7
7
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-sidebar.vue?vue&type=script&setup=true&lang.ts
8
8
  var h = ["aria-label"], g = ["aria-label"], _ = [
@@ -1,8 +1,8 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { Ef as n } from "./components-Cteo76NG.js";
3
+ import { Ef as n } from "./components-DSl6EpOS.js";
4
4
  import { t as r } from "./use-material-translation-Do6ztdtV.js";
5
- import { t as i } from "./pdf-page-BVtXjnHS.js";
5
+ import { t as i } from "./pdf-page-cuvGDdM2.js";
6
6
  import { Fragment as a, createCommentVNode as o, createElementBlock as s, createElementVNode as c, createVNode as l, defineComponent as u, normalizeClass as d, openBlock as f, renderList as p, toDisplayString as m, unref as h, withCtx as g } from "vue";
7
7
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-thumbnail-sidebar.vue?vue&type=script&setup=true&lang.ts
8
8
  var _ = ["aria-label"], v = {
@@ -1,10 +1,10 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
3
  import { t as n } from "./use-material-translation-Do6ztdtV.js";
4
- import { d as r } from "./Button-D1uzdbOJ.js";
5
- import { t as i } from "./icon-button-D60G5B_Z.js";
4
+ import { d as r } from "./Button-CNbJT5qV.js";
5
+ import { t as i } from "./icon-button-DXXjxBgr.js";
6
6
  import { a, i as o, n as s, o as c, r as l, t as u } from "./DropdownMenuTrigger-BsuyNVzV.js";
7
- import { o as d } from "./HoverCardTrigger-DaM5d4hE.js";
7
+ import { o as d } from "./HoverCardTrigger-BTAHE6wK.js";
8
8
  import { Fragment as f, computed as p, createBlock as m, createCommentVNode as h, createElementBlock as g, createElementVNode as _, createTextVNode as v, createVNode as y, defineComponent as b, nextTick as x, normalizeClass as S, openBlock as C, ref as w, renderList as T, toDisplayString as E, unref as D, useTemplateRef as O, watch as k, withCtx as A } from "vue";
9
9
  import { ChevronDown as j, ChevronLeft as M, ChevronRight as N, ChevronUp as P, Download as F, Maximize2 as I, Minimize2 as L, Moon as R, Printer as z, RotateCw as B, Search as V, Sidebar as H, ZoomIn as U, ZoomOut as W } from "@lucide/vue";
10
10
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-toolbar-search.vue?vue&type=script&setup=true&lang.ts
package/dist/pdf.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as e, t } from "./use-material-translation-Do6ztdtV.js";
2
- import { t as n } from "./Button-D1uzdbOJ.js";
2
+ import { t as n } from "./Button-CNbJT5qV.js";
3
3
  import { computed as r, createBlock as i, createTextVNode as a, defineComponent as o, getCurrentInstance as s, nextTick as c, onMounted as l, onScopeDispose as u, openBlock as d, ref as f, renderSlot as p, shallowRef as m, toDisplayString as ee, toValue as h, unref as g, watch as te, withCtx as ne } from "vue";
4
4
  import { contentDisposition as re, pdfBytesResponse as ie, toWebStream as ae } from "@nxgt/material-core/pdf/http";
5
5
  import { badge as oe, dataTable as se, defaultPdfTheme as ce, definePdfDocument as _, divider as v, header as y, heading as b, image as x, keyValue as S, keyValueOf as C, link as w, metric as T, page as E, pageBreak as D, pageNumber as O, pdfDocument as k, resolvePdfColor as A, resolvePdfTheme as j, row as M, section as N, spacer as P, stack as F, table as I, text as L, totals as R } from "@nxgt/material-core/pdf/model";
@@ -1,6 +1,6 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
2
  import { t } from "./use-material-translation-Do6ztdtV.js";
3
- import { t as n } from "./icon-button-D60G5B_Z.js";
3
+ import { t as n } from "./icon-button-DXXjxBgr.js";
4
4
  import { Teleport as r, computed as i, createBlock as a, createCommentVNode as o, createElementBlock as s, createVNode as c, defineComponent as l, inject as u, mergeProps as d, normalizeStyle as f, onUnmounted as p, openBlock as m, provide as h, reactive as g, ref as _, renderSlot as v, toValue as y, unref as b, useAttrs as x, watch as S, watchEffect as C, withCtx as w, withModifiers as T } from "vue";
5
5
  import { GripVertical as E } from "@lucide/vue";
6
6
  import { Primitive as D } from "reka-ui";
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { $t as t, Jt as n, Mp as r, Qt as i, Yt as a, Zt as o, en as s, qt as c } from "./components-Cteo76NG.js";
2
+ import { $t as t, Jt as n, Mp as r, Qt as i, Yt as a, Zt as o, en as s, qt as c } from "./components-DSl6EpOS.js";
3
3
  import { t as l } from "./use-material-translation-Do6ztdtV.js";
4
4
  import { createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createTextVNode as m, createVNode as h, defineComponent as g, normalizeClass as _, openBlock as v, toDisplayString as y, unref as b, withCtx as x } from "vue";
5
5
  //#region lib/components/ui/media/stx-player/partials/stx-player-frame.vue?vue&type=script&setup=true&lang.ts
@@ -175,23 +175,32 @@ keys, and an edge fades on each side where columns are hidden
175
175
  server; once focused, it stays a tab stop until focus leaves, even if it
176
176
  stops overflowing. The empty state is centred in the part of the table you
177
177
  can see (the box's measured width, `--table-visible-width`), not across its
178
- whole scroll width. The table needs no width from its parent: inside a
178
+ whole scroll width: a wide `#empty` slot stays inside the box on a 375px
179
+ phone. The table needs no width from its parent: inside a
179
180
  content-sized one (`w-fit`, `inline-flex`, an `auto` grid track, a popover)
180
181
  it is as wide as its columns.
181
182
 
182
183
  The `select` column's checkboxes keep their 16px box, with a 24px target
183
- around it (WCAG 2.5.8). The toolbar renders only when it has content:
184
- the `actions` prop or the `#actions` slot that render nothing (`null`,
185
- `false`, whitespace, a `v-if` left out) leave no empty `<fieldset>` behind.
184
+ around it (WCAG 2.5.8), and a 12px margin after it: axe's `target-size`
185
+ measures the box itself, so a 24px circle around it stays clear of the next
186
+ column's control — "Select all" beside a `DataTableColumnHeader` sort button
187
+ included.
188
+
189
+ The toolbar renders only when it has content: the `actions` prop or the
190
+ `#actions` slot that render nothing (`null`, `false`, `''`, whitespace, a
191
+ `v-if` left out, an empty fragment or array) leave no empty `<fieldset>`
192
+ behind. A component of yours that renders nothing still counts as content:
193
+ put the `v-if` on it instead.
186
194
 
187
195
  Selection actions do not move the rows. With `actions` or the `#actions`
188
196
  slot and the `select` column (`predefinedColumns().select`, id `select`),
189
- the toolbar's height is reserved — an empty, hidden placeholder
197
+ the toolbar's height (68px) is reserved — an empty, hidden placeholder
190
198
  (`[data-slot="header-placeholder"]`) while they render nothing, the same
191
199
  minimum height once they do — so the "Delete selected" that appears on the
192
200
  first selection does not push the tapped row down. The reserved height is
193
201
  one row of default-size buttons (`h-9`); taller or wrapping actions still
194
- grow the toolbar.
202
+ grow the toolbar. Without the `select` column, or without actions, no band
203
+ is kept above the table.
195
204
 
196
205
  ```vue
197
206
  <template>
@@ -83,7 +83,12 @@ await page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: tru
83
83
  first day (Monday in French). Each header is labelled by its full weekday
84
84
  ("lundi"), as in `@nxgt/material`; reka hides the row from assistive tech
85
85
  (`<thead aria-hidden>`), since every day's name already carries its weekday.
86
- It reads at 4.5:1 or more on the popover in the light and the dark theme.
86
+ It reads at 4.5:1 or more on its tinted header (`th`, `bg-primary/15`), on the
87
+ page and in a popover, in the light and the dark theme.
88
+
89
+ **Contrast.** The days of the neighbouring months are muted but not faded:
90
+ they can be picked, so they read at 4.5:1 or more, on a tinted week too
91
+ (`Date & time/Calendar` → Contrast measures them all).
87
92
 
88
93
  No form wrapper — use `DateFormField`.
89
94
  **Storybook:** `Date & time/Calendar` · **React:** `Calendar`, `CalendarDayButton`
@@ -96,8 +96,17 @@ const open = ref(true);
96
96
  | `dismissible` | `boolean` | `false` | shows a close button that emits `dismiss`; the banner does not hide itself |
97
97
 
98
98
  Where React shows the close button when `onDismiss` is passed, Vue asks for
99
- `dismissible` and `@dismiss`. The banner has `role="status"`. It stays mounted
100
- until you stop rendering it.
99
+ `dismissible` and `@dismiss`. It stays mounted until you stop rendering it.
100
+
101
+ **Announced:** every tone, `error` included, is a polite live region:
102
+ `role="status"` `aria-live="polite"` `aria-atomic="true"`. A change of its
103
+ text is read out, and a modal layer keeps it reachable when it makes the page
104
+ `inert`; a `Banner` mounted later inside a part of the page made inert stays
105
+ inert until the layer closes. Behind the layer, a screen reader's browse mode
106
+ still reaches its action and dismiss button. For an urgent banner, pass
107
+ `role="alert"` (and `aria-live="assertive"`): the
108
+ attributes you pass (`id`, `data-*`, `aria-*`, `role`) fall through to its
109
+ root and win over these.
101
110
 
102
111
  **Emits:** `dismiss` — no payload.
103
112
  **Storybook:** `Feedback & status/Banner` · **React:** `Banner`
@@ -643,6 +652,12 @@ touch screen a tap no longer fans out a stack of two or more toasts either: pass
643
652
  only collapses the stack); this `Toast` does when focus is inside it, with or
644
653
  without an action. The close button is reached with Tab.
645
654
 
655
+ **The action's colours.** The toast is the inverse of the page (dark in the
656
+ light theme, light in the dark one), so its `{ label }` action takes the other
657
+ theme's primary tone, on an opaque mix of primary toward the toast, and reads
658
+ at 4.5:1 or more in both themes (WCAG 1.4.3). A function action's own buttons
659
+ keep the page's tones: give them colours measured on the toast.
660
+
646
661
  **Focus after an action.** A click on a toast's action first gives the focus
647
662
  back to the control that had it before the focus entered the toast, then runs
648
663
  your `onClick`, then closes the toast; a function action's `dismiss` does the
@@ -248,19 +248,20 @@ import { Input, Label } from '@nxgt/material-vue/components';
248
248
 
249
249
  | Prop | Type | Default | Effect |
250
250
  | --- | --- | --- | --- |
251
- | `with-asterisk` | `boolean` | — | draws a red `*` with CSS after the label, so it is in neither the label's text nor its accessible name (`getByLabel('Email', { exact: true })` and `getByLabelText` match "Email"): mark the field itself `required` (or `aria-required`), which every field of this library does for you |
251
+ | `with-asterisk` | `boolean` | — | draws a red `*` after the label, an `aria-hidden` SVG, so it is in neither the label's text nor its accessible name (`getByLabel('Email', { exact: true })` and `getByLabelText` match "Email"): mark the field itself `required` (or `aria-required`), which every field of this library does for you |
252
252
  | `for` | `string` | — | the control it labels; clicking the label focuses it |
253
253
 
254
254
  Fields on the [Form fields](form-fields.md) page render it for you through
255
255
  their `label` prop.
256
256
 
257
- To restyle the mark, target `[data-slot="label-text"][data-required]::after`:
258
- `background-color` sets its colour (default `var(--color-error)`), `width` and
259
- `height` its size. It stays visible in forced-colors mode and in print.
257
+ To restyle the mark, target `[data-slot="label-required-mark"]`: `color` sets
258
+ its colour (default `var(--color-error)`), `width` and `height` its size. It
259
+ stays visible in forced-colors mode and in print, and it loads no image, so a
260
+ Content-Security-Policy whose `img-src` refuses `data:` draws it too.
260
261
 
261
262
  ```css
262
- [data-slot="label-text"][data-required]::after {
263
- background-color: #b45309;
263
+ [data-slot="label-required-mark"] {
264
+ color: #b45309;
264
265
  width: 0.75em;
265
266
  height: 0.75em;
266
267
  }
@@ -251,7 +251,8 @@ button ("Open menu") that opens the same entries in a sheet. Breadcrumbs are
251
251
  hidden there. The sheet is a dialog named "Navigation"
252
252
  (`material.app-shell.navigation`) with a visible "Close" button. Following an
253
253
  entry closes it — so does any route change, Back included — and focus goes to
254
- the page's `<main>`; closed with Escape, the close button or the overlay,
254
+ the page's `<main>`, unless the new screen already focused an element inside it
255
+ (an error page's `h1`), which keeps it; closed with Escape, the close button or the overlay,
255
256
  focus returns to the menu button. Its entries show their labels, so they have
256
257
  no tooltip: none opens over the first entry as focus moves in.
257
258
 
package/docs/roadmap.md CHANGED
@@ -1,15 +1,65 @@
1
1
  # Roadmap
2
2
 
3
3
  What is being worked on, what is likely to follow, and what has been decided
4
- against. This is a direction, not a commitment: the only number on this page is
5
- the version something shipped in.
4
+ against. This is a direction, not a commitment: shipped entries carry the
5
+ version they shipped in, and planned entries may name the major they target.
6
6
 
7
7
  ## Now
8
8
 
9
- Nothing in progress right now; see Next.
9
+ - **Empty and error states that look designed** — `EmptyState` and a new
10
+ `ErrorState` in three sizes (`default`, `compact`, `inline`), with in-house
11
+ inline SVG illustrations or a slot for your own. `ErrorState` is a
12
+ `role="alert"` with a Retry action and collapsible technical details. Every
13
+ built-in empty or error state uses them — the `DataTable` default (your
14
+ `#empty` slot and `empty` prop still replace it), `PdfReader`, `CommandEmpty`,
15
+ the gallery, the kanban board, chat and the partials. Ships in
16
+ `@nxgt/material-vue` 1.0, with the rest of the redesign.
10
17
 
11
18
  ## Next
12
19
 
20
+ - **One media player, video and audio** — `MediaPlayer`, `MediaPlayerMini`,
21
+ `useMediaPlayer` and `AudioPlayer`, built on the native `<video>` / `<audio>`.
22
+ `StxPlayer` and `VideoPlayer` are removed in `@nxgt/material-vue` 1.0, with a
23
+ migration guide. It plays streams — native HLS, plus `hls.js` with a quality
24
+ menu when you install it (an optional peer, loaded only when a stream needs
25
+ it) — shows chapters and a transcript that follows the playback, a waveform
26
+ for audio, thumbnails on hover (VTT sprites), resumes where the viewer left
27
+ off, and adds gestures, shuffle, a shortcut-help panel and a screen that stays
28
+ awake. It runs embedded at an aspect ratio, fullscreen, floating (a corner
29
+ mini player, and an audio bar that persists across pages) or in a modal
30
+ lightbox. Built for WCAG 2.2 AA and selected AAA criteria: controls at 7:1,
31
+ captions plus a transcript, a full keyboard, screen-reader announcements,
32
+ `aria-valuetext` on sliders, reduced motion and forced colors.
33
+ - **PDFs generated without React** — a framework-free engine on pdfkit replaces
34
+ `@react-pdf/renderer`, so the Vue library no longer needs react. One data API,
35
+ `definePdfDocument`, everywhere; the HTML `Print*` blocks stay for
36
+ `window.print()`. Output is tagged for accessibility (PDF/UA), archival
37
+ (PDF/A-3) and electronic invoicing (Factur-X, EN 16931). It also brings the
38
+ items below.
39
+ - **Your brand on the page** — a print theme whose defaults follow the screen
40
+ theme, and your own font files registered in one call (the library bundles
41
+ no fonts).
42
+ - **QR codes, barcodes and charts in a PDF** — pass the value or the chart
43
+ options and get vector output, no data URL to prepare.
44
+ - **Rich text and Markdown in a PDF** — the rich-text editor's content and
45
+ Markdown rendered as PDF headings, paragraphs, lists, tables and links.
46
+ - **A live preview inside `PdfReader`** — `PdfReader` takes a document and
47
+ renders it itself, refreshing a moment after its data stops changing.
48
+ - **Test helpers for generated PDFs** — read a generated PDF's text, page count
49
+ and metadata in a unit test, without a browser.
50
+ - **Archive-ready PDFs** — PDF/A-3 output through one option, for invoices and
51
+ records that must be kept.
52
+ - **Electronic invoices (Factur-X)** — an invoice PDF that carries its
53
+ machine-readable data (EN 16931), built on archive-ready PDFs and the
54
+ invoice template, for e-invoicing mandates such as France's.
55
+ - **A PDF reader that annotates and fills forms** — pdf.js used directly in
56
+ both libraries (React drops `react-pdf`), with one `PdfReader` whose `mode`
57
+ is `embedded`, `dialog` or `page` and whose toolbar adapts to the width of
58
+ its container. Viewers add their own annotations, held as JSON in
59
+ `value` / `onChange` and exportable into the PDF; fill AcroForms; read two
60
+ pages side by side; scroll continuously or page by page; hear the text read
61
+ aloud; or switch to a reflowed text mode. Annotation export loads `pdf-lib`
62
+ only when used.
13
63
  - **Print blocks that paginate on paper** — the HTML `Print*` blocks repeat
14
64
  their table header on every printed page, keep rows and totals whole, print
15
65
  their colours, and a `PrintPage` sets size and margins for `window.print()`.
@@ -19,6 +69,11 @@ Nothing in progress right now; see Next.
19
69
 
20
70
  ## Later
21
71
 
72
+ - **Next major: an error `Banner` interrupts** — `tone="error"` becomes
73
+ `role="alert"` `aria-live="assertive"`, so a screen reader reads it out at
74
+ once. Every tone is `role="status"` `aria-live="polite"` today; pass
75
+ `role="alert"` for an urgent banner until then. Breaking for a test that
76
+ finds an error `Banner` with `getByRole('status')`.
22
77
  - **Next major: `ListTile`'s `data-slot` becomes `list-tile`** — it is
23
78
  `item-detail` today, so the name matches the component like every other
24
79
  slot. Breaking for a selector or a test that looks for `item-detail`.
@@ -38,23 +93,6 @@ Nothing in progress right now; see Next.
38
93
  - **Ready-made documents** — invoice, quote, receipt, report, certificate and
39
94
  label-sheet templates that take your data and return a document, their labels
40
95
  translated.
41
- - **Your brand on the page** — a print theme whose defaults follow the screen
42
- theme, and your own font files registered in one call — the library bundles
43
- no fonts.
44
- - **QR codes, barcodes and charts in a PDF** — pass the value or the chart
45
- options and get vector output, no data URL to prepare.
46
- - **Rich text and Markdown in a PDF** — the rich-text editor's content and
47
- Markdown rendered as PDF headings, paragraphs, lists, tables and links.
48
- - **A live preview inside `PdfReader`** — `PdfReader` renders a document
49
- itself and refreshes it a moment after its data stops changing. Today
50
- `usePdf` plus `<PdfReader :file="blob">` covers a manual preview.
51
- - **Test helpers for generated PDFs** — read a generated PDF's text, page count
52
- and metadata in a unit test, without a browser.
53
- - **Archive-ready PDFs** — PDF/A output through one option, for invoices and
54
- records that must be kept.
55
- - **Electronic invoices (Factur-X)** — an invoice PDF that carries its
56
- machine-readable data, built on archive-ready PDFs and the invoice template,
57
- for e-invoicing mandates such as France's.
58
96
 
59
97
  ## Not planned
60
98
 
@@ -85,13 +123,14 @@ Nothing in progress right now; see Next.
85
123
  - **An app shell that builds its own menu** — `items` stays a prop and labels
86
124
  stay already-translated, because each app filters its menu by its own
87
125
  permissions and translates in its own namespace.
88
- - **Right-to-left and complex-script PDFs** — the PDF engine
89
- (`@react-pdf/renderer`) does not shape Arabic or Hebrew reliably. Render such
90
- documents with the HTML `Print*` blocks and print them with a browser:
91
- `window.print()`, or Playwright or Gotenberg on a server.
92
- - **A second PDF engine** — no Vue-native renderer and no pdfmake: `/pdf`
93
- renders with the same engine as `@nxgt/material/print`, so one document model
94
- and one set of templates serve both libraries.
126
+ - **Right-to-left and complex-script PDFs** — pdfkit does not lay out
127
+ right-to-left text or shape Arabic and Hebrew reliably, so the new engine
128
+ does not promise it. Render such documents with the HTML `Print*` blocks and
129
+ print them with a browser: `window.print()`, or Playwright or Gotenberg on a
130
+ server.
131
+ - **Two PDF engines** — pdfkit replaces `@react-pdf/renderer`; there is no
132
+ Vue-native renderer, no pdfmake and no second engine beside it. One engine
133
+ and one document model serve both libraries, and react is no longer needed.
95
134
 
96
135
  ## Shipped
97
136
 
@@ -217,24 +256,23 @@ Nothing in progress right now; see Next.
217
256
  keyboard and in fullscreen. 0.22.0.
218
257
  ([PdfReader](components/media-and-files.md#pdfreader))
219
258
  - **Amounts and dates in your locale** — `formatAmount` and `formatDate` take a
220
- locale and time zone, use each currency's own decimals (`{ fractionDigits: 2 }`
221
- keeps two) and keep a date-only value on its day; they are on
259
+ locale and time zone, use each currency's own decimals (`{ fractionDigits: 2
260
+ }` keeps two) and keep a date-only value on its day; they are on
222
261
  `@nxgt/material-vue/lib` as in `@nxgt/material`; `useFormatters()` binds them
223
262
  to the `I18nProvider` language and `createFormatters()` to a locale you pass,
224
263
  so a document rendered on a server reads like the screen. 0.22.0.
225
264
  ([printing](guides/printing.md#formatamount-and-formatdate))
226
- - **More built-in labels follow the language** — calendar navigation,
227
- the data table's selection checkboxes, filter presets and
228
- `Min`/`Max`, the spinner, scroll-to-top, upload `Retry`/`Remove`, the map's
229
- `You are here`, zoom and `My position` buttons, close buttons (including
230
- `DialogFooter`'s), and the editor's slash menu (searchable in either language), font names and table alignment;
231
- then the pagination buttons, breadcrumb names, `CommandDialog`'s title, the
232
- carousel's role description, the filter's empty state and chips,
265
+ - **More built-in labels follow the language** — calendar navigation, the data
266
+ table's selection checkboxes, filter presets and `Min`/`Max`, the spinner,
267
+ scroll-to-top, upload `Retry`/`Remove`, the map's `You are here`, zoom and `My
268
+ position` buttons, close buttons (including `DialogFooter`'s), and the
269
+ editor's slash menu (searchable in either language), font names and table
270
+ alignment; then the pagination buttons, breadcrumb names, `CommandDialog`'s
271
+ title, the carousel's role description, the filter's empty state and chips,
233
272
  `MessageCard`'s "Unread", the upload field's replace button and errors, the
234
273
  attribute options' "Add option", the editor's placeholder and emoji buttons,
235
274
  `CircularProgress`, `StxPlayer`, `PaneLayoutPane`'s sheet title,
236
- `JsonViewer`'s "Empty JSON" and `TimePicker`'s AM/PM.
237
- 0.22.0.
275
+ `JsonViewer`'s "Empty JSON" and `TimePicker`'s AM/PM. 0.22.0.
238
276
  ([i18n](guides/i18n.md#the-material-namespace))
239
277
  - **PdfReader fixes** — downloads finish in every browser and take the file name
240
278
  you give instead of `document.pdf`, a wrong password says so, Escape inside a
@@ -23,6 +23,7 @@ words in your error.
23
23
  - [An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link](#an-accessibility-audit-reports-low-contrast-on-a-tonal-or-outlined-button-a-selected-list-tile-or-a-hovered-badge-link)
24
24
  - [An accessibility audit reports low contrast on error text in the dark theme](#an-accessibility-audit-reports-low-contrast-on-error-text-in-the-dark-theme)
25
25
  - [Dialogs, toasts or buttons no longer animate](#dialogs-toasts-or-buttons-no-longer-animate)
26
+ - [A Content-Security-Policy reports a `data:` image on every form with a required field](#a-content-security-policy-reports-a-data-image-on-every-form-with-a-required-field)
26
27
  - [A Button's label overflows a card on a phone](#a-buttons-label-overflows-a-card-on-a-phone)
27
28
  - [`JsBarcode is not a function`](#jsbarcode-is-not-a-function)
28
29
  - [A `LinkButton` selected as a `button` stops matching](#a-linkbutton-selected-as-a-button-stops-matching)
@@ -55,6 +56,8 @@ words in your error.
55
56
  - [A toast never goes away after a tap](#a-toast-never-goes-away-after-a-tap)
56
57
  - [A toast's action cannot be clicked while a dialog is open](#a-toasts-action-cannot-be-clicked-while-a-dialog-is-open)
57
58
  - [`toasts.success(message, { action })` shows no action button](#toastssuccessmessage--action--shows-no-action-button)
59
+ - [An accessibility audit reports low contrast on a toast's action](#an-accessibility-audit-reports-low-contrast-on-a-toasts-action)
60
+ - [A `Banner` is not announced while a dialog is open](#a-banner-is-not-announced-while-a-dialog-is-open)
58
61
  - [The focus leaves a dialog that a toast's action opened](#the-focus-leaves-a-dialog-that-a-toasts-action-opened)
59
62
  - [A `ConfirmationDialog` reopened at once stays closed](#a-confirmationdialog-reopened-at-once-stays-closed)
60
63
  - [The first Escape after closing a nested dialog does nothing](#the-first-escape-after-closing-a-nested-dialog-does-nothing)
@@ -62,6 +65,7 @@ words in your error.
62
65
  **Navigation**
63
66
 
64
67
  - [A `ListTile` link reloads the whole page](#a-listtile-link-reloads-the-whole-page)
68
+ - [After a link in the phone menu, the focus goes to `<main>`, not to the page's heading](#after-a-link-in-the-phone-menu-the-focus-goes-to-main-not-to-the-pages-heading)
65
69
  - [The phone menu needs two Escapes to close, or a tooltip shows over it](#the-phone-menu-needs-two-escapes-to-close-or-a-tooltip-shows-over-it)
66
70
  - [`Tabs` look different after upgrading](#tabs-look-different-after-upgrading)
67
71
  - [A screen reader announces an icon-only tab as "tab", with no name](#a-screen-reader-announces-an-icon-only-tab-as-tab-with-no-name)
@@ -129,6 +133,7 @@ words in your error.
129
133
  - [Zod messages ignore the language, or your `material.zod.*` strings](#zod-messages-ignore-the-language-or-your-materialzod-strings)
130
134
  - [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
131
135
  - [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
136
+ - [An accessibility audit reports low contrast on a calendar's weekday row or its days](#an-accessibility-audit-reports-low-contrast-on-a-calendars-weekday-row-or-its-days)
132
137
 
133
138
  **Runtime**
134
139
 
@@ -166,6 +171,7 @@ words in your error.
166
171
  - [A `DataTable` shows its header row on a phone](#a-datatable-shows-its-header-row-on-a-phone)
167
172
  - [A test's `getByRole('region')` finds more than one region](#a-tests-getbyroleregion-finds-more-than-one-region)
168
173
  - [A `DataTable` toolbar is gone](#a-datatable-toolbar-is-gone)
174
+ - [axe reports `target-size` on a `DataTable`'s "Select all"](#axe-reports-target-size-on-a-datatables-select-all)
169
175
 
170
176
  **PDF reader**
171
177
 
@@ -852,10 +858,9 @@ await expect(page.getByRole('combobox', { name: 'Delivery date' })).toHaveAttrib
852
858
  ```
853
859
 
854
860
  Custom CSS that targeted the old `<span class="text-error">` should target
855
- `[data-slot="label-text"][data-required]::after` instead; the recipe is in
856
- [Label](components/primitives.md#label). The mark is drawn by the package stylesheet, so a
857
- snapshot of the DOM no longer contains it, and it stays visible in
858
- forced-colors mode.
861
+ `[data-slot="label-required-mark"]` instead; the recipe is in
862
+ [Label](components/primitives.md#label). The mark is an `aria-hidden` SVG with
863
+ no text, and it stays visible in forced-colors mode.
859
864
 
860
865
  ### An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry
861
866
 
@@ -916,8 +921,8 @@ In the dark theme, a filled `secondary` `Button` gave its near-white text
916
921
  `success`, `info` or `warning` one 3.5:1, 2.5:1 and 3.4:1. The dark
917
922
  `--secondary` is brighter (`oklch(0.66 ...)`, 6.5:1 as text on
918
923
  `--background`), and the filled `secondary`, `success`, `info` and `warning`
919
- `Button`s and `IconButton`s are tinted in the dark theme
920
- (`dark:bg-secondary/70`, `dark:bg-success/60`, …) so their text keeps 4.5:1.
924
+ `Button`s and `IconButton`s are filled in the dark theme with an opaque mix
925
+ toward `--background` (about 70 %), so their text keeps 4.5:1 over any backdrop.
921
926
  A filled button given a background of your own through `className` loses the
922
927
  tint in dark mode: add a matching `dark:` one.
923
928
 
@@ -934,8 +939,8 @@ is open and leaves it focusable; a control the tab order reaches inside an
934
939
  reports it predates the inert background.
935
940
  **Fix:** upgrade: the page behind a modal layer of this library is `inert`
936
941
  as well — out of the tab order and of the accessibility tree — and the rule
937
- passes. Live regions (the `Toaster`) stay reachable. Do not exclude
938
- `[aria-hidden]` or `[data-aria-hidden]` from the audit: it hides real
942
+ passes. Live regions (the `Toaster`, every `Banner`) stay reachable. Do not
943
+ exclude `[aria-hidden]` or `[data-aria-hidden]` from the audit: it hides real
939
944
  failures. A layer of yours built on reka directly (not on these
940
945
  components) still marks the page `aria-hidden` only; make the rest of the
941
946
  page inert with core's `inertOthers`, which keeps live regions and counts
@@ -1164,6 +1169,42 @@ toasts.success({ title: 'Staff member restored', action: { label: 'Send an invit
1164
1169
  `data` takes no `icon` (a preset draws its own) and no `cancel` (the X button
1165
1170
  beside the action stands for it).
1166
1171
 
1172
+ ### An accessibility audit reports low contrast on a toast's action
1173
+
1174
+ **When:** axe (or Lighthouse) reports `color-contrast` on a toast's action
1175
+ button (`[data-slot="toast-action"]`), the `{ label }` one: an
1176
+ "Undo", a "Send an invitation", or the "Close" a preset shows in its place.
1177
+ **Why:** the action is a `tonal` `Button`, whose tones are made for the page,
1178
+ and the toast is the inverse of the page (`bg-foreground`: dark in the light
1179
+ theme, light in the dark one). Its `primary` label on a 15 % primary tint over
1180
+ the toast read 2.5:1 in the light theme and 1.5:1 in the dark one.
1181
+ **Fix:** upgrade: the action takes the other theme's tone (the lightened
1182
+ primary on the dark toast, `primary` on the light one) on an opaque mix of
1183
+ primary toward the toast's surface, 9.4:1 and 5.5:1 at rest, and more hovered,
1184
+ focused or pressed. Drop an exclusion of `[data-slot="toast-action"]` from
1185
+ your contrast scan. A function action (`(dismiss) => h(Button, …)`) renders
1186
+ your own buttons, which keep the page's tones: give them colours measured on
1187
+ the toast, or use the `{ label, onClick }` form.
1188
+
1189
+ ### A `Banner` is not announced while a dialog is open
1190
+
1191
+ **When:** a `Banner` (an update notice, a session notice) changes or shows
1192
+ while a `Dialog`, `Sheet` or another modal layer is open, and a screen reader
1193
+ says nothing.
1194
+ **Why:** a modal layer makes the page behind it `inert` and keeps only live
1195
+ regions (`[aria-live]`) reachable; a `Banner` was a `role="status"` without
1196
+ `aria-live`, so it went inert with the page.
1197
+ **Fix:** upgrade: every `Banner`, `error` included, is a polite live region
1198
+ (`role="status"` `aria-live="polite"` `aria-atomic="true"`), so the layer
1199
+ keeps it reachable. The attributes you pass (`id`, `data-*`, `aria-*`,
1200
+ `role`) fall through to its root and win: pass `role="alert"` for an urgent
1201
+ banner. Drop a `<div aria-live="polite">` wrapper kept for this. The page is
1202
+ made inert when the layer opens: a `Banner` mounted later inside a part of
1203
+ the page made inert stays inert until the layer closes, so render it before
1204
+ (and change its text), or keep it in a live container that is already there.
1205
+ A kept `Banner` stays interactive behind the layer: a screen reader's browse
1206
+ mode reaches its action and dismiss button (Tab stays in the layer).
1207
+
1167
1208
  ### The focus leaves a dialog that a toast's action opened
1168
1209
 
1169
1210
  **When:** a toast's action opens a `Dialog` (or an `AlertDialog`, a `Sheet`,
@@ -1280,6 +1321,31 @@ export const AppLink = defineComponent({
1280
1321
  <ListTile title="Notifications" href="/settings/notifications" :link-component="AppLink" />
1281
1322
  ```
1282
1323
 
1324
+ ### After a link in the phone menu, the focus goes to `<main>`, not to the page's heading
1325
+
1326
+ **When:** on a phone, a link followed in the menu sheet of `ActivityLayout` or
1327
+ `AppHeader` (or a navigation while it is open) opens a screen that moves the
1328
+ focus to its own element as it mounts (an error page's `h1`, a step's title),
1329
+ and the focus ends on `<main>` instead.
1330
+ **Why:** as the sheet closed after a navigation, it always focused `<main>`,
1331
+ over what the new screen had focused.
1332
+ **Fix:** update `@nxgt/material-vue`. The sheet now leaves the focus where the screen put it
1333
+ inside `<main>`, and focuses `<main>` only when the focus is elsewhere (on the
1334
+ body, or still in the closing sheet). Code that gave the focus back
1335
+ to the screen's element after the sheet closed can go.
1336
+
1337
+ ### A Content-Security-Policy reports a `data:` image on every form with a required field
1338
+
1339
+ **When:** a page with a required field (`Label` with the asterisk, or a field
1340
+ of this library with `required`) reports a CSP violation for a
1341
+ `data:image/svg+xml` image under a policy whose `img-src` refuses `data:`
1342
+ (`img-src 'self'`), and the red asterisk is missing.
1343
+ **Why:** the mark was a CSS mask whose image was a `data:` URI.
1344
+ **Fix:** update `@nxgt/material-vue`. The mark is now an `aria-hidden` SVG in the label's
1345
+ markup, which loads no image. An override of `--label-required-mark` (or a copy
1346
+ of the mark served by your app) can go; restyle the mark with
1347
+ `[data-slot="label-required-mark"]` (see [Label](components/primitives.md#label)).
1348
+
1283
1349
  ### The phone menu needs two Escapes to close, or a tooltip shows over it
1284
1350
 
1285
1351
  **When:** on a phone, the menu sheet of `ActivityLayout` or `AppHeader` opens
@@ -2304,6 +2370,25 @@ z.config({ customError: zodLocaleError({ language: () => requestLanguage() }) })
2304
2370
 
2305
2371
  See [helpers](guides/helpers.md#zodlocaleerror-limits).
2306
2372
 
2373
+ ### An accessibility audit reports low contrast on a calendar's weekday row or its days
2374
+
2375
+ **When:** axe (or Lighthouse) reports `color-contrast` on the weekday row
2376
+ (`[data-slot="calendar-head-cell"]`, or the head cells of `DateRangeField`'s
2377
+ calendar) of a `Calendar`, `DateField`, `DateRangeField` or `EventCalendar`'s
2378
+ date picker, in the light theme, on the page or in its popover.
2379
+ **Why:** the base stylesheet tints every table header (`th`,
2380
+ `bg-primary/15`) and every other row (`bg-primary/5`), and a calendar is a
2381
+ table. Before this fix, the weekday row drew `--muted-foreground` on that
2382
+ header tint: 4.4:1.
2383
+ **Fix:** update. The weekday row is muted toward the foreground: 5.6:1 in
2384
+ the light theme, 6.2:1 in the dark one on the popover. The neighbouring
2385
+ months' days keep `--muted-foreground`, unfaded: 5.1:1 light and 5.6:1 dark
2386
+ at least, on a tinted week too. Drop an exclusion your accessibility test kept
2387
+ for the weekday row. If the audit still fails, your stylesheet overrides
2388
+ `--muted-foreground`, `--primary` or the table rules, or a `class` of your
2389
+ own sets the head cell back to `text-muted-foreground`; check it at 4.5:1 on
2390
+ `bg-primary/15` over `--popover`.
2391
+
2307
2392
  ### A calendar's weekday row is in English on a French page
2308
2393
 
2309
2394
  **When:** under `<I18nProvider>` set to French, a `DateField`,
@@ -3106,6 +3191,12 @@ page.getByRole('region', { name: 'Invoices' });
3106
3191
  **Why:** the toolbar is rendered only when it has content (yours, or `show-view-options`): an empty `<fieldset>` is a nameless group to a screen reader.
3107
3192
  **Fix:** look for what the toolbar holds (a filter by its label, the view options by "View"), not for the `fieldset` itself. A component of yours that renders nothing still counts as content: render it conditionally instead.
3108
3193
 
3194
+ ### axe reports `target-size` on a `DataTable`'s "Select all"
3195
+
3196
+ **When:** axe (WCAG 2.5.8) flags the header row's "Select all" checkbox (`thead [data-slot="checkbox-field"]`) when the column after the `select` column is a sortable `DataTableColumnHeader`, mostly on a phone, where the columns are narrowest. The rows' checkboxes pass.
3197
+ **Why:** axe measures the 16px box itself, not its 24px `::before`, so it asks for a 24px circle around the box clear of every other control. The sort button reaches 12px back into its cell's padding (`-ml-3`), and the `select` cell had no padding after its checkbox: the circle met the button.
3198
+ **Fix:** upgrade: the `select` column's checkboxes now keep a 12px margin after them. Drop any axe `exclude` of `thead [data-slot="checkbox-field"]` you kept for it. A `select` column of your own (not `predefinedColumns().select`) needs the same: `me-3` on its checkboxes.
3199
+
3109
3200
  ---
3110
3201
 
3111
3202
  ---
@@ -70,8 +70,8 @@
70
70
  --primary: oklch(46% 0.1135 276.35);
71
71
  --primary-foreground: oklch(0.984 0.003 247.858);
72
72
  /* Brighter than the light one: it is read as text on the dark surfaces (6.5:1
73
- on --background, 5.8:1 on --card); a filled secondary surface tints it
74
- (dark:bg-secondary/70) to keep 5.2:1 under its near-white text. */
73
+ on --background, 5.8:1 on --card); a filled secondary surface mixes it toward --background
74
+ (the opaque 70 % mix) to keep 5.2:1 under its near-white text. */
75
75
  --secondary: oklch(0.66 0.1135 245.65);
76
76
  --secondary-foreground: oklch(0.984 0.003 247.858);
77
77
  --muted: oklch(0.279 0.041 260.031);