@formicoidea/labre-framework-c4 0.58.0 → 0.60.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 +3 -2
- package/dist/actions.js +48 -125
- package/dist/component.d.ts +1 -76
- package/dist/component.js +2 -54
- package/dist/consts.d.ts +1 -1
- package/dist/consts.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/interchange-headless.d.ts +1 -0
- package/dist/interchange-headless.js +3 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/tier-boxes.d.ts +76 -0
- package/dist/tier-boxes.js +54 -0
- package/dist/toolbox.d.ts +33 -0
- package/dist/toolbox.js +181 -0
- package/dist/type-line.d.ts +1 -80
- package/dist/type-line.js +3 -84
- package/dist/type-word.d.ts +61 -0
- package/dist/type-word.js +90 -0
- package/package.json +3 -3
package/dist/actions.d.ts
CHANGED
|
@@ -59,9 +59,10 @@ export declare function createC4Board(std: BlockStdScope): void;
|
|
|
59
59
|
* `variantProp` in `background.ts`). The name is document data from that moment
|
|
60
60
|
* on — renaming a boundary never contradicts its variant.
|
|
61
61
|
*
|
|
62
|
-
* ## The role and the variant are written
|
|
62
|
+
* ## The role and the variant are written together, or not at all
|
|
63
63
|
*
|
|
64
|
-
*
|
|
64
|
+
* `c4BoundaryElement` (`toolbox.ts`) is the ONE place either field is written,
|
|
65
|
+
* for this action and for a host with no editor, and it writes both from the same
|
|
65
66
|
* argument: the `variant` the renderer and the exporter read, and the child ROLE
|
|
66
67
|
* the zoom rules read ({@link C4_BOUNDARY_ROLE}). They are two spellings of one
|
|
67
68
|
* fact — which level this frame is drawn at — and nothing downstream can tell
|
package/dist/actions.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
|
-
import { C4BoardElementModel, ConnectorMode,
|
|
3
|
+
import { C4BoardElementModel, ConnectorMode, PointStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
|
|
4
4
|
import { translateKey } from '@formicoidea/labre-core/shared/services';
|
|
5
5
|
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
6
|
-
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
7
6
|
import { GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
|
|
8
|
-
import {
|
|
9
|
-
import { BOARD_REF_HEIGHT, BOARD_REF_WIDTH, BOUNDARY_LABEL, boundaryLabelKey, BOUNDARY_REF_HEIGHT, BOUNDARY_REF_WIDTH, DESCRIPTION_FONT_SIZE, DESCRIPTION_PLACEHOLDER, DESCRIPTION_PLACEHOLDER_KEY, NODE_LABEL, nodeLabelKey, NODE_PALETTE, NODE_SIZE, RELATIONSHIP_STROKE, RELATIONSHIP_WIDTH, TITLE_FONT_SIZE, TYPE_FONT_SIZE, } from './consts.js';
|
|
7
|
+
import { BOARD_REF_HEIGHT, BOARD_REF_WIDTH, BOUNDARY_LABEL, boundaryLabelKey, BOUNDARY_REF_HEIGHT, BOUNDARY_REF_WIDTH, DESCRIPTION_PLACEHOLDER, DESCRIPTION_PLACEHOLDER_KEY, NODE_LABEL, nodeLabelKey, NODE_SIZE, RELATIONSHIP_STROKE, RELATIONSHIP_WIDTH, } from './consts.js';
|
|
10
8
|
import { C4_MERMAID_EXPORT, c4BoardFrom, c4SafeFilename } from './interchange.js';
|
|
11
|
-
import { c4NodeProps } from './presets.js';
|
|
12
9
|
import { C4_BOUNDARY_ROLE, C4_ROLE, c4BoardRoleKey } from './roles.js';
|
|
10
|
+
import { c4BoardElement, c4BoundaryElement, c4NodeElements } from './toolbox.js';
|
|
13
11
|
import { c4TypePlaceholder } from './type-line.js';
|
|
14
12
|
/**
|
|
15
13
|
* Standalone creation/activation actions for the C4 toolbox — the same shape
|
|
@@ -24,38 +22,6 @@ function finish(gfx, id) {
|
|
|
24
22
|
gfx.selection.set({ elements: [id], editing: false });
|
|
25
23
|
// Keep the palette open (native sub-menu behaviour).
|
|
26
24
|
}
|
|
27
|
-
/**
|
|
28
|
-
* One of a component's two written tiers, as a canvas TEXT element.
|
|
29
|
-
*
|
|
30
|
-
* Every style prop is passed EXPLICITLY, and that is not belt and braces: both
|
|
31
|
-
* creation APIs run the new props through `EditPropsStore.applyLastProps('text',
|
|
32
|
-
* …)`, which merges whatever the user last set on a free text element
|
|
33
|
-
* underneath — a 24px face, a colour from another diagram, a left alignment.
|
|
34
|
-
* Explicit props win the merge, so the only ones that survive it are the ones
|
|
35
|
-
* written here. A type line inheriting the colour of the last sticky note
|
|
36
|
-
* somebody typed would be a notation set by accident.
|
|
37
|
-
*
|
|
38
|
-
* `hasMaxWidth` is what keeps a long sentence inside the element instead of
|
|
39
|
-
* running out over the canvas: the box wraps at its own width and grows
|
|
40
|
-
* downward, and the group grows with it, so a component always contains its own
|
|
41
|
-
* words.
|
|
42
|
-
*/
|
|
43
|
-
function addTier(surface, index, role, text, fontSize, fontWeight, color, box) {
|
|
44
|
-
return surface.addElement({
|
|
45
|
-
type: 'text',
|
|
46
|
-
role,
|
|
47
|
-
text,
|
|
48
|
-
index,
|
|
49
|
-
color,
|
|
50
|
-
fontFamily: FontFamily.Inter,
|
|
51
|
-
fontSize,
|
|
52
|
-
fontWeight,
|
|
53
|
-
fontStyle: FontStyle.Normal,
|
|
54
|
-
textAlign: TextAlign.Center,
|
|
55
|
-
hasMaxWidth: true,
|
|
56
|
-
xywh: new Bound(box.x, box.y, box.w, box.h).serialize(),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
25
|
/**
|
|
60
26
|
* Create a C4 component centred on the viewport: the shape, its THREE written
|
|
61
27
|
* tiers, and the GROUP that makes the four one thing.
|
|
@@ -109,64 +75,34 @@ export function createC4Node(std, kind) {
|
|
|
109
75
|
return;
|
|
110
76
|
const { w, h } = NODE_SIZE[kind];
|
|
111
77
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const boxes = c4TierBoxes(kind, x, y, w, h);
|
|
125
|
-
const titleId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE.title,
|
|
126
|
-
// The kind's own label — `Person`, `Web app`. A name and a prompt at once,
|
|
127
|
-
// which is why the exporter writes it through unchanged. Resolved HERE,
|
|
128
|
-
// once, exactly like a BPMN node's caption (`nodeLabelKey`): content from
|
|
129
|
-
// the moment it lands, so a component drawn in a translated host starts in
|
|
130
|
-
// that language.
|
|
131
|
-
translateKey(std, nodeLabelKey(kind), NODE_LABEL[kind]), TITLE_FONT_SIZE,
|
|
132
|
-
// The one tier with weight on it: it is the heading of the box, and at 20px
|
|
133
|
-
// against a 16px sentence the size alone does not carry that.
|
|
134
|
-
FontWeight.SemiBold, paint.text, boxes.title);
|
|
135
|
-
const typeLineId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE['type-line'],
|
|
136
|
-
// Resolved at placement, like the title above: `c4TypePlaceholder` reads
|
|
137
|
-
// through the host's catalogue for both halves of the prompt (the
|
|
138
|
-
// bracketed word AND the technology slot), and `C4TypeLineWatcher`
|
|
139
|
-
// (`node/type-line-watcher.ts`) now recomputes the canonical line the same
|
|
140
|
-
// way on every edit commit, so a translated seed here is no longer
|
|
141
|
-
// reverted to English the first time an author opens and closes the
|
|
142
|
-
// tier's editor without typing anything. `technologyOfTypeLine` and
|
|
143
|
-
// `c4StatedTechnology` (`component.ts`) recognise EITHER the English
|
|
144
|
-
// literal or the host's own resolved word (`type-line.ts`).
|
|
145
|
-
c4TypePlaceholder(kind, std), TYPE_FONT_SIZE, FontWeight.Regular, paint.text, boxes.typeLine);
|
|
146
|
-
const descriptionId = addTier(surface, gfx.layer.generateIndex(), C4_ROLE.description,
|
|
147
|
-
// Resolved at placement like the title above. `c4StatedDescription`
|
|
148
|
-
// (`component.ts`) still compares the stored text against the ENGLISH
|
|
149
|
-
// `DESCRIPTION_PLACEHOLDER` to read "nothing stated yet" — this lot's own
|
|
150
|
-
// French proposal for this key is the identical word ('description'), so
|
|
151
|
-
// the shipped catalogue keeps that comparison true; a host catalogue that
|
|
152
|
-
// chose a different wording would regain the same residual risk the type
|
|
153
|
-
// line carries permanently (see `notes`).
|
|
154
|
-
translateKey(std, DESCRIPTION_PLACEHOLDER_KEY, DESCRIPTION_PLACEHOLDER), DESCRIPTION_FONT_SIZE, FontWeight.Regular, paint.text, boxes.description);
|
|
155
|
-
const groupId = surface.addElement({
|
|
156
|
-
type: 'group',
|
|
157
|
-
index: gfx.layer.generateIndex(),
|
|
158
|
-
// A plain record is a legal `children`: the group's own `propsToY` takes the
|
|
159
|
-
// KEYS and forces every value to `true`.
|
|
160
|
-
children: {
|
|
161
|
-
[shapeId]: true,
|
|
162
|
-
[titleId]: true,
|
|
163
|
-
[typeLineId]: true,
|
|
164
|
-
[descriptionId]: true,
|
|
165
|
-
},
|
|
166
|
-
// No title, deliberately. The group renderer paints one only while the
|
|
167
|
-
// component is selected, and a component announcing itself as "Group 3"
|
|
168
|
-
// above its own name is a label nobody wrote.
|
|
78
|
+
// The five elements come from the ONE assembly a host with no editor also
|
|
79
|
+
// calls (`toolbox.ts`, `docs/adr/0045`), so a component placed here and one
|
|
80
|
+
// placed by a host are the same element. What only an editor has stays
|
|
81
|
+
// here: the viewport centre, the seeds resolved in the host's language
|
|
82
|
+
// (`docs/adr/0023`: a creation site writes the resolved value) and the layer
|
|
83
|
+
// indexes. The indexes are stability statements, not layering ones: the
|
|
84
|
+
// five are written in painting order, the shape first and the words above
|
|
85
|
+
// it, because two elements sharing an index sort by nanoid.
|
|
86
|
+
const elements = c4NodeElements(kind, { x: cx - w / 2, y: cy - h / 2 }, {
|
|
87
|
+
title: translateKey(std, nodeLabelKey(kind), NODE_LABEL[kind]),
|
|
88
|
+
typeLine: c4TypePlaceholder(kind, std),
|
|
89
|
+
description: translateKey(std, DESCRIPTION_PLACEHOLDER_KEY, DESCRIPTION_PLACEHOLDER),
|
|
169
90
|
});
|
|
91
|
+
const minted = new Map();
|
|
92
|
+
for (const { id: local, ...props } of elements) {
|
|
93
|
+
if (props.type === 'group') {
|
|
94
|
+
// A plain record is a legal `children`: the group's own `propsToY` takes
|
|
95
|
+
// the KEYS and forces every value to `true`.
|
|
96
|
+
props.children = Object.fromEntries(Object.keys(props.children).map(child => [
|
|
97
|
+
minted.get(child) ?? child,
|
|
98
|
+
true,
|
|
99
|
+
]));
|
|
100
|
+
}
|
|
101
|
+
minted.set(local, surface.addElement({ ...props, index: gfx.layer.generateIndex() }));
|
|
102
|
+
}
|
|
103
|
+
const groupId = minted.get(`${kind}:group`);
|
|
104
|
+
if (!groupId)
|
|
105
|
+
return;
|
|
170
106
|
// The GROUP is what the gesture produced, so the group is what is selected: a
|
|
171
107
|
// click selects the component, a double-click descends into the tier under
|
|
172
108
|
// the pointer. That is the whole recette in one selection.
|
|
@@ -179,21 +115,15 @@ export function createC4Board(std) {
|
|
|
179
115
|
if (!surface)
|
|
180
116
|
return;
|
|
181
117
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
182
|
-
const id =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// re-resolved. The key is the BOARD ROLE's: the two are the same noun, and
|
|
192
|
-
// a second key would ask a host to word "C4 diagram" twice.
|
|
193
|
-
name: translateKey(std, c4BoardRoleKey, 'C4 diagram'),
|
|
194
|
-
xywh: new Bound(cx - BOARD_REF_WIDTH / 2, cy - BOARD_REF_HEIGHT / 2, BOARD_REF_WIDTH, BOARD_REF_HEIGHT).serialize(),
|
|
195
|
-
});
|
|
196
|
-
finish(gfx, id);
|
|
118
|
+
const { id: _local, ...props } = c4BoardElement({ x: cx - BOARD_REF_WIDTH / 2, y: cy - BOARD_REF_HEIGHT / 2 },
|
|
119
|
+
// The sheet's own name, written HERE rather than left to the model's
|
|
120
|
+
// default so that a board drawn in a translated host starts in that
|
|
121
|
+
// language (#183). It is content from that moment on — the title is
|
|
122
|
+
// editable on a double-click — so it is asked for once and never
|
|
123
|
+
// re-resolved. The key is the BOARD ROLE's: the two are the same noun, and
|
|
124
|
+
// a second key would ask a host to word "C4 diagram" twice.
|
|
125
|
+
translateKey(std, c4BoardRoleKey, 'C4 diagram'));
|
|
126
|
+
finish(gfx, surface.addElement(props));
|
|
197
127
|
}
|
|
198
128
|
/**
|
|
199
129
|
* Create a C4 boundary centred on the viewport.
|
|
@@ -204,9 +134,10 @@ export function createC4Board(std) {
|
|
|
204
134
|
* `variantProp` in `background.ts`). The name is document data from that moment
|
|
205
135
|
* on — renaming a boundary never contradicts its variant.
|
|
206
136
|
*
|
|
207
|
-
* ## The role and the variant are written
|
|
137
|
+
* ## The role and the variant are written together, or not at all
|
|
208
138
|
*
|
|
209
|
-
*
|
|
139
|
+
* `c4BoundaryElement` (`toolbox.ts`) is the ONE place either field is written,
|
|
140
|
+
* for this action and for a host with no editor, and it writes both from the same
|
|
210
141
|
* argument: the `variant` the renderer and the exporter read, and the child ROLE
|
|
211
142
|
* the zoom rules read ({@link C4_BOUNDARY_ROLE}). They are two spellings of one
|
|
212
143
|
* fact — which level this frame is drawn at — and nothing downstream can tell
|
|
@@ -222,20 +153,12 @@ export function createC4Boundary(std, variant) {
|
|
|
222
153
|
if (!surface)
|
|
223
154
|
return;
|
|
224
155
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
225
|
-
const id =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// Resolved HERE, once — the boundary's name is exported verbatim
|
|
232
|
-
// (`export.ts`) with no placeholder comparison, so it is as safe to
|
|
233
|
-
// translate as a component's title.
|
|
234
|
-
name: translateKey(std, boundaryLabelKey(variant), BOUNDARY_LABEL[variant]),
|
|
235
|
-
variant,
|
|
236
|
-
xywh: new Bound(cx - BOUNDARY_REF_WIDTH / 2, cy - BOUNDARY_REF_HEIGHT / 2, BOUNDARY_REF_WIDTH, BOUNDARY_REF_HEIGHT).serialize(),
|
|
237
|
-
});
|
|
238
|
-
finish(gfx, id);
|
|
156
|
+
const { id: _local, ...props } = c4BoundaryElement(variant, { x: cx - BOUNDARY_REF_WIDTH / 2, y: cy - BOUNDARY_REF_HEIGHT / 2 },
|
|
157
|
+
// Resolved HERE, once — the boundary's name is exported verbatim
|
|
158
|
+
// (`export.ts`) with no placeholder comparison, so it is as safe to
|
|
159
|
+
// translate as a component's title.
|
|
160
|
+
translateKey(std, boundaryLabelKey(variant), BOUNDARY_LABEL[variant]));
|
|
161
|
+
finish(gfx, surface.addElement(props));
|
|
239
162
|
}
|
|
240
163
|
/**
|
|
241
164
|
* Arm the native connector tool, pre-styled for a C4 relationship: STRAIGHT,
|
package/dist/component.d.ts
CHANGED
|
@@ -1,79 +1,4 @@
|
|
|
1
|
-
|
|
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;
|
|
1
|
+
export { type C4TierBox, type C4TierBoxes, c4TierBoxes } from './tier-boxes.js';
|
|
77
2
|
/**
|
|
78
3
|
* The little a resolution needs to know about a text element — an id, a role,
|
|
79
4
|
* and whatever it says.
|
package/dist/component.js
CHANGED
|
@@ -1,59 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DESCRIPTION_PLACEHOLDER } from './consts.js';
|
|
2
2
|
import { C4_ROLE } from './roles.js';
|
|
3
3
|
import { technologyOfTypeLine, TYPE_TECHNOLOGY_PLACEHOLDER } from './type-line.js';
|
|
4
|
-
|
|
5
|
-
* The three text boxes of a component, laid out against the node's own box.
|
|
6
|
-
*
|
|
7
|
-
* ## The rhythm
|
|
8
|
-
*
|
|
9
|
-
* A margin, the name over two lines, a small gap, the type line, a wider gap,
|
|
10
|
-
* the description over two, and the same margin again — which is exactly what
|
|
11
|
-
* {@link NODE_BOX} is tall enough for, because the box is derived from this
|
|
12
|
-
* stack rather than the stack fitted into the box. So the default element is
|
|
13
|
-
* neither cramped nor half empty, and the two gaps say what they mean: the name
|
|
14
|
-
* and its type line are one heading, the sentence under them is a second
|
|
15
|
-
* statement.
|
|
16
|
-
*
|
|
17
|
-
* The tiers are stacked by walking DOWN — each one placed under the last plus
|
|
18
|
-
* its gap — rather than by six independent offsets. Six offsets is six chances
|
|
19
|
-
* for two tiers to overlap; a walk cannot produce one.
|
|
20
|
-
*
|
|
21
|
-
* ## The person
|
|
22
|
-
*
|
|
23
|
-
* `bodyTop` is the one asymmetry and it is the stencil's: a person's head stands
|
|
24
|
-
* clear ABOVE a body of the standard height, and its words are laid out in the
|
|
25
|
-
* BODY (`v:textRect` is the body box exactly), so the whole stack starts below
|
|
26
|
-
* the head rather than across it.
|
|
27
|
-
*
|
|
28
|
-
* ## What this is not
|
|
29
|
-
*
|
|
30
|
-
* A creation-time answer and nothing more. The tiers are real elements from the
|
|
31
|
-
* moment they are drawn: an author who moves one has moved it, and nothing here
|
|
32
|
-
* runs again to put it back. It is also proportional in the one direction that
|
|
33
|
-
* matters — a node dragged taller keeps its margins where they were, because
|
|
34
|
-
* they are absolutes; only the person's head, which is a picture, scales.
|
|
35
|
-
*/
|
|
36
|
-
export function c4TierBoxes(kind, x, y, w, h) {
|
|
37
|
-
const bodyTop = kind === 'person' || kind === 'person-ext' ? h * PERSON_BODY_TOP : 0;
|
|
38
|
-
const inset = w * TIER_SIDE_INSET;
|
|
39
|
-
const width = w - inset * 2;
|
|
40
|
-
let top = bodyTop + TIER_MARGIN;
|
|
41
|
-
const tier = (fontSize, lines, gapAfter) => {
|
|
42
|
-
const box = {
|
|
43
|
-
x: x + inset,
|
|
44
|
-
y: y + top,
|
|
45
|
-
w: width,
|
|
46
|
-
h: fontSize * TIER_LINE_HEIGHT * lines,
|
|
47
|
-
};
|
|
48
|
-
top += box.h + gapAfter;
|
|
49
|
-
return box;
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
title: tier(TITLE_FONT_SIZE, TITLE_LINES, TITLE_TYPE_GAP),
|
|
53
|
-
typeLine: tier(TYPE_FONT_SIZE, 1, TYPE_DESCRIPTION_GAP),
|
|
54
|
-
description: tier(DESCRIPTION_FONT_SIZE, DESCRIPTION_LINES, 0),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
4
|
+
export { c4TierBoxes } from './tier-boxes.js';
|
|
57
5
|
/**
|
|
58
6
|
* The tiers of the component a node belongs to — `{}` when it belongs to none.
|
|
59
7
|
*
|
package/dist/consts.d.ts
CHANGED
|
@@ -303,7 +303,7 @@ export declare const BOARD_MARGIN = 24;
|
|
|
303
303
|
* board's metrics, and so the declaration keeps reading it from where it always
|
|
304
304
|
* did — under its own name, since a margin that is a painted band is a band.
|
|
305
305
|
*/
|
|
306
|
-
export { C4_BOARD_TITLE_BAND_HEIGHT } from '@formicoidea/labre-core/model';
|
|
306
|
+
export { C4_BOARD_TITLE_BAND_HEIGHT } from '@formicoidea/labre-core/model/headless';
|
|
307
307
|
/**
|
|
308
308
|
* The boundary's frame and its name, at the stencil's own values ×2 (`.st20`
|
|
309
309
|
* and `.st8` in the reference model).
|
package/dist/consts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/
|
|
1
|
+
import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/headless';
|
|
2
2
|
/**
|
|
3
3
|
* Visual constants for the C4 pack.
|
|
4
4
|
*
|
|
@@ -349,7 +349,7 @@ export const BOARD_MARGIN = 24;
|
|
|
349
349
|
* board's metrics, and so the declaration keeps reading it from where it always
|
|
350
350
|
* did — under its own name, since a margin that is a painted band is a band.
|
|
351
351
|
*/
|
|
352
|
-
export { C4_BOARD_TITLE_BAND_HEIGHT } from '@formicoidea/labre-core/model';
|
|
352
|
+
export { C4_BOARD_TITLE_BAND_HEIGHT } from '@formicoidea/labre-core/model/headless';
|
|
353
353
|
/* ── The boundary ──────────────────────────────────────────────────────── */
|
|
354
354
|
/**
|
|
355
355
|
* The boundary's frame and its name, at the stencil's own values ×2 (`.st20`
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { C4_ROLE, C4_ROLE_OF_KIND, C4_ROLES, type C4Role, type C4RoleId, } from
|
|
|
10
10
|
export { c4BoardsForExport, c4ExportBoardOf } from './actions.js';
|
|
11
11
|
export { type C4ExportBoard, C4_MERMAID_OF_KIND, type C4MermaidMapping, exportC4Mermaid, toMermaidAlias, toMermaidText, } from './export.js';
|
|
12
12
|
export { C4_INTERCHANGE, C4_MERMAID_EXPORT, C4_MERMAID_EXTENSION, C4_MERMAID_FORMAT, C4_MERMAID_MIME, c4BoardFrom, c4SafeFilename, } from './interchange.js';
|
|
13
|
+
export { C4_TOOLBOX, c4BoardElement, c4BoundaryElement, c4NodeElements, type C4NodeTexts, c4RelationshipElement, } from './toolbox.js';
|
|
13
14
|
export { C4_ELEMENT_MATRIX, C4_RELATIONSHIP_MATRIX, C4_RULES, } from './rules.js';
|
|
14
15
|
export { c4CommandIcons, c4Commands } from './commands.js';
|
|
15
16
|
export { c4TranslationEntries } from './translations.js';
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ export { C4_MERMAID_OF_KIND, exportC4Mermaid, toMermaidAlias, toMermaidText, } f
|
|
|
44
44
|
// mermaid OUT. Exported whole so a host can ask what C4 can read and write
|
|
45
45
|
// without mounting an editor, and call it without one either.
|
|
46
46
|
export { C4_INTERCHANGE, C4_MERMAID_EXPORT, C4_MERMAID_EXTENSION, C4_MERMAID_FORMAT, C4_MERMAID_MIME, c4BoardFrom, c4SafeFilename, } from './interchange.js';
|
|
47
|
+
// The toolbar as pure builders (`docs/adr/0045`), also on `./interchange`.
|
|
48
|
+
export { C4_TOOLBOX, c4BoardElement, c4BoundaryElement, c4NodeElements, c4RelationshipElement, } from './toolbox.js';
|
|
47
49
|
export { C4_ELEMENT_MATRIX, C4_RELATIONSHIP_MATRIX, C4_RULES, } from './rules.js';
|
|
48
50
|
// The toolbox, for the host that composes the command registry and the
|
|
49
51
|
// translation-key manifest out of the frameworks it installed (see
|
|
@@ -35,3 +35,4 @@ export declare const C4_MERMAID_FORMAT: InterchangeFormat;
|
|
|
35
35
|
* `safe-filename-parity.unit.spec.ts`.
|
|
36
36
|
*/
|
|
37
37
|
export declare function c4SafeFilename(raw: string | undefined): string;
|
|
38
|
+
export { C4_TOOLBOX, c4BoardElement, c4BoundaryElement, c4NodeElements, type C4NodeTexts, c4RelationshipElement, } from './toolbox.js';
|
|
@@ -41,3 +41,6 @@ export const C4_MERMAID_FORMAT = {
|
|
|
41
41
|
export function c4SafeFilename(raw) {
|
|
42
42
|
return safeFilename(raw, 'diagram');
|
|
43
43
|
}
|
|
44
|
+
// The toolbar as pure builders (`docs/adr/0045`): what a host with no editor
|
|
45
|
+
// places to draw the very elements a user places from the toolbar.
|
|
46
|
+
export { C4_TOOLBOX, c4BoardElement, c4BoundaryElement, c4NodeElements, c4RelationshipElement, } from './toolbox.js';
|
package/dist/presets.d.ts
CHANGED
package/dist/presets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FontFamily, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { FontFamily, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model/headless';
|
|
2
2
|
import { NODE_PALETTE, NODE_RADIUS, NODE_STROKE_WIDTH, TITLE_FONT_SIZE, } from './consts.js';
|
|
3
3
|
import { C4_ROLE_OF_KIND } from './roles.js';
|
|
4
4
|
/**
|
|
@@ -0,0 +1,76 @@
|
|
|
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;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DESCRIPTION_FONT_SIZE, DESCRIPTION_LINES, PERSON_BODY_TOP, TIER_LINE_HEIGHT, TIER_MARGIN, TIER_SIDE_INSET, TITLE_FONT_SIZE, TITLE_LINES, TITLE_TYPE_GAP, TYPE_DESCRIPTION_GAP, TYPE_FONT_SIZE, } from './consts.js';
|
|
2
|
+
/**
|
|
3
|
+
* The three text boxes of a component, laid out against the node's own box.
|
|
4
|
+
*
|
|
5
|
+
* ## The rhythm
|
|
6
|
+
*
|
|
7
|
+
* A margin, the name over two lines, a small gap, the type line, a wider gap,
|
|
8
|
+
* the description over two, and the same margin again — which is exactly what
|
|
9
|
+
* {@link NODE_BOX} is tall enough for, because the box is derived from this
|
|
10
|
+
* stack rather than the stack fitted into the box. So the default element is
|
|
11
|
+
* neither cramped nor half empty, and the two gaps say what they mean: the name
|
|
12
|
+
* and its type line are one heading, the sentence under them is a second
|
|
13
|
+
* statement.
|
|
14
|
+
*
|
|
15
|
+
* The tiers are stacked by walking DOWN — each one placed under the last plus
|
|
16
|
+
* its gap — rather than by six independent offsets. Six offsets is six chances
|
|
17
|
+
* for two tiers to overlap; a walk cannot produce one.
|
|
18
|
+
*
|
|
19
|
+
* ## The person
|
|
20
|
+
*
|
|
21
|
+
* `bodyTop` is the one asymmetry and it is the stencil's: a person's head stands
|
|
22
|
+
* clear ABOVE a body of the standard height, and its words are laid out in the
|
|
23
|
+
* BODY (`v:textRect` is the body box exactly), so the whole stack starts below
|
|
24
|
+
* the head rather than across it.
|
|
25
|
+
*
|
|
26
|
+
* ## What this is not
|
|
27
|
+
*
|
|
28
|
+
* A creation-time answer and nothing more. The tiers are real elements from the
|
|
29
|
+
* moment they are drawn: an author who moves one has moved it, and nothing here
|
|
30
|
+
* runs again to put it back. It is also proportional in the one direction that
|
|
31
|
+
* matters — a node dragged taller keeps its margins where they were, because
|
|
32
|
+
* they are absolutes; only the person's head, which is a picture, scales.
|
|
33
|
+
*/
|
|
34
|
+
export function c4TierBoxes(kind, x, y, w, h) {
|
|
35
|
+
const bodyTop = kind === 'person' || kind === 'person-ext' ? h * PERSON_BODY_TOP : 0;
|
|
36
|
+
const inset = w * TIER_SIDE_INSET;
|
|
37
|
+
const width = w - inset * 2;
|
|
38
|
+
let top = bodyTop + TIER_MARGIN;
|
|
39
|
+
const tier = (fontSize, lines, gapAfter) => {
|
|
40
|
+
const box = {
|
|
41
|
+
x: x + inset,
|
|
42
|
+
y: y + top,
|
|
43
|
+
w: width,
|
|
44
|
+
h: fontSize * TIER_LINE_HEIGHT * lines,
|
|
45
|
+
};
|
|
46
|
+
top += box.h + gapAfter;
|
|
47
|
+
return box;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
title: tier(TITLE_FONT_SIZE, TITLE_LINES, TITLE_TYPE_GAP),
|
|
51
|
+
typeLine: tier(TYPE_FONT_SIZE, 1, TYPE_DESCRIPTION_GAP),
|
|
52
|
+
description: tier(DESCRIPTION_FONT_SIZE, DESCRIPTION_LINES, 0),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FrameworkToolbox, SerializedElementProps, ToolboxPoint } from '@formicoidea/labre-core/interchange';
|
|
2
|
+
import type { C4BoundaryVariant, C4NodeKind } from '@formicoidea/labre-core/model';
|
|
3
|
+
/** The text slots of a component, in reading order. */
|
|
4
|
+
export interface C4NodeTexts {
|
|
5
|
+
/** The name; the kind's English label when absent. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** The technology of a container or component (`Java`, `PostgreSQL`). */
|
|
8
|
+
technology?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The whole middle tier, verbatim: the editor's creation site passes the
|
|
11
|
+
* prompt resolved in the host's language (`c4TypePlaceholder`). Wins over
|
|
12
|
+
* `technology`.
|
|
13
|
+
*/
|
|
14
|
+
typeLine?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A C4 component at `at` (its top-left): the shape, its three tiers and the
|
|
19
|
+
* group that makes them one thing — the five elements `createC4Node`
|
|
20
|
+
* documents. Ids are local (`<local>:shape`…), the group last.
|
|
21
|
+
*/
|
|
22
|
+
export declare function c4NodeElements(kind: C4NodeKind, at: ToolboxPoint, texts?: C4NodeTexts, local?: string): SerializedElementProps[];
|
|
23
|
+
/** The sheet one C4 diagram is drawn on, at `at`. */
|
|
24
|
+
export declare function c4BoardElement(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps;
|
|
25
|
+
/**
|
|
26
|
+
* A boundary at `at`. The variant and the child role are written together,
|
|
27
|
+
* from the same argument, as `createC4Boundary` requires.
|
|
28
|
+
*/
|
|
29
|
+
export declare function c4BoundaryElement(variant: C4BoundaryVariant, at: ToolboxPoint, name?: string, local?: string): SerializedElementProps;
|
|
30
|
+
/** The connector the relationship tool is armed with, between two ends. */
|
|
31
|
+
export declare function c4RelationshipElement(from: string, to: string, label?: string): SerializedElementProps;
|
|
32
|
+
/** Every item of the C4 toolbar, in the toolbar's order. */
|
|
33
|
+
export declare const C4_TOOLBOX: FrameworkToolbox;
|
package/dist/toolbox.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ConnectorMode, FontFamily, FontStyle, FontWeight, PointStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model/headless';
|
|
2
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
|
+
import { c4CommandsManifest } from './commands-manifest.js';
|
|
4
|
+
import { c4TierBoxes } from './tier-boxes.js';
|
|
5
|
+
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';
|
|
6
|
+
import { c4NodeProps } from './presets.js';
|
|
7
|
+
import { C4_BOUNDARY_ROLE, C4_ROLE } from './roles.js';
|
|
8
|
+
import { C4_TYPE_PLACEHOLDER, C4_TYPE_WORD, typeLineOf } from './type-word.js';
|
|
9
|
+
/**
|
|
10
|
+
* C4's toolbar as pure builders (`docs/adr/0045`): what `createC4Node`,
|
|
11
|
+
* `createC4Board`, `createC4Boundary` and the relationship tool place, with no
|
|
12
|
+
* editor behind them. The actions call these, so a component built here and
|
|
13
|
+
* one placed from the toolbar are the same five elements.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* One of a component's three written tiers, as a canvas TEXT element.
|
|
17
|
+
*
|
|
18
|
+
* Every style prop is passed EXPLICITLY, and that is not belt and braces: both
|
|
19
|
+
* creation APIs run the new props through `EditPropsStore.applyLastProps('text',
|
|
20
|
+
* …)`, which merges whatever the user last set on a free text element
|
|
21
|
+
* underneath — a 24px face, a colour from another diagram, a left alignment.
|
|
22
|
+
* Explicit props win the merge, so the only ones that survive it are the ones
|
|
23
|
+
* written here. A type line inheriting the colour of the last sticky note
|
|
24
|
+
* somebody typed would be a notation set by accident.
|
|
25
|
+
*
|
|
26
|
+
* `hasMaxWidth` is what keeps a long sentence inside the element instead of
|
|
27
|
+
* running out over the canvas: the box wraps at its own width and grows
|
|
28
|
+
* downward, and the group grows with it, so a component always contains its own
|
|
29
|
+
* words.
|
|
30
|
+
*/
|
|
31
|
+
function tier(id, role, text, fontSize, fontWeight, color, box) {
|
|
32
|
+
return {
|
|
33
|
+
type: 'text',
|
|
34
|
+
id,
|
|
35
|
+
role,
|
|
36
|
+
text,
|
|
37
|
+
color,
|
|
38
|
+
fontFamily: FontFamily.Inter,
|
|
39
|
+
fontSize,
|
|
40
|
+
fontWeight,
|
|
41
|
+
fontStyle: FontStyle.Normal,
|
|
42
|
+
textAlign: TextAlign.Center,
|
|
43
|
+
hasMaxWidth: true,
|
|
44
|
+
xywh: new Bound(box.x, box.y, box.w, box.h).serialize(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A C4 component at `at` (its top-left): the shape, its three tiers and the
|
|
49
|
+
* group that makes them one thing — the five elements `createC4Node`
|
|
50
|
+
* documents. Ids are local (`<local>:shape`…), the group last.
|
|
51
|
+
*/
|
|
52
|
+
export function c4NodeElements(kind, at, texts = {}, local = kind) {
|
|
53
|
+
const { w, h } = NODE_SIZE[kind];
|
|
54
|
+
const paint = NODE_PALETTE[kind];
|
|
55
|
+
const boxes = c4TierBoxes(kind, at.x, at.y, w, h);
|
|
56
|
+
const id = (part) => `${local}:${part}`;
|
|
57
|
+
const technology = texts.technology?.trim();
|
|
58
|
+
const members = [
|
|
59
|
+
{
|
|
60
|
+
...c4NodeProps(kind, { xywh: new Bound(at.x, at.y, w, h).serialize() }),
|
|
61
|
+
id: id('shape'),
|
|
62
|
+
},
|
|
63
|
+
tier(id('title'), C4_ROLE.title, texts.title?.trim() || NODE_LABEL[kind], TITLE_FONT_SIZE, FontWeight.SemiBold, paint.text, boxes.title),
|
|
64
|
+
tier(id('type-line'), C4_ROLE['type-line'], texts.typeLine ??
|
|
65
|
+
(technology
|
|
66
|
+
? typeLineOf(C4_TYPE_WORD[kind], technology)
|
|
67
|
+
: C4_TYPE_PLACEHOLDER[kind]), TYPE_FONT_SIZE, FontWeight.Regular, paint.text, boxes.typeLine),
|
|
68
|
+
tier(id('description'), C4_ROLE.description, texts.description?.trim() || DESCRIPTION_PLACEHOLDER, DESCRIPTION_FONT_SIZE, FontWeight.Regular, paint.text, boxes.description),
|
|
69
|
+
];
|
|
70
|
+
return [
|
|
71
|
+
...members,
|
|
72
|
+
{
|
|
73
|
+
type: 'group',
|
|
74
|
+
id: id('group'),
|
|
75
|
+
children: Object.fromEntries(members.map(m => [m.id, true])),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
/** The sheet one C4 diagram is drawn on, at `at`. */
|
|
80
|
+
export function c4BoardElement(at, name = 'C4 diagram', local = 'board') {
|
|
81
|
+
return {
|
|
82
|
+
type: 'c4Board',
|
|
83
|
+
id: local,
|
|
84
|
+
// The FRAME the elements are drawn on, and a role of its own: a rule
|
|
85
|
+
// written on the artefacts must never fall on the sheet holding them.
|
|
86
|
+
role: C4_ROLE.board,
|
|
87
|
+
name,
|
|
88
|
+
xywh: new Bound(at.x, at.y, BOARD_REF_WIDTH, BOARD_REF_HEIGHT).serialize(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A boundary at `at`. The variant and the child role are written together,
|
|
93
|
+
* from the same argument, as `createC4Boundary` requires.
|
|
94
|
+
*/
|
|
95
|
+
export function c4BoundaryElement(variant, at, name = BOUNDARY_LABEL[variant], local = `${variant}-boundary`) {
|
|
96
|
+
return {
|
|
97
|
+
type: 'c4Boundary',
|
|
98
|
+
id: local,
|
|
99
|
+
// The CHILD role matching the variant — never the parent, which is the
|
|
100
|
+
// role of a boundary drawn before the split and of nothing created now.
|
|
101
|
+
role: C4_BOUNDARY_ROLE[variant],
|
|
102
|
+
name,
|
|
103
|
+
variant,
|
|
104
|
+
xywh: new Bound(at.x, at.y, BOUNDARY_REF_WIDTH, BOUNDARY_REF_HEIGHT).serialize(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The connector the relationship tool is armed with, between two ends. */
|
|
108
|
+
export function c4RelationshipElement(from, to, label) {
|
|
109
|
+
return {
|
|
110
|
+
type: 'connector',
|
|
111
|
+
mode: ConnectorMode.Straight,
|
|
112
|
+
role: C4_ROLE.relationship,
|
|
113
|
+
stroke: RELATIONSHIP_STROKE,
|
|
114
|
+
strokeStyle: StrokeStyle.Dash,
|
|
115
|
+
strokeWidth: RELATIONSHIP_WIDTH,
|
|
116
|
+
frontEndpointStyle: PointStyle.None,
|
|
117
|
+
rearEndpointStyle: PointStyle.Triangle,
|
|
118
|
+
source: { id: from },
|
|
119
|
+
target: { id: to },
|
|
120
|
+
...(label?.trim() ? { text: label.trim() } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** The toolbar command's label, so a host names an item as the toolbar does. */
|
|
124
|
+
function label(commandId) {
|
|
125
|
+
const row = c4CommandsManifest.find(r => r.id === commandId);
|
|
126
|
+
return {
|
|
127
|
+
labelKey: row?.labelKey ?? commandId,
|
|
128
|
+
labelFallback: row?.labelFallback ?? commandId,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const NODE_COMMANDS = {
|
|
132
|
+
person: 'c4.addPerson',
|
|
133
|
+
'person-ext': 'c4.addPersonExt',
|
|
134
|
+
system: 'c4.addSystem',
|
|
135
|
+
'system-ext': 'c4.addSystemExt',
|
|
136
|
+
container: 'c4.addContainer',
|
|
137
|
+
component: 'c4.addComponent',
|
|
138
|
+
database: 'c4.addDatabase',
|
|
139
|
+
mobile: 'c4.addMobile',
|
|
140
|
+
browser: 'c4.addBrowser',
|
|
141
|
+
};
|
|
142
|
+
const nodeItem = (kind) => ({
|
|
143
|
+
id: kind,
|
|
144
|
+
...label(NODE_COMMANDS[kind]),
|
|
145
|
+
texts: ['title', 'technology', 'description'],
|
|
146
|
+
size: NODE_SIZE[kind],
|
|
147
|
+
build: (at, texts, local) => c4NodeElements(kind, at, texts, local),
|
|
148
|
+
});
|
|
149
|
+
const boundaryItem = (variant) => ({
|
|
150
|
+
id: `${variant}-boundary`,
|
|
151
|
+
...label(variant === 'system' ? 'c4.addSystemBoundary' : 'c4.addContainerBoundary'),
|
|
152
|
+
texts: ['name'],
|
|
153
|
+
size: { w: BOUNDARY_REF_WIDTH, h: BOUNDARY_REF_HEIGHT },
|
|
154
|
+
build: (at, texts, local) => [
|
|
155
|
+
c4BoundaryElement(variant, at, texts?.name?.trim() || undefined, local),
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
const relationship = {
|
|
159
|
+
id: 'relationship',
|
|
160
|
+
...label('c4.relationshipTool'),
|
|
161
|
+
build: c4RelationshipElement,
|
|
162
|
+
};
|
|
163
|
+
/** Every item of the C4 toolbar, in the toolbar's order. */
|
|
164
|
+
export const C4_TOOLBOX = {
|
|
165
|
+
framework: 'c4',
|
|
166
|
+
items: [
|
|
167
|
+
{
|
|
168
|
+
id: 'board',
|
|
169
|
+
...label('c4.addBoard'),
|
|
170
|
+
texts: ['name'],
|
|
171
|
+
size: { w: BOARD_REF_WIDTH, h: BOARD_REF_HEIGHT },
|
|
172
|
+
build: (at, texts, local) => [
|
|
173
|
+
c4BoardElement(at, texts?.name?.trim() || undefined, local),
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
...Object.keys(NODE_COMMANDS).map(nodeItem),
|
|
177
|
+
boundaryItem('system'),
|
|
178
|
+
boundaryItem('container'),
|
|
179
|
+
],
|
|
180
|
+
links: [relationship],
|
|
181
|
+
};
|
package/dist/type-line.d.ts
CHANGED
|
@@ -1,48 +1,7 @@
|
|
|
1
1
|
import type { C4NodeKind } from '@formicoidea/labre-core/model';
|
|
2
2
|
import { type ChromeWording } from '@formicoidea/labre-core/shared/services';
|
|
3
3
|
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
4
|
-
|
|
5
|
-
* The middle tier of a C4 element's label — `[Person]`, `[Software System]`,
|
|
6
|
-
* `[Container: Java]` — SEMI-DERIVED: the word comes from the kind, the
|
|
7
|
-
* technology from the author.
|
|
8
|
-
*
|
|
9
|
-
* ## Semi-derived, and where the line actually lives
|
|
10
|
-
*
|
|
11
|
-
* The line is a real canvas TEXT element now (PO recette, 28/08/2026): a C4
|
|
12
|
-
* component is the shape and its own words, grouped, and every tier is edited in
|
|
13
|
-
* place like any other text on the canvas rather than typed into a form. So the
|
|
14
|
-
* string IS stored — but only half of it is the author's. The bracketed word is
|
|
15
|
-
* the notation's and comes from `kind`, which is what the renderer paints and
|
|
16
|
-
* what the exporter maps; letting an author retype it would be letting the words
|
|
17
|
-
* disagree with the picture.
|
|
18
|
-
*
|
|
19
|
-
* {@link normalizeC4TypeLine} is what keeps the halves apart. It runs when an
|
|
20
|
-
* edit COMMITS, reads whatever was typed for the one thing the notation leaves
|
|
21
|
-
* to the author — the technology — and writes the canonical line back. Type
|
|
22
|
-
* `Java` and the line becomes `[Container: Java]`; type `[Database: Java]` on a
|
|
23
|
-
* cylinder and it becomes `[Container: Java]`, because a database IS a container
|
|
24
|
-
* and the stencil says so.
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* The base word each kind is announced by, verbatim from the stencil's own
|
|
28
|
-
* `<desc>` strings (`C4Model_default.svg`, the PO's reference model).
|
|
29
|
-
*
|
|
30
|
-
* Two readings are worth spelling out, because both look like mistakes:
|
|
31
|
-
*
|
|
32
|
-
* - `database` says **Container**, not "Database". The stencil labels the
|
|
33
|
-
* cylinder `[Container: technology]` exactly as it labels the plain box: a
|
|
34
|
-
* database is a CONTAINER, and the cylinder is a picture of one, not a fourth
|
|
35
|
-
* level. (The mermaid export still emits `ContainerDb` — that is the one
|
|
36
|
-
* specialisation mermaid's own grammar draws, and it is a different question
|
|
37
|
-
* from what the box says on the canvas.)
|
|
38
|
-
* - an `-ext` variant says the same word as the kind it is external to. What
|
|
39
|
-
* "external" changes is the COLOUR — grey — and the stencil's gray sheet
|
|
40
|
-
* carries the identical `[Person]` / `[Software System]` wording.
|
|
41
|
-
*
|
|
42
|
-
* `Record<C4NodeKind, …>` and therefore compile-total: a kind added to the model
|
|
43
|
-
* without a word to announce it fails the build here.
|
|
44
|
-
*/
|
|
45
|
-
export declare const C4_TYPE_WORD: Record<C4NodeKind, string>;
|
|
4
|
+
export { C4_TYPE_PLACEHOLDER, C4_TYPE_TAKES_TECHNOLOGY, C4_TYPE_WORD, TYPE_TECHNOLOGY_PLACEHOLDER, } from './type-word.js';
|
|
46
5
|
export declare const C4_TYPE_WORD_KEY: Record<C4NodeKind, string>;
|
|
47
6
|
/**
|
|
48
7
|
* The type line as it is drawn, brackets included.
|
|
@@ -108,14 +67,6 @@ export declare function technologyOfTypeLine(text: string | null | undefined, st
|
|
|
108
67
|
* has an `std` to give it.
|
|
109
68
|
*/
|
|
110
69
|
export declare function normalizeC4TypeLine(kind: C4NodeKind, rawText: string | null | undefined, std?: BlockStdScope): string;
|
|
111
|
-
/**
|
|
112
|
-
* The word a fresh type line prompts the author with, in the technology's slot.
|
|
113
|
-
*
|
|
114
|
-
* The stencil's own: its container reads `[Container: technology]` before
|
|
115
|
-
* anybody fills it in. A prompt rather than a value — see
|
|
116
|
-
* {@link C4_TYPE_PLACEHOLDER}.
|
|
117
|
-
*/
|
|
118
|
-
export declare const TYPE_TECHNOLOGY_PLACEHOLDER = "technology";
|
|
119
70
|
/**
|
|
120
71
|
* {@link TYPE_TECHNOLOGY_PLACEHOLDER}'s own key — see the long comment on
|
|
121
72
|
* {@link C4_TYPE_WORD_KEY} for why this lot's shipped French proposal keeps
|
|
@@ -129,36 +80,6 @@ export declare const C4_TECHNOLOGY_PLACEHOLDER_KEY = "com.labre.c4.type.technolo
|
|
|
129
80
|
* commit the watcher normalizes).
|
|
130
81
|
*/
|
|
131
82
|
export declare const C4_TYPE_LINE_WORDINGS: readonly ChromeWording[];
|
|
132
|
-
/**
|
|
133
|
-
* Which kinds announce a technology in their type line AT ALL.
|
|
134
|
-
*
|
|
135
|
-
* The notation's own division and not mermaid's, though the two agree: a person
|
|
136
|
-
* is not built with a technology and a software system's is a level down, so
|
|
137
|
-
* both are written `[Person]` / `[Software System]` full stop. Every container
|
|
138
|
-
* — the cylinder, the phone and the browser window included — and every
|
|
139
|
-
* component carries `[Word: technology]`, because at those two levels "what is
|
|
140
|
-
* it built with" is the question the diagram exists to answer.
|
|
141
|
-
*
|
|
142
|
-
* This decides the PLACEHOLDER only. An author who types a technology onto a
|
|
143
|
-
* person gets it drawn, exactly as `c4TypeLine` has always drawn it: the
|
|
144
|
-
* notation is a prompt here, not a validator.
|
|
145
|
-
*/
|
|
146
|
-
export declare const C4_TYPE_TAKES_TECHNOLOGY: Record<C4NodeKind, boolean>;
|
|
147
|
-
/**
|
|
148
|
-
* The type line a freshly created element is born carrying, per kind.
|
|
149
|
-
*
|
|
150
|
-
* Every tier of a C4 component exists from the moment it is drawn (PO
|
|
151
|
-
* arbitration, 28/08/2026), so the author meets three lines of stencil rather
|
|
152
|
-
* than a box and two invisible slots they have to be told about. What they meet
|
|
153
|
-
* is what the official stencil shows: `[Person]`, `[Software System]`,
|
|
154
|
-
* `[Container: technology]`, `[Component: technology]`.
|
|
155
|
-
*
|
|
156
|
-
* A PROMPT, not a value: {@link C4_TYPE_PLACEHOLDER} is what the exporter
|
|
157
|
-
* compares against to decide the author has stated nothing yet, so an untouched
|
|
158
|
-
* element exports as `Container(alias, "Container")` rather than as one built
|
|
159
|
-
* with a technology literally called "technology".
|
|
160
|
-
*/
|
|
161
|
-
export declare const C4_TYPE_PLACEHOLDER: Record<C4NodeKind, string>;
|
|
162
83
|
/**
|
|
163
84
|
* {@link C4_TYPE_PLACEHOLDER}, resolved through `std` when given — the
|
|
164
85
|
* placement site (`createC4Node`, `actions.ts`) seeds a fresh element with
|
package/dist/type-line.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
2
|
+
import { C4_TYPE_PLACEHOLDER, C4_TYPE_TAKES_TECHNOLOGY, C4_TYPE_WORD, TYPE_TECHNOLOGY_PLACEHOLDER, typeLineOf, } from './type-word.js';
|
|
3
|
+
export { C4_TYPE_PLACEHOLDER, C4_TYPE_TAKES_TECHNOLOGY, C4_TYPE_WORD, TYPE_TECHNOLOGY_PLACEHOLDER, } from './type-word.js';
|
|
2
4
|
/**
|
|
3
5
|
* The middle tier of a C4 element's label — `[Person]`, `[Software System]`,
|
|
4
6
|
* `[Container: Java]` — SEMI-DERIVED: the word comes from the kind, the
|
|
@@ -21,36 +23,6 @@ import { translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
|
21
23
|
* cylinder and it becomes `[Container: Java]`, because a database IS a container
|
|
22
24
|
* and the stencil says so.
|
|
23
25
|
*/
|
|
24
|
-
/**
|
|
25
|
-
* The base word each kind is announced by, verbatim from the stencil's own
|
|
26
|
-
* `<desc>` strings (`C4Model_default.svg`, the PO's reference model).
|
|
27
|
-
*
|
|
28
|
-
* Two readings are worth spelling out, because both look like mistakes:
|
|
29
|
-
*
|
|
30
|
-
* - `database` says **Container**, not "Database". The stencil labels the
|
|
31
|
-
* cylinder `[Container: technology]` exactly as it labels the plain box: a
|
|
32
|
-
* database is a CONTAINER, and the cylinder is a picture of one, not a fourth
|
|
33
|
-
* level. (The mermaid export still emits `ContainerDb` — that is the one
|
|
34
|
-
* specialisation mermaid's own grammar draws, and it is a different question
|
|
35
|
-
* from what the box says on the canvas.)
|
|
36
|
-
* - an `-ext` variant says the same word as the kind it is external to. What
|
|
37
|
-
* "external" changes is the COLOUR — grey — and the stencil's gray sheet
|
|
38
|
-
* carries the identical `[Person]` / `[Software System]` wording.
|
|
39
|
-
*
|
|
40
|
-
* `Record<C4NodeKind, …>` and therefore compile-total: a kind added to the model
|
|
41
|
-
* without a word to announce it fails the build here.
|
|
42
|
-
*/
|
|
43
|
-
export const C4_TYPE_WORD = {
|
|
44
|
-
person: 'Person',
|
|
45
|
-
'person-ext': 'Person',
|
|
46
|
-
system: 'Software System',
|
|
47
|
-
'system-ext': 'Software System',
|
|
48
|
-
container: 'Container',
|
|
49
|
-
database: 'Container',
|
|
50
|
-
mobile: 'Container',
|
|
51
|
-
browser: 'Container',
|
|
52
|
-
component: 'Component',
|
|
53
|
-
};
|
|
54
26
|
/**
|
|
55
27
|
* {@link C4_TYPE_WORD}'s own i18n key, one per DISTINCT word (four kinds
|
|
56
28
|
* share `Container`, two share `Person`, two share `Software System`, one
|
|
@@ -110,9 +82,7 @@ function typeWord(kind, std) {
|
|
|
110
82
|
* all and get the same English answer they always have.
|
|
111
83
|
*/
|
|
112
84
|
export function c4TypeLine(kind, technology, std) {
|
|
113
|
-
|
|
114
|
-
const techn = (technology ?? '').replaceAll(/\s+/g, ' ').trim();
|
|
115
|
-
return techn ? `[${word}: ${techn}]` : `[${word}]`;
|
|
85
|
+
return typeLineOf(typeWord(kind, std), technology);
|
|
116
86
|
}
|
|
117
87
|
/* ── Reading the line back ─────────────────────────────────────────────── */
|
|
118
88
|
/**
|
|
@@ -214,14 +184,6 @@ export function normalizeC4TypeLine(kind, rawText, std) {
|
|
|
214
184
|
return c4TypeLine(kind, technologyOfTypeLine(rawText, std), std);
|
|
215
185
|
}
|
|
216
186
|
/* ── What a fresh element says before anybody writes on it ─────────────── */
|
|
217
|
-
/**
|
|
218
|
-
* The word a fresh type line prompts the author with, in the technology's slot.
|
|
219
|
-
*
|
|
220
|
-
* The stencil's own: its container reads `[Container: technology]` before
|
|
221
|
-
* anybody fills it in. A prompt rather than a value — see
|
|
222
|
-
* {@link C4_TYPE_PLACEHOLDER}.
|
|
223
|
-
*/
|
|
224
|
-
export const TYPE_TECHNOLOGY_PLACEHOLDER = 'technology';
|
|
225
187
|
/**
|
|
226
188
|
* {@link TYPE_TECHNOLOGY_PLACEHOLDER}'s own key — see the long comment on
|
|
227
189
|
* {@link C4_TYPE_WORD_KEY} for why this lot's shipped French proposal keeps
|
|
@@ -247,49 +209,6 @@ export const C4_TYPE_LINE_WORDINGS = [
|
|
|
247
209
|
[C4_TYPE_WORD_KEY.component, C4_TYPE_WORD.component],
|
|
248
210
|
[C4_TECHNOLOGY_PLACEHOLDER_KEY, TYPE_TECHNOLOGY_PLACEHOLDER],
|
|
249
211
|
];
|
|
250
|
-
/**
|
|
251
|
-
* Which kinds announce a technology in their type line AT ALL.
|
|
252
|
-
*
|
|
253
|
-
* The notation's own division and not mermaid's, though the two agree: a person
|
|
254
|
-
* is not built with a technology and a software system's is a level down, so
|
|
255
|
-
* both are written `[Person]` / `[Software System]` full stop. Every container
|
|
256
|
-
* — the cylinder, the phone and the browser window included — and every
|
|
257
|
-
* component carries `[Word: technology]`, because at those two levels "what is
|
|
258
|
-
* it built with" is the question the diagram exists to answer.
|
|
259
|
-
*
|
|
260
|
-
* This decides the PLACEHOLDER only. An author who types a technology onto a
|
|
261
|
-
* person gets it drawn, exactly as `c4TypeLine` has always drawn it: the
|
|
262
|
-
* notation is a prompt here, not a validator.
|
|
263
|
-
*/
|
|
264
|
-
export const C4_TYPE_TAKES_TECHNOLOGY = {
|
|
265
|
-
person: false,
|
|
266
|
-
'person-ext': false,
|
|
267
|
-
system: false,
|
|
268
|
-
'system-ext': false,
|
|
269
|
-
container: true,
|
|
270
|
-
database: true,
|
|
271
|
-
mobile: true,
|
|
272
|
-
browser: true,
|
|
273
|
-
component: true,
|
|
274
|
-
};
|
|
275
|
-
/**
|
|
276
|
-
* The type line a freshly created element is born carrying, per kind.
|
|
277
|
-
*
|
|
278
|
-
* Every tier of a C4 component exists from the moment it is drawn (PO
|
|
279
|
-
* arbitration, 28/08/2026), so the author meets three lines of stencil rather
|
|
280
|
-
* than a box and two invisible slots they have to be told about. What they meet
|
|
281
|
-
* is what the official stencil shows: `[Person]`, `[Software System]`,
|
|
282
|
-
* `[Container: technology]`, `[Component: technology]`.
|
|
283
|
-
*
|
|
284
|
-
* A PROMPT, not a value: {@link C4_TYPE_PLACEHOLDER} is what the exporter
|
|
285
|
-
* compares against to decide the author has stated nothing yet, so an untouched
|
|
286
|
-
* element exports as `Container(alias, "Container")` rather than as one built
|
|
287
|
-
* with a technology literally called "technology".
|
|
288
|
-
*/
|
|
289
|
-
export const C4_TYPE_PLACEHOLDER = Object.fromEntries(Object.keys(C4_TYPE_WORD).map(kind => [
|
|
290
|
-
kind,
|
|
291
|
-
c4TypeLine(kind, C4_TYPE_TAKES_TECHNOLOGY[kind] ? TYPE_TECHNOLOGY_PLACEHOLDER : undefined),
|
|
292
|
-
]));
|
|
293
212
|
/**
|
|
294
213
|
* {@link C4_TYPE_PLACEHOLDER}, resolved through `std` when given — the
|
|
295
214
|
* placement site (`createC4Node`, `actions.ts`) seeds a fresh element with
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { C4NodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
/**
|
|
3
|
+
* The base word each kind is announced by, verbatim from the stencil's own
|
|
4
|
+
* `<desc>` strings (`C4Model_default.svg`, the PO's reference model).
|
|
5
|
+
*
|
|
6
|
+
* Two readings are worth spelling out, because both look like mistakes:
|
|
7
|
+
*
|
|
8
|
+
* - `database` says **Container**, not "Database". The stencil labels the
|
|
9
|
+
* cylinder `[Container: technology]` exactly as it labels the plain box: a
|
|
10
|
+
* database is a CONTAINER, and the cylinder is a picture of one, not a fourth
|
|
11
|
+
* level. (The mermaid export still emits `ContainerDb` — that is the one
|
|
12
|
+
* specialisation mermaid's own grammar draws, and it is a different question
|
|
13
|
+
* from what the box says on the canvas.)
|
|
14
|
+
* - an `-ext` variant says the same word as the kind it is external to. What
|
|
15
|
+
* "external" changes is the COLOUR — grey — and the stencil's gray sheet
|
|
16
|
+
* carries the identical `[Person]` / `[Software System]` wording.
|
|
17
|
+
*
|
|
18
|
+
* `Record<C4NodeKind, …>` and therefore compile-total: a kind added to the model
|
|
19
|
+
* without a word to announce it fails the build here.
|
|
20
|
+
*/
|
|
21
|
+
export declare const C4_TYPE_WORD: Record<C4NodeKind, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Which kinds announce a technology in their type line AT ALL.
|
|
24
|
+
*
|
|
25
|
+
* The notation's own division and not mermaid's, though the two agree: a person
|
|
26
|
+
* is not built with a technology and a software system's is a level down, so
|
|
27
|
+
* both are written `[Person]` / `[Software System]` full stop. Every container
|
|
28
|
+
* — the cylinder, the phone and the browser window included — and every
|
|
29
|
+
* component carries `[Word: technology]`, because at those two levels "what is
|
|
30
|
+
* it built with" is the question the diagram exists to answer.
|
|
31
|
+
*
|
|
32
|
+
* This decides the PLACEHOLDER only. An author who types a technology onto a
|
|
33
|
+
* person gets it drawn, exactly as `c4TypeLine` has always drawn it: the
|
|
34
|
+
* notation is a prompt here, not a validator.
|
|
35
|
+
*/
|
|
36
|
+
export declare const C4_TYPE_TAKES_TECHNOLOGY: Record<C4NodeKind, boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* The word a fresh type line prompts the author with, in the technology's slot.
|
|
39
|
+
*
|
|
40
|
+
* The stencil's own: its container reads `[Container: technology]` before
|
|
41
|
+
* anybody fills it in. A prompt rather than a value — see
|
|
42
|
+
* {@link C4_TYPE_PLACEHOLDER}.
|
|
43
|
+
*/
|
|
44
|
+
export declare const TYPE_TECHNOLOGY_PLACEHOLDER = "technology";
|
|
45
|
+
/** `[Word]`, or `[Word: technology]` when one is set (see `c4TypeLine`). */
|
|
46
|
+
export declare function typeLineOf(word: string, technology?: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* The type line a freshly created element is born carrying, per kind.
|
|
49
|
+
*
|
|
50
|
+
* Every tier of a C4 component exists from the moment it is drawn (PO
|
|
51
|
+
* arbitration, 28/08/2026), so the author meets three lines of stencil rather
|
|
52
|
+
* than a box and two invisible slots they have to be told about. What they meet
|
|
53
|
+
* is what the official stencil shows: `[Person]`, `[Software System]`,
|
|
54
|
+
* `[Container: technology]`, `[Component: technology]`.
|
|
55
|
+
*
|
|
56
|
+
* A PROMPT, not a value: {@link C4_TYPE_PLACEHOLDER} is what the exporter
|
|
57
|
+
* compares against to decide the author has stated nothing yet, so an untouched
|
|
58
|
+
* element exports as `Container(alias, "Container")` rather than as one built
|
|
59
|
+
* with a technology literally called "technology".
|
|
60
|
+
*/
|
|
61
|
+
export declare const C4_TYPE_PLACEHOLDER: Record<C4NodeKind, string>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The English words of the type line, editor-free: the toolbox
|
|
2
|
+
// (`toolbox.ts`, `docs/adr/0045`) seeds a component with them from the
|
|
3
|
+
// `./interchange` graph, which must not load the services that resolve a
|
|
4
|
+
// word in the host's language (`type-line.ts` does that).
|
|
5
|
+
/**
|
|
6
|
+
* The base word each kind is announced by, verbatim from the stencil's own
|
|
7
|
+
* `<desc>` strings (`C4Model_default.svg`, the PO's reference model).
|
|
8
|
+
*
|
|
9
|
+
* Two readings are worth spelling out, because both look like mistakes:
|
|
10
|
+
*
|
|
11
|
+
* - `database` says **Container**, not "Database". The stencil labels the
|
|
12
|
+
* cylinder `[Container: technology]` exactly as it labels the plain box: a
|
|
13
|
+
* database is a CONTAINER, and the cylinder is a picture of one, not a fourth
|
|
14
|
+
* level. (The mermaid export still emits `ContainerDb` — that is the one
|
|
15
|
+
* specialisation mermaid's own grammar draws, and it is a different question
|
|
16
|
+
* from what the box says on the canvas.)
|
|
17
|
+
* - an `-ext` variant says the same word as the kind it is external to. What
|
|
18
|
+
* "external" changes is the COLOUR — grey — and the stencil's gray sheet
|
|
19
|
+
* carries the identical `[Person]` / `[Software System]` wording.
|
|
20
|
+
*
|
|
21
|
+
* `Record<C4NodeKind, …>` and therefore compile-total: a kind added to the model
|
|
22
|
+
* without a word to announce it fails the build here.
|
|
23
|
+
*/
|
|
24
|
+
export const C4_TYPE_WORD = {
|
|
25
|
+
person: 'Person',
|
|
26
|
+
'person-ext': 'Person',
|
|
27
|
+
system: 'Software System',
|
|
28
|
+
'system-ext': 'Software System',
|
|
29
|
+
container: 'Container',
|
|
30
|
+
database: 'Container',
|
|
31
|
+
mobile: 'Container',
|
|
32
|
+
browser: 'Container',
|
|
33
|
+
component: 'Component',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Which kinds announce a technology in their type line AT ALL.
|
|
37
|
+
*
|
|
38
|
+
* The notation's own division and not mermaid's, though the two agree: a person
|
|
39
|
+
* is not built with a technology and a software system's is a level down, so
|
|
40
|
+
* both are written `[Person]` / `[Software System]` full stop. Every container
|
|
41
|
+
* — the cylinder, the phone and the browser window included — and every
|
|
42
|
+
* component carries `[Word: technology]`, because at those two levels "what is
|
|
43
|
+
* it built with" is the question the diagram exists to answer.
|
|
44
|
+
*
|
|
45
|
+
* This decides the PLACEHOLDER only. An author who types a technology onto a
|
|
46
|
+
* person gets it drawn, exactly as `c4TypeLine` has always drawn it: the
|
|
47
|
+
* notation is a prompt here, not a validator.
|
|
48
|
+
*/
|
|
49
|
+
export const C4_TYPE_TAKES_TECHNOLOGY = {
|
|
50
|
+
person: false,
|
|
51
|
+
'person-ext': false,
|
|
52
|
+
system: false,
|
|
53
|
+
'system-ext': false,
|
|
54
|
+
container: true,
|
|
55
|
+
database: true,
|
|
56
|
+
mobile: true,
|
|
57
|
+
browser: true,
|
|
58
|
+
component: true,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The word a fresh type line prompts the author with, in the technology's slot.
|
|
62
|
+
*
|
|
63
|
+
* The stencil's own: its container reads `[Container: technology]` before
|
|
64
|
+
* anybody fills it in. A prompt rather than a value — see
|
|
65
|
+
* {@link C4_TYPE_PLACEHOLDER}.
|
|
66
|
+
*/
|
|
67
|
+
export const TYPE_TECHNOLOGY_PLACEHOLDER = 'technology';
|
|
68
|
+
/** `[Word]`, or `[Word: technology]` when one is set (see `c4TypeLine`). */
|
|
69
|
+
export function typeLineOf(word, technology) {
|
|
70
|
+
const techn = (technology ?? '').replaceAll(/\s+/g, ' ').trim();
|
|
71
|
+
return techn ? `[${word}: ${techn}]` : `[${word}]`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The type line a freshly created element is born carrying, per kind.
|
|
75
|
+
*
|
|
76
|
+
* Every tier of a C4 component exists from the moment it is drawn (PO
|
|
77
|
+
* arbitration, 28/08/2026), so the author meets three lines of stencil rather
|
|
78
|
+
* than a box and two invisible slots they have to be told about. What they meet
|
|
79
|
+
* is what the official stencil shows: `[Person]`, `[Software System]`,
|
|
80
|
+
* `[Container: technology]`, `[Component: technology]`.
|
|
81
|
+
*
|
|
82
|
+
* A PROMPT, not a value: {@link C4_TYPE_PLACEHOLDER} is what the exporter
|
|
83
|
+
* compares against to decide the author has stated nothing yet, so an untouched
|
|
84
|
+
* element exports as `Container(alias, "Container")` rather than as one built
|
|
85
|
+
* with a technology literally called "technology".
|
|
86
|
+
*/
|
|
87
|
+
export const C4_TYPE_PLACEHOLDER = Object.fromEntries(Object.keys(C4_TYPE_WORD).map(kind => [
|
|
88
|
+
kind,
|
|
89
|
+
typeLineOf(C4_TYPE_WORD[kind], C4_TYPE_TAKES_TECHNOLOGY[kind] ? TYPE_TECHNOLOGY_PLACEHOLDER : undefined),
|
|
90
|
+
]));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formicoidea/labre-framework-c4",
|
|
3
3
|
"description": "Labre c4 framework for @formicoidea/labre-core.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.60.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "lajola",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@formicoidea/labre-core": "0.
|
|
39
|
-
"@formicoidea/labre-ddd-shared": "0.
|
|
38
|
+
"@formicoidea/labre-core": "0.60.0",
|
|
39
|
+
"@formicoidea/labre-ddd-shared": "0.60.0",
|
|
40
40
|
"lit": "^3.2.0"
|
|
41
41
|
}
|
|
42
42
|
}
|