@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
package/dist/consts.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual constants for the C4 pack.
|
|
3
|
+
*
|
|
4
|
+
* Unlike BPMN — a black-and-white notation where the SHAPE carries the meaning
|
|
5
|
+
* and colour is decoration — C4's official stencil is a colour code: the four
|
|
6
|
+
* levels are four blues, from the near-navy of a person down to the pale wash of
|
|
7
|
+
* a component, and anything outside the scope of the diagram is grey. That is
|
|
8
|
+
* the one thing a reader uses to tell a container from a component when both are
|
|
9
|
+
* rounded rectangles with words in them, so the palette below IS the notation
|
|
10
|
+
* and is written down as data rather than left to whoever draws next.
|
|
11
|
+
*
|
|
12
|
+
* Every hex, every size and every radius below is read off the PO's own
|
|
13
|
+
* reference model — the Visio-exported `C4Model_default.svg` stencil — rather
|
|
14
|
+
* than approximated, and the file's units are carried through at ×2 so that a
|
|
15
|
+
* default node is a comfortable size on a canvas. Where a number is a fraction
|
|
16
|
+
* of the node box rather than an absolute, it lives in the renderer beside the
|
|
17
|
+
* path it shapes. Every value here is a creation-time DEFAULT; each one is an
|
|
18
|
+
* editable shape property afterwards, exactly as in BPMN.
|
|
19
|
+
*/
|
|
20
|
+
/** The stencil's own unit → model unit factor. Every absolute below is ×2. */
|
|
21
|
+
export const STENCIL_SCALE = 2;
|
|
22
|
+
const WHITE = '#ffffff';
|
|
23
|
+
const BLACK = '#000000';
|
|
24
|
+
/**
|
|
25
|
+
* The grey every EXTERNAL element is drawn in — a person or a system somebody
|
|
26
|
+
* else owns. One grey for both, because "outside the scope of this diagram" is
|
|
27
|
+
* one statement and the level of the thing outside it is not the point.
|
|
28
|
+
*/
|
|
29
|
+
const EXTERNAL = {
|
|
30
|
+
fill: '#999999',
|
|
31
|
+
border: '#8b8b8b',
|
|
32
|
+
text: WHITE,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The palette, per kind. TOTAL over {@link C4NodeKind} by its type, so a kind
|
|
36
|
+
* added to the model cannot land without being given a colour.
|
|
37
|
+
*
|
|
38
|
+
* The four levels run light as they go IN: a person is the darkest thing on the
|
|
39
|
+
* page (#08427b), a software system one step lighter (#1168bd), every container
|
|
40
|
+
* lighter again (#438dd5) and a component palest of all (#85bbf0) — which is
|
|
41
|
+
* also the one that takes black text, because white on that wash is unreadable.
|
|
42
|
+
* `database`, `mobile` and `browser` are CONTAINERS and take the container's
|
|
43
|
+
* colour exactly: what makes them different is the silhouette the renderer
|
|
44
|
+
* draws, never the level, and a fourth blue would say otherwise.
|
|
45
|
+
*
|
|
46
|
+
* Every border is the stencil's own darker shade of its fill, lifted verbatim
|
|
47
|
+
* from the reference model's stylesheet (`.st1`, `.st5`, `.st6`, `.st7`,
|
|
48
|
+
* `.st22`) rather than darkened by eye — which is what makes the two decorated
|
|
49
|
+
* containers work at all: `mobile` and `browser` paint their BEZEL in the border
|
|
50
|
+
* colour and their SCREEN in the fill, so the pair has to be the stencil's pair.
|
|
51
|
+
*/
|
|
52
|
+
export const NODE_PALETTE = {
|
|
53
|
+
person: { fill: '#08427b', border: '#073b6f', text: WHITE },
|
|
54
|
+
'person-ext': EXTERNAL,
|
|
55
|
+
system: { fill: '#1168bd', border: '#1864ad', text: WHITE },
|
|
56
|
+
'system-ext': EXTERNAL,
|
|
57
|
+
container: { fill: '#438dd5', border: '#3d81c3', text: WHITE },
|
|
58
|
+
database: { fill: '#438dd5', border: '#3d81c3', text: WHITE },
|
|
59
|
+
mobile: { fill: '#438dd5', border: '#3d81c3', text: WHITE },
|
|
60
|
+
browser: { fill: '#438dd5', border: '#3d81c3', text: WHITE },
|
|
61
|
+
component: { fill: '#85bbf0', border: '#78a8d8', text: BLACK },
|
|
62
|
+
};
|
|
63
|
+
/** Border weight of every node — one line (stencil `stroke-width:1`), ×2. */
|
|
64
|
+
export const NODE_STROKE_WIDTH = 2;
|
|
65
|
+
/**
|
|
66
|
+
* Corner radius of each kind's OUTER body, in model units.
|
|
67
|
+
*
|
|
68
|
+
* A table rather than one number, because the stencil is not uniform and the
|
|
69
|
+
* difference is legible: `system`, `system-ext`, `container` and `component` are
|
|
70
|
+
* plain `<rect>`s with **no `rx` at all** — square corners — while the two
|
|
71
|
+
* decorated containers are rounded, the phone noticeably (`rx="4.252"`) and the
|
|
72
|
+
* browser window barely (`rx="1.4173"`). The three glyph-bodied kinds carry `0`
|
|
73
|
+
* because their native rect paints nothing: a person's shoulders and a
|
|
74
|
+
* cylinder's lid are curves the glyph draws itself, in proportion to the box.
|
|
75
|
+
*
|
|
76
|
+
* This corrects the pack's first pass, which rounded every kind at 10 — a
|
|
77
|
+
* plausible house style, and one the reference model does not draw.
|
|
78
|
+
*/
|
|
79
|
+
export const NODE_RADIUS = {
|
|
80
|
+
person: 0,
|
|
81
|
+
'person-ext': 0,
|
|
82
|
+
system: 0,
|
|
83
|
+
'system-ext': 0,
|
|
84
|
+
container: 0,
|
|
85
|
+
database: 0,
|
|
86
|
+
mobile: 4.252 * STENCIL_SCALE,
|
|
87
|
+
browser: 1.4173 * STENCIL_SCALE,
|
|
88
|
+
component: 0,
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The three text tiers, at the stencil's own sizes (10 / 6 / 8) ×2.
|
|
92
|
+
*
|
|
93
|
+
* All three are creation-time DEFAULTS the author can change afterwards, and all
|
|
94
|
+
* three are now CANVAS TEXT ELEMENTS: since the PO's recette of 28/08/2026 a C4
|
|
95
|
+
* component is a group holding the shape and its three lines of words — the
|
|
96
|
+
* name included — so every tier is ordinary text with its own toolbar rather
|
|
97
|
+
* than something a renderer painted and nobody could type on.
|
|
98
|
+
*
|
|
99
|
+
* The name was the shape's native inner text for one iteration and is not any
|
|
100
|
+
* more, which is the second half of that recette: two kinds of text in one
|
|
101
|
+
* component meant two editors, two toolbars and two sets of rules for the same
|
|
102
|
+
* three lines. Now there is one of each.
|
|
103
|
+
*
|
|
104
|
+
* The ladder 20 / 16 / 12 is the notation and not typography — the type line is
|
|
105
|
+
* smaller than the name on every C4 diagram ever drawn — but it is a ladder an
|
|
106
|
+
* author can climb off, which is the price of letting them write on the picture.
|
|
107
|
+
*/
|
|
108
|
+
export const TITLE_FONT_SIZE = 10 * STENCIL_SCALE;
|
|
109
|
+
export const TYPE_FONT_SIZE = 6 * STENCIL_SCALE;
|
|
110
|
+
export const DESCRIPTION_FONT_SIZE = 8 * STENCIL_SCALE;
|
|
111
|
+
/**
|
|
112
|
+
* One line box, as a multiple of its own font size — what the creation site
|
|
113
|
+
* measures a tier's HEIGHT in.
|
|
114
|
+
*
|
|
115
|
+
* The stencil states its tiers as baselines, which is the right unit for a
|
|
116
|
+
* renderer painting into a box and the wrong one for a creation site placing an
|
|
117
|
+
* element: a text element is a rectangle, and where its first baseline lands
|
|
118
|
+
* inside that rectangle is the text renderer's business, not this file's. So the
|
|
119
|
+
* stencil's baseline steps are re-read here as line boxes, which is the same
|
|
120
|
+
* geometry counted from the other end.
|
|
121
|
+
*/
|
|
122
|
+
export const TIER_LINE_HEIGHT = 1.2;
|
|
123
|
+
/**
|
|
124
|
+
* How many lines each of the two wrapping tiers opens with.
|
|
125
|
+
*
|
|
126
|
+
* The title gets TWO, and that is what grew the element (see {@link NODE_BOX}):
|
|
127
|
+
* "Internet Banking System" is 23 characters, and 23 characters at 20px do not
|
|
128
|
+
* fit across 187 units of usable width. A one-line title box would have meant
|
|
129
|
+
* every real system name spilling out of its own tier on the day it was typed —
|
|
130
|
+
* which is precisely the cramped stack the PO's recette was about.
|
|
131
|
+
*
|
|
132
|
+
* The description gets two as well, which is what the stencil's own sentences
|
|
133
|
+
* run to. Neither is a limit: a longer text wraps inside the tier's width and
|
|
134
|
+
* grows the box downward, and the group grows with it, so a component keeps
|
|
135
|
+
* containing its own words.
|
|
136
|
+
*/
|
|
137
|
+
export const TITLE_LINES = 2;
|
|
138
|
+
export const DESCRIPTION_LINES = 2;
|
|
139
|
+
/**
|
|
140
|
+
* The vertical rhythm of the stack, in model units.
|
|
141
|
+
*
|
|
142
|
+
* Absolutes rather than multiples of a font size, and deliberately so: what the
|
|
143
|
+
* eye reads here is the SPACE between three blocks of different sizes, and a gap
|
|
144
|
+
* expressed as 0.7em of whichever tier happens to be below it changes meaning
|
|
145
|
+
* every time somebody resizes one of them. These three numbers are the layout.
|
|
146
|
+
*
|
|
147
|
+
* The two gaps are different on purpose. The name and its type line are ONE
|
|
148
|
+
* heading — `Web Application` / `[Container: Java]` is a single statement over
|
|
149
|
+
* two lines — so they sit close. The description is a different statement, and
|
|
150
|
+
* the wider gap under the type line is the stencil's own blank line: it is what
|
|
151
|
+
* keeps a sentence from reading as a fourth tier of the heading.
|
|
152
|
+
*
|
|
153
|
+
* {@link TIER_MARGIN} is equal top and bottom, which is what makes the stack sit
|
|
154
|
+
* in its box rather than in the top of it.
|
|
155
|
+
*/
|
|
156
|
+
export const TIER_MARGIN = 24;
|
|
157
|
+
export const TITLE_TYPE_GAP = 8;
|
|
158
|
+
export const TYPE_DESCRIPTION_GAP = 16;
|
|
159
|
+
/** Side inset the three text tiers sit within, as a fraction of the node width. */
|
|
160
|
+
export const TIER_SIDE_INSET = 0.06;
|
|
161
|
+
/**
|
|
162
|
+
* The height of the three tiers and the two gaps between them — the number the
|
|
163
|
+
* default element size is DERIVED from rather than fitted to.
|
|
164
|
+
*
|
|
165
|
+
* 48 + 8 + 14.4 + 16 + 38.4 = 124.8. Written as the sum it is so that changing
|
|
166
|
+
* a font size or a gap moves the box with it: a rhythm and a footprint that can
|
|
167
|
+
* disagree is a rhythm that will.
|
|
168
|
+
*/
|
|
169
|
+
export const TIER_STACK_HEIGHT = TITLE_FONT_SIZE * TIER_LINE_HEIGHT * TITLE_LINES +
|
|
170
|
+
TITLE_TYPE_GAP +
|
|
171
|
+
TYPE_FONT_SIZE * TIER_LINE_HEIGHT +
|
|
172
|
+
TYPE_DESCRIPTION_GAP +
|
|
173
|
+
DESCRIPTION_FONT_SIZE * TIER_LINE_HEIGHT * DESCRIPTION_LINES;
|
|
174
|
+
/**
|
|
175
|
+
* The sentence a fresh description prompts the author with.
|
|
176
|
+
*
|
|
177
|
+
* The stencil's own placeholder, and a PROMPT rather than a value: every tier of
|
|
178
|
+
* a C4 component exists from the moment it is drawn (PO arbitration,
|
|
179
|
+
* 28/08/2026), so the author meets three lines of stencil rather than a box and
|
|
180
|
+
* two invisible slots somebody has to tell them about. The exporter compares
|
|
181
|
+
* against it to decide that nothing has been stated yet — see
|
|
182
|
+
* `C4_TYPE_PLACEHOLDER` in `type-line.ts` for the same call on the other tier.
|
|
183
|
+
*/
|
|
184
|
+
export const DESCRIPTION_PLACEHOLDER = 'description';
|
|
185
|
+
/**
|
|
186
|
+
* How far the person's head stands clear ABOVE its body, in model units.
|
|
187
|
+
*
|
|
188
|
+
* The stencil's own `47.767` at ×2, solved off the silhouette path (`mID 1`):
|
|
189
|
+
* its head arc is drawn with `large-arc-flag=1` about a centre 21.26 units above
|
|
190
|
+
* the body's top edge, with `ry=26.504`, so the head clears the body by the sum
|
|
191
|
+
* of the two. Independent of how tall the BODY is, which is what lets the body
|
|
192
|
+
* grow with the text rhythm below without moving the head.
|
|
193
|
+
*/
|
|
194
|
+
export const PERSON_HEAD_CLEARANCE = 47.767 * STENCIL_SCALE;
|
|
195
|
+
/**
|
|
196
|
+
* Default node sizes (model units) per kind.
|
|
197
|
+
*
|
|
198
|
+
* ## One footprint, and one exception the stencil itself draws
|
|
199
|
+
*
|
|
200
|
+
* The reference model gives every element the SAME box — `106.3 × 74.409`, a
|
|
201
|
+
* `v:textRect` repeated verbatim on the system, the container, the component,
|
|
202
|
+
* the database, the phone and the browser window. Seven of the nine kinds take
|
|
203
|
+
* one footprint, and a row of C4 elements lining up without anybody arranging
|
|
204
|
+
* them is not a convenience, it is what makes a level readable.
|
|
205
|
+
*
|
|
206
|
+
* ## Why the box is taller than the stencil's, and by exactly how much
|
|
207
|
+
*
|
|
208
|
+
* The WIDTH is the stencil's, untouched: `106.3 × 2 = 212.6`. Widening it would
|
|
209
|
+
* change every glyph with it — a person's head radius is derived from the width
|
|
210
|
+
* — and the reference proportions are the one thing the recette of 27/08 was
|
|
211
|
+
* about.
|
|
212
|
+
*
|
|
213
|
+
* The HEIGHT is derived from the words instead, which is the PO's call of
|
|
214
|
+
* 28/08/2026: grow the shapes if that is what it takes to have room to write.
|
|
215
|
+
* `74.409 × 2 = 148.8` was the stencil's textRect for a box holding a name it
|
|
216
|
+
* could paint in a single line at whatever size it liked. This one holds three
|
|
217
|
+
* REAL text elements, at fixed sizes, with margins and gaps a reader can see —
|
|
218
|
+
* and the title alone needs two lines, because a system name is routinely longer
|
|
219
|
+
* than 187 units of usable width at 20px. So the height is
|
|
220
|
+
* {@link TIER_STACK_HEIGHT} plus a margin at each end: **212.6 × 172.8**, up
|
|
221
|
+
* from 212.6 × 148.8.
|
|
222
|
+
*
|
|
223
|
+
* Derived rather than chosen, so the box can never disagree with what it holds:
|
|
224
|
+
* change a tier's size or a gap and the footprint follows.
|
|
225
|
+
*
|
|
226
|
+
* ## The person
|
|
227
|
+
*
|
|
228
|
+
* `person` and `person-ext` are the exception, and it is the FILE's exception,
|
|
229
|
+
* not a preference. Their silhouette is one path (`mID 1`) whose head arc is
|
|
230
|
+
* drawn about a centre above the body's top edge, so the head stands
|
|
231
|
+
* {@link PERSON_HEAD_CLEARANCE} clear of a body that is itself the standard box.
|
|
232
|
+
* The stencil's own sheet shows it: the person's group is translated further
|
|
233
|
+
* down the page than the system beside it, precisely to make room. The body
|
|
234
|
+
* grows with everything else, so the person is now **212.6 × 268.3**.
|
|
235
|
+
*
|
|
236
|
+
* Forcing a person into the boxed footprint was considered and rejected: the
|
|
237
|
+
* head is a CIRCLE (`rx 26.362`, `ry 26.504`) and stays one only at the
|
|
238
|
+
* silhouette's own aspect ratio — squeezed into a box far wider than it is tall
|
|
239
|
+
* it becomes a flat ellipse, which is the one thing about a C4 person everybody
|
|
240
|
+
* recognises and the one thing that would then be wrong.
|
|
241
|
+
*/
|
|
242
|
+
export const NODE_BOX = {
|
|
243
|
+
w: 106.3 * STENCIL_SCALE,
|
|
244
|
+
h: TIER_MARGIN * 2 + TIER_STACK_HEIGHT,
|
|
245
|
+
};
|
|
246
|
+
/** The person's full silhouette — the standard body, plus the head above it. */
|
|
247
|
+
export const PERSON_BOX = {
|
|
248
|
+
w: NODE_BOX.w,
|
|
249
|
+
h: PERSON_HEAD_CLEARANCE + NODE_BOX.h,
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Where the person's body top edge sits, as a fraction of the whole silhouette.
|
|
253
|
+
*
|
|
254
|
+
* Derived from the two boxes rather than restated as the stencil's own ratio, so
|
|
255
|
+
* that growing the body cannot leave this pointing at the middle of the head.
|
|
256
|
+
* The renderer derives the same edge from the head RADIUS instead
|
|
257
|
+
* (`PERSON.bodyTopPerHead`), which is the form that survives an element dragged
|
|
258
|
+
* to an aspect ratio the head has to be clamped at; the two agree at the default
|
|
259
|
+
* size. This one is what the creation site needs: it has a box and no glyph, and
|
|
260
|
+
* it has to know where the words can go.
|
|
261
|
+
*/
|
|
262
|
+
export const PERSON_BODY_TOP = PERSON_HEAD_CLEARANCE / PERSON_BOX.h;
|
|
263
|
+
export const NODE_SIZE = {
|
|
264
|
+
person: PERSON_BOX,
|
|
265
|
+
'person-ext': PERSON_BOX,
|
|
266
|
+
system: NODE_BOX,
|
|
267
|
+
'system-ext': NODE_BOX,
|
|
268
|
+
container: NODE_BOX,
|
|
269
|
+
database: NODE_BOX,
|
|
270
|
+
mobile: NODE_BOX,
|
|
271
|
+
browser: NODE_BOX,
|
|
272
|
+
component: NODE_BOX,
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Default inner text per kind.
|
|
276
|
+
*
|
|
277
|
+
* Every kind carries one, unlike BPMN — where an event's meaning IS its glyph
|
|
278
|
+
* and the spec puts its name outside the shape. Here the box is the same box at
|
|
279
|
+
* three of the four levels, so a C4 element with nothing written in it says
|
|
280
|
+
* nothing at all: the words are the artefact.
|
|
281
|
+
*/
|
|
282
|
+
export const NODE_LABEL = {
|
|
283
|
+
person: 'Person',
|
|
284
|
+
'person-ext': 'External person',
|
|
285
|
+
system: 'Software system',
|
|
286
|
+
'system-ext': 'External system',
|
|
287
|
+
container: 'Container',
|
|
288
|
+
database: 'Database',
|
|
289
|
+
mobile: 'Mobile app',
|
|
290
|
+
browser: 'Web app',
|
|
291
|
+
component: 'Component',
|
|
292
|
+
};
|
|
293
|
+
/* ── The board ─────────────────────────────────────────────────────────── */
|
|
294
|
+
export const FONT_FAMILY = 'Inter, sans-serif';
|
|
295
|
+
/** The card — the same white every framework background paints. */
|
|
296
|
+
export const BOARD_CARD_FILL = '#ffffff';
|
|
297
|
+
export const BOARD_CARD_BORDER = '#d5d9e0';
|
|
298
|
+
export const BOARD_BORDER_WIDTH = 1.5;
|
|
299
|
+
export const BOARD_CORNER_RADIUS = 12;
|
|
300
|
+
export const BOARD_TITLE_FONT_SIZE = 20;
|
|
301
|
+
export const BOARD_TITLE_COLOR = '#262626';
|
|
302
|
+
/**
|
|
303
|
+
* The size a fresh board is created at, and the room its furniture takes.
|
|
304
|
+
*
|
|
305
|
+
* The Context Map board's own numbers: a C4 diagram is the same kind of object —
|
|
306
|
+
* a sheet you spread out and add boxes to as the system is discovered — so it
|
|
307
|
+
* starts at the same size and grows the same way. The top margin is deeper than
|
|
308
|
+
* the other three because that is where the title is written.
|
|
309
|
+
*/
|
|
310
|
+
export const BOARD_REF_WIDTH = 1400;
|
|
311
|
+
export const BOARD_REF_HEIGHT = 900;
|
|
312
|
+
export const BOARD_MARGIN = 24;
|
|
313
|
+
export const BOARD_TITLE_MARGIN = 56;
|
|
314
|
+
/* ── The boundary ──────────────────────────────────────────────────────── */
|
|
315
|
+
/**
|
|
316
|
+
* The boundary's frame and its name, at the stencil's own values ×2 (`.st20`
|
|
317
|
+
* and `.st8` in the reference model).
|
|
318
|
+
*
|
|
319
|
+
* All four numbers changed with the PO's recette of 27/08/2026, and all four
|
|
320
|
+
* were house style before it: the frame was a mid grey at weight 2 with rounded
|
|
321
|
+
* corners, and the stencil draws `#444444` at weight 1 with SQUARE ones. The
|
|
322
|
+
* name is black rather than grey and a size larger, which is what it takes to
|
|
323
|
+
* read a boundary's name over the diagram it is drawn on top of.
|
|
324
|
+
*/
|
|
325
|
+
export const BOUNDARY_STROKE = '#444444';
|
|
326
|
+
export const BOUNDARY_WIDTH = 0.5 * STENCIL_SCALE;
|
|
327
|
+
export const BOUNDARY_CORNER_RADIUS = 0;
|
|
328
|
+
/** The dash, in model units: the stencil's `stroke-dasharray:7.5,4.5`, ×2. */
|
|
329
|
+
export const BOUNDARY_DASH = [
|
|
330
|
+
7.5 * STENCIL_SCALE,
|
|
331
|
+
4.5 * STENCIL_SCALE,
|
|
332
|
+
];
|
|
333
|
+
export const BOUNDARY_NAME_FONT_SIZE = 10 * STENCIL_SCALE;
|
|
334
|
+
export const BOUNDARY_NAME_COLOR = '#000000';
|
|
335
|
+
/**
|
|
336
|
+
* The bracket line under a boundary's name — `[Software System]`, `[Container]`
|
|
337
|
+
* — at the stencil's own 6px ×2, on the same 1.917em baseline step it uses.
|
|
338
|
+
*
|
|
339
|
+
* Vocabulary, not user text: the words come from the declaration's `labelKey`
|
|
340
|
+
* and are translatable through the host's catalogue, which is also what keeps
|
|
341
|
+
* them out of the in-place editor. A boundary's NAME is the author's; what kind
|
|
342
|
+
* of boundary it is, is the notation's.
|
|
343
|
+
*/
|
|
344
|
+
export const BOUNDARY_TYPE_FONT_SIZE = 6 * STENCIL_SCALE;
|
|
345
|
+
export const BOUNDARY_TYPE_STEP = 1.917 * BOUNDARY_TYPE_FONT_SIZE;
|
|
346
|
+
/** How far above the bottom edge of the plot the name's baseline sits. */
|
|
347
|
+
export const BOUNDARY_NAME_INSET = 8;
|
|
348
|
+
export const BOUNDARY_REF_WIDTH = 520;
|
|
349
|
+
export const BOUNDARY_REF_HEIGHT = 360;
|
|
350
|
+
/**
|
|
351
|
+
* The boundary's inset. Small and equal on all four sides: unlike a board, a
|
|
352
|
+
* boundary has no furniture to make room for — the name is written INSIDE the
|
|
353
|
+
* bottom-left corner of the plot, over the diagram, exactly as C4 draws it.
|
|
354
|
+
*/
|
|
355
|
+
export const BOUNDARY_MARGIN = 12;
|
|
356
|
+
/**
|
|
357
|
+
* The wording a fresh boundary is named with, per variant.
|
|
358
|
+
*
|
|
359
|
+
* The variant changes the DEFAULT NAME and nothing else: both are the same
|
|
360
|
+
* dashed rectangle, and C4 tells them apart by what is written under the corner.
|
|
361
|
+
* Read by the creation site (which writes `name`), never by the renderer — the
|
|
362
|
+
* declaration draws whatever the user's own `name` says, so a boundary renamed
|
|
363
|
+
* on the canvas keeps its words whatever its variant.
|
|
364
|
+
*/
|
|
365
|
+
export const BOUNDARY_LABEL = {
|
|
366
|
+
system: 'System boundary',
|
|
367
|
+
container: 'Container boundary',
|
|
368
|
+
};
|
|
369
|
+
/* ── The relationship ──────────────────────────────────────────────────── */
|
|
370
|
+
/**
|
|
371
|
+
* Relationship connector preset — the dashed arrow C4 draws between elements.
|
|
372
|
+
*
|
|
373
|
+
* DASHED and not solid, which is the stencil's own choice and worth keeping:
|
|
374
|
+
* every line on a C4 diagram is a relationship, so the dash is not a
|
|
375
|
+
* distinction between two kinds of line but the house style of the one kind
|
|
376
|
+
* there is. Grey rather than black for the same reason the boundary is: the
|
|
377
|
+
* boxes are the statement, the arrows are the grammar between them.
|
|
378
|
+
*
|
|
379
|
+
* `#444444` at weight 1.5 is the stencil's `.st15` (`stroke:#444444`,
|
|
380
|
+
* `stroke-width:0.75`) at ×2, and it is the SAME grey the boundary frame is
|
|
381
|
+
* drawn in — one neutral for everything that is not an element.
|
|
382
|
+
*/
|
|
383
|
+
export const RELATIONSHIP_STROKE = '#444444';
|
|
384
|
+
export const RELATIONSHIP_WIDTH = 0.75 * STENCIL_SCALE;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { C4RenderViewExtension, C4ViewExtension } from './view.js';
|
|
2
|
+
/** Host wiring for the c4 framework. */
|
|
3
|
+
export declare const c4Framework: {
|
|
4
|
+
readonly flag: "c4";
|
|
5
|
+
readonly telemetryKey: "c4";
|
|
6
|
+
readonly extensions: readonly [{
|
|
7
|
+
readonly viewExtension: typeof C4RenderViewExtension;
|
|
8
|
+
}, {
|
|
9
|
+
readonly flag: "c4";
|
|
10
|
+
readonly viewExtension: typeof C4ViewExtension;
|
|
11
|
+
}];
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { C4RenderViewExtension, C4ViewExtension } from './view.js';
|
|
2
|
+
/** Host wiring for the c4 framework. */
|
|
3
|
+
export const c4Framework = {
|
|
4
|
+
flag: 'c4',
|
|
5
|
+
telemetryKey: 'c4',
|
|
6
|
+
extensions: [
|
|
7
|
+
{ viewExtension: C4RenderViewExtension },
|
|
8
|
+
{ flag: 'c4', viewExtension: C4ViewExtension },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EdgelessC4Menu } from './toolbar/c4-menu.js';
|
|
2
|
+
import { EdgelessC4SeniorButton } from './toolbar/c4-senior-button.js';
|
|
3
|
+
export declare function effects(): void;
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
'edgeless-c4-menu': EdgelessC4Menu;
|
|
7
|
+
'edgeless-c4-senior-button': EdgelessC4SeniorButton;
|
|
8
|
+
}
|
|
9
|
+
}
|
package/dist/effects.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EdgelessC4Menu } from './toolbar/c4-menu.js';
|
|
2
|
+
import { EdgelessC4SeniorButton } from './toolbar/c4-senior-button.js';
|
|
3
|
+
export function effects() {
|
|
4
|
+
customElements.define('edgeless-c4-menu', EdgelessC4Menu);
|
|
5
|
+
customElements.define('edgeless-c4-senior-button', EdgelessC4SeniorButton);
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import type { C4BoardElementModel, C4BoundaryElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
/**
|
|
4
|
+
* Canvas renderers for the two C4 frames.
|
|
5
|
+
*
|
|
6
|
+
* There is no C4 drawing code here: both are INSTANTIATIONS of the
|
|
7
|
+
* framework-background primitive, configured by the declarations in
|
|
8
|
+
* `background.ts`. Exported as functions as well as extensions because the
|
|
9
|
+
* fidelity suite drives them directly with a canvas stub.
|
|
10
|
+
*/
|
|
11
|
+
export declare const c4Board: ElementRenderer<C4BoardElementModel>;
|
|
12
|
+
export declare const C4BoardRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
|
|
13
|
+
identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<C4BoardElementModel>>;
|
|
14
|
+
};
|
|
15
|
+
export declare const c4Boundary: ElementRenderer<C4BoundaryElementModel>;
|
|
16
|
+
export declare const C4BoundaryRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
|
|
17
|
+
identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<C4BoundaryElementModel>>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { C4_BOARD_BACKGROUND, C4_BOUNDARY_BACKGROUND } from './background.js';
|
|
3
|
+
/**
|
|
4
|
+
* Canvas renderers for the two C4 frames.
|
|
5
|
+
*
|
|
6
|
+
* There is no C4 drawing code here: both are INSTANTIATIONS of the
|
|
7
|
+
* framework-background primitive, configured by the declarations in
|
|
8
|
+
* `background.ts`. Exported as functions as well as extensions because the
|
|
9
|
+
* fidelity suite drives them directly with a canvas stub.
|
|
10
|
+
*/
|
|
11
|
+
export const c4Board = createFrameworkBackgroundRenderer(C4_BOARD_BACKGROUND);
|
|
12
|
+
export const C4BoardRendererExtension = ElementRendererExtension(C4_BOARD_BACKGROUND.type, c4Board);
|
|
13
|
+
export const c4Boundary = createFrameworkBackgroundRenderer(C4_BOUNDARY_BACKGROUND);
|
|
14
|
+
export const C4BoundaryRendererExtension = ElementRendererExtension(C4_BOUNDARY_BACKGROUND.type, c4Boundary);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import type { C4BoardElementModel, C4BoundaryElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
/**
|
|
5
|
+
* The one gesture the two C4 frames carry: a double-click on the name edits it
|
|
6
|
+
* in place.
|
|
7
|
+
*
|
|
8
|
+
* Both frames are a card with exactly one editable word on it — the board's
|
|
9
|
+
* title, the boundary's name — so the gesture is written once here and the two
|
|
10
|
+
* views differ only in which declaration they hit-test against. That is the
|
|
11
|
+
* simplified version of `BpmnPoolView`: no lanes, no separators, no armed drag,
|
|
12
|
+
* because neither frame has anything inside it to divide.
|
|
13
|
+
*
|
|
14
|
+
* Which labels exist, where they sit, what they SAY and which are editable all
|
|
15
|
+
* come from the declaration the renderer paints (`backgroundLabelHits`), so a
|
|
16
|
+
* label can never be drawn in one place and clicked in another — the same
|
|
17
|
+
* source `WardleyView` reads, and the reason this class has no coordinates of
|
|
18
|
+
* its own.
|
|
19
|
+
*
|
|
20
|
+
* ponytail: like every other framework view in the library, only `name` may be
|
|
21
|
+
* written. The declarations bind exactly one prop each; the guard is what keeps
|
|
22
|
+
* that true if a second label is ever declared with a prop nobody meant to
|
|
23
|
+
* expose to an in-place editor.
|
|
24
|
+
*/
|
|
25
|
+
declare abstract class C4FrameView<T extends C4BoardElementModel | C4BoundaryElementModel> extends GfxElementModelView<T> {
|
|
26
|
+
/** The declaration this view hit-tests against — the one the renderer paints. */
|
|
27
|
+
protected abstract get def(): FrameworkBackgroundDef;
|
|
28
|
+
/** The in-place `<input>` used to edit the name, or null when idle. */
|
|
29
|
+
private _editor;
|
|
30
|
+
onCreated(): void;
|
|
31
|
+
onDestroyed(): void;
|
|
32
|
+
private _onDblClick;
|
|
33
|
+
/**
|
|
34
|
+
* @param current the words currently DRAWN, which is what the user aimed at —
|
|
35
|
+
* never `model.name`, so a frame showing its declared wording opens on that
|
|
36
|
+
* wording rather than on an empty box.
|
|
37
|
+
*/
|
|
38
|
+
private _openEditor;
|
|
39
|
+
private _closeEditor;
|
|
40
|
+
}
|
|
41
|
+
/** The sheet a C4 diagram is drawn on. Double-click its title to rename it. */
|
|
42
|
+
export declare class C4BoardView extends C4FrameView<C4BoardElementModel> {
|
|
43
|
+
static type: string;
|
|
44
|
+
protected get def(): FrameworkBackgroundDef;
|
|
45
|
+
}
|
|
46
|
+
/** The dashed frame drawn round part of one. Double-click its name to rename. */
|
|
47
|
+
export declare class C4BoundaryView extends C4FrameView<C4BoundaryElementModel> {
|
|
48
|
+
static type: string;
|
|
49
|
+
protected get def(): FrameworkBackgroundDef;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { backgroundLabelHits, EdgelessCRUDIdentifier, hitTestBackgroundLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { TranslationProvider } from '@formicoidea/labre-core/shared/services';
|
|
3
|
+
import { rotatePoint } from '@formicoidea/labre-core/global/gfx';
|
|
4
|
+
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
5
|
+
import { C4_BOARD_BACKGROUND, C4_BOUNDARY_BACKGROUND } from './background.js';
|
|
6
|
+
/**
|
|
7
|
+
* The one gesture the two C4 frames carry: a double-click on the name edits it
|
|
8
|
+
* in place.
|
|
9
|
+
*
|
|
10
|
+
* Both frames are a card with exactly one editable word on it — the board's
|
|
11
|
+
* title, the boundary's name — so the gesture is written once here and the two
|
|
12
|
+
* views differ only in which declaration they hit-test against. That is the
|
|
13
|
+
* simplified version of `BpmnPoolView`: no lanes, no separators, no armed drag,
|
|
14
|
+
* because neither frame has anything inside it to divide.
|
|
15
|
+
*
|
|
16
|
+
* Which labels exist, where they sit, what they SAY and which are editable all
|
|
17
|
+
* come from the declaration the renderer paints (`backgroundLabelHits`), so a
|
|
18
|
+
* label can never be drawn in one place and clicked in another — the same
|
|
19
|
+
* source `WardleyView` reads, and the reason this class has no coordinates of
|
|
20
|
+
* its own.
|
|
21
|
+
*
|
|
22
|
+
* ponytail: like every other framework view in the library, only `name` may be
|
|
23
|
+
* written. The declarations bind exactly one prop each; the guard is what keeps
|
|
24
|
+
* that true if a second label is ever declared with a prop nobody meant to
|
|
25
|
+
* expose to an in-place editor.
|
|
26
|
+
*/
|
|
27
|
+
class C4FrameView extends GfxElementModelView {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
/** The in-place `<input>` used to edit the name, or null when idle. */
|
|
31
|
+
this._editor = null;
|
|
32
|
+
}
|
|
33
|
+
onCreated() {
|
|
34
|
+
super.onCreated();
|
|
35
|
+
this.on('dblclick', e => this._onDblClick(e));
|
|
36
|
+
}
|
|
37
|
+
onDestroyed() {
|
|
38
|
+
this._closeEditor();
|
|
39
|
+
super.onDestroyed();
|
|
40
|
+
}
|
|
41
|
+
_onDblClick(e) {
|
|
42
|
+
if (this.gfx.std.store.readonly || this.model.isLocked())
|
|
43
|
+
return;
|
|
44
|
+
const [mx, my] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
|
45
|
+
const [bx, by, w, h] = this.model.deserializedXYWH;
|
|
46
|
+
// Element-local coordinates, undoing the element rotation about its centre.
|
|
47
|
+
let lx = mx - bx;
|
|
48
|
+
let ly = my - by;
|
|
49
|
+
const rot = this.model.rotate ?? 0;
|
|
50
|
+
if (rot) {
|
|
51
|
+
const center = [bx + w / 2, by + h / 2];
|
|
52
|
+
const [ux, uy] = rotatePoint([mx, my], center, -rot);
|
|
53
|
+
lx = ux - bx;
|
|
54
|
+
ly = uy - by;
|
|
55
|
+
}
|
|
56
|
+
const hit = hitTestBackgroundLabel(backgroundLabelHits(this.def, this.model, w, h, this.gfx.std.getOptional(TranslationProvider)), lx, ly);
|
|
57
|
+
if (!hit || hit.prop !== 'name')
|
|
58
|
+
return;
|
|
59
|
+
this._openEditor(hit.text, e);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @param current the words currently DRAWN, which is what the user aimed at —
|
|
63
|
+
* never `model.name`, so a frame showing its declared wording opens on that
|
|
64
|
+
* wording rather than on an empty box.
|
|
65
|
+
*/
|
|
66
|
+
_openEditor(current, e) {
|
|
67
|
+
this._closeEditor();
|
|
68
|
+
const input = document.createElement('input');
|
|
69
|
+
input.value = current;
|
|
70
|
+
Object.assign(input.style, {
|
|
71
|
+
position: 'fixed',
|
|
72
|
+
left: `${e.raw.clientX}px`,
|
|
73
|
+
top: `${e.raw.clientY}px`,
|
|
74
|
+
transform: 'translate(-50%, -50%)',
|
|
75
|
+
zIndex: '10000',
|
|
76
|
+
minWidth: '140px',
|
|
77
|
+
padding: '3px 8px',
|
|
78
|
+
font: '14px Inter, sans-serif',
|
|
79
|
+
color: 'var(--affine-text-primary-color, #1f2328)',
|
|
80
|
+
background: 'var(--affine-background-overlay-panel-color, #ffffff)',
|
|
81
|
+
border: '1px solid var(--affine-primary-color, #1e96eb)',
|
|
82
|
+
borderRadius: '6px',
|
|
83
|
+
boxShadow: 'var(--affine-shadow-2, 0 2px 8px rgba(0,0,0,0.18))',
|
|
84
|
+
outline: 'none',
|
|
85
|
+
});
|
|
86
|
+
document.body.append(input);
|
|
87
|
+
this._editor = input;
|
|
88
|
+
// Mark "editing" so the global edgeless key handlers (delete, escape, …)
|
|
89
|
+
// don't act on the frame while the user types.
|
|
90
|
+
this.gfx.selection.set({ elements: [this.model.id], editing: true });
|
|
91
|
+
input.focus();
|
|
92
|
+
input.select();
|
|
93
|
+
const commit = () => {
|
|
94
|
+
// Guard against re-entrancy: removing the input fires `blur`, which would
|
|
95
|
+
// otherwise call `commit` a second time.
|
|
96
|
+
if (this._editor !== input)
|
|
97
|
+
return;
|
|
98
|
+
const value = input.value;
|
|
99
|
+
this._closeEditor();
|
|
100
|
+
// Opening an editor is not renaming: an untouched value would push an
|
|
101
|
+
// empty entry onto undo and freeze the drawn wording as the user's own.
|
|
102
|
+
if (value === current)
|
|
103
|
+
return;
|
|
104
|
+
this.gfx.std.store.captureSync();
|
|
105
|
+
this.gfx.std
|
|
106
|
+
.get(EdgelessCRUDIdentifier)
|
|
107
|
+
.updateElement(this.model.id, { name: value });
|
|
108
|
+
};
|
|
109
|
+
input.addEventListener('keydown', ev => {
|
|
110
|
+
ev.stopPropagation();
|
|
111
|
+
if (ev.key === 'Enter') {
|
|
112
|
+
ev.preventDefault();
|
|
113
|
+
commit();
|
|
114
|
+
}
|
|
115
|
+
else if (ev.key === 'Escape') {
|
|
116
|
+
ev.preventDefault();
|
|
117
|
+
this._closeEditor();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
input.addEventListener('blur', commit);
|
|
121
|
+
}
|
|
122
|
+
_closeEditor() {
|
|
123
|
+
if (!this._editor)
|
|
124
|
+
return;
|
|
125
|
+
const input = this._editor;
|
|
126
|
+
this._editor = null;
|
|
127
|
+
input.remove();
|
|
128
|
+
if (this.isConnected) {
|
|
129
|
+
this.gfx.selection.set({ elements: [this.model.id], editing: false });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The sheet a C4 diagram is drawn on. Double-click its title to rename it. */
|
|
134
|
+
export class C4BoardView extends C4FrameView {
|
|
135
|
+
static { this.type = 'c4Board'; }
|
|
136
|
+
get def() {
|
|
137
|
+
return C4_BOARD_BACKGROUND;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** The dashed frame drawn round part of one. Double-click its name to rename. */
|
|
141
|
+
export class C4BoundaryView extends C4FrameView {
|
|
142
|
+
static { this.type = 'c4Boundary'; }
|
|
143
|
+
get def() {
|
|
144
|
+
return C4_BOUNDARY_BACKGROUND;
|
|
145
|
+
}
|
|
146
|
+
}
|