@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.
- package/README.md +12 -0
- package/dist/agent/Accordion.md +1 -1
- package/dist/agent/Alert.md +1 -1
- package/dist/agent/AppShell.md +1 -1
- package/dist/agent/ArchitectureFigure.md +48 -0
- package/dist/agent/AreaChart.md +1 -1
- package/dist/agent/Article.md +1 -1
- package/dist/agent/Avatar.md +1 -1
- package/dist/agent/Badge.md +1 -1
- package/dist/agent/BarChart.md +1 -1
- package/dist/agent/BarList.md +1 -1
- package/dist/agent/BigNumber.md +1 -1
- package/dist/agent/BoxPlot.md +1 -1
- package/dist/agent/Breadcrumb.md +1 -1
- package/dist/agent/BulletChart.md +1 -1
- package/dist/agent/Button.md +1 -1
- package/dist/agent/Calendar.md +1 -1
- package/dist/agent/Card.md +1 -1
- package/dist/agent/Checkbox.md +1 -1
- package/dist/agent/Collapsible.md +1 -1
- package/dist/agent/Combobox.md +1 -1
- package/dist/agent/Command.md +1 -1
- package/dist/agent/ComposedChart.md +1 -1
- package/dist/agent/ContextMenu.md +1 -1
- package/dist/agent/DataflowFigure.md +27 -0
- package/dist/agent/DatePicker.md +1 -1
- package/dist/agent/Diagram.md +1 -1
- package/dist/agent/DiagramCanvas.md +47 -0
- package/dist/agent/DiagramExportMenu.md +38 -0
- package/dist/agent/DiagramInspector.md +50 -0
- package/dist/agent/DiagramLegend.md +34 -0
- package/dist/agent/DiagramMinimap.md +48 -0
- package/dist/agent/DiagramToolbar.md +47 -0
- package/dist/agent/Dialog.md +1 -1
- package/dist/agent/DropdownMenu.md +1 -1
- package/dist/agent/EmptyState.md +1 -1
- package/dist/agent/ErrorState.md +1 -1
- package/dist/agent/Facet.md +1 -1
- package/dist/agent/Field.md +1 -1
- package/dist/agent/FigureBand.md +1 -1
- package/dist/agent/FloatingIconButton.md +1 -1
- package/dist/agent/FunnelChart.md +1 -1
- package/dist/agent/Heatmap.md +1 -1
- package/dist/agent/Histogram.md +1 -1
- package/dist/agent/Input.md +1 -1
- package/dist/agent/Kbd.md +1 -1
- package/dist/agent/LifecycleFigure.md +28 -0
- package/dist/agent/LineChart.md +1 -1
- package/dist/agent/LinkArrow.md +1 -1
- package/dist/agent/NativeSelect.md +1 -1
- package/dist/agent/NavItem.md +1 -1
- package/dist/agent/Pagination.md +1 -1
- package/dist/agent/PieChart.md +1 -1
- package/dist/agent/Popover.md +1 -1
- package/dist/agent/Progress.md +1 -1
- package/dist/agent/RadarChart.md +1 -1
- package/dist/agent/RadialChart.md +1 -1
- package/dist/agent/RadioGroup.md +1 -1
- package/dist/agent/SankeyChart.md +1 -1
- package/dist/agent/ScatterChart.md +1 -1
- package/dist/agent/ScrollArea.md +1 -1
- package/dist/agent/SearchableMenu.md +1 -1
- package/dist/agent/Select.md +1 -1
- package/dist/agent/Separator.md +1 -1
- package/dist/agent/SequenceFigure.md +28 -0
- package/dist/agent/Sheet.md +1 -1
- package/dist/agent/Skeleton.md +1 -1
- package/dist/agent/Slider.md +1 -1
- package/dist/agent/Sparkline.md +1 -1
- package/dist/agent/Spinner.md +1 -1
- package/dist/agent/StatusDot.md +1 -1
- package/dist/agent/StatusPill.md +1 -1
- package/dist/agent/Steps.md +1 -1
- package/dist/agent/Switch.md +1 -1
- package/dist/agent/Table.md +1 -1
- package/dist/agent/Tabs.md +1 -1
- package/dist/agent/Tag.md +1 -1
- package/dist/agent/Textarea.md +1 -1
- package/dist/agent/Toast.md +1 -1
- package/dist/agent/ToggleGroup.md +1 -1
- package/dist/agent/Tooltip.md +1 -1
- package/dist/agent/TreemapChart.md +1 -1
- package/dist/agent/WaterfallChart.md +1 -1
- package/dist/agent/WorkflowFigure.md +28 -0
- package/dist/agent/catalog.json +223 -3
- package/dist/agent/index.md +15 -1
- package/dist/charts/index.d.ts +2 -1
- package/dist/charts/lib/export.d.ts +33 -43
- package/dist/charts/lib/export.js +16 -158
- package/dist/charts/lib/export.js.map +1 -1
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +49 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +135 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.d.ts +28 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +129 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +64 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +222 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +72 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +125 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.d.ts +70 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +94 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.d.ts +42 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.js +35 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.js.map +1 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +61 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +79 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.d.ts +48 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.js +53 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.js.map +1 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.d.ts +31 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +202 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.d.ts +38 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +231 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +34 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +211 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -0
- package/dist/diagrams/index.d.ts +16 -0
- package/dist/diagrams/index.js +19 -0
- package/dist/diagrams/index.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +97 -0
- package/dist/diagrams/lib/frame.js +116 -0
- package/dist/diagrams/lib/frame.js.map +1 -0
- package/dist/diagrams/lib/geometry.d.ts +114 -0
- package/dist/diagrams/lib/geometry.js +113 -0
- package/dist/diagrams/lib/geometry.js.map +1 -0
- package/dist/diagrams/lib/legend.d.ts +32 -0
- package/dist/diagrams/lib/legend.js +84 -0
- package/dist/diagrams/lib/legend.js.map +1 -0
- package/dist/diagrams/lib/marks.d.ts +207 -0
- package/dist/diagrams/lib/marks.js +480 -0
- package/dist/diagrams/lib/marks.js.map +1 -0
- package/dist/diagrams/lib/route.d.ts +85 -0
- package/dist/diagrams/lib/route.js +178 -0
- package/dist/diagrams/lib/route.js.map +1 -0
- package/dist/diagrams/lib/wires.d.ts +59 -0
- package/dist/diagrams/lib/wires.js +90 -0
- package/dist/diagrams/lib/wires.js.map +1 -0
- package/dist/diagrams/spec.d.ts +353 -0
- package/dist/diagrams/spec.js +1 -0
- package/dist/diagrams/spec.js.map +1 -0
- package/dist/lib/svg-export.d.ts +138 -0
- package/dist/lib/svg-export.js +198 -0
- package/dist/lib/svg-export.js.map +1 -0
- package/dist/semantic.css +34 -0
- package/dist/styles.css +1 -1
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.js +82 -0
- package/dist/tokens.json +82 -0
- package/package.json +5 -1
- package/skills/misoto22-design/SKILL.md +21 -7
|
@@ -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 };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
function DiagramToolbar({
|
|
5
|
+
children,
|
|
6
|
+
className,
|
|
7
|
+
placement = "inline",
|
|
8
|
+
align = "end",
|
|
9
|
+
label,
|
|
10
|
+
...rest
|
|
11
|
+
}) {
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
"div",
|
|
14
|
+
{
|
|
15
|
+
role: "toolbar",
|
|
16
|
+
"aria-label": label,
|
|
17
|
+
"aria-orientation": "horizontal",
|
|
18
|
+
className: cn(
|
|
19
|
+
"flex items-center gap-1 rounded-(--radius) border border-(--rule-2) bg-(--panel-bg) p-1",
|
|
20
|
+
placement === "floating" && "absolute top-3 z-(--z-sticky)",
|
|
21
|
+
placement === "floating" && (align === "end" ? "end-3" : "start-3"),
|
|
22
|
+
className
|
|
23
|
+
),
|
|
24
|
+
...rest,
|
|
25
|
+
children
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function DiagramToolbarGroup({
|
|
30
|
+
children,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}) {
|
|
34
|
+
return /* @__PURE__ */ jsx(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
className: cn(
|
|
38
|
+
"flex items-center gap-0.5",
|
|
39
|
+
"border-s border-(--rule) ps-1 first:border-s-0 first:ps-0",
|
|
40
|
+
className
|
|
41
|
+
),
|
|
42
|
+
...rest,
|
|
43
|
+
children
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
var DiagramToolbar_default = DiagramToolbar;
|
|
48
|
+
export {
|
|
49
|
+
DiagramToolbar,
|
|
50
|
+
DiagramToolbarGroup,
|
|
51
|
+
DiagramToolbar_default as default
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=DiagramToolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/DiagramToolbar/DiagramToolbar.tsx"],"sourcesContent":["'use client'\n\nimport type { ComponentProps, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface DiagramToolbarProps extends Omit<ComponentProps<'div'>, 'children'> {\n children: ReactNode\n /** Where it sits. `floating` pins it over the surface it acts on. */\n placement?: 'inline' | 'floating'\n /** Which corner a floating bar pins to. */\n align?: 'start' | 'end'\n label: string\n}\n\n/**\n * A bar of actions that belong to the surface underneath them.\n *\n * The package already has `FloatingIconButton` for ONE pinned action. This is\n * the container for several — which is a different problem, because several\n * pinned buttons need to read as one object rather than as a scatter: one\n * plate, one border, hairlines between the groups, and a single `role=\"toolbar\"`\n * so a screen reader announces a toolbar instead of six unrelated buttons.\n *\n * `role=\"toolbar\"` also changes the keyboard contract, and callers should know\n * what they are opting into: arrow keys are expected to move between the\n * controls and Tab is expected to leave the bar. This component does not\n * implement roving focus for you — a bar of three buttons where Tab visits all\n * three is honest and fine — so pass `placement=\"inline\"` and skip the role by\n * using a plain `<div>` if that is not what you want.\n *\n * @example\n * <DiagramToolbar label=\"Diagram actions\" placement=\"floating\">\n * <DiagramToolbarGroup>\n * <Button size=\"sm\" variant=\"ghost\">Theme</Button>\n * </DiagramToolbarGroup>\n * <DiagramToolbarGroup>\n * <DiagramExportMenu targetRef={svg} title=\"Request path\" />\n * </DiagramToolbarGroup>\n * </DiagramToolbar>\n */\nexport function DiagramToolbar({\n children,\n className,\n placement = 'inline',\n align = 'end',\n label,\n ...rest\n}: DiagramToolbarProps) {\n return (\n <div\n role=\"toolbar\"\n aria-label={label}\n aria-orientation=\"horizontal\"\n className={cn(\n 'flex items-center gap-1 rounded-(--radius) border border-(--rule-2) bg-(--panel-bg) p-1',\n placement === 'floating' && 'absolute top-3 z-(--z-sticky)',\n placement === 'floating' && (align === 'end' ? 'end-3' : 'start-3'),\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\n/**\n * One run of related controls inside the bar.\n *\n * Separated by a hairline rather than by space, because a gap large enough to\n * read as a group boundary is also large enough to stop the bar reading as one\n * object — which is the whole reason the controls were collected into a bar.\n */\nexport function DiagramToolbarGroup({\n children,\n className,\n ...rest\n}: ComponentProps<'div'>) {\n return (\n <div\n className={cn(\n 'flex items-center gap-0.5',\n 'border-s border-(--rule) ps-1 first:border-s-0 first:ps-0',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nexport default DiagramToolbar\n"],"mappings":";AAiDI;AA9CJ,SAAS,UAAU;AAqCZ,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,oBAAiB;AAAA,MACjB,WAAW;AAAA,QACT;AAAA,QACA,cAAc,cAAc;AAAA,QAC5B,cAAc,eAAe,UAAU,QAAQ,UAAU;AAAA,QACzD;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AASO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,yBAAQ;","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { FigureChrome } from '../lib/frame.js';
|
|
3
|
+
import { LifecycleSpec } from '../spec.js';
|
|
4
|
+
import '../lib/geometry.js';
|
|
5
|
+
|
|
6
|
+
interface LifecycleFigureProps extends FigureChrome {
|
|
7
|
+
spec: LifecycleSpec;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A state machine: what something can BE, and what moves it.
|
|
11
|
+
*
|
|
12
|
+
* The one figure here that spends colour, and it spends exactly the two tokens
|
|
13
|
+
* this system reserves for state: `--success` on a terminal success,
|
|
14
|
+
* `--danger` on a terminal failure. Nothing else in the package is allowed
|
|
15
|
+
* those two, and a lifecycle diagram is the case they were reserved for — the
|
|
16
|
+
* whole point of the picture is which end a run came out of.
|
|
17
|
+
*
|
|
18
|
+
* The other six kinds are carried by shape, so a greyscale print loses the two
|
|
19
|
+
* outcomes' hue and keeps every other distinction: a start is a filled cap, a
|
|
20
|
+
* decision is a diamond, a wait is a dashed frame, an external is a plate with
|
|
21
|
+
* a cut corner.
|
|
22
|
+
*
|
|
23
|
+
* A RECOVERABLE FAILURE IS A FAILURE WITH A WAY BACK. `type: "failure"` says
|
|
24
|
+
* how a state is drawn; a transition OUT of it is what says whether the run is
|
|
25
|
+
* over. A retryable error and a terminal one look identical until you follow
|
|
26
|
+
* the arrows, which is correct — the difference between them is a real edge in
|
|
27
|
+
* the machine, not an adjective on a box.
|
|
28
|
+
*/
|
|
29
|
+
declare function LifecycleFigure({ spec, className, legend, cards, heading, activeIds, onSelectNode, }: LifecycleFigureProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
export { LifecycleFigure, type LifecycleFigureProps, LifecycleFigure as default };
|
|
@@ -0,0 +1,202 @@
|
|
|
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, textWidth, union, wrapText } from "../lib/geometry.js";
|
|
6
|
+
import { resolveLegend, stateLegend } from "../lib/legend.js";
|
|
7
|
+
import { Chip, PLATE, plateHeight, StatePlate } from "../lib/marks.js";
|
|
8
|
+
import { renderWires, wiresFor } from "../lib/wires.js";
|
|
9
|
+
const KINDS = [
|
|
10
|
+
"start",
|
|
11
|
+
"active",
|
|
12
|
+
"waiting",
|
|
13
|
+
"decision",
|
|
14
|
+
"success",
|
|
15
|
+
"failure",
|
|
16
|
+
"neutral",
|
|
17
|
+
"external"
|
|
18
|
+
];
|
|
19
|
+
const GRID = { originX: 132, originY: 56, colW: 158, gapX: 62, laneGap: 52, stateH: 68 };
|
|
20
|
+
const SECONDARY_LANE_OFFSET = 2;
|
|
21
|
+
function textRoom(width, kind) {
|
|
22
|
+
return kind === "decision" ? width * 0.56 : width - PLATE.padX * 2;
|
|
23
|
+
}
|
|
24
|
+
function LifecycleFigure({
|
|
25
|
+
spec,
|
|
26
|
+
className,
|
|
27
|
+
legend = "auto",
|
|
28
|
+
cards = true,
|
|
29
|
+
heading = true,
|
|
30
|
+
activeIds,
|
|
31
|
+
onSelectNode
|
|
32
|
+
}) {
|
|
33
|
+
const uid = useId().replace(/:/g, "");
|
|
34
|
+
const model = useMemo(
|
|
35
|
+
() => buildModel(spec, uid, activeIds, onSelectNode),
|
|
36
|
+
[spec, uid, activeIds, onSelectNode]
|
|
37
|
+
);
|
|
38
|
+
return /* @__PURE__ */ jsx(
|
|
39
|
+
DiagramFrame,
|
|
40
|
+
{
|
|
41
|
+
meta: spec.meta,
|
|
42
|
+
model,
|
|
43
|
+
cards: spec.cards,
|
|
44
|
+
showCards: cards,
|
|
45
|
+
heading,
|
|
46
|
+
legend,
|
|
47
|
+
className,
|
|
48
|
+
activeIds,
|
|
49
|
+
onSelectNode
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
54
|
+
const lanes = spec.lanes ?? [{ id: "", label: "" }];
|
|
55
|
+
const laneIndex = new Map(lanes.map((lane, index) => [lane.id, index]));
|
|
56
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
57
|
+
const placed = spec.states.map((state) => {
|
|
58
|
+
const w = state.width ?? (state.type === "decision" ? GRID.colW + 34 : GRID.colW);
|
|
59
|
+
const lines = wrapText(state.label, TYPE.label, textRoom(w, state.type), 2);
|
|
60
|
+
const h = plateHeight(lines.length, Boolean(state.sublabel), true, state.height ?? GRID.stateH);
|
|
61
|
+
const lane = laneIndex.get(state.lane ?? "") ?? 0;
|
|
62
|
+
const col = state.col + (lane === 0 ? 0 : SECONDARY_LANE_OFFSET);
|
|
63
|
+
const x = GRID.originX + col * (GRID.colW + GRID.gapX);
|
|
64
|
+
const y = GRID.originY + lane * (GRID.stateH + GRID.laneGap);
|
|
65
|
+
const box = { x, y, w, h };
|
|
66
|
+
boxes.set(state.id, box);
|
|
67
|
+
return { state, box, lane };
|
|
68
|
+
});
|
|
69
|
+
const rail = placed.filter((entry) => entry.lane === 0).sort((a, b) => a.state.col - b.state.col);
|
|
70
|
+
const declared = new Set((spec.transitions ?? []).map((t) => `${t.from}\u2192${t.to}`));
|
|
71
|
+
const implied = rail.slice(0, -1).map((entry, index) => ({ from: entry.state.id, to: rail[index + 1].state.id })).filter((edge) => !declared.has(`${edge.from}\u2192${edge.to}`));
|
|
72
|
+
const wires = [
|
|
73
|
+
...wiresFor(implied, boxes, () => ({ weight: 2.1 })),
|
|
74
|
+
...wiresFor(spec.transitions, boxes)
|
|
75
|
+
];
|
|
76
|
+
const { paths, labels } = renderWires(wires, uid);
|
|
77
|
+
const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 34);
|
|
78
|
+
const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null;
|
|
79
|
+
const laneRules = lanes.map((lane, index) => {
|
|
80
|
+
const inside = placed.filter((entry) => entry.lane === index).map((entry) => entry.box);
|
|
81
|
+
const span = union(inside);
|
|
82
|
+
return span ? { lane, y: span.y - 16, x1: extent.x + 18, x2: span.x + span.w } : null;
|
|
83
|
+
});
|
|
84
|
+
const artwork = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
85
|
+
laneRules.map(
|
|
86
|
+
(rule, index) => rule && rule.lane.label ? /* @__PURE__ */ jsxs("g", { "aria-hidden": "true", children: [
|
|
87
|
+
/* @__PURE__ */ jsx(
|
|
88
|
+
"path",
|
|
89
|
+
{
|
|
90
|
+
d: `M ${round(rule.x1)} ${round(rule.y)} H ${round(rule.x2)}`,
|
|
91
|
+
className: "stroke-(--diagram-rule) [stroke-width:1]"
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
"text",
|
|
96
|
+
{
|
|
97
|
+
x: round(rule.x1),
|
|
98
|
+
y: round(rule.y - 7),
|
|
99
|
+
className: "fill-(--diagram-ink-3) font-mono [font-size:10.5px] [letter-spacing:0.08em] uppercase",
|
|
100
|
+
children: rule.lane.label
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
] }, `lane-${index}`) : null
|
|
104
|
+
),
|
|
105
|
+
paths,
|
|
106
|
+
placed.map(({ state, box }) => {
|
|
107
|
+
const lines = wrapText(state.label, TYPE.label, textRoom(box.w, state.type), 2);
|
|
108
|
+
const bottomReserve = state.tag ? 20 : 0;
|
|
109
|
+
const blockHeight = lines.length * (TYPE.label * 1.25) + (state.sublabel ? TYPE.sub * 1.5 : 0);
|
|
110
|
+
const top = box.y + (box.h - bottomReserve - blockHeight) / 2;
|
|
111
|
+
const reversed = state.type === "start";
|
|
112
|
+
const centreX = box.x + box.w / 2;
|
|
113
|
+
return /* @__PURE__ */ jsx(
|
|
114
|
+
"g",
|
|
115
|
+
{
|
|
116
|
+
"data-node": state.id,
|
|
117
|
+
"data-active": active?.has(state.id) ? "" : void 0,
|
|
118
|
+
className: active && !active.has(state.id) ? "opacity-25" : void 0,
|
|
119
|
+
onClick: onSelectNode ? () => onSelectNode(state.id) : void 0,
|
|
120
|
+
children: /* @__PURE__ */ jsxs(StatePlate, { box, kind: state.type, children: [
|
|
121
|
+
active?.has(state.id) && /* @__PURE__ */ jsx(
|
|
122
|
+
"rect",
|
|
123
|
+
{
|
|
124
|
+
x: round(box.x) - 3,
|
|
125
|
+
y: round(box.y) - 3,
|
|
126
|
+
width: round(box.w) + 6,
|
|
127
|
+
height: round(box.h) + 6,
|
|
128
|
+
rx: 9,
|
|
129
|
+
fill: "none",
|
|
130
|
+
className: "stroke-(--accent) [stroke-width:1.4]"
|
|
131
|
+
}
|
|
132
|
+
),
|
|
133
|
+
state.step && /* @__PURE__ */ jsx(
|
|
134
|
+
"text",
|
|
135
|
+
{
|
|
136
|
+
x: round(box.x + 10),
|
|
137
|
+
y: round(box.y + 15),
|
|
138
|
+
className: reversed ? "fill-(--diagram-plate-ink) font-mono [font-size:9.5px] opacity-70" : "fill-(--diagram-ink-3) font-mono [font-size:9.5px]",
|
|
139
|
+
children: state.step
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
lines.map((line, index) => /* @__PURE__ */ jsx(
|
|
143
|
+
"text",
|
|
144
|
+
{
|
|
145
|
+
x: round(centreX),
|
|
146
|
+
y: round(top + (index + 1) * TYPE.label * 1.25 - 3.5),
|
|
147
|
+
textAnchor: "middle",
|
|
148
|
+
className: reversed ? "fill-(--diagram-plate-ink) font-sans [font-size:13px]" : "fill-(--diagram-ink) font-sans [font-size:13px]",
|
|
149
|
+
children: line
|
|
150
|
+
},
|
|
151
|
+
index
|
|
152
|
+
)),
|
|
153
|
+
state.sublabel && /* @__PURE__ */ jsx(
|
|
154
|
+
"text",
|
|
155
|
+
{
|
|
156
|
+
x: round(centreX),
|
|
157
|
+
y: round(top + lines.length * TYPE.label * 1.25 + TYPE.sub),
|
|
158
|
+
textAnchor: "middle",
|
|
159
|
+
className: reversed ? "fill-(--diagram-plate-ink) font-mono [font-size:10.5px] opacity-70" : "fill-(--diagram-ink-3) font-mono [font-size:10.5px]",
|
|
160
|
+
children: state.sublabel
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
state.tag && /* @__PURE__ */ jsx(Chip, { x: centreX, y: box.y + box.h - 9, text: state.tag, align: "middle" })
|
|
164
|
+
] })
|
|
165
|
+
},
|
|
166
|
+
state.id
|
|
167
|
+
);
|
|
168
|
+
}),
|
|
169
|
+
labels
|
|
170
|
+
] });
|
|
171
|
+
const used = [...new Set(spec.states.map((state) => state.type))];
|
|
172
|
+
const widest = Math.max(
|
|
173
|
+
0,
|
|
174
|
+
...lanes.map((lane) => textWidth(lane.label, TYPE.band))
|
|
175
|
+
);
|
|
176
|
+
const railLeft = Math.min(extent.x, GRID.originX - widest - 30);
|
|
177
|
+
return {
|
|
178
|
+
extent: { ...extent, x: railLeft, w: extent.w + (extent.x - railLeft) },
|
|
179
|
+
artwork,
|
|
180
|
+
nodes: spec.states.map((state) => ({
|
|
181
|
+
id: state.id,
|
|
182
|
+
label: state.label,
|
|
183
|
+
sublabel: state.sublabel,
|
|
184
|
+
kind: state.type
|
|
185
|
+
})),
|
|
186
|
+
edges: (spec.transitions ?? []).map((transition) => ({
|
|
187
|
+
from: transition.from,
|
|
188
|
+
to: transition.to,
|
|
189
|
+
label: [transition.label, transition.note].filter(Boolean).join(" \u2014 ") || void 0
|
|
190
|
+
})),
|
|
191
|
+
legend: stateLegend(
|
|
192
|
+
resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),
|
|
193
|
+
spec.meta.legend?.entries
|
|
194
|
+
)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
var LifecycleFigure_default = LifecycleFigure;
|
|
198
|
+
export {
|
|
199
|
+
LifecycleFigure,
|
|
200
|
+
LifecycleFigure_default as default
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=LifecycleFigure.js.map
|