@nxgt/material 2.13.0 → 2.14.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 (96) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +18 -5
  3. package/dist/{blockDiagram-BEXU5L5S-CQaNV8rX.js → blockDiagram-BEXU5L5S-Cn9cG3vt.js} +1 -1
  4. package/dist/{chunk-3FUC2YCW-DmtO_Y9L.js → chunk-3FUC2YCW-XrT5pZo5.js} +1 -1
  5. package/dist/{chunk-7M6MHVWA-BQ0cJz5u.js → chunk-7M6MHVWA-Uf-8NCRF.js} +1 -1
  6. package/dist/{chunk-GNY47TPC-DiVMvIit.js → chunk-GNY47TPC-CGgBmY0O.js} +4 -4
  7. package/dist/{chunk-NETBCI7D-B5Av2fJk.js → chunk-NETBCI7D-g-jPsM9q.js} +1 -1
  8. package/dist/{chunk-Z7XXMR3K-D4uKosJW.js → chunk-Z7XXMR3K-BzO1efzA.js} +1 -1
  9. package/dist/{classDiagram-v2-NBCMYWYE-BYktffst.js → classDiagram-v2-NBCMYWYE-7WgpfVcn.js} +2 -2
  10. package/dist/components/components.css +13 -2
  11. package/dist/components/ui/feedback/spinner/spinner.css +28 -0
  12. package/dist/components/ui/fields/otp-field/input-otp.css +48 -0
  13. package/dist/components/ui/media/pdf-viewer/pdf-viewer.css +24 -0
  14. package/dist/components/ui/overlays/drawer/vaul-drawer.css +199 -0
  15. package/dist/components/ui/overlays/drawer/vaul-keyframes.css +95 -0
  16. package/dist/{components-C-y3kTRm.js → components-qTUjhFuo.js} +116137 -127138
  17. package/dist/components.js +6 -6
  18. package/dist/{dagre-6A5THRUB-BvQAvoOX.js → dagre-6A5THRUB-BOuRxJdA.js} +3 -3
  19. package/dist/{diagram-22UHCM2B-BYd0lEdS.js → diagram-22UHCM2B-CF0vLJbn.js} +2 -2
  20. package/dist/{diagram-3UASUU5V-B3ndNq2p.js → diagram-3UASUU5V-RLo-MTfV.js} +1 -1
  21. package/dist/{elk-276RUBZZ-BxXn1nvc.js → elk-276RUBZZ-Dr2yBM7k.js} +3 -3
  22. package/dist/{erDiagram-OPXOYQCR-DjYy4h_r.js → erDiagram-OPXOYQCR-l7kPZ4FO.js} +2 -2
  23. package/dist/{field-blur-V8o79CDN.js → field-blur-BKGVq9_k.js} +865 -870
  24. package/dist/{flowDiagram-KWPJA3E3-BD7xvcMh.js → flowDiagram-KWPJA3E3-BQEK3XTl.js} +3 -3
  25. package/dist/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  26. package/dist/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  27. package/dist/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  28. package/dist/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  29. package/dist/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  30. package/dist/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  31. package/dist/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  32. package/dist/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  33. package/dist/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  34. package/dist/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  35. package/dist/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  36. package/dist/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  37. package/dist/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  38. package/dist/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  39. package/dist/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  40. package/dist/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  41. package/dist/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  42. package/dist/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  43. package/dist/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  44. package/dist/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  45. package/dist/{ganttDiagram-FUAMR5RP-BAuk7X6i.js → ganttDiagram-FUAMR5RP-WYm8XV7U.js} +1 -1
  46. package/dist/hooks.js +25 -25
  47. package/dist/{journeyDiagram-ZHPQQLJL-h2S56BMC.js → journeyDiagram-ZHPQQLJL-p90NnenY.js} +1 -1
  48. package/dist/lib/components/ui/fields/attributes/attribute-form/consts.d.ts +5 -1
  49. package/dist/lib/components/ui/fields/attributes/attribute-form/types.d.ts +2 -2
  50. package/dist/lib/components/ui/fields/opening-hours/consts.d.ts +12 -1
  51. package/dist/lib/components/ui/media/image-cropper/image-crop-aspects.d.ts +6 -0
  52. package/dist/lib/components/ui/media/image-cropper/use-frame-size.d.ts +4 -0
  53. package/dist/lib/components/ui/primitives/index.d.ts +1 -0
  54. package/dist/lib/components/ui/primitives/nonce-provider/index.d.ts +1 -0
  55. package/dist/lib/components/ui/primitives/nonce-provider/nonce-provider.d.ts +20 -0
  56. package/dist/lib/components/ui/widgets/rich-text-editor/extensions/extensions.d.ts +2 -2
  57. package/dist/lib/components/ui/widgets/rich-text-editor/use-rich-text-editor.d.ts +17 -0
  58. package/dist/lib/hooks/index.d.ts +1 -0
  59. package/dist/lib/hooks/use-csp-nonce.d.ts +16 -0
  60. package/dist/lib/lib/index.d.ts +1 -0
  61. package/dist/lib/vite/index.d.ts +3 -2
  62. package/dist/lib.js +30 -29
  63. package/dist/main.js +16 -16
  64. package/dist/map.js +1 -1
  65. package/dist/material.css +25 -1
  66. package/dist/{mermaid.core-BWF4Tq7X.js → mermaid.core-CW8DO1T1.js} +21 -21
  67. package/dist/{mindmap-definition-NLK3R4M7-Q-4J7KMu.js → mindmap-definition-NLK3R4M7-DJcCg9PL.js} +2 -2
  68. package/dist/{pieDiagram-5QR66LMP-BIxQ164y.js → pieDiagram-5QR66LMP-b_dHXzkV.js} +1 -1
  69. package/dist/{quadrantDiagram-O4NWA36T-BlI0otnv.js → quadrantDiagram-O4NWA36T-BSqAcBs7.js} +1 -1
  70. package/dist/{requirementDiagram-PLB6GJNP-CuVPUh0S.js → requirementDiagram-PLB6GJNP-BcxkmIFp.js} +2 -2
  71. package/dist/{sankeyDiagram-IPEJSGJF-Cke5i_9w.js → sankeyDiagram-IPEJSGJF-aD5r_n1q.js} +1 -1
  72. package/dist/{stateDiagram-v2-GCMORJYK-CT53U-pL.js → stateDiagram-v2-GCMORJYK-Ct9ylaoP.js} +2 -2
  73. package/dist/{swimlanes-2SLR337P-BwEtm09G.js → swimlanes-2SLR337P-BwCqwEoR.js} +3 -3
  74. package/dist/{swimlanesDiagram-TC7HE7FX-36JXV6X2.js → swimlanesDiagram-TC7HE7FX-WxBGUt0w.js} +3 -3
  75. package/dist/{timeline-definition-EJHVYXUP-BeX-S1oM.js → timeline-definition-EJHVYXUP-5Q8GU13Q.js} +1 -1
  76. package/dist/{use-heading-spy--7KHY3lw.js → use-heading-spy-wlOv87q3.js} +63 -57
  77. package/dist/{usecaseDiagram-POWQR4AR-Dfd0rXLr.js → usecaseDiagram-POWQR4AR-CzGOd4ic.js} +2 -2
  78. package/dist/vite.js +2 -2
  79. package/dist/{xychartDiagram-PMCCYNJV-D0UPPflL.js → xychartDiagram-PMCCYNJV-Bh_1zZoW.js} +1 -1
  80. package/docs/README.md +1 -0
  81. package/docs/components/data-display.md +5 -0
  82. package/docs/components/form-fields.md +14 -0
  83. package/docs/components/primitives.md +31 -0
  84. package/docs/getting-started.md +5 -1
  85. package/docs/guides/csp.md +408 -0
  86. package/docs/guides/helpers.md +2 -0
  87. package/docs/guides/theming.md +27 -0
  88. package/docs/roadmap.md +17 -1
  89. package/docs/troubleshooting.md +139 -0
  90. package/lib/components/components.css +13 -2
  91. package/lib/components/ui/feedback/spinner/spinner.css +28 -0
  92. package/lib/components/ui/fields/otp-field/input-otp.css +48 -0
  93. package/lib/components/ui/media/pdf-viewer/pdf-viewer.css +24 -0
  94. package/lib/components/ui/overlays/drawer/vaul-drawer.css +199 -0
  95. package/lib/components/ui/overlays/drawer/vaul-keyframes.css +95 -0
  96. package/package.json +4 -4
@@ -27,6 +27,7 @@ other pages are built from these.
27
27
  | <img src="../previews/primitives-toggle.webp" width="96" alt=""> | [Toggle](#toggle) | Pressable on / off button |
28
28
  | <img src="../previews/primitives-togglegroup.webp" width="96" alt=""> | [ToggleGroup](#togglegroup) | Single or multiple toggle group |
29
29
  | | [Activity](#activity) | React `<Activity>` switched by a boolean |
30
+ | | [NonceProvider](#nonceprovider) | Gives the page's CSP nonce to every component below |
30
31
 
31
32
  ## Accordion
32
33
 
@@ -773,3 +774,33 @@ import { Activity } from '@nxgt/material/components';
773
774
  | `visible` | `boolean` | `false` | `true` renders with `mode="visible"`; `false` hides the subtree and keeps its state |
774
775
 
775
776
  **Storybook:** — · **Vue:** `Activity`
777
+
778
+ ## NonceProvider
779
+
780
+ No preview: it draws nothing of its own, it hands the page's Content Security
781
+ Policy nonce down.
782
+
783
+ Gives this request's CSP nonce to every component below it: the `<style>`
784
+ elements of `ScrollArea` and `Select`, the stylesheet tiptap injects for
785
+ `RichTextEditor`, and, on the client, the scroll lock of every modal. Render it once, at the root, on the server and on
786
+ the client, with the same nonce.
787
+
788
+ ```tsx
789
+ import { NonceProvider } from '@nxgt/material/components';
790
+
791
+ <NonceProvider nonce={nonce}>
792
+ <App />
793
+ </NonceProvider>;
794
+ ```
795
+
796
+ | Prop | Type | Default | Effect |
797
+ | --- | --- | --- | --- |
798
+ | `nonce` | `string \| undefined` | — | the nonce of the page's `style-src-elem 'nonce-…'`; `undefined` or `''` inherits the nearest outer `NonceProvider`'s nonce (none at the root) |
799
+
800
+ `useCspNonce()` (`@nxgt/material/hooks`) reads it in a component of your own;
801
+ `getCspNonce()` and `setCspNonce()` (`@nxgt/material/lib`) read and keep the
802
+ page's nonce outside React. The server entry, the client entry and what each
803
+ component needs are in the [Content Security Policy
804
+ guide](../guides/csp.md#nonceprovider).
805
+
806
+ **Storybook:** `Primitives/NonceProvider` · **Vue:** `NonceProvider`
@@ -30,7 +30,9 @@ does not import that entry never has them in its tree.
30
30
  | `@nxgt/material/print` | `bun add @react-pdf/renderer@^4.9 react-pdf-tailwind` ([printing](guides/printing.md)) |
31
31
  | `@nxgt/material/map` | `bun add leaflet react-leaflet @react-leaflet/core @adamscybot/react-leaflet-component-marker react-geolocated` ([maps](guides/maps.md)) |
32
32
 
33
- The tarball contains the built `dist/`, `lib/styles.css`, the component
33
+ The tarball contains the built `dist/` (with KaTeX's fonts in
34
+ `dist/fonts/katex/`, which the compiled stylesheet loads),
35
+ `lib/styles.css`, the component
34
36
  stylesheets it imports (`lib/components/**/*.css`), `lib/map/map.css`,
35
37
  these docs and `CHANGELOG.md` (from the release after 2.6.1). There is no
36
38
  `postinstall` build step.
@@ -45,6 +47,8 @@ In the app's root stylesheet, in this order:
45
47
  @source "../node_modules/@nxgt/material/";
46
48
  ```
47
49
 
50
+ Toaster, Drawer and InputOTP take their CSS from this stylesheet: without it they render unstyled.
51
+
48
52
  - **`styles.css` is source, not compiled CSS.** It holds the Tailwind v4
49
53
  `@theme` layer (colours, `--radius`, the `dark` variant, base styles), so it
50
54
  runs through *your* Tailwind build. Nothing injects it: import it
@@ -0,0 +1,408 @@
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` 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/react` 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/react';
51
+
52
+ loader.config({ paths: { vs: '/monaco/vs' } });
53
+ ```
54
+
55
+ ### Packages the app imports itself
56
+
57
+ `zod` and `@monaco-editor/react` are dependencies of the library, and
58
+ neither is bundled: the library imports them at runtime. A strict package
59
+ manager (pnpm) does not let the app import a package it does not list, so
60
+ install what you import: `bun add zod @monaco-editor/react`. Keep your zod
61
+ inside `^4.4.3` (zod 4.4 or later shares its configuration through
62
+ `globalThis`) and the app and the library share one copy.
63
+
64
+ ## NonceProvider
65
+
66
+ Render `NonceProvider` once, at the root, with this request's nonce, on the
67
+ server and on the client. Every component below it takes the nonce: the
68
+ `<style>` elements of `ScrollArea` and `Select`, the stylesheet tiptap
69
+ injects for `RichTextEditor` (its `injectNonce` option, unless you pass your
70
+ own), and, on the client, the scroll lock of every modal.
71
+
72
+ ```tsx
73
+ // app.tsx
74
+ import { NonceProvider } from '@nxgt/material';
75
+ import type { ReactNode } from 'react';
76
+
77
+ export function App({ nonce, children }: { nonce?: string; children: ReactNode }) {
78
+ return <NonceProvider nonce={nonce}>{children}</NonceProvider>;
79
+ }
80
+ ```
81
+
82
+ On the server, the nonce goes down through React context only. On the
83
+ client, `NonceProvider` also keeps it for the code that runs outside React
84
+ (`claimCspNonce`, in an insertion effect, so never on a server: one
85
+ request's nonce never reaches another's). With nested providers, the
86
+ innermost one mounted is the page's nonce, and it falls back to the outer
87
+ one's when it unmounts. The server entry is under [SSR](#ssr). On the
88
+ client, read the nonce the server wrote into the page and render the same
89
+ tree:
90
+
91
+ ```tsx
92
+ // entry.client.tsx
93
+ import { getCspNonce } from '@nxgt/material';
94
+ import { hydrateRoot } from 'react-dom/client';
95
+ import { App } from './app';
96
+ import { Routes } from './routes';
97
+
98
+ hydrateRoot(document, <App nonce={getCspNonce()}><Routes /></App>);
99
+ ```
100
+
101
+ `getCspNonce()` reads, in order: the value given to `setCspNonce`,
102
+ `<meta property="csp-nonce" nonce="…">` (what Vite's `html.cspNonce` writes)
103
+ or `<meta name="csp-nonce" content="…">`, the first `<script nonce>` or
104
+ `<style nonce>` of the page, then `__webpack_nonce__`. A browser hides a
105
+ `nonce` attribute's value once the page is parsed; `getCspNonce()` reads the
106
+ `nonce` property, which keeps it.
107
+
108
+ A component of your own that renders a `<style>` takes the nonce with
109
+ `useCspNonce()`:
110
+
111
+ ```tsx
112
+ import { useCspNonce } from '@nxgt/material/hooks';
113
+
114
+ export function Keyframes({ css }: { css: string }) {
115
+ return <style nonce={useCspNonce()}>{css}</style>;
116
+ }
117
+ ```
118
+
119
+ `useCspNonce()` returns the provider's nonce, or `undefined` outside one. It
120
+ never reads the page while rendering, so the server and the client render the
121
+ same attribute and hydration does not mismatch. Code that runs in an effect
122
+ (an imperative widget creating its own `<style>`) can call `getCspNonce()`
123
+ instead.
124
+
125
+ ## `<style>` elements the components inject
126
+
127
+ `style-src-elem 'self' 'nonce-X'` is enough for every component below: your
128
+ stylesheet carries the CSS the dependencies used to inject, and under a
129
+ `NonceProvider` the few `<style>` elements left carry the nonce. No route
130
+ needs `'unsafe-inline'` in `style-src-elem` for them; mermaid and pdf.js's
131
+ font fallback are the [known gaps](#known-gaps).
132
+
133
+ | Component | What it needs | How |
134
+ | --- | --- | --- |
135
+ | `Toaster` (sonner), `Drawer` (vaul), `InputOTP` (input-otp) | nothing | their CSS is in the stylesheet you import (`styles.css`, or `dist/material.css`); the library build removes the `<style>` they would inject |
136
+ | `ImageCropper` (react-easy-crop) | nothing | its CSS is in the stylesheet; it renders with `disableAutomaticStylesInjection` |
137
+ | `Spinner` (variant `4`), `PdfReader` (search highlights, night mode) | nothing | their rules are in the stylesheet (`spinner.css`, `pdf-viewer.css`) |
138
+ | `ScrollArea`, `Select` (Radix) | the nonce | Radix renders a `<style>` element with `NonceProvider`'s nonce: wrap the app in [`NonceProvider`](#nonceprovider) |
139
+ | `RichTextEditor` (tiptap) | the nonce | tiptap, on mount, with `NonceProvider`'s nonce as `injectNonce` |
140
+ | `Dialog`, `AlertDialog`, `Sheet`, `ConfirmationDialog`, … (Radix scroll lock) | the nonce | react-style-singleton, when a modal opens: see below |
141
+
142
+ Your app must import the library's stylesheet for the first three rows, as
143
+ the [README](../../README.md) already asks: `@import
144
+ "@nxgt/material/styles.css"` in a Tailwind app, or `dist/material.css`. An
145
+ app that imports only the JavaScript renders toasts and drawers unstyled.
146
+
147
+ The library's CI checks this on every build: it fails when the built
148
+ JavaScript creates a `<style>`, or calls `insertRule`, `Function` or `eval`,
149
+ outside a short list, each entry with its reason (the scroll lock, tiptap,
150
+ the gaps below). It then imports every built entry and fails if importing
151
+ calls `Function`: a schema built at import would. A story renders a
152
+ `Toaster` with a toast, an open `Drawer` and `Dialog`, an `InputOTP`,
153
+ `Spinner` and `ImageCropper` under this policy and counts the violations:
154
+ none. Another adds `script-src 'self' 'nonce-X'`, no `'unsafe-eval'`, calls
155
+ `z.config({ jitless: true })`, then renders `AttributeForm`, submits it and
156
+ parses with `openingHoursSchema()`: no `script-src` violation.
157
+
158
+ ### Allowed by the check, nothing for you to do
159
+
160
+ Code that stays in the build and the CSP never sees, or only in a case the
161
+ library avoids:
162
+
163
+ | Where | What it is | Why it is no violation |
164
+ | --- | --- | --- |
165
+ | `Resizable` (react-resizable-panels) | `insertRule` | on a constructed stylesheet (`adoptedStyleSheets`), which CSP does not govern |
166
+ | `ImageCropper` (react-easy-crop) | `createElement('style')` | the branch is skipped: `disableAutomaticStylesInjection` is passed |
167
+ | Nivo charts (react-virtualized-auto-sizer) | `<style>` of a resize detector | only in a browser without `ResizeObserver`, which no supported browser is |
168
+ | `PdfReader` (pdf.js) | the `isEvalSupported` probe, `new Function("")` | unreachable: `PdfReader` passes `isEvalSupported: false`. If you call `pdfjs.getDocument` yourself, pass it too |
169
+ | lodash (a dependency of some chart code) | `Function("return this")()` | only when neither `global` nor `self` exists: never in a browser |
170
+ | `InputOTP` (input-otp) | a `<style>` inside `<noscript>` | rendered only with scripts off, where no CSP report is sent |
171
+
172
+ ### The scroll lock of modals
173
+
174
+ Radix locks the page scroll with `react-style-singleton`, which adds a
175
+ `<style>` to `<head>` when a modal opens and takes its nonce from
176
+ `get-nonce`. The library bundles it pointed at its own nonce store
177
+ (`@nxgt/material-core/csp-nonce`), so it reads what `NonceProvider` keeps,
178
+ else what `getCspNonce()` finds in the page (the meta tag, a
179
+ `<script nonce>`). Nothing else to set: the `__webpack_nonce__` assignment
180
+ earlier versions of this guide asked for is no longer needed.
181
+
182
+ A page without a `NonceProvider` (one that renders no `ScrollArea`, `Select`
183
+ or `RichTextEditor`) needs nothing either, as long as the server writes the
184
+ nonce into the page (the meta tag, or its `<script nonce>`): the scroll lock
185
+ reads it through `getCspNonce()`. Call `setCspNonce(nonce)` on the client only
186
+ when the page carries it nowhere. `setCspNonce` does nothing on a server (no
187
+ `document`), so a request's nonce never lands in a module global there: see
188
+ [SSR](#ssr).
189
+
190
+ ### Known gaps
191
+
192
+ Not fixable by configuration yet, and still reported by a strict policy:
193
+
194
+ - the `<style>` element inside mermaid's SVG, and the one cytoscape adds to
195
+ `<head>` for mermaid's mindmap and architecture diagrams;
196
+ - the font fallback of pdf.js, when a PDF font is not embedded;
197
+ - this repository's Storybook, or an app that imports react-use's barrel
198
+ itself on a dev server: react-use served as one module, whose `useCss`
199
+ makes nano-css create three `<style>` elements at import. Our `dist`
200
+ bundles the hooks it uses and never imports react-use or nano-css, so
201
+ an app that only uses `@nxgt/material` does not report it.
202
+
203
+ ## zod and `'unsafe-eval'`
204
+
205
+ Building a `z.object()` makes zod probe `new Function` once, to choose
206
+ between its compiled parser and its interpreter. Without `'unsafe-eval'` the
207
+ browser reports a violation. The error is caught, so nothing breaks, but the
208
+ report is sent. Switch zod to its interpreter **before the first schema is
209
+ built**, in a module the app imports first:
210
+
211
+ ```ts
212
+ // zod-config.ts
213
+ import { z } from 'zod';
214
+
215
+ z.config({ jitless: true });
216
+ ```
217
+
218
+ ```ts
219
+ // entry.client.tsx
220
+ import './zod-config'; // first
221
+ import './app';
222
+ ```
223
+
224
+ The library builds no schema while it loads, so that is all it takes:
225
+
226
+ - **zod is not bundled.** `@nxgt/material` imports the zod your app has (a
227
+ dependency, `^4.4.3`), so your `z.config` is the one its schemas see. zod
228
+ 4.4 or later shares its configuration through `globalThis`, so a
229
+ second copy of zod 4 that a package manager nested still sees it; that is
230
+ why the range is `^4.4.3`.
231
+ - **Its schemas are factories**, built on the first call and the same schema
232
+ after: `openingHoursSchema()`, and the attribute form's, which
233
+ `AttributeForm` builds when it renders. A component that validates builds
234
+ its schema when it first renders, never at import.
235
+
236
+ | Deprecated constant (removed in 3.0) | Use |
237
+ | --- | --- |
238
+ | `OpeningHoursSchema` | `openingHoursSchema()` |
239
+
240
+ The deprecated constants still work: each is built on its first use and
241
+ behaves as the schema its factory returns (`.parse`, `.safeParse`, `.shape`,
242
+ `.extend`, `instanceof z.ZodObject`). It is not the same object, so do not
243
+ compare it with `===` or register it: a `.meta()` or `registry.add()` on
244
+ the stand-in is not seen through the factory's schema.
245
+
246
+ ## KaTeX fonts
247
+
248
+ `Markdown` renders formulas with KaTeX, which needs its fonts
249
+ (`KaTeX_*.woff2`). They are files on both routes, so `font-src 'self'` allows
250
+ them, and a browser downloads a font only when a formula draws a glyph from
251
+ it: a page with no math downloads none.
252
+
253
+ - **Compiled route** (`dist/material.css`): the `@font-face` rules point at
254
+ `dist/fonts/katex/*.woff2`, shipped beside the stylesheet, through a
255
+ relative `url()`. A bundler emits them as files of your build; an app that
256
+ serves `dist/` as it is must serve `dist/fonts/` with it. Copy only the
257
+ stylesheet and math falls back to the system fonts
258
+ ([troubleshooting](../troubleshooting.md#math-renders-in-the-system-fonts)).
259
+ Earlier versions inlined the 60 fonts as `data:` URIs (1.4 MB of the
260
+ stylesheet), and `font-src` needed `data:`.
261
+ - **Tailwind route** (`@import "@nxgt/material/styles.css"`): your bundler emits
262
+ KaTeX's fonts as files, but Vite inlines any asset under
263
+ `assetsInlineLimit` (4096 bytes) as a `data:` URI, and
264
+ `KaTeX_Size3-Regular.woff2` is 3.6 KB, so it is inlined and `font-src 'self'`
265
+ blocks it. Never inline fonts, with `cspAssetsInlineLimit` from
266
+ `@nxgt/material/vite` (the same helper as [Inter](#inter--the-sans-font)):
267
+
268
+ ```ts
269
+ // vite.config.ts
270
+ import { cspAssetsInlineLimit } from '@nxgt/material/vite';
271
+ import { defineConfig } from 'vite';
272
+
273
+ export default defineConfig({ build: { assetsInlineLimit: cspAssetsInlineLimit } });
274
+ ```
275
+
276
+ It returns `false` for a font and leaves every other asset to Vite's
277
+ default. (`assetsInlineLimit: 0` also works, and inlines nothing at all.)
278
+ The compiled route through Vite needs the same setting, for the same file.
279
+
280
+ The library's CI fails when a stylesheet in `dist/` carries a `data:` font.
281
+
282
+ ## Inter / the sans font
283
+
284
+ The library ships no web font. Its sans text uses the Tailwind token
285
+ `--font-sans`, which the theme does not set, so it is Tailwind's system stack
286
+ (`ui-sans-serif, system-ui, sans-serif`, then the emoji fonts). Many apps name
287
+ Inter there and load it from Google Fonts or rsms.me; both are blocked by
288
+ `font-src 'self'` and `style-src-elem 'self' 'nonce-X'`, because the page then
289
+ asks for another origin's stylesheet and fonts. Host Inter yourself instead:
290
+
291
+ ```sh
292
+ bun add @fontsource-variable/inter
293
+ ```
294
+
295
+ ```css
296
+ /* the app's root stylesheet */
297
+ @import "tailwindcss";
298
+ @import "@nxgt/material/styles.css";
299
+ @import "@fontsource-variable/inter";
300
+ @source "../node_modules/@nxgt/material/";
301
+
302
+ @theme {
303
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
304
+ }
305
+ ```
306
+
307
+ (`@fontsource-variable/inter` is the variable font, family `Inter Variable`.
308
+ For fixed weights install `@fontsource/inter` instead, import
309
+ `@fontsource/inter/400.css` and the weights you use, and name the family
310
+ `"Inter"`.) A JavaScript entry works as well: `import '@fontsource-variable/inter';`.
311
+
312
+ Set it in the root `@theme`, like the colours: `@theme` entries are resolved
313
+ on the root, so a value set on an inner element does not change what the
314
+ rest of the page inherits. See [theming](theming.md#fonts).
315
+
316
+ - **Under the policy.** The package's stylesheet is bundled into yours (no
317
+ second origin, so `style-src-elem 'self'` holds) and its `@font-face` rules
318
+ point at `.woff2` files that your bundler emits into the build. They load
319
+ from your own origin, so `font-src 'self'` allows them. A browser downloads
320
+ only the subsets a page draws: Latin first, the other scripts on demand.
321
+ - **Vite and `assetsInlineLimit`.** Vite inlines an asset under 4096 bytes as
322
+ a `data:` URI, which `font-src 'self'` blocks. Fontsource's files are
323
+ larger, so they are emitted as files without any setting. A small subset
324
+ file can still fall under the limit, and the KaTeX route needs it anyway:
325
+ keep the one setting that never inlines a font, `cspAssetsInlineLimit`
326
+ from `@nxgt/material/vite`:
327
+
328
+ ```ts
329
+ // vite.config.ts
330
+ import { cspAssetsInlineLimit } from '@nxgt/material/vite';
331
+ import { defineConfig } from 'vite';
332
+
333
+ export default defineConfig({ build: { assetsInlineLimit: cspAssetsInlineLimit } });
334
+ ```
335
+
336
+ ([KaTeX fonts](#katex-fonts) uses the same setting.)
337
+ - **If Inter is not loaded.** Nothing breaks and no violation is reported: the
338
+ browser skips `"Inter Variable"`, which names no installed font, and draws
339
+ the next entry of the stack, the system sans. Only the look changes. This is
340
+ also what you get when you keep the token at its default.
341
+ - **Troubleshooting.** A request to `fonts.googleapis.com` or
342
+ `fonts.gstatic.com` is not ours: see
343
+ [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).
344
+
345
+ ## SSR
346
+
347
+ Generate the nonce **per request**, put it in the header and hand it to the
348
+ renderer. React's stream takes it as an option, and stamps the scripts it
349
+ emits:
350
+
351
+ ```tsx
352
+ // entry.server.tsx
353
+ import { randomBytes } from 'node:crypto';
354
+ import { NonceProvider } from '@nxgt/material';
355
+ import { renderToPipeableStream } from 'react-dom/server';
356
+ import { PassThrough } from 'node:stream';
357
+ import type { ReactNode } from 'react';
358
+
359
+ export function render(app: ReactNode, respond: (html: PassThrough, csp: string) => void) {
360
+ const nonce = randomBytes(16).toString('base64');
361
+ const csp = [
362
+ `script-src 'nonce-${nonce}' 'strict-dynamic'`,
363
+ `style-src-elem 'self' 'nonce-${nonce}'`,
364
+ `style-src-attr 'unsafe-inline'`,
365
+ `font-src 'self'`,
366
+ ].join('; ');
367
+
368
+ const body = new PassThrough();
369
+ const { pipe } = renderToPipeableStream(<NonceProvider nonce={nonce}>{app}</NonceProvider>, {
370
+ nonce,
371
+ onShellReady() {
372
+ respond(body, csp);
373
+ pipe(body);
374
+ },
375
+ });
376
+ }
377
+ ```
378
+
379
+ The stream stamps its own scripts with `nonce`; `NonceProvider` hands the same
380
+ value to the components. Also give the nonce to your `<script>` and
381
+ inline-style tags (`<Scripts nonce>` in React Router) and to a
382
+ `meta[name="csp-nonce"]` tag, which `getCspNonce()` reads on the client.
383
+
384
+ **Never keep the nonce in a module-level variable on the server.** Two
385
+ requests handled at once would overwrite each other, and one page would carry
386
+ the other's nonce. Pass it down the request: as an argument, through
387
+ `NonceProvider` (React context) or `AsyncLocalStorage`. `setCspNonce` is a
388
+ no-op there for that reason.
389
+
390
+ See also the [SSR guide](ssr.md).
391
+
392
+ ## Status
393
+
394
+ Nothing is left to ship for the policy. What was done:
395
+
396
+ - the CSS the dependencies injected at runtime (`Toaster`, `Drawer`,
397
+ `InputOTP`, `ImageCropper`) and the `<style>` of `Spinner` and `PdfReader`
398
+ are in the stylesheet;
399
+ - the few `<style>` elements left carry the nonce (`NonceProvider`);
400
+ - KaTeX's fonts ship as files, so `font-src 'self'` needs no `data:`, and
401
+ Inter is documented as a self-hosted font;
402
+ - zod is an external dependency whose schemas are built on first use (see
403
+ above).
404
+
405
+ What remains are the [known gaps](#known-gaps) and the rows marked "not yet
406
+ verified" in the tables above; each is waiting on a dependency, not on this
407
+ library. Progress is on the [roadmap](../roadmap.md). A report you cannot match
408
+ to an entry is in [troubleshooting](../troubleshooting.md#content-security-policy).
@@ -14,6 +14,7 @@ differences are listed under each table.
14
14
  | --- | --- |
15
15
  | `cn(...classes)` | `clsx` + `tailwind-merge`: joins conditional classes, last Tailwind utility wins. |
16
16
  | `clientOnly(Component)` | Wraps a component so it renders nothing on the server and mounts only on the client. See [SSR](ssr.md). |
17
+ | `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). |
17
18
  | `renderSlot(slot, context)` | Resolves a `ContentSlot`: a `ReactNode` or a `(context) => ReactNode`. |
18
19
  | `DATE_UTILS` | `format(iso, 'PP', locale?)` (`'-'` when empty), `parseFromTimestring('HH:mm')`, `parseFromISOString`, `formatDateRangeFilter`. |
19
20
  | `formatDateRangeFilter(range)` | Turns a day-picker range into `{ from, to }` ISO strings at start / end of day; `undefined` when empty. |
@@ -119,6 +120,7 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
119
120
  | `useTheme()` | `{ theme, preference, updateTheme }`: light / dark / system, stored in `localStorage`, applied as the `dark` class. See [theming](theming.md#usetheme). |
120
121
  | `resolveTheme(stored, prefersDark)`, `applyResolvedTheme(theme)` | The pure theme logic behind `useTheme`, for a boot script. |
121
122
  | `useIsClient()` | `false` on the server and first render, `true` after mount: a hydration guard. |
123
+ | `useCspNonce()` | The nonce of the nearest `NonceProvider`, for a `<style>` of your own (`<style nonce={useCspNonce()}>`); `undefined` outside one, on the server and on the client alike. See [CSP](csp.md#nonceprovider). |
122
124
  | `useIsMobile()` | `true` below 768 px (`ResponsiveButton` no longer uses it: it collapses with CSS). `false` on the server and while hydrating, so SSR HTML is the desktop layout; a phone switches right after. |
123
125
  | `useHeadingSpy(ids, onActive, options?)` | IntersectionObserver over heading ids; reports the topmost visible one. |
124
126
  | `useLocalizedDate()` | `{ language, locale, format }`: `DATE_UTILS.format` bound to the `I18nProvider` language (`en` without a provider). |
@@ -68,6 +68,27 @@ Some colours are derived rather than tokens of their own:
68
68
  `bg-primary-container`, `bg-secondary-container`, `bg-accent-container`,
69
69
  `bg-error-container`. They follow the token they are computed from.
70
70
 
71
+ ## Fonts
72
+
73
+ The library ships no font. Text uses Tailwind's `--font-sans`, which the theme
74
+ does not set, so the default is the system sans stack
75
+ (`ui-sans-serif, system-ui, sans-serif`, then the emoji fonts); there is
76
+ nothing to download. To use Inter, self-host it and set the token in the root
77
+ `@theme`:
78
+
79
+ ```css
80
+ @import "@fontsource-variable/inter";
81
+
82
+ @theme {
83
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
84
+ }
85
+ ```
86
+
87
+ If Inter is not loaded, the browser draws the next font of the stack. Loading it
88
+ from Google Fonts or rsms.me is blocked by a strict CSP: the install, the
89
+ bundler settings and the policy are in
90
+ [Inter / the sans font](csp.md#inter--the-sans-font).
91
+
71
92
  ## Overriding tokens
72
93
 
73
94
  Redefine the variables **after** the import, for light and dark separately:
@@ -374,6 +395,12 @@ other bundlers that support package `exports` in CSS. The file is not exported
374
395
  under a path of its own, so `import '@nxgt/material/dist/material.css'` does
375
396
  not resolve.
376
397
 
398
+ The compiled stylesheet loads KaTeX's fonts (`Markdown` maths) from
399
+ `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
400
+ browser fetches one only when a formula needs it. Serving `dist/material.css`
401
+ without `dist/fonts/` makes math fall back to the system fonts
402
+ ([troubleshooting](../troubleshooting.md#math-renders-in-the-system-fonts)).
403
+
377
404
  Tokens can be overridden the same way, after the import. What you cannot do
378
405
  without Tailwind is use the token utilities (`bg-primary`…) in your own
379
406
  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 `ScrollArea`, `Select`,
18
+ `RichTextEditor` and the scroll lock of modals; `Toaster`, `Drawer`,
19
+ `InputOTP`, `ImageCropper`, `Spinner` and `PdfReader` inject no `<style>`,
20
+ their CSS is in the stylesheet; zod is no longer bundled and no schema is
21
+ built 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,7 +99,8 @@ version they shipped in, and planned entries may name the major they target.
84
99
  `ConfirmationDialog` and the attribute editors, so an app without
85
100
  react-hook-form never installs it. `ContactItem` and `OpeningHoursItem`,
86
101
  deprecated in 2.x, are removed then: use `ContactsFormField` and
87
- `OpeningHoursFormField`.
102
+ `OpeningHoursFormField`. So is the `OpeningHoursSchema` constant: call
103
+ `openingHoursSchema()`.
88
104
  The `Switch` row's misspelt `data-filed` attribute goes too: select
89
105
  `[data-checked="true"]` instead. `DateField` / `DateRangeField` stop
90
106
  accepting a day rule as `disabled`: pass it as `disabledDays`.