@joewinke/jatui 0.37.0 → 0.38.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/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/package.json +11 -2
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram block — params schema, validation and layout (mos-d70v4).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only (no DOM, no canvas): box sizing comes from a
|
|
5
|
+
* deterministic char-count estimate so SSR and the client agree without a
|
|
6
|
+
* measurement pass, and colors NEVER appear here — the renderer maps tones
|
|
7
|
+
* to theme-token classes. dagre is NOT imported at module level: the
|
|
8
|
+
* renderer resolves it (resolveDagre) and passes the engine in, so this
|
|
9
|
+
* file stays testable without the dependency and the build doesn't hard-
|
|
10
|
+
* wire a transitive package. The layered fallback (layoutLayered) keeps
|
|
11
|
+
* publishing alive even if dagre is unavailable.
|
|
12
|
+
*
|
|
13
|
+
* Untrusted-input rules (CONTRACT.md): text params are length-capped
|
|
14
|
+
* strings, arrays are capped (50 nodes / 80 edges), ids are validated.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { toneSchema } from "../../params";
|
|
18
|
+
import { MAX_EDGES, MAX_NODES, NODE_TEXT_MAX, EDGE_TEXT_MAX, DEFAULT_RANKSEP, DEFAULT_NODESEP, DEFAULT_EDGESEP, MIN_RANKSEP, MIN_NODESEP, NODE_PAD_X, NODE_PAD_Y, NODE_LINE_H, NODE_CHAR_W, SUB_CHAR_W, NODE_MAX_LINES, NODE_MIN_W, NODE_MAX_W, NODE_MIN_H, DIAMOND_TEXT_FACTOR, CIRCLE_MIN, SEQ_ACTOR_H, SEQ_ACTOR_PAD_X, SEQ_ACTOR_MIN_W, SEQ_ACTOR_MAX_W, SEQ_MSG_GAP_DEFAULT, SEQ_MSG_GAP_MIN, SEQ_ROW_PAD, SEQ_PAD_X, SEQ_COL_MIN, SEQ_LABEL_CHAR_W, SEQ_WIDTH_DEFAULT, SEQ_WIDTH_MIN, SEQ_WIDTH_MAX, EDGE_LABEL_W, EDGE_LABEL_H, VIEW_PAD, } from "./constants";
|
|
19
|
+
// ── Schemas ───────────────────────────────────────────────────────────────
|
|
20
|
+
/** ids accept the block-id charset; duplicates are rejected in the refine. */
|
|
21
|
+
const diagramIdSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(64)
|
|
25
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/);
|
|
26
|
+
const nodeShapeValues = [
|
|
27
|
+
"rect",
|
|
28
|
+
"pill",
|
|
29
|
+
"diamond",
|
|
30
|
+
"circle",
|
|
31
|
+
"stadium",
|
|
32
|
+
];
|
|
33
|
+
const nodeKindValues = [
|
|
34
|
+
"default",
|
|
35
|
+
"input",
|
|
36
|
+
"output",
|
|
37
|
+
"decision",
|
|
38
|
+
"terminal",
|
|
39
|
+
"process",
|
|
40
|
+
"data",
|
|
41
|
+
"subroutine",
|
|
42
|
+
"external",
|
|
43
|
+
"note",
|
|
44
|
+
];
|
|
45
|
+
const diagramNodeSchema = z.object({
|
|
46
|
+
id: diagramIdSchema,
|
|
47
|
+
label: z.string().min(1).max(NODE_TEXT_MAX),
|
|
48
|
+
sublabel: z.string().max(NODE_TEXT_MAX).optional(),
|
|
49
|
+
kind: z.enum(nodeKindValues).optional(),
|
|
50
|
+
tone: toneSchema.optional(),
|
|
51
|
+
shape: z.enum(nodeShapeValues).optional(),
|
|
52
|
+
});
|
|
53
|
+
const diagramEdgeSchema = z.object({
|
|
54
|
+
from: diagramIdSchema,
|
|
55
|
+
to: diagramIdSchema,
|
|
56
|
+
label: z.string().max(EDGE_TEXT_MAX).optional(),
|
|
57
|
+
dashed: z.boolean().optional(),
|
|
58
|
+
tone: toneSchema.optional(),
|
|
59
|
+
});
|
|
60
|
+
export const diagramParamsSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
mode: z.enum(["flow", "sequence"]).default("flow"),
|
|
63
|
+
title: z.string().max(200).optional(),
|
|
64
|
+
subtitle: z.string().max(300).optional(),
|
|
65
|
+
direction: z.enum(["LR", "RL", "TB", "BT"]).default("LR"),
|
|
66
|
+
nodes: z.array(diagramNodeSchema).min(1).max(MAX_NODES),
|
|
67
|
+
edges: z.array(diagramEdgeSchema).max(MAX_EDGES).default([]),
|
|
68
|
+
edgeLabels: z.boolean().default(true),
|
|
69
|
+
ranksep: z.number().min(MIN_RANKSEP).max(400).optional(),
|
|
70
|
+
nodesep: z.number().min(MIN_NODESEP).max(300).optional(),
|
|
71
|
+
edgesep: z.number().min(4).max(200).optional(),
|
|
72
|
+
sequenceWidth: z
|
|
73
|
+
.number()
|
|
74
|
+
.int()
|
|
75
|
+
.min(SEQ_WIDTH_MIN)
|
|
76
|
+
.max(SEQ_WIDTH_MAX)
|
|
77
|
+
.optional(),
|
|
78
|
+
msgGap: z.number().min(SEQ_MSG_GAP_MIN).max(200).optional(),
|
|
79
|
+
})
|
|
80
|
+
.superRefine((p, ctx) => {
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
for (const [i, n] of p.nodes.entries()) {
|
|
83
|
+
if (ids.has(n.id)) {
|
|
84
|
+
ctx.addIssue({
|
|
85
|
+
code: "custom",
|
|
86
|
+
message: `Duplicate node id "${n.id}" (node ${i})`,
|
|
87
|
+
path: ["nodes", i, "id"],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
ids.add(n.id);
|
|
91
|
+
}
|
|
92
|
+
p.edges.forEach((e, i) => {
|
|
93
|
+
for (const key of ["from", "to"]) {
|
|
94
|
+
if (!ids.has(e[key])) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: "custom",
|
|
97
|
+
message: `Edge ${i} ${key} "${e[key]}" has no node`,
|
|
98
|
+
path: ["edges", i, key],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (e.from === e.to) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: "custom",
|
|
105
|
+
message: `Edge ${i} is a self-loop (from === to)`,
|
|
106
|
+
path: ["edges", i],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// flow only: duplicate (from,to) pairs render as one line — refuse
|
|
110
|
+
// them. In sequence mode repeated actor pairs are the NORM (each is a
|
|
111
|
+
// row), so the check is mode-gated.
|
|
112
|
+
if (p.mode === "flow") {
|
|
113
|
+
for (let j = 0; j < i; j++) {
|
|
114
|
+
const other = p.edges[j];
|
|
115
|
+
if (other.from === e.from && other.to === e.to) {
|
|
116
|
+
ctx.addIssue({
|
|
117
|
+
code: "custom",
|
|
118
|
+
message: `Edge ${i} duplicates edge ${j} (${e.from} → ${e.to})`,
|
|
119
|
+
path: ["edges", i],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (p.mode === "sequence") {
|
|
126
|
+
if (p.nodes.length < 2) {
|
|
127
|
+
ctx.addIssue({
|
|
128
|
+
code: "custom",
|
|
129
|
+
message: "Sequence needs at least 2 actors (nodes)",
|
|
130
|
+
path: ["nodes"],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const labels = new Set();
|
|
134
|
+
for (const [i, n] of p.nodes.entries()) {
|
|
135
|
+
if (labels.has(n.label)) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: `Duplicate actor label "${n.label}" (node ${i})`,
|
|
139
|
+
path: ["nodes", i, "label"],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
labels.add(n.label);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* Pull the dagre engine out of whatever the renderer imported. Handles both
|
|
148
|
+
* the ESM default export (`{ graphlib, layout }` — what @dagrejs/dagre
|
|
149
|
+
* actually ships) and a namespace import. Returns null when dagre isn't
|
|
150
|
+
* there — the caller falls back to the layered layout.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDagre(mod) {
|
|
153
|
+
const d = mod?.default ?? mod;
|
|
154
|
+
if (!d || typeof d !== "object")
|
|
155
|
+
return null;
|
|
156
|
+
const eng = d;
|
|
157
|
+
if (!eng.graphlib ||
|
|
158
|
+
typeof eng.graphlib.Graph !== "function" ||
|
|
159
|
+
typeof eng.layout !== "function")
|
|
160
|
+
return null;
|
|
161
|
+
return d;
|
|
162
|
+
}
|
|
163
|
+
// ── Deterministic text estimate (no canvas — SSR == client) ───────────────
|
|
164
|
+
/** Char-count text width estimate. Deliberately NOT a canvas measure: the
|
|
165
|
+
* layout must be byte-identical between SSR and hydration. */
|
|
166
|
+
export function textWidth(text, charW) {
|
|
167
|
+
return text.length * charW;
|
|
168
|
+
}
|
|
169
|
+
/** Greedy word wrap on the estimate. Long single words hard-split. */
|
|
170
|
+
function wrapText(text, maxWidth, charW, maxLines) {
|
|
171
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
172
|
+
const lines = [];
|
|
173
|
+
let cur = "";
|
|
174
|
+
for (const w of words) {
|
|
175
|
+
const candidate = cur ? `${cur} ${w}` : w;
|
|
176
|
+
if (textWidth(candidate, charW) <= maxWidth || !cur) {
|
|
177
|
+
cur = candidate;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
lines.push(cur);
|
|
181
|
+
cur = w;
|
|
182
|
+
if (lines.length >= maxLines)
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (cur && lines.length < maxLines)
|
|
187
|
+
lines.push(cur);
|
|
188
|
+
if (lines.length === maxLines && words.length > 0) {
|
|
189
|
+
// overflow: hard-truncate the last line
|
|
190
|
+
const rest = words.slice(lines.join(" ").split(" ").length).join(" ");
|
|
191
|
+
if (rest) {
|
|
192
|
+
let last = rest;
|
|
193
|
+
while (textWidth(last, charW) > maxWidth && last.length > 1)
|
|
194
|
+
last = last.slice(0, -1);
|
|
195
|
+
lines[maxLines - 1] = `${last}…`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lines.length ? lines : [text];
|
|
199
|
+
}
|
|
200
|
+
/** Default shape for a node kind (explicit `shape` wins). */
|
|
201
|
+
export function kindShape(kind) {
|
|
202
|
+
switch (kind) {
|
|
203
|
+
case "decision":
|
|
204
|
+
return "diamond";
|
|
205
|
+
case "terminal":
|
|
206
|
+
return "stadium";
|
|
207
|
+
case "input":
|
|
208
|
+
case "output":
|
|
209
|
+
return "pill";
|
|
210
|
+
case "external":
|
|
211
|
+
case "note":
|
|
212
|
+
return "rect";
|
|
213
|
+
default:
|
|
214
|
+
return "rect";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Dashed border for note/external kinds unless the shape is a diamond. */
|
|
218
|
+
export function kindDashed(kind) {
|
|
219
|
+
return kind === "note" || kind === "external";
|
|
220
|
+
}
|
|
221
|
+
/** Box size for a node from the deterministic estimate. */
|
|
222
|
+
function nodeSizeFor(node) {
|
|
223
|
+
const shape = node.shape ?? kindShape(node.kind);
|
|
224
|
+
if (shape === "diamond") {
|
|
225
|
+
const lines = wrapText(node.label, NODE_MAX_W * DIAMOND_TEXT_FACTOR, NODE_CHAR_W, 2);
|
|
226
|
+
const widest = Math.max(...lines.map((l) => textWidth(l, NODE_CHAR_W)));
|
|
227
|
+
const w = Math.min(Math.max(widest / DIAMOND_TEXT_FACTOR + 32, 130), 340);
|
|
228
|
+
const h = Math.min(Math.max(lines.length * NODE_LINE_H * 2 + 26, 76), 180);
|
|
229
|
+
return { w, h };
|
|
230
|
+
}
|
|
231
|
+
if (shape === "circle") {
|
|
232
|
+
const lines = wrapText(node.label, 120, NODE_CHAR_W, 2);
|
|
233
|
+
const widest = Math.max(...lines.map((l) => textWidth(l, NODE_CHAR_W)));
|
|
234
|
+
const side = Math.min(Math.max(Math.max(widest, 16) + 40, CIRCLE_MIN), 170);
|
|
235
|
+
return { w: side, h: side };
|
|
236
|
+
}
|
|
237
|
+
const inner = NODE_MAX_W - NODE_PAD_X * 2;
|
|
238
|
+
const labelLines = wrapText(node.label, inner, NODE_CHAR_W, NODE_MAX_LINES);
|
|
239
|
+
const widestLabel = Math.max(...labelLines.map((l) => textWidth(l, NODE_CHAR_W)));
|
|
240
|
+
const subLines = node.sublabel
|
|
241
|
+
? wrapText(node.sublabel, inner, SUB_CHAR_W, 2)
|
|
242
|
+
: [];
|
|
243
|
+
const widestSub = subLines.length
|
|
244
|
+
? Math.max(...subLines.map((l) => textWidth(l, SUB_CHAR_W)))
|
|
245
|
+
: 0;
|
|
246
|
+
const w = Math.min(Math.max(Math.max(widestLabel, widestSub) + NODE_PAD_X * 2, NODE_MIN_W), NODE_MAX_W);
|
|
247
|
+
const h = Math.max(labelLines.length * NODE_LINE_H +
|
|
248
|
+
subLines.length * (NODE_LINE_H - 4) +
|
|
249
|
+
NODE_PAD_Y * 2 +
|
|
250
|
+
(subLines.length ? 4 : 0), NODE_MIN_H);
|
|
251
|
+
return { w, h };
|
|
252
|
+
}
|
|
253
|
+
/** Text lines the renderer paints for a node (same budgets as nodeSizeFor
|
|
254
|
+
* so the box always fits its text). */
|
|
255
|
+
export function nodeTextLines(node, size) {
|
|
256
|
+
const shape = node.shape ?? kindShape(node.kind);
|
|
257
|
+
if (shape === "diamond")
|
|
258
|
+
return {
|
|
259
|
+
label: wrapText(node.label, size.w * DIAMOND_TEXT_FACTOR - 12, NODE_CHAR_W, 2),
|
|
260
|
+
sub: [],
|
|
261
|
+
};
|
|
262
|
+
if (shape === "circle")
|
|
263
|
+
return { label: wrapText(node.label, size.w - 26, NODE_CHAR_W, 2), sub: [] };
|
|
264
|
+
const inner = size.w - NODE_PAD_X * 2;
|
|
265
|
+
return {
|
|
266
|
+
label: wrapText(node.label, inner, NODE_CHAR_W, NODE_MAX_LINES),
|
|
267
|
+
sub: node.sublabel ? wrapText(node.sublabel, inner, SUB_CHAR_W, 2) : [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ── Flow layout (dagre, with layered fallback) ────────────────────────────
|
|
271
|
+
function padLayout(boxes, edges) {
|
|
272
|
+
const minX = Math.min(VIEW_PAD, ...boxes.map((b) => b.x), ...edges.flatMap((e) => e.points.map((p) => p.x)));
|
|
273
|
+
const minY = Math.min(VIEW_PAD, ...boxes.map((b) => b.y), ...edges.flatMap((e) => e.points.map((p) => p.y)));
|
|
274
|
+
const dx = VIEW_PAD - minX;
|
|
275
|
+
const dy = VIEW_PAD - minY;
|
|
276
|
+
for (const b of boxes) {
|
|
277
|
+
b.x += dx;
|
|
278
|
+
b.y += dy;
|
|
279
|
+
}
|
|
280
|
+
for (const e of edges) {
|
|
281
|
+
for (const pt of e.points) {
|
|
282
|
+
pt.x += dx;
|
|
283
|
+
pt.y += dy;
|
|
284
|
+
}
|
|
285
|
+
if (e.labelPoint) {
|
|
286
|
+
e.labelPoint.x += dx;
|
|
287
|
+
e.labelPoint.y += dy;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const width = Math.max(VIEW_PAD, ...boxes.map((b) => b.x + b.w), ...edges.flatMap((e) => e.points.map((p) => p.x))) + VIEW_PAD;
|
|
291
|
+
const height = Math.max(VIEW_PAD, ...boxes.map((b) => b.y + b.h), ...edges.flatMap((e) => e.points.map((p) => p.y))) + VIEW_PAD;
|
|
292
|
+
return { boxes, edges, width, height, engine: "dagre" };
|
|
293
|
+
}
|
|
294
|
+
function layoutDagre(p, engine) {
|
|
295
|
+
const g = new engine.graphlib.Graph();
|
|
296
|
+
g.setGraph({
|
|
297
|
+
rankdir: p.direction,
|
|
298
|
+
ranksep: p.ranksep ?? DEFAULT_RANKSEP,
|
|
299
|
+
nodesep: p.nodesep ?? DEFAULT_NODESEP,
|
|
300
|
+
edgesep: p.edgesep ?? DEFAULT_EDGESEP,
|
|
301
|
+
marginx: 0,
|
|
302
|
+
marginy: 0,
|
|
303
|
+
});
|
|
304
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
305
|
+
for (const n of p.nodes) {
|
|
306
|
+
const { w, h } = nodeSizeFor(n);
|
|
307
|
+
g.setNode(n.id, { width: w, height: h });
|
|
308
|
+
}
|
|
309
|
+
for (const e of p.edges) {
|
|
310
|
+
g.setEdge(e.from, e.to, p.edgeLabels && e.label
|
|
311
|
+
? {
|
|
312
|
+
label: e.label,
|
|
313
|
+
width: EDGE_LABEL_W,
|
|
314
|
+
height: EDGE_LABEL_H,
|
|
315
|
+
labelpos: "c",
|
|
316
|
+
}
|
|
317
|
+
: {});
|
|
318
|
+
}
|
|
319
|
+
engine.layout(g);
|
|
320
|
+
const byId = new Map();
|
|
321
|
+
for (const n of p.nodes) {
|
|
322
|
+
const meta = g.node(n.id);
|
|
323
|
+
if (!meta ||
|
|
324
|
+
typeof meta.x !== "number" ||
|
|
325
|
+
typeof meta.y !== "number" ||
|
|
326
|
+
!Number.isFinite(meta.x) ||
|
|
327
|
+
!Number.isFinite(meta.y))
|
|
328
|
+
throw new Error("dagre layout produced no position");
|
|
329
|
+
const size = nodeSizeFor(n);
|
|
330
|
+
byId.set(n.id, {
|
|
331
|
+
...n,
|
|
332
|
+
x: meta.x - size.w / 2,
|
|
333
|
+
y: meta.y - size.h / 2,
|
|
334
|
+
w: size.w,
|
|
335
|
+
h: size.h,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const edges = [];
|
|
339
|
+
for (const e of p.edges) {
|
|
340
|
+
const meta = g.edge(e.from, e.to);
|
|
341
|
+
const points = meta?.points;
|
|
342
|
+
if (!points || points.length === 0)
|
|
343
|
+
throw new Error("no edge points");
|
|
344
|
+
let labelPoint = null;
|
|
345
|
+
if (p.edgeLabels && e.label && meta) {
|
|
346
|
+
if (typeof meta.x === "number" &&
|
|
347
|
+
typeof meta.y === "number" &&
|
|
348
|
+
Number.isFinite(meta.x) &&
|
|
349
|
+
Number.isFinite(meta.y))
|
|
350
|
+
labelPoint = { x: meta.x, y: meta.y };
|
|
351
|
+
}
|
|
352
|
+
edges.push({
|
|
353
|
+
from: e.from,
|
|
354
|
+
to: e.to,
|
|
355
|
+
label: e.label,
|
|
356
|
+
dashed: e.dashed,
|
|
357
|
+
tone: e.tone,
|
|
358
|
+
points,
|
|
359
|
+
labelPoint: labelPoint ?? midpoint(points),
|
|
360
|
+
labelW: EDGE_LABEL_W,
|
|
361
|
+
labelH: EDGE_LABEL_H,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const gr = g.graph();
|
|
365
|
+
const width = typeof gr.width === "number" ? gr.width : 0;
|
|
366
|
+
const height = typeof gr.height === "number" ? gr.height : 0;
|
|
367
|
+
if (width <= 0 || height <= 0)
|
|
368
|
+
throw new Error("dagre graph has no extent");
|
|
369
|
+
return padLayout([...byId.values()], edges);
|
|
370
|
+
}
|
|
371
|
+
/** Layered fallback: longest-path layering (cycle-safe) + insertion order
|
|
372
|
+
* within layers. Plain straight-edge routing. Pure, no dagre. */
|
|
373
|
+
function layoutLayered(p) {
|
|
374
|
+
const vertical = p.direction === "TB" || p.direction === "BT";
|
|
375
|
+
const reverse = p.direction === "RL" || p.direction === "BT";
|
|
376
|
+
const rankSep = p.ranksep ?? DEFAULT_RANKSEP;
|
|
377
|
+
const nodeSep = p.nodesep ?? DEFAULT_NODESEP;
|
|
378
|
+
const sizes = new Map();
|
|
379
|
+
for (const n of p.nodes)
|
|
380
|
+
sizes.set(n.id, nodeSizeFor(n));
|
|
381
|
+
// longest-path layering (cycle-safe: Kahn first, leftovers in input order)
|
|
382
|
+
const layer = new Map();
|
|
383
|
+
const preds = new Map();
|
|
384
|
+
for (const n of p.nodes)
|
|
385
|
+
preds.set(n.id, []);
|
|
386
|
+
for (const e of p.edges)
|
|
387
|
+
preds.get(e.to)?.push(e.from);
|
|
388
|
+
const indeg = new Map();
|
|
389
|
+
for (const n of p.nodes)
|
|
390
|
+
indeg.set(n.id, (preds.get(n.id) ?? []).filter((f) => f !== n.id).length);
|
|
391
|
+
const queue = p.nodes
|
|
392
|
+
.filter((n) => (indeg.get(n.id) ?? 0) === 0)
|
|
393
|
+
.map((n) => n.id);
|
|
394
|
+
for (const id of queue)
|
|
395
|
+
layer.set(id, 0);
|
|
396
|
+
while (queue.length) {
|
|
397
|
+
const id = queue.shift();
|
|
398
|
+
for (const e of p.edges) {
|
|
399
|
+
if (e.from !== id)
|
|
400
|
+
continue;
|
|
401
|
+
const next = (layer.get(id) ?? 0) + 1;
|
|
402
|
+
if (!layer.has(e.to) || layer.get(e.to) < next) {
|
|
403
|
+
layer.set(e.to, next);
|
|
404
|
+
if ((indeg.get(e.to) ?? 0) > 0)
|
|
405
|
+
indeg.set(e.to, indeg.get(e.to) - 1);
|
|
406
|
+
queue.push(e.to);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const n of p.nodes)
|
|
411
|
+
if (!layer.has(n.id)) {
|
|
412
|
+
const pl = (preds.get(n.id) ?? [])
|
|
413
|
+
.map((f) => layer.get(f))
|
|
414
|
+
.filter((v) => v !== undefined);
|
|
415
|
+
layer.set(n.id, pl.length ? Math.max(...pl) + 1 : 0);
|
|
416
|
+
}
|
|
417
|
+
const layers = new Map();
|
|
418
|
+
for (const n of p.nodes) {
|
|
419
|
+
const l = layer.get(n.id) ?? 0;
|
|
420
|
+
if (!layers.has(l))
|
|
421
|
+
layers.set(l, []);
|
|
422
|
+
layers.get(l)?.push(n.id);
|
|
423
|
+
}
|
|
424
|
+
const sorted = [...layers.keys()].sort((a, b) => a - b);
|
|
425
|
+
const boxes = [];
|
|
426
|
+
const byId = new Map();
|
|
427
|
+
let primary = VIEW_PAD;
|
|
428
|
+
for (const l of sorted) {
|
|
429
|
+
let secondary = VIEW_PAD;
|
|
430
|
+
let layerMax = 0;
|
|
431
|
+
for (const id of layers.get(l) ?? []) {
|
|
432
|
+
const n = p.nodes.find((nn) => nn.id === id);
|
|
433
|
+
if (!n)
|
|
434
|
+
continue;
|
|
435
|
+
const { w, h } = sizes.get(id);
|
|
436
|
+
const box = {
|
|
437
|
+
...n,
|
|
438
|
+
x: vertical ? secondary : primary,
|
|
439
|
+
y: vertical ? primary : secondary,
|
|
440
|
+
w,
|
|
441
|
+
h,
|
|
442
|
+
};
|
|
443
|
+
boxes.push(box);
|
|
444
|
+
byId.set(id, box);
|
|
445
|
+
secondary += (vertical ? w : h) + nodeSep;
|
|
446
|
+
layerMax = Math.max(layerMax, vertical ? w : h);
|
|
447
|
+
}
|
|
448
|
+
primary += (vertical ? layerMax : 0) + rankSep + (vertical ? 0 : 44);
|
|
449
|
+
}
|
|
450
|
+
// horizontal mode: primary advances per layer by ranksep; recompute
|
|
451
|
+
if (!vertical) {
|
|
452
|
+
let x = VIEW_PAD;
|
|
453
|
+
for (const l of sorted) {
|
|
454
|
+
let maxW = 0;
|
|
455
|
+
for (const b of boxes) {
|
|
456
|
+
if ((layer.get(b.id) ?? 0) !== l)
|
|
457
|
+
continue;
|
|
458
|
+
b.x = x;
|
|
459
|
+
maxW = Math.max(maxW, b.w);
|
|
460
|
+
}
|
|
461
|
+
x += maxW + rankSep;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (reverse) {
|
|
465
|
+
const extent = Math.max(...boxes.map((b) => (vertical ? b.y + b.h : b.x + b.w)));
|
|
466
|
+
for (const b of boxes) {
|
|
467
|
+
if (vertical)
|
|
468
|
+
b.y = extent - b.y - b.h;
|
|
469
|
+
else
|
|
470
|
+
b.x = extent - b.x - b.w;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const edges = [];
|
|
474
|
+
for (const e of p.edges) {
|
|
475
|
+
const a = byId.get(e.from);
|
|
476
|
+
const b = byId.get(e.to);
|
|
477
|
+
if (!a || !b)
|
|
478
|
+
continue;
|
|
479
|
+
const points = borderPoints(a, b);
|
|
480
|
+
edges.push({
|
|
481
|
+
from: e.from,
|
|
482
|
+
to: e.to,
|
|
483
|
+
label: e.label,
|
|
484
|
+
dashed: e.dashed,
|
|
485
|
+
tone: e.tone,
|
|
486
|
+
points,
|
|
487
|
+
labelPoint: midpoint(points),
|
|
488
|
+
labelW: EDGE_LABEL_W,
|
|
489
|
+
labelH: EDGE_LABEL_H,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const width = Math.max(...boxes.map((b) => b.x + b.w)) + VIEW_PAD;
|
|
493
|
+
const height = Math.max(...boxes.map((b) => b.y + b.h)) + VIEW_PAD;
|
|
494
|
+
return { boxes, edges, width, height, engine: "layered" };
|
|
495
|
+
}
|
|
496
|
+
/** Border intersection points between two boxes (center → center clipped). */
|
|
497
|
+
function borderPoints(a, b) {
|
|
498
|
+
const ac = { x: a.x + a.w / 2, y: a.y + a.h / 2 };
|
|
499
|
+
const bc = { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
500
|
+
return [clipToBox(ac, bc, a), clipToBox(bc, ac, b)];
|
|
501
|
+
}
|
|
502
|
+
function clipToBox(from, toward, box) {
|
|
503
|
+
const dx = toward.x - from.x;
|
|
504
|
+
const dy = toward.y - from.y;
|
|
505
|
+
if (dx === 0 && dy === 0)
|
|
506
|
+
return { x: box.x + box.w / 2, y: box.y + box.h / 2 };
|
|
507
|
+
const scale = Math.min(dx !== 0
|
|
508
|
+
? (dx > 0 ? box.x + box.w - from.x : box.x - from.x) / dx
|
|
509
|
+
: Infinity, dy !== 0
|
|
510
|
+
? (dy > 0 ? box.y + box.h - from.y : box.y - from.y) / dy
|
|
511
|
+
: Infinity);
|
|
512
|
+
return { x: from.x + dx * scale, y: from.y + dy * scale };
|
|
513
|
+
}
|
|
514
|
+
function midpoint(points) {
|
|
515
|
+
if (!points.length)
|
|
516
|
+
return { x: 0, y: 0 };
|
|
517
|
+
const total = points
|
|
518
|
+
.slice(1)
|
|
519
|
+
.reduce((acc, p, i) => acc + Math.hypot(p.x - points[i].x, p.y - points[i].y), 0);
|
|
520
|
+
let acc = 0;
|
|
521
|
+
for (let i = 1; i < points.length; i++) {
|
|
522
|
+
const seg = Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
|
|
523
|
+
if (acc + seg >= total / 2 || i === points.length - 1) {
|
|
524
|
+
const t = seg === 0 ? 0 : (total / 2 - acc) / seg;
|
|
525
|
+
return {
|
|
526
|
+
x: points[i - 1].x + (points[i].x - points[i - 1].x) * t,
|
|
527
|
+
y: points[i - 1].y + (points[i].y - points[i - 1].y) * t,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
acc += seg;
|
|
531
|
+
}
|
|
532
|
+
return points[Math.floor(points.length / 2)];
|
|
533
|
+
}
|
|
534
|
+
/** Flow layout: dagre when an engine resolves, layered otherwise. */
|
|
535
|
+
export function layoutFlow(p, engine) {
|
|
536
|
+
if (engine) {
|
|
537
|
+
try {
|
|
538
|
+
return layoutDagre(p, engine);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// fall through to the layered fallback — a layout engine must
|
|
542
|
+
// never take the artifact page down.
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return layoutLayered(p);
|
|
546
|
+
}
|
|
547
|
+
// ── Sequence layout ───────────────────────────────────────────────────────
|
|
548
|
+
export function layoutSequence(p) {
|
|
549
|
+
const gap = p.msgGap ?? SEQ_MSG_GAP_DEFAULT;
|
|
550
|
+
const width = p.sequenceWidth ?? SEQ_WIDTH_DEFAULT;
|
|
551
|
+
const n = Math.max(2, p.nodes.length);
|
|
552
|
+
const inner = Math.max(width - SEQ_PAD_X * 2, SEQ_COL_MIN * n);
|
|
553
|
+
const colW = Math.max(inner / n, SEQ_COL_MIN);
|
|
554
|
+
const actors = p.nodes.map((n, i) => {
|
|
555
|
+
const w = Math.min(Math.max(textWidth(n.label, SEQ_LABEL_CHAR_W) + SEQ_ACTOR_PAD_X * 2, SEQ_ACTOR_MIN_W), Math.min(SEQ_ACTOR_MAX_W, colW - 12));
|
|
556
|
+
return {
|
|
557
|
+
id: n.id,
|
|
558
|
+
label: n.label,
|
|
559
|
+
tone: n.tone,
|
|
560
|
+
x: SEQ_PAD_X + colW * (i + 0.5),
|
|
561
|
+
w,
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
const byId = new Map(actors.map((a) => [a.id, a]));
|
|
565
|
+
const messages = [];
|
|
566
|
+
for (const [i, e] of p.edges.entries()) {
|
|
567
|
+
const a = byId.get(e.from);
|
|
568
|
+
const b = byId.get(e.to);
|
|
569
|
+
if (!a || !b)
|
|
570
|
+
continue;
|
|
571
|
+
messages.push({
|
|
572
|
+
from: e.from,
|
|
573
|
+
to: e.to,
|
|
574
|
+
label: e.label,
|
|
575
|
+
dashed: e.dashed,
|
|
576
|
+
tone: e.tone,
|
|
577
|
+
y: SEQ_ACTOR_H + SEQ_ROW_PAD + i * gap + gap / 2,
|
|
578
|
+
x1: a.x,
|
|
579
|
+
x2: b.x,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const height = SEQ_ACTOR_H +
|
|
583
|
+
SEQ_ROW_PAD * 2 +
|
|
584
|
+
p.edges.length * gap +
|
|
585
|
+
(messages.length === 0 ? 24 : 0);
|
|
586
|
+
return {
|
|
587
|
+
actors,
|
|
588
|
+
messages,
|
|
589
|
+
width: Math.max(width, SEQ_PAD_X * 2 + colW * n),
|
|
590
|
+
height,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** Flat rows for the accessible data fallback (sr-only table). */
|
|
594
|
+
export function diagramRows(params) {
|
|
595
|
+
const rows = [];
|
|
596
|
+
const labelOf = (id) => params.nodes.find((n) => n.id === id)?.label ?? id;
|
|
597
|
+
if (params.mode === "sequence") {
|
|
598
|
+
for (const n of params.nodes)
|
|
599
|
+
rows.push({ kind: "node", label: n.label, detail: "actor" });
|
|
600
|
+
for (const e of params.edges)
|
|
601
|
+
rows.push({
|
|
602
|
+
kind: "edge",
|
|
603
|
+
label: `${labelOf(e.from)} → ${labelOf(e.to)}`,
|
|
604
|
+
detail: e.label ?? (e.dashed ? "return message" : "message"),
|
|
605
|
+
});
|
|
606
|
+
return rows;
|
|
607
|
+
}
|
|
608
|
+
for (const n of params.nodes)
|
|
609
|
+
rows.push({
|
|
610
|
+
kind: "node",
|
|
611
|
+
label: n.label,
|
|
612
|
+
detail: [n.kind, n.tone].filter(Boolean).join(", ") || "node",
|
|
613
|
+
});
|
|
614
|
+
for (const e of params.edges)
|
|
615
|
+
rows.push({
|
|
616
|
+
kind: "edge",
|
|
617
|
+
label: `${labelOf(e.from)} → ${labelOf(e.to)}`,
|
|
618
|
+
detail: e.label ?? (e.dashed ? "dashed edge" : "edge"),
|
|
619
|
+
});
|
|
620
|
+
return rows;
|
|
621
|
+
}
|