@formicoidea/labre-framework-ddd-core-domain 0.32.0 → 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.
@@ -0,0 +1,29 @@
1
+ import type { BlockStdScope } from '@formicoidea/labre-core/std';
2
+ import { type GfxController } from '@formicoidea/labre-core/std/gfx';
3
+ type Surface = NonNullable<GfxController['surface']>;
4
+ /**
5
+ * Post a Core Domain Chart background, centred on `(cx, cy)`.
6
+ *
7
+ * The size, the role and whether the handles are offered all come from the
8
+ * DECLARATION (`createWardleyBackground` is the pattern): the chart is a
9
+ * first-class role, so validation rules position artefacts against
10
+ * `core-domain:chart` and never against the `coreDomain` element type, and a
11
+ * chart posted from the sub-menu agrees with one posted from a template.
12
+ */
13
+ export declare function createCoreDomainChart(surface: Surface, cx: number, cy: number): string;
14
+ /**
15
+ * Activate the native connector tool, pre-styled for a movement over time
16
+ * (dashed, red, arrow at the far end) and TYPED with `core-domain:movement`.
17
+ *
18
+ * The gesture replaces the free arrow the sub-menu used to drop on the canvas,
19
+ * and the reason is `docs/adr/0010`: a movement is a sentence — "this context
20
+ * is moving to that position" — and its `source → target` pair only becomes a
21
+ * STATEMENT once the user drew it end to end, from the current position to the
22
+ * future one (the role's own gesture hint). `core-domain.malformed-movement`
23
+ * reads exactly that pair back.
24
+ *
25
+ * Arrows drawn before this change carry no role and stay neutral: they are
26
+ * drawings, and no rule will ever speak about them.
27
+ */
28
+ export declare function activateMovement(std: BlockStdScope): void;
29
+ export {};
@@ -0,0 +1,58 @@
1
+ import { backgroundSize } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
+ import { MOVEMENT_COLOR } from '@formicoidea/labre-ddd-shared';
4
+ import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
5
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
6
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
7
+ import { CORE_DOMAIN_BACKGROUND } from './core-domain/background.js';
8
+ import { CORE_DOMAIN_ROLE } from './roles.js';
9
+ /** The stroke width the movement arrow has always been drawn at. */
10
+ const MOVEMENT_STROKE_WIDTH = 2;
11
+ /**
12
+ * Post a Core Domain Chart background, centred on `(cx, cy)`.
13
+ *
14
+ * The size, the role and whether the handles are offered all come from the
15
+ * DECLARATION (`createWardleyBackground` is the pattern): the chart is a
16
+ * first-class role, so validation rules position artefacts against
17
+ * `core-domain:chart` and never against the `coreDomain` element type, and a
18
+ * chart posted from the sub-menu agrees with one posted from a template.
19
+ */
20
+ export function createCoreDomainChart(surface, cx, cy) {
21
+ const { width, height } = backgroundSize(CORE_DOMAIN_BACKGROUND);
22
+ return surface.addElement({
23
+ type: CORE_DOMAIN_BACKGROUND.type,
24
+ role: CORE_DOMAIN_BACKGROUND.role,
25
+ resizeEnabled: CORE_DOMAIN_BACKGROUND.geometry.resizable,
26
+ xywh: new Bound(cx - width / 2, cy - height / 2, width, height).serialize(),
27
+ });
28
+ }
29
+ /**
30
+ * Activate the native connector tool, pre-styled for a movement over time
31
+ * (dashed, red, arrow at the far end) and TYPED with `core-domain:movement`.
32
+ *
33
+ * The gesture replaces the free arrow the sub-menu used to drop on the canvas,
34
+ * and the reason is `docs/adr/0010`: a movement is a sentence — "this context
35
+ * is moving to that position" — and its `source → target` pair only becomes a
36
+ * STATEMENT once the user drew it end to end, from the current position to the
37
+ * future one (the role's own gesture hint). `core-domain.malformed-movement`
38
+ * reads exactly that pair back.
39
+ *
40
+ * Arrows drawn before this change carry no role and stay neutral: they are
41
+ * drawings, and no rule will ever speak about them.
42
+ */
43
+ export function activateMovement(std) {
44
+ const gfx = std.get(GfxControllerIdentifier);
45
+ gfx.tool.setTool(ConnectorTool, {
46
+ mode: ConnectorMode.Straight,
47
+ role: CORE_DOMAIN_ROLE.movement,
48
+ // The look rides on the activation, never through the last-props store:
49
+ // the plain connector tool must keep the user's own style (#144 M1).
50
+ style: {
51
+ stroke: MOVEMENT_COLOR,
52
+ strokeStyle: StrokeStyle.Dash,
53
+ strokeWidth: MOVEMENT_STROKE_WIDTH,
54
+ frontEndpointStyle: PointStyle.None,
55
+ rearEndpointStyle: PointStyle.Arrow,
56
+ },
57
+ });
58
+ }
package/dist/commands.js CHANGED
@@ -1,7 +1,7 @@
1
- import { addConnector, addDot, addMarker, CD_SUBDOMAINS, MOVEMENT_COLOR, placeDddElement, TEAM_TOPOLOGIES, } from '@formicoidea/labre-ddd-shared';
2
- import { Bound } from '@formicoidea/labre-core/global/gfx';
1
+ import { addDot, addMarker, CD_SUBDOMAINS, MOVEMENT_COLOR, placeDddElement, TEAM_TOPOLOGIES, } from '@formicoidea/labre-ddd-shared';
3
2
  import { svg } from 'lit';
4
- import { REF_H, REF_W } from './core-domain/consts.js';
3
+ import { activateMovement, createCoreDomainChart } from './actions.js';
4
+ import { markerRole, subdomainRole } from './roles.js';
5
5
  /**
6
6
  * The Core Domain Chart palette as commands: the background, the five
7
7
  * sub-domain dots, the three Team Topologies markers and the movement arrow.
@@ -19,10 +19,7 @@ const SPECS = [
19
19
  iconKey: 'ddd-core-domain.chart',
20
20
  element: 'background',
21
21
  icon: chartSwatch,
22
- run: std => placeDddElement(std, (surface, cx, cy) => surface.addElement({
23
- type: 'coreDomain',
24
- xywh: new Bound(cx - REF_W / 2, cy - REF_H / 2, REF_W, REF_H).serialize(),
25
- })),
22
+ run: std => placeDddElement(std, (surface, cx, cy) => createCoreDomainChart(surface, cx, cy)),
26
23
  },
27
24
  ...CD_SUBDOMAINS.map((preset) => ({
28
25
  id: `add${preset.kind[0].toUpperCase()}${preset.kind.slice(1)}`,
@@ -30,7 +27,11 @@ const SPECS = [
30
27
  iconKey: `ddd-core-domain.subdomain.${preset.kind}`,
31
28
  element: `subdomain:${preset.kind}`,
32
29
  icon: dotSwatch(preset.fill),
33
- run: std => placeDddElement(std, (surface, cx, cy) => addDot(surface, std, cx, cy, preset.fill, preset.label)),
30
+ run: std => placeDddElement(std, (surface, cx, cy) => addDot(surface, std, cx, cy, preset.fill, preset.label,
31
+ // The dot IS the sub-domain: the role rides on the ellipse, so a
32
+ // rule about where a sub-domain sits measures the artefact and not
33
+ // the group that also holds its name.
34
+ subdomainRole(preset.kind))),
34
35
  })),
35
36
  ...TEAM_TOPOLOGIES.map((preset) => ({
36
37
  id: `add${preset.kind[0].toUpperCase()}${preset.kind.slice(1)}`,
@@ -42,19 +43,24 @@ const SPECS = [
42
43
  fill: preset.fill,
43
44
  letter: preset.letter,
44
45
  label: preset.label,
46
+ // The square IS the marker, so the role rides on it and not on the
47
+ // group that also holds its caption — the same call as the dot.
48
+ // Without this the automatic legend, which detects by role and only
49
+ // by role, could not see the markers at all.
50
+ role: markerRole(preset.kind),
45
51
  })),
46
52
  })),
47
53
  {
48
54
  id: 'addMovement',
49
55
  label: 'Movement over time',
50
56
  iconKey: 'ddd-core-domain.movement',
57
+ // Historical telemetry value, unchanged by the gesture becoming a drag.
51
58
  element: 'movement',
52
59
  icon: movementSwatch,
53
- run: std => placeDddElement(std, (surface, cx, cy) => addConnector(surface, cx - 80, cy + 60, cx + 80, cy - 60, {
54
- rearArrow: true,
55
- dashed: true,
56
- stroke: MOVEMENT_COLOR,
57
- })),
60
+ // No longer a free arrow dropped at the viewport centre: the movement is a
61
+ // typed edge, so the user draws it from the current position to the future
62
+ // one and the pair they drew IS the statement (`docs/adr/0010`).
63
+ run: activateMovement,
58
64
  },
59
65
  ];
60
66
  export const coreDomainCommands = SPECS.map((spec, order) => ({
@@ -0,0 +1,4 @@
1
+ import type { FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
2
+ /** The palette entries the notation is made of — the tone convention's reference. */
3
+ export declare const CORE_DOMAIN_LEGEND_TONES: readonly string[];
4
+ export declare const CORE_DOMAIN_BACKGROUND: FrameworkBackgroundDef;
@@ -0,0 +1,370 @@
1
+ import { CD_SUBDOMAINS, FONT_FAMILY } from '@formicoidea/labre-ddd-shared';
2
+ import { CORE_DOMAIN_ROLE } from '../roles.js';
3
+ /**
4
+ * The Core Domain Chart background, DECLARED.
5
+ *
6
+ * This file is the whole of what makes a Core Domain Chart look like a Core
7
+ * Domain Chart. There is no chart-specific rendering code left: the primitive
8
+ * (`FrameworkBackgroundDef` / `createFrameworkBackgroundRenderer` in
9
+ * `@labre/affine-block-surface`) paints this declaration, and would paint any
10
+ * other framework's the same way.
11
+ *
12
+ * Nothing here changes the DOCUMENT. The persisted element type is still
13
+ * `coreDomain` and its props are untouched — `showZones`, `showLabels`,
14
+ * `resizeEnabled` — they are simply named by the declaration instead of being
15
+ * read by hand-written drawing code. A chart authored before this file existed
16
+ * opens with the same geometry, the same bands and the same words: every
17
+ * coordinate below is the old `consts.ts` value converted, once, into a ratio of
18
+ * the plot, and `__tests__/background.unit.spec.ts` pins the conversion against
19
+ * the absolute numbers it came from.
20
+ *
21
+ * ## Two readings of one frame
22
+ *
23
+ * `variantProp: 'variant'` (see `CoreDomainChartElementModel.variant`) selects
24
+ * between the CLASSIC chart — Generic / Supporting / Core — and the MIGRATION
25
+ * one, whose four quadrants name the migration conversation instead. Same axes,
26
+ * same geometry, same element: only the regions and the vertical axis' title
27
+ * change, which is precisely what a variant is for. No validation rule cites a
28
+ * migration zone.
29
+ */
30
+ /**
31
+ * The authoring reference space, kept as documentation of where every ratio
32
+ * below comes from: the chart was drawn at 900 × 820 with its plot inset by the
33
+ * margins declared in {@link CORE_DOMAIN_BACKGROUND}, so the plot is 786 × 746
34
+ * and `(absolute - origin) / span` is the conversion.
35
+ */
36
+ const PLOT = { x0: 60, y0: 24, width: 786, height: 746 };
37
+ /** An absolute X of the reference drawing, as a ratio of the plot. */
38
+ const rx = (x) => (x - PLOT.x0) / PLOT.width;
39
+ /** An absolute Y of the reference drawing, as a ratio of the plot. */
40
+ const ry = (y) => (y - PLOT.y0) / PLOT.height;
41
+ /** An absolute width, as a ratio of the plot width. */
42
+ const rw = (w) => w / PLOT.width;
43
+ /** An absolute height, as a ratio of the plot height. */
44
+ const rh = (h) => h / PLOT.height;
45
+ /**
46
+ * The colour code: every colour is named, never repeated as a hex.
47
+ *
48
+ * The last five entries are the **legend**, and they are palette entries for a
49
+ * reason: `core-domain.off-legend-colour` names them
50
+ * (`ToneConventionDef.palette`) instead of restating a colour, so the notation
51
+ * and the chart are restyled together, in one place. They are read straight off
52
+ * `CD_SUBDOMAINS`, the very table the sub-menu builds its five dots from — the
53
+ * convention and the swatches can therefore never drift apart.
54
+ *
55
+ * The zone tints carry their alpha BAKED INTO THE HEX (`…99` is the 0.6 the
56
+ * old renderer set with `globalAlpha`): a zone fill is handed to `fillStyle`
57
+ * verbatim, so eight-digit hex is how a translucent band is declared.
58
+ */
59
+ const PALETTE = {
60
+ axis: '#000000',
61
+ title: '#000000',
62
+ tick: '#777777',
63
+ /** Zone names of the classic reading: white, over saturated bands. */
64
+ zoneLabel: '#ffffff',
65
+ /** Zone names of the migration reading: ink, over pale tints. */
66
+ zoneLabelDark: '#1f2328',
67
+ zoneGeneric: '#b3b3b399',
68
+ zoneSupporting: '#9933ff99',
69
+ zoneCore: '#4d990099',
70
+ zoneLowHangingFruit: '#4d990026',
71
+ zoneRiskSeeking: '#9933ff26',
72
+ zoneRiskAverse: '#b3b3b326',
73
+ zoneLastToothpaste: '#ff333326',
74
+ ...Object.fromEntries(CD_SUBDOMAINS.map(preset => [preset.kind, preset.fill])),
75
+ };
76
+ /** The palette entries the notation is made of — the tone convention's reference. */
77
+ export const CORE_DOMAIN_LEGEND_TONES = CD_SUBDOMAINS.map(preset => preset.kind);
78
+ const AXIS_TEXT = {
79
+ size: 14,
80
+ weight: 600,
81
+ color: '@title',
82
+ };
83
+ const TICK_TEXT = { size: 12, color: '@tick' };
84
+ const ZONE_TEXT = {
85
+ size: 20,
86
+ weight: 700,
87
+ color: '@zoneLabel',
88
+ };
89
+ const CORE_TEXT = { ...ZONE_TEXT, size: 26 };
90
+ const MIGRATION_TEXT = {
91
+ size: 20,
92
+ weight: 700,
93
+ color: '@zoneLabelDark',
94
+ };
95
+ /** A centred zone name, gated by `showLabels` like every other word on the chart. */
96
+ function zoneLabel(id, key, fallback, x, y, style) {
97
+ return {
98
+ id,
99
+ labelKey: `com.labre.core-domain.background.zone.${key}`,
100
+ fallback,
101
+ anchor: { x: rx(x), y: ry(y) },
102
+ style,
103
+ align: 'center',
104
+ visibleProp: 'showLabels',
105
+ };
106
+ }
107
+ /**
108
+ * The classic reading: the three named bands of the DDD Crew chart, plus the
109
+ * fourth (unnamed) quadrant the template tints without ever writing on.
110
+ *
111
+ * `variants: ['classic']` is what makes them a READING rather than the chart:
112
+ * turn the frame to `migration` and these regions are not there — which is also
113
+ * why `core-domain.outsourced-core` falls silent on a migration chart instead of
114
+ * measuring against a quadrant nobody can see.
115
+ *
116
+ * NOTE on the ids: `supporting-low-complexity` names the bottom-right quadrant
117
+ * — high differentiation, LOW complexity, since the complexity axis runs
118
+ * upwards. (The plan first named it `supporting-high-complexity`, factually
119
+ * inverted; corrected at the merge recette of 26/08/2026 — no rule cites it.)
120
+ */
121
+ const CLASSIC_ZONES = [
122
+ {
123
+ id: 'generic',
124
+ variants: ['classic'],
125
+ rect: { x: rx(70), y: ry(30), w: rw(150), h: rh(720) },
126
+ fill: '@zoneGeneric',
127
+ fillVisibleProp: 'showZones',
128
+ label: zoneLabel('generic', 'generic', 'Generic', 150, 474, ZONE_TEXT),
129
+ },
130
+ {
131
+ id: 'supporting-low-diff',
132
+ variants: ['classic'],
133
+ rect: { x: rx(220), y: ry(30), w: rw(220), h: rh(720) },
134
+ fill: '@zoneSupporting',
135
+ fillVisibleProp: 'showZones',
136
+ label: zoneLabel('supporting', 'supporting', 'Supporting', 340, 474, ZONE_TEXT),
137
+ },
138
+ {
139
+ id: 'core',
140
+ variants: ['classic'],
141
+ rect: { x: rx(440), y: ry(30), w: rw(400), h: rh(360) },
142
+ fill: '@zoneCore',
143
+ fillVisibleProp: 'showZones',
144
+ label: zoneLabel('core', 'core', 'Core', 640, 214, CORE_TEXT),
145
+ },
146
+ {
147
+ // The template tints this one and writes nothing on it, so neither does the
148
+ // declaration: an invented name would be prose nobody asked for.
149
+ id: 'supporting-low-complexity',
150
+ variants: ['classic'],
151
+ rect: { x: rx(440), y: ry(390), w: rw(400), h: rh(360) },
152
+ fill: '@zoneSupporting',
153
+ fillVisibleProp: 'showZones',
154
+ },
155
+ ];
156
+ /**
157
+ * The migration reading: the same frame, four quadrants, and the vocabulary of
158
+ * a modernisation conversation.
159
+ *
160
+ * Regions of READING only — deliberately not cited by any rule. What counts as
161
+ * a low-hanging fruit is a judgement about cost and appetite that no coordinate
162
+ * decides, and a rule reading it off a dot's position would be inventing a
163
+ * verdict the chart never claimed.
164
+ */
165
+ const MIGRATION_ZONES = [
166
+ {
167
+ id: 'last-toothpaste',
168
+ variants: ['migration'],
169
+ rect: { x: 0, y: 0, w: 0.5, h: 0.5 },
170
+ fill: '@zoneLastToothpaste',
171
+ fillVisibleProp: 'showZones',
172
+ label: {
173
+ id: 'lastToothpaste',
174
+ labelKey: 'com.labre.core-domain.background.zone.last-toothpaste',
175
+ fallback: 'Last toothpaste',
176
+ anchor: { x: 0.25, y: 0.25 },
177
+ style: MIGRATION_TEXT,
178
+ align: 'center',
179
+ visibleProp: 'showLabels',
180
+ },
181
+ },
182
+ {
183
+ id: 'risk-seeking',
184
+ variants: ['migration'],
185
+ rect: { x: 0.5, y: 0, w: 0.5, h: 0.5 },
186
+ fill: '@zoneRiskSeeking',
187
+ fillVisibleProp: 'showZones',
188
+ label: {
189
+ id: 'riskSeeking',
190
+ labelKey: 'com.labre.core-domain.background.zone.risk-seeking',
191
+ fallback: 'Risk-seeking',
192
+ anchor: { x: 0.75, y: 0.25 },
193
+ style: MIGRATION_TEXT,
194
+ align: 'center',
195
+ visibleProp: 'showLabels',
196
+ },
197
+ },
198
+ {
199
+ id: 'risk-averse',
200
+ variants: ['migration'],
201
+ rect: { x: 0, y: 0.5, w: 0.5, h: 0.5 },
202
+ fill: '@zoneRiskAverse',
203
+ fillVisibleProp: 'showZones',
204
+ label: {
205
+ id: 'riskAverse',
206
+ labelKey: 'com.labre.core-domain.background.zone.risk-averse',
207
+ fallback: 'Risk-averse',
208
+ anchor: { x: 0.25, y: 0.75 },
209
+ style: MIGRATION_TEXT,
210
+ align: 'center',
211
+ visibleProp: 'showLabels',
212
+ },
213
+ },
214
+ {
215
+ id: 'lhf',
216
+ variants: ['migration'],
217
+ rect: { x: 0.5, y: 0.5, w: 0.5, h: 0.5 },
218
+ fill: '@zoneLowHangingFruit',
219
+ fillVisibleProp: 'showZones',
220
+ label: {
221
+ id: 'lowHangingFruit',
222
+ labelKey: 'com.labre.core-domain.background.zone.lhf',
223
+ fallback: 'Low-hanging fruit',
224
+ anchor: { x: 0.75, y: 0.75 },
225
+ style: MIGRATION_TEXT,
226
+ align: 'center',
227
+ visibleProp: 'showLabels',
228
+ },
229
+ },
230
+ ];
231
+ /**
232
+ * The two axes. Complexity runs up the left edge, Business differentiation
233
+ * along the bottom; both arrows point "forward", i.e. towards MORE.
234
+ *
235
+ * Neither axis carries a `visibleProp`: the frame of reference is the chart, and
236
+ * the template has never offered to hide it. `showLabels` gates the WORDS — the
237
+ * titles and the four Low/High ticks — which is the toggle the toolbar has
238
+ * always had (and which the `showLabels` prop has always claimed to cover, zone
239
+ * names included).
240
+ *
241
+ * `arrowSize: 9` reproduces the reference arrowhead's LENGTH exactly; the
242
+ * primitive draws it 9 wide against the template's 10, a half-unit either side
243
+ * of a nine-unit triangle.
244
+ */
245
+ const CORE_DOMAIN_AXES = [
246
+ {
247
+ id: 'complexity',
248
+ orientation: 'vertical',
249
+ at: 0,
250
+ arrow: 'forward',
251
+ arrowSize: 9,
252
+ stroke: { color: '@axis', width: 2 },
253
+ title: {
254
+ id: 'complexityTitle',
255
+ // The classic reading's vertical axis. The migration one relabels it —
256
+ // see `MIGRATION_AXIS_TITLE` below, and why it rides in `endLabels`.
257
+ variants: ['classic'],
258
+ labelKey: 'com.labre.core-domain.background.axis.complexity',
259
+ fallback: 'Complexity',
260
+ anchor: { x: 0, y: ry(400), dx: -32 },
261
+ style: AXIS_TEXT,
262
+ vertical: true,
263
+ visibleProp: 'showLabels',
264
+ },
265
+ endLabels: [
266
+ {
267
+ id: 'complexityLow',
268
+ labelKey: 'com.labre.core-domain.background.complexity.low',
269
+ fallback: 'Low',
270
+ anchor: { x: 0, y: ry(758), dx: -12 },
271
+ style: TICK_TEXT,
272
+ vertical: true,
273
+ visibleProp: 'showLabels',
274
+ },
275
+ {
276
+ id: 'complexityHigh',
277
+ labelKey: 'com.labre.core-domain.background.complexity.high',
278
+ fallback: 'High',
279
+ anchor: { x: 0, y: ry(44), dx: -22 },
280
+ style: TICK_TEXT,
281
+ vertical: true,
282
+ visibleProp: 'showLabels',
283
+ },
284
+ /**
285
+ * The migration reading's vertical axis title, at the very position and
286
+ * in the very style the classic one occupies.
287
+ *
288
+ * It rides in `endLabels` because an axis declares ONE `title`, and the
289
+ * two are alternatives selected by the variant rather than two things
290
+ * drawn together. `endLabels` is the only list of free texts an axis
291
+ * owns; the alternative would have been a second declaration of the whole
292
+ * background, which is exactly what a variant exists to avoid.
293
+ */
294
+ {
295
+ id: 'complexityTitleMigration',
296
+ variants: ['migration'],
297
+ labelKey: 'com.labre.core-domain.background.axis.migration-cost',
298
+ fallback: 'Cost of migration',
299
+ anchor: { x: 0, y: ry(400), dx: -32 },
300
+ style: AXIS_TEXT,
301
+ vertical: true,
302
+ visibleProp: 'showLabels',
303
+ },
304
+ ],
305
+ },
306
+ {
307
+ id: 'differentiation',
308
+ orientation: 'horizontal',
309
+ at: 1,
310
+ arrow: 'forward',
311
+ arrowSize: 9,
312
+ stroke: { color: '@axis', width: 2 },
313
+ title: {
314
+ id: 'differentiationTitle',
315
+ labelKey: 'com.labre.core-domain.background.axis.differentiation',
316
+ fallback: 'Business differentiation',
317
+ anchor: { x: rx(450), y: 1, dy: 30 },
318
+ style: AXIS_TEXT,
319
+ align: 'center',
320
+ visibleProp: 'showLabels',
321
+ },
322
+ endLabels: [
323
+ {
324
+ id: 'differentiationLow',
325
+ labelKey: 'com.labre.core-domain.background.differentiation.low',
326
+ fallback: 'Low',
327
+ anchor: { x: rx(84), y: 1, dy: 22 },
328
+ style: TICK_TEXT,
329
+ align: 'center',
330
+ visibleProp: 'showLabels',
331
+ },
332
+ {
333
+ id: 'differentiationHigh',
334
+ labelKey: 'com.labre.core-domain.background.differentiation.high',
335
+ fallback: 'High',
336
+ anchor: { x: rx(838), y: 1, dy: 22 },
337
+ style: TICK_TEXT,
338
+ align: 'center',
339
+ visibleProp: 'showLabels',
340
+ },
341
+ ],
342
+ },
343
+ ];
344
+ export const CORE_DOMAIN_BACKGROUND = {
345
+ type: 'coreDomain',
346
+ // The chart is a first-class role: validation rules position artefacts
347
+ // against `core-domain:chart`, never against the `coreDomain` element type.
348
+ role: CORE_DOMAIN_ROLE.chart,
349
+ variantProp: 'variant',
350
+ geometry: {
351
+ width: 900,
352
+ height: 820,
353
+ lockAspectRatio: true,
354
+ // The chart has always offered its handles (`resizeEnabled` defaults to
355
+ // true on the model); the declaration says so, and the toolbar toggle takes
356
+ // over from there.
357
+ resizable: true,
358
+ margin: { top: 24, right: 54, bottom: 50, left: 60 },
359
+ },
360
+ chrome: {
361
+ fontFamily: FONT_FAMILY,
362
+ palette: PALETTE,
363
+ // No card, on purpose: the template draws its translucent bands straight
364
+ // onto the canvas, and painting a white rectangle under them would be a
365
+ // visual change dressed up as a refactor.
366
+ surface: {},
367
+ },
368
+ zones: [...CLASSIC_ZONES, ...MIGRATION_ZONES],
369
+ axes: CORE_DOMAIN_AXES,
370
+ };
@@ -1,10 +1,13 @@
1
1
  import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { CoreDomainChartElementModel } from '@formicoidea/labre-core/model';
3
3
  /**
4
- * Canvas renderer for the Core Domain Chart background — the translucent
5
- * Generic / Supporting / Core zone bands, the two axes with arrow heads and the
6
- * Low/High ticks + zone names. Drawn in the fixed reference space and scaled
7
- * uniformly to the element bounds.
4
+ * The Core Domain Chart background renderer — the framework-background
5
+ * primitive, driven by {@link CORE_DOMAIN_BACKGROUND}.
6
+ *
7
+ * There is no chart-specific drawing code any more: the bands, the two axes with
8
+ * their arrowheads, the zone names and the Low/High ticks are all declared, and
9
+ * `__tests__/background.unit.spec.ts` pins the picture against the absolute
10
+ * constants the deleted imperative renderer drew from.
8
11
  */
9
12
  export declare const coreDomain: ElementRenderer<CoreDomainChartElementModel>;
10
13
  export declare const CoreDomainRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
@@ -1,93 +1,13 @@
1
- import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
- import { FONT_FAMILY, refScale } from '@formicoidea/labre-ddd-shared';
3
- import { AXIS, COLORS, REF_H, REF_W, ZONE_LABELS, ZONES } from './consts.js';
1
+ import { createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { CORE_DOMAIN_BACKGROUND } from './background.js';
4
3
  /**
5
- * Canvas renderer for the Core Domain Chart background — the translucent
6
- * Generic / Supporting / Core zone bands, the two axes with arrow heads and the
7
- * Low/High ticks + zone names. Drawn in the fixed reference space and scaled
8
- * uniformly to the element bounds.
4
+ * The Core Domain Chart background renderer — the framework-background
5
+ * primitive, driven by {@link CORE_DOMAIN_BACKGROUND}.
6
+ *
7
+ * There is no chart-specific drawing code any more: the bands, the two axes with
8
+ * their arrowheads, the zone names and the Low/High ticks are all declared, and
9
+ * `__tests__/background.unit.spec.ts` pins the picture against the absolute
10
+ * constants the deleted imperative renderer drew from.
9
11
  */
10
- export const coreDomain = (model, ctx, matrix) => {
11
- const [, , w, h] = model.deserializedXYWH;
12
- const cx = w / 2;
13
- const cy = h / 2;
14
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
15
- const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
16
- ctx.translate(ox, oy);
17
- ctx.scale(s, s);
18
- // ── Translucent zone bands ──────────────────────────────────────────
19
- if (model.showZones) {
20
- ctx.globalAlpha = 0.6;
21
- for (const z of ZONES) {
22
- ctx.fillStyle = z.fill;
23
- ctx.fillRect(z.x, z.y, z.w, z.h);
24
- }
25
- ctx.globalAlpha = 1;
26
- }
27
- // ── Axes with arrow heads ───────────────────────────────────────────
28
- ctx.strokeStyle = COLORS.axis;
29
- ctx.fillStyle = COLORS.axis;
30
- ctx.lineWidth = 2;
31
- ctx.lineJoin = 'round';
32
- // Y axis (up)
33
- ctx.beginPath();
34
- ctx.moveTo(AXIS.ox, AXIS.oy);
35
- ctx.lineTo(AXIS.ox, AXIS.top + 6);
36
- ctx.stroke();
37
- arrow(ctx, AXIS.ox, AXIS.top, 0);
38
- // X axis (right)
39
- ctx.beginPath();
40
- ctx.moveTo(AXIS.ox, AXIS.oy);
41
- ctx.lineTo(AXIS.right - 6, AXIS.oy);
42
- ctx.stroke();
43
- arrow(ctx, AXIS.right, AXIS.oy, 90);
44
- ctx.textBaseline = 'alphabetic';
45
- // ── Zone names ──────────────────────────────────────────────────────
46
- ctx.textAlign = 'center';
47
- for (const [text, x, y, size] of ZONE_LABELS) {
48
- ctx.fillStyle = COLORS.zoneLabel;
49
- ctx.font = `700 ${size}px ${FONT_FAMILY}`;
50
- ctx.fillText(text, x, y);
51
- }
52
- // ── Axis titles + Low/High ticks ────────────────────────────────────
53
- if (model.showLabels) {
54
- ctx.fillStyle = COLORS.title;
55
- ctx.font = `600 14px ${FONT_FAMILY}`;
56
- ctx.save();
57
- ctx.translate(28, 400);
58
- ctx.rotate(-Math.PI / 2);
59
- ctx.fillText('Complexity', 0, 0);
60
- ctx.restore();
61
- ctx.fillText('Business differentiation', 450, 800);
62
- ctx.fillStyle = COLORS.tick;
63
- ctx.font = `12px ${FONT_FAMILY}`;
64
- // X ticks
65
- ctx.fillText('Low', 84, 792);
66
- ctx.fillText('High', 838, 792);
67
- // Y ticks
68
- ctx.save();
69
- ctx.translate(48, 758);
70
- ctx.rotate(-Math.PI / 2);
71
- ctx.fillText('Low', 0, 0);
72
- ctx.restore();
73
- ctx.save();
74
- ctx.translate(38, 44);
75
- ctx.rotate(-Math.PI / 2);
76
- ctx.fillText('High', 0, 0);
77
- ctx.restore();
78
- }
79
- };
80
- /** Filled triangular arrow head at (x, y); `deg` rotates it (0 = pointing up). */
81
- function arrow(ctx, x, y, deg) {
82
- ctx.save();
83
- ctx.translate(x, y);
84
- ctx.rotate((deg * Math.PI) / 180);
85
- ctx.beginPath();
86
- ctx.moveTo(0, 0);
87
- ctx.lineTo(-5, 9);
88
- ctx.lineTo(5, 9);
89
- ctx.closePath();
90
- ctx.fill();
91
- ctx.restore();
92
- }
12
+ export const coreDomain = createFrameworkBackgroundRenderer(CORE_DOMAIN_BACKGROUND);
93
13
  export const CoreDomainRendererExtension = ElementRendererExtension('coreDomain', coreDomain);
@@ -7,5 +7,10 @@ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
7
7
  export declare class CoreDomainView extends GfxElementModelView<CoreDomainChartElementModel> {
8
8
  static type: string;
9
9
  }
10
- /** Resize gating: handles hidden unless `model.resizeEnabled` (toolbar toggle). */
10
+ /**
11
+ * Resize gating, driven by the declaration: the handles are offered while
12
+ * `model.resizeEnabled` says so (the toolbar toggle), and an element carrying no
13
+ * such prop falls back to `geometry.resizable` — the same behaviour the
14
+ * hand-written extension had, minus the hand.
15
+ */
11
16
  export declare const CoreDomainInteraction: import("@formicoidea/labre-core/store").ExtensionType;