@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.
- package/dist/actions.d.ts +179 -0
- package/dist/actions.js +375 -0
- package/dist/background.d.ts +77 -0
- package/dist/background.js +223 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +221 -0
- package/dist/component.d.ts +192 -0
- package/dist/component.js +188 -0
- package/dist/consts.d.ts +331 -0
- package/dist/consts.js +384 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +18 -0
- package/dist/element-renderer.js +14 -0
- package/dist/element-view.d.ts +51 -0
- package/dist/element-view.js +146 -0
- package/dist/export.d.ts +184 -0
- package/dist/export.js +454 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +52 -0
- package/dist/interchange.d.ts +74 -0
- package/dist/interchange.js +143 -0
- package/dist/legend.d.ts +37 -0
- package/dist/legend.js +123 -0
- package/dist/levels.d.ts +70 -0
- package/dist/levels.js +46 -0
- package/dist/morph.d.ts +89 -0
- package/dist/morph.js +229 -0
- package/dist/node/node-renderer.d.ts +6 -0
- package/dist/node/node-renderer.js +304 -0
- package/dist/node/node-view.d.ts +45 -0
- package/dist/node/node-view.js +80 -0
- package/dist/node/type-line-watcher.d.ts +70 -0
- package/dist/node/type-line-watcher.js +142 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.js +149 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +177 -0
- package/dist/roles.d.ts +116 -0
- package/dist/roles.js +303 -0
- package/dist/rules.d.ts +95 -0
- package/dist/rules.js +1261 -0
- package/dist/toolbar/c4-menu.d.ts +11 -0
- package/dist/toolbar/c4-menu.js +14 -0
- package/dist/toolbar/c4-senior-button.d.ts +19 -0
- package/dist/toolbar/c4-senior-button.js +23 -0
- package/dist/toolbar/config.d.ts +150 -0
- package/dist/toolbar/config.js +436 -0
- package/dist/toolbar/icons.d.ts +90 -0
- package/dist/toolbar/icons.js +157 -0
- package/dist/toolbar/senior-tool.d.ts +1 -0
- package/dist/toolbar/senior-tool.js +11 -0
- package/dist/translations.d.ts +18 -0
- package/dist/translations.js +42 -0
- package/dist/type-line.d.ts +175 -0
- package/dist/type-line.js +244 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +148 -0
- 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;
|
package/dist/actions.js
ADDED
|
@@ -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;
|