@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,223 @@
|
|
|
1
|
+
import { BOARD_BORDER_WIDTH, BOARD_CARD_BORDER, BOARD_CARD_FILL, BOARD_CORNER_RADIUS, BOARD_MARGIN, BOARD_REF_HEIGHT, BOARD_REF_WIDTH, BOARD_TITLE_COLOR, BOARD_TITLE_FONT_SIZE, BOARD_TITLE_MARGIN, BOUNDARY_CORNER_RADIUS, BOUNDARY_DASH, BOUNDARY_MARGIN, BOUNDARY_NAME_COLOR, BOUNDARY_NAME_FONT_SIZE, BOUNDARY_NAME_INSET, BOUNDARY_REF_HEIGHT, BOUNDARY_REF_WIDTH, BOUNDARY_STROKE, BOUNDARY_TYPE_FONT_SIZE, BOUNDARY_TYPE_STEP, BOUNDARY_WIDTH, FONT_FAMILY, } from './consts.js';
|
|
2
|
+
import { C4_ROLE } from './roles.js';
|
|
3
|
+
/**
|
|
4
|
+
* The two C4 frames, DECLARED (the `FrameworkBackgroundDef` primitive).
|
|
5
|
+
*
|
|
6
|
+
* There is no C4 drawing code for either of them: the primitive paints these
|
|
7
|
+
* declarations, and would paint any other framework's the same way
|
|
8
|
+
* (`docs/adr/0009` on why a framework declares rather than draws).
|
|
9
|
+
*/
|
|
10
|
+
/* ── The board ─────────────────────────────────────────────────────────── */
|
|
11
|
+
/**
|
|
12
|
+
* The C4 board: a titled white card, and nothing else.
|
|
13
|
+
*
|
|
14
|
+
* **No axes and no zones**, exactly like the Context Map board this is modelled
|
|
15
|
+
* on, and for the same reason: a C4 diagram is a GRAPH, not a chart. A system
|
|
16
|
+
* drawn top left says nothing more than one drawn bottom right, and graduating
|
|
17
|
+
* the card would invent a frame of reference C4 does not have — and then judge
|
|
18
|
+
* people against it.
|
|
19
|
+
*
|
|
20
|
+
* What the declaration is for here is the ROLE, the geometry and the TITLE:
|
|
21
|
+
* `c4:board` is what a rule frames its subjects against, and the title is what
|
|
22
|
+
* says which of the four levels this particular sheet is drawing.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the title is declared as a zone label
|
|
25
|
+
*
|
|
26
|
+
* The primitive knows three places words can come from: a side band's label, a
|
|
27
|
+
* zone's label and an axis' title (`backgroundTexts`). A board has no band and
|
|
28
|
+
* no axis, so a single full-plot zone — no fill, no tint, nothing painted but
|
|
29
|
+
* its name — is what carries the title. That keeps it on the ONE walk both the
|
|
30
|
+
* renderer and the hit tester use, which is what makes the words the user
|
|
31
|
+
* double-clicks the same words they see (`C4BoardView`).
|
|
32
|
+
*/
|
|
33
|
+
export const C4_BOARD_BACKGROUND = {
|
|
34
|
+
type: 'c4Board',
|
|
35
|
+
role: C4_ROLE.board,
|
|
36
|
+
geometry: {
|
|
37
|
+
// Wide and free, the Context Map board's own call: a C4 diagram grows
|
|
38
|
+
// sideways as the system is discovered, so neither dimension is locked to
|
|
39
|
+
// the other and the handles are offered from the start.
|
|
40
|
+
width: BOARD_REF_WIDTH,
|
|
41
|
+
height: BOARD_REF_HEIGHT,
|
|
42
|
+
lockAspectRatio: false,
|
|
43
|
+
resizable: true,
|
|
44
|
+
// Only the top margin is deep: that is where the title is written.
|
|
45
|
+
margin: {
|
|
46
|
+
top: BOARD_TITLE_MARGIN,
|
|
47
|
+
right: BOARD_MARGIN,
|
|
48
|
+
bottom: BOARD_MARGIN,
|
|
49
|
+
left: BOARD_MARGIN,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
zones: [
|
|
53
|
+
{
|
|
54
|
+
id: 'title',
|
|
55
|
+
// The whole plot, and nothing painted over it: this zone exists to carry
|
|
56
|
+
// a label, not to tint a region. Reported by the audit as one zone
|
|
57
|
+
// covering the board, which is the honest answer — a C4 board has exactly
|
|
58
|
+
// one region and it is the board.
|
|
59
|
+
rect: { x: 0, y: 0, w: 1, h: 1 },
|
|
60
|
+
label: {
|
|
61
|
+
id: 'name',
|
|
62
|
+
// The user's own words, and only those: a diagram is titled by whoever
|
|
63
|
+
// draws it, so there is no vocabulary to fall back to and no `labelKey`
|
|
64
|
+
// to declare. Same call the BPMN pool's participant name makes.
|
|
65
|
+
prop: 'name',
|
|
66
|
+
// `y: 0` is the top of the plot; a negative `dy` walks back UP into the
|
|
67
|
+
// title margin, where the words are written.
|
|
68
|
+
anchor: { x: 0, y: 0, dy: -BOARD_TITLE_MARGIN / 3 },
|
|
69
|
+
style: {
|
|
70
|
+
size: BOARD_TITLE_FONT_SIZE,
|
|
71
|
+
weight: 600,
|
|
72
|
+
color: '@title',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
chrome: {
|
|
78
|
+
fontFamily: FONT_FAMILY,
|
|
79
|
+
palette: {
|
|
80
|
+
card: BOARD_CARD_FILL,
|
|
81
|
+
cardBorder: BOARD_CARD_BORDER,
|
|
82
|
+
title: BOARD_TITLE_COLOR,
|
|
83
|
+
},
|
|
84
|
+
surface: {
|
|
85
|
+
fill: '@card',
|
|
86
|
+
border: {
|
|
87
|
+
color: '@cardBorder',
|
|
88
|
+
width: BOARD_BORDER_WIDTH,
|
|
89
|
+
radius: BOARD_CORNER_RADIUS,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
/* ── The boundary ──────────────────────────────────────────────────────── */
|
|
95
|
+
/**
|
|
96
|
+
* The C4 boundary: a dashed rectangle with its name in the bottom-left corner.
|
|
97
|
+
*
|
|
98
|
+
* The one background in the library that is deliberately TRANSPARENT. Every
|
|
99
|
+
* other one is a card you put things on — a Wardley map, a pool, a board — and
|
|
100
|
+
* the PO's recette of 26/08/2026 settled that they all paint white. A boundary
|
|
101
|
+
* is the opposite object: it is drawn OVER a diagram, round elements that are
|
|
102
|
+
* already there, and an opaque card would hide the very thing it is pointing at.
|
|
103
|
+
* So it declares a border and no fill, which the primitive paints as an unfilled
|
|
104
|
+
* frame.
|
|
105
|
+
*
|
|
106
|
+
* The consequence is worth stating because it is the reverse of the usual one: a
|
|
107
|
+
* boundary dropped over existing elements does NOT cover them, and it is not
|
|
108
|
+
* hit anywhere but on its own frame area — it is a lasso, in the same sense
|
|
109
|
+
* BPMN's group is.
|
|
110
|
+
*
|
|
111
|
+
* ## The dash
|
|
112
|
+
*
|
|
113
|
+
* Declared, not drawn: `surface.border.dash` was added to the primitive for this
|
|
114
|
+
* (see `BackgroundSurfaceDef`). It is the whole distinction between a boundary
|
|
115
|
+
* and a board at a glance, so it belongs where the rest of the frame is
|
|
116
|
+
* declared, in data a reviewer can read.
|
|
117
|
+
*
|
|
118
|
+
* ## The variant, and the bracket line it decides
|
|
119
|
+
*
|
|
120
|
+
* The stencil writes TWO lines in that corner: the author's name, and under it
|
|
121
|
+
* the level — `[Software System]` or `[Container]`. The second is derived from
|
|
122
|
+
* the variant and is therefore VOCABULARY, declared with a `labelKey` and no
|
|
123
|
+
* `prop`: it is translatable through the host's catalogue, and it is not offered
|
|
124
|
+
* to the in-place editor, because what kind of boundary this is was said by
|
|
125
|
+
* picking the tool rather than by typing.
|
|
126
|
+
*
|
|
127
|
+
* `variantProp` names {@link C4BoundaryElementModel.variantOrDefault} rather
|
|
128
|
+
* than `variant` itself, and that is the whole trick. `variant` is OPTIONAL: an
|
|
129
|
+
* unstated one stringifies to `"undefined"`, matches no declared variant and
|
|
130
|
+
* would paint NOTHING — which is exactly why this declaration used to declare no
|
|
131
|
+
* `variantProp` at all and let the creation site's default NAME carry the
|
|
132
|
+
* distinction. The derived getter applies the documented default (`'system'`)
|
|
133
|
+
* before the gate sees it, so every boundary — including every one already on
|
|
134
|
+
* disk, which stored nothing — reads as a real variant and gets its line.
|
|
135
|
+
*
|
|
136
|
+
* The default NAME still lives at the creation site (`BOUNDARY_LABEL` in
|
|
137
|
+
* `consts.ts`) and is still the author's from that moment on: renaming a
|
|
138
|
+
* boundary never contradicts its variant, and never silences its bracket line.
|
|
139
|
+
*/
|
|
140
|
+
export const C4_BOUNDARY_BACKGROUND = {
|
|
141
|
+
type: 'c4Boundary',
|
|
142
|
+
role: C4_ROLE.boundary,
|
|
143
|
+
geometry: {
|
|
144
|
+
width: BOUNDARY_REF_WIDTH,
|
|
145
|
+
height: BOUNDARY_REF_HEIGHT,
|
|
146
|
+
// A boundary is stretched to fit whatever it has been drawn round, which is
|
|
147
|
+
// never the same shape twice.
|
|
148
|
+
lockAspectRatio: false,
|
|
149
|
+
resizable: true,
|
|
150
|
+
margin: {
|
|
151
|
+
top: BOUNDARY_MARGIN,
|
|
152
|
+
right: BOUNDARY_MARGIN,
|
|
153
|
+
bottom: BOUNDARY_MARGIN,
|
|
154
|
+
left: BOUNDARY_MARGIN,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
// The level this instance encloses, with the optional field's documented
|
|
158
|
+
// default already applied — see the note above on why it is the DERIVED
|
|
159
|
+
// getter and not `variant` itself.
|
|
160
|
+
variantProp: 'variantOrDefault',
|
|
161
|
+
zones: [
|
|
162
|
+
{
|
|
163
|
+
id: 'name',
|
|
164
|
+
rect: { x: 0, y: 0, w: 1, h: 1 },
|
|
165
|
+
label: {
|
|
166
|
+
id: 'name',
|
|
167
|
+
prop: 'name',
|
|
168
|
+
// Bottom-left INSIDE the plot — the corner C4 writes a boundary's name
|
|
169
|
+
// in, and the one corner of a frame that is least likely to have an
|
|
170
|
+
// element sitting in it. It sits one bracket-line step up, because the
|
|
171
|
+
// line below it is the level.
|
|
172
|
+
anchor: {
|
|
173
|
+
x: 0,
|
|
174
|
+
y: 1,
|
|
175
|
+
dy: -(BOUNDARY_NAME_INSET + BOUNDARY_TYPE_STEP),
|
|
176
|
+
},
|
|
177
|
+
style: {
|
|
178
|
+
size: BOUNDARY_NAME_FONT_SIZE,
|
|
179
|
+
weight: 600,
|
|
180
|
+
color: '@name',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
// …and the bracket line under it, one zone per variant. Two zones rather
|
|
185
|
+
// than one label with two wordings because a zone carries exactly one
|
|
186
|
+
// label, and the gate that picks between them is the zone's own `variants`
|
|
187
|
+
// — which `backgroundTexts` propagates onto the label it carries.
|
|
188
|
+
...['system', 'container'].map(variant => ({
|
|
189
|
+
id: `type-${variant}`,
|
|
190
|
+
variants: [variant],
|
|
191
|
+
rect: { x: 0, y: 0, w: 1, h: 1 },
|
|
192
|
+
label: {
|
|
193
|
+
id: `type-${variant}`,
|
|
194
|
+
// NO `prop`: this is vocabulary, so it is translatable and it is not
|
|
195
|
+
// offered to the in-place editor. The fallback is the stencil's own
|
|
196
|
+
// wording, brackets included.
|
|
197
|
+
labelKey: `com.labre.c4.boundary.type.${variant}`,
|
|
198
|
+
fallback: variant === 'system' ? '[Software System]' : '[Container]',
|
|
199
|
+
anchor: { x: 0, y: 1, dy: -BOUNDARY_NAME_INSET },
|
|
200
|
+
style: {
|
|
201
|
+
size: BOUNDARY_TYPE_FONT_SIZE,
|
|
202
|
+
color: '@name',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
})),
|
|
206
|
+
],
|
|
207
|
+
chrome: {
|
|
208
|
+
fontFamily: FONT_FAMILY,
|
|
209
|
+
palette: {
|
|
210
|
+
frame: BOUNDARY_STROKE,
|
|
211
|
+
name: BOUNDARY_NAME_COLOR,
|
|
212
|
+
},
|
|
213
|
+
surface: {
|
|
214
|
+
// NO fill — see the note above. This is the transparent one.
|
|
215
|
+
border: {
|
|
216
|
+
color: '@frame',
|
|
217
|
+
width: BOUNDARY_WIDTH,
|
|
218
|
+
radius: BOUNDARY_CORNER_RADIUS,
|
|
219
|
+
dash: BOUNDARY_DASH,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { activateC4Relationship, c4BoardsForExport, createC4Board, createC4Boundary, createC4Node, exportC4MermaidFile, } from './actions.js';
|
|
2
|
+
import { c4ExportMermaidIcon, C4_TOOLBOX_ICONS } from './toolbar/icons.js';
|
|
3
|
+
const SPECS = [
|
|
4
|
+
/* ── Boards first: the sheet everything else is drawn on ────────────── */
|
|
5
|
+
{
|
|
6
|
+
id: 'addBoard',
|
|
7
|
+
label: 'C4 board',
|
|
8
|
+
iconKey: 'c4.board',
|
|
9
|
+
kind: 'artefact',
|
|
10
|
+
category: 'diagrams',
|
|
11
|
+
element: 'board',
|
|
12
|
+
run: createC4Board,
|
|
13
|
+
},
|
|
14
|
+
/* ── The base components: the four levels, each plain form immediately
|
|
15
|
+
followed by its external variant ─────────────────────────────────── */
|
|
16
|
+
{
|
|
17
|
+
id: 'addPerson',
|
|
18
|
+
label: 'Person',
|
|
19
|
+
iconKey: 'c4.person',
|
|
20
|
+
kind: 'artefact',
|
|
21
|
+
category: 'elements',
|
|
22
|
+
element: 'node:person',
|
|
23
|
+
run: std => createC4Node(std, 'person'),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'addPersonExt',
|
|
27
|
+
label: 'Person (external)',
|
|
28
|
+
iconKey: 'c4.person.external',
|
|
29
|
+
kind: 'artefact',
|
|
30
|
+
category: 'elements',
|
|
31
|
+
element: 'node:person-ext',
|
|
32
|
+
run: std => createC4Node(std, 'person-ext'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'addSystem',
|
|
36
|
+
label: 'Software system',
|
|
37
|
+
iconKey: 'c4.system',
|
|
38
|
+
kind: 'artefact',
|
|
39
|
+
category: 'elements',
|
|
40
|
+
element: 'node:system',
|
|
41
|
+
run: std => createC4Node(std, 'system'),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'addSystemExt',
|
|
45
|
+
label: 'Software system (external)',
|
|
46
|
+
iconKey: 'c4.system.external',
|
|
47
|
+
kind: 'artefact',
|
|
48
|
+
category: 'elements',
|
|
49
|
+
element: 'node:system-ext',
|
|
50
|
+
run: std => createC4Node(std, 'system-ext'),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'addContainer',
|
|
54
|
+
label: 'Container',
|
|
55
|
+
iconKey: 'c4.container',
|
|
56
|
+
kind: 'artefact',
|
|
57
|
+
category: 'elements',
|
|
58
|
+
element: 'node:container',
|
|
59
|
+
run: std => createC4Node(std, 'container'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'addComponent',
|
|
63
|
+
label: 'Component',
|
|
64
|
+
iconKey: 'c4.component',
|
|
65
|
+
kind: 'artefact',
|
|
66
|
+
category: 'elements',
|
|
67
|
+
element: 'node:component',
|
|
68
|
+
run: std => createC4Node(std, 'component'),
|
|
69
|
+
},
|
|
70
|
+
/* ── The niche components: a container with a picture on it ─────────── */
|
|
71
|
+
{
|
|
72
|
+
// The container flavour every real system has one of, and the only one with
|
|
73
|
+
// a role of its own — so it leads the three.
|
|
74
|
+
id: 'addDatabase',
|
|
75
|
+
label: 'Database',
|
|
76
|
+
iconKey: 'c4.database',
|
|
77
|
+
kind: 'artefact',
|
|
78
|
+
category: 'elements',
|
|
79
|
+
element: 'node:database',
|
|
80
|
+
run: std => createC4Node(std, 'database'),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'addMobile',
|
|
84
|
+
label: 'Mobile app',
|
|
85
|
+
iconKey: 'c4.mobile',
|
|
86
|
+
kind: 'artefact',
|
|
87
|
+
category: 'elements',
|
|
88
|
+
element: 'node:mobile',
|
|
89
|
+
run: std => createC4Node(std, 'mobile'),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'addBrowser',
|
|
93
|
+
label: 'Web browser',
|
|
94
|
+
iconKey: 'c4.browser',
|
|
95
|
+
kind: 'artefact',
|
|
96
|
+
category: 'elements',
|
|
97
|
+
element: 'node:browser',
|
|
98
|
+
run: std => createC4Node(std, 'browser'),
|
|
99
|
+
},
|
|
100
|
+
/* ── What joins the components, then the frames drawn round them ────── */
|
|
101
|
+
{
|
|
102
|
+
// C4 asks every relationship to be READ as a sentence, and the tool is where
|
|
103
|
+
// the author says which way it runs.
|
|
104
|
+
id: 'relationshipTool',
|
|
105
|
+
label: 'Relationship',
|
|
106
|
+
iconKey: 'c4.relationship',
|
|
107
|
+
kind: 'tool',
|
|
108
|
+
category: 'relations',
|
|
109
|
+
element: 'connector:relationship',
|
|
110
|
+
run: activateC4Relationship,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'addSystemBoundary',
|
|
114
|
+
label: 'System boundary',
|
|
115
|
+
iconKey: 'c4.boundary.system',
|
|
116
|
+
kind: 'artefact',
|
|
117
|
+
category: 'boundaries',
|
|
118
|
+
element: 'boundary:system',
|
|
119
|
+
run: std => createC4Boundary(std, 'system'),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'addContainerBoundary',
|
|
123
|
+
label: 'Container boundary',
|
|
124
|
+
iconKey: 'c4.boundary.container',
|
|
125
|
+
kind: 'artefact',
|
|
126
|
+
category: 'boundaries',
|
|
127
|
+
element: 'boundary:container',
|
|
128
|
+
run: std => createC4Boundary(std, 'container'),
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const toolboxCommands = SPECS.map((spec, order) => ({
|
|
132
|
+
id: `c4.${spec.id}`,
|
|
133
|
+
owner: 'c4',
|
|
134
|
+
kind: spec.kind,
|
|
135
|
+
labelKey: `com.labre.commands.c4.${spec.id}`,
|
|
136
|
+
labelFallback: spec.label,
|
|
137
|
+
category: spec.category,
|
|
138
|
+
iconKey: spec.iconKey,
|
|
139
|
+
// All thirteen, on every surface: the catalogue is the total surface and the
|
|
140
|
+
// sub-menu holds fourteen, so nothing has to be left out of either.
|
|
141
|
+
surfaces: ['senior-menu', 'catalogue', 'palette', 'agent'],
|
|
142
|
+
order,
|
|
143
|
+
scope: 'edgeless',
|
|
144
|
+
// Keyless by intent — still bindable from Settings › Shortcuts, which is what
|
|
145
|
+
// `toShortcutDescriptor` being total buys.
|
|
146
|
+
defaultKeys: { mac: [], other: [] },
|
|
147
|
+
availability: 'always',
|
|
148
|
+
run: spec.run,
|
|
149
|
+
telemetry: { framework: 'c4', element: spec.element },
|
|
150
|
+
}));
|
|
151
|
+
/**
|
|
152
|
+
* The EXPORT — the first C4 command whose subject is a whole BOARD.
|
|
153
|
+
*
|
|
154
|
+
* ## Why it hangs off the board's toolbar, and why the board is also the scope
|
|
155
|
+
*
|
|
156
|
+
* BPMN's export makes the opposite call and the difference is the notation's,
|
|
157
|
+
* not a preference. A BPMN document is a process and half a process is not a
|
|
158
|
+
* smaller process, so `bpmn.exportXml` serializes the whole surface and the
|
|
159
|
+
* selected pool decides only the filename. A C4 board is one LEVEL of one model
|
|
160
|
+
* — a context diagram, a container diagram, a component diagram — and the whole
|
|
161
|
+
* point of drawing three of them side by side is that they are three separate
|
|
162
|
+
* diagrams. Merging them would produce the picture C4 exists to stop people
|
|
163
|
+
* drawing, and mermaid renders one diagram per document anyway. So the selection
|
|
164
|
+
* is the scope: one board, one document; several boards, several documents in
|
|
165
|
+
* one file.
|
|
166
|
+
*
|
|
167
|
+
* ## Surfaces
|
|
168
|
+
*
|
|
169
|
+
* It declines `'senior-menu'`: the sub-menu is what you reach for to DRAW
|
|
170
|
+
* something, and this draws nothing — and C4 has exactly one senior slot left,
|
|
171
|
+
* which an export would be a poor use of. It keeps `'catalogue'`, which is not a
|
|
172
|
+
* category claim but the registry's own invariant: the catalogue is the TOTAL
|
|
173
|
+
* surface, and a command missing from it is unreachable the moment its framework
|
|
174
|
+
* overflows the fourteen slots (pinned by `registry.unit.spec.ts`). It is also
|
|
175
|
+
* what makes the catalogue exactly fourteen — the cap, to the entry.
|
|
176
|
+
*
|
|
177
|
+
* On the row itself it sits in the "⋮" rather than as a button: it is the rarest
|
|
178
|
+
* thing anybody does to a board, and the row already carries the resize toggle
|
|
179
|
+
* and the legend.
|
|
180
|
+
*
|
|
181
|
+
* ## Where the "⋮" entry is registered
|
|
182
|
+
*
|
|
183
|
+
* In the ALWAYS-ON toolbar module (`c4BoardToolbarConfig`), not the flag-gated
|
|
184
|
+
* one, which is where BPMN puts its own export entry: `bpmnPoolToolbarExtension`
|
|
185
|
+
* carries `bpmn.exportXml` and is registered by `BpmnRenderViewExtension`. The
|
|
186
|
+
* entry hides itself when the command is absent from the registry — the `when`
|
|
187
|
+
* guard every `commandAction` carries — so with the C4 tooling flag off the row
|
|
188
|
+
* is the resize toggle alone, and nothing on it can be clicked into a no-op.
|
|
189
|
+
*/
|
|
190
|
+
const exportCommand = {
|
|
191
|
+
id: 'c4.exportMermaid',
|
|
192
|
+
owner: 'c4',
|
|
193
|
+
kind: 'action',
|
|
194
|
+
labelKey: 'com.labre.commands.c4.exportMermaid',
|
|
195
|
+
labelFallback: 'Export as mermaid',
|
|
196
|
+
descriptionKey: 'com.labre.commands.c4.exportMermaid.description',
|
|
197
|
+
descriptionFallback: 'Download the selected board as a mermaid C4 diagram, ready to paste into any mermaid renderer.',
|
|
198
|
+
// Filed with the board, which is the selection that offers it.
|
|
199
|
+
category: 'diagrams',
|
|
200
|
+
iconKey: 'c4.export-mermaid',
|
|
201
|
+
surfaces: ['catalogue', 'contextual-toolbar', 'palette', 'agent'],
|
|
202
|
+
order: SPECS.length,
|
|
203
|
+
scope: 'edgeless',
|
|
204
|
+
defaultKeys: { mac: [], other: [] },
|
|
205
|
+
availability: 'selection',
|
|
206
|
+
run: exportC4MermaidFile,
|
|
207
|
+
telemetry: { framework: 'c4', element: 'board:export-mermaid' },
|
|
208
|
+
// A board in the selection, and no more than that: an export READS, so unlike
|
|
209
|
+
// the legend button it is offered on a read-only document — which is precisely
|
|
210
|
+
// the board somebody wants to take away. That is what `c4BoardsForExport` is,
|
|
211
|
+
// and why it is not `c4BoardsSelected`.
|
|
212
|
+
when: std => c4BoardsForExport(std).length > 0,
|
|
213
|
+
};
|
|
214
|
+
export const c4Commands = [
|
|
215
|
+
...toolboxCommands,
|
|
216
|
+
exportCommand,
|
|
217
|
+
];
|
|
218
|
+
export const c4CommandIcons = {
|
|
219
|
+
...C4_TOOLBOX_ICONS,
|
|
220
|
+
'c4.export-mermaid': c4ExportMermaidIcon,
|
|
221
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { C4NodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
/**
|
|
3
|
+
* A C4 component — the SHAPE and its own words, grouped.
|
|
4
|
+
*
|
|
5
|
+
* ## What the PO's recette of 28/08/2026 changed
|
|
6
|
+
*
|
|
7
|
+
* A C4 element used to be one shape carrying a title, plus two fields edited in
|
|
8
|
+
* a "Details" popover and painted underneath by the renderer. The verdict was
|
|
9
|
+
* that the popover is the wrong mechanism: an architect writes ON the picture.
|
|
10
|
+
* So a component is now a native `group` holding three elements —
|
|
11
|
+
*
|
|
12
|
+
* 1. the `c4Node` shape, whose native inner text is the TITLE;
|
|
13
|
+
* 2. a canvas `text` element carrying the type line, `[Container: Java]`;
|
|
14
|
+
* 3. a canvas `text` element carrying the description —
|
|
15
|
+
*
|
|
16
|
+
* and every one of the three is edited in place, on a double-click, exactly like
|
|
17
|
+
* any other words on the canvas. There is no form left.
|
|
18
|
+
*
|
|
19
|
+
* ## This module
|
|
20
|
+
*
|
|
21
|
+
* The two questions that arise from that arrangement, both PURE and both free of
|
|
22
|
+
* `std`, so the creation site, the exporter, the commit hook and a test all
|
|
23
|
+
* answer them the same way:
|
|
24
|
+
*
|
|
25
|
+
* - **where do the tiers go**, given a node's box ({@link c4TierBoxes});
|
|
26
|
+
* - **which text belongs to which node**, given a flat list of elements
|
|
27
|
+
* ({@link c4ComponentTiers}) — resolved by GROUP MEMBERSHIP plus the role
|
|
28
|
+
* stamped on each text, never by position in `children`.
|
|
29
|
+
*
|
|
30
|
+
* The role stays on the SHAPE alone; the group carries none (`roles.ts`).
|
|
31
|
+
*/
|
|
32
|
+
/** A box in the same units and origin as the node's own. */
|
|
33
|
+
export interface C4TierBox {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
w: number;
|
|
37
|
+
h: number;
|
|
38
|
+
}
|
|
39
|
+
export interface C4TierBoxes {
|
|
40
|
+
title: C4TierBox;
|
|
41
|
+
typeLine: C4TierBox;
|
|
42
|
+
description: C4TierBox;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The three text boxes of a component, laid out against the node's own box.
|
|
46
|
+
*
|
|
47
|
+
* ## The rhythm
|
|
48
|
+
*
|
|
49
|
+
* A margin, the name over two lines, a small gap, the type line, a wider gap,
|
|
50
|
+
* the description over two, and the same margin again — which is exactly what
|
|
51
|
+
* {@link NODE_BOX} is tall enough for, because the box is derived from this
|
|
52
|
+
* stack rather than the stack fitted into the box. So the default element is
|
|
53
|
+
* neither cramped nor half empty, and the two gaps say what they mean: the name
|
|
54
|
+
* and its type line are one heading, the sentence under them is a second
|
|
55
|
+
* statement.
|
|
56
|
+
*
|
|
57
|
+
* The tiers are stacked by walking DOWN — each one placed under the last plus
|
|
58
|
+
* its gap — rather than by six independent offsets. Six offsets is six chances
|
|
59
|
+
* for two tiers to overlap; a walk cannot produce one.
|
|
60
|
+
*
|
|
61
|
+
* ## The person
|
|
62
|
+
*
|
|
63
|
+
* `bodyTop` is the one asymmetry and it is the stencil's: a person's head stands
|
|
64
|
+
* clear ABOVE a body of the standard height, and its words are laid out in the
|
|
65
|
+
* BODY (`v:textRect` is the body box exactly), so the whole stack starts below
|
|
66
|
+
* the head rather than across it.
|
|
67
|
+
*
|
|
68
|
+
* ## What this is not
|
|
69
|
+
*
|
|
70
|
+
* A creation-time answer and nothing more. The tiers are real elements from the
|
|
71
|
+
* moment they are drawn: an author who moves one has moved it, and nothing here
|
|
72
|
+
* runs again to put it back. It is also proportional in the one direction that
|
|
73
|
+
* matters — a node dragged taller keeps its margins where they were, because
|
|
74
|
+
* they are absolutes; only the person's head, which is a picture, scales.
|
|
75
|
+
*/
|
|
76
|
+
export declare function c4TierBoxes(kind: C4NodeKind, x: number, y: number, w: number, h: number): C4TierBoxes;
|
|
77
|
+
/**
|
|
78
|
+
* The little a resolution needs to know about a text element — an id, a role,
|
|
79
|
+
* and whatever it says.
|
|
80
|
+
*
|
|
81
|
+
* `text` is `unknown` because on a real element it is a `Y.Text` and in a test
|
|
82
|
+
* it is a string, and this module has no business knowing which: it is read
|
|
83
|
+
* through {@link c4TierText}, which stringifies whatever it is given. That is
|
|
84
|
+
* also what keeps this file free of a Yjs import.
|
|
85
|
+
*/
|
|
86
|
+
export interface C4TierElement {
|
|
87
|
+
id: string;
|
|
88
|
+
role?: string;
|
|
89
|
+
text?: unknown;
|
|
90
|
+
}
|
|
91
|
+
/** The little a resolution needs to know about a group. */
|
|
92
|
+
export interface C4ComponentGroup {
|
|
93
|
+
id: string;
|
|
94
|
+
childIds: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
/** The three tiers of one node's component — any of them may be absent. */
|
|
97
|
+
export interface C4ComponentTiers {
|
|
98
|
+
title?: C4TierElement;
|
|
99
|
+
typeLine?: C4TierElement;
|
|
100
|
+
description?: C4TierElement;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The tiers of the component a node belongs to — `{}` when it belongs to none.
|
|
104
|
+
*
|
|
105
|
+
* ## Group membership, then roles
|
|
106
|
+
*
|
|
107
|
+
* The group answers "which words are THIS node's" — two containers side by side
|
|
108
|
+
* both have a `[Container: …]` under them, and only the grouping says which is
|
|
109
|
+
* which. The role then answers "which of these words is the type line", which
|
|
110
|
+
* position in `children` cannot: a group's child order is an implementation
|
|
111
|
+
* detail that a reorder, a copy or a regroup rewrites, while a role is written
|
|
112
|
+
* on the element and travels with it.
|
|
113
|
+
*
|
|
114
|
+
* ## What a bare node resolves to, and why that is the right answer
|
|
115
|
+
*
|
|
116
|
+
* `{}` — no name, no technology, no description. Which is exactly what happens
|
|
117
|
+
* to a node whose group was released (native "ungroup"), to one whose texts were
|
|
118
|
+
* deleted, and to one drawn before this change. None of those is an error and
|
|
119
|
+
* none of them is guessed at: an element with no words on it states nothing, and
|
|
120
|
+
* the export says so by writing nothing. The picture is still a C4 element — the
|
|
121
|
+
* role is on the shape and survives everything.
|
|
122
|
+
*
|
|
123
|
+
* The NAME is the one tier with somewhere else to look, and only for the last of
|
|
124
|
+
* those three: an element drawn before the title became a child keeps its name
|
|
125
|
+
* in the shape's own inner text. See {@link c4StatedName}.
|
|
126
|
+
*
|
|
127
|
+
* The FIRST group holding the node wins, and the first text of each role in it.
|
|
128
|
+
* Groups nest, so a component grouped again inside a bigger group has two
|
|
129
|
+
* ancestors; document order picks the one written first, which is the innermost
|
|
130
|
+
* the creation site made.
|
|
131
|
+
*/
|
|
132
|
+
export declare function c4ComponentTiers(nodeId: string, groups: readonly C4ComponentGroup[], texts: readonly C4TierElement[]): C4ComponentTiers;
|
|
133
|
+
/**
|
|
134
|
+
* Everything grouped with this element — `[]` when it is grouped with nothing.
|
|
135
|
+
*
|
|
136
|
+
* The other direction of the same question {@link c4ComponentTiers} asks, and
|
|
137
|
+
* the one the commit hook needs: given a type line somebody has just finished
|
|
138
|
+
* typing into, which shape does it belong to, so its kind can supply the word?
|
|
139
|
+
*
|
|
140
|
+
* The element itself is among the siblings, which is what the id list actually
|
|
141
|
+
* says and what saves the caller from reasoning about whether it was excluded.
|
|
142
|
+
*/
|
|
143
|
+
export declare function c4ComponentSiblings(elementId: string, groups: readonly C4ComponentGroup[]): readonly string[];
|
|
144
|
+
/** Whatever a tier says, as a plain trimmed string. */
|
|
145
|
+
export declare function c4TierText(tier: C4TierElement | undefined): string;
|
|
146
|
+
/**
|
|
147
|
+
* The NAME a component states — its `c4:title` tier if it has one, and the
|
|
148
|
+
* shape's own inner text if it has not.
|
|
149
|
+
*
|
|
150
|
+
* ## No placeholder suppression, unlike the other two
|
|
151
|
+
*
|
|
152
|
+
* A fresh element's title reads `Container`, and that goes into the export
|
|
153
|
+
* verbatim. It is not a prompt standing in for a value the way
|
|
154
|
+
* `[Container: technology]` is: an unnamed container IS a container, and
|
|
155
|
+
* `Container(x, "Container")` is a true statement about a box somebody drew and
|
|
156
|
+
* has not named yet. Blanking it would hand the reader `?` instead — less
|
|
157
|
+
* information, not more honesty. The other two tiers suppress their prompts
|
|
158
|
+
* because "built with a technology called technology" is not true of anything.
|
|
159
|
+
*
|
|
160
|
+
* ## The fallback, and who needs it
|
|
161
|
+
*
|
|
162
|
+
* An element drawn before 28/08/2026 keeps its name in the SHAPE's native inner
|
|
163
|
+
* text, which is where that iteration put it, and it has no title child at all.
|
|
164
|
+
* That is the whole of the compatibility story and it costs one `??`: nothing is
|
|
165
|
+
* migrated, nothing is rewritten, and such an element exports exactly as it
|
|
166
|
+
* always did. The test is EXISTENCE of the tier, not whether it is empty — a
|
|
167
|
+
* component whose title the author deliberately cleared has been cleared, and
|
|
168
|
+
* reaching past it to a shape text that is not there either would say nothing
|
|
169
|
+
* different anyway.
|
|
170
|
+
*/
|
|
171
|
+
export declare function c4StatedName(tiers: C4ComponentTiers, shapeText: unknown): string;
|
|
172
|
+
/**
|
|
173
|
+
* The technology a component STATES — `''` when it states none.
|
|
174
|
+
*
|
|
175
|
+
* The one place the creation placeholder is read as "nothing yet". Every tier
|
|
176
|
+
* exists from the moment a component is drawn, so an element nobody has typed on
|
|
177
|
+
* carries a literal `[Container: technology]`; exporting that as a technology
|
|
178
|
+
* would put the word "technology" in the technology slot of a file somebody is
|
|
179
|
+
* about to paste into a renderer.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately NOT done in `technologyOfTypeLine`, which the commit hook also
|
|
182
|
+
* calls: normalising the placeholder to "nothing" would let a focus-and-blur
|
|
183
|
+
* silently rewrite `[Container: technology]` to `[Container]` and eat the
|
|
184
|
+
* stencil's own prompt. Reading and rewriting are different questions, and only
|
|
185
|
+
* the reader gets to be opinionated.
|
|
186
|
+
*
|
|
187
|
+
* The comparison is on the WORD rather than on the whole line, so it holds for
|
|
188
|
+
* every kind and for a placeholder an author moved brackets around.
|
|
189
|
+
*/
|
|
190
|
+
export declare function c4StatedTechnology(tier: C4TierElement | undefined): string;
|
|
191
|
+
/** The description a component STATES — `''` for the untouched placeholder. */
|
|
192
|
+
export declare function c4StatedDescription(tier: C4TierElement | undefined): string;
|