@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.
- package/dist/actions.d.ts +179 -0
- package/dist/actions.js +375 -0
- package/dist/background.d.ts +77 -0
- package/dist/background.js +223 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +221 -0
- package/dist/component.d.ts +192 -0
- package/dist/component.js +188 -0
- package/dist/consts.d.ts +331 -0
- package/dist/consts.js +384 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +18 -0
- package/dist/element-renderer.js +14 -0
- package/dist/element-view.d.ts +51 -0
- package/dist/element-view.js +146 -0
- package/dist/export.d.ts +184 -0
- package/dist/export.js +454 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +52 -0
- package/dist/interchange.d.ts +74 -0
- package/dist/interchange.js +143 -0
- package/dist/legend.d.ts +37 -0
- package/dist/legend.js +123 -0
- package/dist/levels.d.ts +70 -0
- package/dist/levels.js +46 -0
- package/dist/morph.d.ts +89 -0
- package/dist/morph.js +229 -0
- package/dist/node/node-renderer.d.ts +6 -0
- package/dist/node/node-renderer.js +304 -0
- package/dist/node/node-view.d.ts +45 -0
- package/dist/node/node-view.js +80 -0
- package/dist/node/type-line-watcher.d.ts +70 -0
- package/dist/node/type-line-watcher.js +142 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.js +149 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +177 -0
- package/dist/roles.d.ts +116 -0
- package/dist/roles.js +303 -0
- package/dist/rules.d.ts +95 -0
- package/dist/rules.js +1261 -0
- package/dist/toolbar/c4-menu.d.ts +11 -0
- package/dist/toolbar/c4-menu.js +14 -0
- package/dist/toolbar/c4-senior-button.d.ts +19 -0
- package/dist/toolbar/c4-senior-button.js +23 -0
- package/dist/toolbar/config.d.ts +150 -0
- package/dist/toolbar/config.js +436 -0
- package/dist/toolbar/icons.d.ts +90 -0
- package/dist/toolbar/icons.js +157 -0
- package/dist/toolbar/senior-tool.d.ts +1 -0
- package/dist/toolbar/senior-tool.js +11 -0
- package/dist/translations.d.ts +18 -0
- package/dist/translations.js +42 -0
- package/dist/type-line.d.ts +175 -0
- package/dist/type-line.js +244 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +148 -0
- package/package.json +34 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
+
/**
|
|
3
|
+
* Keeps a component's type line SEMI-DERIVED: the author owns the technology,
|
|
4
|
+
* the notation owns the word.
|
|
5
|
+
*
|
|
6
|
+
* ## The rule, and where it is applied
|
|
7
|
+
*
|
|
8
|
+
* `[Container: Java]` is two statements in one line. Which of the four levels
|
|
9
|
+
* this box is, is the diagram's — it comes from `kind`, it is what the renderer
|
|
10
|
+
* paints and what the exporter maps, and an author retyping it would let the
|
|
11
|
+
* words disagree with the picture. What it is built with is theirs, and it is
|
|
12
|
+
* the only half the notation leaves to them (PO arbitration, 28/08/2026).
|
|
13
|
+
*
|
|
14
|
+
* Since the tier is a real canvas text element that half is typed IN PLACE, on
|
|
15
|
+
* the picture, with nothing standing between the keyboard and the document. So
|
|
16
|
+
* this watcher reads back what was typed, keeps the technology, and rewrites the
|
|
17
|
+
* canonical line: `Java` becomes `[Container: Java]`, `[Person: Java]` on a
|
|
18
|
+
* container becomes `[Container: Java]`, an emptied line becomes `[Container]`.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the seam is the SELECTION and not `elementUpdated`
|
|
21
|
+
*
|
|
22
|
+
* The obvious hook is `surface.elementUpdated` filtered on `props.text`, which
|
|
23
|
+
* is how every other reaction in this library is wired. It is the wrong one
|
|
24
|
+
* here, and visibly so: a canvas text editor binds its inline editor straight
|
|
25
|
+
* onto the element's `Y.Text`, so that signal fires once per KEYSTROKE. A
|
|
26
|
+
* normalizer on it would rewrite the line under the cursor while somebody was
|
|
27
|
+
* still typing into it — an author who has got as far as `[Container: Ja` would
|
|
28
|
+
* have their caret thrown by a rewrite of the very text they are editing.
|
|
29
|
+
*
|
|
30
|
+
* "The edit committed" is therefore read where the editor actually says so: the
|
|
31
|
+
* text editor holds an EDITING selection for as long as it is mounted and drops
|
|
32
|
+
* it when it closes, so an id leaving the editing set is a commit and nothing
|
|
33
|
+
* else is. One rewrite per edit, after the editor is gone, and a single undo
|
|
34
|
+
* entry for it.
|
|
35
|
+
*
|
|
36
|
+
* ## Always-on, and local-only
|
|
37
|
+
*
|
|
38
|
+
* Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): this
|
|
39
|
+
* does not author anything: it keeps an element that is ALREADY in the document
|
|
40
|
+
* readable while somebody edits it, and a diagram drawn while the C4 button was
|
|
41
|
+
* on must stay editable when it goes off. It is also inherently local — a
|
|
42
|
+
* selection is this peer's own, so there is no fleet of observers to filter out
|
|
43
|
+
* the way `EdgyRelationResolver` has to.
|
|
44
|
+
*/
|
|
45
|
+
export declare class C4TypeLineWatcher extends InteractivityExtension {
|
|
46
|
+
static key: string;
|
|
47
|
+
private _subscription;
|
|
48
|
+
private _editing;
|
|
49
|
+
mounted(): void;
|
|
50
|
+
unmounted(): void;
|
|
51
|
+
/** The kind of the C4 shape this element is grouped with, if there is one. */
|
|
52
|
+
private _kindOf;
|
|
53
|
+
private _normalize;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Which ids just stopped being edited, and what is being edited now.
|
|
57
|
+
*
|
|
58
|
+
* Split out as a pure function because it is the whole of the seam's logic and
|
|
59
|
+
* the only part of it worth being wrong about: an id that appears in the new
|
|
60
|
+
* editing set has merely started, an id in both is still going, and only one
|
|
61
|
+
* that has LEFT is a commit. Kept total over an empty selection, which is what
|
|
62
|
+
* arrives when the editor closes onto bare canvas.
|
|
63
|
+
*/
|
|
64
|
+
export declare function c4EditingTransition(previous: ReadonlySet<string>, selections: readonly {
|
|
65
|
+
elements: readonly string[];
|
|
66
|
+
editing?: boolean;
|
|
67
|
+
}[]): {
|
|
68
|
+
editing: Set<string>;
|
|
69
|
+
left: string[];
|
|
70
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { C4NodeElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
import { c4ComponentSiblings } from '../component.js';
|
|
4
|
+
import { C4_ROLE } from '../roles.js';
|
|
5
|
+
import { normalizeC4TypeLine } from '../type-line.js';
|
|
6
|
+
/**
|
|
7
|
+
* Keeps a component's type line SEMI-DERIVED: the author owns the technology,
|
|
8
|
+
* the notation owns the word.
|
|
9
|
+
*
|
|
10
|
+
* ## The rule, and where it is applied
|
|
11
|
+
*
|
|
12
|
+
* `[Container: Java]` is two statements in one line. Which of the four levels
|
|
13
|
+
* this box is, is the diagram's — it comes from `kind`, it is what the renderer
|
|
14
|
+
* paints and what the exporter maps, and an author retyping it would let the
|
|
15
|
+
* words disagree with the picture. What it is built with is theirs, and it is
|
|
16
|
+
* the only half the notation leaves to them (PO arbitration, 28/08/2026).
|
|
17
|
+
*
|
|
18
|
+
* Since the tier is a real canvas text element that half is typed IN PLACE, on
|
|
19
|
+
* the picture, with nothing standing between the keyboard and the document. So
|
|
20
|
+
* this watcher reads back what was typed, keeps the technology, and rewrites the
|
|
21
|
+
* canonical line: `Java` becomes `[Container: Java]`, `[Person: Java]` on a
|
|
22
|
+
* container becomes `[Container: Java]`, an emptied line becomes `[Container]`.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the seam is the SELECTION and not `elementUpdated`
|
|
25
|
+
*
|
|
26
|
+
* The obvious hook is `surface.elementUpdated` filtered on `props.text`, which
|
|
27
|
+
* is how every other reaction in this library is wired. It is the wrong one
|
|
28
|
+
* here, and visibly so: a canvas text editor binds its inline editor straight
|
|
29
|
+
* onto the element's `Y.Text`, so that signal fires once per KEYSTROKE. A
|
|
30
|
+
* normalizer on it would rewrite the line under the cursor while somebody was
|
|
31
|
+
* still typing into it — an author who has got as far as `[Container: Ja` would
|
|
32
|
+
* have their caret thrown by a rewrite of the very text they are editing.
|
|
33
|
+
*
|
|
34
|
+
* "The edit committed" is therefore read where the editor actually says so: the
|
|
35
|
+
* text editor holds an EDITING selection for as long as it is mounted and drops
|
|
36
|
+
* it when it closes, so an id leaving the editing set is a commit and nothing
|
|
37
|
+
* else is. One rewrite per edit, after the editor is gone, and a single undo
|
|
38
|
+
* entry for it.
|
|
39
|
+
*
|
|
40
|
+
* ## Always-on, and local-only
|
|
41
|
+
*
|
|
42
|
+
* Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): this
|
|
43
|
+
* does not author anything: it keeps an element that is ALREADY in the document
|
|
44
|
+
* readable while somebody edits it, and a diagram drawn while the C4 button was
|
|
45
|
+
* on must stay editable when it goes off. It is also inherently local — a
|
|
46
|
+
* selection is this peer's own, so there is no fleet of observers to filter out
|
|
47
|
+
* the way `EdgyRelationResolver` has to.
|
|
48
|
+
*/
|
|
49
|
+
export class C4TypeLineWatcher extends InteractivityExtension {
|
|
50
|
+
constructor() {
|
|
51
|
+
super(...arguments);
|
|
52
|
+
this._subscription = null;
|
|
53
|
+
this._editing = new Set();
|
|
54
|
+
}
|
|
55
|
+
static { this.key = 'c4-type-line-watcher'; }
|
|
56
|
+
mounted() {
|
|
57
|
+
this._subscription = this.gfx.selection.slots.updated.subscribe(selections => {
|
|
58
|
+
const { editing, left } = c4EditingTransition(this._editing, selections);
|
|
59
|
+
this._editing = editing;
|
|
60
|
+
for (const id of left)
|
|
61
|
+
this._normalize(id);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
unmounted() {
|
|
65
|
+
this._subscription?.unsubscribe();
|
|
66
|
+
this._subscription = null;
|
|
67
|
+
this._editing = new Set();
|
|
68
|
+
super.unmounted();
|
|
69
|
+
}
|
|
70
|
+
/** The kind of the C4 shape this element is grouped with, if there is one. */
|
|
71
|
+
_kindOf(elementId) {
|
|
72
|
+
const surface = this.gfx.surface;
|
|
73
|
+
if (!surface)
|
|
74
|
+
return null;
|
|
75
|
+
const groups = surface.elementModels.filter((element) => element instanceof GroupElementModel);
|
|
76
|
+
for (const siblingId of c4ComponentSiblings(elementId, groups)) {
|
|
77
|
+
const sibling = surface.getElementById(siblingId);
|
|
78
|
+
if (sibling instanceof C4NodeElementModel)
|
|
79
|
+
return sibling.kind;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
_normalize(elementId) {
|
|
84
|
+
// A read-only document is READ: every write below would be refused, and the
|
|
85
|
+
// author cannot have typed anything to normalize in the first place.
|
|
86
|
+
if (this.std.store.readonly)
|
|
87
|
+
return;
|
|
88
|
+
const surface = this.gfx.surface;
|
|
89
|
+
if (!surface)
|
|
90
|
+
return;
|
|
91
|
+
const element = surface.getElementById(elementId);
|
|
92
|
+
if (!(element instanceof TextElementModel))
|
|
93
|
+
return;
|
|
94
|
+
if (element.role !== C4_ROLE['type-line'])
|
|
95
|
+
return;
|
|
96
|
+
if (element.isLocked())
|
|
97
|
+
return;
|
|
98
|
+
const kind = this._kindOf(elementId);
|
|
99
|
+
// A type line whose shape is gone — the node deleted, the group released
|
|
100
|
+
// and the text dragged away — has no kind to derive a word from. Left
|
|
101
|
+
// exactly as the author typed it: guessing at a level would be this watcher
|
|
102
|
+
// asserting something nobody drew.
|
|
103
|
+
if (!kind)
|
|
104
|
+
return;
|
|
105
|
+
const raw = element.text.toString();
|
|
106
|
+
const canonical = normalizeC4TypeLine(kind, raw);
|
|
107
|
+
if (canonical === raw)
|
|
108
|
+
return;
|
|
109
|
+
this.std.store.captureSync();
|
|
110
|
+
// Mutated IN PLACE rather than replaced: the `Y.Text` instance is what the
|
|
111
|
+
// element's own change watcher and any bound view hold, and swapping it for
|
|
112
|
+
// a fresh one would leave both pointing at a text nobody is editing. One
|
|
113
|
+
// transaction, so the rewrite is one undo entry and not two.
|
|
114
|
+
this.std.store.transact(() => {
|
|
115
|
+
element.text.delete(0, element.text.length);
|
|
116
|
+
element.text.insert(0, canonical);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Which ids just stopped being edited, and what is being edited now.
|
|
122
|
+
*
|
|
123
|
+
* Split out as a pure function because it is the whole of the seam's logic and
|
|
124
|
+
* the only part of it worth being wrong about: an id that appears in the new
|
|
125
|
+
* editing set has merely started, an id in both is still going, and only one
|
|
126
|
+
* that has LEFT is a commit. Kept total over an empty selection, which is what
|
|
127
|
+
* arrives when the editor closes onto bare canvas.
|
|
128
|
+
*/
|
|
129
|
+
export function c4EditingTransition(previous, selections) {
|
|
130
|
+
const editing = new Set();
|
|
131
|
+
for (const selection of selections) {
|
|
132
|
+
if (!selection.editing)
|
|
133
|
+
continue;
|
|
134
|
+
for (const id of selection.elements)
|
|
135
|
+
editing.add(id);
|
|
136
|
+
}
|
|
137
|
+
const left = [];
|
|
138
|
+
for (const id of previous)
|
|
139
|
+
if (!editing.has(id))
|
|
140
|
+
left.push(id);
|
|
141
|
+
return { editing, left };
|
|
142
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type C4NodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
/**
|
|
3
|
+
* What a C4 shape IS, as props — the ONE description the creation site and the
|
|
4
|
+
* morph both read.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `actions.ts` when the morph landed, for the reason BPMN's
|
|
7
|
+
* `presets.ts` gives about `bpmnNodeProps`: a kind's appearance is written by
|
|
8
|
+
* the preset of the kind it was created as and nothing else ever rewrites it,
|
|
9
|
+
* so a morph that restated the table would agree with the palette the day it
|
|
10
|
+
* was written and drift on the first restyle. Derived, they cannot: a morphed
|
|
11
|
+
* component and one freshly drawn from the sub-menu are the same element.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The kinds whose GLYPH draws the body, so the native shape underneath paints
|
|
15
|
+
* nothing at all.
|
|
16
|
+
*
|
|
17
|
+
* Five of nine, and the renderer is the authority on which. A person is a head
|
|
18
|
+
* fused into a rounded body and a database is a cylinder, neither of which a
|
|
19
|
+
* native rect can be. `mobile` and `browser` joined them with the PO's recette
|
|
20
|
+
* of 27/08/2026: the reference stencil paints their OUTER rectangle in the
|
|
21
|
+
* node's darker colour — the bezel — and insets a lighter SCREEN in it, which is
|
|
22
|
+
* the reverse of a band painted over a body, so there is nothing left for a
|
|
23
|
+
* native rect to contribute. Same call BPMN makes for `dataObject` /
|
|
24
|
+
* `dataStore`.
|
|
25
|
+
*
|
|
26
|
+
* They stay hit-testable across their whole area all the same:
|
|
27
|
+
* `C4NodeElementModel.includesPoint` forces the interior test regardless of
|
|
28
|
+
* `filled` — which is the fix for the PO's second report, that these nodes could
|
|
29
|
+
* not be double-clicked into their text editor.
|
|
30
|
+
*
|
|
31
|
+
* This is also the one table the container FAMILY morphs across: a `container`
|
|
32
|
+
* is a plain filled rect and a `database`, a `mobile` and a `browser` are not,
|
|
33
|
+
* so `filled` and `strokeStyle` genuinely flip on the way between them — which
|
|
34
|
+
* is exactly why {@link c4MorphProps} is the whole preset and not `{kind, role}`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const GLYPH_BODY_KINDS: ReadonlySet<C4NodeKind>;
|
|
37
|
+
/**
|
|
38
|
+
* Every prop a C4 shape is created with, for one kind and one box.
|
|
39
|
+
*
|
|
40
|
+
* `index` is deliberately NOT here: it is a layering statement the creation
|
|
41
|
+
* site makes about the five elements it is about to write in painting order,
|
|
42
|
+
* and it means nothing to an element that already exists.
|
|
43
|
+
*
|
|
44
|
+
* No `text` either, and that is the arrangement rather than an omission: since
|
|
45
|
+
* the PO's recette of 28/08/2026 the name is a `c4:title` CHILD, and the shape
|
|
46
|
+
* is a body and nothing else. Its text COLOUR is still seeded — an element
|
|
47
|
+
* drawn before that change carries its name here, and the node view routes a
|
|
48
|
+
* double-click to that editor for exactly those, so the words have to stay
|
|
49
|
+
* legible.
|
|
50
|
+
*/
|
|
51
|
+
export declare function c4NodeProps(kind: C4NodeKind, box: {
|
|
52
|
+
xywh: string;
|
|
53
|
+
}): Record<string, unknown> & {
|
|
54
|
+
type: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* What a kind is worth to a shape that ALREADY EXISTS — {@link c4NodeProps}
|
|
58
|
+
* minus the three things a morph has no business touching.
|
|
59
|
+
*
|
|
60
|
+
* ## Why the whole preset, and not `{kind, role}`
|
|
61
|
+
*
|
|
62
|
+
* Because on this pack's own table it is visibly not enough. The container
|
|
63
|
+
* family — `container`, `database`, `mobile`, `browser` — is four kinds where
|
|
64
|
+
* one paints its body natively and three hand it to the renderer, so `filled`
|
|
65
|
+
* and `strokeStyle` flip between them, and two of them round their corners
|
|
66
|
+
* where the others do not ({@link NODE_RADIUS}). Morph a container to a
|
|
67
|
+
* database with two keys and the cylinder arrives with the rectangle still
|
|
68
|
+
* painted behind it. The `-ext` families are the same argument in colour: grey
|
|
69
|
+
* IS what "somebody else owns this" means, and it lives in `fillColor` /
|
|
70
|
+
* `strokeColor`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function c4MorphProps(kind: C4NodeKind): Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
75
|
+
* writes and this one does not.
|
|
76
|
+
*
|
|
77
|
+
* EMPTY for every kind today, because no C4 preset spreads anything
|
|
78
|
+
* conditionally: all nine write the same key set with different values. Kept
|
|
79
|
+
* anyway, and derived rather than hard-coded to `[]`, for the reason BPMN's
|
|
80
|
+
* equivalent exists at all — a patch cannot express absence, and the day one
|
|
81
|
+
* kind stops writing a key the previous kind's value would otherwise stay in
|
|
82
|
+
* the Y.Map, silently in force.
|
|
83
|
+
*/
|
|
84
|
+
export declare function c4MorphClears(kind: C4NodeKind): readonly string[];
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { FontFamily, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { NODE_PALETTE, NODE_RADIUS, NODE_STROKE_WIDTH, TITLE_FONT_SIZE, } from './consts.js';
|
|
3
|
+
import { C4_ROLE_OF_KIND } from './roles.js';
|
|
4
|
+
/**
|
|
5
|
+
* What a C4 shape IS, as props — the ONE description the creation site and the
|
|
6
|
+
* morph both read.
|
|
7
|
+
*
|
|
8
|
+
* Split out of `actions.ts` when the morph landed, for the reason BPMN's
|
|
9
|
+
* `presets.ts` gives about `bpmnNodeProps`: a kind's appearance is written by
|
|
10
|
+
* the preset of the kind it was created as and nothing else ever rewrites it,
|
|
11
|
+
* so a morph that restated the table would agree with the palette the day it
|
|
12
|
+
* was written and drift on the first restyle. Derived, they cannot: a morphed
|
|
13
|
+
* component and one freshly drawn from the sub-menu are the same element.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The kinds whose GLYPH draws the body, so the native shape underneath paints
|
|
17
|
+
* nothing at all.
|
|
18
|
+
*
|
|
19
|
+
* Five of nine, and the renderer is the authority on which. A person is a head
|
|
20
|
+
* fused into a rounded body and a database is a cylinder, neither of which a
|
|
21
|
+
* native rect can be. `mobile` and `browser` joined them with the PO's recette
|
|
22
|
+
* of 27/08/2026: the reference stencil paints their OUTER rectangle in the
|
|
23
|
+
* node's darker colour — the bezel — and insets a lighter SCREEN in it, which is
|
|
24
|
+
* the reverse of a band painted over a body, so there is nothing left for a
|
|
25
|
+
* native rect to contribute. Same call BPMN makes for `dataObject` /
|
|
26
|
+
* `dataStore`.
|
|
27
|
+
*
|
|
28
|
+
* They stay hit-testable across their whole area all the same:
|
|
29
|
+
* `C4NodeElementModel.includesPoint` forces the interior test regardless of
|
|
30
|
+
* `filled` — which is the fix for the PO's second report, that these nodes could
|
|
31
|
+
* not be double-clicked into their text editor.
|
|
32
|
+
*
|
|
33
|
+
* This is also the one table the container FAMILY morphs across: a `container`
|
|
34
|
+
* is a plain filled rect and a `database`, a `mobile` and a `browser` are not,
|
|
35
|
+
* so `filled` and `strokeStyle` genuinely flip on the way between them — which
|
|
36
|
+
* is exactly why {@link c4MorphProps} is the whole preset and not `{kind, role}`.
|
|
37
|
+
*/
|
|
38
|
+
export const GLYPH_BODY_KINDS = new Set([
|
|
39
|
+
'person',
|
|
40
|
+
'person-ext',
|
|
41
|
+
'database',
|
|
42
|
+
'mobile',
|
|
43
|
+
'browser',
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Every prop a C4 shape is created with, for one kind and one box.
|
|
47
|
+
*
|
|
48
|
+
* `index` is deliberately NOT here: it is a layering statement the creation
|
|
49
|
+
* site makes about the five elements it is about to write in painting order,
|
|
50
|
+
* and it means nothing to an element that already exists.
|
|
51
|
+
*
|
|
52
|
+
* No `text` either, and that is the arrangement rather than an omission: since
|
|
53
|
+
* the PO's recette of 28/08/2026 the name is a `c4:title` CHILD, and the shape
|
|
54
|
+
* is a body and nothing else. Its text COLOUR is still seeded — an element
|
|
55
|
+
* drawn before that change carries its name here, and the node view routes a
|
|
56
|
+
* double-click to that editor for exactly those, so the words have to stay
|
|
57
|
+
* legible.
|
|
58
|
+
*/
|
|
59
|
+
export function c4NodeProps(kind, box) {
|
|
60
|
+
const paint = NODE_PALETTE[kind];
|
|
61
|
+
const glyphBody = GLYPH_BODY_KINDS.has(kind);
|
|
62
|
+
return {
|
|
63
|
+
type: 'c4Node',
|
|
64
|
+
kind,
|
|
65
|
+
// Semantic identity, posted next to `kind` — which stays untouched and keeps
|
|
66
|
+
// driving the rendering. The role is the authority on what the box MEANS,
|
|
67
|
+
// and it is the only thing that can say so: three of the four levels are the
|
|
68
|
+
// same rounded rectangle (see `./roles.ts`).
|
|
69
|
+
role: C4_ROLE_OF_KIND[kind],
|
|
70
|
+
shapeType: 'rect',
|
|
71
|
+
// A glyph-bodied kind paints nothing natively: the head, the block and the
|
|
72
|
+
// cylinder are drawn by the renderer, which reads `fillColor` /
|
|
73
|
+
// `strokeColor` off this same model — so both stay editable from the shape
|
|
74
|
+
// toolbar exactly like every other node's.
|
|
75
|
+
filled: !glyphBody,
|
|
76
|
+
fillColor: paint.fill,
|
|
77
|
+
strokeColor: paint.border,
|
|
78
|
+
strokeWidth: NODE_STROKE_WIDTH,
|
|
79
|
+
strokeStyle: glyphBody ? StrokeStyle.None : StrokeStyle.Solid,
|
|
80
|
+
shapeStyle: ShapeStyle.General,
|
|
81
|
+
roughness: 0,
|
|
82
|
+
// Per kind, and mostly ZERO: the stencil draws the boxed levels as plain
|
|
83
|
+
// square-cornered rectangles, and rounds only the two devices. Harmless on a
|
|
84
|
+
// glyph-bodied kind, whose native shape is invisible.
|
|
85
|
+
radius: NODE_RADIUS[kind],
|
|
86
|
+
color: paint.text,
|
|
87
|
+
fontFamily: FontFamily.Inter,
|
|
88
|
+
fontSize: TITLE_FONT_SIZE,
|
|
89
|
+
textAlign: TextAlign.Center,
|
|
90
|
+
xywh: box.xywh,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The box {@link c4MorphProps} hands {@link c4NodeProps} and then throws away.
|
|
95
|
+
* Never written to a document: a morph keeps the geometry the element already
|
|
96
|
+
* has, and this exists only because the one builder takes a box.
|
|
97
|
+
*/
|
|
98
|
+
const DISCARDED_BOX = '[0,0,0,0]';
|
|
99
|
+
/** What a morph must never rewrite: identity, geometry, and the user's words. */
|
|
100
|
+
const NOT_A_MORPH = ['type', 'xywh', 'text'];
|
|
101
|
+
/**
|
|
102
|
+
* What a kind is worth to a shape that ALREADY EXISTS — {@link c4NodeProps}
|
|
103
|
+
* minus the three things a morph has no business touching.
|
|
104
|
+
*
|
|
105
|
+
* ## Why the whole preset, and not `{kind, role}`
|
|
106
|
+
*
|
|
107
|
+
* Because on this pack's own table it is visibly not enough. The container
|
|
108
|
+
* family — `container`, `database`, `mobile`, `browser` — is four kinds where
|
|
109
|
+
* one paints its body natively and three hand it to the renderer, so `filled`
|
|
110
|
+
* and `strokeStyle` flip between them, and two of them round their corners
|
|
111
|
+
* where the others do not ({@link NODE_RADIUS}). Morph a container to a
|
|
112
|
+
* database with two keys and the cylinder arrives with the rectangle still
|
|
113
|
+
* painted behind it. The `-ext` families are the same argument in colour: grey
|
|
114
|
+
* IS what "somebody else owns this" means, and it lives in `fillColor` /
|
|
115
|
+
* `strokeColor`.
|
|
116
|
+
*/
|
|
117
|
+
export function c4MorphProps(kind) {
|
|
118
|
+
// Widened to the plain record on the way in: `type` is required on what the
|
|
119
|
+
// creation builder returns, and `delete` may only take an optional key.
|
|
120
|
+
const props = {
|
|
121
|
+
...c4NodeProps(kind, { xywh: DISCARDED_BOX }),
|
|
122
|
+
};
|
|
123
|
+
for (const key of NOT_A_MORPH)
|
|
124
|
+
delete props[key];
|
|
125
|
+
return props;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Every key ANY kind's props may carry — the union over the whole pack.
|
|
129
|
+
*
|
|
130
|
+
* Computed rather than listed, so a preset that starts spreading a key
|
|
131
|
+
* conditionally is covered on the day it is added rather than on the day
|
|
132
|
+
* somebody notices.
|
|
133
|
+
*/
|
|
134
|
+
const EVERY_MORPH_KEY = new Set(Object.keys(NODE_PALETTE).flatMap(kind => Object.keys(c4MorphProps(kind))));
|
|
135
|
+
/**
|
|
136
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
137
|
+
* writes and this one does not.
|
|
138
|
+
*
|
|
139
|
+
* EMPTY for every kind today, because no C4 preset spreads anything
|
|
140
|
+
* conditionally: all nine write the same key set with different values. Kept
|
|
141
|
+
* anyway, and derived rather than hard-coded to `[]`, for the reason BPMN's
|
|
142
|
+
* equivalent exists at all — a patch cannot express absence, and the day one
|
|
143
|
+
* kind stops writing a key the previous kind's value would otherwise stay in
|
|
144
|
+
* the Y.Map, silently in force.
|
|
145
|
+
*/
|
|
146
|
+
export function c4MorphClears(kind) {
|
|
147
|
+
const present = new Set(Object.keys(c4MorphProps(kind)));
|
|
148
|
+
return [...EVERY_MORPH_KEY].filter(key => !present.has(key));
|
|
149
|
+
}
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4 validation profiles.
|
|
3
|
+
*
|
|
4
|
+
* DATA owned by the framework, like its rules and its roles. A profile is
|
|
5
|
+
* chosen per FRAME — the board, and the boundary, which are the two elements
|
|
6
|
+
* this framework's rules measure against — and the choice rides on the element
|
|
7
|
+
* itself, so two diagrams at two levels of requirement coexist on one canvas:
|
|
8
|
+
* the board somebody is still sketching stays a sketch while the one that is
|
|
9
|
+
* being handed over is held to the checklist.
|
|
10
|
+
*
|
|
11
|
+
* Registered from the flag-gated `C4ViewExtension`, beside the rules:
|
|
12
|
+
* switching the `c4` flag off takes the choice away with the rest of the
|
|
13
|
+
* tooling, and a board already set to `strict` simply stops being checked until
|
|
14
|
+
* it comes back — the id stays written, untouched.
|
|
15
|
+
*
|
|
16
|
+
* ## Both tables spell out all SIXTEEN ids
|
|
17
|
+
*
|
|
18
|
+
* Every severity a user can get is either the one its rule declares or one of
|
|
19
|
+
* these lines — nothing is raised implicitly (PF9.4). Spelling them all out is
|
|
20
|
+
* what makes the level READABLE: a reviewer asking what `c4.strict` actually
|
|
21
|
+
* requires reads sixteen lines here instead of one file per rule, and a rule
|
|
22
|
+
* shipped later cannot join a level in silence.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Sketch: every rule at `audit`. Findings still reach `violations$` — a host
|
|
26
|
+
* panel, a check-up and a conformance report see them — and the canvas says
|
|
27
|
+
* nothing.
|
|
28
|
+
*
|
|
29
|
+
* The DEFAULT, and deliberately so (PRD principle 3), and more deliberately
|
|
30
|
+
* here than anywhere else in the library: a C4 diagram is drawn from the
|
|
31
|
+
* outside in. The boxes go down, then the arrows, then the words, and for the
|
|
32
|
+
* whole of that every element is isolated, every arrow is unlabelled, every box
|
|
33
|
+
* is unnamed and half the links are still plain connectors somebody
|
|
34
|
+
* quick-dragged. A tool arguing with that hand is a tool switched off within
|
|
35
|
+
* the hour, and it would be arguing about a diagram the author already knows is
|
|
36
|
+
* unfinished.
|
|
37
|
+
*
|
|
38
|
+
* The sketch PRIMES: the findings are computed, collected and available the
|
|
39
|
+
* moment the author asks — through the panel, through a check-up, through the
|
|
40
|
+
* profile switch — so nothing has to be re-derived when they decide the drawing
|
|
41
|
+
* is a deliverable. Being the default also means it WRITES NOTHING: a board on
|
|
42
|
+
* `sketch` carries no profile key, so every C4 diagram ever drawn is on it,
|
|
43
|
+
* with no migration and no backfill.
|
|
44
|
+
*/
|
|
45
|
+
const sketch = {
|
|
46
|
+
id: 'c4.sketch',
|
|
47
|
+
framework: 'c4',
|
|
48
|
+
labelKey: 'com.labre.c4.profile.sketch',
|
|
49
|
+
fallback: 'Sketch',
|
|
50
|
+
isDefault: true,
|
|
51
|
+
rules: {
|
|
52
|
+
'c4.unlabeled-relationship': 'audit',
|
|
53
|
+
'c4.unnamed-element': 'audit',
|
|
54
|
+
'c4.untyped-link': 'audit',
|
|
55
|
+
'c4.relationship-endpoints': 'audit',
|
|
56
|
+
'c4.relationship-self-loop': 'audit',
|
|
57
|
+
'c4.isolated-system': 'audit',
|
|
58
|
+
'c4.isolated-container': 'audit',
|
|
59
|
+
'c4.isolated-component': 'audit',
|
|
60
|
+
'c4.database-initiates': 'audit',
|
|
61
|
+
'c4.homeless-component': 'audit',
|
|
62
|
+
'c4.person-in-boundary': 'audit',
|
|
63
|
+
'c4.system-in-boundary': 'audit',
|
|
64
|
+
'c4.container-in-container-boundary': 'audit',
|
|
65
|
+
'c4.component-level-skip': 'audit',
|
|
66
|
+
'c4.context-diagram-level': 'audit',
|
|
67
|
+
'c4.container-diagram-level': 'audit',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Strict: the diagram is a DELIVERABLE, and it is held to the review checklist.
|
|
72
|
+
*
|
|
73
|
+
* The level somebody chooses when a diagram stops being a thinking aid and
|
|
74
|
+
* becomes something another team will be handed. ELEVEN rules move to
|
|
75
|
+
* `warning`.
|
|
76
|
+
*
|
|
77
|
+
* Three of them read a question from the checklist itself (c4model.com): is
|
|
78
|
+
* every element named, is every relationship labelled, is every arrow one the
|
|
79
|
+
* model can state.
|
|
80
|
+
*
|
|
81
|
+
* Two are the LEVEL rules — `c4.context-diagram-level` and
|
|
82
|
+
* `c4.container-diagram-level` — and they are the easiest promotion in the
|
|
83
|
+
* table, because the user has already spoken: a board declaring a level is an
|
|
84
|
+
* author saying which sheet this is, and a container drawn on a sheet its own
|
|
85
|
+
* author called a context diagram is a contradiction inside one document. There
|
|
86
|
+
* is no reading under which the drawing meant it — and the one reading under
|
|
87
|
+
* which nothing should be said, a sheet whose level was never stated, is silence
|
|
88
|
+
* from the rules themselves rather than from this table.
|
|
89
|
+
*
|
|
90
|
+
* Three more are the ZOOM rules — `c4.system-in-boundary`,
|
|
91
|
+
* `c4.container-in-container-boundary` and `c4.component-level-skip`. They are
|
|
92
|
+
* promoted on the same test as everything else here: whether the diagram might
|
|
93
|
+
* honestly have meant it. None of the three can be meant. A boundary is a system
|
|
94
|
+
* or a container, so nothing inside one is a system; a container boundary IS its
|
|
95
|
+
* container, so no container goes in it; and a component framed by no container
|
|
96
|
+
* is a sheet with a level missing. Each is a drawing whose author will change it
|
|
97
|
+
* once they see it, which is the definition of a `warning` at the level where
|
|
98
|
+
* somebody has said the diagram is a deliverable.
|
|
99
|
+
*
|
|
100
|
+
* The other three — `c4.untyped-link`, `c4.relationship-self-loop` and
|
|
101
|
+
* `c4.homeless-component` — are OURS, and they are promoted anyway. Provenance
|
|
102
|
+
* and severity are ORTHOGONAL, and this table is where that shows: what decides
|
|
103
|
+
* whether a house reading bites is not where it came from but whether the
|
|
104
|
+
* diagram might honestly have meant it. A connector the model never recorded, a
|
|
105
|
+
* box drawn as using itself, a component belonging to no container — none of
|
|
106
|
+
* the three has a second reading, so at the level where somebody has said the
|
|
107
|
+
* drawing is a deliverable, saying so is the point. Each message names the rule
|
|
108
|
+
* as our convention rather than implying C4 forbids it, which is what keeps the
|
|
109
|
+
* promotion honest.
|
|
110
|
+
*
|
|
111
|
+
* Note the partition here is NOT the provenance one — eleven promoted against
|
|
112
|
+
* eleven `recommendation` rules is a coincidence of arithmetic and nothing else,
|
|
113
|
+
* since the two elevens do not hold the same rules: the three `c4.isolated-*`
|
|
114
|
+
* come from the checklist and stay remarks, while three of ours are promoted.
|
|
115
|
+
* The spec constant is called `PROMOTED` for exactly that reason.
|
|
116
|
+
*
|
|
117
|
+
* ## The five that do NOT move, and why the table spells them out
|
|
118
|
+
*
|
|
119
|
+
* Five out of sixteen, still — the two rules this slice added both moved.
|
|
120
|
+
*
|
|
121
|
+
* `c4.isolated-system`, `c4.isolated-container`, `c4.isolated-component`,
|
|
122
|
+
* `c4.database-initiates` and `c4.person-in-boundary` stay `audit` here too.
|
|
123
|
+
*
|
|
124
|
+
* The three isolation rules report a diagram that is UNFINISHED rather than
|
|
125
|
+
* wrong: an element with no arrow on it is a box whose author has not got to
|
|
126
|
+
* the arrows yet, and "not done" is what a panel is for. Promoting them would
|
|
127
|
+
* put a bracket on every box for the first minutes of every drawing, at the one
|
|
128
|
+
* level where the user has explicitly said the drawing matters.
|
|
129
|
+
*
|
|
130
|
+
* The other two are OURS rather than the checklist's — idioms of the notation,
|
|
131
|
+
* not requirements of it — and, unlike the self-loop promoted above, each has a
|
|
132
|
+
* reading under which the author is right. A store with a change feed really
|
|
133
|
+
* does push, and a person really can be drawn inside a boundary by somebody
|
|
134
|
+
* making a point about an operator embedded in a process. Both are worth
|
|
135
|
+
* remarking on and neither is something the tool should claim is a mistake, at
|
|
136
|
+
* any level. That is the line between a convention that bites and one that does
|
|
137
|
+
* not: whether the diagram might have meant it.
|
|
138
|
+
*
|
|
139
|
+
* ## Nothing is `blocking-overridable`, and nothing ever will be here
|
|
140
|
+
*
|
|
141
|
+
* Nothing in this library implements refusal — no gesture is declined anywhere
|
|
142
|
+
* — so declaring the level would be data claiming an effect that does not exist
|
|
143
|
+
* (the `wardley/rules.ts:30` promise, kept). Unlike BPMN, C4 has no rule that
|
|
144
|
+
* would move there the day refusal lands: a review checklist is a set of
|
|
145
|
+
* questions asked of a finished drawing, not a grammar that can refuse a
|
|
146
|
+
* gesture while it is being made.
|
|
147
|
+
*/
|
|
148
|
+
const strict = {
|
|
149
|
+
id: 'c4.strict',
|
|
150
|
+
framework: 'c4',
|
|
151
|
+
labelKey: 'com.labre.c4.profile.strict',
|
|
152
|
+
fallback: 'Review checklist',
|
|
153
|
+
rules: {
|
|
154
|
+
'c4.unlabeled-relationship': 'warning',
|
|
155
|
+
'c4.unnamed-element': 'warning',
|
|
156
|
+
'c4.untyped-link': 'warning',
|
|
157
|
+
'c4.relationship-endpoints': 'warning',
|
|
158
|
+
'c4.relationship-self-loop': 'warning',
|
|
159
|
+
'c4.homeless-component': 'warning',
|
|
160
|
+
// The three zoom rules: a drawing that contradicts the model's own levels
|
|
161
|
+
// has no second reading — see the header.
|
|
162
|
+
'c4.system-in-boundary': 'warning',
|
|
163
|
+
'c4.container-in-container-boundary': 'warning',
|
|
164
|
+
'c4.component-level-skip': 'warning',
|
|
165
|
+
// The two LEVEL rules: the author has SAID which sheet this is, and the
|
|
166
|
+
// sheet shows something that sheet does not show — see the header.
|
|
167
|
+
'c4.context-diagram-level': 'warning',
|
|
168
|
+
'c4.container-diagram-level': 'warning',
|
|
169
|
+
// The five that do not move — see the header.
|
|
170
|
+
'c4.isolated-system': 'audit',
|
|
171
|
+
'c4.isolated-container': 'audit',
|
|
172
|
+
'c4.isolated-component': 'audit',
|
|
173
|
+
'c4.database-initiates': 'audit',
|
|
174
|
+
'c4.person-in-boundary': 'audit',
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
export const C4_PROFILES = [sketch, strict];
|