@nxgt/material-vue 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +19 -6
  3. package/dist/{camera-DlWK0PLP.js → camera-C52IuvMH.js} +1 -1
  4. package/dist/components/components.css +3 -1
  5. package/dist/components/ui/fields/otp-field/input-otp.css +48 -0
  6. package/dist/{components-Dqg7KwA2.js → components-DFEmacXJ.js} +12895 -16816
  7. package/dist/components.js +8 -8
  8. package/dist/composables.js +24 -24
  9. package/dist/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  10. package/dist/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  11. package/dist/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  12. package/dist/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  13. package/dist/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  14. package/dist/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  15. package/dist/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  16. package/dist/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  17. package/dist/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  18. package/dist/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  19. package/dist/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  20. package/dist/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  21. package/dist/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  22. package/dist/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  23. package/dist/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  24. package/dist/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  25. package/dist/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  26. package/dist/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  27. package/dist/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  28. package/dist/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  29. package/dist/{form-scope-btEYYnGW.js → form-scope-CaYs-o2w.js} +1 -1
  30. package/dist/lib/components/ui/fields/attributes/form-schema.d.ts +11 -2
  31. package/dist/lib/components/ui/fields/opening-hours/consts.d.ts +12 -1
  32. package/dist/lib/components/ui/primitives/index.d.ts +1 -0
  33. package/dist/lib/components/ui/primitives/nonce-provider/index.d.ts +2 -0
  34. package/dist/lib/components/ui/primitives/nonce-provider/nonce-provider.vue.d.ts +20 -0
  35. package/dist/lib/components/ui/primitives/typography/typography.vue.d.ts +7 -4
  36. package/dist/lib/composables/index.d.ts +1 -0
  37. package/dist/lib/composables/use-csp-nonce.d.ts +18 -0
  38. package/dist/lib/lib/index.d.ts +1 -0
  39. package/dist/lib/nuxt/auto-imports.d.ts +1 -1
  40. package/dist/lib/nuxt/component-names.d.ts +1 -1
  41. package/dist/lib/vite/index.d.ts +3 -2
  42. package/dist/lib.js +49 -48
  43. package/dist/main.js +17 -17
  44. package/dist/material-vue.css +1 -1
  45. package/dist/{number.utils-Cy1z6qI7.js → number.utils-Bpo9tUaJ.js} +20 -20
  46. package/dist/{pdf-outline-sidebar-DchMD-_k.js → pdf-outline-sidebar-C_iJ8gqD.js} +1 -1
  47. package/dist/{pdf-page-D-kDQq5h.js → pdf-page-DOiJlWQn.js} +1 -1
  48. package/dist/{pdf-page-list-Cta0S89W.js → pdf-page-list-BTmUd7Ga.js} +2 -2
  49. package/dist/{pdf-reader-BwWozoIQ.js → pdf-reader-CEMMLJnB.js} +48 -45
  50. package/dist/{pdf-sidebar-B0Addf1P.js → pdf-sidebar-KLd76E5S.js} +2 -2
  51. package/dist/{pdf-thumbnail-sidebar-Dsptq4fb.js → pdf-thumbnail-sidebar-B4JkT_LV.js} +2 -2
  52. package/dist/{print-format.utils-DwJfU1tK.js → print-format.utils-DpBtUJvL.js} +1 -1
  53. package/dist/{stx-player-Bpnir1KO.js → stx-player-B6-PSgzP.js} +13 -13
  54. package/dist/{use-heading-spy-Czi_Xv3u.js → use-heading-spy-ClhUg50z.js} +54 -46
  55. package/dist/vite.js +2 -2
  56. package/docs/README.md +1 -0
  57. package/docs/components/data-display.md +5 -0
  58. package/docs/components/date-and-time.md +6 -0
  59. package/docs/components/feedback-and-status.md +3 -1
  60. package/docs/components/form-fields.md +23 -0
  61. package/docs/components/primitives.md +51 -2
  62. package/docs/getting-started.md +5 -1
  63. package/docs/guides/csp.md +420 -0
  64. package/docs/guides/helpers.md +2 -0
  65. package/docs/guides/i18n.md +2 -0
  66. package/docs/guides/theming.md +27 -0
  67. package/docs/roadmap.md +17 -0
  68. package/docs/troubleshooting.md +161 -0
  69. package/lib/components/components.css +3 -1
  70. package/lib/components/ui/fields/otp-field/input-otp.css +48 -0
  71. package/lib/nuxt/auto-imports.ts +1 -0
  72. package/lib/nuxt/component-names.ts +1 -0
  73. package/lib/vite/index.ts +6 -2
  74. package/package.json +4 -4
@@ -0,0 +1,420 @@
1
+ # Content Security Policy
2
+
3
+ This page is for an app that serves a strict Content Security Policy (CSP) and
4
+ wants `@nxgt/material-vue` to run under it: the directives to send, the few
5
+ places the library still needs a relaxation (and which components cause it),
6
+ and the settings that quiet the console. Nothing here is needed on a page
7
+ without a CSP.
8
+
9
+ ## Start from this policy
10
+
11
+ ```http
12
+ Content-Security-Policy:
13
+ default-src 'self';
14
+ script-src 'nonce-{NONCE}' 'strict-dynamic';
15
+ style-src-elem 'self' 'nonce-{NONCE}';
16
+ style-src-attr 'unsafe-inline';
17
+ font-src 'self';
18
+ img-src 'self' data: blob:;
19
+ worker-src 'self';
20
+ connect-src 'self';
21
+ object-src 'none';
22
+ base-uri 'self'
23
+ ```
24
+
25
+ | Directive | Why |
26
+ | --- | --- |
27
+ | `script-src 'nonce-X' 'strict-dynamic'` | Your entry script carries the nonce; the chunks it loads are trusted through `strict-dynamic`. The library needs no `'unsafe-eval'` once the zod setting below is applied |
28
+ | `style-src-elem 'self' 'nonce-X'` | Your stylesheet and the `<style>` elements that carry the nonce. No component needs `'unsafe-inline'` here, apart from the [known gaps](#known-gaps): see [the `<style>` elements the components inject](#style-elements-the-components-inject) |
29
+ | `style-src-attr 'unsafe-inline'` | The `style="…"` attributes in the server-rendered HTML and those set through `setAttribute('style', …)`. CSP blocks only these two; `el.style.x = …` and `style.setProperty` are not blocked. An attribute cannot carry a nonce, so this is the only way to allow it. It does not allow `<style>` elements or scripts |
30
+ | `font-src 'self'` | The library ships no web font (host Inter yourself: [Inter / the sans font](#inter--the-sans-font)), and KaTeX's fonts are files, never `data:` URIs: see [KaTeX fonts](#katex-fonts) for the one Vite setting the Tailwind route needs |
31
+ | `img-src 'self' data: blob:` | `data:` for the cropper's output, a captured `Camera` photo, a `Markdown` mermaid diagram and the PDF viewer's annotation icons (SVG `data:` images; the icons the components draw are inline `<svg>` elements and need none); `blob:` for previews of a file the user picked. Drop what you do not use |
32
+ | `worker-src 'self'` | The pdf.js worker of `PdfReader` is a same-origin file your bundler emits (Monaco: see below) |
33
+ | `connect-src 'self'` | `PdfReader` fetches the PDF from its `url`: add that origin when it is not yours |
34
+
35
+ Components outside this table need nothing more. Measured per component:
36
+
37
+ | Component | Extra directive | Status |
38
+ | --- | --- | --- |
39
+ | `PdfReader`, `PdfSidebar`, `PdfPageList`, `PdfThumbnailSidebar` | `worker-src 'self'` (the worker is a bundled asset), `connect-src` for the PDF's origin | worker URL read from the code; fonts: see [gaps](#known-gaps) |
40
+ | `CodeEditor`, `CodeDiff` (Monaco) | `@monaco-editor/loader` loads Monaco from `https://cdn.jsdelivr.net/npm/monaco-editor@…/min/vs` by default: that host in `style-src-elem` (Monaco's CSS) and `font-src` (its icon font), plus `worker-src blob:` | not yet verified in a browser. Safe default: allow `https://cdn.jsdelivr.net` in `script-src`, `style-src-elem` and `font-src`, and `worker-src blob:`; or stay on `'self'` by [serving Monaco yourself](#monaco-on-self) |
41
+ | `Markdown` with a `mermaid` block | mermaid is imported lazily and runs with `securityLevel: 'strict'` | whether mermaid needs `'unsafe-eval'` is not yet verified. Safe default: keep `'unsafe-eval'` out and report any violation you see; its SVG carries a `<style>` element, see [gaps](#known-gaps) |
42
+ | `Camera` | none: `getUserMedia` is not governed by CSP; the captured photo is a `data:` image | `media-src` for the live preview is not yet verified. Safe default: `media-src 'self' blob:` (a `srcObject` stream usually needs nothing) |
43
+
44
+ ### Monaco on `'self'`
45
+
46
+ Copy Monaco's `min/vs` folder to your public files (for example `/monaco/vs`)
47
+ and point the loader at it, before the first editor mounts:
48
+
49
+ ```ts
50
+ import loader from '@monaco-editor/loader';
51
+
52
+ loader.config({ paths: { vs: '/monaco/vs' } });
53
+ ```
54
+
55
+ ### Packages the app imports itself
56
+
57
+ `zod`, `reka-ui` and `@monaco-editor/loader` are dependencies of the library,
58
+ imported at runtime rather than bundled (keep your zod inside `^4.4.3`, since zod 4.4 or later shares its
59
+ configuration through `globalThis`, and the app and the library share one copy). A strict
60
+ package manager (pnpm) does not let the app import a package it does not
61
+ list, so install what you import: `bun add zod reka-ui @monaco-editor/loader`.
62
+
63
+ ## NonceProvider
64
+
65
+ Render `NonceProvider` once, at the root, with this request's nonce, on the
66
+ server and on the client. Every component below it takes the nonce: reka's
67
+ `Select`, `Combobox`, `ScrollArea` and `Splitter` (`NonceProvider` renders a
68
+ reka `ConfigProvider` with `nonce`), `InputOTP` when you pass it no `nonce`
69
+ of its own, and the stylesheet tiptap injects for `RichTextEditor` (its
70
+ `injectNonce` option).
71
+
72
+ ```vue
73
+ <!-- App.vue -->
74
+ <script setup lang="ts">
75
+ import { NonceProvider } from '@nxgt/material-vue';
76
+
77
+ defineProps<{ nonce?: string }>();
78
+ </script>
79
+
80
+ <template>
81
+ <NonceProvider :nonce="nonce">
82
+ <RouterView />
83
+ </NonceProvider>
84
+ </template>
85
+ ```
86
+
87
+ With Nuxt, the module registers `NonceProvider` and auto-imports
88
+ `useCspNonce`: see [SSR](#ssr) for `app.vue` with nuxt-security.
89
+
90
+ On the server, the nonce goes down through `provide`/`inject` only. On the
91
+ client, `NonceProvider` also keeps it for the code that runs outside Vue
92
+ (`claimCspNonce`, once mounted, so never on a server: one request's nonce
93
+ never reaches another's). With nested providers, the innermost one mounted is
94
+ the page's nonce, and it falls back to the outer one's when it unmounts. On
95
+ the client, read the nonce the server wrote into the page and give the root
96
+ the same prop:
97
+
98
+ ```ts
99
+ // entry-client.ts
100
+ import { getCspNonce } from '@nxgt/material-vue';
101
+ import { createSSRApp } from 'vue';
102
+ import App from './App.vue';
103
+
104
+ createSSRApp(App, { nonce: getCspNonce() }).mount('#app');
105
+ ```
106
+
107
+ `getCspNonce()` reads, in order: the value given to `setCspNonce`,
108
+ `<meta property="csp-nonce" nonce="…">` (what Vite's `html.cspNonce` writes)
109
+ or `<meta name="csp-nonce" content="…">`, the first `<script nonce>` or
110
+ `<style nonce>` of the page, then `__webpack_nonce__`. A browser hides a
111
+ `nonce` attribute's value once the page is parsed; `getCspNonce()` reads the
112
+ `nonce` property, which keeps it.
113
+
114
+ A nested reka `ConfigProvider` replaces its parent's settings instead of
115
+ merging with them. `NonceProvider` carries over the `dir`, `locale`,
116
+ `scrollBody`, `teleportTo` and `useId` of a `ConfigProvider` above it, so
117
+ put yours outside: a `ConfigProvider` inside `NonceProvider` without a
118
+ `nonce` hides the nonce from the reka components below it.
119
+
120
+ A component of your own that creates a `<style>` takes the nonce with
121
+ `useCspNonce()`, in `setup`:
122
+
123
+ ```vue
124
+ <script setup lang="ts">
125
+ import { useCspNonce } from '@nxgt/material-vue/composables';
126
+ import { h } from 'vue';
127
+
128
+ const props = defineProps<{ css: string }>();
129
+ const nonce = useCspNonce();
130
+ const Keyframes = () => h('style', { nonce: nonce.value }, props.css);
131
+ </script>
132
+
133
+ <template>
134
+ <Keyframes />
135
+ </template>
136
+ ```
137
+
138
+ `useCspNonce()` is a computed ref: `NonceProvider`'s nonce, else the nearest
139
+ reka `ConfigProvider`'s, else `undefined`. It never reads the page in
140
+ `setup`, so the server and the client render the same attribute and
141
+ hydration does not mismatch. Code that runs once mounted (an imperative
142
+ widget creating its own `<style>`) can call `getCspNonce()` instead.
143
+
144
+ ## `<style>` elements the components inject
145
+
146
+ `style-src-elem 'self' 'nonce-X'` is enough for every component below: your
147
+ stylesheet carries the dependencies' CSS, and under a `NonceProvider` the few
148
+ `<style>` elements left carry the nonce. No route needs `'unsafe-inline'` in
149
+ `style-src-elem` for them; mermaid and pdf.js's font fallback are the
150
+ [known gaps](#known-gaps).
151
+
152
+ | Component | What it needs | How |
153
+ | --- | --- | --- |
154
+ | `Toaster` (vue-sonner), `ImageCropper` (vue-advanced-cropper), `Spinner`, `PdfReader` | nothing | their CSS is in the stylesheet you import (`styles.css`, or `dist/material-vue.css`); none of them creates a `<style>` (measured: vue-advanced-cropper's code has none) |
155
+ | `Drawer`, the scroll lock of modals | nothing | they only set style properties from JavaScript, which CSP does not block |
156
+ | `InputOTP` (vue-input-otp) | the nonce | vue-input-otp adds a `<style>` on mount, with its `nonce` prop, else `NonceProvider`'s. Its rules are in the stylesheet too, so a page without the nonce still renders it right, but reports the blocked `<style>` |
157
+ | `Select`, `Combobox`, `ScrollArea`, `Splitter` (reka-ui) | the nonce | reka renders a `<style>` element and reads `ConfigProvider`'s `nonce`, which `NonceProvider` sets |
158
+ | `RichTextEditor` (tiptap) | the nonce | tiptap, on mount, with `NonceProvider`'s nonce as `injectNonce` |
159
+
160
+ The library's CI checks this on every build: it fails when the built
161
+ JavaScript creates a `<style>`, or calls `insertRule`, `Function` or `eval`,
162
+ outside a short list, each entry with its reason (tiptap, the gaps below,
163
+ and the two below). It then imports every built entry and fails if importing
164
+ calls `Function`: a schema built at import would. vue-input-otp and
165
+ vue-sonner are not bundled, so that check does not see them: a story renders
166
+ a `Toaster` with a toast, an open `Drawer` and `Dialog`, an `InputOTP`,
167
+ `Spinner` and `ImageCropper` under this policy and counts the violations:
168
+ none. Another adds `script-src 'self' 'nonce-X'`, no `'unsafe-eval'`, calls
169
+ `z.config({ jitless: true })`, then renders `AttributeForm`, submits it and
170
+ parses with `openingHoursSchema()`: no violation at all.
171
+
172
+ A `ConfigProvider :nonce` of your own, as earlier versions of this guide
173
+ showed, still works for reka's components; `NonceProvider` replaces it and
174
+ reaches `InputOTP` and `RichTextEditor` too.
175
+
176
+ ### Allowed by the check, nothing for you to do
177
+
178
+ | Where | What it is | Why it is no violation |
179
+ | --- | --- | --- |
180
+ | `PdfReader` (pdf.js) | the `isEvalSupported` probe, `new Function("")` | unreachable: `PdfReader` passes `isEvalSupported: false`. If you call `pdfjs.getDocument` yourself, pass it too |
181
+ | lodash (a dependency of some chart code) | `Function("return this")()` | only when neither `global` nor `self` exists: never in a browser |
182
+
183
+ ### Known gaps
184
+
185
+ Not fixable by configuration yet, and still reported by a strict policy:
186
+
187
+ - the `<style>` element inside mermaid's SVG, and the one cytoscape adds to
188
+ `<head>` for mermaid's mindmap and architecture diagrams;
189
+ - the font fallback of pdf.js, when a PDF font is not embedded.
190
+
191
+ ## zod and `'unsafe-eval'`
192
+
193
+ Building a `z.object()` makes zod probe `new Function` once, to choose
194
+ between its compiled parser and its interpreter. Without `'unsafe-eval'` the
195
+ browser reports a violation. The error is caught, so nothing breaks, but the
196
+ report is sent. Switch zod to its interpreter **before the first schema is
197
+ built**, in a module the app imports first:
198
+
199
+ ```ts
200
+ // zod-config.ts
201
+ import { z } from 'zod';
202
+
203
+ z.config({ jitless: true });
204
+ ```
205
+
206
+ ```ts
207
+ // main.ts (in Nuxt, a plugin file named 00.zod.client.ts: plugins load in file order)
208
+ import './zod-config'; // first
209
+ import './app';
210
+ ```
211
+
212
+ The library builds no schema while it loads, so that is all it takes:
213
+
214
+ - **zod is not bundled.** `@nxgt/material-vue` imports the zod your app has (a
215
+ dependency, `^4.4.3`), so your `z.config` is the one its schemas see. zod
216
+ 4.4 or later shares its configuration through `globalThis`, so a
217
+ second copy of zod 4 that a package manager nested still sees it; that is
218
+ why the range is `^4.4.3`.
219
+ - **Its schemas are factories**, built on the first call and the same schema
220
+ after: `openingHoursSchema()` and `attributeFormSchema()`, which
221
+ `AttributeForm` builds in its setup. A component that validates builds its
222
+ schema when it first renders, never at import.
223
+
224
+ | Deprecated constant (removed in 1.0) | Use |
225
+ | --- | --- |
226
+ | `OpeningHoursSchema` | `openingHoursSchema()` |
227
+ | `schema` (the attribute form's) | `attributeFormSchema()`; `toAttributeFormSchema()` is unchanged |
228
+
229
+ The deprecated constants still work: each is built on its first use and
230
+ behaves as the schema its factory returns (`.parse`, `.safeParse`, `.shape`,
231
+ `.extend`, `instanceof z.ZodObject`). It is not the same object, so do not
232
+ compare it with `===` or register it: a `.meta()` or `registry.add()` on
233
+ the stand-in is not seen through the factory's schema.
234
+
235
+ ## KaTeX fonts
236
+
237
+ `Markdown` renders formulas with KaTeX, which needs its fonts
238
+ (`KaTeX_*.woff2`). They are files on both routes, so `font-src 'self'` allows
239
+ them, and a browser downloads a font only when a formula draws a glyph from
240
+ it: a page with no math downloads none.
241
+
242
+ - **Compiled route** (`dist/material-vue.css`): the `@font-face` rules point at
243
+ `dist/fonts/katex/*.woff2`, shipped beside the stylesheet, through a
244
+ relative `url()`. A bundler emits them as files of your build; an app that
245
+ serves `dist/` as it is must serve `dist/fonts/` with it. Copy only the
246
+ stylesheet and math falls back to the system fonts
247
+ ([troubleshooting](../troubleshooting.md#math-renders-in-the-system-fonts)).
248
+ Earlier versions inlined the 60 fonts as `data:` URIs (1.4 MB of the
249
+ stylesheet), and `font-src` needed `data:`.
250
+ - **Tailwind route** (`@import "@nxgt/material-vue/styles.css"`): your bundler emits
251
+ KaTeX's fonts as files, but Vite inlines any asset under
252
+ `assetsInlineLimit` (4096 bytes) as a `data:` URI, and
253
+ `KaTeX_Size3-Regular.woff2` is 3.6 KB, so it is inlined and `font-src 'self'`
254
+ blocks it. Never inline fonts, with `cspAssetsInlineLimit` from
255
+ `@nxgt/material-vue/vite` (the same helper as [Inter](#inter--the-sans-font)):
256
+
257
+ ```ts
258
+ // vite.config.ts
259
+ import { cspAssetsInlineLimit } from '@nxgt/material-vue/vite';
260
+ import { defineConfig } from 'vite';
261
+
262
+ export default defineConfig({ build: { assetsInlineLimit: cspAssetsInlineLimit } });
263
+ ```
264
+
265
+ It returns `false` for a font and leaves every other asset to Vite's
266
+ default. (`assetsInlineLimit: 0` also works, and inlines nothing at all.)
267
+ The compiled route through Vite needs the same setting, for the same file.
268
+
269
+ The library's CI fails when a stylesheet in `dist/` carries a `data:` font.
270
+
271
+ ## Inter / the sans font
272
+
273
+ The library ships no web font. Its sans text uses the Tailwind token
274
+ `--font-sans`, which the theme does not set, so it is Tailwind's system stack
275
+ (`ui-sans-serif, system-ui, sans-serif`, then the emoji fonts). Many apps name
276
+ Inter there and load it from Google Fonts or rsms.me; both are blocked by
277
+ `font-src 'self'` and `style-src-elem 'self' 'nonce-X'`, because the page then
278
+ asks for another origin's stylesheet and fonts. Host Inter yourself instead:
279
+
280
+ ```sh
281
+ bun add @fontsource-variable/inter
282
+ ```
283
+
284
+ ```css
285
+ /* the app's root stylesheet */
286
+ @import "tailwindcss";
287
+ @import "@nxgt/material-vue/styles.css";
288
+ @import "@fontsource-variable/inter";
289
+
290
+ @theme {
291
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
292
+ }
293
+ ```
294
+
295
+ (`@fontsource-variable/inter` is the variable font, family `Inter Variable`.
296
+ For fixed weights install `@fontsource/inter` instead, import
297
+ `@fontsource/inter/400.css` and the weights you use, and name the family
298
+ `"Inter"`.) A JavaScript entry works as well: `import '@fontsource-variable/inter';`.
299
+
300
+ Set it in the root `@theme`, like the colours: `@theme` entries are resolved
301
+ on the root, so a value set on an inner element does not change what the
302
+ rest of the page inherits. See [theming](theming.md#fonts).
303
+
304
+ - **Under the policy.** The package's stylesheet is bundled into yours (no
305
+ second origin, so `style-src-elem 'self'` holds) and its `@font-face` rules
306
+ point at `.woff2` files that your bundler emits into the build. They load
307
+ from your own origin, so `font-src 'self'` allows them. A browser downloads
308
+ only the subsets a page draws: Latin first, the other scripts on demand.
309
+ - **Vite and `assetsInlineLimit`.** Vite inlines an asset under 4096 bytes as
310
+ a `data:` URI, which `font-src 'self'` blocks. Fontsource's files are
311
+ larger, so they are emitted as files without any setting. A small subset
312
+ file can still fall under the limit, and the KaTeX route needs it anyway:
313
+ keep the one setting that never inlines a font, `cspAssetsInlineLimit`
314
+ from `@nxgt/material-vue/vite`:
315
+
316
+ ```ts
317
+ // vite.config.ts
318
+ import { cspAssetsInlineLimit } from '@nxgt/material-vue/vite';
319
+ import { defineConfig } from 'vite';
320
+
321
+ export default defineConfig({ build: { assetsInlineLimit: cspAssetsInlineLimit } });
322
+ ```
323
+
324
+ ([KaTeX fonts](#katex-fonts) uses the same setting.)
325
+ - **If Inter is not loaded.** Nothing breaks and no violation is reported: the
326
+ browser skips `"Inter Variable"`, which names no installed font, and draws
327
+ the next entry of the stack, the system sans. Only the look changes. This is
328
+ also what you get when you keep the token at its default.
329
+ - **Troubleshooting.** A request to `fonts.googleapis.com` or
330
+ `fonts.gstatic.com` is not ours: see
331
+ [the font refused for another host](../troubleshooting.md#refused-to-load-the-font-httpsfontsgstaticcom-because-it-violates-the-following-content-security-policy-directive-font-src-self).
332
+
333
+ ## SSR
334
+
335
+ Generate the nonce **per request**, put it in the header and hand it to the
336
+ renderer. With Nuxt, let `nuxt-security` do it: it creates a nonce for each
337
+ request, adds it to the scripts and styles Nuxt emits, and exposes it as
338
+ `useNonce()`:
339
+
340
+ ```ts
341
+ // nuxt.config.ts
342
+ export default defineNuxtConfig({
343
+ modules: ['@nxgt/material-vue/nuxt', 'nuxt-security'],
344
+ security: {
345
+ nonce: true,
346
+ headers: {
347
+ contentSecurityPolicy: {
348
+ 'script-src': ["'nonce-{{nonce}}'", "'strict-dynamic'"],
349
+ 'style-src-elem': ["'self'", "'nonce-{{nonce}}'"],
350
+ 'style-src-attr': ["'unsafe-inline'"],
351
+ 'font-src': ["'self'"],
352
+ 'img-src': ["'self'", 'data:', 'blob:'],
353
+ },
354
+ },
355
+ },
356
+ });
357
+ ```
358
+
359
+ ```vue
360
+ <!-- app.vue: NonceProvider is registered by @nxgt/material-vue/nuxt -->
361
+ <script setup lang="ts">
362
+ import { getCspNonce } from '@nxgt/material-vue/lib';
363
+
364
+ // The request's nonce on the server; on the client, the one in the page.
365
+ const nonce = import.meta.server ? useNonce() : getCspNonce();
366
+ </script>
367
+
368
+ <template>
369
+ <NonceProvider :nonce="nonce">
370
+ <NuxtPage />
371
+ </NonceProvider>
372
+ </template>
373
+ ```
374
+
375
+ `useNonce()` reads the request, so it answers only on the server;
376
+ `getCspNonce()` finds the same value in the page on the client (nuxt-security
377
+ stamps Nuxt's scripts with it), so both render the same tree.
378
+
379
+ On your own Vite SSR server, generate the nonce in the request handler, put it
380
+ in the header and in every `<script>` and `<style>` you emit, and pass it to
381
+ `NonceProvider` through your root component's props:
382
+
383
+ ```ts
384
+ // entry-server.ts
385
+ import { createSSRApp } from 'vue';
386
+ import { renderToString } from 'vue/server-renderer';
387
+ import App from './App.vue';
388
+
389
+ export function render(nonce: string) {
390
+ return renderToString(createSSRApp(App, { nonce }));
391
+ }
392
+ ```
393
+
394
+ **Never keep the nonce in a module-level variable on the server.** Two
395
+ requests handled at once would overwrite each other, and one page would carry
396
+ the other's nonce. Pass it down the request: as an argument, a prop, or
397
+ `NonceProvider` (`provide`/`inject`). `setCspNonce` is a no-op there for that
398
+ reason.
399
+
400
+ See also the [SSR and Nuxt guide](ssr.md).
401
+
402
+ ## Status
403
+
404
+ Nothing is left to ship for the policy. What was done:
405
+
406
+ - the CSS of `Toaster`, `ImageCropper`, `Spinner` and `PdfReader` is in the
407
+ stylesheet, and none of them creates a `<style>`; `Drawer` and the scroll
408
+ lock of modals need nothing, since they only set style properties;
409
+ - the `<style>` elements left carry the nonce (`NonceProvider`): reka's,
410
+ the one `InputOTP` adds on mount (its rules are in the stylesheet too) and
411
+ tiptap's;
412
+ - KaTeX's fonts ship as files, so `font-src 'self'` needs no `data:`, and
413
+ Inter is documented as a self-hosted font;
414
+ - zod is an external dependency whose schemas are built on first use (see
415
+ above).
416
+
417
+ What remains are the [known gaps](#known-gaps) and the rows marked "not yet
418
+ verified" in the tables above; each is waiting on a dependency, not on this
419
+ library. Progress is on the [roadmap](../roadmap.md). A report you cannot match
420
+ to an entry is in [troubleshooting](../troubleshooting.md#content-security-policy).
@@ -17,6 +17,7 @@ differences are listed under each table. React's `/hooks` is
17
17
  | Export | What it does |
18
18
  | --- | --- |
19
19
  | `cn(...classes)` | `clsx` + `tailwind-merge`: joins conditional classes, last Tailwind utility wins. |
20
+ | `getCspNonce(doc?)`, `setCspNonce(nonce)` | The page's Content Security Policy nonce, from `@nxgt/material-core/csp-nonce`: `getCspNonce` reads what `setCspNonce` kept, else the `csp-nonce` meta tag, a `<script nonce>` or `<style nonce>`, else `__webpack_nonce__`; `undefined` on a server, where `setCspNonce` does nothing. See [CSP](csp.md#nonceprovider). |
20
21
  | `clientOnly(component)` | Wraps a component so it renders nothing on the server and on the first client render, and mounts only after `onMounted`. Pair it with `defineAsyncComponent(() => import(…))`. See [SSR](ssr.md). |
21
22
  | `renderSlot(slot, context)`, `ContentSlot<T>` | Resolves a `ContentSlot`: a `VNode`, a string or number, or a `(context) => VNode \| string \| number`. |
22
23
  | `formatAmount(value, currency?, { locale?, fractionDigits? })`, `formatDate(value, { locale?, timeZone?, dateStyle?, timeStyle? })` | An amount in its currency's own decimals (two without a currency) and a medium date, in the runtime's locale unless you pass one; a date-only `YYYY-MM-DD` keeps its day in every time zone; `'—'` when empty. Also on `/components`. See [printing](printing.md#formatamount-and-formatdate). |
@@ -195,6 +196,7 @@ they are auto-imported.
195
196
  | `useTheme()` | `{ theme, preference, updateTheme }`: light / dark / system, stored in `localStorage.theme`, applied as the `dark` class. `theme` and `preference` are reactive getters. See [theming](theming.md#usetheme). |
196
197
  | `resolveTheme(stored, prefersDark)`, `applyResolvedTheme(theme)` | The pure theme logic behind `useTheme`, for a boot script. `applyResolvedTheme` does nothing without a `document`. |
197
198
  | `useIsClient()` | `Ref<boolean>`: `false` on the server and first render, `true` after mount: a hydration guard. |
199
+ | `useCspNonce()` | `ComputedRef<string \| undefined>`: the nonce of the nearest `NonceProvider`, else of reka's nearest `ConfigProvider`, for a `<style>` of your own; `undefined` without either, on the server and on the client alike. Call it in `setup`. See [CSP](csp.md#nonceprovider). |
198
200
  | `useIsMobile()` | `Ref<boolean>`, `true` below 768 px (`@vueuse/core`'s `useMediaQuery`). On the server and in a component's first client render (an SPA's too) it answers for `provideSSRWidth()`'s width, or a 1024 px desktop (`false`), so hydration matches; a phone switches once the component mounts. Called outside a component (a Pinia setup store), it reads the screen straight away. |
199
201
  | `useHeadingSpy(ids, onActive, options?)` | IntersectionObserver over heading ids (a ref, getter or array); reports the topmost visible one. |
200
202
  | `useLocalizedDate()` | `{ language, locale, format }`: `DATE_UTILS.format` bound to the `I18nProvider` language (`en` without a provider). |
@@ -549,6 +549,8 @@ For money and dates shown to a user, `useFormatters()` (`formatAmount`,
549
549
  `useNumberFormatter()` is for the other `Intl.NumberFormat` styles
550
550
  (percentages, units). See [helpers](helpers.md#nxgtmaterial-vuecomposables).
551
551
 
552
+ The trigger of a `DateField`, `DateRangeField` or `TimeField` follows the same language: `01/09/2028` on a French page, `09/01/2028` in English.
553
+
552
554
  ## Reference
553
555
 
554
556
  Everything `@nxgt/material-vue/i18n` exports is listed in
@@ -69,6 +69,27 @@ Some colours are derived rather than tokens of their own:
69
69
  `bg-secondary-container`, `bg-accent-container`, `bg-error-container`. They
70
70
  follow the token they are computed from.
71
71
 
72
+ ## Fonts
73
+
74
+ The library ships no font. Text uses Tailwind's `--font-sans`, which the theme
75
+ does not set, so the default is the system sans stack
76
+ (`ui-sans-serif, system-ui, sans-serif`, then the emoji fonts); there is
77
+ nothing to download. To use Inter, self-host it and set the token in the root
78
+ `@theme`:
79
+
80
+ ```css
81
+ @import "@fontsource-variable/inter";
82
+
83
+ @theme {
84
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
85
+ }
86
+ ```
87
+
88
+ If Inter is not loaded, the browser draws the next font of the stack. Loading it
89
+ from Google Fonts or rsms.me is blocked by a strict CSP: the install, the
90
+ bundler settings and the policy are in
91
+ [Inter / the sans font](csp.md#inter--the-sans-font).
92
+
72
93
  ## Overriding tokens
73
94
 
74
95
  Redefine the variables **after** the import, for light and dark separately:
@@ -411,6 +432,12 @@ do other bundlers that support package `exports` in CSS. The file is not
411
432
  exported under a path of its own, so
412
433
  `import '@nxgt/material-vue/dist/material-vue.css'` does not resolve.
413
434
 
435
+ The compiled stylesheet loads KaTeX's fonts (`Markdown` maths) from
436
+ `dist/fonts/katex/`, beside it: your bundler emits them as files (Vite inlines the 3.6 KB `KaTeX_Size3` font unless `assetsInlineLimit` is set: see the CSP guide's KaTeX fonts section), and a
437
+ browser fetches one only when a formula needs it. Serving `dist/material-vue.css`
438
+ without `dist/fonts/` makes math fall back to the system fonts
439
+ ([troubleshooting](../troubleshooting.md#math-renders-in-the-system-fonts)).
440
+
414
441
  Tokens can be overridden the same way, after the import. What you cannot do
415
442
  without Tailwind is use the token utilities (`bg-primary`…) in your own
416
443
  markup beyond those the library already uses.
package/docs/roadmap.md CHANGED
@@ -6,6 +6,21 @@ version they shipped in, and planned entries may name the major they target.
6
6
 
7
7
  ## Now
8
8
 
9
+ - **A strict Content Security Policy** — a page that sends `script-src` with
10
+ a nonce and no `'unsafe-eval'`, and `style-src-elem` with a nonce and no
11
+ `'unsafe-inline'`, runs every component with no violation report: a
12
+ `NonceProvider` forwards the nonce to each component, the CSS the
13
+ dependencies inject moves into the stylesheet, zod becomes external with
14
+ schemas built lazily, and the KaTeX fonts ship as files. Done so far:
15
+ KaTeX's fonts are files beside the compiled stylesheet instead of 1.4 MB of
16
+ `data:` URIs inside it, so `font-src 'self'` needs no `data:`;
17
+ `NonceProvider` takes the nonce to reka's `Select`, `Combobox`,
18
+ `ScrollArea` and `Splitter`, `InputOTP` and `RichTextEditor`;
19
+ `ImageCropper` is measured and injects no `<style>`, and `InputOTP`'s rules
20
+ are in the stylesheet too; zod is no longer bundled and no schema is built
21
+ at import, so the app's `z.config({ jitless: true })` keeps zod from
22
+ probing `new Function`. Today the
23
+ [Content Security Policy guide](guides/csp.md) lists what is left to relax.
9
24
  - **Empty and error states that look designed** — `EmptyState` and a new
10
25
  `ErrorState` in three sizes (`default`, `compact`, `inline`), with in-house
11
26
  inline SVG illustrations or a slot for your own. `ErrorState` is a
@@ -84,6 +99,8 @@ version they shipped in, and planned entries may name the major they target.
84
99
  `@nxgt/material` (removed there in 3.0): single rows bound to a vee-validate
85
100
  array that no component of this package renders. Use `ContactsFormField` and
86
101
  `OpeningHoursFormField`, which own the list, the dialog and the array.
102
+ The `OpeningHoursSchema` and `schema` constants go as well: call
103
+ `openingHoursSchema()` and `attributeFormSchema()`.
87
104
  The `Switch` row's misspelt `data-filed` attribute goes too: select
88
105
  `[data-checked="true"]` instead. So do `PdfReader`'s `onPageChange`,
89
106
  `onLoadSuccess` and `onLoadError` props: listen with `@page-change`,