@formicoidea/labre-framework-c4 0.40.0 → 0.42.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/commands.js CHANGED
@@ -1,5 +1,131 @@
1
1
  import { activateC4Relationship, c4BoardsForExport, createC4Board, createC4Boundary, createC4Node, exportC4MermaidFile, } from './actions.js';
2
- import { c4ExportMermaidIcon, C4_TOOLBOX_ICONS } from './toolbar/icons.js';
2
+ import { BOUNDARY_STROKE, NODE_PALETTE, RELATIONSHIP_STROKE } from './consts.js';
3
+ import { C4_ROLE } from './roles.js';
4
+ import { c4ExportMermaidIcon, c4ToolbarIcon, C4_TOOLBOX_ICONS, } from './toolbar/icons.js';
5
+ /**
6
+ * The C4 toolbox as commands — the single source every surface reads: the
7
+ * senior sub-menu, the artefact catalogue, the palette, Settings › Shortcuts
8
+ * and the agent (`docs/adr/0008`).
9
+ *
10
+ * ## Thirteen drawn, fourteen declared — exactly the cap
11
+ *
12
+ * The pack draws nine elements, two boundaries, one board and one connecting
13
+ * object: thirteen entries, all thirteen on a sub-menu that holds fourteen. The
14
+ * mermaid EXPORT is the fourteenth catalogue entry and declines the sub-menu, so
15
+ * the catalogue lands on fourteen against a cap of fourteen — which
16
+ * `selectSeniorMenuCommands` measures on the CATALOGUE, not on the menu. C4 is
17
+ * therefore still the framework that FITS, to the entry: nothing is arbitrated,
18
+ * the ranking never runs, and the sub-menu is exactly this list in exactly this
19
+ * order.
20
+ *
21
+ * The board's automatic LEGEND is deliberately not among them, and is not a
22
+ * command at all: it is reached from the selected board's contextual toolbar and
23
+ * from nowhere else (PO arbitration, 27/08/2026 — the same call the Context Map
24
+ * board makes). See `toolbar/config.ts` for what that costs and why it is the
25
+ * arbitrated exception to the command bottleneck.
26
+ *
27
+ * What the legend IS made of, though, is declared right here: each entry below
28
+ * subscribes the row its artefact puts in the box (`CommandDescriptor.legend`,
29
+ * `docs/adr/0026`), so the row and the gesture cannot drift apart the way a
30
+ * second hand-written table did.
31
+ *
32
+ * That does not make the ORDER free — it makes it a fifteenth entry away from
33
+ * mattering. Add one more of anything, an artefact or a second export, and the
34
+ * catalogue outgrows the cap, the ranking kicks in, and the first
35
+ * `SENIOR_MENU_RANKED_SLOTS` of this list become the COLD START every new user
36
+ * meets. BPMN learned that the hard way in a live recette (#144): its
37
+ * declarations were grouped by family, so the head a first-time user met was six
38
+ * events and a task, with nothing to connect them. The fix was the order, and
39
+ * the lesson is cheaper to apply before the overflow than after it.
40
+ *
41
+ * The head is THIRTEEN slots since #167 (seven recent + six frequent), not the
42
+ * seven it was when BPMN met the problem — which makes the cold start nearly the
43
+ * whole toolbox and the author's order nearly the whole answer. Named through
44
+ * the constant rather than restated as a number, so the next widening does not
45
+ * leave a lie here.
46
+ *
47
+ * ## The house order for a framework that FITS (PO, 28/08/2026)
48
+ *
49
+ * A general convention, decided on this pack and applying to every framework of
50
+ * fourteen commands or fewer — i.e. every one whose sub-menu is never
51
+ * arbitrated, so the author's order is the only order anybody ever sees:
52
+ *
53
+ * 1. the BOARDS first — the sheet has to exist before anything can be put on
54
+ * it, and a first-time user who reaches for a person before a board draws
55
+ * a person on the void;
56
+ * 2. then the BASE components, the ones a diagram of this kind is mostly made
57
+ * of;
58
+ * 3. then the NICHE ones last;
59
+ * 4. and components of the SAME TYPE stay adjacent — the external variant sits
60
+ * against the plain one it varies, not in a ghetto of externals at the end.
61
+ *
62
+ * This SUPERSEDES the previous C4 order, which led with the four levels and put
63
+ * the board sixth: it was built to make the would-be cold start drawable, and
64
+ * the PO's answer is that a cold start which opens with the sheet is drawable
65
+ * sooner. Applied here, it reads: the board, then person / person-ext, system /
66
+ * system-ext (each variant next to its plain form), container, component,
67
+ * database, then the two container flavours that are a container with a picture,
68
+ * then the relationship, then the two boundaries, then the export.
69
+ */
70
+ /* ── The legend, SUBSCRIBED rather than tabulated (`docs/adr/0026`) ────── */
71
+ /**
72
+ * The legend's own three section titles — this framework's word about this
73
+ * framework's vocabulary, which is why they are keyed per framework and never
74
+ * shared with UML's identical three (`translation-service/README.md`).
75
+ *
76
+ * They are DECLARED rather than derived from the commands' `category` because
77
+ * the catalogue files a boundary under `boundaries` and the legend calls the
78
+ * same thing a "Frame": one is where a gesture sits in a toolbox, the other is
79
+ * what the drawing is. The keys are the ones already shipped to the host
80
+ * catalogue, unchanged.
81
+ */
82
+ const SECTION_ELEMENTS = [
83
+ 'com.labre.c4.legend.section.elements',
84
+ 'Elements',
85
+ ];
86
+ const SECTION_FRAMES = [
87
+ 'com.labre.c4.legend.section.frames',
88
+ 'Frames',
89
+ ];
90
+ const SECTION_RELATIONS = [
91
+ 'com.labre.c4.legend.section.relations',
92
+ 'Relations',
93
+ ];
94
+ /** Every section wording above, for `translations.ts`'s manifest. */
95
+ export const C4_LEGEND_SECTION_WORDINGS = [
96
+ SECTION_ELEMENTS,
97
+ SECTION_FRAMES,
98
+ SECTION_RELATIONS,
99
+ ];
100
+ /**
101
+ * One element row, in the very fill {@link NODE_PALETTE} paints that kind with
102
+ * — never a second literal, so restyling the pack restyles its swatches. The
103
+ * WORDING is nowhere here either: the engine reads the role's own label, so
104
+ * renaming a role renames its row.
105
+ *
106
+ * `exact` is asked for on the container and nowhere else. `c4:database`
107
+ * specialises `c4:container`, so an inclusive row on the parent would put a
108
+ * blue square on a board carrying nothing but cylinders — a row naming a shape
109
+ * that is nowhere on the diagram. The other four element roles are childless,
110
+ * so the flag would change nothing for them and marks only the place where the
111
+ * distinction is real.
112
+ */
113
+ function elementEntry(role, fill, exact = false) {
114
+ return {
115
+ role,
116
+ row: { swatch: 'square', color: fill },
117
+ ...(exact ? { exact } : {}),
118
+ section: SECTION_ELEMENTS,
119
+ };
120
+ }
121
+ /**
122
+ * A line row: a boundary and a relationship are both drawn, never filled, and
123
+ * both are dashed on the canvas — a filled chip would picture the one thing
124
+ * this board deliberately does not paint.
125
+ */
126
+ function lineEntry(role, color, section) {
127
+ return { role, row: { swatch: 'line', color, dashed: true }, section };
128
+ }
3
129
  const SPECS = [
4
130
  /* ── Boards first: the sheet everything else is drawn on ────────────── */
5
131
  {
@@ -10,6 +136,8 @@ const SPECS = [
10
136
  category: 'diagrams',
11
137
  element: 'board',
12
138
  board: true,
139
+ // Wider than the shared 260: the longest C4 row reads "Software system".
140
+ legendBox: { width: 290 },
13
141
  run: createC4Board,
14
142
  },
15
143
  /* ── The base components: the four levels, each plain form immediately
@@ -21,6 +149,7 @@ const SPECS = [
21
149
  kind: 'artefact',
22
150
  category: 'elements',
23
151
  element: 'node:person',
152
+ legend: elementEntry(C4_ROLE.person, NODE_PALETTE.person.fill),
24
153
  run: std => createC4Node(std, 'person'),
25
154
  },
26
155
  {
@@ -39,6 +168,7 @@ const SPECS = [
39
168
  kind: 'artefact',
40
169
  category: 'elements',
41
170
  element: 'node:system',
171
+ legend: elementEntry(C4_ROLE.system, NODE_PALETTE.system.fill),
42
172
  run: std => createC4Node(std, 'system'),
43
173
  },
44
174
  {
@@ -57,6 +187,7 @@ const SPECS = [
57
187
  kind: 'artefact',
58
188
  category: 'elements',
59
189
  element: 'node:container',
190
+ legend: elementEntry(C4_ROLE.container, NODE_PALETTE.container.fill, true),
60
191
  run: std => createC4Node(std, 'container'),
61
192
  },
62
193
  {
@@ -66,6 +197,7 @@ const SPECS = [
66
197
  kind: 'artefact',
67
198
  category: 'elements',
68
199
  element: 'node:component',
200
+ legend: elementEntry(C4_ROLE.component, NODE_PALETTE.component.fill),
69
201
  run: std => createC4Node(std, 'component'),
70
202
  },
71
203
  /* ── The niche components: a container with a picture on it ─────────── */
@@ -78,6 +210,7 @@ const SPECS = [
78
210
  kind: 'artefact',
79
211
  category: 'elements',
80
212
  element: 'node:database',
213
+ legend: elementEntry(C4_ROLE.database, NODE_PALETTE.database.fill),
81
214
  run: std => createC4Node(std, 'database'),
82
215
  },
83
216
  {
@@ -108,6 +241,7 @@ const SPECS = [
108
241
  kind: 'tool',
109
242
  category: 'relations',
110
243
  element: 'connector:relationship',
244
+ legend: lineEntry(C4_ROLE.relationship, RELATIONSHIP_STROKE, SECTION_RELATIONS),
111
245
  run: activateC4Relationship,
112
246
  },
113
247
  {
@@ -117,6 +251,9 @@ const SPECS = [
117
251
  kind: 'artefact',
118
252
  category: 'boundaries',
119
253
  element: 'boundary:system',
254
+ // ONE row for both boundaries, written on the PARENT role: a container
255
+ // boundary IS a boundary, and two rows would name one frame twice.
256
+ legend: lineEntry(C4_ROLE.boundary, BOUNDARY_STROKE, SECTION_FRAMES),
120
257
  run: std => createC4Boundary(std, 'system'),
121
258
  },
122
259
  {
@@ -148,6 +285,8 @@ const toolboxCommands = SPECS.map((spec, order) => ({
148
285
  availability: 'always',
149
286
  run: spec.run,
150
287
  telemetry: { framework: 'c4', element: spec.element, board: spec.board },
288
+ ...(spec.legend ? { legend: spec.legend } : {}),
289
+ ...(spec.legendBox ? { legendBox: spec.legendBox } : {}),
151
290
  }));
152
291
  /**
153
292
  * The EXPORT — the first C4 command whose subject is a whole BOARD.
@@ -219,4 +358,7 @@ export const c4Commands = [
219
358
  export const c4CommandIcons = {
220
359
  ...C4_TOOLBOX_ICONS,
221
360
  'c4.export-mermaid': c4ExportMermaidIcon,
361
+ // The senior button's 56×56 glyph, so `FrameworkDescriptor.iconKey` resolves
362
+ // through `getCommandIcon`.
363
+ 'c4.toolbar': c4ToolbarIcon,
222
364
  };
package/dist/consts.d.ts CHANGED
@@ -246,6 +246,22 @@ export declare const NODE_SIZE: Record<C4NodeKind, {
246
246
  * nothing at all: the words are the artefact.
247
247
  */
248
248
  export declare const NODE_LABEL: Record<C4NodeKind, string>;
249
+ /**
250
+ * The i18n key {@link NODE_LABEL} is the English default of — mirrors BPMN's
251
+ * {@link nodeLabelKey} exactly, one package over: resolved at placement
252
+ * (`createC4Node`, the NAME tier) and never afterwards, since the title is
253
+ * content from the moment it lands and an author is free to retype it.
254
+ *
255
+ * Safe to translate where the type line is not: the exporter reads a
256
+ * component's NAME verbatim (`c4StatedName`) with no placeholder suppression,
257
+ * so nothing downstream compares this text against the English literal.
258
+ */
259
+ export declare const nodeLabelKey: (kind: C4NodeKind) => string;
260
+ /**
261
+ * The prompt a fresh description tier carries, resolved at placement exactly
262
+ * like {@link DESCRIPTION_PLACEHOLDER} below is the English default of.
263
+ */
264
+ export declare const DESCRIPTION_PLACEHOLDER_KEY = "com.labre.c4.seed.description-placeholder";
249
265
  export declare const FONT_FAMILY = "Inter, sans-serif";
250
266
  /**
251
267
  * The card — the same white every framework background paints, framed and
@@ -336,6 +352,14 @@ export declare const BOUNDARY_MARGIN = 12;
336
352
  * on the canvas keeps its words whatever its variant.
337
353
  */
338
354
  export declare const BOUNDARY_LABEL: Record<C4BoundaryVariant, string>;
355
+ /**
356
+ * The i18n key {@link BOUNDARY_LABEL} is the English default of — resolved at
357
+ * placement (`createC4Boundary`) exactly like {@link nodeLabelKey}: the name
358
+ * is exported verbatim (`export.ts`, `boundary.name`) with no comparison
359
+ * against the English literal, so translating it is as safe as a component's
360
+ * title.
361
+ */
362
+ export declare const boundaryLabelKey: (variant: C4BoundaryVariant) => string;
339
363
  /**
340
364
  * Relationship connector preset — the dashed arrow C4 draws between elements.
341
365
  *
package/dist/consts.js CHANGED
@@ -291,6 +291,22 @@ export const NODE_LABEL = {
291
291
  browser: 'Web app',
292
292
  component: 'Component',
293
293
  };
294
+ /**
295
+ * The i18n key {@link NODE_LABEL} is the English default of — mirrors BPMN's
296
+ * {@link nodeLabelKey} exactly, one package over: resolved at placement
297
+ * (`createC4Node`, the NAME tier) and never afterwards, since the title is
298
+ * content from the moment it lands and an author is free to retype it.
299
+ *
300
+ * Safe to translate where the type line is not: the exporter reads a
301
+ * component's NAME verbatim (`c4StatedName`) with no placeholder suppression,
302
+ * so nothing downstream compares this text against the English literal.
303
+ */
304
+ export const nodeLabelKey = (kind) => `com.labre.c4.seed.${kind}`;
305
+ /**
306
+ * The prompt a fresh description tier carries, resolved at placement exactly
307
+ * like {@link DESCRIPTION_PLACEHOLDER} below is the English default of.
308
+ */
309
+ export const DESCRIPTION_PLACEHOLDER_KEY = 'com.labre.c4.seed.description-placeholder';
294
310
  /* ── The board ─────────────────────────────────────────────────────────── */
295
311
  export const FONT_FAMILY = 'Inter, sans-serif';
296
312
  /**
@@ -389,6 +405,14 @@ export const BOUNDARY_LABEL = {
389
405
  system: 'System boundary',
390
406
  container: 'Container boundary',
391
407
  };
408
+ /**
409
+ * The i18n key {@link BOUNDARY_LABEL} is the English default of — resolved at
410
+ * placement (`createC4Boundary`) exactly like {@link nodeLabelKey}: the name
411
+ * is exported verbatim (`export.ts`, `boundary.name`) with no comparison
412
+ * against the English literal, so translating it is as safe as a component's
413
+ * title.
414
+ */
415
+ export const boundaryLabelKey = (variant) => `com.labre.c4.seed.boundary-${variant}`;
392
416
  /* ── The relationship ──────────────────────────────────────────────────── */
393
417
  /**
394
418
  * Relationship connector preset — the dashed arrow C4 draws between elements.
package/dist/index.d.ts CHANGED
@@ -13,5 +13,4 @@ export { C4_INTERCHANGE, C4_MERMAID_EXPORT, C4_MERMAID_EXTENSION, C4_MERMAID_FOR
13
13
  export { C4_ELEMENT_MATRIX, C4_RELATIONSHIP_MATRIX, C4_RULES, } from './rules.js';
14
14
  export { c4CommandIcons, c4Commands } from './commands.js';
15
15
  export { c4TranslationEntries } from './translations.js';
16
- export { C4_AUTO_LEGEND } from './legend.js';
17
16
  export { C4_COMPONENT_READING, C4_CONTAINER_READING, C4_PERSON_READING, C4_READINGS, C4_SYSTEM_READING, } from './reading.js';
package/dist/index.js CHANGED
@@ -48,10 +48,10 @@ export { C4_ELEMENT_MATRIX, C4_RELATIONSHIP_MATRIX, C4_RULES, } from './rules.js
48
48
  // The toolbox, for the host that composes the command registry and the
49
49
  // translation-key manifest out of the frameworks it installed (see
50
50
  // `packages/affine/all/src/{commands,translations}.ts`).
51
+ // What the board's automatic legend can say is no longer a table of its own:
52
+ // every row is subscribed by the command that draws the artefact
53
+ // (`CommandDescriptor.legend`, `docs/adr/0026`), so `c4Commands` already
54
+ // answers "what does this framework document about itself" without a canvas.
51
55
  export { c4CommandIcons, c4Commands } from './commands.js';
52
56
  export { c4TranslationEntries } from './translations.js';
53
- // The legend TABLE — what the board's automatic legend can say. Exported for
54
- // the same reason the two background declarations are: it answers "what does
55
- // this framework document about itself" without a canvas.
56
- export { C4_AUTO_LEGEND } from './legend.js';
57
57
  export { C4_COMPONENT_READING, C4_CONTAINER_READING, C4_PERSON_READING, C4_READINGS, C4_SYSTEM_READING, } from './reading.js';
package/dist/morph.js CHANGED
@@ -167,8 +167,8 @@ function rewriteTier(model, role, decide) {
167
167
  * "Customer database" keeps that name whatever it becomes, which is the PO's
168
168
  * "the label is intact" for every title that is real content.
169
169
  */
170
- function rewriteTiers(model, from, to) {
171
- rewriteTier(model, C4_ROLE['type-line'], text => c4MorphedTypeLine(from, to, text));
170
+ function rewriteTiers(model, from, to, std) {
171
+ rewriteTier(model, C4_ROLE['type-line'], text => c4MorphedTypeLine(from, to, text, std));
172
172
  rewriteTier(model, C4_ROLE.title, text => c4MorphedTitle(from, to, text));
173
173
  }
174
174
  /**
@@ -103,7 +103,12 @@ export class C4TypeLineWatcher extends InteractivityExtension {
103
103
  if (!kind)
104
104
  return;
105
105
  const raw = element.text.toString();
106
- const canonical = normalizeC4TypeLine(kind, raw);
106
+ // `this.std` resolves the canonical line through the host's catalogue —
107
+ // both halves of the prompt, and the technology round-trip
108
+ // (`technologyOfTypeLine`) recognising the host's own resolved word —
109
+ // so a translated document's type line stays in that language across a
110
+ // commit instead of being silently reverted to English (`type-line.ts`).
111
+ const canonical = normalizeC4TypeLine(kind, raw, this.std);
107
112
  if (canonical === raw)
108
113
  return;
109
114
  this.std.store.captureSync();
package/dist/templates.js CHANGED
@@ -46,6 +46,8 @@ const boundaryPreview = (inner = '') => `<svg ${ATTRS} fill="none"><rect x="14"
46
46
  const CONTAINER = NODE_PALETTE.container;
47
47
  export const c4TemplateCategory = {
48
48
  name: 'C4',
49
+ // Reuses the senior button's own key — see `TemplateCategory.nameKey`.
50
+ nameKey: 'com.labre.framework.c4',
49
51
  templates: [
50
52
  templateFromCommand(byId('c4.addBoard'), `<svg ${ATTRS} fill="none"><rect x="10" y="10" width="115" height="60" rx="6" fill="${BOARD_CARD_FILL}" stroke="${BOARD_CARD_BORDER}" stroke-width="1.5"/><path d="M10 26 H125" stroke="${BOARD_CARD_BORDER}" stroke-width="1.5"/><rect x="11" y="11" width="113" height="15" fill="${BOARD_BAND_FILL}"/><rect x="20" y="15" width="40" height="6" rx="3" fill="${BOARD_TITLE_COLOR}" opacity="0.55"/><rect x="22" y="36" width="34" height="20" rx="2" fill="${NODE_PALETTE.system.fill}"/><rect x="79" y="36" width="34" height="20" rx="2" fill="${CONTAINER.fill}"/><path d="M58 46 H75" stroke="${RELATIONSHIP_STROKE}" stroke-width="1.6" stroke-dasharray="4 3"/></svg>`),
51
53
  templateFromCommand(byId('c4.addPerson'), personPreview('person')),
@@ -16,4 +16,5 @@ export declare class EdgelessC4SeniorButton extends DddSeniorButtonBase {
16
16
  protected label: string;
17
17
  protected labelKey: string;
18
18
  protected icon: import("lit-html").TemplateResult<2>;
19
+ protected owner: "c4";
19
20
  }
@@ -19,5 +19,6 @@ export class EdgelessC4SeniorButton extends DddSeniorButtonBase {
19
19
  this.label = 'C4 model';
20
20
  this.labelKey = 'com.labre.framework.c4';
21
21
  this.icon = c4ToolbarIcon;
22
+ this.owner = 'c4';
22
23
  }
23
24
  }
@@ -1,5 +1,4 @@
1
- import { ActionPlacement, type ToolbarContext, type ToolbarModuleConfig } from '@formicoidea/labre-core/shared/services';
2
- import { type TemplateResult } from 'lit';
1
+ import { type ToolbarContext, type ToolbarModuleConfig } from '@formicoidea/labre-core/shared/services';
3
2
  /**
4
3
  * The selected C4 board's contextual toolbar: the resize toggle, and the
5
4
  * automatic legend of what is actually drawn on the board.
@@ -13,15 +12,14 @@ import { type TemplateResult } from 'lit';
13
12
  * The PO's call is that generating one belongs to a board you have SELECTED and
14
13
  * to nothing else: it is not an artefact to pick off a palette, and an entry in
15
14
  * a catalogue of things C4 draws would offer it to a user with no board in front
16
- * of them. So there is no `c4.legend` command, this button is the only way to
17
- * reach it, and the telemetry it owes is emitted by hand below.
15
+ * of them. So there is no `c4.legend` command, and this button is the only way
16
+ * to reach it.
18
17
  *
19
- * That is the same shape — and the same exception — the Context Map board makes
20
- * (`ddd-context-map/src/toolbar/board-config.ts`), down to the payload, so the
21
- * two frameworks' legends stay comparable on one dashboard. The cost is the one
22
- * the bottleneck exists to avoid and is accepted knowingly: this `track()` call
23
- * is a second emitter, and it is on whoever edits it to keep the wire values
24
- * matching what `reportCommandTelemetry` would have sent.
18
+ * That is the same shape — and the same exception — every other framework with a
19
+ * legend makes. The cost is the one the bottleneck exists to avoid: the
20
+ * telemetry it owes is emitted outside `runCommand`. It is emitted ONCE, by the
21
+ * shared factory (`legend-toolbar.ts`), so the seven copies of those wire values
22
+ * that used to sit in seven files can no longer drift apart.
25
23
  *
26
24
  * ## Two modules on one element, and why
27
25
  *
@@ -66,35 +64,26 @@ export declare const c4BoardToolbarConfig: {
66
64
  readonly actions: [{
67
65
  readonly id: "a.toggle-resize";
68
66
  readonly tooltipWording: import("@formicoidea/labre-core/shared/services").ChromeWording;
69
- readonly icon: TemplateResult<1>;
67
+ readonly icon: import("lit-html").TemplateResult<1>;
70
68
  readonly active: (ctx: ToolbarContext) => boolean;
71
69
  readonly run: (ctx: ToolbarContext) => void;
72
- }, {
73
- id: string;
74
- placement: ActionPlacement;
75
- when: (ctx: ToolbarContext) => boolean;
76
- generate: (ctx: ToolbarContext) => {
77
- icon: TemplateResult;
78
- label: string;
79
- run: (runCtx: ToolbarContext) => void;
80
- };
81
- }];
70
+ }, import("@formicoidea/labre-core/shared/services").ToolbarActionGenerator];
82
71
  readonly when: (ctx: ToolbarContext) => boolean;
83
72
  };
84
73
  export declare const c4BoardToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
85
74
  /**
86
75
  * The legend button — the flag-gated half of the row (see the note above).
87
76
  *
88
- * `b.` sorts it after the resize toggle, so the two modules render as the one
89
- * row a user sees rather than in registration order.
77
+ * The SHARED factory, not a C4 copy of it: the button, the placement, the box
78
+ * and the one `FrameworkLegendCreated` emission all live in the surface block
79
+ * (`legend-toolbar.ts`, `docs/adr/0026`), and C4 hands it the board model to
80
+ * look for, whose commands to read the rows off, and the historical wire value.
81
+ *
82
+ * `b.` — the factory's default — sorts it after the resize toggle, so the two
83
+ * modules render as the one row a user sees rather than in registration order.
90
84
  */
91
85
  export declare const c4LegendToolbarConfig: {
92
- readonly actions: [{
93
- readonly id: "b.legend";
94
- readonly tooltipWording: import("@formicoidea/labre-core/shared/services").ChromeWording;
95
- readonly icon: TemplateResult<1>;
96
- readonly run: (ctx: ToolbarContext) => void;
97
- }];
86
+ readonly actions: [import("@formicoidea/labre-core/shared/services").ToolbarAction];
98
87
  readonly when: (ctx: ToolbarContext) => boolean;
99
88
  };
100
89
  /**
@@ -1,10 +1,8 @@
1
- import { EdgelessCRUDIdentifier, validationToolbarConfig, } from '@formicoidea/labre-core/blocks/surface';
2
- import { dddLegendIcon } from '@formicoidea/labre-ddd-shared';
1
+ import { EdgelessCRUDIdentifier, legendToolbarAction, validationToolbarConfig, } from '@formicoidea/labre-core/blocks/surface';
3
2
  import { C4BoardElementModel } from '@formicoidea/labre-core/model';
4
- import { ActionPlacement, BOARD_LEGEND_NOTATION, BOARD_RESIZE_TOGGLE, TelemetryProvider, ToolbarModuleExtension, translateKey, } from '@formicoidea/labre-core/shared/services';
5
- import { BlockFlavourIdentifier, getRegisteredCommands, runCommand, } from '@formicoidea/labre-core/std';
3
+ import { BOARD_RESIZE_TOGGLE, commandMoreAction, TelemetryProvider, ToolbarModuleExtension, translateKey, } from '@formicoidea/labre-core/shared/services';
4
+ import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
6
5
  import { html, nothing } from 'lit';
7
- import { createC4Legend } from '../actions.js';
8
6
  import { C4_BOARD_LEVEL_MENU } from '../levels.js';
9
7
  import { c4ExportMermaidIcon } from './icons.js';
10
8
  const ResizeIcon = html `<svg
@@ -56,55 +54,6 @@ const CheckIcon = html `<svg
56
54
  >
57
55
  <path d="M5 12.5l4.5 4.5L19 7.5" />
58
56
  </svg>`;
59
- const findCommand = (ctx, id) => getRegisteredCommands(ctx.std).find(candidate => candidate.id === id);
60
- /**
61
- * A "⋮" entry that INVOKES a registered command instead of restating what it
62
- * does — the shape `docs/adr/0010` M3 introduced, and the reason the export has
63
- * one behaviour, one availability rule and one telemetry emission whether it is
64
- * reached from here, from the catalogue, from the palette, from
65
- * Settings › Shortcuts or from the agent.
66
- *
67
- * Two things the widget imposes: a menu line is drawn from `label` (a tooltip on
68
- * a line that is already words would be a second copy of them), and
69
- * `placement: ActionPlacement.More` is what partitions it out of the row in the
70
- * first place — `renderToolbar` splits on exactly that flag, and the entries it
71
- * collects come from every module contributing to the element, so a framework
72
- * can add to the "⋮" without owning it.
73
- *
74
- * `generate` rather than a static entry because the i18n seam needs `std`:
75
- * `translateKey` is what reaches the host's catalogue, and a hard-coded English
76
- * label would be the one wording a host could not override.
77
- *
78
- * Lifted from the BPMN pool's row, where `bpmn.exportXml` sits in exactly this
79
- * position.
80
- */
81
- function commandMoreAction(id, commandId, labelKey, labelFallback, icon) {
82
- return {
83
- id,
84
- placement: ActionPlacement.More,
85
- when: (ctx) => {
86
- const command = findCommand(ctx, commandId);
87
- return command !== undefined && (command.when?.(ctx.std) ?? true);
88
- },
89
- generate: (ctx) => ({
90
- icon,
91
- label: translateKey(ctx.std, labelKey, labelFallback),
92
- run: (runCtx) => {
93
- const command = findCommand(runCtx, commandId);
94
- if (!command)
95
- return;
96
- // The same `source` the row's own entries report: the "⋮" is a
97
- // degradation of the row, not a surface of its own, and the
98
- // `ElementCreationSource` union deliberately names places rather than
99
- // widths (`docs/adr/0008`).
100
- runCommand(runCtx.std, command, {
101
- surface: 'contextual-toolbar',
102
- source: 'toolbar:general',
103
- });
104
- },
105
- }),
106
- };
107
- }
108
57
  /**
109
58
  * The selected C4 board's contextual toolbar: the resize toggle, and the
110
59
  * automatic legend of what is actually drawn on the board.
@@ -118,15 +67,14 @@ function commandMoreAction(id, commandId, labelKey, labelFallback, icon) {
118
67
  * The PO's call is that generating one belongs to a board you have SELECTED and
119
68
  * to nothing else: it is not an artefact to pick off a palette, and an entry in
120
69
  * a catalogue of things C4 draws would offer it to a user with no board in front
121
- * of them. So there is no `c4.legend` command, this button is the only way to
122
- * reach it, and the telemetry it owes is emitted by hand below.
70
+ * of them. So there is no `c4.legend` command, and this button is the only way
71
+ * to reach it.
123
72
  *
124
- * That is the same shape — and the same exception — the Context Map board makes
125
- * (`ddd-context-map/src/toolbar/board-config.ts`), down to the payload, so the
126
- * two frameworks' legends stay comparable on one dashboard. The cost is the one
127
- * the bottleneck exists to avoid and is accepted knowingly: this `track()` call
128
- * is a second emitter, and it is on whoever edits it to keep the wire values
129
- * matching what `reportCommandTelemetry` would have sent.
73
+ * That is the same shape — and the same exception — every other framework with a
74
+ * legend makes. The cost is the one the bottleneck exists to avoid: the
75
+ * telemetry it owes is emitted outside `runCommand`. It is emitted ONCE, by the
76
+ * shared factory (`legend-toolbar.ts`), so the seven copies of those wire values
77
+ * that used to sit in seven files can no longer drift apart.
130
78
  *
131
79
  * ## Two modules on one element, and why
132
80
  *
@@ -345,32 +293,21 @@ const levelPickerAction = {
345
293
  /**
346
294
  * The legend button — the flag-gated half of the row (see the note above).
347
295
  *
348
- * `b.` sorts it after the resize toggle, so the two modules render as the one
349
- * row a user sees rather than in registration order.
296
+ * The SHARED factory, not a C4 copy of it: the button, the placement, the box
297
+ * and the one `FrameworkLegendCreated` emission all live in the surface block
298
+ * (`legend-toolbar.ts`, `docs/adr/0026`), and C4 hands it the board model to
299
+ * look for, whose commands to read the rows off, and the historical wire value.
300
+ *
301
+ * `b.` — the factory's default — sorts it after the resize toggle, so the two
302
+ * modules render as the one row a user sees rather than in registration order.
350
303
  */
351
304
  export const c4LegendToolbarConfig = {
352
305
  actions: [
353
- {
354
- id: 'b.legend',
355
- tooltipWording: BOARD_LEGEND_NOTATION,
356
- icon: dddLegendIcon,
357
- run(ctx) {
358
- createC4Legend(ctx.std);
359
- ctx.std
360
- .getOptional(TelemetryProvider)
361
- ?.track('FrameworkLegendCreated', {
362
- // The WIRE values, and they are the ones `reportCommandTelemetry`
363
- // would have sent for a `kind: 'legend'` command — `framework` from
364
- // the descriptor's `telemetryKey`, `element: 'legend'` as Wardley
365
- // and the Context Map both emit, so the three are one metric.
366
- framework: 'c4',
367
- element: 'legend',
368
- page: 'whiteboard editor',
369
- segment: 'element toolbar',
370
- module: 'c4 toolbar',
371
- });
372
- },
373
- },
306
+ legendToolbarAction({
307
+ Model: C4BoardElementModel,
308
+ owner: 'c4',
309
+ framework: 'c4',
310
+ }),
374
311
  ],
375
312
  when: (ctx) => ctx.getSurfaceModelsByType(C4BoardElementModel).length > 0,
376
313
  };
@@ -67,11 +67,14 @@ export declare const c4ExportMermaidIcon: import("lit-html").TemplateResult<2>;
67
67
  *
68
68
  * {@link c4ExportMermaidIcon} is deliberately NOT here and is added to
69
69
  * `c4CommandIcons` in `commands.ts` instead: this record is the TOOLBOX — the
70
- * thirteen glyphs of things C4 DRAWS — and an export draws nothing.
70
+ * thirteen glyphs of things C4 DRAWS — and an export draws nothing. The same
71
+ * goes for {@link c4ToolbarIcon}, registered there under `c4.toolbar` for
72
+ * `FrameworkDescriptor.iconKey`; it is the one key that test exempts from the
73
+ * glyph ⇄ command bijection.
71
74
  *
72
75
  * The legend's glyph is in neither, because the legend is no longer a command at
73
- * all: it is `dddLegendIcon`, referenced straight from `config.ts` by the button
74
- * that is now the only way to reach it (PO arbitration, 27/08/2026).
76
+ * all: it is the surface block's own `legendIcon`, drawn by the shared button
77
+ * factory that is now the only way to reach it (PO arbitration, 27/08/2026).
75
78
  */
76
79
  export declare const C4_TOOLBOX_ICONS: {
77
80
  readonly 'c4.person': import("lit-html").TemplateResult<2>;
@@ -134,11 +134,14 @@ export const c4ExportMermaidIcon = svg `<svg width="24" height="24" viewBox="0 0
134
134
  *
135
135
  * {@link c4ExportMermaidIcon} is deliberately NOT here and is added to
136
136
  * `c4CommandIcons` in `commands.ts` instead: this record is the TOOLBOX — the
137
- * thirteen glyphs of things C4 DRAWS — and an export draws nothing.
137
+ * thirteen glyphs of things C4 DRAWS — and an export draws nothing. The same
138
+ * goes for {@link c4ToolbarIcon}, registered there under `c4.toolbar` for
139
+ * `FrameworkDescriptor.iconKey`; it is the one key that test exempts from the
140
+ * glyph ⇄ command bijection.
138
141
  *
139
142
  * The legend's glyph is in neither, because the legend is no longer a command at
140
- * all: it is `dddLegendIcon`, referenced straight from `config.ts` by the button
141
- * that is now the only way to reach it (PO arbitration, 27/08/2026).
143
+ * all: it is the surface block's own `legendIcon`, drawn by the shared button
144
+ * factory that is now the only way to reach it (PO arbitration, 27/08/2026).
142
145
  */
143
146
  export const C4_TOOLBOX_ICONS = {
144
147
  'c4.person': c4PersonIcon,