@misoto22/design 0.6.1 → 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 +1 -1
  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 +1 -1
  11. package/dist/agent/BarList.md +1 -1
  12. package/dist/agent/BigNumber.md +1 -1
  13. package/dist/agent/BoxPlot.md +1 -1
  14. package/dist/agent/Breadcrumb.md +1 -1
  15. package/dist/agent/BulletChart.md +1 -1
  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 +1 -1
  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 +1 -1
  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 +1 -1
  43. package/dist/agent/Heatmap.md +1 -1
  44. package/dist/agent/Histogram.md +1 -1
  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 +1 -1
  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 +1 -1
  54. package/dist/agent/Popover.md +1 -1
  55. package/dist/agent/Progress.md +1 -1
  56. package/dist/agent/RadarChart.md +1 -1
  57. package/dist/agent/RadialChart.md +1 -1
  58. package/dist/agent/RadioGroup.md +1 -1
  59. package/dist/agent/SankeyChart.md +1 -1
  60. package/dist/agent/ScatterChart.md +1 -1
  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 +1 -1
  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 +1 -1
  83. package/dist/agent/WaterfallChart.md +1 -1
  84. package/dist/agent/WorkflowFigure.md +28 -0
  85. package/dist/agent/catalog.json +223 -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 +21 -7
@@ -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":"AAYA,MAAM,SAAS;AAUf,MAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgEA,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,MAAM,IAAI,sBAAsB;AACtC,QAAM,QAAQ,KAAK,KAAK,IAAI,KAAK;AACjC,QAAM,SAAS,KAAK,KAAK,IAAI,MAAM;AACnC,MAAI,UAAU,KAAK,WAAW,GAAG;AAI/B,UAAM,IAAI,MAAM,kFAA6E;AAAA,EAC/F;AAEA,QAAM,QAAQ,IAAI,UAAU,IAAI;AAChC,cAAY,KAAK,KAAK;AACtB,QAAM,aAAa,SAAS,OAAO,KAAK,CAAC;AACzC,QAAM,aAAa,UAAU,OAAO,MAAM,CAAC;AAE3C,QAAM,YAAY,QAAQ,KAAK;AAC/B,QAAM,aAAa,QAAQ,UAAU;AACrC,QAAM,cAAc,SAAS,UAAU,IAAI;AAE3C,QAAM,OAAO,SAAS,gBAAgB,QAAQ,KAAK;AACnD,OAAK,aAAa,SAAS,MAAM;AACjC,OAAK,aAAa,SAAS,OAAO,UAAU,CAAC;AAC7C,OAAK,aAAa,UAAU,OAAO,WAAW,CAAC;AAC/C,OAAK,aAAa,WAAW,OAAO,UAAU,IAAI,WAAW,EAAE;AAE/D,QAAM,QAAQ,SAAS,gBAAgB,QAAQ,MAAM;AACrD,QAAM,aAAa,SAAS,OAAO,UAAU,CAAC;AAC9C,QAAM,aAAa,UAAU,OAAO,WAAW,CAAC;AAChD,QAAM,aAAa,QAAQ,cAAc,UAAU,SAAS,mBAAmB,SAAS,CAAC;AACzF,OAAK,OAAO,KAAK;AAEjB,MAAI,OAAO;AACT,UAAM,UAAU,SAAS,gBAAgB,QAAQ,MAAM;AACvD,YAAQ,aAAa,KAAK,OAAO,OAAO,CAAC;AACzC,YAAQ,aAAa,KAAK,OAAO,UAAU,EAAE,CAAC;AAC9C,YAAQ,aAAa,QAAQ,UAAU,SAAS,SAAS,SAAS,CAAC;AACnE,YAAQ,aAAa,aAAa,IAAI;AACtC,YAAQ,aAAa,eAAe,KAAK;AACzC,YAAQ,aAAa,eAAe,iBAAiB,OAAO,EAAE,cAAc,YAAY;AACxF,YAAQ,cAAc;AACtB,SAAK,OAAO,OAAO;AAAA,EACrB;AAEA,QAAM,QAAQ,SAAS,gBAAgB,QAAQ,GAAG;AAClD,QAAM,aAAa,aAAa,aAAa,OAAO,KAAK,UAAU,SAAS,GAAG;AAC/E,QAAM,OAAO,KAAK;AAClB,OAAK,OAAO,KAAK;AAEjB,QAAM,SAAS,IAAI,cAAc,EAAE,kBAAkB,IAAI;AACzD,QAAM,MAAM,IAAI;AAAA,IACd,IAAI,KAAK,CAAC;AAAA,EAA2C,MAAM,EAAE,GAAG;AAAA,MAC9D,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,GAAG;AACjC,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ,KAAK,KAAK,aAAa,KAAK;AAC3C,WAAO,SAAS,KAAK,KAAK,cAAc,KAAK;AAE7C,UAAM,UAAU,OAAO,WAAW,IAAI;AACtC,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,yDAAyD;AACvF,YAAQ,UAAU,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAE1D,WAAO,MAAM,aAAa,MAAM;AAAA,EAClC,UAAE;AACA,QAAI,gBAAgB,GAAG;AAAA,EACzB;AACF;AASA,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;AAGA,SAAS,YAAY,MAAe,OAAsB;AACxD,QAAM,WAAW,iBAAiB,IAAI;AACtC,MAAI,MAAM;AACV,aAAW,YAAY,oBAAoB;AACzC,UAAM,QAAQ,SAAS,iBAAiB,QAAQ;AAChD,QAAI,MAAO,QAAO,GAAG,QAAQ,IAAI,KAAK;AAAA,EACxC;AACA,MAAI,IAAK,OAAM,aAAa,SAAS,GAAG;AAGxC,QAAM,WAAW,KAAK;AACtB,QAAM,SAAS,MAAM;AACrB,WAAS,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS,GAAG;AACvD,UAAM,YAAY,SAAS,KAAK;AAChC,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,aAAa,WAAY,aAAY,WAAW,UAAU;AAAA,EAChE;AACF;AAWA,SAAS,UAAU,SAAkB,MAAc,UAA0B;AAC3E,QAAM,QAAQ,iBAAiB,OAAO,EAAE,iBAAiB,IAAI,EAAE,KAAK;AACpE,SAAO,SAAS,CAAC,MAAM,SAAS,MAAM,IAAI,QAAQ;AACpD;AAEA,SAAS,UAAU,KAAwC;AACzD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,QAAQ,IAAI,MAAM;AACxB,UAAM,iBAAiB,QAAQ,MAAM,QAAQ,KAAK,CAAC;AACnD,UAAM;AAAA,MAAiB;AAAA,MAAS,MAC9B,OAAO,IAAI,MAAM,gEAAgE,CAAC;AAAA,IACpF;AACA,UAAM,MAAM;AAAA,EACd,CAAC;AACH;AAEA,SAAS,aAAa,QAA0C;AAC9D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI;AACF,aAAO,OAAO,CAAC,SAAS;AACtB,YAAI,KAAM,SAAQ,IAAI;AAAA,YACjB,QAAO,IAAI,MAAM,6CAA6C,CAAC;AAAA,MACtE,GAAG,WAAW;AAAA,IAChB,SAAS,OAAO;AAId;AAAA,QACE,IAAI;AAAA,UACF;AAAA,UACA,EAAE,MAAM;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;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;AASO,SAAS,aAAa,MAAY,UAAwB;AAC/D,MAAI,OAAO,aAAa,aAAa;AACnC,UAAM,IAAI,MAAM,6EAAwE;AAAA,EAC1F;AAEA,QAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,QAAM,SAAS,SAAS,cAAc,GAAG;AACzC,SAAO,OAAO;AACd,SAAO,WAAW;AAClB,SAAO,MAAM;AACb,SAAO,MAAM,UAAU;AACvB,WAAS,KAAK,OAAO,MAAM;AAC3B,SAAO,MAAM;AACb,SAAO,OAAO;AAId,aAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,CAAC;AAC9C;AASO,SAAS,eAAe,OAAe,WAA2B;AACvE,QAAM,OAAO,MACV,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AACzB,SAAO,GAAG,QAAQ,OAAO,IAAI,SAAS;AACxC;","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"]}
@@ -0,0 +1,28 @@
1
+ import * as react from 'react';
2
+ import { FigureChrome } from '../lib/frame.js';
3
+ import { DataflowSpec } from '../spec.js';
4
+ import '../lib/geometry.js';
5
+
6
+ interface DataflowFigureProps extends FigureChrome {
7
+ spec: DataflowSpec;
8
+ }
9
+ /**
10
+ * A pipeline: where data comes from, what happens to it, and who ends up with
11
+ * it.
12
+ *
13
+ * Structurally close to an architecture map and read for a completely different
14
+ * question, which is why it is a separate renderer rather than a preset. An
15
+ * architecture diagram is read for "what talks to what". A data-flow diagram is
16
+ * read for "what is IN this arrow" — and that question is why `classification`
17
+ * gets its own mono chip under the label rather than being folded into the
18
+ * wording. `clickstream / PII touch` and `clickstream` are two different facts,
19
+ * and a governance reviewer is looking for the second one.
20
+ *
21
+ * Stages are printed as column headings on a rule across the top. They are the
22
+ * axis of the figure: a node's `stage` is its position along the pipeline, and
23
+ * a reader should be able to answer "how far has this got" by looking up rather
24
+ * than by tracing arrows.
25
+ */
26
+ declare function DataflowFigure({ spec, className, legend, cards, heading, activeIds, onSelectNode, }: DataflowFigureProps): react.JSX.Element;
27
+
28
+ export { DataflowFigure, type DataflowFigureProps, DataflowFigure as default };
@@ -0,0 +1,129 @@
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, round, TYPE, union, wrapText } from "../lib/geometry.js";
6
+ import { kindLegend, resolveLegend } from "../lib/legend.js";
7
+ import { 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 = { originX: 40, originY: 92, colW: 184, gapX: 78, rowH: 52, nodeH: 74 };
19
+ function DataflowFigure({
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(
30
+ () => buildModel(spec, uid, activeIds, onSelectNode),
31
+ [spec, uid, activeIds, onSelectNode]
32
+ );
33
+ return /* @__PURE__ */ jsx(
34
+ DiagramFrame,
35
+ {
36
+ meta: spec.meta,
37
+ model,
38
+ cards: spec.cards,
39
+ showCards: cards,
40
+ heading,
41
+ legend,
42
+ className,
43
+ activeIds,
44
+ onSelectNode
45
+ }
46
+ );
47
+ }
48
+ function buildModel(spec, uid, activeIds, onSelectNode) {
49
+ const boxes = /* @__PURE__ */ new Map();
50
+ const placed = spec.nodes.map((node) => {
51
+ const w = node.width ?? GRID.colW;
52
+ const lines = wrapText(node.label, TYPE.label, w - PLATE.padX * 2, 2);
53
+ const h = plateHeight(lines.length, Boolean(node.sublabel), true, node.height ?? 0);
54
+ const x = GRID.originX + node.stage * (GRID.colW + GRID.gapX);
55
+ const y = GRID.originY + node.row * (GRID.rowH + GRID.nodeH) + (node.yOffset ?? 0);
56
+ const box = { x, y, w, h };
57
+ boxes.set(node.id, box);
58
+ return { node, box };
59
+ });
60
+ const wires = wiresFor(spec.flows, boxes);
61
+ const { paths, labels } = renderWires(wires, uid);
62
+ const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 30);
63
+ const headerY = Math.min(GRID.originY - 30, extent.y + 28);
64
+ const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null;
65
+ const artwork = /* @__PURE__ */ jsxs(Fragment, { children: [
66
+ /* @__PURE__ */ jsx(
67
+ "path",
68
+ {
69
+ d: `M ${round(extent.x + 14)} ${round(headerY + 8)} H ${round(extent.x + extent.w - 14)}`,
70
+ className: "stroke-(--diagram-rule) [stroke-width:1]",
71
+ "aria-hidden": "true"
72
+ }
73
+ ),
74
+ spec.stages.map((stage, index) => /* @__PURE__ */ jsx(
75
+ "text",
76
+ {
77
+ x: round(GRID.originX + index * (GRID.colW + GRID.gapX)),
78
+ y: round(headerY),
79
+ className: "fill-(--diagram-ink-3) font-mono [font-size:10.5px] [letter-spacing:0.08em] uppercase",
80
+ "aria-hidden": "true",
81
+ children: stage.label
82
+ },
83
+ `${stage.label}-${index}`
84
+ )),
85
+ paths,
86
+ placed.map(({ node, box }) => /* @__PURE__ */ jsx(
87
+ NodePlate,
88
+ {
89
+ nodeId: node.id,
90
+ box,
91
+ label: node.label,
92
+ sublabel: node.sublabel,
93
+ tag: node.tag,
94
+ kind: node.type,
95
+ active: active?.has(node.id),
96
+ dimmed: active ? !active.has(node.id) : false,
97
+ onSelect: onSelectNode ? () => onSelectNode(node.id) : void 0
98
+ },
99
+ node.id
100
+ )),
101
+ labels
102
+ ] });
103
+ const used = [...new Set(spec.nodes.map((node) => node.type))];
104
+ return {
105
+ extent: { ...extent, y: headerY - 22, h: extent.h + (extent.y - (headerY - 22)) },
106
+ artwork,
107
+ nodes: spec.nodes.map((node) => ({
108
+ id: node.id,
109
+ label: node.label,
110
+ sublabel: node.sublabel,
111
+ kind: node.type
112
+ })),
113
+ edges: (spec.flows ?? []).map((flow) => ({
114
+ from: flow.from,
115
+ to: flow.to,
116
+ label: [flow.label, flow.classification].filter(Boolean).join(" \u2014 ") || void 0
117
+ })),
118
+ legend: kindLegend(
119
+ resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),
120
+ spec.meta.legend?.entries
121
+ )
122
+ };
123
+ }
124
+ var DataflowFigure_default = DataflowFigure;
125
+ export {
126
+ DataflowFigure,
127
+ DataflowFigure_default as default
128
+ };
129
+ //# sourceMappingURL=DataflowFigure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/DataflowFigure/DataflowFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { DiagramFrame, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, round, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { DataflowSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\nconst GRID = { originX: 40, originY: 92, colW: 184, gapX: 78, rowH: 52, nodeH: 74 }\n\nexport interface DataflowFigureProps extends FigureChrome {\n spec: DataflowSpec\n}\n\n/**\n * A pipeline: where data comes from, what happens to it, and who ends up with\n * it.\n *\n * Structurally close to an architecture map and read for a completely different\n * question, which is why it is a separate renderer rather than a preset. An\n * architecture diagram is read for \"what talks to what\". A data-flow diagram is\n * read for \"what is IN this arrow\" — and that question is why `classification`\n * gets its own mono chip under the label rather than being folded into the\n * wording. `clickstream / PII touch` and `clickstream` are two different facts,\n * and a governance reviewer is looking for the second one.\n *\n * Stages are printed as column headings on a rule across the top. They are the\n * axis of the figure: a node's `stage` is its position along the pipeline, and\n * a reader should be able to answer \"how far has this got\" by looking up rather\n * than by tracing arrows.\n */\nexport function DataflowFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: DataflowFigureProps) {\n const uid = useId().replace(/:/g, '')\n const model = useMemo(\n () => buildModel(spec, uid, activeIds, onSelectNode),\n [spec, uid, activeIds, onSelectNode],\n )\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: DataflowSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const boxes = new Map<string, Box>()\n const placed = spec.nodes.map((node) => {\n const w = node.width ?? GRID.colW\n const lines = wrapText(node.label, TYPE.label, w - PLATE.padX * 2, 2)\n const h = plateHeight(lines.length, Boolean(node.sublabel), true, node.height ?? 0)\n\n const x = GRID.originX + node.stage * (GRID.colW + GRID.gapX)\n const y = GRID.originY + node.row * (GRID.rowH + GRID.nodeH) + (node.yOffset ?? 0)\n\n const box: Box = { x, y, w, h }\n boxes.set(node.id, box)\n return { node, box }\n })\n\n // Waypoints refer to the author's own grid, not this one — see\n // `Wire.keepWaypoints`. A data-flow specification always places by stage and\n // row, so there is never a case where honouring them would be correct.\n const wires = wiresFor(spec.flows, boxes)\n const { paths, labels } = renderWires(wires, uid)\n\n const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 30)\n const headerY = Math.min(GRID.originY - 30, extent.y + 28)\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {/* Stage headings, on one rule across the whole figure. */}\n <path\n d={`M ${round(extent.x + 14)} ${round(headerY + 8)} H ${round(extent.x + extent.w - 14)}`}\n className=\"stroke-(--diagram-rule) [stroke-width:1]\"\n aria-hidden=\"true\"\n />\n {spec.stages.map((stage, index) => (\n <text\n key={`${stage.label}-${index}`}\n x={round(GRID.originX + index * (GRID.colW + GRID.gapX))}\n y={round(headerY)}\n className=\"fill-(--diagram-ink-3) font-mono [font-size:10.5px] [letter-spacing:0.08em] uppercase\"\n aria-hidden=\"true\"\n >\n {stage.label}\n </text>\n ))}\n\n {paths}\n\n {placed.map(({ node, box }) => (\n <NodePlate\n key={node.id}\n nodeId={node.id}\n box={box}\n label={node.label}\n sublabel={node.sublabel}\n tag={node.tag}\n kind={node.type}\n active={active?.has(node.id)}\n dimmed={active ? !active.has(node.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(node.id) : undefined}\n />\n ))}\n\n {labels}\n </>\n )\n\n const used = [...new Set(spec.nodes.map((node) => node.type))]\n\n return {\n extent: { ...extent, y: headerY - 22, h: extent.h + (extent.y - (headerY - 22)) },\n artwork,\n nodes: spec.nodes.map((node) => ({\n id: node.id,\n label: node.label,\n sublabel: node.sublabel,\n kind: node.type,\n })),\n edges: (spec.flows ?? []).map((flow) => ({\n from: flow.from,\n to: flow.to,\n label: [flow.label, flow.classification].filter(Boolean).join(' — ') || undefined,\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 DataflowFigure\n"],"mappings":"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","names":[]}
@@ -0,0 +1,64 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, Ref } from 'react';
3
+
4
+ interface CanvasView {
5
+ scale: number;
6
+ x: number;
7
+ y: number;
8
+ }
9
+ /** What a caller can do to the canvas from outside it. */
10
+ interface DiagramCanvasHandle {
11
+ zoomIn: () => void;
12
+ zoomOut: () => void;
13
+ reset: () => void;
14
+ /** Pans so a point in CONTENT coordinates sits in the middle of the frame. */
15
+ centerOn: (x: number, y: number) => void;
16
+ view: CanvasView;
17
+ }
18
+ interface DiagramCanvasProps {
19
+ children: ReactNode;
20
+ className?: string;
21
+ /** How tall the frame is. Anything CSS accepts. */
22
+ height?: string;
23
+ /** Told about every view change, for a minimap or a percentage readout. */
24
+ onViewChange?: (view: CanvasView) => void;
25
+ /** Hides the built-in zoom controls, for a caller supplying its own. */
26
+ controls?: boolean;
27
+ /** Names the region for assistive tech. */
28
+ label?: string;
29
+ ref?: Ref<DiagramCanvasHandle>;
30
+ }
31
+ /**
32
+ * A frame that a picture larger than it can be moved around inside.
33
+ *
34
+ * Pan with a drag, zoom with the controls or with ⌘/Ctrl and the wheel, and
35
+ * reset with a key. That is the whole of it — this is deliberately a VIEWPORT
36
+ * and not a diagram editor: nothing here knows what a node is, so it works for
37
+ * any oversized figure, an SVG, an image, a table that will not fold.
38
+ *
39
+ * Three decisions here are not obvious.
40
+ *
41
+ * **A plain wheel scrolls the page, not the diagram.** A canvas that swallows
42
+ * the wheel is a scroll trap: a reader flicking down an article hits the figure
43
+ * and the page stops moving for no reason they can see. Zooming needs the
44
+ * modifier — which is also the platform gesture for zoom everywhere else, and
45
+ * is what a trackpad pinch already sends.
46
+ *
47
+ * **The transform is on a wrapper, not on the content.** The child keeps its
48
+ * own coordinate space, so a figure inside can still be measured, exported and
49
+ * read by anything that walks it. Scaling the child directly would make every
50
+ * `getBoundingClientRect` inside it a lie.
51
+ *
52
+ * **Keyboard first, and the frame is a real tab stop.** `+` / `-` / `0` and the
53
+ * arrow keys move the view; the frame takes focus so they can be pressed at
54
+ * all. A canvas that only answers a drag is a canvas half the readers cannot
55
+ * operate.
56
+ *
57
+ * @example
58
+ * <DiagramCanvas height="26rem" label="Request path">
59
+ * <ArchitectureFigure spec={spec} heading={false} legend="hidden" cards={false} />
60
+ * </DiagramCanvas>
61
+ */
62
+ declare function DiagramCanvas({ children, className, height, onViewChange, controls, label, ref, }: DiagramCanvasProps): react.JSX.Element;
63
+
64
+ export { type CanvasView, DiagramCanvas, type DiagramCanvasHandle, type DiagramCanvasProps, DiagramCanvas as default };