@misoto22/design 0.6.0 → 0.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.
Files changed (157) hide show
  1. package/README.md +12 -0
  2. package/dist/agent/Accordion.md +1 -1
  3. package/dist/agent/Alert.md +1 -1
  4. package/dist/agent/AppShell.md +1 -1
  5. package/dist/agent/ArchitectureFigure.md +48 -0
  6. package/dist/agent/AreaChart.md +2 -2
  7. package/dist/agent/Article.md +1 -1
  8. package/dist/agent/Avatar.md +1 -1
  9. package/dist/agent/Badge.md +1 -1
  10. package/dist/agent/BarChart.md +2 -2
  11. package/dist/agent/BarList.md +2 -2
  12. package/dist/agent/BigNumber.md +2 -2
  13. package/dist/agent/BoxPlot.md +2 -2
  14. package/dist/agent/Breadcrumb.md +1 -1
  15. package/dist/agent/BulletChart.md +2 -2
  16. package/dist/agent/Button.md +1 -1
  17. package/dist/agent/Calendar.md +1 -1
  18. package/dist/agent/Card.md +1 -1
  19. package/dist/agent/Checkbox.md +1 -1
  20. package/dist/agent/Collapsible.md +1 -1
  21. package/dist/agent/Combobox.md +1 -1
  22. package/dist/agent/Command.md +1 -1
  23. package/dist/agent/ComposedChart.md +2 -2
  24. package/dist/agent/ContextMenu.md +1 -1
  25. package/dist/agent/DataflowFigure.md +27 -0
  26. package/dist/agent/DatePicker.md +1 -1
  27. package/dist/agent/Diagram.md +1 -1
  28. package/dist/agent/DiagramCanvas.md +47 -0
  29. package/dist/agent/DiagramExportMenu.md +38 -0
  30. package/dist/agent/DiagramInspector.md +50 -0
  31. package/dist/agent/DiagramLegend.md +34 -0
  32. package/dist/agent/DiagramMinimap.md +48 -0
  33. package/dist/agent/DiagramToolbar.md +47 -0
  34. package/dist/agent/Dialog.md +1 -1
  35. package/dist/agent/DropdownMenu.md +1 -1
  36. package/dist/agent/EmptyState.md +1 -1
  37. package/dist/agent/ErrorState.md +1 -1
  38. package/dist/agent/Facet.md +2 -2
  39. package/dist/agent/Field.md +1 -1
  40. package/dist/agent/FigureBand.md +1 -1
  41. package/dist/agent/FloatingIconButton.md +1 -1
  42. package/dist/agent/FunnelChart.md +2 -2
  43. package/dist/agent/Heatmap.md +2 -2
  44. package/dist/agent/Histogram.md +2 -2
  45. package/dist/agent/Input.md +1 -1
  46. package/dist/agent/Kbd.md +1 -1
  47. package/dist/agent/LifecycleFigure.md +28 -0
  48. package/dist/agent/LineChart.md +2 -2
  49. package/dist/agent/LinkArrow.md +1 -1
  50. package/dist/agent/NativeSelect.md +1 -1
  51. package/dist/agent/NavItem.md +1 -1
  52. package/dist/agent/Pagination.md +1 -1
  53. package/dist/agent/PieChart.md +2 -2
  54. package/dist/agent/Popover.md +1 -1
  55. package/dist/agent/Progress.md +1 -1
  56. package/dist/agent/RadarChart.md +2 -2
  57. package/dist/agent/RadialChart.md +2 -2
  58. package/dist/agent/RadioGroup.md +1 -1
  59. package/dist/agent/SankeyChart.md +2 -2
  60. package/dist/agent/ScatterChart.md +2 -2
  61. package/dist/agent/ScrollArea.md +1 -1
  62. package/dist/agent/SearchableMenu.md +1 -1
  63. package/dist/agent/Select.md +1 -1
  64. package/dist/agent/Separator.md +1 -1
  65. package/dist/agent/SequenceFigure.md +28 -0
  66. package/dist/agent/Sheet.md +1 -1
  67. package/dist/agent/Skeleton.md +1 -1
  68. package/dist/agent/Slider.md +1 -1
  69. package/dist/agent/Sparkline.md +2 -2
  70. package/dist/agent/Spinner.md +1 -1
  71. package/dist/agent/StatusDot.md +1 -1
  72. package/dist/agent/StatusPill.md +1 -1
  73. package/dist/agent/Steps.md +1 -1
  74. package/dist/agent/Switch.md +1 -1
  75. package/dist/agent/Table.md +1 -1
  76. package/dist/agent/Tabs.md +1 -1
  77. package/dist/agent/Tag.md +1 -1
  78. package/dist/agent/Textarea.md +1 -1
  79. package/dist/agent/Toast.md +1 -1
  80. package/dist/agent/ToggleGroup.md +1 -1
  81. package/dist/agent/Tooltip.md +1 -1
  82. package/dist/agent/TreemapChart.md +2 -2
  83. package/dist/agent/WaterfallChart.md +2 -2
  84. package/dist/agent/WorkflowFigure.md +28 -0
  85. package/dist/agent/catalog.json +295 -3
  86. package/dist/agent/index.md +15 -1
  87. package/dist/charts/index.d.ts +2 -1
  88. package/dist/charts/lib/export.d.ts +33 -43
  89. package/dist/charts/lib/export.js +16 -158
  90. package/dist/charts/lib/export.js.map +1 -1
  91. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +49 -0
  92. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +135 -0
  93. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -0
  94. package/dist/diagrams/DataflowFigure/DataflowFigure.d.ts +28 -0
  95. package/dist/diagrams/DataflowFigure/DataflowFigure.js +129 -0
  96. package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -0
  97. package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +64 -0
  98. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +222 -0
  99. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -0
  100. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +72 -0
  101. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +125 -0
  102. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -0
  103. package/dist/diagrams/DiagramInspector/DiagramInspector.d.ts +70 -0
  104. package/dist/diagrams/DiagramInspector/DiagramInspector.js +94 -0
  105. package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -0
  106. package/dist/diagrams/DiagramLegend/DiagramLegend.d.ts +42 -0
  107. package/dist/diagrams/DiagramLegend/DiagramLegend.js +35 -0
  108. package/dist/diagrams/DiagramLegend/DiagramLegend.js.map +1 -0
  109. package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +61 -0
  110. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +79 -0
  111. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -0
  112. package/dist/diagrams/DiagramToolbar/DiagramToolbar.d.ts +48 -0
  113. package/dist/diagrams/DiagramToolbar/DiagramToolbar.js +53 -0
  114. package/dist/diagrams/DiagramToolbar/DiagramToolbar.js.map +1 -0
  115. package/dist/diagrams/LifecycleFigure/LifecycleFigure.d.ts +31 -0
  116. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +202 -0
  117. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -0
  118. package/dist/diagrams/SequenceFigure/SequenceFigure.d.ts +38 -0
  119. package/dist/diagrams/SequenceFigure/SequenceFigure.js +231 -0
  120. package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -0
  121. package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +34 -0
  122. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +211 -0
  123. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -0
  124. package/dist/diagrams/index.d.ts +16 -0
  125. package/dist/diagrams/index.js +19 -0
  126. package/dist/diagrams/index.js.map +1 -0
  127. package/dist/diagrams/lib/frame.d.ts +97 -0
  128. package/dist/diagrams/lib/frame.js +116 -0
  129. package/dist/diagrams/lib/frame.js.map +1 -0
  130. package/dist/diagrams/lib/geometry.d.ts +114 -0
  131. package/dist/diagrams/lib/geometry.js +113 -0
  132. package/dist/diagrams/lib/geometry.js.map +1 -0
  133. package/dist/diagrams/lib/legend.d.ts +32 -0
  134. package/dist/diagrams/lib/legend.js +84 -0
  135. package/dist/diagrams/lib/legend.js.map +1 -0
  136. package/dist/diagrams/lib/marks.d.ts +207 -0
  137. package/dist/diagrams/lib/marks.js +480 -0
  138. package/dist/diagrams/lib/marks.js.map +1 -0
  139. package/dist/diagrams/lib/route.d.ts +85 -0
  140. package/dist/diagrams/lib/route.js +178 -0
  141. package/dist/diagrams/lib/route.js.map +1 -0
  142. package/dist/diagrams/lib/wires.d.ts +59 -0
  143. package/dist/diagrams/lib/wires.js +90 -0
  144. package/dist/diagrams/lib/wires.js.map +1 -0
  145. package/dist/diagrams/spec.d.ts +353 -0
  146. package/dist/diagrams/spec.js +1 -0
  147. package/dist/diagrams/spec.js.map +1 -0
  148. package/dist/lib/svg-export.d.ts +138 -0
  149. package/dist/lib/svg-export.js +198 -0
  150. package/dist/lib/svg-export.js.map +1 -0
  151. package/dist/semantic.css +34 -0
  152. package/dist/styles.css +1 -1
  153. package/dist/tokens.d.ts +16 -0
  154. package/dist/tokens.js +82 -0
  155. package/dist/tokens.json +82 -0
  156. package/package.json +5 -1
  157. package/skills/misoto22-design/SKILL.md +58 -6
@@ -1,4 +1,4 @@
1
- # @misoto22/design 0.6.0
1
+ # @misoto22/design 0.7.0
2
2
 
3
3
  > Monochrome design system: portable CSS tokens and accessible React 19
4
4
  > primitives. Components ship compiled — import them, do not copy them.
@@ -129,3 +129,17 @@ re-pointed in a stylesheet.
129
129
  - **Histogram** — The shape of one distribution.
130
130
  - **WaterfallChart** — How a total got from one figure to another.
131
131
  - **Facet** — The same chart once per group, on one shared scale.
132
+
133
+ ### Diagrams
134
+
135
+ - **ArchitectureFigure** — A component map: services, datastores, trust boundaries, and what talks to what.
136
+ - **WorkflowFigure** — A process in lanes and phases, with the main path drawn heavier than its branches.
137
+ - **SequenceFigure** — A call chain over time: who asks whom, in what order, and what comes back.
138
+ - **DataflowFigure** — A pipeline: where data comes from, what happens to it, and who ends up with it.
139
+ - **LifecycleFigure** — A state machine: what something can be, and what moves it between states.
140
+ - **DiagramCanvas** — A frame that a picture larger than it can be panned and zoomed inside.
141
+ - **DiagramToolbar** — A bar of actions belonging to the surface underneath them.
142
+ - **DiagramExportMenu** — Taking the figure off the page: PNG, JPEG, WebP, SVG and a 1200×630 share card.
143
+ - **DiagramInspector** — What the reader just picked, written out beside the picture.
144
+ - **DiagramMinimap** — Where you are in something bigger than the window.
145
+ - **DiagramLegend** — The key: which drawn form means which kind of thing.
@@ -31,11 +31,12 @@ export { SONIFY_DEFAULTS, SonifyController, SonifyOptions, SonifyPoint, SonifySe
31
31
  export { ChartSonifyButton, ChartSonifyButtonProps, Sonify, SonifyAlign, SonifyProps, UseSonifyOptions, UseSonifyResult, chartSonifySeries, useChartSonifySeries, useSonify } from './lib/sonify-control.js';
32
32
  export { ChartControls, ChartControlsProps, ChartExportFormat, ChartToolbar, ChartToolbarProps, Toolbar, ToolbarProps } from './lib/toolbar.js';
33
33
  export { ChartZoom, ChartZoomOptions, ChartZoomSurface, ChartZoomSurfaceProps, clampWindow, panWindow, useChartZoom, zoomWindow } from './lib/zoom.js';
34
- export { ChartPngOptions, chartToCsv, chartToPng, downloadBlob, exportFilename } from './lib/export.js';
34
+ export { ChartPngOptions, chartToCsv, chartToPng, exportFilename } from './lib/export.js';
35
35
  export { ANNOTATION_LAYER, Annotation, AnnotationProps, AnnotationWeight, ReferenceBand, ReferenceBandProps, ReferenceLine, ReferenceLineProps } from './lib/annotations.js';
36
36
  export { ValueLabelMode, Values, ValuesProps } from './lib/values.js';
37
37
  export { ChartEmpty, ChartEmptyProps } from './lib/empty.js';
38
38
  export { NumberFormatOptions, NumberStyle, defaultTick, formatNumber } from './lib/format.js';
39
+ export { downloadBlob } from '../lib/svg-export.js';
39
40
  import 'react';
40
41
  import 'recharts';
41
42
  import 'recharts/types/component/DefaultTooltipContent';
@@ -1,6 +1,22 @@
1
1
  import { ChartColumn } from './figure.js';
2
+ export { downloadBlob } from '../../lib/svg-export.js';
2
3
  import 'react';
3
4
 
5
+ /**
6
+ * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.
7
+ *
8
+ * The general half — walking a live tree to inline its resolved paint,
9
+ * serialising a standalone SVG document, rasterising one through a canvas — is
10
+ * not chart-shaped at all, and lives in `src/lib/svg-export.ts` shared with the
11
+ * diagrams entry. What is left here is what only a Recharts chart knows: which
12
+ * `<svg>` in the subtree is the plot, what a row of chart data looks like as a
13
+ * CSV record, and that the ground behind an exported plot is `--chart-surface`.
14
+ *
15
+ * Nothing here touches the DOM at module scope. Every browser API is reached
16
+ * for inside a function body, so this module is safe to import from a server
17
+ * component and only fails when a consumer actually asks for a file on a
18
+ * server, which is a call that cannot succeed anywhere.
19
+ */
4
20
  interface ChartPngOptions {
5
21
  /**
6
22
  * Printed above the plot in the exported image. Pass the figure's title: a
@@ -21,46 +37,27 @@ interface ChartPngOptions {
21
37
  /**
22
38
  * The chart's `<svg>`, rasterised to a PNG blob.
23
39
  *
24
- * ## The problem this function exists to solve
25
- *
26
- * Serialising the `<svg>` and handing it to an `<img>` puts it in a document
27
- * of its own. That document has none of the page's stylesheets, so every
28
- * `var(--color-desktop-0)`, `var(--chart-fill)` and `var(--ink)` in the markup
29
- * resolves to nothing — and a paint that resolves to nothing is not a fallback
30
- * colour, it is an invisible mark. Serialise a chart naively and you get an
31
- * empty rectangle with axes on it.
32
- *
33
- * ## The solution
40
+ * Three chart-specific decisions and nothing else. `serializeSvg` does the
41
+ * work, and its comment carries the reason a naive `XMLSerializer` export of a
42
+ * themed chart comes out as an empty rectangle with axes on it.
34
43
  *
35
- * Walk the live tree and the clone in step, and on every node write
36
- * `getComputedStyle`'s answer for the painted properties into an inline
37
- * `style`. Computed values have already had `var()` substituted by the engine,
38
- * so `fill: var(--color-desktop-0)` arrives as `fill: rgb(16, 16, 16)` — and,
39
- * because the walk reads the LIVE element, it reads it under whichever theme
40
- * is currently applied. Light and dark come out right for free, with no theme
41
- * argument anywhere in this file.
42
- *
43
- * `<stop>` elements are walked too, which is the half that is easy to miss: a
44
- * series is painted `fill="url(#…-colors-desktop)"`, the url survives
45
- * serialisation because the gradient is inside the same `<svg>`, and it is the
46
- * gradient's STOPS that carry the custom properties.
44
+ * 1. WHICH `<svg>` the plot, not the first one in the subtree. See
45
+ * `findPlotSvg`.
46
+ * 2. WHAT GROUND `--chart-surface`, resolved against the live element, which
47
+ * is what makes a dark-mode export come out dark rather than transparent.
48
+ * 3. WHAT INK AND FACE for the caption `--ink` and the element's own
49
+ * resolved family, so the exported title matches the page it came from.
47
50
  *
48
51
  * ## What this cannot capture, honestly
49
52
  *
50
53
  * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and
51
54
  * the hidden data table are DOM, not SVG. The PNG is the plot.
52
- * - **Web fonts.** The isolated document cannot fetch the page's `@font-face`
53
- * sources, so text is rasterised in whatever the resolved family stack finds
54
- * locally. Type metrics will differ from the screen.
55
- * - **Cross-origin images.** A chart with an external `<image>` in it taints
56
- * the canvas and `toBlob` throws; there is no way around it from script.
57
- * - **The exact rendering.** This is the browser's SVG rasteriser re-drawing
58
- * the markup, not a screenshot. Antialiasing, `backdrop-filter` and any
59
- * effect a page stylesheet applied from outside the `<svg>` are not in it.
55
+ * - **Web fonts, cross-origin images, and the exact rendering.** See
56
+ * `serializeSvg` and `rasterize`.
60
57
  *
61
58
  * @example
62
59
  * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })
63
- * downloadBlob(blob, 'visitors-per-month.png')
60
+ * downloadBlob(blob, exportFilename('Visitors per month', 'png'))
64
61
  */
65
62
  declare function chartToPng(element: HTMLElement | SVGSVGElement, options?: ChartPngOptions): Promise<Blob>;
66
63
  /**
@@ -83,21 +80,14 @@ declare function chartToPng(element: HTMLElement | SVGSVGElement, options?: Char
83
80
  * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')
84
81
  */
85
82
  declare function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string;
86
- /**
87
- * Hands a blob to the browser as a download.
88
- *
89
- * Throws rather than no-oping without a `document`: a download is something a
90
- * person just asked for, and a silent return would look to them like a click
91
- * that did nothing.
92
- */
93
- declare function downloadBlob(blob: Blob, filename: string): void;
83
+
94
84
  /**
95
85
  * A figure's title as a filename.
96
86
  *
97
- * Lowercase, ASCII-safe and hyphenated, because a downloaded file crosses into
98
- * shells, zip archives and Windows paths where a title's spaces and slashes
99
- * are someone else's problem.
87
+ * The shared slugger, with `chart` rather than `figure` as the fallback for a
88
+ * title that slugs to nothing an export from this entry is a chart, and the
89
+ * name a reader finds in their downloads folder should say so.
100
90
  */
101
91
  declare function exportFilename(title: string, extension: string): string;
102
92
 
103
- export { type ChartPngOptions, chartToCsv, chartToPng, downloadBlob, exportFilename };
93
+ export { type ChartPngOptions, chartToCsv, chartToPng, exportFilename };
@@ -1,154 +1,28 @@
1
- const SVG_NS = "http://www.w3.org/2000/svg";
2
- const PAINTED_PROPERTIES = [
3
- "display",
4
- "visibility",
5
- "opacity",
6
- "color",
7
- "fill",
8
- "fill-opacity",
9
- "fill-rule",
10
- "stroke",
11
- "stroke-opacity",
12
- "stroke-width",
13
- "stroke-linecap",
14
- "stroke-linejoin",
15
- "stroke-dasharray",
16
- "stroke-dashoffset",
17
- "stop-color",
18
- "stop-opacity",
19
- "flood-color",
20
- "flood-opacity",
21
- "mask",
22
- "filter",
23
- "clip-path",
24
- "clip-rule",
25
- "paint-order",
26
- "mix-blend-mode",
27
- "shape-rendering",
28
- "text-anchor",
29
- "dominant-baseline",
30
- "font-family",
31
- "font-size",
32
- "font-style",
33
- "font-weight",
34
- "letter-spacing",
35
- "text-transform"
36
- ];
1
+ import {
2
+ downloadBlob,
3
+ exportFilename as figureFilename,
4
+ rasterize,
5
+ readToken,
6
+ serializeSvg
7
+ } from "../../lib/svg-export.js";
37
8
  async function chartToPng(element, options = {}) {
38
9
  const { title, scale = 2, background, padding = 16 } = options;
39
10
  const svg = findPlotSvg(element);
40
11
  if (!svg) throw new Error("chartToPng: found no <svg> to export inside the element it was given");
41
- const box = svg.getBoundingClientRect();
42
- const width = Math.ceil(box.width);
43
- const height = Math.ceil(box.height);
44
- if (width === 0 || height === 0) {
45
- throw new Error("chartToPng: the chart has no measured size yet \u2014 export it after it renders");
46
- }
47
- const clone = svg.cloneNode(true);
48
- inlinePaint(svg, clone);
49
- clone.setAttribute("width", String(width));
50
- clone.setAttribute("height", String(height));
51
- const titleBand = title ? 30 : 0;
52
- const totalWidth = width + padding * 2;
53
- const totalHeight = height + padding * 2 + titleBand;
54
- const page = document.createElementNS(SVG_NS, "svg");
55
- page.setAttribute("xmlns", SVG_NS);
56
- page.setAttribute("width", String(totalWidth));
57
- page.setAttribute("height", String(totalHeight));
58
- page.setAttribute("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
59
- const plate = document.createElementNS(SVG_NS, "rect");
60
- plate.setAttribute("width", String(totalWidth));
61
- plate.setAttribute("height", String(totalHeight));
62
- plate.setAttribute("fill", background ?? readToken(element, "--chart-surface", "#ffffff"));
63
- page.append(plate);
64
- if (title) {
65
- const caption = document.createElementNS(SVG_NS, "text");
66
- caption.setAttribute("x", String(padding));
67
- caption.setAttribute("y", String(padding + 14));
68
- caption.setAttribute("fill", readToken(element, "--ink", "#101010"));
69
- caption.setAttribute("font-size", "13");
70
- caption.setAttribute("font-weight", "500");
71
- caption.setAttribute("font-family", getComputedStyle(element).fontFamily || "sans-serif");
72
- caption.textContent = title;
73
- page.append(caption);
74
- }
75
- const group = document.createElementNS(SVG_NS, "g");
76
- group.setAttribute("transform", `translate(${padding}, ${padding + titleBand})`);
77
- group.append(clone);
78
- page.append(group);
79
- const markup = new XMLSerializer().serializeToString(page);
80
- const url = URL.createObjectURL(
81
- new Blob([`<?xml version="1.0" encoding="UTF-8"?>
82
- ${markup}`], {
83
- type: "image/svg+xml;charset=utf-8"
84
- })
85
- );
86
- try {
87
- const image = await loadImage(url);
88
- const canvas = document.createElement("canvas");
89
- canvas.width = Math.ceil(totalWidth * scale);
90
- canvas.height = Math.ceil(totalHeight * scale);
91
- const context = canvas.getContext("2d");
92
- if (!context) throw new Error("chartToPng: this browser gave back no 2D canvas context");
93
- context.drawImage(image, 0, 0, canvas.width, canvas.height);
94
- return await canvasToBlob(canvas);
95
- } finally {
96
- URL.revokeObjectURL(url);
97
- }
12
+ const markup = serializeSvg(svg, {
13
+ padding,
14
+ background: background ?? readToken(element, "--chart-surface", "#ffffff"),
15
+ title,
16
+ titleColor: readToken(element, "--ink", "#101010"),
17
+ titleFont: getComputedStyle(element).fontFamily || "sans-serif"
18
+ });
19
+ return rasterize(markup, "png", { scale });
98
20
  }
99
21
  function findPlotSvg(element) {
100
22
  if (element.tagName.toLowerCase() === "svg") return element;
101
23
  const scope = element.querySelector('[data-slot="chart"]') ?? element;
102
24
  return scope.querySelector("svg.recharts-surface") ?? scope.querySelector("svg");
103
25
  }
104
- function inlinePaint(live, clone) {
105
- const computed = getComputedStyle(live);
106
- let css = "";
107
- for (const property of PAINTED_PROPERTIES) {
108
- const value = computed.getPropertyValue(property);
109
- if (value) css += `${property}:${value};`;
110
- }
111
- if (css) clone.setAttribute("style", css);
112
- const children = live.children;
113
- const clones = clone.children;
114
- for (let index = 0; index < children.length; index += 1) {
115
- const liveChild = children[index];
116
- const cloneChild = clones[index];
117
- if (liveChild && cloneChild) inlinePaint(liveChild, cloneChild);
118
- }
119
- }
120
- function readToken(element, name, fallback) {
121
- const value = getComputedStyle(element).getPropertyValue(name).trim();
122
- return value && !value.includes("var(") ? value : fallback;
123
- }
124
- function loadImage(url) {
125
- return new Promise((resolve, reject) => {
126
- const image = new Image();
127
- image.addEventListener("load", () => resolve(image));
128
- image.addEventListener(
129
- "error",
130
- () => reject(new Error("chartToPng: the browser refused to decode the serialised chart"))
131
- );
132
- image.src = url;
133
- });
134
- }
135
- function canvasToBlob(canvas) {
136
- return new Promise((resolve, reject) => {
137
- try {
138
- canvas.toBlob((blob) => {
139
- if (blob) resolve(blob);
140
- else reject(new Error("chartToPng: the canvas produced no PNG data"));
141
- }, "image/png");
142
- } catch (cause) {
143
- reject(
144
- new Error(
145
- "chartToPng: the canvas is tainted, which means the chart contains a cross-origin image",
146
- { cause }
147
- )
148
- );
149
- }
150
- });
151
- }
152
26
  const UTF8_BOM = "\uFEFF";
153
27
  function chartToCsv(rows, columns) {
154
28
  if (columns.length === 0) return UTF8_BOM;
@@ -176,24 +50,8 @@ function cellText(value) {
176
50
  if (value instanceof Date) return value.toISOString();
177
51
  return "";
178
52
  }
179
- function downloadBlob(blob, filename) {
180
- if (typeof document === "undefined") {
181
- throw new Error("downloadBlob: needs a browser document \u2014 call it from an event handler");
182
- }
183
- const url = URL.createObjectURL(blob);
184
- const anchor = document.createElement("a");
185
- anchor.href = url;
186
- anchor.download = filename;
187
- anchor.rel = "noopener";
188
- anchor.style.display = "none";
189
- document.body.append(anchor);
190
- anchor.click();
191
- anchor.remove();
192
- setTimeout(() => URL.revokeObjectURL(url), 0);
193
- }
194
53
  function exportFilename(title, extension) {
195
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
196
- return `${slug || "chart"}.${extension}`;
54
+ return figureFilename(title, extension, "chart");
197
55
  }
198
56
  export {
199
57
  chartToCsv,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/charts/lib/export.ts"],"sourcesContent":["import type { ChartColumn } from './figure'\n\n/**\n * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.\n *\n * Nothing here touches the DOM at module scope. Every browser API — `document`,\n * `canvas`, `URL.createObjectURL` — is reached for inside a function body, so\n * this module is safe to import from a server component and only fails when a\n * consumer actually asks for a file on a server, which is a call that cannot\n * succeed anywhere.\n */\n\nconst SVG_NS = 'http://www.w3.org/2000/svg'\n\n/**\n * The properties copied from the live DOM onto the serialised clone.\n *\n * Paint, geometry-adjacent paint, and type. Layout is deliberately absent: an\n * SVG's geometry is in its attributes, which the clone already carries, and\n * copying computed `width`/`height`/`transform` onto every node would fight\n * them.\n */\nconst PAINTED_PROPERTIES = [\n 'display',\n 'visibility',\n 'opacity',\n 'color',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'stroke',\n 'stroke-opacity',\n 'stroke-width',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stop-color',\n 'stop-opacity',\n 'flood-color',\n 'flood-opacity',\n 'mask',\n 'filter',\n 'clip-path',\n 'clip-rule',\n 'paint-order',\n 'mix-blend-mode',\n 'shape-rendering',\n 'text-anchor',\n 'dominant-baseline',\n 'font-family',\n 'font-size',\n 'font-style',\n 'font-weight',\n 'letter-spacing',\n 'text-transform',\n] as const\n\nexport interface ChartPngOptions {\n /**\n * Printed above the plot in the exported image. Pass the figure's title: a\n * PNG travels into a slide deck with none of the page's context around it,\n * and an unnamed plot in a deck is a plot nobody can cite.\n */\n title?: string\n /** Multiplier on the on-screen size. 2 is a retina-sharp export. */\n scale?: number\n /**\n * Painted behind the plot. Defaults to the resolved `--chart-surface`, which\n * is what makes a dark-mode export come out dark rather than transparent.\n */\n background?: string\n /** Space around the plot, in CSS pixels. */\n padding?: number\n}\n\n/**\n * The chart's `<svg>`, rasterised to a PNG blob.\n *\n * ## The problem this function exists to solve\n *\n * Serialising the `<svg>` and handing it to an `<img>` puts it in a document\n * of its own. That document has none of the page's stylesheets, so every\n * `var(--color-desktop-0)`, `var(--chart-fill)` and `var(--ink)` in the markup\n * resolves to nothing — and a paint that resolves to nothing is not a fallback\n * colour, it is an invisible mark. Serialise a chart naively and you get an\n * empty rectangle with axes on it.\n *\n * ## The solution\n *\n * Walk the live tree and the clone in step, and on every node write\n * `getComputedStyle`'s answer for the painted properties into an inline\n * `style`. Computed values have already had `var()` substituted by the engine,\n * so `fill: var(--color-desktop-0)` arrives as `fill: rgb(16, 16, 16)` — and,\n * because the walk reads the LIVE element, it reads it under whichever theme\n * is currently applied. Light and dark come out right for free, with no theme\n * argument anywhere in this file.\n *\n * `<stop>` elements are walked too, which is the half that is easy to miss: a\n * series is painted `fill=\"url(#…-colors-desktop)\"`, the url survives\n * serialisation because the gradient is inside the same `<svg>`, and it is the\n * gradient's STOPS that carry the custom properties.\n *\n * ## What this cannot capture, honestly\n *\n * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and\n * the hidden data table are DOM, not SVG. The PNG is the plot.\n * - **Web fonts.** The isolated document cannot fetch the page's `@font-face`\n * sources, so text is rasterised in whatever the resolved family stack finds\n * locally. Type metrics will differ from the screen.\n * - **Cross-origin images.** A chart with an external `<image>` in it taints\n * the canvas and `toBlob` throws; there is no way around it from script.\n * - **The exact rendering.** This is the browser's SVG rasteriser re-drawing\n * the markup, not a screenshot. Antialiasing, `backdrop-filter` and any\n * effect a page stylesheet applied from outside the `<svg>` are not in it.\n *\n * @example\n * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })\n * downloadBlob(blob, 'visitors-per-month.png')\n */\nexport async function chartToPng(\n element: HTMLElement | SVGSVGElement,\n options: ChartPngOptions = {},\n): Promise<Blob> {\n const { title, scale = 2, background, padding = 16 } = options\n\n const svg = findPlotSvg(element)\n if (!svg) throw new Error('chartToPng: found no <svg> to export inside the element it was given')\n\n const box = svg.getBoundingClientRect()\n const width = Math.ceil(box.width)\n const height = Math.ceil(box.height)\n if (width === 0 || height === 0) {\n // A chart that has not been measured yet has no size to export. Silently\n // producing a 0×0 PNG would look like a broken download rather than a\n // too-early call.\n throw new Error('chartToPng: the chart has no measured size yet — export it after it renders')\n }\n\n const clone = svg.cloneNode(true) as SVGSVGElement\n inlinePaint(svg, clone)\n clone.setAttribute('width', String(width))\n clone.setAttribute('height', String(height))\n\n const titleBand = title ? 30 : 0\n const totalWidth = width + padding * 2\n const totalHeight = height + padding * 2 + titleBand\n\n const page = document.createElementNS(SVG_NS, 'svg')\n page.setAttribute('xmlns', SVG_NS)\n page.setAttribute('width', String(totalWidth))\n page.setAttribute('height', String(totalHeight))\n page.setAttribute('viewBox', `0 0 ${totalWidth} ${totalHeight}`)\n\n const plate = document.createElementNS(SVG_NS, 'rect')\n plate.setAttribute('width', String(totalWidth))\n plate.setAttribute('height', String(totalHeight))\n plate.setAttribute('fill', background ?? readToken(element, '--chart-surface', '#ffffff'))\n page.append(plate)\n\n if (title) {\n const caption = document.createElementNS(SVG_NS, 'text')\n caption.setAttribute('x', String(padding))\n caption.setAttribute('y', String(padding + 14))\n caption.setAttribute('fill', readToken(element, '--ink', '#101010'))\n caption.setAttribute('font-size', '13')\n caption.setAttribute('font-weight', '500')\n caption.setAttribute('font-family', getComputedStyle(element).fontFamily || 'sans-serif')\n caption.textContent = title\n page.append(caption)\n }\n\n const group = document.createElementNS(SVG_NS, 'g')\n group.setAttribute('transform', `translate(${padding}, ${padding + titleBand})`)\n group.append(clone)\n page.append(group)\n\n const markup = new XMLSerializer().serializeToString(page)\n const url = URL.createObjectURL(\n new Blob([`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${markup}`], {\n type: 'image/svg+xml;charset=utf-8',\n }),\n )\n\n try {\n const image = await loadImage(url)\n const canvas = document.createElement('canvas')\n canvas.width = Math.ceil(totalWidth * scale)\n canvas.height = Math.ceil(totalHeight * scale)\n\n const context = canvas.getContext('2d')\n if (!context) throw new Error('chartToPng: this browser gave back no 2D canvas context')\n context.drawImage(image, 0, 0, canvas.width, canvas.height)\n\n return await canvasToBlob(canvas)\n } finally {\n URL.revokeObjectURL(url)\n }\n}\n\n/**\n * The plot's `<svg>`, not the first `<svg>` in the subtree.\n *\n * The distinction matters because the toolbar sits inside the same wrapper and\n * every icon in it is an `<svg>`. Narrowing to the measured chart box first is\n * what stops an export of a chart from being a picture of a magnifying glass.\n */\nfunction findPlotSvg(element: HTMLElement | SVGSVGElement): SVGSVGElement | null {\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n const scope = element.querySelector('[data-slot=\"chart\"]') ?? element\n return scope.querySelector<SVGSVGElement>('svg.recharts-surface') ?? scope.querySelector('svg')\n}\n\n/** Copies the resolved paint of every node in `live` onto the matching node in `clone`. */\nfunction inlinePaint(live: Element, clone: Element): void {\n const computed = getComputedStyle(live)\n let css = ''\n for (const property of PAINTED_PROPERTIES) {\n const value = computed.getPropertyValue(property)\n if (value) css += `${property}:${value};`\n }\n if (css) clone.setAttribute('style', css)\n\n // The clone is a faithful deep copy, so child lists line up by index.\n const children = live.children\n const clones = clone.children\n for (let index = 0; index < children.length; index += 1) {\n const liveChild = children[index]\n const cloneChild = clones[index]\n if (liveChild && cloneChild) inlinePaint(liveChild, cloneChild)\n }\n}\n\n/**\n * A custom property, resolved against the live element.\n *\n * `var()` inside a custom property is substituted at computed-value time, so\n * `--chart-surface: var(--paper)` reads back as a colour. The guard is for the\n * case where it did not — an unregistered name, or a value the engine left as\n * a token stream — because writing `var(--paper)` into a serialised SVG paints\n * nothing at all.\n */\nfunction readToken(element: Element, name: string, fallback: string): string {\n const value = getComputedStyle(element).getPropertyValue(name).trim()\n return value && !value.includes('var(') ? value : fallback\n}\n\nfunction loadImage(url: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.addEventListener('load', () => resolve(image))\n image.addEventListener('error', () =>\n reject(new Error('chartToPng: the browser refused to decode the serialised chart')),\n )\n image.src = url\n })\n}\n\nfunction canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) => {\n try {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob)\n else reject(new Error('chartToPng: the canvas produced no PNG data'))\n }, 'image/png')\n } catch (cause) {\n // A cross-origin image anywhere in the plot taints the canvas and this\n // throws SecurityError. Nothing in script can untaint it, so the honest\n // move is to say which failure it was.\n reject(\n new Error(\n 'chartToPng: the canvas is tainted, which means the chart contains a cross-origin image',\n { cause },\n ),\n )\n }\n })\n}\n\n/**\n * A UTF-8 byte order mark.\n *\n * Excel does not sniff encodings. Handed a CSV with no BOM it decodes the file\n * with the machine's legacy code page, so \"Müller\", \"東京\" and \"€\" arrive as\n * mojibake on most Windows installs — and the reader's conclusion is that the\n * export is broken, not that the encoding was guessed. The BOM is the only\n * in-band signal Excel honours, and every other tool skips it silently.\n */\nconst UTF8_BOM = '\\uFEFF'\n\n/**\n * The chart's rows as RFC 4180 CSV.\n *\n * Quotes a field only when it needs quoting — a comma, a double quote, or a\n * line break in it — doubles any quote inside, and separates records with CRLF\n * as the RFC requires rather than with the `\\n` a Unix habit would reach for.\n *\n * Numbers are written unformatted on purpose. `1,234` would need quoting and\n * would then land in the spreadsheet as text, which is the one thing a numeric\n * export must not do; formatting belongs to the chart, not to the file.\n *\n * A column's `label` is a `ReactNode`, so only a string or a number can be a\n * header. Anything else falls back to the column's key, which is a worse\n * header than the label but a better one than an empty cell.\n *\n * @example\n * const csv = chartToCsv(data, [{ key: 'desktop', label: 'Desktop' }])\n * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')\n */\nexport function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string {\n if (columns.length === 0) return UTF8_BOM\n\n const lines = [columns.map((column) => csvField(headerText(column))).join(',')]\n\n for (const row of rows) {\n lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(','))\n }\n\n // The RFC makes the last CRLF optional; writing it matches what a\n // spreadsheet emits and keeps the file from ending mid-record in a terminal.\n return `${UTF8_BOM}${lines.join('\\r\\n')}\\r\\n`\n}\n\n/** One field, quoted only when the RFC says it has to be. */\nfunction csvField(value: string): string {\n if (!/[\",\\r\\n]/.test(value)) return value\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n}\n\n/** A column's header text, falling back to its key when the label is markup. */\nfunction headerText(column: ChartColumn): string {\n const { label } = column\n if (typeof label === 'string') return label\n if (typeof label === 'number') return String(label)\n return column.key\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return ''\n}\n\n/**\n * Hands a blob to the browser as a download.\n *\n * Throws rather than no-oping without a `document`: a download is something a\n * person just asked for, and a silent return would look to them like a click\n * that did nothing.\n */\nexport function downloadBlob(blob: Blob, filename: string): void {\n if (typeof document === 'undefined') {\n throw new Error('downloadBlob: needs a browser document — call it from an event handler')\n }\n\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n anchor.style.display = 'none'\n document.body.append(anchor)\n anchor.click()\n anchor.remove()\n\n // Revoking in the same tick cancels the download in Safari; one turn of the\n // event loop is enough for the navigation to have started.\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * A figure's title as a filename.\n *\n * Lowercase, ASCII-safe and hyphenated, because a downloaded file crosses into\n * shells, zip archives and Windows paths where a title's spaces and slashes\n * are someone else's problem.\n */\nexport function exportFilename(title: string, extension: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return `${slug || 'chart'}.${extension}`\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/charts/lib/export.ts"],"sourcesContent":["import type { ChartColumn } from './figure'\nimport {\n downloadBlob,\n exportFilename as figureFilename,\n rasterize,\n readToken,\n serializeSvg,\n} from '../../lib/svg-export'\n\n/**\n * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.\n *\n * The general half — walking a live tree to inline its resolved paint,\n * serialising a standalone SVG document, rasterising one through a canvas — is\n * not chart-shaped at all, and lives in `src/lib/svg-export.ts` shared with the\n * diagrams entry. What is left here is what only a Recharts chart knows: which\n * `<svg>` in the subtree is the plot, what a row of chart data looks like as a\n * CSV record, and that the ground behind an exported plot is `--chart-surface`.\n *\n * Nothing here touches the DOM at module scope. Every browser API is reached\n * for inside a function body, so this module is safe to import from a server\n * component and only fails when a consumer actually asks for a file on a\n * server, which is a call that cannot succeed anywhere.\n */\n\nexport interface ChartPngOptions {\n /**\n * Printed above the plot in the exported image. Pass the figure's title: a\n * PNG travels into a slide deck with none of the page's context around it,\n * and an unnamed plot in a deck is a plot nobody can cite.\n */\n title?: string\n /** Multiplier on the on-screen size. 2 is a retina-sharp export. */\n scale?: number\n /**\n * Painted behind the plot. Defaults to the resolved `--chart-surface`, which\n * is what makes a dark-mode export come out dark rather than transparent.\n */\n background?: string\n /** Space around the plot, in CSS pixels. */\n padding?: number\n}\n\n/**\n * The chart's `<svg>`, rasterised to a PNG blob.\n *\n * Three chart-specific decisions and nothing else. `serializeSvg` does the\n * work, and its comment carries the reason a naive `XMLSerializer` export of a\n * themed chart comes out as an empty rectangle with axes on it.\n *\n * 1. WHICH `<svg>` — the plot, not the first one in the subtree. See\n * `findPlotSvg`.\n * 2. WHAT GROUND — `--chart-surface`, resolved against the live element, which\n * is what makes a dark-mode export come out dark rather than transparent.\n * 3. WHAT INK AND FACE for the caption — `--ink` and the element's own\n * resolved family, so the exported title matches the page it came from.\n *\n * ## What this cannot capture, honestly\n *\n * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and\n * the hidden data table are DOM, not SVG. The PNG is the plot.\n * - **Web fonts, cross-origin images, and the exact rendering.** See\n * `serializeSvg` and `rasterize`.\n *\n * @example\n * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })\n * downloadBlob(blob, exportFilename('Visitors per month', 'png'))\n */\nexport async function chartToPng(\n element: HTMLElement | SVGSVGElement,\n options: ChartPngOptions = {},\n): Promise<Blob> {\n const { title, scale = 2, background, padding = 16 } = options\n\n const svg = findPlotSvg(element)\n if (!svg) throw new Error('chartToPng: found no <svg> to export inside the element it was given')\n\n const markup = serializeSvg(svg, {\n padding,\n background: background ?? readToken(element, '--chart-surface', '#ffffff'),\n title,\n titleColor: readToken(element, '--ink', '#101010'),\n titleFont: getComputedStyle(element).fontFamily || 'sans-serif',\n })\n\n return rasterize(markup, 'png', { scale })\n}\n\n/**\n * The plot's `<svg>`, not the first `<svg>` in the subtree.\n *\n * Two narrowings, and both are load-bearing. The toolbar sits outside the plot\n * wrapper and every control in it is an `<svg>`, so the scope comes first;\n * INSIDE the wrapper the legend draws its own swatches, so `.recharts-surface`\n * comes second. Either one missing exports a picture of an icon, which looks\n * like a working download until somebody opens the file.\n */\nfunction findPlotSvg(element: HTMLElement | SVGSVGElement): SVGSVGElement | null {\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n const scope = element.querySelector('[data-slot=\"chart\"]') ?? element\n return scope.querySelector<SVGSVGElement>('svg.recharts-surface') ?? scope.querySelector('svg')\n}\n\n/**\n * A UTF-8 byte order mark.\n *\n * Excel does not sniff encodings. Handed a CSV with no BOM it decodes the file\n * with the machine's legacy code page, so \"Müller\", \"東京\" and \"€\" arrive as\n * mojibake on most Windows installs — and the reader's conclusion is that the\n * export is broken, not that the encoding was guessed. The BOM is the only\n * in-band signal Excel honours, and every other tool skips it silently.\n */\nconst UTF8_BOM = '\\uFEFF'\n\n/**\n * The chart's rows as RFC 4180 CSV.\n *\n * Quotes a field only when it needs quoting — a comma, a double quote, or a\n * line break in it — doubles any quote inside, and separates records with CRLF\n * as the RFC requires rather than with the `\\n` a Unix habit would reach for.\n *\n * Numbers are written unformatted on purpose. `1,234` would need quoting and\n * would then land in the spreadsheet as text, which is the one thing a numeric\n * export must not do; formatting belongs to the chart, not to the file.\n *\n * A column's `label` is a `ReactNode`, so only a string or a number can be a\n * header. Anything else falls back to the column's key, which is a worse\n * header than the label but a better one than an empty cell.\n *\n * @example\n * const csv = chartToCsv(data, [{ key: 'desktop', label: 'Desktop' }])\n * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')\n */\nexport function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string {\n if (columns.length === 0) return UTF8_BOM\n\n const lines = [columns.map((column) => csvField(headerText(column))).join(',')]\n\n for (const row of rows) {\n lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(','))\n }\n\n // The RFC makes the last CRLF optional; writing it matches what a\n // spreadsheet emits and keeps the file from ending mid-record in a terminal.\n return `${UTF8_BOM}${lines.join('\\r\\n')}\\r\\n`\n}\n\n/** One field, quoted only when the RFC says it has to be. */\nfunction csvField(value: string): string {\n if (!/[\",\\r\\n]/.test(value)) return value\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n}\n\n/** A column's header text, falling back to its key when the label is markup. */\nfunction headerText(column: ChartColumn): string {\n const { label } = column\n if (typeof label === 'string') return label\n if (typeof label === 'number') return String(label)\n return column.key\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return ''\n}\n\nexport { downloadBlob }\n\n/**\n * A figure's title as a filename.\n *\n * The shared slugger, with `chart` rather than `figure` as the fallback for a\n * title that slugs to nothing — an export from this entry is a chart, and the\n * name a reader finds in their downloads folder should say so.\n */\nexport function exportFilename(title: string, extension: string): string {\n return figureFilename(title, extension, 'chart')\n}\n"],"mappings":"AACA;AAAA,EACE;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA6DP,eAAsB,WACpB,SACA,UAA2B,CAAC,GACb;AACf,QAAM,EAAE,OAAO,QAAQ,GAAG,YAAY,UAAU,GAAG,IAAI;AAEvD,QAAM,MAAM,YAAY,OAAO;AAC/B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,sEAAsE;AAEhG,QAAM,SAAS,aAAa,KAAK;AAAA,IAC/B;AAAA,IACA,YAAY,cAAc,UAAU,SAAS,mBAAmB,SAAS;AAAA,IACzE;AAAA,IACA,YAAY,UAAU,SAAS,SAAS,SAAS;AAAA,IACjD,WAAW,iBAAiB,OAAO,EAAE,cAAc;AAAA,EACrD,CAAC;AAED,SAAO,UAAU,QAAQ,OAAO,EAAE,MAAM,CAAC;AAC3C;AAWA,SAAS,YAAY,SAA4D;AAC/E,MAAI,QAAQ,QAAQ,YAAY,MAAM,MAAO,QAAO;AACpD,QAAM,QAAQ,QAAQ,cAAc,qBAAqB,KAAK;AAC9D,SAAO,MAAM,cAA6B,sBAAsB,KAAK,MAAM,cAAc,KAAK;AAChG;AAWA,MAAM,WAAW;AAqBV,SAAS,WAAW,MAAiC,SAAgC;AAC1F,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,QAAQ,CAAC,QAAQ,IAAI,CAAC,WAAW,SAAS,WAAW,MAAM,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAE9E,aAAW,OAAO,MAAM;AACtB,UAAM,KAAK,QAAQ,IAAI,CAAC,WAAW,SAAS,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,EACnF;AAIA,SAAO,GAAG,QAAQ,GAAG,MAAM,KAAK,MAAM,CAAC;AAAA;AACzC;AAGA,SAAS,SAAS,OAAuB;AACvC,MAAI,CAAC,WAAW,KAAK,KAAK,EAAG,QAAO;AACpC,SAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,CAAC;AACtC;AAGA,SAAS,WAAW,QAA6B;AAC/C,QAAM,EAAE,MAAM,IAAI;AAClB,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK;AAClD,SAAO,OAAO;AAChB;AAGA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI;AAC/E,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAChF,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACT;AAWO,SAAS,eAAe,OAAe,WAA2B;AACvE,SAAO,eAAe,OAAO,WAAW,OAAO;AACjD;","names":[]}
@@ -0,0 +1,49 @@
1
+ import * as react from 'react';
2
+ import { FigureChrome } from '../lib/frame.js';
3
+ import { ArchitectureSpec } from '../spec.js';
4
+ import '../lib/geometry.js';
5
+
6
+ interface ArchitectureFigureProps extends FigureChrome {
7
+ spec: ArchitectureSpec;
8
+ }
9
+ /**
10
+ * A component map: services, datastores, boundaries, and what talks to what.
11
+ *
12
+ * Takes the same JSON an archify `architecture` specification carries —
13
+ * `components`, `boundaries`, `connections` — and draws it in this system's own
14
+ * terms: paper plates on a hairline, one reversed plate for the component the
15
+ * diagram is about, and seven drawn sigils where archify uses seven hues.
16
+ *
17
+ * IT RENDERS ON A SERVER because every position is already in the
18
+ * specification. A component gives a `row` and a `col` into a grid whose cell
19
+ * size is either declared or defaulted, or it gives an absolute `pos` — nothing
20
+ * here is solved for, relaxed, or measured. So the markup is a pure function of
21
+ * the input, it is identical on the server and in the browser, and there is no
22
+ * layout shift on hydration because there is no layout to do.
23
+ *
24
+ * BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A `region` is where
25
+ * something RUNS — a VPC, a zone, a cluster — and is a solid frame. A
26
+ * `security-group` is what may REACH it, and is dashed. That is not decoration:
27
+ * an infrastructure diagram is very often read for exactly one of those two
28
+ * questions, and a reader should be able to tell which line answers which
29
+ * without reading either label.
30
+ *
31
+ * @example
32
+ * <ArchitectureFigure
33
+ * spec={{
34
+ * meta: { title: 'Request path' },
35
+ * components: [
36
+ * { id: 'cdn', type: 'cloud', label: 'CloudFront', row: 0, col: 0 },
37
+ * { id: 'api', type: 'backend', label: 'FastAPI', sublabel: 'ECS', row: 0, col: 1 },
38
+ * { id: 'db', type: 'database', label: 'Postgres', row: 0, col: 2 },
39
+ * ],
40
+ * connections: [
41
+ * { from: 'cdn', to: 'api', label: 'HTTPS' },
42
+ * { from: 'api', to: 'db', label: 'SQL' },
43
+ * ],
44
+ * }}
45
+ * />
46
+ */
47
+ declare function ArchitectureFigure({ spec, className, legend, cards, heading, activeIds, onSelectNode, }: ArchitectureFigureProps): react.JSX.Element;
48
+
49
+ export { ArchitectureFigure, type ArchitectureFigureProps, ArchitectureFigure as default };
@@ -0,0 +1,135 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useMemo } from "react";
4
+ import { DiagramFrame } from "../lib/frame.js";
5
+ import { inflate, TYPE, union, wrapText } from "../lib/geometry.js";
6
+ import { kindLegend, resolveLegend } from "../lib/legend.js";
7
+ import { BandFrame, NodePlate, PLATE, plateHeight } from "../lib/marks.js";
8
+ import { renderWires, wiresFor } from "../lib/wires.js";
9
+ const KINDS = [
10
+ "frontend",
11
+ "backend",
12
+ "database",
13
+ "cloud",
14
+ "security",
15
+ "messagebus",
16
+ "external"
17
+ ];
18
+ const GRID = { origin: [40, 48], cols: 4, cellW: 184, cellH: 72, gapX: 64, gapY: 76 };
19
+ function ArchitectureFigure({
20
+ spec,
21
+ className,
22
+ legend = "auto",
23
+ cards = true,
24
+ heading = true,
25
+ activeIds,
26
+ onSelectNode
27
+ }) {
28
+ const uid = useId().replace(/:/g, "");
29
+ const model = useMemo(() => buildModel(spec, uid, activeIds, onSelectNode), [spec, uid, activeIds, onSelectNode]);
30
+ return /* @__PURE__ */ jsx(
31
+ DiagramFrame,
32
+ {
33
+ meta: spec.meta,
34
+ model,
35
+ cards: spec.cards,
36
+ showCards: cards,
37
+ heading,
38
+ legend,
39
+ className,
40
+ activeIds,
41
+ onSelectNode
42
+ }
43
+ );
44
+ }
45
+ function buildModel(spec, uid, activeIds, onSelectNode) {
46
+ const layout = { ...GRID, ...spec.layout };
47
+ const [originX, originY] = layout.origin;
48
+ const boxes = /* @__PURE__ */ new Map();
49
+ const placed = spec.components.map((component) => {
50
+ const labelWidth = component.size?.[0] ?? component.width ?? GRID.cellW;
51
+ const lines = wrapText(component.label, TYPE.label, labelWidth - PLATE.padX * 2, 2);
52
+ const height = plateHeight(
53
+ lines.length,
54
+ Boolean(component.sublabel),
55
+ Boolean(component.type || component.tag),
56
+ component.size?.[1] ?? component.height ?? 0
57
+ );
58
+ const x = component.pos ? component.pos[0] : originX + (component.col ?? 0) * (layout.cellW + layout.gapX);
59
+ const y = component.pos ? component.pos[1] : originY + (component.row ?? 0) * (layout.cellH + layout.gapY);
60
+ const box = { x, y, w: labelWidth, h: height };
61
+ boxes.set(component.id, box);
62
+ return { component, box };
63
+ });
64
+ const boundaries = (spec.boundaries ?? []).map((boundary) => {
65
+ const wrapped = boundary.wraps.map((id) => boxes.get(id)).filter((box) => Boolean(box));
66
+ const extent2 = union(wrapped);
67
+ return { boundary, box: extent2 ? inflate(extent2, boundary.pad ?? 28) : null };
68
+ });
69
+ const absolute = new Set(
70
+ spec.components.filter((component) => component.pos).map((component) => component.id)
71
+ );
72
+ const wires = wiresFor(spec.connections, boxes, (connection) => ({
73
+ keepWaypoints: absolute.has(connection.from) && absolute.has(connection.to)
74
+ }));
75
+ const { paths, labels } = renderWires(wires, uid);
76
+ const drawn = [...boxes.values(), ...boundaries.map((entry) => entry.box).filter((box) => Boolean(box))];
77
+ const extent = inflate(union(drawn) ?? { x: 0, y: 0, w: 400, h: 200 }, 22);
78
+ const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null;
79
+ const artwork = /* @__PURE__ */ jsxs(Fragment, { children: [
80
+ boundaries.map(
81
+ ({ boundary, box }, index) => box ? /* @__PURE__ */ jsx(
82
+ BandFrame,
83
+ {
84
+ box,
85
+ label: boundary.label,
86
+ dashed: boundary.kind === "security-group"
87
+ },
88
+ `${boundary.label}-${index}`
89
+ ) : null
90
+ ),
91
+ paths,
92
+ placed.map(({ component, box }) => /* @__PURE__ */ jsx(
93
+ NodePlate,
94
+ {
95
+ nodeId: component.id,
96
+ box,
97
+ label: component.label,
98
+ sublabel: component.sublabel,
99
+ tag: component.tag,
100
+ kind: component.type,
101
+ active: active?.has(component.id),
102
+ dimmed: active ? !active.has(component.id) : false,
103
+ onSelect: onSelectNode ? () => onSelectNode(component.id) : void 0
104
+ },
105
+ component.id
106
+ )),
107
+ labels
108
+ ] });
109
+ const used = [...new Set(spec.components.map((component) => component.type))];
110
+ return {
111
+ extent,
112
+ artwork,
113
+ nodes: spec.components.map((component) => ({
114
+ id: component.id,
115
+ label: component.label,
116
+ sublabel: component.sublabel,
117
+ kind: component.type
118
+ })),
119
+ edges: (spec.connections ?? []).map((connection) => ({
120
+ from: connection.from,
121
+ to: connection.to,
122
+ label: connection.label
123
+ })),
124
+ legend: kindLegend(
125
+ resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),
126
+ spec.meta.legend?.entries
127
+ )
128
+ };
129
+ }
130
+ var ArchitectureFigure_default = ArchitectureFigure;
131
+ export {
132
+ ArchitectureFigure,
133
+ ArchitectureFigure_default as default
134
+ };
135
+ //# sourceMappingURL=ArchitectureFigure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/ArchitectureFigure/ArchitectureFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { DiagramFrame, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { BandFrame, NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { ArchitectureSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\n/** What a cell is, when the specification does not say. */\nconst GRID = { origin: [40, 48] as [number, number], cols: 4, cellW: 184, cellH: 72, gapX: 64, gapY: 76 }\n\nexport interface ArchitectureFigureProps extends FigureChrome {\n spec: ArchitectureSpec\n}\n\n/**\n * A component map: services, datastores, boundaries, and what talks to what.\n *\n * Takes the same JSON an archify `architecture` specification carries —\n * `components`, `boundaries`, `connections` — and draws it in this system's own\n * terms: paper plates on a hairline, one reversed plate for the component the\n * diagram is about, and seven drawn sigils where archify uses seven hues.\n *\n * IT RENDERS ON A SERVER because every position is already in the\n * specification. A component gives a `row` and a `col` into a grid whose cell\n * size is either declared or defaulted, or it gives an absolute `pos` — nothing\n * here is solved for, relaxed, or measured. So the markup is a pure function of\n * the input, it is identical on the server and in the browser, and there is no\n * layout shift on hydration because there is no layout to do.\n *\n * BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A `region` is where\n * something RUNS — a VPC, a zone, a cluster — and is a solid frame. A\n * `security-group` is what may REACH it, and is dashed. That is not decoration:\n * an infrastructure diagram is very often read for exactly one of those two\n * questions, and a reader should be able to tell which line answers which\n * without reading either label.\n *\n * @example\n * <ArchitectureFigure\n * spec={{\n * meta: { title: 'Request path' },\n * components: [\n * { id: 'cdn', type: 'cloud', label: 'CloudFront', row: 0, col: 0 },\n * { id: 'api', type: 'backend', label: 'FastAPI', sublabel: 'ECS', row: 0, col: 1 },\n * { id: 'db', type: 'database', label: 'Postgres', row: 0, col: 2 },\n * ],\n * connections: [\n * { from: 'cdn', to: 'api', label: 'HTTPS' },\n * { from: 'api', to: 'db', label: 'SQL' },\n * ],\n * }}\n * />\n */\nexport function ArchitectureFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: ArchitectureFigureProps) {\n const uid = useId().replace(/:/g, '')\n const model = useMemo(() => buildModel(spec, uid, activeIds, onSelectNode), [spec, uid, activeIds, onSelectNode])\n\n return (\n <DiagramFrame\n meta={spec.meta}\n model={model}\n cards={spec.cards}\n showCards={cards}\n heading={heading}\n legend={legend}\n className={className}\n activeIds={activeIds}\n onSelectNode={onSelectNode}\n />\n )\n}\n\nfunction buildModel(\n spec: ArchitectureSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const layout = { ...GRID, ...spec.layout }\n const [originX, originY] = layout.origin\n\n const boxes = new Map<string, Box>()\n const placed = spec.components.map((component) => {\n const labelWidth = component.size?.[0] ?? component.width ?? GRID.cellW\n const lines = wrapText(component.label, TYPE.label, labelWidth - PLATE.padX * 2, 2)\n const height = plateHeight(\n lines.length,\n Boolean(component.sublabel),\n Boolean(component.type || component.tag),\n component.size?.[1] ?? component.height ?? 0,\n )\n\n const x = component.pos ? component.pos[0] : originX + (component.col ?? 0) * (layout.cellW + layout.gapX)\n const y = component.pos ? component.pos[1] : originY + (component.row ?? 0) * (layout.cellH + layout.gapY)\n\n const box: Box = { x, y, w: labelWidth, h: height }\n boxes.set(component.id, box)\n return { component, box }\n })\n\n const boundaries = (spec.boundaries ?? []).map((boundary) => {\n const wrapped = boundary.wraps.map((id) => boxes.get(id)).filter((box): box is Box => Boolean(box))\n const extent = union(wrapped)\n return { boundary, box: extent ? inflate(extent, boundary.pad ?? 28) : null }\n })\n\n // A waypoint is only meaningful when both ends were placed absolutely — see\n // `Wire.keepWaypoints`. In this diagram type that is common, which is why an\n // archify architecture specification's hand-tuned routes survive the move.\n const absolute = new Set(\n spec.components.filter((component) => component.pos).map((component) => component.id),\n )\n const wires = wiresFor(spec.connections, boxes, (connection) => ({\n keepWaypoints: absolute.has(connection.from) && absolute.has(connection.to),\n }))\n const { paths, labels } = renderWires(wires, uid)\n\n const drawn = [...boxes.values(), ...boundaries.map((entry) => entry.box).filter((box): box is Box => Boolean(box))]\n const extent = inflate(union(drawn) ?? { x: 0, y: 0, w: 400, h: 200 }, 22)\n\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {boundaries.map(({ boundary, box }, index) =>\n box ? (\n <BandFrame\n key={`${boundary.label}-${index}`}\n box={box}\n label={boundary.label}\n dashed={boundary.kind === 'security-group'}\n />\n ) : null,\n )}\n {paths}\n {placed.map(({ component, box }) => (\n <NodePlate\n key={component.id}\n nodeId={component.id}\n box={box}\n label={component.label}\n sublabel={component.sublabel}\n tag={component.tag}\n kind={component.type}\n active={active?.has(component.id)}\n dimmed={active ? !active.has(component.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(component.id) : undefined}\n />\n ))}\n {labels}\n </>\n )\n\n const used = [...new Set(spec.components.map((component) => component.type))]\n\n return {\n extent,\n artwork,\n nodes: spec.components.map((component) => ({\n id: component.id,\n label: component.label,\n sublabel: component.sublabel,\n kind: component.type,\n })),\n edges: (spec.connections ?? []).map((connection) => ({\n from: connection.from,\n to: connection.to,\n label: connection.label,\n })),\n legend: kindLegend(\n resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),\n spec.meta.legend?.entries,\n ),\n }\n}\n\nexport default ArchitectureFigure\n"],"mappings":"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","names":["extent"]}