@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
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ // The declarations ARE the two frames: what they look like, where their plot is
2
+ // and which words they carry are all read off these two values (`docs/adr/0009`
3
+ // on why a framework declares rather than draws). Exported so a host — and the
4
+ // audit's own consumers — can answer those questions without a canvas.
5
+ export { C4_BOARD_BACKGROUND, C4_BOUNDARY_BACKGROUND } from './background.js';
6
+ export { BOUNDARY_LABEL, DESCRIPTION_PLACEHOLDER, NODE_LABEL, NODE_PALETTE, NODE_RADIUS, NODE_SIZE, NODE_STROKE_WIDTH, RELATIONSHIP_STROKE, RELATIONSHIP_WIDTH, } from './consts.js';
7
+ // The middle tier of an element's label, SEMI-DERIVED: the word from the kind,
8
+ // the technology from the author. Pure and total over the nine kinds and over
9
+ // every string an author can type, so a host, a rule or a reading can name an
10
+ // element exactly as the canvas does — and read the technology back out of it.
11
+ export { C4_TYPE_PLACEHOLDER, C4_TYPE_TAKES_TECHNOLOGY, C4_TYPE_WORD, c4MorphedTypeLine, c4TypeLine, normalizeC4TypeLine, technologyOfTypeLine, TYPE_TECHNOLOGY_PLACEHOLDER, } from './type-line.js';
12
+ // What a C4 shape is BORN as, shared by the palette and — since the morph — by
13
+ // the toolbar that says one artefact differently, so the two can never disagree
14
+ // about what a database looks like.
15
+ export { c4MorphClears, c4MorphProps, c4NodeProps, GLYPH_BODY_KINDS, } from './presets.js';
16
+ // What an artefact may BECOME: the declared families, the spec the generic
17
+ // morph module is registered with, and the resolution from a selected group to
18
+ // the shape inside it. Data a host can read without an editor.
19
+ export { C4_MORPH_FAMILIES, C4_MORPH_SPEC, c4MorphedTitle, c4NodeOfComponent, } from './morph.js';
20
+ // A C4 component — the shape and its own words, grouped. Where the two written
21
+ // tiers are placed at creation, and which of them belongs to which node. Pure,
22
+ // so the creation site, the exporter and a host all resolve a component the
23
+ // same way.
24
+ export { c4ComponentSiblings, c4ComponentTiers, c4StatedDescription, c4StatedName, c4StatedTechnology, c4TierBoxes, c4TierText, } from './component.js';
25
+ // Which of the three C4 diagrams a board declares it draws, and the words the
26
+ // picker offers them under — DATA, like the rules that read the fact.
27
+ export { C4_BOARD_LEVEL_MENU } from './levels.js';
28
+ // The levels of requirement, and the rules they arbitrate: DATA a host can
29
+ // read, ship and reason about without an editor.
30
+ export { C4_PROFILES } from './profiles.js';
31
+ export { C4_ROLE, C4_ROLE_OF_KIND, C4_ROLES, } from './roles.js';
32
+ // The half of the export that needs an editor, kept apart from the half that
33
+ // does not — same split BPMN's index makes for the same reason.
34
+ export { c4BoardsForExport, c4ExportBoardOf } from './actions.js';
35
+ // The export itself: a PURE function over element models — models in, mermaid
36
+ // out — so a host can export a board it never rendered, and a rule or a test can
37
+ // call it with plain stubs.
38
+ export { C4_MERMAID_OF_KIND, exportC4Mermaid, toMermaidAlias, toMermaidText, } from './export.js';
39
+ // C4's entries in the interchange registry (`docs/adr/0012`) — one today,
40
+ // mermaid OUT. Exported whole so a host can ask what C4 can read and write
41
+ // without mounting an editor, and call it without one either.
42
+ export { C4_INTERCHANGE, C4_MERMAID_EXPORT, C4_MERMAID_EXTENSION, C4_MERMAID_FORMAT, C4_MERMAID_MIME, c4BoardFrom, c4SafeFilename, } from './interchange.js';
43
+ export { C4_ELEMENT_MATRIX, C4_RELATIONSHIP_MATRIX, C4_RULES, } from './rules.js';
44
+ // The toolbox, for the host that composes the command registry and the
45
+ // translation-key manifest out of the frameworks it installed (see
46
+ // `packages/affine/all/src/{commands,translations}.ts`).
47
+ export { c4CommandIcons, c4Commands } from './commands.js';
48
+ export { c4TranslationEntries } from './translations.js';
49
+ // The legend TABLE — what the board's automatic legend can say. Exported for
50
+ // the same reason the two background declarations are: it answers "what does
51
+ // this framework document about itself" without a canvas.
52
+ export { C4_AUTO_LEGEND } from './legend.js';
@@ -0,0 +1,74 @@
1
+ import type { InterchangeCapability, InterchangeExportCapability, InterchangeFormat } from '@formicoidea/labre-core/blocks/surface';
2
+ import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
3
+ import { type C4ExportBoard } from './export.js';
4
+ /**
5
+ * C4's entries in the interchange registry (`docs/adr/0012`, P1).
6
+ *
7
+ * One entry today — mermaid OUT — declared exactly as BPMN declares its `.bpmn`
8
+ * export: C4 can write a mermaid C4 diagram and cannot yet read one, and the
9
+ * registry says so instead of leaving a reader to infer a symmetry nobody
10
+ * implemented.
11
+ *
12
+ * Everything here is pure. The serializer was already a pure function with no
13
+ * `std` in sight, and this file adds no editor to it: it only picks the
14
+ * artefacts the serializer speaks about out of the elements it is handed.
15
+ *
16
+ * One C4-specific reading of the contract: the SELECTION is expressed by the
17
+ * caller through which boards it includes in the element list. A C4 board is
18
+ * one level of one model, so `exportC4Mermaid` scopes to the boards it is
19
+ * given — every `c4Board` in the list becomes a document, and the caller that
20
+ * wants only the selected ones simply leaves the others out. A headless host
21
+ * that hands over the whole surface gets every board, one document each, which
22
+ * is the honest whole-surface answer for this framework.
23
+ */
24
+ /**
25
+ * mermaid C4 source. **Semantic** — the file carries a model, not a picture, so
26
+ * it takes the whole preservation contract the day an importer is written.
27
+ *
28
+ * `.mmd` is the extension the mermaid CLI and every editor plugin watch for.
29
+ * The format carries no `mime`, where `BPMN_XML_FORMAT` carries
30
+ * `application/xml`, and the difference is the truth rather than an oversight:
31
+ * mermaid has no registered media type, so there is none to declare. What a
32
+ * DOWNLOAD is served as is a different question with an answer — `text/plain`,
33
+ * because an invented type is a file some browsers refuse to save — and that
34
+ * answer is stated on the export result, where it belongs.
35
+ *
36
+ * The charset rides along with it because the text is the whole payload: unlike
37
+ * XML, a mermaid file has no prolog to declare its own encoding in.
38
+ */
39
+ export declare const C4_MERMAID_EXTENSION = ".mmd";
40
+ export declare const C4_MERMAID_MIME = "text/plain;charset=utf-8";
41
+ export declare const C4_MERMAID_FORMAT: InterchangeFormat;
42
+ /**
43
+ * The artefacts the exporter speaks about, picked out of a list of elements and
44
+ * kept in the order they were given.
45
+ *
46
+ * Six lists and not four: since the PO's recette of 28/08/2026 a C4 component
47
+ * is a shape, its three written tiers as canvas TEXT elements, and the GROUP
48
+ * that makes the four one thing — so the technology and the description the
49
+ * exporter writes are read off the canvas rather than off two model fields.
50
+ * Both lists are collected whole and unfiltered, because a tier belongs to its
51
+ * node through the group and not through the geometry (`component.ts`).
52
+ *
53
+ * Document order matters for the reason it does everywhere in this framework:
54
+ * it is the tie-break attribution breaks on — a centre inside two overlapping
55
+ * boundaries goes to the first — and the audit's `attribute()` breaks it the
56
+ * same way. Sorting here would make the export disagree with the badge the
57
+ * user can see.
58
+ */
59
+ export declare function c4BoardFrom(elements: readonly GfxPrimitiveElementModel[]): C4ExportBoard;
60
+ /**
61
+ * A name a file system will accept, minus the extension.
62
+ *
63
+ * The same transformations `bpmnSafeFilename` applies and for the same reasons
64
+ * — reserved characters become `-`, whitespace runs collapse, the result is
65
+ * capped, and the Windows tail of dots and spaces is trimmed AFTER the cap so
66
+ * the extension is not the thing that gets eaten. Duplicated rather than
67
+ * imported: a filename rule is not API, and coupling two frameworks to share
68
+ * eight lines would be the worse trade.
69
+ */
70
+ export declare function c4SafeFilename(raw: string | undefined): string;
71
+ /** `c4:mermaid:export` — C4's first capability. */
72
+ export declare const C4_MERMAID_EXPORT: InterchangeExportCapability;
73
+ /** Everything C4 registers, in one list the view extension can hand over. */
74
+ export declare const C4_INTERCHANGE: readonly InterchangeCapability[];
@@ -0,0 +1,143 @@
1
+ import { interchangeCapabilityId } from '@formicoidea/labre-core/blocks/surface';
2
+ import { C4BoardElementModel, C4BoundaryElementModel, C4NodeElementModel, ConnectorElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
3
+ import { exportC4Mermaid } from './export.js';
4
+ /**
5
+ * C4's entries in the interchange registry (`docs/adr/0012`, P1).
6
+ *
7
+ * One entry today — mermaid OUT — declared exactly as BPMN declares its `.bpmn`
8
+ * export: C4 can write a mermaid C4 diagram and cannot yet read one, and the
9
+ * registry says so instead of leaving a reader to infer a symmetry nobody
10
+ * implemented.
11
+ *
12
+ * Everything here is pure. The serializer was already a pure function with no
13
+ * `std` in sight, and this file adds no editor to it: it only picks the
14
+ * artefacts the serializer speaks about out of the elements it is handed.
15
+ *
16
+ * One C4-specific reading of the contract: the SELECTION is expressed by the
17
+ * caller through which boards it includes in the element list. A C4 board is
18
+ * one level of one model, so `exportC4Mermaid` scopes to the boards it is
19
+ * given — every `c4Board` in the list becomes a document, and the caller that
20
+ * wants only the selected ones simply leaves the others out. A headless host
21
+ * that hands over the whole surface gets every board, one document each, which
22
+ * is the honest whole-surface answer for this framework.
23
+ */
24
+ /* ── The format ───────────────────────────────────────────────────────── */
25
+ /**
26
+ * mermaid C4 source. **Semantic** — the file carries a model, not a picture, so
27
+ * it takes the whole preservation contract the day an importer is written.
28
+ *
29
+ * `.mmd` is the extension the mermaid CLI and every editor plugin watch for.
30
+ * The format carries no `mime`, where `BPMN_XML_FORMAT` carries
31
+ * `application/xml`, and the difference is the truth rather than an oversight:
32
+ * mermaid has no registered media type, so there is none to declare. What a
33
+ * DOWNLOAD is served as is a different question with an answer — `text/plain`,
34
+ * because an invented type is a file some browsers refuse to save — and that
35
+ * answer is stated on the export result, where it belongs.
36
+ *
37
+ * The charset rides along with it because the text is the whole payload: unlike
38
+ * XML, a mermaid file has no prolog to declare its own encoding in.
39
+ */
40
+ export const C4_MERMAID_EXTENSION = '.mmd';
41
+ export const C4_MERMAID_MIME = 'text/plain;charset=utf-8';
42
+ export const C4_MERMAID_FORMAT = {
43
+ id: 'mermaid',
44
+ tier: 'semantic',
45
+ extensions: [C4_MERMAID_EXTENSION],
46
+ };
47
+ /* ── Pure board helpers ───────────────────────────────────────────────── */
48
+ /**
49
+ * The artefacts the exporter speaks about, picked out of a list of elements and
50
+ * kept in the order they were given.
51
+ *
52
+ * Six lists and not four: since the PO's recette of 28/08/2026 a C4 component
53
+ * is a shape, its three written tiers as canvas TEXT elements, and the GROUP
54
+ * that makes the four one thing — so the technology and the description the
55
+ * exporter writes are read off the canvas rather than off two model fields.
56
+ * Both lists are collected whole and unfiltered, because a tier belongs to its
57
+ * node through the group and not through the geometry (`component.ts`).
58
+ *
59
+ * Document order matters for the reason it does everywhere in this framework:
60
+ * it is the tie-break attribution breaks on — a centre inside two overlapping
61
+ * boundaries goes to the first — and the audit's `attribute()` breaks it the
62
+ * same way. Sorting here would make the export disagree with the badge the
63
+ * user can see.
64
+ */
65
+ export function c4BoardFrom(elements) {
66
+ const boards = [];
67
+ const nodes = [];
68
+ const boundaries = [];
69
+ const connectors = [];
70
+ const texts = [];
71
+ const groups = [];
72
+ for (const element of elements) {
73
+ if (element instanceof C4BoardElementModel)
74
+ boards.push(element);
75
+ else if (element instanceof C4NodeElementModel)
76
+ nodes.push(element);
77
+ else if (element instanceof C4BoundaryElementModel)
78
+ boundaries.push(element);
79
+ else if (element instanceof ConnectorElementModel)
80
+ connectors.push(element);
81
+ else if (element instanceof TextElementModel)
82
+ texts.push(element);
83
+ else if (element instanceof GroupElementModel)
84
+ groups.push(element);
85
+ }
86
+ return { boards, nodes, boundaries, connectors, texts, groups };
87
+ }
88
+ /**
89
+ * A name a file system will accept, minus the extension.
90
+ *
91
+ * The same transformations `bpmnSafeFilename` applies and for the same reasons
92
+ * — reserved characters become `-`, whitespace runs collapse, the result is
93
+ * capped, and the Windows tail of dots and spaces is trimmed AFTER the cap so
94
+ * the extension is not the thing that gets eaten. Duplicated rather than
95
+ * imported: a filename rule is not API, and coupling two frameworks to share
96
+ * eight lines would be the worse trade.
97
+ */
98
+ export function c4SafeFilename(raw) {
99
+ const safe = (raw ?? '')
100
+ .trim()
101
+ .replaceAll(/[\\/:*?"<>|]/g, '-')
102
+ .replaceAll(/\s+/g, ' ')
103
+ .trim()
104
+ .slice(0, 120)
105
+ .replace(/[. ]+$/, '');
106
+ return safe || 'diagram';
107
+ }
108
+ /* ── The capability ───────────────────────────────────────────────────── */
109
+ /**
110
+ * The board as a mermaid C4 document.
111
+ *
112
+ * A thin adapter and nothing else: it picks the C4 artefacts out of the
113
+ * elements it was handed, runs {@link exportC4Mermaid} — unchanged — and names
114
+ * the file. There is no second door: `c4.exportMermaid` calls THIS, so the
115
+ * command and the registry cannot produce different bytes, different filenames
116
+ * or different content types — there is nowhere for them to differ.
117
+ *
118
+ * No `warnings`, where the BPMN exporter has some: the C4 writer drops nothing
119
+ * it could have written down. What it leaves out — a neutral connector, a shape
120
+ * with no role, a relationship whose far end is on another board — is not a
121
+ * loss but a statement the author never made (`docs/adr/0010`), and a warning
122
+ * about it would be the export telling the user off for their own drawing.
123
+ */
124
+ const runC4MermaidExport = (elements, context) => {
125
+ const name = c4SafeFilename(context.name);
126
+ return {
127
+ text: exportC4Mermaid(c4BoardFrom(elements)),
128
+ filename: `${name}${C4_MERMAID_EXTENSION}`,
129
+ mime: C4_MERMAID_MIME,
130
+ };
131
+ };
132
+ /** `c4:mermaid:export` — C4's first capability. */
133
+ export const C4_MERMAID_EXPORT = {
134
+ id: interchangeCapabilityId('c4', C4_MERMAID_FORMAT.id, 'export'),
135
+ framework: 'c4',
136
+ format: C4_MERMAID_FORMAT,
137
+ direction: 'export',
138
+ run: runC4MermaidExport,
139
+ };
140
+ /** Everything C4 registers, in one list the view extension can hand over. */
141
+ export const C4_INTERCHANGE = [
142
+ C4_MERMAID_EXPORT,
143
+ ];
@@ -0,0 +1,37 @@
1
+ import { type AutoLegendSpec } from '@formicoidea/labre-ddd-shared';
2
+ /**
3
+ * What the C4 board's automatic legend can say — a TABLE, and nothing else: the
4
+ * scan, the placement and the box are `createAutoLegend`'s job, shared with the
5
+ * three DDD boards.
6
+ *
7
+ * Every row is DERIVED twice over: its WORDING from the role vocabulary's own
8
+ * `labelFallback` ({@link roleLabel}), and its COLOUR from the very palette the
9
+ * creation site paints with ({@link NODE_PALETTE}). Neither is restated here, so
10
+ * renaming a role renames its legend row and restyling the pack restyles its
11
+ * swatches — which is what keeps a legend a description of the board rather than
12
+ * a second opinion about it.
13
+ *
14
+ * ## Why `exact` on the container, and only there
15
+ *
16
+ * `c4:database` specialises `c4:container`, so an inclusive entry on the parent
17
+ * would put a "Container" row — with the container's blue square — on a board
18
+ * carrying nothing but cylinders. The row would name a shape that is nowhere on
19
+ * the diagram. EDGY's four base kinds hit this first and `AutoLegendEntry.exact`
20
+ * exists for it: base and specialisation become two separate statements, each
21
+ * listed when it is the thing the user actually drew.
22
+ *
23
+ * The other four element roles are childless, so `exact` would change nothing
24
+ * for them and is left off — the flag marks the places where the distinction is
25
+ * real.
26
+ *
27
+ * ## What is missing, and why
28
+ *
29
+ * `mobile` and `browser` get no row of their own: they carry `c4:container`,
30
+ * because a phone app and a single-page app are containers with a picture
31
+ * (`roles.ts`). Detection is by role and only by role, so a board of nothing but
32
+ * mobile apps lists "Container" — which is the honest answer about what the
33
+ * boxes MEAN. `person-ext` and `system-ext` collapse the same way onto person
34
+ * and system, for the same reason: the grey says "out of scope", not "a
35
+ * different sort of thing".
36
+ */
37
+ export declare const C4_AUTO_LEGEND: AutoLegendSpec;
package/dist/legend.js ADDED
@@ -0,0 +1,123 @@
1
+ import { roleLabel } from '@formicoidea/labre-ddd-shared';
2
+ import { BOUNDARY_STROKE, NODE_PALETTE, RELATIONSHIP_STROKE } from './consts.js';
3
+ import { C4_ROLE, C4_ROLES } from './roles.js';
4
+ /**
5
+ * What the C4 board's automatic legend can say — a TABLE, and nothing else: the
6
+ * scan, the placement and the box are `createAutoLegend`'s job, shared with the
7
+ * three DDD boards.
8
+ *
9
+ * Every row is DERIVED twice over: its WORDING from the role vocabulary's own
10
+ * `labelFallback` ({@link roleLabel}), and its COLOUR from the very palette the
11
+ * creation site paints with ({@link NODE_PALETTE}). Neither is restated here, so
12
+ * renaming a role renames its legend row and restyling the pack restyles its
13
+ * swatches — which is what keeps a legend a description of the board rather than
14
+ * a second opinion about it.
15
+ *
16
+ * ## Why `exact` on the container, and only there
17
+ *
18
+ * `c4:database` specialises `c4:container`, so an inclusive entry on the parent
19
+ * would put a "Container" row — with the container's blue square — on a board
20
+ * carrying nothing but cylinders. The row would name a shape that is nowhere on
21
+ * the diagram. EDGY's four base kinds hit this first and `AutoLegendEntry.exact`
22
+ * exists for it: base and specialisation become two separate statements, each
23
+ * listed when it is the thing the user actually drew.
24
+ *
25
+ * The other four element roles are childless, so `exact` would change nothing
26
+ * for them and is left off — the flag marks the places where the distinction is
27
+ * real.
28
+ *
29
+ * ## What is missing, and why
30
+ *
31
+ * `mobile` and `browser` get no row of their own: they carry `c4:container`,
32
+ * because a phone app and a single-page app are containers with a picture
33
+ * (`roles.ts`). Detection is by role and only by role, so a board of nothing but
34
+ * mobile apps lists "Container" — which is the honest answer about what the
35
+ * boxes MEAN. `person-ext` and `system-ext` collapse the same way onto person
36
+ * and system, for the same reason: the grey says "out of scope", not "a
37
+ * different sort of thing".
38
+ */
39
+ export const C4_AUTO_LEGEND = {
40
+ title: 'Legend',
41
+ width: 290,
42
+ roles: C4_ROLES,
43
+ sections: [
44
+ {
45
+ title: 'Elements',
46
+ entries: [
47
+ {
48
+ role: C4_ROLE.person,
49
+ row: {
50
+ swatch: 'square',
51
+ color: NODE_PALETTE.person.fill,
52
+ label: roleLabel(C4_ROLES, C4_ROLE.person),
53
+ },
54
+ },
55
+ {
56
+ role: C4_ROLE.system,
57
+ row: {
58
+ swatch: 'square',
59
+ color: NODE_PALETTE.system.fill,
60
+ label: roleLabel(C4_ROLES, C4_ROLE.system),
61
+ },
62
+ },
63
+ {
64
+ role: C4_ROLE.container,
65
+ // See the note above: a board of cylinders must not claim a container.
66
+ exact: true,
67
+ row: {
68
+ swatch: 'square',
69
+ color: NODE_PALETTE.container.fill,
70
+ label: roleLabel(C4_ROLES, C4_ROLE.container),
71
+ },
72
+ },
73
+ {
74
+ role: C4_ROLE.database,
75
+ row: {
76
+ swatch: 'square',
77
+ color: NODE_PALETTE.database.fill,
78
+ label: roleLabel(C4_ROLES, C4_ROLE.database),
79
+ },
80
+ },
81
+ {
82
+ role: C4_ROLE.component,
83
+ row: {
84
+ swatch: 'square',
85
+ color: NODE_PALETTE.component.fill,
86
+ label: roleLabel(C4_ROLES, C4_ROLE.component),
87
+ },
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ title: 'Frames',
93
+ entries: [
94
+ {
95
+ role: C4_ROLE.boundary,
96
+ row: {
97
+ // A line, not a square: a boundary has no body — it is the dashed
98
+ // frame itself, and a filled swatch would draw the one thing this
99
+ // background deliberately does not paint.
100
+ swatch: 'line',
101
+ color: BOUNDARY_STROKE,
102
+ dashed: true,
103
+ label: roleLabel(C4_ROLES, C4_ROLE.boundary),
104
+ },
105
+ },
106
+ ],
107
+ },
108
+ {
109
+ title: 'Relations',
110
+ entries: [
111
+ {
112
+ role: C4_ROLE.relationship,
113
+ row: {
114
+ swatch: 'line',
115
+ color: RELATIONSHIP_STROKE,
116
+ dashed: true,
117
+ label: roleLabel(C4_ROLES, C4_ROLE.relationship),
118
+ },
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ };
@@ -0,0 +1,70 @@
1
+ import type { C4BoardLevel } from '@formicoidea/labre-core/model';
2
+ /**
3
+ * The LEVEL a board declares, as the picker offers it — DATA, like everything
4
+ * else this framework contributes.
5
+ *
6
+ * A C4 diagram is drawn at one level: the context of a system, the containers
7
+ * inside it, the components inside one of those. Which one a sheet is showing
8
+ * used to live nowhere the tool could read — the title is free text, and a board
9
+ * called "Payments" says nothing about which of the three it is. So the board
10
+ * carries an optional `level` (`C4BoardElementModel`), this table is the words
11
+ * for it, and `rules.ts` is what judges the sheet against it.
12
+ *
13
+ * ## The title stays the author's
14
+ *
15
+ * Choosing a level RENAMES NOTHING. The level is a declared fact sitting beside
16
+ * the title, not a replacement for it: a context diagram of the payments
17
+ * platform is still called whatever its author called it, and the picker writes
18
+ * one prop and touches no other.
19
+ *
20
+ * ## Why "Free sketch" is an option and not the absence of one
21
+ *
22
+ * The default is no level at all, and it has to be REACHABLE: a user who set a
23
+ * level and then decided the sheet is a working surface after all must be able
24
+ * to say so, and the way back cannot be "delete the board and draw another".
25
+ * Picking it CLEARS the field rather than writing a fourth value — the same call
26
+ * the validation profile picker makes for its default level of requirement — so
27
+ * a board on Free sketch is byte-identical to every C4 board drawn before this
28
+ * existed.
29
+ *
30
+ * ## All four C's are offered, `code` included
31
+ *
32
+ * C4 is named after Context, Containers, Components and Code, and the picker
33
+ * says all four. The fourth is the one C4's own author tells you to skip unless
34
+ * the picture is generated from the source, and this editor draws no code-level
35
+ * artefact — but that is a fact about the TOOLING, not about what a sheet may
36
+ * declare. An architect keeping a board for the class diagram they paste in from
37
+ * elsewhere is entitled to say what it is, and a picker that refused would be
38
+ * presenting our limitation as the notation's.
39
+ *
40
+ * Two of the four are judged by no rule at all — `component` and `code` — and
41
+ * `rules.ts` says why: a component diagram legitimately shows everything C4
42
+ * names, and nothing in the pack knows what a code diagram admits. Both are
43
+ * declarations the tool records and does not check, which is a perfectly honest
44
+ * thing for a declaration to be.
45
+ */
46
+ export interface C4BoardLevelOption {
47
+ /**
48
+ * What gets written on the board — or `undefined`, which CLEARS the field and
49
+ * puts the sheet back to a free sketch.
50
+ */
51
+ level: C4BoardLevel | undefined;
52
+ /** i18n key of the entry's words; resolved by the host. */
53
+ labelKey: string;
54
+ /** The framework's own wording, for a host that ships no catalogue. */
55
+ labelFallback: string;
56
+ }
57
+ /**
58
+ * The picker, whole: its own heading and its five entries, in the order a
59
+ * reader zooms — no level, then the four C's from out to in.
60
+ *
61
+ * One object rather than a loose array plus a stray heading constant, because
62
+ * the manifest walks DECLARATIONS: `c4TranslationEntries` hands this value to
63
+ * `collectTranslationKeys` and every key below reaches a host's catalogue with
64
+ * nothing restated anywhere.
65
+ */
66
+ export declare const C4_BOARD_LEVEL_MENU: {
67
+ labelKey: string;
68
+ labelFallback: string;
69
+ options: readonly C4BoardLevelOption[];
70
+ };
package/dist/levels.js ADDED
@@ -0,0 +1,46 @@
1
+ /** i18n key stem: `context` → `com.labre.c4.level.context`. */
2
+ const levelKey = (name) => `com.labre.c4.level.${name}`;
3
+ /**
4
+ * The picker, whole: its own heading and its five entries, in the order a
5
+ * reader zooms — no level, then the four C's from out to in.
6
+ *
7
+ * One object rather than a loose array plus a stray heading constant, because
8
+ * the manifest walks DECLARATIONS: `c4TranslationEntries` hands this value to
9
+ * `collectTranslationKeys` and every key below reaches a host's catalogue with
10
+ * nothing restated anywhere.
11
+ */
12
+ export const C4_BOARD_LEVEL_MENU = {
13
+ labelKey: levelKey('section'),
14
+ labelFallback: 'Level',
15
+ options: [
16
+ {
17
+ // The default, and the only entry that writes nothing.
18
+ level: undefined,
19
+ labelKey: levelKey('none'),
20
+ labelFallback: 'Free sketch',
21
+ },
22
+ {
23
+ level: 'context',
24
+ labelKey: levelKey('context'),
25
+ labelFallback: 'Context',
26
+ },
27
+ {
28
+ level: 'container',
29
+ labelKey: levelKey('container'),
30
+ labelFallback: 'Container',
31
+ },
32
+ {
33
+ level: 'component',
34
+ labelKey: levelKey('component'),
35
+ labelFallback: 'Component',
36
+ },
37
+ {
38
+ // The fourth C. Recorded, exported and readable; judged by no rule — see
39
+ // the header, and `rules.ts` on why an empty forbidden list is data that
40
+ // can never fire.
41
+ level: 'code',
42
+ labelKey: levelKey('code'),
43
+ labelFallback: 'Code',
44
+ },
45
+ ],
46
+ };
@@ -0,0 +1,89 @@
1
+ import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
2
+ import { C4NodeElementModel, type C4NodeKind } from '@formicoidea/labre-core/model';
3
+ import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
+ /**
5
+ * What a C4 artefact may BECOME — the declaration behind the "Change type"
6
+ * dropdown on a selected component's contextual toolbar.
7
+ *
8
+ * ## Families, and the one thing that makes them safe here
9
+ *
10
+ * A person and an external person are the same actor, said with a different
11
+ * ownership; a container, a database, a mobile app and a browser app are the
12
+ * same level of the model, drawn with different silhouettes. Realising halfway
13
+ * through a container diagram that the box should have been the cylinder is
14
+ * modelling, not a mistake — and the only way through it today is delete,
15
+ * re-draw, re-connect, and retype three tiers of words.
16
+ *
17
+ * The three families below are DATA. Nothing derives them, and the role tree
18
+ * would have been the wrong source in the same way it is for BPMN: `roleIsA`
19
+ * makes `c4:database` a `c4:container`, so a derivation would reach in one
20
+ * direction and not the other, and it says nothing at all about
21
+ * `person`/`person-ext`, which are ONE role and still two artefacts.
22
+ *
23
+ * What makes these three families cheap is a property they happen to have and
24
+ * a later one may not: **every member of a family lays its words out
25
+ * identically**. `c4TierBoxes` differs on exactly one thing — the person's head
26
+ * clearance, which both people share — and `NODE_SIZE` on exactly one, the
27
+ * person's taller silhouette, which both people also share. So a morph inside
28
+ * a family needs no re-layout at all: the three tiers stay where they are, the
29
+ * group's derived box stays what it was, and the geometry a morph promises not
30
+ * to touch is genuinely untouched. `component` is deliberately in NO family
31
+ * partly for that reason and mostly for a better one: a component is a part of
32
+ * a container, not another drawing of one, and offering the swap would invite
33
+ * a diagram that mixes two levels of the model — the one thing C4 exists to
34
+ * stop. The two boundaries are frames rather than artefacts and are excluded
35
+ * for the reason `roles.ts` gives.
36
+ */
37
+ export declare const C4_MORPH_FAMILIES: readonly (readonly C4NodeKind[])[];
38
+ /**
39
+ * The `c4Node` shape a selected GROUP is the component of — `undefined` when
40
+ * the group is not one.
41
+ *
42
+ * A C4 component is a native `group` holding the shape and its three lines of
43
+ * words (PO recette, 28/08/2026), so what a click selects is the group and what
44
+ * carries `kind` is the shape. This is the whole of the indirection, and it is
45
+ * also the gate: a plain group somebody lassoed round three rectangles, a
46
+ * Wardley component (a group of a circle and its label) and a group of two C4
47
+ * components all answer `undefined`, so none of them is ever offered the menu.
48
+ *
49
+ * TWO shapes is a refusal rather than a first-wins pick. A group holding two
50
+ * components is a group somebody made of two components — morphing "it" would
51
+ * mean picking one of them by document order, and the honest answer to an
52
+ * ambiguous selection is the same one the generic module gives to a mixed
53
+ * family: nothing.
54
+ */
55
+ export declare function c4NodeOfComponent(model: GfxPrimitiveElementModel): C4NodeElementModel | undefined;
56
+ /**
57
+ * The name a component should carry once its shape has morphed — or `null`
58
+ * when the name is the AUTHOR's and must not be touched.
59
+ *
60
+ * Exactly one case rewrites: the title is the source kind's own creation
61
+ * prompt, letter for letter, which is what a component nobody has named still
62
+ * says. Everything else — a name typed over it, a prompt with a word added, a
63
+ * cleared title, another kind's prompt — is content, and content survives a
64
+ * morph untouched.
65
+ *
66
+ * This is the tier the type line does NOT cover, and the only one where a user
67
+ * sees the contradiction: {@link NODE_LABEL} differs across the container
68
+ * family where `C4_TYPE_WORD` does not, so a fresh container morphed to a
69
+ * database would otherwise be a cylinder captioned "Container".
70
+ *
71
+ * Pure and total over every string, like its sibling in `type-line.ts`: the
72
+ * input is a canvas text element somebody may have typed anything into.
73
+ */
74
+ export declare function c4MorphedTitle(from: C4NodeKind, to: C4NodeKind, rawText: string | null | undefined): string | null;
75
+ /**
76
+ * C4's morph declaration, handed to the generic `morphToolbarConfig`.
77
+ *
78
+ * `modelType` is the native `GroupElementModel` and not `C4NodeElementModel`,
79
+ * because the group is what a click on a component selects. Everything that
80
+ * makes that safe is in {@link c4NodeOfComponent}: the toolbar's own
81
+ * homogeneity test only proves that every selected element is A group, and the
82
+ * resolution proves that every one of them is one of OURS.
83
+ *
84
+ * `propsOf` is the shipped creation builder minus `type` / `xywh` / `text`
85
+ * ({@link c4MorphProps}), so the palette and the morph cannot disagree about
86
+ * what a database looks like; `clearOf` is empty on today's table and derived
87
+ * rather than assumed so (`presets.ts`).
88
+ */
89
+ export declare const C4_MORPH_SPEC: MorphSpec<C4NodeKind>;