@formicoidea/labre-framework-c4 0.33.0

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