@nxgt/material 3.5.0 → 3.7.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 +92 -0
- package/README.md +46 -1
- package/dist/components/components.css +2 -0
- package/dist/components/theme-tokens.css +84 -72
- package/dist/components/tones/tokens.css +5 -5
- package/dist/components/ui/display/chip/chip-hairline.css +1 -1
- package/dist/components/ui/layout/sidebar/sidebar-tokens.css +1 -1
- package/dist/components/ui/overlays/dialog/dialog.css +2 -2
- package/dist/components/ui/primitives/field-box/field-box-tokens.css +1 -1
- package/dist/components/ui/primitives/toggle-group/toggle-group-segmented.css +2 -2
- package/dist/components/ui/primitives/toggle-group/toggle-group-separate.css +1 -1
- package/dist/components/ui/widgets/drawing/drawing-animate.css +123 -0
- package/dist/components/ui/widgets/drawing/drawing.css +459 -0
- package/dist/{components-CXpL1zEs.js → components-BX4icy-g.js} +734 -734
- package/dist/components.js +16 -16
- package/dist/{dialog-JQT6iETD.js → dialog-DdJ1C4BR.js} +168 -168
- package/dist/dnd.js +1 -1
- package/dist/draw-animate.js +106 -0
- package/dist/draw-png.js +2 -0
- package/dist/draw-sketch.js +38 -0
- package/dist/draw.js +65 -0
- package/dist/drawing-B8VuG7nx.js +390 -0
- package/dist/drawing-context-CeKeZE6b.js +10 -0
- package/dist/editor.js +110 -110
- package/dist/{icon-badge-C30yKsAM.js → icon-badge-Bb5-73XP.js} +18 -18
- package/dist/{icon-button-BXlI3DJ_.js → icon-button-5A6Mrj6w.js} +60 -90
- package/dist/lib/components/ui/display/shape/use-shape-morph.d.ts +1 -0
- package/dist/lib/components/ui/widgets/drawing/drawing-a11y.d.ts +20 -0
- package/dist/lib/components/ui/widgets/drawing/drawing-context.d.ts +23 -0
- package/dist/lib/components/ui/widgets/drawing/drawing-types.d.ts +63 -0
- package/dist/lib/components/ui/widgets/drawing/drawing.d.ts +17 -0
- package/dist/lib/components/ui/widgets/drawing/index.d.ts +5 -0
- package/dist/lib/components/ui/widgets/drawing/partials/drawing-root.d.ts +17 -0
- package/dist/lib/components/ui/widgets/drawing/partials/interactive-node.d.ts +22 -0
- package/dist/lib/components/ui/widgets/drawing/partials/primitives/content.d.ts +20 -0
- package/dist/lib/components/ui/widgets/drawing/partials/primitives/path.d.ts +29 -0
- package/dist/lib/components/ui/widgets/drawing/partials/primitives/primitive.d.ts +19 -0
- package/dist/lib/components/ui/widgets/drawing/partials/primitives/shapes.d.ts +18 -0
- package/dist/lib/components/ui/widgets/drawing/partials/react-attrs.d.ts +5 -0
- package/dist/lib/components/ui/widgets/drawing/partials/ring.d.ts +9 -0
- package/dist/lib/components/ui/widgets/drawing/partials/svg-tree.d.ts +29 -0
- package/dist/lib/components/ui/widgets/drawing/use-exact-text.d.ts +16 -0
- package/dist/lib/draw/index.d.ts +32 -0
- package/dist/lib/draw-animate/index.d.ts +19 -0
- package/dist/lib/draw-animate/morph-drawing.d.ts +32 -0
- package/dist/lib/draw-animate/reduced-motion.d.ts +2 -0
- package/dist/lib/draw-animate/use-drawing-controls.d.ts +27 -0
- package/dist/lib/draw-animate/use-scene-morph.d.ts +15 -0
- package/dist/lib/draw-png/index.d.ts +11 -0
- package/dist/lib/draw-sketch/index.d.ts +17 -0
- package/dist/lib/draw-sketch/sketch.d.ts +19 -0
- package/dist/lib/print/components/print-drawing.d.ts +16 -0
- package/dist/lib/print/index.d.ts +2 -1
- package/dist/main.js +21 -21
- package/dist/map.js +63 -63
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +123 -115
- package/dist/{public-Cde8_SN8.js → public-CBlV2XuA.js} +742 -742
- package/dist/{sortable-provider-DYlpq8lP.js → sortable-provider-BvD98s8Y.js} +19 -19
- package/dist/{use-chat-composer-BwpSgP_D.js → use-chat-composer-BhdPNmYm.js} +912 -911
- package/dist/use-shape-morph-BKA_R5P5.js +35 -0
- package/docs/README.md +1 -0
- package/docs/components/data-display.md +2 -2
- package/docs/components/rich-widgets.md +196 -0
- package/docs/guides/drawing.md +499 -0
- package/docs/guides/printing.md +47 -0
- package/docs/guides/theming.md +86 -43
- package/docs/previews/rich-widgets-drawing.webp +0 -0
- package/docs/roadmap.md +17 -0
- package/docs/troubleshooting.md +166 -2
- package/lib/components/components.css +2 -0
- package/lib/components/theme-tokens.css +84 -72
- package/lib/components/tones/tokens.css +5 -5
- package/lib/components/ui/display/chip/chip-hairline.css +1 -1
- package/lib/components/ui/layout/sidebar/sidebar-tokens.css +1 -1
- package/lib/components/ui/overlays/dialog/dialog.css +2 -2
- package/lib/components/ui/primitives/field-box/field-box-tokens.css +1 -1
- package/lib/components/ui/primitives/toggle-group/toggle-group-segmented.css +2 -2
- package/lib/components/ui/primitives/toggle-group/toggle-group-separate.css +1 -1
- package/lib/components/ui/widgets/drawing/drawing-animate.css +123 -0
- package/lib/components/ui/widgets/drawing/drawing.css +459 -0
- 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).
|
package/docs/guides/printing.md
CHANGED
|
@@ -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
|