@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
@@ -0,0 +1,125 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import {
6
+ downloadBlob,
7
+ exportFilename,
8
+ rasterize,
9
+ readToken,
10
+ serializeSvg
11
+ } from "../../lib/svg-export.js";
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuLabel,
16
+ DropdownMenuSeparator,
17
+ DropdownMenuTrigger
18
+ } from "../../components/DropdownMenu/DropdownMenu.js";
19
+ import { Button } from "../../components/Button/Button.js";
20
+ const SHARE_CARD = { width: 1200, height: 630 };
21
+ const ITEMS = [
22
+ { group: "Image", format: "png", title: "PNG", hint: "Lossless, 2\xD7 for retina" },
23
+ { group: "Image", format: "jpeg", title: "JPEG", hint: "Compact, flattened onto paper" },
24
+ { group: "Image", format: "webp", title: "WebP", hint: "Smaller, modern browsers" },
25
+ { group: "Vector", format: "svg", title: "SVG", hint: "Editable, colours resolved" },
26
+ { group: "Share", format: "share-card", title: "Share card", hint: "1200 \xD7 630 PNG" }
27
+ ];
28
+ function DiagramExportMenu({
29
+ targetRef,
30
+ title,
31
+ trigger,
32
+ className,
33
+ onExport,
34
+ onResult
35
+ }) {
36
+ const [busy, setBusy] = useState(null);
37
+ async function run(format) {
38
+ setBusy(format);
39
+ try {
40
+ if (onExport) {
41
+ await onExport(format);
42
+ } else {
43
+ await exportFigure(targetRef.current, title, format);
44
+ }
45
+ onResult?.({ format, ok: true });
46
+ } catch (cause) {
47
+ onResult?.({ format, ok: false, error: cause });
48
+ } finally {
49
+ setBusy(null);
50
+ }
51
+ }
52
+ const groups = [...new Set(ITEMS.map((item) => item.group))];
53
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
54
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", className, children: "Export" }) }),
55
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-64", children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", { children: [
56
+ index > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
57
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: group }),
58
+ ITEMS.filter((item) => item.group === group).map((item) => /* @__PURE__ */ jsxs(
59
+ "button",
60
+ {
61
+ type: "button",
62
+ disabled: busy !== null,
63
+ onClick: () => void run(item.format),
64
+ className: cn(
65
+ "flex w-full flex-col items-start gap-0.5 rounded-(--radius-sm) px-2 py-1.5 text-start",
66
+ "transition-colors duration-(--duration-fast)",
67
+ "hover:bg-(--stone) focus-visible:bg-(--stone) focus-visible:outline-none",
68
+ "disabled:pointer-events-none disabled:opacity-(--disabled-opacity)"
69
+ ),
70
+ children: [
71
+ /* @__PURE__ */ jsxs("span", { className: "text-[13px] leading-tight text-(--ink)", children: [
72
+ item.title,
73
+ busy === item.format && " \u2026"
74
+ ] }),
75
+ /* @__PURE__ */ jsx("span", { className: "mono-meta text-(--ink-3-aa)", children: item.hint })
76
+ ]
77
+ },
78
+ item.format
79
+ ))
80
+ ] }, group)) })
81
+ ] });
82
+ }
83
+ async function exportFigure(target, title, format) {
84
+ const svg = findArtwork(target);
85
+ if (!svg) throw new Error("exportFigure: found no <svg> to export inside the given element");
86
+ const paper = readToken(svg, "--diagram-surface", "#ffffff");
87
+ const ink = readToken(svg, "--ink", "#101010");
88
+ const font = getComputedStyle(svg).fontFamily || "sans-serif";
89
+ if (format === "share-card") {
90
+ const markup2 = serializeSvg(svg, {
91
+ padding: 56,
92
+ background: paper,
93
+ title,
94
+ titleColor: ink,
95
+ titleFont: font,
96
+ frame: SHARE_CARD
97
+ });
98
+ const blob2 = await rasterize(markup2, "png", { scale: 1 });
99
+ downloadBlob(blob2, exportFilename(title, "png", "share-card"));
100
+ return;
101
+ }
102
+ const markup = serializeSvg(svg, { padding: 20, background: paper });
103
+ if (format === "svg") {
104
+ downloadBlob(new Blob([markup], { type: "image/svg+xml;charset=utf-8" }), exportFilename(title, "svg"));
105
+ return;
106
+ }
107
+ const blob = await rasterize(markup, format, {
108
+ scale: 2,
109
+ // JPEG has no alpha channel, so an unflattened export lands on black.
110
+ background: format === "jpeg" ? paper : void 0
111
+ });
112
+ downloadBlob(blob, exportFilename(title, format));
113
+ }
114
+ function findArtwork(element) {
115
+ if (!element) return null;
116
+ if (element.tagName.toLowerCase() === "svg") return element;
117
+ return element.querySelector("svg[data-diagram-artwork]") ?? element.querySelector("svg");
118
+ }
119
+ var DiagramExportMenu_default = DiagramExportMenu;
120
+ export {
121
+ DiagramExportMenu,
122
+ DiagramExportMenu_default as default,
123
+ exportFigure
124
+ };
125
+ //# sourceMappingURL=DiagramExportMenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/DiagramExportMenu/DiagramExportMenu.tsx"],"sourcesContent":["'use client'\n\nimport { useState, type ReactNode, type RefObject } from 'react'\nimport { cn } from '../../lib/cn'\nimport {\n downloadBlob,\n exportFilename,\n rasterize,\n readToken,\n serializeSvg,\n} from '../../lib/svg-export'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../../components/DropdownMenu/DropdownMenu'\nimport { Button } from '../../components/Button/Button'\n\n/** The files a figure can be taken away as. */\nexport type ExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'share-card'\n\n/** What came of an export, for a page that wants to say so. */\nexport interface ExportResult {\n format: ExportFormat\n ok: boolean\n error?: Error\n}\n\n/** A share card is a fixed frame, because the surfaces that consume one are. */\nconst SHARE_CARD = { width: 1200, height: 630 } as const\n\nconst ITEMS: { format: ExportFormat; title: string; hint: string; group: string }[] = [\n { group: 'Image', format: 'png', title: 'PNG', hint: 'Lossless, 2× for retina' },\n { group: 'Image', format: 'jpeg', title: 'JPEG', hint: 'Compact, flattened onto paper' },\n { group: 'Image', format: 'webp', title: 'WebP', hint: 'Smaller, modern browsers' },\n { group: 'Vector', format: 'svg', title: 'SVG', hint: 'Editable, colours resolved' },\n { group: 'Share', format: 'share-card', title: 'Share card', hint: '1200 × 630 PNG' },\n]\n\nexport interface DiagramExportMenuProps {\n /**\n * The `<svg>` to export, or an element containing exactly one.\n *\n * A ref rather than a selector, because a page can hold several figures and a\n * selector would export whichever the document happened to reach first.\n */\n targetRef: RefObject<HTMLElement | SVGSVGElement | null>\n /** Names the file, and is printed on the share card. */\n title: string\n /** Replaces the trigger. */\n trigger?: ReactNode\n className?: string\n /** Runs instead of the built-in export — for a caller with its own pipeline. */\n onExport?: (format: ExportFormat) => void | Promise<void>\n /** Told what happened, so a page can raise a toast. */\n onResult?: (result: ExportResult) => void\n}\n\n/**\n * Taking the figure off the page: five files, one menu.\n *\n * WHY THE MENU DOES THE WORK rather than handing back a format: every one of\n * these exports is the same six steps — find the `<svg>`, walk it with\n * `getComputedStyle` to bake the custom properties into real colours,\n * serialise, rasterise, name the file, hand it to the browser — and only the\n * last two differ between them. A menu that emitted `'png'` and left the caller\n * to do the rest would be a menu that every consumer reimplements, badly, and\n * the interesting half (a serialised SVG resolves `var(--ink)` to nothing and\n * comes out invisible) is exactly the half a caller would not know to write.\n *\n * `onExport` is still there for a page with its own pipeline — a server-side\n * renderer, a different frame size — and taking it turns everything below into\n * a no-op.\n *\n * What each format actually is, stated rather than implied.\n *\n * **SVG** is the artwork with resolved colours. It is editable and it is the\n * only lossless one, but it carries no web fonts: a machine without the family\n * renders it in a fallback, so type metrics will differ.\n *\n * **PNG, JPEG and WebP** are the browser's own rasteriser re-drawing that SVG\n * at 2×. Not a screenshot — antialiasing and any effect a page stylesheet\n * applied from OUTSIDE the `<svg>` are not in it. **JPEG has no alpha**, so it\n * is flattened onto the resolved paper colour rather than onto black, which is\n * what a transparent PNG becomes when a format with no transparency is asked\n * to hold it.\n *\n * **The share card** is a 1200 × 630 frame with the title on it and the whole\n * diagram letterboxed inside — never cropped. A card that cropped to fill the\n * frame would be a picture of a different diagram.\n */\nexport function DiagramExportMenu({\n targetRef,\n title,\n trigger,\n className,\n onExport,\n onResult,\n}: DiagramExportMenuProps) {\n const [busy, setBusy] = useState<ExportFormat | null>(null)\n\n async function run(format: ExportFormat) {\n setBusy(format)\n try {\n if (onExport) {\n await onExport(format)\n } else {\n await exportFigure(targetRef.current, title, format)\n }\n onResult?.({ format, ok: true })\n } catch (cause) {\n // Never swallowed: a download is something the reader just asked for, and\n // a click that quietly does nothing is indistinguishable from a broken\n // button. The caller gets the error to put in front of them.\n onResult?.({ format, ok: false, error: cause as Error })\n } finally {\n setBusy(null)\n }\n }\n\n const groups = [...new Set(ITEMS.map((item) => item.group))]\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n {trigger ?? (\n <Button size=\"sm\" variant=\"secondary\" className={className}>\n Export\n </Button>\n )}\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-64\">\n {groups.map((group, index) => (\n <div key={group}>\n {index > 0 && <DropdownMenuSeparator />}\n <DropdownMenuLabel>{group}</DropdownMenuLabel>\n {ITEMS.filter((item) => item.group === group).map((item) => (\n <button\n key={item.format}\n type=\"button\"\n disabled={busy !== null}\n onClick={() => void run(item.format)}\n className={cn(\n 'flex w-full flex-col items-start gap-0.5 rounded-(--radius-sm) px-2 py-1.5 text-start',\n 'transition-colors duration-(--duration-fast)',\n 'hover:bg-(--stone) focus-visible:bg-(--stone) focus-visible:outline-none',\n 'disabled:pointer-events-none disabled:opacity-(--disabled-opacity)',\n )}\n >\n <span className=\"text-[13px] leading-tight text-(--ink)\">\n {item.title}\n {busy === item.format && ' …'}\n </span>\n <span className=\"mono-meta text-(--ink-3-aa)\">{item.hint}</span>\n </button>\n ))}\n </div>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * One figure, one format, one file.\n *\n * Exported so a page can wire a keyboard shortcut or its own button to exactly\n * what the menu does, without reimplementing the six steps.\n */\nexport async function exportFigure(\n target: HTMLElement | SVGSVGElement | null,\n title: string,\n format: ExportFormat,\n): Promise<void> {\n const svg = findArtwork(target)\n if (!svg) throw new Error('exportFigure: found no <svg> to export inside the given element')\n\n // Read against the LIVE element, so the file comes out in whichever theme the\n // reader is looking at rather than in whichever one the code assumed.\n const paper = readToken(svg, '--diagram-surface', '#ffffff')\n const ink = readToken(svg, '--ink', '#101010')\n const font = getComputedStyle(svg).fontFamily || 'sans-serif'\n\n if (format === 'share-card') {\n const markup = serializeSvg(svg, {\n padding: 56,\n background: paper,\n title,\n titleColor: ink,\n titleFont: font,\n frame: SHARE_CARD,\n })\n const blob = await rasterize(markup, 'png', { scale: 1 })\n downloadBlob(blob, exportFilename(title, 'png', 'share-card'))\n return\n }\n\n const markup = serializeSvg(svg, { padding: 20, background: paper })\n\n if (format === 'svg') {\n downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), exportFilename(title, 'svg'))\n return\n }\n\n const blob = await rasterize(markup, format, {\n scale: 2,\n // JPEG has no alpha channel, so an unflattened export lands on black.\n background: format === 'jpeg' ? paper : undefined,\n })\n downloadBlob(blob, exportFilename(title, format))\n}\n\n/**\n * The figure's own `<svg>`, not the first one in the subtree.\n *\n * The distinction is load-bearing: a toolbar sits inside the same wrapper and\n * every icon in it is an `<svg>`. Looking for the artwork marker first is what\n * stops an export of a diagram from being a picture of a chevron.\n */\nfunction findArtwork(element: HTMLElement | SVGSVGElement | null): SVGSVGElement | null {\n if (!element) return null\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n return (\n element.querySelector<SVGSVGElement>('svg[data-diagram-artwork]') ??\n element.querySelector<SVGSVGElement>('svg')\n )\n}\n\nexport default DiagramExportMenu\n"],"mappings":"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","names":["markup","blob"]}
@@ -0,0 +1,70 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** One fact about the selected thing: a short label and its value. */
5
+ interface InspectorFact {
6
+ label: string;
7
+ value: ReactNode;
8
+ /** Sets the value in mono — an id, a path, a version, a port. */
9
+ mono?: boolean;
10
+ }
11
+ /** A relationship the selected thing takes part in. */
12
+ interface InspectorLink {
13
+ id?: string;
14
+ /** Which way it runs, relative to the selected node. */
15
+ direction: 'in' | 'out';
16
+ label: string;
17
+ /** What sits at the other end. */
18
+ peer: string;
19
+ onSelect?: () => void;
20
+ }
21
+ interface DiagramInspectorProps {
22
+ /** The kicker over the title — what KIND of thing this panel is showing. */
23
+ eyebrow?: string;
24
+ title: string;
25
+ description?: ReactNode;
26
+ facts?: InspectorFact[];
27
+ links?: InspectorLink[];
28
+ /** Buttons under the facts — copy a link, open the source. */
29
+ actions?: ReactNode;
30
+ onClose?: () => void;
31
+ className?: string;
32
+ /** Renders as a floating panel over the surface rather than in the flow. */
33
+ floating?: boolean;
34
+ }
35
+ /**
36
+ * What the reader just picked, written out.
37
+ *
38
+ * A diagram can hold about eight words per node before it stops being a diagram
39
+ * and starts being a document with lines drawn on it. Everything past those
40
+ * eight — the port, the owning team, the file it was read out of, the six
41
+ * relationships it takes part in — belongs beside the picture rather than
42
+ * inside it, and this is that place.
43
+ *
44
+ * NOT A DIALOG, and that is deliberate. An inspector is a REGION that changes
45
+ * with the selection, not a modal. Giving it `role="dialog"` would trap focus
46
+ * and demand dismissal for something the reader never opened — they clicked a
47
+ * node, and the panel followed. So it is a labelled region with
48
+ * `aria-live="polite"`, which is what makes a screen reader announce the new
49
+ * selection without stealing the cursor from whatever the reader was doing.
50
+ *
51
+ * The relationships are real buttons when they carry `onSelect`, which is how
52
+ * the diagram becomes navigable from the keyboard: a reader can walk the graph
53
+ * peer by peer without ever touching the picture.
54
+ *
55
+ * @example
56
+ * <DiagramInspector
57
+ * eyebrow="Service"
58
+ * title="API Server"
59
+ * description="FastAPI, behind the load balancer."
60
+ * facts={[
61
+ * { label: 'Port', value: '8000', mono: true },
62
+ * { label: 'Id', value: 'api', mono: true },
63
+ * ]}
64
+ * links={[{ direction: 'out', label: 'SQL', peer: 'PostgreSQL' }]}
65
+ * onClose={() => setSelected(null)}
66
+ * />
67
+ */
68
+ declare function DiagramInspector({ eyebrow, title, description, facts, links, actions, onClose, className, floating, }: DiagramInspectorProps): react.JSX.Element;
69
+
70
+ export { DiagramInspector, type DiagramInspectorProps, type InspectorFact, type InspectorLink, DiagramInspector as default };
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { X } from "lucide-react";
4
+ import { cn } from "../../lib/cn.js";
5
+ function DiagramInspector({
6
+ eyebrow,
7
+ title,
8
+ description,
9
+ facts = [],
10
+ links = [],
11
+ actions,
12
+ onClose,
13
+ className,
14
+ floating
15
+ }) {
16
+ return /* @__PURE__ */ jsxs(
17
+ "section",
18
+ {
19
+ "aria-label": `${title} details`,
20
+ "aria-live": "polite",
21
+ className: cn(
22
+ "flex flex-col gap-3 rounded-(--radius-lg) border border-(--rule-2) bg-(--panel-bg) p-4",
23
+ floating && "absolute bottom-3 start-3 z-(--z-sticky) w-72 max-w-[calc(100%-1.5rem)]",
24
+ className
25
+ ),
26
+ children: [
27
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
29
+ eyebrow && /* @__PURE__ */ jsx("span", { className: "eyebrow text-(--ink-3-aa)", children: eyebrow }),
30
+ /* @__PURE__ */ jsx("h3", { className: "m-0 font-sans text-[15px] leading-tight font-normal text-(--ink)", children: title })
31
+ ] }),
32
+ onClose && /* @__PURE__ */ jsx(
33
+ "button",
34
+ {
35
+ type: "button",
36
+ onClick: onClose,
37
+ "aria-label": "Clear the selection",
38
+ className: "-me-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-(--radius-xs) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)",
39
+ children: /* @__PURE__ */ jsx(X, { size: 14, strokeWidth: 1.5, "aria-hidden": true })
40
+ }
41
+ )
42
+ ] }),
43
+ description && /* @__PURE__ */ jsx("p", { className: "m-0 text-[13px] leading-[1.55] text-(--ink-2)", children: description }),
44
+ facts.length > 0 && // A definition list, because that is what this is: each row is a term
45
+ // and its value, and a grid of divs tells a screen reader nothing about
46
+ // which value belongs to which label.
47
+ /* @__PURE__ */ jsx("dl", { className: "m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5", children: facts.map((fact) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
48
+ /* @__PURE__ */ jsx("dt", { className: "mono-meta text-(--ink-3-aa)", children: fact.label }),
49
+ /* @__PURE__ */ jsx(
50
+ "dd",
51
+ {
52
+ className: cn(
53
+ "m-0 min-w-0 break-words text-[13px] leading-snug text-(--ink)",
54
+ fact.mono && "font-mono text-[12px]"
55
+ ),
56
+ children: fact.value
57
+ }
58
+ )
59
+ ] }, fact.label)) }),
60
+ links.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 border-t border-(--rule) pt-3", children: [
61
+ /* @__PURE__ */ jsx("span", { className: "eyebrow text-(--ink-3-aa)", children: "Relationships" }),
62
+ /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-0.5 p-0", children: links.map((link, index) => {
63
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
64
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "font-mono text-[12px] text-(--ink-3-aa)", children: link.direction === "out" ? "\u2192" : "\u2190" }),
65
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: link.direction === "out" ? "to" : "from" }),
66
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[13px] text-(--ink)", children: link.peer }),
67
+ link.label && /* @__PURE__ */ jsx("span", { className: "mono-meta shrink-0 text-(--ink-3-aa)", children: link.label })
68
+ ] });
69
+ const row = "flex w-full items-baseline gap-2 rounded-(--radius-xs) px-1 py-1 text-start";
70
+ return /* @__PURE__ */ jsx("li", { children: link.onSelect ? /* @__PURE__ */ jsx(
71
+ "button",
72
+ {
73
+ type: "button",
74
+ onClick: link.onSelect,
75
+ className: cn(
76
+ row,
77
+ "transition-colors duration-(--duration-fast) hover:bg-(--stone) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)"
78
+ ),
79
+ children: body
80
+ }
81
+ ) : /* @__PURE__ */ jsx("span", { className: row, children: body }) }, link.id ?? index);
82
+ }) })
83
+ ] }),
84
+ actions && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 pt-1", children: actions })
85
+ ]
86
+ }
87
+ );
88
+ }
89
+ var DiagramInspector_default = DiagramInspector;
90
+ export {
91
+ DiagramInspector,
92
+ DiagramInspector_default as default
93
+ };
94
+ //# sourceMappingURL=DiagramInspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/DiagramInspector/DiagramInspector.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport { X } from 'lucide-react'\nimport { cn } from '../../lib/cn'\n\n/** One fact about the selected thing: a short label and its value. */\nexport interface InspectorFact {\n label: string\n value: ReactNode\n /** Sets the value in mono — an id, a path, a version, a port. */\n mono?: boolean\n}\n\n/** A relationship the selected thing takes part in. */\nexport interface InspectorLink {\n id?: string\n /** Which way it runs, relative to the selected node. */\n direction: 'in' | 'out'\n label: string\n /** What sits at the other end. */\n peer: string\n onSelect?: () => void\n}\n\nexport interface DiagramInspectorProps {\n /** The kicker over the title — what KIND of thing this panel is showing. */\n eyebrow?: string\n title: string\n description?: ReactNode\n facts?: InspectorFact[]\n links?: InspectorLink[]\n /** Buttons under the facts — copy a link, open the source. */\n actions?: ReactNode\n onClose?: () => void\n className?: string\n /** Renders as a floating panel over the surface rather than in the flow. */\n floating?: boolean\n}\n\n/**\n * What the reader just picked, written out.\n *\n * A diagram can hold about eight words per node before it stops being a diagram\n * and starts being a document with lines drawn on it. Everything past those\n * eight — the port, the owning team, the file it was read out of, the six\n * relationships it takes part in — belongs beside the picture rather than\n * inside it, and this is that place.\n *\n * NOT A DIALOG, and that is deliberate. An inspector is a REGION that changes\n * with the selection, not a modal. Giving it `role=\"dialog\"` would trap focus\n * and demand dismissal for something the reader never opened — they clicked a\n * node, and the panel followed. So it is a labelled region with\n * `aria-live=\"polite\"`, which is what makes a screen reader announce the new\n * selection without stealing the cursor from whatever the reader was doing.\n *\n * The relationships are real buttons when they carry `onSelect`, which is how\n * the diagram becomes navigable from the keyboard: a reader can walk the graph\n * peer by peer without ever touching the picture.\n *\n * @example\n * <DiagramInspector\n * eyebrow=\"Service\"\n * title=\"API Server\"\n * description=\"FastAPI, behind the load balancer.\"\n * facts={[\n * { label: 'Port', value: '8000', mono: true },\n * { label: 'Id', value: 'api', mono: true },\n * ]}\n * links={[{ direction: 'out', label: 'SQL', peer: 'PostgreSQL' }]}\n * onClose={() => setSelected(null)}\n * />\n */\nexport function DiagramInspector({\n eyebrow,\n title,\n description,\n facts = [],\n links = [],\n actions,\n onClose,\n className,\n floating,\n}: DiagramInspectorProps) {\n return (\n <section\n aria-label={`${title} details`}\n aria-live=\"polite\"\n className={cn(\n 'flex flex-col gap-3 rounded-(--radius-lg) border border-(--rule-2) bg-(--panel-bg) p-4',\n floating && 'absolute bottom-3 start-3 z-(--z-sticky) w-72 max-w-[calc(100%-1.5rem)]',\n className,\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 flex-col gap-1\">\n {eyebrow && <span className=\"eyebrow text-(--ink-3-aa)\">{eyebrow}</span>}\n <h3 className=\"m-0 font-sans text-[15px] leading-tight font-normal text-(--ink)\">\n {title}\n </h3>\n </div>\n {onClose && (\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Clear the selection\"\n className=\"-me-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-(--radius-xs) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)\"\n >\n <X size={14} strokeWidth={1.5} aria-hidden />\n </button>\n )}\n </div>\n\n {description && (\n <p className=\"m-0 text-[13px] leading-[1.55] text-(--ink-2)\">{description}</p>\n )}\n\n {facts.length > 0 && (\n // A definition list, because that is what this is: each row is a term\n // and its value, and a grid of divs tells a screen reader nothing about\n // which value belongs to which label.\n <dl className=\"m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5\">\n {facts.map((fact) => (\n <div key={fact.label} className=\"contents\">\n <dt className=\"mono-meta text-(--ink-3-aa)\">{fact.label}</dt>\n <dd\n className={cn(\n 'm-0 min-w-0 break-words text-[13px] leading-snug text-(--ink)',\n fact.mono && 'font-mono text-[12px]',\n )}\n >\n {fact.value}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {links.length > 0 && (\n <div className=\"flex flex-col gap-1 border-t border-(--rule) pt-3\">\n <span className=\"eyebrow text-(--ink-3-aa)\">Relationships</span>\n <ul className=\"m-0 flex list-none flex-col gap-0.5 p-0\">\n {links.map((link, index) => {\n const body = (\n <>\n {/* The arrow is hidden from assistive tech and the direction\n spelled out instead: a screen reader reading \"→\" aloud is\n either silence or the word \"arrow\", and neither says which\n way the relationship runs. */}\n <span aria-hidden className=\"font-mono text-[12px] text-(--ink-3-aa)\">\n {link.direction === 'out' ? '→' : '←'}\n </span>\n <span className=\"sr-only\">\n {link.direction === 'out' ? 'to' : 'from'}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[13px] text-(--ink)\">\n {link.peer}\n </span>\n {link.label && (\n <span className=\"mono-meta shrink-0 text-(--ink-3-aa)\">{link.label}</span>\n )}\n </>\n )\n\n const row = 'flex w-full items-baseline gap-2 rounded-(--radius-xs) px-1 py-1 text-start'\n\n return (\n <li key={link.id ?? index}>\n {link.onSelect ? (\n <button\n type=\"button\"\n onClick={link.onSelect}\n className={cn(\n row,\n 'transition-colors duration-(--duration-fast) hover:bg-(--stone) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)',\n )}\n >\n {body}\n </button>\n ) : (\n <span className={row}>{body}</span>\n )}\n </li>\n )\n })}\n </ul>\n </div>\n )}\n\n {actions && <div className=\"flex flex-wrap items-center gap-2 pt-1\">{actions}</div>}\n </section>\n )\n}\n\nexport default DiagramInspector\n"],"mappings":";AA+FQ,SAiDQ,UAhDM,KADd;AA5FR,SAAS,SAAS;AAClB,SAAS,UAAU;AAqEZ,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ,CAAC;AAAA,EACT,QAAQ,CAAC;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,GAAG,KAAK;AAAA,MACpB,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MAEA;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,SAAI,WAAU,+BACZ;AAAA,uBAAW,oBAAC,UAAK,WAAU,6BAA6B,mBAAQ;AAAA,YACjE,oBAAC,QAAG,WAAU,oEACX,iBACH;AAAA,aACF;AAAA,UACC,WACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,cAAW;AAAA,cACX,WAAU;AAAA,cAEV,8BAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,UAC7C;AAAA,WAEJ;AAAA,QAEC,eACC,oBAAC,OAAE,WAAU,iDAAiD,uBAAY;AAAA,QAG3E,MAAM,SAAS;AAAA;AAAA;AAAA,QAId,oBAAC,QAAG,WAAU,mDACX,gBAAM,IAAI,CAAC,SACV,qBAAC,SAAqB,WAAU,YAC9B;AAAA,8BAAC,QAAG,WAAU,+BAA+B,eAAK,OAAM;AAAA,UACxD;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,KAAK,QAAQ;AAAA,cACf;AAAA,cAEC,eAAK;AAAA;AAAA,UACR;AAAA,aATQ,KAAK,KAUf,CACD,GACH;AAAA,QAGD,MAAM,SAAS,KACd,qBAAC,SAAI,WAAU,qDACb;AAAA,8BAAC,UAAK,WAAU,6BAA4B,2BAAa;AAAA,UACzD,oBAAC,QAAG,WAAU,2CACX,gBAAM,IAAI,CAAC,MAAM,UAAU;AAC1B,kBAAM,OACJ,iCAKE;AAAA,kCAAC,UAAK,eAAW,MAAC,WAAU,2CACzB,eAAK,cAAc,QAAQ,WAAM,UACpC;AAAA,cACA,oBAAC,UAAK,WAAU,WACb,eAAK,cAAc,QAAQ,OAAO,QACrC;AAAA,cACA,oBAAC,UAAK,WAAU,oDACb,eAAK,MACR;AAAA,cACC,KAAK,SACJ,oBAAC,UAAK,WAAU,wCAAwC,eAAK,OAAM;AAAA,eAEvE;AAGF,kBAAM,MAAM;AAEZ,mBACE,oBAAC,QACE,eAAK,WACJ;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,KAAK;AAAA,gBACd,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBAEC;AAAA;AAAA,YACH,IAEA,oBAAC,UAAK,WAAW,KAAM,gBAAK,KAbvB,KAAK,MAAM,KAepB;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA,QAGD,WAAW,oBAAC,SAAI,WAAU,0CAA0C,mBAAQ;AAAA;AAAA;AAAA,EAC/E;AAEJ;AAEA,IAAO,2BAAQ;","names":[]}
@@ -0,0 +1,42 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface DiagramLegendEntry {
5
+ /** Stable key, and what a caller keys its own data by. */
6
+ key: string;
7
+ label: string;
8
+ /**
9
+ * The drawn mark, as SVG children on a 14 × 14 grid.
10
+ *
11
+ * Markup rather than a name from a fixed list, because the whole point of a
12
+ * key is that it shows the SAME mark the figure drew — and a figure that
13
+ * invented a new one would have to add it here too. `kindLegend`,
14
+ * `variantLegend` and `stateLegend` build the standard sets out of the
15
+ * renderers' own drawing code, so those can never drift.
16
+ */
17
+ sample: ReactNode;
18
+ }
19
+ interface DiagramLegendProps {
20
+ entries: DiagramLegendEntry[];
21
+ /** The kicker before the row. Pass null for a bare row. */
22
+ title?: string | null;
23
+ className?: string;
24
+ }
25
+ /**
26
+ * The key: which drawn form means which kind of thing.
27
+ *
28
+ * Not optional furniture in a monochrome system, and this is the one place that
29
+ * has to be said plainly. When the difference between a queue and a cache is a
30
+ * sigil rather than a colour, the key is the only place a reader is told what
31
+ * the sigil means — a figure whose forms are undocumented is a figure whose
32
+ * forms might as well not have been drawn.
33
+ *
34
+ * A `<ul>` rather than a row of `<span>`s: it is a list of pairs, and the count
35
+ * is part of what a screen reader should say about it.
36
+ *
37
+ * @example
38
+ * <DiagramLegend entries={kindLegend(['backend', 'database'])} />
39
+ */
40
+ declare function DiagramLegend({ entries, title, className }: DiagramLegendProps): react.JSX.Element | null;
41
+
42
+ export { DiagramLegend, type DiagramLegendEntry, type DiagramLegendProps, DiagramLegend as default };
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../../lib/cn.js";
4
+ function DiagramLegend({ entries, title = "Key", className }) {
5
+ if (entries.length === 0) return null;
6
+ return /* @__PURE__ */ jsxs(
7
+ "div",
8
+ {
9
+ className: cn(
10
+ "flex flex-wrap items-baseline gap-x-5 gap-y-2 border-t border-(--rule) pt-3",
11
+ className
12
+ ),
13
+ children: [
14
+ title && /* @__PURE__ */ jsx("span", { className: "eyebrow text-(--ink-3-aa)", children: title }),
15
+ /* @__PURE__ */ jsx(
16
+ "ul",
17
+ {
18
+ className: "m-0 flex list-none flex-wrap items-center gap-x-4 gap-y-2 p-0",
19
+ "aria-label": title ?? "Key",
20
+ children: entries.map((entry) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
21
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 14 14", className: "size-4 shrink-0", "aria-hidden": "true", children: entry.sample }),
22
+ /* @__PURE__ */ jsx("span", { className: "mono-meta text-(--ink-2)", children: entry.label })
23
+ ] }, entry.key))
24
+ }
25
+ )
26
+ ]
27
+ }
28
+ );
29
+ }
30
+ var DiagramLegend_default = DiagramLegend;
31
+ export {
32
+ DiagramLegend,
33
+ DiagramLegend_default as default
34
+ };
35
+ //# sourceMappingURL=DiagramLegend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/DiagramLegend/DiagramLegend.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface DiagramLegendEntry {\n /** Stable key, and what a caller keys its own data by. */\n key: string\n label: string\n /**\n * The drawn mark, as SVG children on a 14 × 14 grid.\n *\n * Markup rather than a name from a fixed list, because the whole point of a\n * key is that it shows the SAME mark the figure drew — and a figure that\n * invented a new one would have to add it here too. `kindLegend`,\n * `variantLegend` and `stateLegend` build the standard sets out of the\n * renderers' own drawing code, so those can never drift.\n */\n sample: ReactNode\n}\n\nexport interface DiagramLegendProps {\n entries: DiagramLegendEntry[]\n /** The kicker before the row. Pass null for a bare row. */\n title?: string | null\n className?: string\n}\n\n/**\n * The key: which drawn form means which kind of thing.\n *\n * Not optional furniture in a monochrome system, and this is the one place that\n * has to be said plainly. When the difference between a queue and a cache is a\n * sigil rather than a colour, the key is the only place a reader is told what\n * the sigil means — a figure whose forms are undocumented is a figure whose\n * forms might as well not have been drawn.\n *\n * A `<ul>` rather than a row of `<span>`s: it is a list of pairs, and the count\n * is part of what a screen reader should say about it.\n *\n * @example\n * <DiagramLegend entries={kindLegend(['backend', 'database'])} />\n */\nexport function DiagramLegend({ entries, title = 'Key', className }: DiagramLegendProps) {\n if (entries.length === 0) return null\n\n return (\n <div\n className={cn(\n 'flex flex-wrap items-baseline gap-x-5 gap-y-2 border-t border-(--rule) pt-3',\n className,\n )}\n >\n {title && <span className=\"eyebrow text-(--ink-3-aa)\">{title}</span>}\n <ul\n className=\"m-0 flex list-none flex-wrap items-center gap-x-4 gap-y-2 p-0\"\n aria-label={title ?? 'Key'}\n >\n {entries.map((entry) => (\n <li key={entry.key} className=\"flex items-center gap-2\">\n <svg viewBox=\"0 0 14 14\" className=\"size-4 shrink-0\" aria-hidden=\"true\">\n {entry.sample}\n </svg>\n <span className=\"mono-meta text-(--ink-2)\">{entry.label}</span>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n\nexport default DiagramLegend\n"],"mappings":";AAqDgB,cAMN,YANM;AAlDhB,SAAS,UAAU;AAwCZ,SAAS,cAAc,EAAE,SAAS,QAAQ,OAAO,UAAU,GAAuB;AACvF,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA,iBAAS,oBAAC,UAAK,WAAU,6BAA6B,iBAAM;AAAA,QAC7D;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,cAAY,SAAS;AAAA,YAEpB,kBAAQ,IAAI,CAAC,UACZ,qBAAC,QAAmB,WAAU,2BAC5B;AAAA,kCAAC,SAAI,SAAQ,aAAY,WAAU,mBAAkB,eAAY,QAC9D,gBAAM,QACT;AAAA,cACA,oBAAC,UAAK,WAAU,4BAA4B,gBAAM,OAAM;AAAA,iBAJjD,MAAM,GAKf,CACD;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,wBAAQ;","names":[]}
@@ -0,0 +1,61 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { CanvasView } from '../DiagramCanvas/DiagramCanvas.js';
4
+
5
+ interface DiagramMinimapProps {
6
+ /** The whole artwork, at its natural size in CSS pixels. */
7
+ content: {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ /** The frame the artwork is being looked at through, in CSS pixels. */
12
+ frame: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /** Where that frame currently sits — a `DiagramCanvas`'s `onViewChange`. */
17
+ view: CanvasView;
18
+ /** A miniature of the artwork. Usually the same figure, rendered again. */
19
+ children?: ReactNode;
20
+ /** Called with a point in CONTENT coordinates when the reader picks one. */
21
+ onSeek?: (x: number, y: number) => void;
22
+ /** How wide the map is, in CSS pixels. Height follows the aspect ratio. */
23
+ width?: number;
24
+ className?: string;
25
+ label?: string;
26
+ }
27
+ /**
28
+ * Where you are in something bigger than the window.
29
+ *
30
+ * Two things at once, and both are needed: a miniature of the whole artwork,
31
+ * and a rectangle showing which part of it the frame is currently over. The
32
+ * miniature alone answers "what is there"; the rectangle answers "and where am
33
+ * I", which is the question a reader who has just panned twice actually has.
34
+ *
35
+ * THE RECTANGLE IS DERIVED, never stored. Its position comes out of the
36
+ * canvas's own view — the same three numbers the canvas is already transforming
37
+ * by — divided by the map's scale. Keeping a second copy of "where the viewport
38
+ * is" is how a minimap comes to disagree with the thing it is a map of, and a
39
+ * map that disagrees is worse than none.
40
+ *
41
+ * CLICKING RECENTRES rather than jumping. `onSeek` reports a point in CONTENT
42
+ * coordinates, which is what a canvas's `centerOn` takes. The minimap does not
43
+ * move anything itself: it has no authority over the view, it only says where
44
+ * the reader pointed.
45
+ *
46
+ * @example
47
+ * const canvas = useRef<DiagramCanvasHandle>(null)
48
+ * const [view, setView] = useState({ scale: 1, x: 0, y: 0 })
49
+ * // …
50
+ * <DiagramMinimap
51
+ * content={{ width: 1400, height: 620 }}
52
+ * frame={{ width: 720, height: 400 }}
53
+ * view={view}
54
+ * onSeek={(x, y) => canvas.current?.centerOn(x, y)}
55
+ * >
56
+ * <ArchitectureFigure spec={spec} heading={false} legend="hidden" cards={false} />
57
+ * </DiagramMinimap>
58
+ */
59
+ declare function DiagramMinimap({ content, frame, view, children, onSeek, width, className, label, }: DiagramMinimapProps): react.JSX.Element;
60
+
61
+ export { DiagramMinimap, type DiagramMinimapProps, DiagramMinimap as default };
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../../lib/cn.js";
4
+ function DiagramMinimap({
5
+ content,
6
+ frame,
7
+ view,
8
+ children,
9
+ onSeek,
10
+ width = 200,
11
+ className,
12
+ label = "Diagram overview"
13
+ }) {
14
+ const scale = content.width > 0 ? width / content.width : 1;
15
+ const height = Math.max(1, content.height * scale);
16
+ const rect = {
17
+ x: -view.x / view.scale * scale,
18
+ y: -view.y / view.scale * scale,
19
+ w: frame.width / view.scale * scale,
20
+ h: frame.height / view.scale * scale
21
+ };
22
+ function seek(event) {
23
+ if (!onSeek) return;
24
+ const box = event.currentTarget.getBoundingClientRect();
25
+ onSeek((event.clientX - box.left) / scale, (event.clientY - box.top) / scale);
26
+ }
27
+ return /* @__PURE__ */ jsxs(
28
+ "div",
29
+ {
30
+ className: cn(
31
+ "relative overflow-hidden rounded-(--radius) border border-(--rule-2) bg-(--panel-bg)",
32
+ className
33
+ ),
34
+ style: { width, height },
35
+ role: "group",
36
+ "aria-label": label,
37
+ children: [
38
+ /* @__PURE__ */ jsx(
39
+ "div",
40
+ {
41
+ "aria-hidden": "true",
42
+ className: "pointer-events-none absolute start-0 top-0 origin-top-left",
43
+ style: { width: content.width, transform: `scale(${scale})` },
44
+ children
45
+ }
46
+ ),
47
+ /* @__PURE__ */ jsx(
48
+ "div",
49
+ {
50
+ className: cn("absolute inset-0", onSeek && "cursor-crosshair"),
51
+ onPointerDown: seek,
52
+ onPointerMove: (event) => {
53
+ if (event.buttons === 1) seek(event);
54
+ },
55
+ children: /* @__PURE__ */ jsx(
56
+ "div",
57
+ {
58
+ "aria-hidden": "true",
59
+ className: "absolute rounded-[2px] border border-(--accent) bg-(--accent-wash)",
60
+ style: {
61
+ left: `${rect.x}px`,
62
+ top: `${rect.y}px`,
63
+ width: `${Math.max(4, rect.w)}px`,
64
+ height: `${Math.max(4, rect.h)}px`
65
+ }
66
+ }
67
+ )
68
+ }
69
+ )
70
+ ]
71
+ }
72
+ );
73
+ }
74
+ var DiagramMinimap_default = DiagramMinimap;
75
+ export {
76
+ DiagramMinimap,
77
+ DiagramMinimap_default as default
78
+ };
79
+ //# sourceMappingURL=DiagramMinimap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/DiagramMinimap/DiagramMinimap.tsx"],"sourcesContent":["'use client'\n\nimport type { PointerEvent as ReactPointerEvent, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport type { CanvasView } from '../DiagramCanvas/DiagramCanvas'\n\nexport interface DiagramMinimapProps {\n /** The whole artwork, at its natural size in CSS pixels. */\n content: { width: number; height: number }\n /** The frame the artwork is being looked at through, in CSS pixels. */\n frame: { width: number; height: number }\n /** Where that frame currently sits — a `DiagramCanvas`'s `onViewChange`. */\n view: CanvasView\n /** A miniature of the artwork. Usually the same figure, rendered again. */\n children?: ReactNode\n /** Called with a point in CONTENT coordinates when the reader picks one. */\n onSeek?: (x: number, y: number) => void\n /** How wide the map is, in CSS pixels. Height follows the aspect ratio. */\n width?: number\n className?: string\n label?: string\n}\n\n/**\n * Where you are in something bigger than the window.\n *\n * Two things at once, and both are needed: a miniature of the whole artwork,\n * and a rectangle showing which part of it the frame is currently over. The\n * miniature alone answers \"what is there\"; the rectangle answers \"and where am\n * I\", which is the question a reader who has just panned twice actually has.\n *\n * THE RECTANGLE IS DERIVED, never stored. Its position comes out of the\n * canvas's own view — the same three numbers the canvas is already transforming\n * by — divided by the map's scale. Keeping a second copy of \"where the viewport\n * is\" is how a minimap comes to disagree with the thing it is a map of, and a\n * map that disagrees is worse than none.\n *\n * CLICKING RECENTRES rather than jumping. `onSeek` reports a point in CONTENT\n * coordinates, which is what a canvas's `centerOn` takes. The minimap does not\n * move anything itself: it has no authority over the view, it only says where\n * the reader pointed.\n *\n * @example\n * const canvas = useRef<DiagramCanvasHandle>(null)\n * const [view, setView] = useState({ scale: 1, x: 0, y: 0 })\n * // …\n * <DiagramMinimap\n * content={{ width: 1400, height: 620 }}\n * frame={{ width: 720, height: 400 }}\n * view={view}\n * onSeek={(x, y) => canvas.current?.centerOn(x, y)}\n * >\n * <ArchitectureFigure spec={spec} heading={false} legend=\"hidden\" cards={false} />\n * </DiagramMinimap>\n */\nexport function DiagramMinimap({\n content,\n frame,\n view,\n children,\n onSeek,\n width = 200,\n className,\n label = 'Diagram overview',\n}: DiagramMinimapProps) {\n const scale = content.width > 0 ? width / content.width : 1\n const height = Math.max(1, content.height * scale)\n\n // The frame's rectangle in content space, then in map space. `view.x` is how\n // far the stage has been pushed, so the content coordinate at the frame's\n // top-left corner is its negation over the zoom.\n const rect = {\n x: (-view.x / view.scale) * scale,\n y: (-view.y / view.scale) * scale,\n w: (frame.width / view.scale) * scale,\n h: (frame.height / view.scale) * scale,\n }\n\n function seek(event: ReactPointerEvent<HTMLDivElement>) {\n if (!onSeek) return\n const box = event.currentTarget.getBoundingClientRect()\n onSeek((event.clientX - box.left) / scale, (event.clientY - box.top) / scale)\n }\n\n return (\n <div\n className={cn(\n 'relative overflow-hidden rounded-(--radius) border border-(--rule-2) bg-(--panel-bg)',\n className,\n )}\n style={{ width, height }}\n role=\"group\"\n aria-label={label}\n >\n {/* The miniature is decorative: the figure it mirrors already publishes\n its own accessible summary, and a second copy would read the whole\n diagram out twice. */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute start-0 top-0 origin-top-left\"\n style={{ width: content.width, transform: `scale(${scale})` }}\n >\n {children}\n </div>\n\n <div\n className={cn('absolute inset-0', onSeek && 'cursor-crosshair')}\n onPointerDown={seek}\n onPointerMove={(event) => {\n if (event.buttons === 1) seek(event)\n }}\n >\n <div\n aria-hidden=\"true\"\n className=\"absolute rounded-[2px] border border-(--accent) bg-(--accent-wash)\"\n style={{\n left: `${rect.x}px`,\n top: `${rect.y}px`,\n width: `${Math.max(4, rect.w)}px`,\n height: `${Math.max(4, rect.h)}px`,\n }}\n />\n </div>\n </div>\n )\n}\n\nexport default DiagramMinimap\n"],"mappings":";AAqFI,SAYE,KAZF;AAlFJ,SAAS,UAAU;AAoDZ,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,QAAQ;AACV,GAAwB;AACtB,QAAM,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,QAAQ,QAAQ;AAC1D,QAAM,SAAS,KAAK,IAAI,GAAG,QAAQ,SAAS,KAAK;AAKjD,QAAM,OAAO;AAAA,IACX,GAAI,CAAC,KAAK,IAAI,KAAK,QAAS;AAAA,IAC5B,GAAI,CAAC,KAAK,IAAI,KAAK,QAAS;AAAA,IAC5B,GAAI,MAAM,QAAQ,KAAK,QAAS;AAAA,IAChC,GAAI,MAAM,SAAS,KAAK,QAAS;AAAA,EACnC;AAEA,WAAS,KAAK,OAA0C;AACtD,QAAI,CAAC,OAAQ;AACb,UAAM,MAAM,MAAM,cAAc,sBAAsB;AACtD,YAAQ,MAAM,UAAU,IAAI,QAAQ,QAAQ,MAAM,UAAU,IAAI,OAAO,KAAK;AAAA,EAC9E;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,EAAE,OAAO,OAAO;AAAA,MACvB,MAAK;AAAA,MACL,cAAY;AAAA,MAKZ;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA,YACV,OAAO,EAAE,OAAO,QAAQ,OAAO,WAAW,SAAS,KAAK,IAAI;AAAA,YAE3D;AAAA;AAAA,QACH;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,GAAG,oBAAoB,UAAU,kBAAkB;AAAA,YAC9D,eAAe;AAAA,YACf,eAAe,CAAC,UAAU;AACxB,kBAAI,MAAM,YAAY,EAAG,MAAK,KAAK;AAAA,YACrC;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,MAAM,GAAG,KAAK,CAAC;AAAA,kBACf,KAAK,GAAG,KAAK,CAAC;AAAA,kBACd,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC,CAAC;AAAA,kBAC7B,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC,CAAC;AAAA,gBAChC;AAAA;AAAA,YACF;AAAA;AAAA,QACF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,yBAAQ;","names":[]}
@@ -0,0 +1,48 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps, ReactNode } from 'react';
3
+
4
+ interface DiagramToolbarProps extends Omit<ComponentProps<'div'>, 'children'> {
5
+ children: ReactNode;
6
+ /** Where it sits. `floating` pins it over the surface it acts on. */
7
+ placement?: 'inline' | 'floating';
8
+ /** Which corner a floating bar pins to. */
9
+ align?: 'start' | 'end';
10
+ label: string;
11
+ }
12
+ /**
13
+ * A bar of actions that belong to the surface underneath them.
14
+ *
15
+ * The package already has `FloatingIconButton` for ONE pinned action. This is
16
+ * the container for several — which is a different problem, because several
17
+ * pinned buttons need to read as one object rather than as a scatter: one
18
+ * plate, one border, hairlines between the groups, and a single `role="toolbar"`
19
+ * so a screen reader announces a toolbar instead of six unrelated buttons.
20
+ *
21
+ * `role="toolbar"` also changes the keyboard contract, and callers should know
22
+ * what they are opting into: arrow keys are expected to move between the
23
+ * controls and Tab is expected to leave the bar. This component does not
24
+ * implement roving focus for you — a bar of three buttons where Tab visits all
25
+ * three is honest and fine — so pass `placement="inline"` and skip the role by
26
+ * using a plain `<div>` if that is not what you want.
27
+ *
28
+ * @example
29
+ * <DiagramToolbar label="Diagram actions" placement="floating">
30
+ * <DiagramToolbarGroup>
31
+ * <Button size="sm" variant="ghost">Theme</Button>
32
+ * </DiagramToolbarGroup>
33
+ * <DiagramToolbarGroup>
34
+ * <DiagramExportMenu targetRef={svg} title="Request path" />
35
+ * </DiagramToolbarGroup>
36
+ * </DiagramToolbar>
37
+ */
38
+ declare function DiagramToolbar({ children, className, placement, align, label, ...rest }: DiagramToolbarProps): react.JSX.Element;
39
+ /**
40
+ * One run of related controls inside the bar.
41
+ *
42
+ * Separated by a hairline rather than by space, because a gap large enough to
43
+ * read as a group boundary is also large enough to stop the bar reading as one
44
+ * object — which is the whole reason the controls were collected into a bar.
45
+ */
46
+ declare function DiagramToolbarGroup({ children, className, ...rest }: ComponentProps<'div'>): react.JSX.Element;
47
+
48
+ export { DiagramToolbar, DiagramToolbarGroup, type DiagramToolbarProps, DiagramToolbar as default };