@nxgt/material 3.6.0 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +46 -1
  3. package/dist/{button-1Fornjtn.js → button--cF4gY14.js} +1 -1
  4. package/dist/charts.js +1 -1
  5. package/dist/{code-editor-DeZYlIO2.js → code-editor-0CdGwKDg.js} +3 -3
  6. package/dist/code-editor.js +3 -3
  7. package/dist/{code-language-items-Bb7afEj-.js → code-language-items-CGARRbVm.js} +1 -1
  8. package/dist/components/animations/scroll.css +1 -1
  9. package/dist/components/base-content.css +1 -1
  10. package/dist/components/base.css +1 -1
  11. package/dist/components/components.css +2 -0
  12. package/dist/components/editor-field-expressive.css +5 -0
  13. package/dist/components/editor-prose.css +1 -1
  14. package/dist/components/theme-tokens.css +17 -4
  15. package/dist/components/ui/display/period-cards/period-cards.css +2 -2
  16. package/dist/components/ui/media/pdf-viewer/pdf-viewer.css +1 -1
  17. package/dist/components/ui/widgets/drawing/drawing-animate.css +123 -0
  18. package/dist/components/ui/widgets/drawing/drawing.css +459 -0
  19. package/dist/{components-BCYZgeoV.js → components-bYGPzyKd.js} +761 -761
  20. package/dist/components.js +18 -18
  21. package/dist/{dialog-JQT6iETD.js → dialog-DdJ1C4BR.js} +168 -168
  22. package/dist/dnd.js +1 -1
  23. package/dist/draw-animate.js +106 -0
  24. package/dist/draw-png.js +2 -0
  25. package/dist/draw-sketch.js +38 -0
  26. package/dist/draw.js +65 -0
  27. package/dist/drawing-B8VuG7nx.js +390 -0
  28. package/dist/drawing-context-CeKeZE6b.js +10 -0
  29. package/dist/{dropdown-menu-DiZvX4tG.js → dropdown-menu-DWtgUVW7.js} +1 -1
  30. package/dist/editor-viewer.js +1 -1
  31. package/dist/editor.js +120 -120
  32. package/dist/{empty-state-Cpvsmk0R.js → empty-state-CRWLQ1sM.js} +2 -2
  33. package/dist/{icon-badge-C30yKsAM.js → icon-badge-Bb5-73XP.js} +18 -18
  34. package/dist/{icon-button-BXlI3DJ_.js → icon-button-5A6Mrj6w.js} +60 -90
  35. package/dist/lib/components/ui/display/shape/use-shape-morph.d.ts +1 -0
  36. package/dist/lib/components/ui/widgets/drawing/drawing-a11y.d.ts +20 -0
  37. package/dist/lib/components/ui/widgets/drawing/drawing-context.d.ts +23 -0
  38. package/dist/lib/components/ui/widgets/drawing/drawing-types.d.ts +63 -0
  39. package/dist/lib/components/ui/widgets/drawing/drawing.d.ts +17 -0
  40. package/dist/lib/components/ui/widgets/drawing/index.d.ts +5 -0
  41. package/dist/lib/components/ui/widgets/drawing/partials/drawing-root.d.ts +17 -0
  42. package/dist/lib/components/ui/widgets/drawing/partials/interactive-node.d.ts +22 -0
  43. package/dist/lib/components/ui/widgets/drawing/partials/primitives/content.d.ts +20 -0
  44. package/dist/lib/components/ui/widgets/drawing/partials/primitives/path.d.ts +29 -0
  45. package/dist/lib/components/ui/widgets/drawing/partials/primitives/primitive.d.ts +19 -0
  46. package/dist/lib/components/ui/widgets/drawing/partials/primitives/shapes.d.ts +18 -0
  47. package/dist/lib/components/ui/widgets/drawing/partials/react-attrs.d.ts +5 -0
  48. package/dist/lib/components/ui/widgets/drawing/partials/ring.d.ts +9 -0
  49. package/dist/lib/components/ui/widgets/drawing/partials/svg-tree.d.ts +29 -0
  50. package/dist/lib/components/ui/widgets/drawing/use-exact-text.d.ts +16 -0
  51. package/dist/lib/draw/index.d.ts +32 -0
  52. package/dist/lib/draw-animate/index.d.ts +19 -0
  53. package/dist/lib/draw-animate/morph-drawing.d.ts +32 -0
  54. package/dist/lib/draw-animate/reduced-motion.d.ts +2 -0
  55. package/dist/lib/draw-animate/use-drawing-controls.d.ts +27 -0
  56. package/dist/lib/draw-animate/use-scene-morph.d.ts +15 -0
  57. package/dist/lib/draw-png/index.d.ts +11 -0
  58. package/dist/lib/draw-sketch/index.d.ts +17 -0
  59. package/dist/lib/draw-sketch/sketch.d.ts +19 -0
  60. package/dist/lib/print/components/print-drawing.d.ts +16 -0
  61. package/dist/lib/print/index.d.ts +2 -1
  62. package/dist/main.js +23 -23
  63. package/dist/map.js +63 -63
  64. package/dist/material.css +1 -1
  65. package/dist/pdf-reader.js +1 -1
  66. package/dist/print.js +123 -115
  67. package/dist/{public-Cde8_SN8.js → public-SsiazLEv.js} +743 -743
  68. package/dist/{render-json-BwJX1jZ2.js → render-json-DyFYFIkZ.js} +1 -1
  69. package/dist/{rich-code-block-yxMXBuGd.js → rich-code-block-CwNwQs5z.js} +3 -3
  70. package/dist/{rich-text-viewer-Bq-2vTVm.js → rich-text-viewer-LO1IZCWs.js} +1 -1
  71. package/dist/{sortable-provider-DYlpq8lP.js → sortable-provider-BvD98s8Y.js} +19 -19
  72. package/dist/{spinner-D-FE_mTW.js → spinner-CtGCPza-.js} +4 -4
  73. package/dist/styles.css +1 -0
  74. package/dist/{toolbar-items-DnkS3bDY.js → toolbar-items-DsDDD5AY.js} +1 -1
  75. package/dist/{use-chat-composer-BwpSgP_D.js → use-chat-composer-DZ01a16D.js} +923 -922
  76. package/dist/use-shape-morph-BKA_R5P5.js +35 -0
  77. package/docs/README.md +1 -0
  78. package/docs/components/rich-widgets.md +196 -0
  79. package/docs/guides/drawing.md +499 -0
  80. package/docs/guides/printing.md +47 -0
  81. package/docs/guides/theming.md +3 -2
  82. package/docs/previews/rich-widgets-drawing.webp +0 -0
  83. package/docs/roadmap.md +17 -0
  84. package/docs/troubleshooting.md +145 -1
  85. package/lib/components/animations/scroll.css +1 -1
  86. package/lib/components/base-content.css +1 -1
  87. package/lib/components/base.css +1 -1
  88. package/lib/components/components.css +2 -0
  89. package/lib/components/editor-field-expressive.css +5 -0
  90. package/lib/components/editor-prose.css +1 -1
  91. package/lib/components/theme-tokens.css +17 -4
  92. package/lib/components/ui/display/period-cards/period-cards.css +2 -2
  93. package/lib/components/ui/media/pdf-viewer/pdf-viewer.css +1 -1
  94. package/lib/components/ui/widgets/drawing/drawing-animate.css +123 -0
  95. package/lib/components/ui/widgets/drawing/drawing.css +459 -0
  96. package/lib/styles.css +1 -0
  97. package/package.json +22 -2
@@ -0,0 +1,499 @@
1
+ # Drawing
2
+
3
+ `@nxgt/material/draw` draws diagrams, badges, charts of your own and
4
+ illustrations from **code**: a scene of plain data, or JSX primitives, becomes
5
+ accessible SVG that follows the theme, is safe under a strict Content Security
6
+ Policy and is the same on the server and in the browser. The same scene also
7
+ becomes a standalone SVG string, a PNG, a PDF block, a hand-drawn sketch or an
8
+ animation.
9
+
10
+ This page is the React side: how to use it in an app. The scene model, the
11
+ path builder, arrows, text metrics, paints and the exact options of each
12
+ function are in the [reference of `@nxgt/material-core`](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md);
13
+ the component's props are in [Rich widgets](../components/rich-widgets.md#drawing).
14
+ Nothing is repeated between them: this guide links to a section when the detail
15
+ lives there.
16
+
17
+ - [Set up](#set-up)
18
+ - [A first drawing](#a-first-drawing)
19
+ - [On a server](#on-a-server)
20
+ - [Content Security Policy](#content-security-policy)
21
+ - [Colours: roles and tones](#colours-roles-and-tones)
22
+ - [Accessibility](#accessibility)
23
+ - [Events and focus](#events-and-focus)
24
+ - [Shapes and layout](#shapes-and-layout)
25
+ - [Sketch](#sketch)
26
+ - [Animate](#animate)
27
+ - [PNG](#png)
28
+ - [PDF](#pdf)
29
+ - [E-mail](#e-mail)
30
+ - [Troubleshooting](#troubleshooting)
31
+
32
+ ## Set up
33
+
34
+ Four entries, each only loaded by the apps that use it:
35
+
36
+ | Import | For |
37
+ | --- | --- |
38
+ | `@nxgt/material/draw` | `Drawing`, the JSX primitives, the builders (`scene`, `rect`, `path`, `text`...), `toSVG`, the shapes and the layout helpers |
39
+ | `@nxgt/material/draw/sketch` | the hand-drawn style: `Sketch`, `sketch` |
40
+ | `@nxgt/material/draw/animate` | `drawOn`, `appear`, `useDrawingControls`, `MorphDrawing`, `useSceneMorph`, `morphScenes` |
41
+ | `@nxgt/material/draw/png` | `drawingToPng`, `downloadDrawingPng` |
42
+
43
+ `Drawing` is not in `@nxgt/material/components` nor in the main entry: a page
44
+ that draws nothing pays nothing. The role colours and the animations are
45
+ classes of `@nxgt/material/styles.css`, which your app already imports for the
46
+ rest of the library (see [Getting started](../getting-started.md)); without it
47
+ a drawing is drawn in black.
48
+
49
+ `Drawing`, `Sketch` and the animation hooks use React hooks, and the built
50
+ entries (`dist/draw.js`, `dist/draw-sketch.js`, `dist/draw-animate.js`) carry
51
+ no `'use client'` directive: render them from a file of yours that starts with
52
+ `'use client'`. The builders, `sketch`, `drawOn`, `appear`, `morphScenes` and
53
+ `toSVG` are plain functions and run anywhere, a Server Component included.
54
+
55
+ ## A first drawing
56
+
57
+ JSX, for a drawing that belongs to one component:
58
+
59
+ ```tsx
60
+ 'use client';
61
+
62
+ import { Circle, Drawing, Rect, Text } from '@nxgt/material/draw';
63
+
64
+ export function ReleaseStatus() {
65
+ return (
66
+ <Drawing w={200} h={80} title="Release status: shipped">
67
+ <Rect x={0} y={0} w={200} h={80} r={16} fill="primary-container" />
68
+ <Circle cx={36} cy={40} r={14} fill="primary" />
69
+ <Text x={64} y={46} fill="on-primary-container">Shipped</Text>
70
+ </Drawing>
71
+ );
72
+ }
73
+ ```
74
+
75
+ A scene, for a drawing you compute, store or send as JSON:
76
+
77
+ ```tsx
78
+ 'use client';
79
+
80
+ import { Drawing, path, rect, scene, text, tone } from '@nxgt/material/draw';
81
+
82
+ const flow = scene({ w: 240, h: 120 }, [
83
+ rect({ x: 8, y: 8, w: 224, h: 104, r: 16, fill: 'primary-container', stroke: 'outline' }),
84
+ path((p) => p.moveTo(24, 90).curveTo(60, 20, 120, 20, 200, 60), {
85
+ stroke: tone('success'),
86
+ strokeWidth: 3,
87
+ arrow: { end: 'triangle' },
88
+ }),
89
+ text('Shipped', { x: 120, y: 64, anchor: 'middle', size: 18, weight: 600, fill: 'on-primary-container' }),
90
+ ]);
91
+
92
+ export const Flow = () => <Drawing scene={flow} title="Release flow" />;
93
+ ```
94
+
95
+ The `<svg>` scales to its container (`max-width: 100%`): size it with
96
+ `className`. Keep a scene in a module constant or a `useMemo`: a new object on
97
+ every render compiles again.
98
+
99
+ ## On a server
100
+
101
+ A scene is JSON, so a loader can build it and the component draws it, on the
102
+ server and again, identically, in the browser (no hydration mismatch: ids are
103
+ derived from `useId`, numbers are rounded the same way, wrapped text breaks with
104
+ Inter's metrics and is refined by the browser only after mount):
105
+
106
+ ```tsx
107
+ // routes/report.tsx (React Router)
108
+ import { Drawing, rect, scene } from '@nxgt/material/draw';
109
+ import { useLoaderData } from 'react-router';
110
+
111
+ export async function loader() {
112
+ const sales = await loadSales(); // [{ month, total }]
113
+ return {
114
+ chart: scene({ w: 300, h: 100 }, sales.map((s, i) =>
115
+ rect({ x: i * 24, y: 100 - s.total, w: 20, h: s.total, fill: 'primary' }),
116
+ )),
117
+ };
118
+ }
119
+
120
+ export default function Report() {
121
+ const { chart } = useLoaderData<typeof loader>();
122
+ return <Drawing scene={chart} title="Sales by month" />;
123
+ }
124
+ ```
125
+
126
+ A resource route can send the same scene as a file, with no React:
127
+
128
+ ```ts
129
+ import { toSVG } from '@nxgt/material/draw';
130
+
131
+ export async function loader() {
132
+ const svg = toSVG(await buildBadge(), { standalone: true, title: 'Build passing', theme: 'light' });
133
+ return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } });
134
+ }
135
+ ```
136
+
137
+ Never use `Math.random()`, the clock or a measurement while building a scene
138
+ that is drawn on both sides: they differ. A seed makes randomness the same
139
+ ([Sketch](#sketch) takes one).
140
+
141
+ ## Content Security Policy
142
+
143
+ Nothing a drawing renders needs `style-src-attr 'unsafe-inline'`:
144
+
145
+ - no `style` attribute anywhere: geometry, colour and opacity are presentation
146
+ attributes, role colours and animations are classes;
147
+ - no `<style>` element and no `insertRule` on the React path (a `<style>` exists
148
+ only when you ask `toSVG` for `standalone` and `embedStyles`, for a file);
149
+ - `assertSafeDrawing` runs on every compile and refuses `on*` attributes, `style`,
150
+ `url()` to anything but a local `#id`, an `href` that is not `http(s)`,
151
+ relative or a `data:` PNG/JPEG/GIF/WebP, and non-finite numbers. A scene loaded
152
+ from JSON is checked before it is drawn;
153
+ - an `image` with an `http(s)` URL is a request your page makes: keep `img-src`
154
+ in your policy.
155
+
156
+ Check a scene you did not write yourself:
157
+
158
+ ```ts
159
+ import { assertSafeDrawing } from '@nxgt/material/draw';
160
+
161
+ try {
162
+ assertSafeDrawing(JSON.parse(untrustedJson));
163
+ } catch (error) {
164
+ reject(error); // "Unsafe drawing at <image>: href must be http(s), relative or data:image …"
165
+ }
166
+ ```
167
+
168
+ The exact rules are in the [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#safety-csp);
169
+ the other components' CSP needs are in the [CSP guide](csp.md).
170
+
171
+ ## Colours: roles and tones
172
+
173
+ A fill, a stroke or a gradient stop is a **role**, a **tone** or a free colour.
174
+
175
+ ```tsx
176
+ <Drawing w={240} h={60} title="Three kinds of colour">
177
+ <Rect x={0} y={0} w={70} h={60} r={12} fill="tertiary-container" stroke="outline" />
178
+ <Rect x={85} y={0} w={70} h={60} r={12} fill={tone('success', 'container')} />
179
+ <Rect x={170} y={0} w={70} h={60} r={12} fill="#7c4dff" />
180
+ </Drawing>
181
+ ```
182
+
183
+ - **Roles** follow Material 3's names and read the theme through the classes of
184
+ `styles.css`: `primary`, `on-primary`, `primary-container`,
185
+ `on-primary-container` (the same four for `secondary`, `tertiary` and `error`),
186
+ `surface`, `on-surface`, `surface-container`, `on-surface-variant`, `outline`,
187
+ `outline-variant`, `success`, `info`, `warning`, `neutral`. `ROLES` lists them
188
+ and `ROLE_VARS` gives each one's CSS variable (`on-primary-container` is
189
+ `--primary-text`, `outline` is `--muted-foreground`).
190
+ They follow light and dark, scoped themes and a retheme with no re-render.
191
+ - **Tones**: `tone('success')`, `tone('success', 'container')` (variants `base`,
192
+ `text`, `on`, `container`) name a theme tone or an app tone of your own, the
193
+ ones the [tones channel](theming.md) defines. One element can use one app tone;
194
+ a fill in one app tone and a stroke in another throws, so put them on two
195
+ nodes in a group.
196
+ - **Free colours** (`#hex`, `rgb()`, `hsl()`, `oklch()`...) are written as they
197
+ are: they do not follow the theme.
198
+
199
+ Out of React the roles have no stylesheet, so `toSVG` writes colours itself:
200
+ `colors: 'literal'` (the default) uses the hex of `DRAW_PALETTE.light` or `.dark`
201
+ (`theme`) and `palette: { primary: '#0a7' }` overrides tokens;
202
+ `colors: 'vars'` writes `var(--primary, #485096)`, which follows the page's theme
203
+ when the SVG is inline in it.
204
+
205
+ ```ts
206
+ import { DRAW_PALETTE, toSVG } from '@nxgt/material/draw';
207
+
208
+ toSVG(art, { theme: 'dark' }); // literal dark hex
209
+ toSVG(art, { colors: 'vars' }); // inline in a themed page
210
+ toSVG(art, { palette: { ...DRAW_PALETTE.light, primary: '#0a7c59' } });
211
+ ```
212
+
213
+ The full mapping and the palette are in the
214
+ [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#colours).
215
+
216
+ ## Accessibility
217
+
218
+ A `Drawing` takes `title` **or** `decorative`; the type refuses neither.
219
+
220
+ ```tsx
221
+ <Drawing scene={chart} title="Sales by month, rising" /> {/* role="img" aria-label */}
222
+ <Drawing scene={flourish} decorative /> {/* aria-hidden */}
223
+ ```
224
+
225
+ - The name is an `aria-label`, never a `<title>` element: React 19 would hoist a
226
+ `<title>` into the document's head.
227
+ - A drawing with an interactive node is `role="group"`, because an image cannot
228
+ hold focusable children; a decorative one cannot hold an interactive node and
229
+ throws `A decorative drawing has an interactive node: give the Drawing a title instead`.
230
+ - Write the title as what the picture says, not "chart": it is what a reader
231
+ gets in place of the picture. Describe long data in the page next to it.
232
+ - Do not rely on colour alone: `error` and `success` read the same to someone who
233
+ cannot tell red from green, so add a shape or a label.
234
+ - Under `prefers-reduced-motion` the animations do not run (see [Animate](#animate)).
235
+
236
+ ## Events and focus
237
+
238
+ A node is interactive when it has a handler or `interactive: true`. It then
239
+ becomes focusable (`tabindex="0"`), is a `role="button"` when it has `onClick`
240
+ (Enter and Space click it) and shows a focus ring on `:focus-visible`. Give it a
241
+ `label`: that is its accessible name.
242
+
243
+ In scene mode, events go by node `id`:
244
+
245
+ ```tsx
246
+ 'use client';
247
+
248
+ import { Drawing, rect, scene } from '@nxgt/material/draw';
249
+ import { useState } from 'react';
250
+
251
+ const board = scene({ w: 220, h: 70 }, [
252
+ rect({ id: 'todo', label: 'To do', interactive: true, x: 0, y: 0, w: 100, h: 70, r: 12, fill: 'surface-container', stroke: 'outline' }),
253
+ rect({ id: 'done', label: 'Done', interactive: true, x: 120, y: 0, w: 100, h: 70, r: 12, fill: 'success' }),
254
+ ]);
255
+
256
+ export function Board() {
257
+ const [picked, setPicked] = useState('');
258
+ return (
259
+ <>
260
+ <Drawing
261
+ scene={board}
262
+ title="Board columns"
263
+ handlers={{ todo: { onPointerEnter: () => setPicked('todo') } }}
264
+ onNodeClick={(id) => setPicked(id)}
265
+ />
266
+ <p aria-live="polite">{picked}</p>
267
+ </>
268
+ );
269
+ }
270
+ ```
271
+
272
+ In JSX the primitives take `onClick`, `onPointerEnter`, `onPointerLeave`,
273
+ `onPointerDown`, `onPointerUp`, `onFocus`, `onBlur` and `onKeyDown`; set
274
+ `interactive` on a server-rendered `Drawing` so the first markup already says
275
+ it is a group. There is no drag: drawings are for showing, not for editing.
276
+
277
+ ## Shapes and layout
278
+
279
+ Material 3 Expressive's shapes are nodes (`shape('cookie-9', { x, y, w, h, fill })`
280
+ or `m3-cookie-9`; `SHAPE_NAMES` lists them), and `bbox`, `stack`, `grid`, `align`
281
+ and `distribute` place nodes by their real boxes. Both come from
282
+ `@nxgt/material/draw`, by name: a bundler keeps only what you import.
283
+
284
+ ```tsx
285
+ import { Drawing, Path, shape } from '@nxgt/material/draw';
286
+
287
+ <Drawing w={120} h={120} title="A cookie">
288
+ <Path node={shape('cookie-9', { x: 10, y: 10, w: 100, h: 100, fill: 'tertiary' })} />
289
+ </Drawing>;
290
+ ```
291
+
292
+ **A name clash.** The builders `text` and `image` of `/draw` have the names of the
293
+ PDF blocks in `@nxgt/material/print`. A file that uses both renames one:
294
+
295
+ ```tsx
296
+ import { text as drawText, rect, scene } from '@nxgt/material/draw';
297
+ import { drawing, page, pdfDocument, text } from '@nxgt/material/print';
298
+
299
+ const badge = scene({ w: 120, h: 40 }, [
300
+ rect({ x: 0, y: 0, w: 120, h: 40, r: 20, fill: 'primary' }),
301
+ drawText('v3', { x: 60, y: 26, anchor: 'middle', fill: 'on-primary' }),
302
+ ]);
303
+
304
+ pdfDocument({ pages: [page({ body: [text('Release 3'), drawing(badge, { width: 120 })] })] });
305
+ ```
306
+
307
+ The units of shapes, `morph` and the layout helpers are in the
308
+ [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#shapes-and-morphs-nxgtmaterial-coredrawshapes).
309
+
310
+ ## Sketch
311
+
312
+ `Sketch` draws what is inside it by hand: strokes in two wobbling passes, loose
313
+ ellipses, hatched fills. The wobble is seeded, so the server and the browser draw
314
+ the same lines.
315
+
316
+ ```tsx
317
+ 'use client';
318
+
319
+ import { Drawing, Rect, Text } from '@nxgt/material/draw';
320
+ import { Sketch } from '@nxgt/material/draw/sketch';
321
+
322
+ export const Wireframe = () => (
323
+ <Sketch seed={7} roughness={1.2} fill="cross-hatch">
324
+ <Drawing w={200} h={80} title="A sketched button">
325
+ <Rect id="go" label="Go" x={10} y={10} w={180} h={60} r={12} fill="primary" stroke="primary" onClick={go} />
326
+ <Text x={100} y={46} anchor="middle" fill="on-primary">Go</Text>
327
+ </Drawing>
328
+ </Sketch>
329
+ );
330
+ ```
331
+
332
+ On plain data, `sketch(scene, options)` returns a scene, which `toSVG`, a PNG or a
333
+ PDF draws like any other. Ids, labels, events and the focus ring survive; text and
334
+ images are left as they are.
335
+
336
+ ```ts
337
+ import { toSVG } from '@nxgt/material/draw';
338
+ import { sketch } from '@nxgt/material/draw/sketch';
339
+
340
+ const rough = toSVG(sketch(flow, { seed: 3, fill: 'hachure' }), { title: 'Release flow' });
341
+ ```
342
+
343
+ Options and the hatch maths: [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#sketchy-style-nxgtmaterial-coredrawsketch).
344
+
345
+ ## Animate
346
+
347
+ Entry animations are data: `drawOn(scene)` traces the strokes one after the next
348
+ and fades the fills in, `appear(scene)` fades and grows the nodes. They only add
349
+ class names that `styles.css` animates, so a server renders them and the CSP
350
+ stays clean. `useDrawingControls(ref)` replays them, and `MorphDrawing` springs
351
+ one scene into another.
352
+
353
+ ```tsx
354
+ 'use client';
355
+
356
+ import { Drawing, scene, shape } from '@nxgt/material/draw';
357
+ import { drawOn, MorphDrawing, useDrawingControls } from '@nxgt/material/draw/animate';
358
+ import { useRef, useState } from 'react';
359
+
360
+ const flow = drawOn(flowScene, { dur: 'l' }); // outside the component: a stable scene
361
+ const cookie = scene({ w: 120, h: 120 }, [shape('cookie-9', { id: 'blob', x: 10, y: 10, w: 100, h: 100, fill: 'primary-container' })]);
362
+ const heart = scene({ w: 120, h: 120 }, [shape('heart', { id: 'blob', x: 10, y: 10, w: 100, h: 100, fill: 'tertiary-container' })]);
363
+
364
+ export function Demo() {
365
+ const ref = useRef<SVGSVGElement>(null);
366
+ const { replay } = useDrawingControls(ref);
367
+ const [liked, setLiked] = useState(false);
368
+ return (
369
+ <>
370
+ <Drawing ref={ref} scene={flow} title="Release flow" />
371
+ <button type="button" onClick={replay}>Replay</button>
372
+ <button type="button" aria-pressed={liked} onClick={() => setLiked(!liked)}>
373
+ <MorphDrawing from={cookie} to={heart} active={liked} title={liked ? 'Liked' : 'Not liked'} />
374
+ </button>
375
+ </>
376
+ );
377
+ }
378
+ ```
379
+
380
+ - Under `prefers-reduced-motion` nothing moves and the last frame shows at once.
381
+ - The delay between two nodes is `--nxgt-draw-stagger` (60 ms a step): set it on
382
+ the drawing or an ancestor.
383
+ - `MorphDrawing` morphs the closed paths both scenes share by `id`; every other
384
+ node switches to the target at once. `useSceneMorph` cross-fades them instead.
385
+ - A standalone `toSVG` file has no stylesheet, so its classes do nothing and the
386
+ drawing simply shows (`embedStyles: true` adds a small one).
387
+
388
+ Options of `drawOn`, `appear` and `morphScenes`:
389
+ [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#animation-nxgtmaterial-coredrawanimate).
390
+
391
+ ## PNG
392
+
393
+ `drawingToPng` makes a PNG in the browser from a scene or from a live `<svg>`, with
394
+ the colours the page shows (roles are resolved through the theme, so a dark page
395
+ gives a dark PNG; `colorsFrom` picks another element's theme).
396
+
397
+ ```tsx
398
+ 'use client';
399
+
400
+ import { Drawing } from '@nxgt/material/draw';
401
+ import { downloadDrawingPng, drawingToPng } from '@nxgt/material/draw/png';
402
+ import { useRef } from 'react';
403
+
404
+ export function Exportable() {
405
+ const ref = useRef<SVGSVGElement>(null);
406
+ return (
407
+ <>
408
+ <Drawing ref={ref} scene={flow} title="Release flow" />
409
+ <button type="button" onClick={() => downloadDrawingPng(ref.current ?? flow, 'release-flow', { scale: 2 })}>
410
+ Save as PNG
411
+ </button>
412
+ <button type="button" onClick={async () => upload(await drawingToPng(flow, { background: 'var(--background)' }))}>
413
+ Upload
414
+ </button>
415
+ </>
416
+ );
417
+ }
418
+ ```
419
+
420
+ - `scale` is pixels per drawing unit (2 by default); the PNG is transparent unless
421
+ you pass a `background`.
422
+ - An `image` with a URL is fetched and inlined first. A cross-origin image needs
423
+ CORS headers, or a `data:` URL (the error names the URL; see
424
+ [the troubleshooting entry](../troubleshooting.md#drawingtopng-cannot-read-the-image-url-reason)).
425
+ - Text uses the system fallback font: the page's web fonts do not reach an SVG
426
+ drawn as an image, so a line may be a little wider or narrower than on the page.
427
+ - From a live `<svg>`, what the stylesheet hides stays hidden: the focus rings of
428
+ interactive nodes do not show in the image.
429
+ - Browser only; the entry imports on a server and touches nothing until you call it.
430
+
431
+ ## PDF
432
+
433
+ A scene prints as vectors in a PDF document made with `@nxgt/material/print`:
434
+ `drawing(scene, { width?, align? })` in a document written as data, or
435
+ `PrintDrawing` inside `PrintDocument`. The engine is the same compile as the
436
+ screen; the PDF is not a browser, so colour roles take the document theme where it
437
+ has them (`primary`, `secondary`, `success`, `error`) and the light palette
438
+ otherwise, text is Helvetica, patterns print as a solid colour and a mask is
439
+ dropped with a warning. The table of what differs is in the
440
+ [reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#in-a-pdf-document-drawing-in-nxgtmaterial-corepdfmodel),
441
+ and the document side in the [printing guide](printing.md#drawings-in-a-document).
442
+
443
+ ```tsx
444
+ import { drawing, page, PrintDocument, PrintDrawing, pdfDocument } from '@nxgt/material/print';
445
+
446
+ const doc = pdfDocument({ pages: [page({ body: [drawing(flow, { width: 400, align: 'center' })] })] });
447
+
448
+ const Report = () => (
449
+ <PrintDocument>
450
+ <PrintDrawing scene={flow} width={400} align="center" />
451
+ </PrintDocument>
452
+ );
453
+ ```
454
+
455
+ `/print` also exports `text` and `image`: see the [name clash](#shapes-and-layout).
456
+
457
+ ## E-mail
458
+
459
+ An e-mail client is neither a browser nor a PDF viewer, and many (Gmail and
460
+ Outlook among them) do not show SVG. There is no server-side raster in the
461
+ library: choose by what your readers' clients do.
462
+
463
+ - **Clients that render SVG** (and a web view of the message): send
464
+ `toSVG(scene, { standalone: true, theme: 'light' })` as an attachment or an
465
+ `<img src>` URL. Its colours are literal hex, so it needs no stylesheet and no
466
+ CSS variable, and it has no `style` attribute.
467
+ - **Everyone else**: make a PNG in a browser (an admin page, a preview button)
468
+ with `drawingToPng(scene, { scale: 2, background: '#ffffff' })`, store it, and
469
+ point an `<img src="https://…/chart.png" width="480" alt="Sales by month">` at
470
+ it. Pass a `background`: a transparent PNG shows the reader's own colours behind it.
471
+
472
+ ```ts
473
+ import { toSVG } from '@nxgt/material/draw';
474
+
475
+ const attachment = {
476
+ filename: 'sales.svg',
477
+ contentType: 'image/svg+xml',
478
+ content: toSVG(chart, { standalone: true, title: 'Sales by month', theme: 'light' }),
479
+ };
480
+ ```
481
+
482
+ Give the `<img>` an `alt` that says what the picture says: it is all a client
483
+ that blocks images shows.
484
+
485
+ ## Troubleshooting
486
+
487
+ Every message a drawing throws is listed with its cause and fix:
488
+
489
+ - [Drawing in the troubleshooting page](../troubleshooting.md#drawing): a decorative drawing with an
490
+ interactive node, `Unsafe drawing at <tag>`, an unknown shape name, a PNG that
491
+ fails on an image, a drawing with no colours, a Server Component that imports
492
+ `Drawing`.
493
+ - The engine's own errors (`Invalid colour`, `Invalid path data`, `Tones "a" and
494
+ "b" meet on one element`, `[pdf] A drawing mask is not supported`) are in the
495
+ [core troubleshooting](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/troubleshooting.md#drawing).
496
+
497
+ Text that wraps one character differently from the page is not an error: widths
498
+ are Inter's until the browser re-measures after mount (`exactText={false}` keeps
499
+ the metrics' breaks).
@@ -1016,6 +1016,7 @@ it:
1016
1016
  | `badge` | `badge(text, color?)` | A pill |
1017
1017
  | `header` | `header({ title, subtitle?, reference?, referencePrefix?, logo?, logoText?, qrDataUrl? })` | The title block, as `PrintHeader`; `referencePrefix` is printed before `reference`, `'#'` by default |
1018
1018
  | `image` | `image(src, { width?, height?, fit? })` | A PNG or JPEG, from a URL or a data URL |
1019
+ | `drawing` | `drawing(scene, { width?, align? })` | A [vector drawing](#drawings-in-a-document) (a scene from `@nxgt/material-core/draw`), as vectors |
1019
1020
  | `link` | `link(href, text)` | A link |
1020
1021
  | `spacer`, `divider`, `pageBreak` | `spacer(points)`, `divider()`, `pageBreak()` | Space, a rule, a new page |
1021
1022
  | `pageNumber` | `pageNumber({ format?, align?, scope? })` | "Page 2 of 5", where you put it (in a `footer`, say); `scope: 'group'` counts within the page group, see [Page groups](#page-groups-numbering-that-restarts) |
@@ -1042,6 +1043,52 @@ table({
1042
1043
  // locale 'fr-FR': 1 avr. 2026 · 1 234,50 € locale 'en-US': Apr 1, 2026 · €1,234.50
1043
1044
  ```
1044
1045
 
1046
+ ### Drawings in a document
1047
+
1048
+ A drawing is a scene (plain data from the builders of `@nxgt/material/draw`). `drawing(scene, { width?, align? })` puts it in a
1049
+ document as vectors, and `PrintDrawing` does the same inside `PrintDocument`:
1050
+
1051
+ ```tsx
1052
+ import { circle, rect, scene, text as drawText } from '@nxgt/material/draw';
1053
+ import { drawing, page, PrintDocument, PrintDrawing, pdfDocument } from '@nxgt/material/print';
1054
+
1055
+ const art = scene({ w: 240, h: 120 }, [
1056
+ rect({ x: 8, y: 8, w: 224, h: 104, r: 16, fill: 'primary-container' }),
1057
+ circle({ cx: 48, cy: 60, r: 22, fill: 'success' }),
1058
+ drawText('Shipped', { x: 120, y: 66, anchor: 'middle', size: 18, weight: 600 }),
1059
+ ]);
1060
+
1061
+ // As data:
1062
+ const doc = pdfDocument({ pages: [page({ body: [drawing(art, { width: 400, align: 'center' })] })] });
1063
+
1064
+ // As components:
1065
+ export const Report = () => (
1066
+ <PrintDocument title="Report">
1067
+ <PrintDrawing scene={art} width={400} align="center" />
1068
+ </PrintDocument>
1069
+ );
1070
+ ```
1071
+
1072
+ `width` is in points and defaults to the scene's own width; the height keeps its
1073
+ aspect. `@nxgt/material/print` and `@nxgt/material/draw` both export `text` and `image`: import the
1074
+ drawing's as `drawText` (and `image` as `drawImage`).
1075
+
1076
+ What changes in a PDF:
1077
+
1078
+ - **Colours.** A role takes the document theme's colour where it has one
1079
+ (`primary`, `secondary`, `success`, `error`), otherwise the light palette's.
1080
+ - **Text is Helvetica**, not Inter (the theme's `fonts` apply), and `maxWidth` wraps
1081
+ with Helvetica's widths. Bold is printed twice a hair apart and italic is a
1082
+ skew, so selecting bold text copies it twice.
1083
+ - **A `pattern` or `hatch`** prints as a solid of its colour, and **a `mask` is
1084
+ dropped** with a warning (`console.warn`, or the `warn` render option).
1085
+ - **An `image`** is placed over the drawing and follows the `translate` and
1086
+ `scale` of its groups; it sits above everything else, and one under a rotation
1087
+ is dropped with a warning.
1088
+
1089
+ The full table is in the [drawing reference](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/draw.md#in-a-pdf-document-drawing-in-nxgtmaterial-corepdfmodel).
1090
+ Vue's `/pdf` does not re-export `drawing` yet (tracked in the Vue drawing issue); import it from `@nxgt/material-core/pdf/model` meanwhile.
1091
+
1045
1092
  ### Page groups: numbering that restarts
1046
1093
 
1047
1094
  A document made of one entry of `pages` per person (a report card per
@@ -65,6 +65,7 @@ at the top are needed is in [getting started](../getting-started.md#styles).
65
65
  | `--nxgt-accent` | `[--nxgt-accent:…]` | the mark colour of the selection controls: the fill, handle, state layer and stop dots of `Slider`, the on state of `Switch`, the box and dot of `Checkbox` and `Radio`. It defaults to `--primary` (`--primary-text` in dark) and is set per control, so override it on the control itself, not on `:root` |
66
66
  | `--error`, `--success`, `--info`, `--warning` (+ `-foreground`) | `bg-error`, `text-success`, … | the status colours of `Button`, `Alert`, `Badge`, toasts |
67
67
  | `--primary-text` | `text-primary-text` | primary drawn as text or an icon on a page, card or popover surface: `Typography tone="primary"`, links, the `Alert` primary icon, selected labels. Equal to `--primary` in light; in dark, `--primary` mixed 60 % with white (`color-mix(in oklab, var(--primary) 40%, white)`), well over 4.5:1 on `--background` and `--card` |
68
+ | `--primary-indicator` | `bg-primary-indicator`, `border-primary-indicator`, `ring-primary-indicator`, `stroke-primary-indicator` | primary drawn as a non-text mark that carries a state (WCAG 1.4.11, 3:1): an active bar or dot, a selected or focused border, a ring, a spinner arc. Equal to `--primary` in light; in dark, `--primary` mixed 25 % with white (`color-mix(in oklab, var(--primary) 75%, white)`), 4.5:1 on `--background`, 4.1:1 on `--card`, 3.7:1 on `--muted` (`--primary` itself gives 2.5, 2.3 and 2.1:1) |
68
69
  | `--error-text` | `text-error-text` | error drawn as text: field messages, `HelperText` in its error state, form and dialog errors, `variant="error"` menu items |
69
70
  | `--destructive` | `bg-destructive`, `text-destructive` | deprecated alias of `--error`: the light theme sets it to `var(--error)`, the dark theme keeps its own value; use `--error` |
70
71
  | `--card`, `--popover`, `--tooltip` (+ `-foreground`) | `bg-card`, `bg-popover`, `bg-tooltip` | surfaces |
@@ -862,8 +863,8 @@ the fill can stay dark while the text is light enough to read. The light value
862
863
  is `color-mix(in oklab, var(--error) 92%, black)` (5.6:1 on `--background`,
863
864
  6.0:1 on `--card`, 5.0:1 on the `bg-error/10` tint of a focused menu item, where
864
865
  `--error` gives 5.0 and 4.2:1); the dark one is
865
- `color-mix(in oklab, var(--error) 80%, white)`, 5.4:1 on `--background` and
866
- 5.0:1 on `--card`, where `--error` itself gives 3.9 and 3.5:1. Field messages,
866
+ `color-mix(in oklab, var(--error) 76%, white)`, 5.85:1 on `--background` and
867
+ 5.35:1 on `--card` (4.78:1 on a focused error menu item), where `--error` itself gives 3.9 and 3.5:1. Field messages,
867
868
  `HelperText` in its error state, form, dialog and upload errors, a textarea's
868
869
  counter over its limit, the error lines of `Terminal`, a
869
870
  `variant="error"` menu item and the falling values of the metric cards use it.
package/docs/roadmap.md CHANGED
@@ -6,6 +6,23 @@ version they shipped in, and planned entries may name the major they target.
6
6
 
7
7
  ## Now
8
8
 
9
+ - **Vector drawings in code** (in the next prerelease) — `@nxgt/material/draw`
10
+ draws a scene of plain data, or JSX primitives (`Rect`, `Path`, `Text`...), as
11
+ SVG that follows the theme: colours are Material 3 roles and tones, with no
12
+ inline `style`, so a strict CSP is satisfied and a server renders the same
13
+ markup as the browser. A drawing is named (`title`) or `decorative`, enforced
14
+ by the type; nodes with events are focusable buttons with a focus ring; wrapped
15
+ text breaks with Inter's metrics and is refined once mounted. Material 3
16
+ Expressive's shapes, morphs and layout helpers (`stack`, `grid`, `align`)
17
+ come with it. `/draw/sketch` redraws any drawing by hand (seeded, so the
18
+ server and the browser agree), `/draw/animate` traces strokes, fades nodes in
19
+ and morphs one scene into another (nothing moves under reduced motion),
20
+ `/draw/png` saves a PNG with the page's colours, and `/print` places the scene
21
+ in a PDF as vectors. For e-mail, `toSVG(scene, { standalone: true })` gives an
22
+ SVG with literal colours and a PNG made in a browser covers the clients that
23
+ do not show SVG. React only for now; the Vue port is on the Vue roadmap.
24
+ ([Drawing guide](guides/drawing.md), [Drawing](components/rich-widgets.md#drawing))
25
+
9
26
  - **A modern code editor** — `CodeEditor` and `CodeDiff` get their own entry,
10
27
  `@nxgt/material/code-editor`, and theme presets: App (from your tokens),
11
28
  Moonlight and Soft, light and dark, or your own `defineCodeTheme`, several on