@formicoidea/labre-framework-c4 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/actions.d.ts +179 -0
  2. package/dist/actions.js +375 -0
  3. package/dist/background.d.ts +77 -0
  4. package/dist/background.js +223 -0
  5. package/dist/commands.d.ts +4 -0
  6. package/dist/commands.js +221 -0
  7. package/dist/component.d.ts +192 -0
  8. package/dist/component.js +188 -0
  9. package/dist/consts.d.ts +331 -0
  10. package/dist/consts.js +384 -0
  11. package/dist/descriptor.d.ts +12 -0
  12. package/dist/descriptor.js +10 -0
  13. package/dist/effects.d.ts +9 -0
  14. package/dist/effects.js +6 -0
  15. package/dist/element-renderer.d.ts +18 -0
  16. package/dist/element-renderer.js +14 -0
  17. package/dist/element-view.d.ts +51 -0
  18. package/dist/element-view.js +146 -0
  19. package/dist/export.d.ts +184 -0
  20. package/dist/export.js +454 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.js +52 -0
  23. package/dist/interchange.d.ts +74 -0
  24. package/dist/interchange.js +143 -0
  25. package/dist/legend.d.ts +37 -0
  26. package/dist/legend.js +123 -0
  27. package/dist/levels.d.ts +70 -0
  28. package/dist/levels.js +46 -0
  29. package/dist/morph.d.ts +89 -0
  30. package/dist/morph.js +229 -0
  31. package/dist/node/node-renderer.d.ts +6 -0
  32. package/dist/node/node-renderer.js +304 -0
  33. package/dist/node/node-view.d.ts +45 -0
  34. package/dist/node/node-view.js +80 -0
  35. package/dist/node/type-line-watcher.d.ts +70 -0
  36. package/dist/node/type-line-watcher.js +142 -0
  37. package/dist/presets.d.ts +84 -0
  38. package/dist/presets.js +149 -0
  39. package/dist/profiles.d.ts +2 -0
  40. package/dist/profiles.js +177 -0
  41. package/dist/roles.d.ts +116 -0
  42. package/dist/roles.js +303 -0
  43. package/dist/rules.d.ts +95 -0
  44. package/dist/rules.js +1261 -0
  45. package/dist/toolbar/c4-menu.d.ts +11 -0
  46. package/dist/toolbar/c4-menu.js +14 -0
  47. package/dist/toolbar/c4-senior-button.d.ts +19 -0
  48. package/dist/toolbar/c4-senior-button.js +23 -0
  49. package/dist/toolbar/config.d.ts +150 -0
  50. package/dist/toolbar/config.js +436 -0
  51. package/dist/toolbar/icons.d.ts +90 -0
  52. package/dist/toolbar/icons.js +157 -0
  53. package/dist/toolbar/senior-tool.d.ts +1 -0
  54. package/dist/toolbar/senior-tool.js +11 -0
  55. package/dist/translations.d.ts +18 -0
  56. package/dist/translations.js +42 -0
  57. package/dist/type-line.d.ts +175 -0
  58. package/dist/type-line.js +244 -0
  59. package/dist/view.d.ts +33 -0
  60. package/dist/view.js +148 -0
  61. package/package.json +34 -0
@@ -0,0 +1,179 @@
1
+ import { C4BoardElementModel, type C4BoundaryVariant, type C4NodeKind } from '@formicoidea/labre-core/model';
2
+ import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
+ import type { C4ExportBoard } from './export.js';
4
+ /**
5
+ * Create a C4 component centred on the viewport: the shape, its THREE written
6
+ * tiers, and the GROUP that makes the four one thing.
7
+ *
8
+ * ## Five elements, and why the group is one of them
9
+ *
10
+ * The PO's recette of 28/08/2026 rejected the "Details" popover the type line
11
+ * and the description used to be typed into: an architect writes on the picture.
12
+ * Its follow-up went one further and took the NAME off the shape too — for one
13
+ * iteration the name was the shape's native inner text and the other two tiers
14
+ * were elements, which meant two kinds of text in one component, two editors,
15
+ * two toolbars and two sets of rules for the same three lines.
16
+ *
17
+ * So the shape is created carrying NO text at all, and the three lines are three
18
+ * canvas `text` elements. Which leaves the problem the popover did not have —
19
+ * four elements have to move, copy and delete as one — and a native `group` is
20
+ * the platform's own answer: one click selects the component, a second descends
21
+ * into whichever tier was clicked, and every gesture the editor already knows
22
+ * works on it.
23
+ *
24
+ * The group's `xywh` is DERIVED from its children. Its `index`, and the tiers',
25
+ * are not layering statements but stability ones: the five elements are created
26
+ * in painting order, the shape first and the words above it, because two
27
+ * elements sharing an index sort by id — and an id is a nanoid.
28
+ *
29
+ * ## An empty shape is still a whole target
30
+ *
31
+ * A shape with no text would normally be hit only near its border and across the
32
+ * few characters of its label, which is the AFFiNE behaviour `includesPoint`
33
+ * implements. `C4NodeElementModel` overrides it to force the interior test, so
34
+ * the body stays draggable, selectable and double-clickable across its whole
35
+ * area with nothing written in it — the override that was added for the
36
+ * glyph-bodied kinds now carries every kind.
37
+ *
38
+ * ## Placeholders, not values
39
+ *
40
+ * All three tiers exist from creation, carrying the stencil's own prompts: the
41
+ * kind's label as the name, `[Container: technology]` under it and `description`
42
+ * under that. The author meets three lines of stencil rather than an empty box.
43
+ * The two lower prompts are read as "nothing stated" by the exporter; the NAME
44
+ * is not, because an unnamed container really is a container (`component.ts`).
45
+ *
46
+ * The ROLE is stamped on the shape and on all three texts, and NOT on the group:
47
+ * the rules, the facts and the export all key on the shape, and the wrapper
48
+ * round a box is not a second box (`roles.ts`).
49
+ */
50
+ export declare function createC4Node(std: BlockStdScope, kind: C4NodeKind): void;
51
+ /** Create a C4 board (the sheet one diagram is drawn on) centred on the viewport. */
52
+ export declare function createC4Board(std: BlockStdScope): void;
53
+ /**
54
+ * Create a C4 boundary centred on the viewport.
55
+ *
56
+ * The variant is WRITTEN on the element and it also decides the default name:
57
+ * the two boundaries are the same dashed rectangle, and C4 tells them apart by
58
+ * what is written under the corner ({@link BOUNDARY_LABEL}, and the note on
59
+ * `variantProp` in `background.ts`). The name is document data from that moment
60
+ * on — renaming a boundary never contradicts its variant.
61
+ *
62
+ * ## The role and the variant are written HERE, together, or not at all
63
+ *
64
+ * This is the ONE place either field is written, and it writes both from the same
65
+ * argument: the `variant` the renderer and the exporter read, and the child ROLE
66
+ * the zoom rules read ({@link C4_BOUNDARY_ROLE}). They are two spellings of one
67
+ * fact — which level this frame is drawn at — and nothing downstream can tell
68
+ * that they were ever separate, so a boundary whose role said "system" and whose
69
+ * variant said "container" would paint one thing and be judged as another, with
70
+ * nothing on screen to show which half was wrong. Anybody adding a second
71
+ * creation site (a paste path, an importer, a template) writes both or writes
72
+ * neither.
73
+ */
74
+ export declare function createC4Boundary(std: BlockStdScope, variant: C4BoundaryVariant): void;
75
+ /**
76
+ * Arm the native connector tool, pre-styled for a C4 relationship: STRAIGHT,
77
+ * DASHED, grey, with a filled triangle head.
78
+ *
79
+ * Three deliberate choices, and all three are the stencil's:
80
+ *
81
+ * - **dashed**, where BPMN's sequence flow is solid. Every line on a C4 diagram
82
+ * is a relationship, so the dash is not a distinction between two kinds of
83
+ * line but the house style of the one kind there is;
84
+ * - **straight**, where BPMN routes orthogonally. A C4 diagram is a graph, not
85
+ * a process laid out in lanes: the elbows a router adds would read as a route
86
+ * through the diagram that nobody drew;
87
+ * - **a filled head**, because the relationship is directed and says so — the
88
+ * verb is "uses", the source is the element with the need.
89
+ *
90
+ * The role is carried by the TOOL, so the connector is born with it rather than
91
+ * acquiring one afterwards (`docs/adr/0010`).
92
+ */
93
+ export declare function activateC4Relationship(std: BlockStdScope): void;
94
+ /**
95
+ * The boards of the current selection.
96
+ *
97
+ * No read-only filter and no lock filter, the same call `bpmnPoolsSelected`
98
+ * makes for the export: generating a legend WRITES, so unlike that one it is
99
+ * offered only on an editable document — which is why the read-only test is
100
+ * here rather than delegated to the caller.
101
+ */
102
+ export declare function c4BoardsSelected(std: BlockStdScope): C4BoardElementModel[];
103
+ /**
104
+ * Draw the legend of what is actually on the selected board, bottom-left of it.
105
+ *
106
+ * The FIRST selected board and no other: a legend is placed relative to one
107
+ * background, and two of them would put two boxes on top of whatever sits in
108
+ * that corner. Everything about the gesture — the scan, the placement, the box —
109
+ * is `createAutoLegend`'s; C4 contributes {@link C4_AUTO_LEGEND}, a table.
110
+ *
111
+ * The one action in this file with no command behind it: the legend is reached
112
+ * from the selected board's contextual toolbar and from nowhere else (PO
113
+ * arbitration, 27/08/2026 — see `toolbar/config.ts`). Kept here beside its
114
+ * siblings all the same, because it is the same kind of thing — a gesture that
115
+ * writes elements — and because a unit test can drive it without a toolbar.
116
+ */
117
+ export declare function createC4Legend(std: BlockStdScope): void;
118
+ /**
119
+ * The boards of the current selection, WITHOUT the read-only filter
120
+ * {@link c4BoardsSelected} applies.
121
+ *
122
+ * That one refuses a read-only document because it is about to WRITE a legend
123
+ * onto the canvas. An export writes nothing: it reads the board and hands the
124
+ * reader a file. A diagram published read-only is precisely the board somebody
125
+ * wants to take away, and refusing it there would be a filter copied for the
126
+ * shape of it rather than for the reason — the same call `bpmnPoolsSelected`
127
+ * makes for `bpmn.exportXml`.
128
+ */
129
+ export declare function c4BoardsForExport(std: BlockStdScope): C4BoardElementModel[];
130
+ /**
131
+ * Everything the exporter speaks about, in document order.
132
+ *
133
+ * Document order matters for the same reason it does in BPMN: it is the
134
+ * tie-break attribution breaks on — a centre inside two overlapping boundaries
135
+ * goes to the first — and the audit's `attribute()` breaks it the same way.
136
+ * Sorting here would make the export disagree with the badge the user can see.
137
+ *
138
+ * `boards` is the SELECTION and not every board on the surface: a C4 board is
139
+ * one level of one model, and merging three of them would produce the very
140
+ * picture C4 exists to stop people drawing.
141
+ */
142
+ export declare function c4ExportBoardOf(std: BlockStdScope): C4ExportBoard;
143
+ /**
144
+ * What the downloaded file is called, minus the extension.
145
+ *
146
+ * The document's own title first — a board is what the file is OF — then the
147
+ * name of the board whose toolbar launched the export, then a last resort. Every
148
+ * character a file system reserves becomes `-`, whitespace runs collapse, and
149
+ * the result is capped: `diagram` is a better download than one a browser
150
+ * silently refuses.
151
+ *
152
+ * Which of the three it is, is the only thing this function decides; making the
153
+ * answer safe to write to disk is {@link c4SafeFilename}, so the command and the
154
+ * interchange capability cannot name the same board differently.
155
+ */
156
+ export declare function c4ExportFilename(std: BlockStdScope): string;
157
+ /**
158
+ * Serialize the selected board(s) as mermaid C4 and hand the file to the
159
+ * browser.
160
+ *
161
+ * Three steps, and only the first and the last know what an editor is: read the
162
+ * surface, run the DECLARED capability (`docs/adr/0012`), download what it
163
+ * produced. The middle step is not re-implemented here — the document, the
164
+ * filename and the content type all come out of `C4_MERMAID_EXPORT.run`, so the
165
+ * command and the registry cannot describe the same board differently. There is
166
+ * one door; the registry is the label on it.
167
+ *
168
+ * A plain import rather than a DI lookup, for the reason `exportBpmnXmlFile`
169
+ * gives: the capability is a pure function and a value, and P3 is explicit that
170
+ * the registry is the editor's view of these functions, not a gate in front of
171
+ * them.
172
+ *
173
+ * A DOWNLOAD and not a clipboard copy, which is the one place this could have
174
+ * diverged from #149. It does not, for three reasons: `.mmd` is the extension
175
+ * the mermaid CLI and every editor plugin watch for; a multi-board export is
176
+ * several documents and a clipboard holds one thing; and the file is what a
177
+ * reader commits next to the code the diagram is about.
178
+ */
179
+ export declare function exportC4MermaidFile(std: BlockStdScope): void;
@@ -0,0 +1,375 @@
1
+ import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
+ import { createAutoLegend } from '@formicoidea/labre-ddd-shared';
4
+ import { C4BoardElementModel, ConnectorMode, FontFamily, FontStyle, FontWeight, PointStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
5
+ import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
6
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
7
+ import { GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
8
+ import { c4TierBoxes } from './component.js';
9
+ import { BOARD_REF_HEIGHT, BOARD_REF_WIDTH, BOUNDARY_LABEL, BOUNDARY_REF_HEIGHT, BOUNDARY_REF_WIDTH, DESCRIPTION_FONT_SIZE, DESCRIPTION_PLACEHOLDER, NODE_LABEL, NODE_PALETTE, NODE_SIZE, RELATIONSHIP_STROKE, RELATIONSHIP_WIDTH, TITLE_FONT_SIZE, TYPE_FONT_SIZE, } from './consts.js';
10
+ import { C4_MERMAID_EXPORT, c4BoardFrom, c4SafeFilename } from './interchange.js';
11
+ import { C4_AUTO_LEGEND } from './legend.js';
12
+ import { c4NodeProps } from './presets.js';
13
+ import { C4_BOUNDARY_ROLE, C4_ROLE } from './roles.js';
14
+ import { C4_TYPE_PLACEHOLDER } from './type-line.js';
15
+ /**
16
+ * Standalone creation/activation actions for the C4 toolbox — the same shape
17
+ * BPMN's `actions.ts` has, and for the same reason: the menu is a pure renderer
18
+ * over the command registry, so what a button DOES lives here and telemetry is
19
+ * emitted once, by `runCommand`.
20
+ */
21
+ const gfxOf = (std) => std.get(GfxControllerIdentifier);
22
+ function finish(gfx, id) {
23
+ gfx.doc.captureSync();
24
+ gfx.tool.setTool(DefaultTool);
25
+ gfx.selection.set({ elements: [id], editing: false });
26
+ // Keep the palette open (native sub-menu behaviour).
27
+ }
28
+ /**
29
+ * One of a component's two written tiers, as a canvas TEXT element.
30
+ *
31
+ * Every style prop is passed EXPLICITLY, and that is not belt and braces: both
32
+ * creation APIs run the new props through `EditPropsStore.applyLastProps('text',
33
+ * …)`, which merges whatever the user last set on a free text element
34
+ * underneath — a 24px face, a colour from another diagram, a left alignment.
35
+ * Explicit props win the merge, so the only ones that survive it are the ones
36
+ * written here. A type line inheriting the colour of the last sticky note
37
+ * somebody typed would be a notation set by accident.
38
+ *
39
+ * `hasMaxWidth` is what keeps a long sentence inside the element instead of
40
+ * running out over the canvas: the box wraps at its own width and grows
41
+ * downward, and the group grows with it, so a component always contains its own
42
+ * words.
43
+ */
44
+ function addTier(surface, index, role, text, fontSize, fontWeight, color, box) {
45
+ return surface.addElement({
46
+ type: 'text',
47
+ role,
48
+ text,
49
+ index,
50
+ color,
51
+ fontFamily: FontFamily.Inter,
52
+ fontSize,
53
+ fontWeight,
54
+ fontStyle: FontStyle.Normal,
55
+ textAlign: TextAlign.Center,
56
+ hasMaxWidth: true,
57
+ xywh: new Bound(box.x, box.y, box.w, box.h).serialize(),
58
+ });
59
+ }
60
+ /**
61
+ * Create a C4 component centred on the viewport: the shape, its THREE written
62
+ * tiers, and the GROUP that makes the four one thing.
63
+ *
64
+ * ## Five elements, and why the group is one of them
65
+ *
66
+ * The PO's recette of 28/08/2026 rejected the "Details" popover the type line
67
+ * and the description used to be typed into: an architect writes on the picture.
68
+ * Its follow-up went one further and took the NAME off the shape too — for one
69
+ * iteration the name was the shape's native inner text and the other two tiers
70
+ * were elements, which meant two kinds of text in one component, two editors,
71
+ * two toolbars and two sets of rules for the same three lines.
72
+ *
73
+ * So the shape is created carrying NO text at all, and the three lines are three
74
+ * canvas `text` elements. Which leaves the problem the popover did not have —
75
+ * four elements have to move, copy and delete as one — and a native `group` is
76
+ * the platform's own answer: one click selects the component, a second descends
77
+ * into whichever tier was clicked, and every gesture the editor already knows
78
+ * works on it.
79
+ *
80
+ * The group's `xywh` is DERIVED from its children. Its `index`, and the tiers',
81
+ * are not layering statements but stability ones: the five elements are created
82
+ * in painting order, the shape first and the words above it, because two
83
+ * elements sharing an index sort by id — and an id is a nanoid.
84
+ *
85
+ * ## An empty shape is still a whole target
86
+ *
87
+ * A shape with no text would normally be hit only near its border and across the
88
+ * few characters of its label, which is the AFFiNE behaviour `includesPoint`
89
+ * implements. `C4NodeElementModel` overrides it to force the interior test, so
90
+ * the body stays draggable, selectable and double-clickable across its whole
91
+ * area with nothing written in it — the override that was added for the
92
+ * glyph-bodied kinds now carries every kind.
93
+ *
94
+ * ## Placeholders, not values
95
+ *
96
+ * All three tiers exist from creation, carrying the stencil's own prompts: the
97
+ * kind's label as the name, `[Container: technology]` under it and `description`
98
+ * under that. The author meets three lines of stencil rather than an empty box.
99
+ * The two lower prompts are read as "nothing stated" by the exporter; the NAME
100
+ * is not, because an unnamed container really is a container (`component.ts`).
101
+ *
102
+ * The ROLE is stamped on the shape and on all three texts, and NOT on the group:
103
+ * the rules, the facts and the export all key on the shape, and the wrapper
104
+ * round a box is not a second box (`roles.ts`).
105
+ */
106
+ export function createC4Node(std, kind) {
107
+ const gfx = gfxOf(std);
108
+ const surface = gfx.surface;
109
+ if (!surface)
110
+ return;
111
+ const { w, h } = NODE_SIZE[kind];
112
+ const { centerX: cx, centerY: cy } = gfx.viewport;
113
+ const x = cx - w / 2;
114
+ const y = cy - h / 2;
115
+ const paint = NODE_PALETTE[kind];
116
+ const shapeId = surface.addElement({
117
+ // Every prop the kind is worth, from the ONE table the morph also reads
118
+ // (`presets.ts`) — so a component drawn here and one morphed into this kind
119
+ // from the toolbar are the same element, whatever either is restyled to.
120
+ // NO `text`: the name is the `c4:title` child below, and the shape is a
121
+ // body and nothing else.
122
+ ...c4NodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
123
+ index: gfx.layer.generateIndex(),
124
+ });
125
+ const boxes = c4TierBoxes(kind, x, y, w, h);
126
+ const titleId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE.title,
127
+ // The kind's own label — `Person`, `Web app`. A name and a prompt at once,
128
+ // which is why the exporter writes it through unchanged.
129
+ NODE_LABEL[kind], TITLE_FONT_SIZE,
130
+ // The one tier with weight on it: it is the heading of the box, and at 20px
131
+ // against a 16px sentence the size alone does not carry that.
132
+ FontWeight.SemiBold, paint.text, boxes.title);
133
+ const typeLineId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE['type-line'], C4_TYPE_PLACEHOLDER[kind], TYPE_FONT_SIZE, FontWeight.Regular, paint.text, boxes.typeLine);
134
+ const descriptionId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE.description, DESCRIPTION_PLACEHOLDER, DESCRIPTION_FONT_SIZE, FontWeight.Regular, paint.text, boxes.description);
135
+ const groupId = surface.addElement({
136
+ type: 'group',
137
+ index: gfx.layer.generateIndex(),
138
+ // A plain record is a legal `children`: the group's own `propsToY` takes the
139
+ // KEYS and forces every value to `true`.
140
+ children: {
141
+ [shapeId]: true,
142
+ [titleId]: true,
143
+ [typeLineId]: true,
144
+ [descriptionId]: true,
145
+ },
146
+ // No title, deliberately. The group renderer paints one only while the
147
+ // component is selected, and a component announcing itself as "Group 3"
148
+ // above its own name is a label nobody wrote.
149
+ });
150
+ // The GROUP is what the gesture produced, so the group is what is selected: a
151
+ // click selects the component, a double-click descends into the tier under
152
+ // the pointer. That is the whole recette in one selection.
153
+ finish(gfx, groupId);
154
+ }
155
+ /** Create a C4 board (the sheet one diagram is drawn on) centred on the viewport. */
156
+ export function createC4Board(std) {
157
+ const gfx = gfxOf(std);
158
+ const surface = gfx.surface;
159
+ if (!surface)
160
+ return;
161
+ const { centerX: cx, centerY: cy } = gfx.viewport;
162
+ const id = surface.addElement({
163
+ type: 'c4Board',
164
+ // The FRAME the elements are drawn on, and a role of its own: a rule written
165
+ // on the artefacts must never fall on the sheet holding them.
166
+ role: C4_ROLE.board,
167
+ xywh: new Bound(cx - BOARD_REF_WIDTH / 2, cy - BOARD_REF_HEIGHT / 2, BOARD_REF_WIDTH, BOARD_REF_HEIGHT).serialize(),
168
+ });
169
+ finish(gfx, id);
170
+ }
171
+ /**
172
+ * Create a C4 boundary centred on the viewport.
173
+ *
174
+ * The variant is WRITTEN on the element and it also decides the default name:
175
+ * the two boundaries are the same dashed rectangle, and C4 tells them apart by
176
+ * what is written under the corner ({@link BOUNDARY_LABEL}, and the note on
177
+ * `variantProp` in `background.ts`). The name is document data from that moment
178
+ * on — renaming a boundary never contradicts its variant.
179
+ *
180
+ * ## The role and the variant are written HERE, together, or not at all
181
+ *
182
+ * This is the ONE place either field is written, and it writes both from the same
183
+ * argument: the `variant` the renderer and the exporter read, and the child ROLE
184
+ * the zoom rules read ({@link C4_BOUNDARY_ROLE}). They are two spellings of one
185
+ * fact — which level this frame is drawn at — and nothing downstream can tell
186
+ * that they were ever separate, so a boundary whose role said "system" and whose
187
+ * variant said "container" would paint one thing and be judged as another, with
188
+ * nothing on screen to show which half was wrong. Anybody adding a second
189
+ * creation site (a paste path, an importer, a template) writes both or writes
190
+ * neither.
191
+ */
192
+ export function createC4Boundary(std, variant) {
193
+ const gfx = gfxOf(std);
194
+ const surface = gfx.surface;
195
+ if (!surface)
196
+ return;
197
+ const { centerX: cx, centerY: cy } = gfx.viewport;
198
+ const id = surface.addElement({
199
+ type: 'c4Boundary',
200
+ // The CHILD role matching the variant — never the parent, which is now the
201
+ // role of a boundary drawn before the split and of nothing this editor
202
+ // creates. See the note above.
203
+ role: C4_BOUNDARY_ROLE[variant],
204
+ name: BOUNDARY_LABEL[variant],
205
+ variant,
206
+ xywh: new Bound(cx - BOUNDARY_REF_WIDTH / 2, cy - BOUNDARY_REF_HEIGHT / 2, BOUNDARY_REF_WIDTH, BOUNDARY_REF_HEIGHT).serialize(),
207
+ });
208
+ finish(gfx, id);
209
+ }
210
+ /**
211
+ * Arm the native connector tool, pre-styled for a C4 relationship: STRAIGHT,
212
+ * DASHED, grey, with a filled triangle head.
213
+ *
214
+ * Three deliberate choices, and all three are the stencil's:
215
+ *
216
+ * - **dashed**, where BPMN's sequence flow is solid. Every line on a C4 diagram
217
+ * is a relationship, so the dash is not a distinction between two kinds of
218
+ * line but the house style of the one kind there is;
219
+ * - **straight**, where BPMN routes orthogonally. A C4 diagram is a graph, not
220
+ * a process laid out in lanes: the elbows a router adds would read as a route
221
+ * through the diagram that nobody drew;
222
+ * - **a filled head**, because the relationship is directed and says so — the
223
+ * verb is "uses", the source is the element with the need.
224
+ *
225
+ * The role is carried by the TOOL, so the connector is born with it rather than
226
+ * acquiring one afterwards (`docs/adr/0010`).
227
+ */
228
+ export function activateC4Relationship(std) {
229
+ gfxOf(std).tool.setTool(ConnectorTool, {
230
+ mode: ConnectorMode.Straight,
231
+ role: C4_ROLE.relationship,
232
+ // The relationship's look rides on the activation, never through the
233
+ // last-props store: the plain connector tool must keep the user's own
234
+ // style (#144 M1). A dashed grey edge with a filled head is the C4
235
+ // stencil's, and it must not become the plain connector's costume.
236
+ style: {
237
+ stroke: RELATIONSHIP_STROKE,
238
+ strokeStyle: StrokeStyle.Dash,
239
+ strokeWidth: RELATIONSHIP_WIDTH,
240
+ frontEndpointStyle: PointStyle.None,
241
+ rearEndpointStyle: PointStyle.Triangle,
242
+ },
243
+ });
244
+ // Keep the palette open (native sub-menu behaviour).
245
+ }
246
+ /* ── The legend ────────────────────────────────────────────────────────── */
247
+ /**
248
+ * The boards of the current selection.
249
+ *
250
+ * No read-only filter and no lock filter, the same call `bpmnPoolsSelected`
251
+ * makes for the export: generating a legend WRITES, so unlike that one it is
252
+ * offered only on an editable document — which is why the read-only test is
253
+ * here rather than delegated to the caller.
254
+ */
255
+ export function c4BoardsSelected(std) {
256
+ if (std.store.readonly)
257
+ return [];
258
+ return gfxOf(std).selection.selectedElements.filter((model) => model instanceof C4BoardElementModel);
259
+ }
260
+ /**
261
+ * Draw the legend of what is actually on the selected board, bottom-left of it.
262
+ *
263
+ * The FIRST selected board and no other: a legend is placed relative to one
264
+ * background, and two of them would put two boxes on top of whatever sits in
265
+ * that corner. Everything about the gesture — the scan, the placement, the box —
266
+ * is `createAutoLegend`'s; C4 contributes {@link C4_AUTO_LEGEND}, a table.
267
+ *
268
+ * The one action in this file with no command behind it: the legend is reached
269
+ * from the selected board's contextual toolbar and from nowhere else (PO
270
+ * arbitration, 27/08/2026 — see `toolbar/config.ts`). Kept here beside its
271
+ * siblings all the same, because it is the same kind of thing — a gesture that
272
+ * writes elements — and because a unit test can drive it without a toolbar.
273
+ */
274
+ export function createC4Legend(std) {
275
+ const board = c4BoardsSelected(std)[0];
276
+ if (!board)
277
+ return;
278
+ createAutoLegend(std, board, C4_AUTO_LEGEND);
279
+ }
280
+ /* ── Export (mermaid C4) ───────────────────────────────────────────────── */
281
+ /**
282
+ * The boards of the current selection, WITHOUT the read-only filter
283
+ * {@link c4BoardsSelected} applies.
284
+ *
285
+ * That one refuses a read-only document because it is about to WRITE a legend
286
+ * onto the canvas. An export writes nothing: it reads the board and hands the
287
+ * reader a file. A diagram published read-only is precisely the board somebody
288
+ * wants to take away, and refusing it there would be a filter copied for the
289
+ * shape of it rather than for the reason — the same call `bpmnPoolsSelected`
290
+ * makes for `bpmn.exportXml`.
291
+ */
292
+ export function c4BoardsForExport(std) {
293
+ return gfxOf(std).selection.selectedElements.filter((model) => model instanceof C4BoardElementModel);
294
+ }
295
+ /**
296
+ * Everything the exporter speaks about, in document order.
297
+ *
298
+ * Document order matters for the same reason it does in BPMN: it is the
299
+ * tie-break attribution breaks on — a centre inside two overlapping boundaries
300
+ * goes to the first — and the audit's `attribute()` breaks it the same way.
301
+ * Sorting here would make the export disagree with the badge the user can see.
302
+ *
303
+ * `boards` is the SELECTION and not every board on the surface: a C4 board is
304
+ * one level of one model, and merging three of them would produce the very
305
+ * picture C4 exists to stop people drawing.
306
+ */
307
+ export function c4ExportBoardOf(std) {
308
+ return c4BoardFrom(c4ExportElementsOf(std));
309
+ }
310
+ /**
311
+ * The elements the export speaks about, as ONE list the declared capability
312
+ * takes: the selected boards, then everything else on the surface in document
313
+ * order.
314
+ *
315
+ * The selection is expressed by which boards are IN the list — that is the
316
+ * capability's contract for this framework (see `interchange.ts`) — so the
317
+ * unselected boards are the one thing left out, and `c4BoardFrom` on the
318
+ * result reads back exactly what {@link c4ExportBoardOf} says.
319
+ *
320
+ * The two written tiers of every component and the groups that say whose words
321
+ * they are ride along untouched, for the reason the picking gives: a tier
322
+ * belongs to its node through the group rather than through the geometry, so
323
+ * both lists cross the seam whole and unfiltered.
324
+ */
325
+ function c4ExportElementsOf(std) {
326
+ const elements = gfxOf(std).surface?.elementModels ?? [];
327
+ return [
328
+ ...c4BoardsForExport(std),
329
+ ...elements.filter(element => !(element instanceof C4BoardElementModel)),
330
+ ];
331
+ }
332
+ /**
333
+ * What the downloaded file is called, minus the extension.
334
+ *
335
+ * The document's own title first — a board is what the file is OF — then the
336
+ * name of the board whose toolbar launched the export, then a last resort. Every
337
+ * character a file system reserves becomes `-`, whitespace runs collapse, and
338
+ * the result is capped: `diagram` is a better download than one a browser
339
+ * silently refuses.
340
+ *
341
+ * Which of the three it is, is the only thing this function decides; making the
342
+ * answer safe to write to disk is {@link c4SafeFilename}, so the command and the
343
+ * interchange capability cannot name the same board differently.
344
+ */
345
+ export function c4ExportFilename(std) {
346
+ const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
347
+ const board = c4BoardsForExport(std)[0]?.name;
348
+ return c4SafeFilename(title || board);
349
+ }
350
+ /**
351
+ * Serialize the selected board(s) as mermaid C4 and hand the file to the
352
+ * browser.
353
+ *
354
+ * Three steps, and only the first and the last know what an editor is: read the
355
+ * surface, run the DECLARED capability (`docs/adr/0012`), download what it
356
+ * produced. The middle step is not re-implemented here — the document, the
357
+ * filename and the content type all come out of `C4_MERMAID_EXPORT.run`, so the
358
+ * command and the registry cannot describe the same board differently. There is
359
+ * one door; the registry is the label on it.
360
+ *
361
+ * A plain import rather than a DI lookup, for the reason `exportBpmnXmlFile`
362
+ * gives: the capability is a pure function and a value, and P3 is explicit that
363
+ * the registry is the editor's view of these functions, not a gate in front of
364
+ * them.
365
+ *
366
+ * A DOWNLOAD and not a clipboard copy, which is the one place this could have
367
+ * diverged from #149. It does not, for three reasons: `.mmd` is the extension
368
+ * the mermaid CLI and every editor plugin watch for; a multi-board export is
369
+ * several documents and a clipboard holds one thing; and the file is what a
370
+ * reader commits next to the code the diagram is about.
371
+ */
372
+ export function exportC4MermaidFile(std) {
373
+ const { text, filename, mime } = C4_MERMAID_EXPORT.run(c4ExportElementsOf(std), { name: c4ExportFilename(std) });
374
+ downloadBlob(new Blob([text], { type: mime }), filename);
375
+ }
@@ -0,0 +1,77 @@
1
+ import type { FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
2
+ /**
3
+ * The two C4 frames, DECLARED (the `FrameworkBackgroundDef` primitive).
4
+ *
5
+ * There is no C4 drawing code for either of them: the primitive paints these
6
+ * declarations, and would paint any other framework's the same way
7
+ * (`docs/adr/0009` on why a framework declares rather than draws).
8
+ */
9
+ /**
10
+ * The C4 board: a titled white card, and nothing else.
11
+ *
12
+ * **No axes and no zones**, exactly like the Context Map board this is modelled
13
+ * on, and for the same reason: a C4 diagram is a GRAPH, not a chart. A system
14
+ * drawn top left says nothing more than one drawn bottom right, and graduating
15
+ * the card would invent a frame of reference C4 does not have — and then judge
16
+ * people against it.
17
+ *
18
+ * What the declaration is for here is the ROLE, the geometry and the TITLE:
19
+ * `c4:board` is what a rule frames its subjects against, and the title is what
20
+ * says which of the four levels this particular sheet is drawing.
21
+ *
22
+ * ## Why the title is declared as a zone label
23
+ *
24
+ * The primitive knows three places words can come from: a side band's label, a
25
+ * zone's label and an axis' title (`backgroundTexts`). A board has no band and
26
+ * no axis, so a single full-plot zone — no fill, no tint, nothing painted but
27
+ * its name — is what carries the title. That keeps it on the ONE walk both the
28
+ * renderer and the hit tester use, which is what makes the words the user
29
+ * double-clicks the same words they see (`C4BoardView`).
30
+ */
31
+ export declare const C4_BOARD_BACKGROUND: FrameworkBackgroundDef;
32
+ /**
33
+ * The C4 boundary: a dashed rectangle with its name in the bottom-left corner.
34
+ *
35
+ * The one background in the library that is deliberately TRANSPARENT. Every
36
+ * other one is a card you put things on — a Wardley map, a pool, a board — and
37
+ * the PO's recette of 26/08/2026 settled that they all paint white. A boundary
38
+ * is the opposite object: it is drawn OVER a diagram, round elements that are
39
+ * already there, and an opaque card would hide the very thing it is pointing at.
40
+ * So it declares a border and no fill, which the primitive paints as an unfilled
41
+ * frame.
42
+ *
43
+ * The consequence is worth stating because it is the reverse of the usual one: a
44
+ * boundary dropped over existing elements does NOT cover them, and it is not
45
+ * hit anywhere but on its own frame area — it is a lasso, in the same sense
46
+ * BPMN's group is.
47
+ *
48
+ * ## The dash
49
+ *
50
+ * Declared, not drawn: `surface.border.dash` was added to the primitive for this
51
+ * (see `BackgroundSurfaceDef`). It is the whole distinction between a boundary
52
+ * and a board at a glance, so it belongs where the rest of the frame is
53
+ * declared, in data a reviewer can read.
54
+ *
55
+ * ## The variant, and the bracket line it decides
56
+ *
57
+ * The stencil writes TWO lines in that corner: the author's name, and under it
58
+ * the level — `[Software System]` or `[Container]`. The second is derived from
59
+ * the variant and is therefore VOCABULARY, declared with a `labelKey` and no
60
+ * `prop`: it is translatable through the host's catalogue, and it is not offered
61
+ * to the in-place editor, because what kind of boundary this is was said by
62
+ * picking the tool rather than by typing.
63
+ *
64
+ * `variantProp` names {@link C4BoundaryElementModel.variantOrDefault} rather
65
+ * than `variant` itself, and that is the whole trick. `variant` is OPTIONAL: an
66
+ * unstated one stringifies to `"undefined"`, matches no declared variant and
67
+ * would paint NOTHING — which is exactly why this declaration used to declare no
68
+ * `variantProp` at all and let the creation site's default NAME carry the
69
+ * distinction. The derived getter applies the documented default (`'system'`)
70
+ * before the gate sees it, so every boundary — including every one already on
71
+ * disk, which stored nothing — reads as a real variant and gets its line.
72
+ *
73
+ * The default NAME still lives at the creation site (`BOUNDARY_LABEL` in
74
+ * `consts.ts`) and is still the author's from that moment on: renaming a
75
+ * boundary never contradicts its variant, and never silences its bracket line.
76
+ */
77
+ export declare const C4_BOUNDARY_BACKGROUND: FrameworkBackgroundDef;