@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.
- 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 +2 -2
- 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 +2 -2
- package/dist/agent/BarList.md +2 -2
- package/dist/agent/BigNumber.md +2 -2
- package/dist/agent/BoxPlot.md +2 -2
- package/dist/agent/Breadcrumb.md +1 -1
- package/dist/agent/BulletChart.md +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- package/dist/agent/Heatmap.md +2 -2
- package/dist/agent/Histogram.md +2 -2
- 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 +2 -2
- 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 +2 -2
- package/dist/agent/Popover.md +1 -1
- package/dist/agent/Progress.md +1 -1
- package/dist/agent/RadarChart.md +2 -2
- package/dist/agent/RadialChart.md +2 -2
- package/dist/agent/RadioGroup.md +1 -1
- package/dist/agent/SankeyChart.md +2 -2
- package/dist/agent/ScatterChart.md +2 -2
- 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 +2 -2
- 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 +2 -2
- package/dist/agent/WaterfallChart.md +2 -2
- package/dist/agent/WorkflowFigure.md +28 -0
- package/dist/agent/catalog.json +295 -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 +58 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic every figure shares: boxes, text extents, and the frame that
|
|
3
|
+
* ends up around them.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is a pure function of numbers already in the specification.
|
|
6
|
+
* That is not a stylistic preference — it is what lets these figures render on
|
|
7
|
+
* a server. A layout that measured real text would need a DOM, a layout that
|
|
8
|
+
* relaxed a force graph would need to settle, and neither can produce the same
|
|
9
|
+
* markup twice. Both would hydrate into a different picture than the one that
|
|
10
|
+
* was sent, which for a diagram is worse than not rendering at all.
|
|
11
|
+
*/
|
|
12
|
+
/** A rectangle in the figure's user-unit space. */
|
|
13
|
+
interface Box {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
w: number;
|
|
17
|
+
h: number;
|
|
18
|
+
}
|
|
19
|
+
declare const centerOf: (box: Box) => [number, number];
|
|
20
|
+
/** Grows a rectangle by `pad` on every side. */
|
|
21
|
+
declare function inflate(box: Box, pad: number): Box;
|
|
22
|
+
/** The smallest rectangle containing all of them. Returns null for an empty list. */
|
|
23
|
+
declare function union(boxes: Box[]): Box | null;
|
|
24
|
+
/**
|
|
25
|
+
* How wide a run of text is, without measuring it.
|
|
26
|
+
*
|
|
27
|
+
* A real measurement needs a DOM and the loaded face, and this module has
|
|
28
|
+
* neither by design — so this is an estimate, and the honest thing is to say
|
|
29
|
+
* what kind. Latin glyphs in the system's sans average close to 0.52em of
|
|
30
|
+
* advance across ordinary label text; digits and capitals run wider; a CJK
|
|
31
|
+
* ideograph is a full em by definition, as is a fullwidth punctuation mark.
|
|
32
|
+
*
|
|
33
|
+
* The estimate is used for two things and neither needs to be exact: choosing a
|
|
34
|
+
* box wide enough that a label is not clipped, and deciding where a line's
|
|
35
|
+
* label mask sits. Both fail gracefully — a box a few units too wide is
|
|
36
|
+
* invisible, and a mask a few units too wide hides a little more of a line than
|
|
37
|
+
* it had to.
|
|
38
|
+
*
|
|
39
|
+
* It is deliberately NOT used to justify, wrap mid-word, or letter-space
|
|
40
|
+
* anything. Those need the real face.
|
|
41
|
+
*/
|
|
42
|
+
declare function textWidth(text: string, fontSize: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* A label broken to fit a width, with a hard cap on how many lines it may take.
|
|
45
|
+
*
|
|
46
|
+
* Breaks on spaces for Latin and between characters for CJK, which has no
|
|
47
|
+
* spaces to break on. A word longer than the whole line is left to overflow
|
|
48
|
+
* rather than hyphenated: an identifier — `TenantMainMiddleware` — split across
|
|
49
|
+
* two lines is harder to read than one that runs slightly wide, and the caller
|
|
50
|
+
* sized the box from `textWidth` anyway.
|
|
51
|
+
*
|
|
52
|
+
* The last line is ellipsised when the text does not fit in `maxLines`, because
|
|
53
|
+
* silently dropping the tail is how a diagram comes to say something it does
|
|
54
|
+
* not mean.
|
|
55
|
+
*/
|
|
56
|
+
declare function wrapText(text: string, fontSize: number, maxWidth: number, maxLines?: number): string[];
|
|
57
|
+
/**
|
|
58
|
+
* The type scale inside a figure, in user units.
|
|
59
|
+
*
|
|
60
|
+
* Fixed numbers rather than tokens, because these are SVG geometry: a `<text>`
|
|
61
|
+
* sized in `rem` inside a scaled `viewBox` changes size when the figure is
|
|
62
|
+
* zoomed, which means a diagram at 200% is a different diagram. The figure
|
|
63
|
+
* scales as one picture, so its type scales with it.
|
|
64
|
+
*/
|
|
65
|
+
declare const TYPE: {
|
|
66
|
+
/** A node's name. */
|
|
67
|
+
readonly label: 13;
|
|
68
|
+
/** The line under it. */
|
|
69
|
+
readonly sub: 10.5;
|
|
70
|
+
/** A mono chip: a tag, a classification, a step number. */
|
|
71
|
+
readonly chip: 9.5;
|
|
72
|
+
/** A relationship's own wording. */
|
|
73
|
+
readonly edge: 10.5;
|
|
74
|
+
/** A band, lane or stage heading. */
|
|
75
|
+
readonly band: 10.5;
|
|
76
|
+
};
|
|
77
|
+
/** Default box sizes per diagram type, in user units. */
|
|
78
|
+
declare const BOX: {
|
|
79
|
+
readonly node: {
|
|
80
|
+
readonly w: 168;
|
|
81
|
+
readonly h: 62;
|
|
82
|
+
};
|
|
83
|
+
readonly wide: {
|
|
84
|
+
readonly w: 200;
|
|
85
|
+
readonly h: 62;
|
|
86
|
+
};
|
|
87
|
+
readonly participant: {
|
|
88
|
+
readonly w: 128;
|
|
89
|
+
readonly h: 46;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* How tall a box has to be to hold what it was given.
|
|
94
|
+
*
|
|
95
|
+
* A declared height is a FLOOR, not a ceiling. When a specification asks for a
|
|
96
|
+
* 64-unit box and then puts two lines of label, a sublabel and a tag chip in
|
|
97
|
+
* it, honouring the 64 prints the tag over the sublabel — which reads as a
|
|
98
|
+
* rendering fault rather than as a box that was asked to hold too much. Growing
|
|
99
|
+
* the box is visible and correct; clipping is invisible and wrong.
|
|
100
|
+
*/
|
|
101
|
+
declare function nodeHeight(labelLines: number, hasSub: boolean, hasTag?: boolean, base?: number): number;
|
|
102
|
+
/** `viewBox` as the attribute wants it. */
|
|
103
|
+
declare function viewBoxOf(box: Box): string;
|
|
104
|
+
/**
|
|
105
|
+
* Coordinates rounded to a tenth.
|
|
106
|
+
*
|
|
107
|
+
* SVG happily accepts eighteen decimal places and puts every one of them in the
|
|
108
|
+
* markup, which on a figure with two hundred line segments is a measurable
|
|
109
|
+
* share of the document. A tenth of a user unit is well under a device pixel at
|
|
110
|
+
* any zoom a reader will use.
|
|
111
|
+
*/
|
|
112
|
+
declare function round(value: number): number;
|
|
113
|
+
|
|
114
|
+
export { BOX, type Box, TYPE, centerOf, inflate, nodeHeight, round, textWidth, union, viewBoxOf, wrapText };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const centerOf = (box) => [box.x + box.w / 2, box.y + box.h / 2];
|
|
2
|
+
function inflate(box, pad) {
|
|
3
|
+
return { x: box.x - pad, y: box.y - pad, w: box.w + pad * 2, h: box.h + pad * 2 };
|
|
4
|
+
}
|
|
5
|
+
function union(boxes) {
|
|
6
|
+
if (boxes.length === 0) return null;
|
|
7
|
+
let minX = Infinity;
|
|
8
|
+
let minY = Infinity;
|
|
9
|
+
let maxX = -Infinity;
|
|
10
|
+
let maxY = -Infinity;
|
|
11
|
+
for (const box of boxes) {
|
|
12
|
+
if (box.x < minX) minX = box.x;
|
|
13
|
+
if (box.y < minY) minY = box.y;
|
|
14
|
+
if (box.x + box.w > maxX) maxX = box.x + box.w;
|
|
15
|
+
if (box.y + box.h > maxY) maxY = box.y + box.h;
|
|
16
|
+
}
|
|
17
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
18
|
+
}
|
|
19
|
+
function textWidth(text, fontSize) {
|
|
20
|
+
let em = 0;
|
|
21
|
+
for (const character of text) {
|
|
22
|
+
const code = character.codePointAt(0) ?? 0;
|
|
23
|
+
if (code > 11903) {
|
|
24
|
+
em += 1;
|
|
25
|
+
} else if (character === " ") {
|
|
26
|
+
em += 0.26;
|
|
27
|
+
} else if (/[.,:;'`!|iljtIf()[\]{}]/.test(character)) {
|
|
28
|
+
em += 0.31;
|
|
29
|
+
} else if (/[A-Z0-9@#%&WM]/.test(character)) {
|
|
30
|
+
em += 0.62;
|
|
31
|
+
} else {
|
|
32
|
+
em += 0.52;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return em * fontSize;
|
|
36
|
+
}
|
|
37
|
+
function wrapText(text, fontSize, maxWidth, maxLines = 2) {
|
|
38
|
+
if (textWidth(text, fontSize) <= maxWidth) return [text];
|
|
39
|
+
const lines = [];
|
|
40
|
+
let current = "";
|
|
41
|
+
const tokens = text.match(/[⺀-鿿豈--]|[^\s⺀-鿿]+|\s+/g) ?? [text];
|
|
42
|
+
for (const token of tokens) {
|
|
43
|
+
const candidate = current + token;
|
|
44
|
+
if (current !== "" && textWidth(candidate.trimEnd(), fontSize) > maxWidth) {
|
|
45
|
+
lines.push(current.trimEnd());
|
|
46
|
+
current = token.trimStart();
|
|
47
|
+
if (lines.length === maxLines) break;
|
|
48
|
+
} else {
|
|
49
|
+
current = candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (lines.length < maxLines && current.trim() !== "") lines.push(current.trimEnd());
|
|
53
|
+
if (lines.length === maxLines) {
|
|
54
|
+
const placed = lines.join("").replace(/\s+/g, "");
|
|
55
|
+
const whole = text.replace(/\s+/g, "");
|
|
56
|
+
if (placed.length < whole.length) {
|
|
57
|
+
const last = lines[maxLines - 1] ?? "";
|
|
58
|
+
lines[maxLines - 1] = `${clampToWidth(last, fontSize, maxWidth - textWidth("\u2026", fontSize))}\u2026`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
function clampToWidth(text, fontSize, maxWidth) {
|
|
64
|
+
let width = 0;
|
|
65
|
+
let out = "";
|
|
66
|
+
for (const character of text) {
|
|
67
|
+
const next = textWidth(character, fontSize);
|
|
68
|
+
if (width + next > maxWidth) break;
|
|
69
|
+
width += next;
|
|
70
|
+
out += character;
|
|
71
|
+
}
|
|
72
|
+
return out.trimEnd();
|
|
73
|
+
}
|
|
74
|
+
const TYPE = {
|
|
75
|
+
/** A node's name. */
|
|
76
|
+
label: 13,
|
|
77
|
+
/** The line under it. */
|
|
78
|
+
sub: 10.5,
|
|
79
|
+
/** A mono chip: a tag, a classification, a step number. */
|
|
80
|
+
chip: 9.5,
|
|
81
|
+
/** A relationship's own wording. */
|
|
82
|
+
edge: 10.5,
|
|
83
|
+
/** A band, lane or stage heading. */
|
|
84
|
+
band: 10.5
|
|
85
|
+
};
|
|
86
|
+
const BOX = {
|
|
87
|
+
node: { w: 168, h: 62 },
|
|
88
|
+
wide: { w: 200, h: 62 },
|
|
89
|
+
participant: { w: 128, h: 46 }
|
|
90
|
+
};
|
|
91
|
+
function nodeHeight(labelLines, hasSub, hasTag = false, base = BOX.node.h) {
|
|
92
|
+
const content = 20 + labelLines * (TYPE.label * 1.25) + (hasSub ? TYPE.sub * 1.5 : 0) + (hasTag ? 20 : 0);
|
|
93
|
+
return Math.max(base, Math.ceil(content));
|
|
94
|
+
}
|
|
95
|
+
function viewBoxOf(box) {
|
|
96
|
+
return `${round(box.x)} ${round(box.y)} ${round(box.w)} ${round(box.h)}`;
|
|
97
|
+
}
|
|
98
|
+
function round(value) {
|
|
99
|
+
return Math.round(value * 10) / 10;
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
BOX,
|
|
103
|
+
TYPE,
|
|
104
|
+
centerOf,
|
|
105
|
+
inflate,
|
|
106
|
+
nodeHeight,
|
|
107
|
+
round,
|
|
108
|
+
textWidth,
|
|
109
|
+
union,
|
|
110
|
+
viewBoxOf,
|
|
111
|
+
wrapText
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/lib/geometry.ts"],"sourcesContent":["/**\n * The arithmetic every figure shares: boxes, text extents, and the frame that\n * ends up around them.\n *\n * Everything here is a pure function of numbers already in the specification.\n * That is not a stylistic preference — it is what lets these figures render on\n * a server. A layout that measured real text would need a DOM, a layout that\n * relaxed a force graph would need to settle, and neither can produce the same\n * markup twice. Both would hydrate into a different picture than the one that\n * was sent, which for a diagram is worse than not rendering at all.\n */\n\n/** A rectangle in the figure's user-unit space. */\nexport interface Box {\n x: number\n y: number\n w: number\n h: number\n}\n\nexport const centerOf = (box: Box): [number, number] => [box.x + box.w / 2, box.y + box.h / 2]\n\n/** Grows a rectangle by `pad` on every side. */\nexport function inflate(box: Box, pad: number): Box {\n return { x: box.x - pad, y: box.y - pad, w: box.w + pad * 2, h: box.h + pad * 2 }\n}\n\n/** The smallest rectangle containing all of them. Returns null for an empty list. */\nexport function union(boxes: Box[]): Box | null {\n if (boxes.length === 0) return null\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n for (const box of boxes) {\n if (box.x < minX) minX = box.x\n if (box.y < minY) minY = box.y\n if (box.x + box.w > maxX) maxX = box.x + box.w\n if (box.y + box.h > maxY) maxY = box.y + box.h\n }\n return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }\n}\n\n/**\n * How wide a run of text is, without measuring it.\n *\n * A real measurement needs a DOM and the loaded face, and this module has\n * neither by design — so this is an estimate, and the honest thing is to say\n * what kind. Latin glyphs in the system's sans average close to 0.52em of\n * advance across ordinary label text; digits and capitals run wider; a CJK\n * ideograph is a full em by definition, as is a fullwidth punctuation mark.\n *\n * The estimate is used for two things and neither needs to be exact: choosing a\n * box wide enough that a label is not clipped, and deciding where a line's\n * label mask sits. Both fail gracefully — a box a few units too wide is\n * invisible, and a mask a few units too wide hides a little more of a line than\n * it had to.\n *\n * It is deliberately NOT used to justify, wrap mid-word, or letter-space\n * anything. Those need the real face.\n */\nexport function textWidth(text: string, fontSize: number): number {\n let em = 0\n for (const character of text) {\n const code = character.codePointAt(0) ?? 0\n if (code > 0x2e7f) {\n // CJK, kana, hangul, fullwidth forms — one em each.\n em += 1\n } else if (character === ' ') {\n em += 0.26\n } else if (/[.,:;'`!|iljtIf()[\\]{}]/.test(character)) {\n em += 0.31\n } else if (/[A-Z0-9@#%&WM]/.test(character)) {\n em += 0.62\n } else {\n em += 0.52\n }\n }\n return em * fontSize\n}\n\n/**\n * A label broken to fit a width, with a hard cap on how many lines it may take.\n *\n * Breaks on spaces for Latin and between characters for CJK, which has no\n * spaces to break on. A word longer than the whole line is left to overflow\n * rather than hyphenated: an identifier — `TenantMainMiddleware` — split across\n * two lines is harder to read than one that runs slightly wide, and the caller\n * sized the box from `textWidth` anyway.\n *\n * The last line is ellipsised when the text does not fit in `maxLines`, because\n * silently dropping the tail is how a diagram comes to say something it does\n * not mean.\n */\nexport function wrapText(\n text: string,\n fontSize: number,\n maxWidth: number,\n maxLines = 2,\n): string[] {\n if (textWidth(text, fontSize) <= maxWidth) return [text]\n\n const lines: string[] = []\n let current = ''\n\n // Latin words stay whole; a CJK run is a sequence of individual break\n // opportunities, so each ideograph becomes its own token.\n const tokens = text.match(/[⺀-鿿豈--]|[^\\s⺀-鿿]+|\\s+/g) ?? [text]\n\n for (const token of tokens) {\n const candidate = current + token\n if (current !== '' && textWidth(candidate.trimEnd(), fontSize) > maxWidth) {\n lines.push(current.trimEnd())\n current = token.trimStart()\n if (lines.length === maxLines) break\n } else {\n current = candidate\n }\n }\n\n if (lines.length < maxLines && current.trim() !== '') lines.push(current.trimEnd())\n\n if (lines.length === maxLines) {\n // Anything still unplaced belongs to the last line, which must therefore\n // admit that it is truncated.\n const placed = lines.join('').replace(/\\s+/g, '')\n const whole = text.replace(/\\s+/g, '')\n if (placed.length < whole.length) {\n const last = lines[maxLines - 1] ?? ''\n lines[maxLines - 1] = `${clampToWidth(last, fontSize, maxWidth - textWidth('…', fontSize))}…`\n }\n }\n\n return lines\n}\n\n/** The longest prefix of `text` that fits, character by character. */\nfunction clampToWidth(text: string, fontSize: number, maxWidth: number): string {\n let width = 0\n let out = ''\n for (const character of text) {\n const next = textWidth(character, fontSize)\n if (width + next > maxWidth) break\n width += next\n out += character\n }\n return out.trimEnd()\n}\n\n/**\n * The type scale inside a figure, in user units.\n *\n * Fixed numbers rather than tokens, because these are SVG geometry: a `<text>`\n * sized in `rem` inside a scaled `viewBox` changes size when the figure is\n * zoomed, which means a diagram at 200% is a different diagram. The figure\n * scales as one picture, so its type scales with it.\n */\nexport const TYPE = {\n /** A node's name. */\n label: 13,\n /** The line under it. */\n sub: 10.5,\n /** A mono chip: a tag, a classification, a step number. */\n chip: 9.5,\n /** A relationship's own wording. */\n edge: 10.5,\n /** A band, lane or stage heading. */\n band: 10.5,\n} as const\n\n/** Default box sizes per diagram type, in user units. */\nexport const BOX = {\n node: { w: 168, h: 62 },\n wide: { w: 200, h: 62 },\n participant: { w: 128, h: 46 },\n} as const\n\n/**\n * How tall a box has to be to hold what it was given.\n *\n * A declared height is a FLOOR, not a ceiling. When a specification asks for a\n * 64-unit box and then puts two lines of label, a sublabel and a tag chip in\n * it, honouring the 64 prints the tag over the sublabel — which reads as a\n * rendering fault rather than as a box that was asked to hold too much. Growing\n * the box is visible and correct; clipping is invisible and wrong.\n */\nexport function nodeHeight(\n labelLines: number,\n hasSub: boolean,\n hasTag = false,\n base: number = BOX.node.h,\n): number {\n const content =\n 20 + labelLines * (TYPE.label * 1.25) + (hasSub ? TYPE.sub * 1.5 : 0) + (hasTag ? 20 : 0)\n return Math.max(base, Math.ceil(content))\n}\n\n/** `viewBox` as the attribute wants it. */\nexport function viewBoxOf(box: Box): string {\n return `${round(box.x)} ${round(box.y)} ${round(box.w)} ${round(box.h)}`\n}\n\n/**\n * Coordinates rounded to a tenth.\n *\n * SVG happily accepts eighteen decimal places and puts every one of them in the\n * markup, which on a figure with two hundred line segments is a measurable\n * share of the document. A tenth of a user unit is well under a device pixel at\n * any zoom a reader will use.\n */\nexport function round(value: number): number {\n return Math.round(value * 10) / 10\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NodeKind, LegendMode, LifecycleStateKind, Variant } from '../spec.js';
|
|
2
|
+
|
|
3
|
+
type LegendEntry = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
sample: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Which entries the key shows.
|
|
10
|
+
*
|
|
11
|
+
* `auto` — the default and the truthful one — lists only the kinds the figure
|
|
12
|
+
* actually drew. A key that names seven kinds beside a figure using three is
|
|
13
|
+
* not more informative; it is four claims the picture does not support.
|
|
14
|
+
*/
|
|
15
|
+
declare function resolveLegend<T extends string>(used: T[], mode?: LegendMode, all?: T[], overrides?: Record<string, {
|
|
16
|
+
label?: string;
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
}>): T[];
|
|
19
|
+
/** A key entry for each node kind, drawn with the same sigil the figure uses. */
|
|
20
|
+
declare function kindLegend(kinds: NodeKind[], overrides?: Record<string, {
|
|
21
|
+
label?: string;
|
|
22
|
+
}>): LegendEntry[];
|
|
23
|
+
/** A key entry for each relationship variant, drawn as the line itself. */
|
|
24
|
+
declare function variantLegend(variants: (Variant | 'return')[], overrides?: Record<string, {
|
|
25
|
+
label?: string;
|
|
26
|
+
}>): LegendEntry[];
|
|
27
|
+
/** A key entry for each lifecycle state kind, drawn as its plate. */
|
|
28
|
+
declare function stateLegend(kinds: LifecycleStateKind[], overrides?: Record<string, {
|
|
29
|
+
label?: string;
|
|
30
|
+
}>): LegendEntry[];
|
|
31
|
+
|
|
32
|
+
export { type LegendEntry, kindLegend, resolveLegend, stateLegend, variantLegend };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { KIND_WORD, Sigil } from "./marks.js";
|
|
3
|
+
const VARIANT_LABEL = {
|
|
4
|
+
default: "Call",
|
|
5
|
+
emphasis: "Primary path",
|
|
6
|
+
security: "Crosses a trust boundary",
|
|
7
|
+
dashed: "Asynchronous",
|
|
8
|
+
return: "Return"
|
|
9
|
+
};
|
|
10
|
+
const STATE_LABEL = {
|
|
11
|
+
start: "Entry",
|
|
12
|
+
active: "Working",
|
|
13
|
+
waiting: "Waiting",
|
|
14
|
+
decision: "Decision",
|
|
15
|
+
success: "Succeeded",
|
|
16
|
+
failure: "Failed",
|
|
17
|
+
neutral: "Idle",
|
|
18
|
+
external: "External"
|
|
19
|
+
};
|
|
20
|
+
function resolveLegend(used, mode = "auto", all = [], overrides) {
|
|
21
|
+
if (mode === "hidden") return [];
|
|
22
|
+
const base = mode === "all" ? all : [...new Set(used)];
|
|
23
|
+
return base.filter((key) => overrides?.[key]?.visible !== false);
|
|
24
|
+
}
|
|
25
|
+
function kindLegend(kinds, overrides) {
|
|
26
|
+
return kinds.map((kind) => ({
|
|
27
|
+
key: kind,
|
|
28
|
+
// The key names a kind with the SAME word the plate's eyebrow prints. They
|
|
29
|
+
// were two vocabularies for one fact — a plate reading SERVICE beside a key
|
|
30
|
+
// reading "Datastore" — which is the one thing a key must never do.
|
|
31
|
+
label: overrides?.[kind]?.label ?? KIND_WORD[kind],
|
|
32
|
+
sample: /* @__PURE__ */ jsx("g", { transform: "translate(1, 1)", children: /* @__PURE__ */ jsx(Sigil, { kind, x: 0, y: 0 }) })
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
function variantLegend(variants, overrides) {
|
|
36
|
+
return variants.map((variant) => ({
|
|
37
|
+
key: variant,
|
|
38
|
+
label: overrides?.[variant]?.label ?? VARIANT_LABEL[variant],
|
|
39
|
+
sample: /* @__PURE__ */ jsx(
|
|
40
|
+
"path",
|
|
41
|
+
{
|
|
42
|
+
d: "M 0.5 7 H 13.5",
|
|
43
|
+
fill: "none",
|
|
44
|
+
className: variant === "emphasis" ? "stroke-(--diagram-line-strong) [stroke-width:2.4]" : variant === "security" ? "stroke-(--diagram-line) [stroke-width:1.6] [stroke-dasharray:4_2]" : variant === "dashed" || variant === "return" ? "stroke-(--diagram-line-soft) [stroke-width:1.6] [stroke-dasharray:3_2.5]" : "stroke-(--diagram-line) [stroke-width:1.6]"
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
function stateLegend(kinds, overrides) {
|
|
50
|
+
return kinds.map((kind) => ({
|
|
51
|
+
key: kind,
|
|
52
|
+
label: overrides?.[kind]?.label ?? STATE_LABEL[kind],
|
|
53
|
+
sample: kind === "decision" ? /* @__PURE__ */ jsx(
|
|
54
|
+
"path",
|
|
55
|
+
{
|
|
56
|
+
d: "M 7 1 L 13 7 L 7 13 L 1 7 Z",
|
|
57
|
+
className: "fill-(--diagram-node) stroke-(--diagram-rule-hard) [stroke-width:1.2]"
|
|
58
|
+
}
|
|
59
|
+
) : kind === "external" ? /* @__PURE__ */ jsx(
|
|
60
|
+
"path",
|
|
61
|
+
{
|
|
62
|
+
d: "M 1 2 H 9.5 L 13 5.5 V 12 H 1 Z",
|
|
63
|
+
className: "fill-(--diagram-node-2) stroke-(--diagram-rule) [stroke-width:1.2]"
|
|
64
|
+
}
|
|
65
|
+
) : /* @__PURE__ */ jsx(
|
|
66
|
+
"rect",
|
|
67
|
+
{
|
|
68
|
+
x: "1",
|
|
69
|
+
y: "3",
|
|
70
|
+
width: "12",
|
|
71
|
+
height: "8",
|
|
72
|
+
rx: kind === "start" ? 4 : 2,
|
|
73
|
+
className: kind === "start" ? "fill-(--diagram-plate) stroke-(--diagram-plate) [stroke-width:1.2]" : kind === "success" ? "fill-(--success-wash) stroke-(--success) [stroke-width:1.2]" : kind === "failure" ? "fill-(--danger-wash) stroke-(--danger) [stroke-width:1.2]" : kind === "waiting" ? "fill-(--diagram-node) stroke-(--diagram-rule) [stroke-width:1.2] [stroke-dasharray:3_2]" : kind === "neutral" ? "fill-(--diagram-node-2) stroke-(--diagram-rule) [stroke-width:1.2]" : "fill-(--diagram-node) stroke-(--diagram-rule-hard) [stroke-width:1.2]"
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
kindLegend,
|
|
80
|
+
resolveLegend,
|
|
81
|
+
stateLegend,
|
|
82
|
+
variantLegend
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=legend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/lib/legend.tsx"],"sourcesContent":["import type { LegendMode, LifecycleStateKind, NodeKind, Variant } from '../spec'\nimport { KIND_WORD, Sigil } from './marks'\n\nconst VARIANT_LABEL: Record<Variant | 'return', string> = {\n default: 'Call',\n emphasis: 'Primary path',\n security: 'Crosses a trust boundary',\n dashed: 'Asynchronous',\n return: 'Return',\n}\n\nconst STATE_LABEL: Record<LifecycleStateKind, string> = {\n start: 'Entry',\n active: 'Working',\n waiting: 'Waiting',\n decision: 'Decision',\n success: 'Succeeded',\n failure: 'Failed',\n neutral: 'Idle',\n external: 'External',\n}\n\nexport type LegendEntry = { key: string; label: string; sample: React.ReactNode }\n\n/**\n * Which entries the key shows.\n *\n * `auto` — the default and the truthful one — lists only the kinds the figure\n * actually drew. A key that names seven kinds beside a figure using three is\n * not more informative; it is four claims the picture does not support.\n */\nexport function resolveLegend<T extends string>(\n used: T[],\n mode: LegendMode = 'auto',\n all: T[] = [],\n overrides?: Record<string, { label?: string; visible?: boolean }>,\n): T[] {\n if (mode === 'hidden') return []\n const base = mode === 'all' ? all : [...new Set(used)]\n return base.filter((key) => overrides?.[key]?.visible !== false)\n}\n\n/** A key entry for each node kind, drawn with the same sigil the figure uses. */\nexport function kindLegend(\n kinds: NodeKind[],\n overrides?: Record<string, { label?: string }>,\n): LegendEntry[] {\n return kinds.map((kind) => ({\n key: kind,\n // The key names a kind with the SAME word the plate's eyebrow prints. They\n // were two vocabularies for one fact — a plate reading SERVICE beside a key\n // reading \"Datastore\" — which is the one thing a key must never do.\n label: overrides?.[kind]?.label ?? KIND_WORD[kind],\n sample: (\n <g transform=\"translate(1, 1)\">\n <Sigil kind={kind} x={0} y={0} />\n </g>\n ),\n }))\n}\n\n/** A key entry for each relationship variant, drawn as the line itself. */\nexport function variantLegend(\n variants: (Variant | 'return')[],\n overrides?: Record<string, { label?: string }>,\n): LegendEntry[] {\n return variants.map((variant) => ({\n key: variant,\n label: overrides?.[variant]?.label ?? VARIANT_LABEL[variant],\n sample: (\n <path\n d=\"M 0.5 7 H 13.5\"\n fill=\"none\"\n className={\n variant === 'emphasis'\n ? 'stroke-(--diagram-line-strong) [stroke-width:2.4]'\n : variant === 'security'\n ? 'stroke-(--diagram-line) [stroke-width:1.6] [stroke-dasharray:4_2]'\n : variant === 'dashed' || variant === 'return'\n ? 'stroke-(--diagram-line-soft) [stroke-width:1.6] [stroke-dasharray:3_2.5]'\n : 'stroke-(--diagram-line) [stroke-width:1.6]'\n }\n />\n ),\n }))\n}\n\n/** A key entry for each lifecycle state kind, drawn as its plate. */\nexport function stateLegend(\n kinds: LifecycleStateKind[],\n overrides?: Record<string, { label?: string }>,\n): LegendEntry[] {\n return kinds.map((kind) => ({\n key: kind,\n label: overrides?.[kind]?.label ?? STATE_LABEL[kind],\n sample:\n kind === 'decision' ? (\n <path\n d=\"M 7 1 L 13 7 L 7 13 L 1 7 Z\"\n className=\"fill-(--diagram-node) stroke-(--diagram-rule-hard) [stroke-width:1.2]\"\n />\n ) : kind === 'external' ? (\n <path\n d=\"M 1 2 H 9.5 L 13 5.5 V 12 H 1 Z\"\n className=\"fill-(--diagram-node-2) stroke-(--diagram-rule) [stroke-width:1.2]\"\n />\n ) : (\n <rect\n x=\"1\"\n y=\"3\"\n width=\"12\"\n height=\"8\"\n rx={kind === 'start' ? 4 : 2}\n className={\n kind === 'start'\n ? 'fill-(--diagram-plate) stroke-(--diagram-plate) [stroke-width:1.2]'\n : kind === 'success'\n ? 'fill-(--success-wash) stroke-(--success) [stroke-width:1.2]'\n : kind === 'failure'\n ? 'fill-(--danger-wash) stroke-(--danger) [stroke-width:1.2]'\n : kind === 'waiting'\n ? 'fill-(--diagram-node) stroke-(--diagram-rule) [stroke-width:1.2] [stroke-dasharray:3_2]'\n : kind === 'neutral'\n ? 'fill-(--diagram-node-2) stroke-(--diagram-rule) [stroke-width:1.2]'\n : 'fill-(--diagram-node) stroke-(--diagram-rule-hard) [stroke-width:1.2]'\n }\n />\n ),\n }))\n}\n"],"mappings":"AAuDQ;AAtDR,SAAS,WAAW,aAAa;AAEjC,MAAM,gBAAoD;AAAA,EACxD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AACV;AAEA,MAAM,cAAkD;AAAA,EACtD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AACZ;AAWO,SAAS,cACd,MACA,OAAmB,QACnB,MAAW,CAAC,GACZ,WACK;AACL,MAAI,SAAS,SAAU,QAAO,CAAC;AAC/B,QAAM,OAAO,SAAS,QAAQ,MAAM,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AACrD,SAAO,KAAK,OAAO,CAAC,QAAQ,YAAY,GAAG,GAAG,YAAY,KAAK;AACjE;AAGO,SAAS,WACd,OACA,WACe;AACf,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,KAAK;AAAA;AAAA;AAAA;AAAA,IAIL,OAAO,YAAY,IAAI,GAAG,SAAS,UAAU,IAAI;AAAA,IACjD,QACE,oBAAC,OAAE,WAAU,mBACX,8BAAC,SAAM,MAAY,GAAG,GAAG,GAAG,GAAG,GACjC;AAAA,EAEJ,EAAE;AACJ;AAGO,SAAS,cACd,UACA,WACe;AACf,SAAO,SAAS,IAAI,CAAC,aAAa;AAAA,IAChC,KAAK;AAAA,IACL,OAAO,YAAY,OAAO,GAAG,SAAS,cAAc,OAAO;AAAA,IAC3D,QACE;AAAA,MAAC;AAAA;AAAA,QACC,GAAE;AAAA,QACF,MAAK;AAAA,QACL,WACE,YAAY,aACR,sDACA,YAAY,aACV,sEACA,YAAY,YAAY,YAAY,WAClC,6EACA;AAAA;AAAA,IAEZ;AAAA,EAEJ,EAAE;AACJ;AAGO,SAAS,YACd,OACA,WACe;AACf,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,KAAK;AAAA,IACL,OAAO,YAAY,IAAI,GAAG,SAAS,YAAY,IAAI;AAAA,IACnD,QACE,SAAS,aACP;AAAA,MAAC;AAAA;AAAA,QACC,GAAE;AAAA,QACF,WAAU;AAAA;AAAA,IACZ,IACE,SAAS,aACX;AAAA,MAAC;AAAA;AAAA,QACC,GAAE;AAAA,QACF,WAAU;AAAA;AAAA,IACZ,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,GAAE;AAAA,QACF,GAAE;AAAA,QACF,OAAM;AAAA,QACN,QAAO;AAAA,QACP,IAAI,SAAS,UAAU,IAAI;AAAA,QAC3B,WACE,SAAS,UACL,uEACA,SAAS,YACP,gEACA,SAAS,YACP,8DACA,SAAS,YACP,4FACA,SAAS,YACP,uEACA;AAAA;AAAA,IAEhB;AAAA,EAEN,EAAE;AACJ;","names":[]}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { NodeKind, Variant, LifecycleStateKind } from '../spec.js';
|
|
4
|
+
import { Box } from './geometry.js';
|
|
5
|
+
|
|
6
|
+
/** What each kind is called on a plate's eyebrow. Short, because it is a kicker. */
|
|
7
|
+
declare const KIND_WORD: Record<NodeKind, string>;
|
|
8
|
+
/**
|
|
9
|
+
* The marker definitions every figure needs.
|
|
10
|
+
*
|
|
11
|
+
* Rendered once per figure. The ids are namespaced by the figure's own id
|
|
12
|
+
* because two diagrams on one page would otherwise share one `<defs>`, and the
|
|
13
|
+
* second figure's arrowheads would silently take the first figure's paint —
|
|
14
|
+
* SVG ids are document-global, and a duplicate is resolved to whichever came
|
|
15
|
+
* first rather than to the nearest one.
|
|
16
|
+
*/
|
|
17
|
+
declare function DiagramDefs({ uid }: {
|
|
18
|
+
uid: string;
|
|
19
|
+
}): react.JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* The 14×14 mark that says what kind of thing a node is.
|
|
22
|
+
*
|
|
23
|
+
* Drawn on a 14-unit grid at a 1.35 stroke. Both numbers went up from the first
|
|
24
|
+
* pass and both had to: at 12 units and a 1.2 stroke the seven forms were a few
|
|
25
|
+
* grey pixels apart at reading size, which is not a distinction — it is a
|
|
26
|
+
* smudge. Every path is closed geometry rather than an icon font, because a
|
|
27
|
+
* figure has to serialise into a standalone file and a font the exported
|
|
28
|
+
* document cannot fetch renders as a box.
|
|
29
|
+
*/
|
|
30
|
+
declare function Sigil({ kind, x, y, reversed, }: {
|
|
31
|
+
kind: NodeKind;
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
reversed?: boolean;
|
|
35
|
+
}): react.JSX.Element;
|
|
36
|
+
/**
|
|
37
|
+
* A plate's internal vertical rhythm, in user units from its top edge.
|
|
38
|
+
*
|
|
39
|
+
* Written out as one object rather than computed inline in three renderers,
|
|
40
|
+
* because these numbers ARE the plate: change one and the card stops looking
|
|
41
|
+
* like a card. The eyebrow sits high and tight, the name is the only thing set
|
|
42
|
+
* at reading size, and the sublabel hangs under it in the meta voice.
|
|
43
|
+
*/
|
|
44
|
+
declare const PLATE: {
|
|
45
|
+
readonly padX: 15;
|
|
46
|
+
readonly eyebrow: 19;
|
|
47
|
+
readonly label: 39;
|
|
48
|
+
readonly sub: 53;
|
|
49
|
+
readonly lineStep: 17;
|
|
50
|
+
/** Where the eyebrow lands when there is none — the label moves up into it. */
|
|
51
|
+
readonly riseWithoutEyebrow: 16;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* How tall a plate has to be to hold what it was given.
|
|
55
|
+
*
|
|
56
|
+
* A declared height is a FLOOR, not a ceiling. When a specification asks for a
|
|
57
|
+
* 60-unit box and then puts two lines of name and a qualifier in it, honouring
|
|
58
|
+
* the 60 prints the qualifier through the bottom rule — which reads as a
|
|
59
|
+
* rendering fault rather than as a box asked to hold too much. Growing is
|
|
60
|
+
* visible and correct; clipping is invisible and wrong.
|
|
61
|
+
*/
|
|
62
|
+
declare function plateHeight(labelLines: number, hasSub: boolean, hasEyebrow: boolean, floor?: number): number;
|
|
63
|
+
interface NodePlateProps {
|
|
64
|
+
box: Box;
|
|
65
|
+
label: string;
|
|
66
|
+
sublabel?: string;
|
|
67
|
+
tag?: string;
|
|
68
|
+
kind?: NodeKind;
|
|
69
|
+
variant?: Variant;
|
|
70
|
+
/** Printed in the trailing corner in mono — a step number. */
|
|
71
|
+
step?: string;
|
|
72
|
+
/** Paints the plate on the recessed ground rather than on paper. */
|
|
73
|
+
muted?: boolean;
|
|
74
|
+
/** Lit by a guided chapter or a reader's focus. */
|
|
75
|
+
active?: boolean;
|
|
76
|
+
/** Dimmed because something else is lit. */
|
|
77
|
+
dimmed?: boolean;
|
|
78
|
+
onSelect?: () => void;
|
|
79
|
+
/** A stable hook the viewer chrome and the tests both address nodes by. */
|
|
80
|
+
nodeId?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One box, with everything that can be printed inside it.
|
|
84
|
+
*
|
|
85
|
+
* Three registers, top to bottom, and each is a different voice on purpose: a
|
|
86
|
+
* mono kicker naming the KIND, the name at reading size in the interface face,
|
|
87
|
+
* and the qualifier back in mono. That is the same ladder a record on a page
|
|
88
|
+
* uses — the diagram is not inventing a second typography for itself.
|
|
89
|
+
*
|
|
90
|
+
* WHAT IT IS NOT is a button, and that is deliberate. The `<svg>` around it
|
|
91
|
+
* carries `role="img"`, which makes everything inside it presentational to
|
|
92
|
+
* assistive technology — so a `role="button"` here would announce a control a
|
|
93
|
+
* screen reader can never reach. The keyboard route to the same selection is
|
|
94
|
+
* the figure's own summary list, which is a real `<ul>` of real `<button>`s
|
|
95
|
+
* sitting beside the picture. `onSelect` here is the POINTER path to that same
|
|
96
|
+
* action, and nothing else.
|
|
97
|
+
*/
|
|
98
|
+
declare function NodePlate({ box, label, sublabel, tag, kind, variant, step, muted, active, dimmed, onSelect, nodeId, }: NodePlateProps): react.JSX.Element;
|
|
99
|
+
/**
|
|
100
|
+
* A mono chip: a version, a region, a protocol, a data classification.
|
|
101
|
+
*
|
|
102
|
+
* A rule and a word, with no fill under it. The filled chip this replaced put a
|
|
103
|
+
* third ground inside a plate that already sat on a figure that sat on the
|
|
104
|
+
* page, and three grounds deep is where a diagram stops reading as structure
|
|
105
|
+
* and starts reading as packaging.
|
|
106
|
+
*/
|
|
107
|
+
declare function Chip({ x, y, text, align, reversed, }: {
|
|
108
|
+
x: number;
|
|
109
|
+
y: number;
|
|
110
|
+
text: string;
|
|
111
|
+
align?: 'start' | 'middle' | 'end';
|
|
112
|
+
reversed?: boolean;
|
|
113
|
+
}): react.JSX.Element;
|
|
114
|
+
/**
|
|
115
|
+
* How a line is painted: the colour as a class, the geometry as attributes.
|
|
116
|
+
*
|
|
117
|
+
* THE SPLIT IS NOT ARBITRARY, and getting it wrong cost an afternoon. Tailwind
|
|
118
|
+
* generates a rule only for a class name it can find as a LITERAL in the
|
|
119
|
+
* source. A width built at runtime — `` `[stroke-width:${weight}]` `` — is a
|
|
120
|
+
* class that never gets a rule, so the browser falls back to SVG's default
|
|
121
|
+
* 1px, silently, on exactly the lines a figure most needs to be heavier. The
|
|
122
|
+
* main path came out the same weight as a side branch and nothing said so.
|
|
123
|
+
*
|
|
124
|
+
* Colour still goes through a class, because a paint has to resolve a custom
|
|
125
|
+
* property and `stroke="var(--x)"` is not valid as an attribute. Width and dash
|
|
126
|
+
* pattern are plain numbers, so they go through presentation attributes where
|
|
127
|
+
* no build step has to have predicted them — and `getComputedStyle` reports an
|
|
128
|
+
* attribute exactly as it reports a rule, so the export still picks them up.
|
|
129
|
+
*/
|
|
130
|
+
declare function edgeClasses(variant?: Variant | 'return'): string;
|
|
131
|
+
/** A line's width and dash pattern, as presentation attributes. */
|
|
132
|
+
declare function edgeStroke(variant?: Variant | 'return', weight?: number): {
|
|
133
|
+
strokeWidth: number;
|
|
134
|
+
strokeDasharray?: string;
|
|
135
|
+
};
|
|
136
|
+
/** Which arrowhead a line ends in. */
|
|
137
|
+
declare function edgeMarker(variant?: Variant | 'return'): 'arrow' | 'arrow-strong' | 'arrow-open';
|
|
138
|
+
interface EdgeLabelProps {
|
|
139
|
+
x: number;
|
|
140
|
+
y: number;
|
|
141
|
+
axis: 'x' | 'y';
|
|
142
|
+
text: string;
|
|
143
|
+
/** A mono chip printed under the wording — a data classification. */
|
|
144
|
+
chip?: string;
|
|
145
|
+
/** A second, quieter line — a condition, a timeout, a retry count. */
|
|
146
|
+
note?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A relationship's wording, on a mask that punches the line out from under it.
|
|
150
|
+
*
|
|
151
|
+
* The mask is not decoration. A label drawn straight onto a line is unreadable
|
|
152
|
+
* at every size, and the two repairs a renderer can make are moving the label
|
|
153
|
+
* or hiding the line behind it. Moving it changes which segment the wording
|
|
154
|
+
* belongs to, which is a semantic change; masking is the one that leaves the
|
|
155
|
+
* meaning alone.
|
|
156
|
+
*
|
|
157
|
+
* Set in mono rather than in the interface face. The wording on a line is an
|
|
158
|
+
* ANNOTATION on the picture, not a second rank of content — and at the sans
|
|
159
|
+
* face it competed with the names on the plates, which is the one comparison it
|
|
160
|
+
* must lose.
|
|
161
|
+
*/
|
|
162
|
+
declare function EdgeLabel({ x, y, axis, text, chip, note }: EdgeLabelProps): react.JSX.Element;
|
|
163
|
+
/**
|
|
164
|
+
* A labelled frame around a set of boxes: a region, a security group, a phase,
|
|
165
|
+
* a process group.
|
|
166
|
+
*
|
|
167
|
+
* A FRAME IS A RULE AND NOT A GROUND. Archify washes its regions with a tint,
|
|
168
|
+
* which this system cannot copy for two reasons: a second ground inside a paper
|
|
169
|
+
* figure reads as packaging rather than as structure — the same argument the
|
|
170
|
+
* containment `Diagram` primitive makes about nested boxes — and every
|
|
171
|
+
* relationship label crossing the wash would then need to know which of two
|
|
172
|
+
* grounds its mask is punching through, which is a question a label cannot
|
|
173
|
+
* answer. `muted` therefore washes only where a band IS the ground it labels: a
|
|
174
|
+
* workflow's exception lane, which nothing routes across.
|
|
175
|
+
*
|
|
176
|
+
* The label sits ABOVE the top rule rather than inside the frame or on it.
|
|
177
|
+
* Inside, it costs a line of vertical space in every frame whether or not the
|
|
178
|
+
* frame had one to spare. ON the rule — which is where it started — it has to
|
|
179
|
+
* punch a mask through the frame, and that mask then competes for the same few
|
|
180
|
+
* units of clear ground as any relationship label routed along the frame's own
|
|
181
|
+
* edge: two captions, two white plates, one on top of the other. Above the
|
|
182
|
+
* rule it needs no mask, leaves the frame unbroken, and reads as a caption for
|
|
183
|
+
* the frame rather than as its first child.
|
|
184
|
+
*/
|
|
185
|
+
declare function BandFrame({ box, label, dashed, muted, }: {
|
|
186
|
+
box: Box;
|
|
187
|
+
label?: string;
|
|
188
|
+
dashed?: boolean;
|
|
189
|
+
muted?: boolean;
|
|
190
|
+
}): react.JSX.Element;
|
|
191
|
+
/**
|
|
192
|
+
* The plate shape for a lifecycle state.
|
|
193
|
+
*
|
|
194
|
+
* This is the one place the system spends colour, and it spends exactly the two
|
|
195
|
+
* tokens it reserves for state: `--success` on a terminal success, `--danger`
|
|
196
|
+
* on a terminal failure. Every other kind is carried by shape — a filled cap
|
|
197
|
+
* for a start, a diamond for a decision, a dashed frame for a wait, a cut
|
|
198
|
+
* corner for something outside the system — so a greyscale print loses the two
|
|
199
|
+
* outcomes' hue and keeps all six other distinctions.
|
|
200
|
+
*/
|
|
201
|
+
declare function StatePlate({ box, kind, children, }: {
|
|
202
|
+
box: Box;
|
|
203
|
+
kind: LifecycleStateKind;
|
|
204
|
+
children?: ReactNode;
|
|
205
|
+
}): react.JSX.Element;
|
|
206
|
+
|
|
207
|
+
export { BandFrame, Chip, DiagramDefs, EdgeLabel, type EdgeLabelProps, KIND_WORD, NodePlate, type NodePlateProps, PLATE, Sigil, StatePlate, edgeClasses, edgeMarker, edgeStroke, plateHeight };
|