@formicoidea/labre-framework-c4 0.59.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 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 HERE, together, or not at all
62
+ * ## The role and the variant are written together, or not at all
63
63
  *
64
- * This is the ONE place either field is written, and it writes both from the same
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, FontFamily, FontStyle, FontWeight, PointStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
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 { c4TierBoxes } from './component.js';
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
- const x = cx - w / 2;
113
- const y = cy - h / 2;
114
- const paint = NODE_PALETTE[kind];
115
- const shapeId = surface.addElement({
116
- // Every prop the kind is worth, from the ONE table the morph also reads
117
- // (`presets.ts`) — so a component drawn here and one morphed into this kind
118
- // from the toolbar are the same element, whatever either is restyled to.
119
- // NO `text`: the name is the `c4:title` child below, and the shape is a
120
- // body and nothing else.
121
- ...c4NodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
122
- index: gfx.layer.generateIndex(),
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 = surface.addElement({
183
- type: 'c4Board',
184
- // The FRAME the elements are drawn on, and a role of its own: a rule written
185
- // on the artefacts must never fall on the sheet holding them.
186
- role: C4_ROLE.board,
187
- // The sheet's own name, written HERE rather than left to the model's
188
- // default so that a board drawn in a translated host starts in that
189
- // language (#183). It is content from that moment on — the title is
190
- // editable on a double-click — so it is asked for once and never
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 HERE, together, or not at all
137
+ * ## The role and the variant are written together, or not at all
208
138
  *
209
- * This is the ONE place either field is written, and it writes both from the same
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 = surface.addElement({
226
- type: 'c4Boundary',
227
- // The CHILD role matching the variant — never the parent, which is now the
228
- // role of a boundary drawn before the split and of nothing this editor
229
- // creates. See the note above.
230
- role: C4_BOUNDARY_ROLE[variant],
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,
@@ -1,79 +1,4 @@
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;
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 { DESCRIPTION_FONT_SIZE, DESCRIPTION_LINES, DESCRIPTION_PLACEHOLDER, 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';
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/consts';
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
@@ -1,4 +1,4 @@
1
- import { type C4NodeKind } from '@formicoidea/labre-core/model';
1
+ import type { C4NodeKind } from '@formicoidea/labre-core/model';
2
2
  /**
3
3
  * What a C4 shape IS, as props — the ONE description the creation site and the
4
4
  * morph both read.
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;
@@ -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
+ };
@@ -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
- const word = typeWord(kind, std);
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.59.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.59.0",
39
- "@formicoidea/labre-ddd-shared": "0.59.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
  }