@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.
- package/CHANGELOG.md +68 -0
- package/README.md +19 -6
- package/dist/{camera-DlWK0PLP.js → camera-C52IuvMH.js} +1 -1
- package/dist/components/components.css +3 -1
- package/dist/components/ui/fields/otp-field/input-otp.css +48 -0
- package/dist/{components-Dqg7KwA2.js → components-DFEmacXJ.js} +12895 -16816
- package/dist/components.js +8 -8
- package/dist/composables.js +24 -24
- package/dist/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
- package/dist/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/dist/{form-scope-btEYYnGW.js → form-scope-CaYs-o2w.js} +1 -1
- package/dist/lib/components/ui/fields/attributes/form-schema.d.ts +11 -2
- package/dist/lib/components/ui/fields/opening-hours/consts.d.ts +12 -1
- package/dist/lib/components/ui/primitives/index.d.ts +1 -0
- package/dist/lib/components/ui/primitives/nonce-provider/index.d.ts +2 -0
- package/dist/lib/components/ui/primitives/nonce-provider/nonce-provider.vue.d.ts +20 -0
- package/dist/lib/components/ui/primitives/typography/typography.vue.d.ts +7 -4
- package/dist/lib/composables/index.d.ts +1 -0
- package/dist/lib/composables/use-csp-nonce.d.ts +18 -0
- package/dist/lib/lib/index.d.ts +1 -0
- package/dist/lib/nuxt/auto-imports.d.ts +1 -1
- package/dist/lib/nuxt/component-names.d.ts +1 -1
- package/dist/lib/vite/index.d.ts +3 -2
- package/dist/lib.js +49 -48
- package/dist/main.js +17 -17
- package/dist/material-vue.css +1 -1
- package/dist/{number.utils-Cy1z6qI7.js → number.utils-Bpo9tUaJ.js} +20 -20
- package/dist/{pdf-outline-sidebar-DchMD-_k.js → pdf-outline-sidebar-C_iJ8gqD.js} +1 -1
- package/dist/{pdf-page-D-kDQq5h.js → pdf-page-DOiJlWQn.js} +1 -1
- package/dist/{pdf-page-list-Cta0S89W.js → pdf-page-list-BTmUd7Ga.js} +2 -2
- package/dist/{pdf-reader-BwWozoIQ.js → pdf-reader-CEMMLJnB.js} +48 -45
- package/dist/{pdf-sidebar-B0Addf1P.js → pdf-sidebar-KLd76E5S.js} +2 -2
- package/dist/{pdf-thumbnail-sidebar-Dsptq4fb.js → pdf-thumbnail-sidebar-B4JkT_LV.js} +2 -2
- package/dist/{print-format.utils-DwJfU1tK.js → print-format.utils-DpBtUJvL.js} +1 -1
- package/dist/{stx-player-Bpnir1KO.js → stx-player-B6-PSgzP.js} +13 -13
- package/dist/{use-heading-spy-Czi_Xv3u.js → use-heading-spy-ClhUg50z.js} +54 -46
- package/dist/vite.js +2 -2
- package/docs/README.md +1 -0
- package/docs/components/data-display.md +5 -0
- package/docs/components/date-and-time.md +6 -0
- package/docs/components/feedback-and-status.md +3 -1
- package/docs/components/form-fields.md +23 -0
- package/docs/components/primitives.md +51 -2
- package/docs/getting-started.md +5 -1
- package/docs/guides/csp.md +420 -0
- package/docs/guides/helpers.md +2 -0
- package/docs/guides/i18n.md +2 -0
- package/docs/guides/theming.md +27 -0
- package/docs/roadmap.md +17 -0
- package/docs/troubleshooting.md +161 -0
- package/lib/components/components.css +3 -1
- package/lib/components/ui/fields/otp-field/input-otp.css +48 -0
- package/lib/nuxt/auto-imports.ts +1 -0
- package/lib/nuxt/component-names.ts +1 -0
- package/lib/vite/index.ts +6 -2
- 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).
|
package/docs/guides/helpers.md
CHANGED
|
@@ -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). |
|
package/docs/guides/i18n.md
CHANGED
|
@@ -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
|
package/docs/guides/theming.md
CHANGED
|
@@ -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`,
|