@formicoidea/labre-framework-cynefin 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,28 @@
2
2
  * Visual constants for the Liminal Cynefin diagram, reproduced from the official
3
3
  * SVG (viewBox 0 0 1080 777). All geometry is authored in that fixed reference
4
4
  * space and scaled uniformly to the element bounds by the renderer.
5
+ *
6
+ * ## i18n
7
+ *
8
+ * Every word painted on the diagram — domain headings and subheadings, the
9
+ * Probe/Sense/Respond decision lines, the teal annotations, the small
10
+ * exaptation sub-labels and the two central markers — carries a `…Key`
11
+ * alongside its English text, resolved by `element-renderer.ts` through the
12
+ * `CanvasRenderer` it is handed at paint time (the 4th argument every
13
+ * `ElementRenderer` receives, `renderer.std`). The letters "A" and "C" on the
14
+ * two markers are notation, not words, and are deliberately NOT keyed (PO
15
+ * decision) — only their spelled-out NAMES ("Aporia", "Confusion") are.
5
16
  */
6
17
  export const REF_W = 1080;
7
18
  export const REF_H = 777;
19
+ /**
20
+ * Type size of a domain heading, in reference units.
21
+ *
22
+ * Named because two things read it: the renderer, which draws the word, and the
23
+ * hit test that offers it to a double-click. A size that lived only in the
24
+ * renderer's font string would let the target drift away from the word.
25
+ */
26
+ export const HEADING_SIZE = 30;
8
27
  export const COLORS = {
9
28
  boundary: '#333333',
10
29
  teal: '#2a9d99',
@@ -84,94 +103,322 @@ export const DASH_RECTS = [
84
103
  [998, 314, 11.9, 168.9],
85
104
  [1019, 310, 11.8, 167.3],
86
105
  ];
106
+ /* ── i18n: headings, subheadings and decision lines ───────────────────── */
107
+ const HEADING_COMPLEX = [
108
+ 'com.labre.cynefin-estuarine.cynefin.heading.complex',
109
+ 'Complex',
110
+ ];
111
+ const HEADING_COMPLICATED = [
112
+ 'com.labre.cynefin-estuarine.cynefin.heading.complicated',
113
+ 'Complicated',
114
+ ];
115
+ const HEADING_CHAOTIC = [
116
+ 'com.labre.cynefin-estuarine.cynefin.heading.chaotic',
117
+ 'Chaotic',
118
+ ];
119
+ const HEADING_CLEAR = [
120
+ 'com.labre.cynefin-estuarine.cynefin.heading.clear',
121
+ 'Clear',
122
+ ];
123
+ const SUBHEADING_ADAPTIVE = [
124
+ 'com.labre.cynefin-estuarine.cynefin.subheading.adaptive-system',
125
+ 'Adaptive system',
126
+ ];
127
+ /** Shared by Complicated and Clear — the same word, one key. */
128
+ const SUBHEADING_ORDERED = [
129
+ 'com.labre.cynefin-estuarine.cynefin.subheading.ordered-system',
130
+ 'Ordered system',
131
+ ];
132
+ const SUBHEADING_UNORDERED = [
133
+ 'com.labre.cynefin-estuarine.cynefin.subheading.un-ordered-system',
134
+ 'Un-ordered system',
135
+ ];
136
+ /**
137
+ * The ten distinct decision sentences (two are shared verbatim between two
138
+ * domains — "Sense how the context reacts", "Sense the context with
139
+ * analytical methods" — one key each, reused rather than duplicated).
140
+ *
141
+ * The fallback is the FULL sentence (`lead` + `rest` concatenated): the
142
+ * renderer draws `lead` bold and `rest` roman as two `fillText` calls only
143
+ * while NO host answered (byte-identical to before these keys existed); once
144
+ * a host resolves the key, the whole sentence is drawn as one run — a
145
+ * translation is not guaranteed to keep the same first word, so splitting it
146
+ * at the English boundary would be a guess.
147
+ */
148
+ const LINE_PROBE = [
149
+ 'com.labre.cynefin-estuarine.cynefin.line.probe',
150
+ 'Probe the context with parallel experiments',
151
+ ];
152
+ const LINE_SENSE_REACTS = [
153
+ 'com.labre.cynefin-estuarine.cynefin.line.sense-reacts',
154
+ 'Sense how the context reacts',
155
+ ];
156
+ const LINE_RESPOND_AMPLIFY = [
157
+ 'com.labre.cynefin-estuarine.cynefin.line.respond-amplify',
158
+ 'Respond by amplifying positive experiments',
159
+ ];
160
+ const LINE_SENSE_ANALYTICAL = [
161
+ 'com.labre.cynefin-estuarine.cynefin.line.sense-analytical',
162
+ 'Sense the context with analytical methods',
163
+ ];
164
+ const LINE_ANALYSE = [
165
+ 'com.labre.cynefin-estuarine.cynefin.line.analyse',
166
+ 'Analyse observations',
167
+ ];
168
+ const LINE_RESPOND_MANY_SOLUTIONS = [
169
+ 'com.labre.cynefin-estuarine.cynefin.line.respond-many-solutions',
170
+ 'Respond by applying one of many good solutions',
171
+ ];
172
+ const LINE_ACT = [
173
+ 'com.labre.cynefin-estuarine.cynefin.line.act',
174
+ 'Act on the context to stabilize (it or yourself)',
175
+ ];
176
+ const LINE_RESPOND_REACT = [
177
+ 'com.labre.cynefin-estuarine.cynefin.line.respond-react',
178
+ 'Respond by re-acting',
179
+ ];
180
+ const LINE_CATEGORIZE = [
181
+ 'com.labre.cynefin-estuarine.cynefin.line.categorize',
182
+ 'Categorize observations',
183
+ ];
184
+ const LINE_RESPOND_PRACTICES = [
185
+ 'com.labre.cynefin-estuarine.cynefin.line.respond-practices',
186
+ 'Respond by applying tried and true practices',
187
+ ];
87
188
  export const DOMAINS = [
88
189
  {
89
- heading: 'Complex',
190
+ heading: HEADING_COMPLEX,
191
+ prop: 'complexTitle',
90
192
  x: 37,
91
193
  hy: 31,
92
- subheading: 'Adaptive system',
194
+ subheading: SUBHEADING_ADAPTIVE,
93
195
  sy: 53,
94
196
  lines: [
95
- { lead: 'Probe', rest: ' the context with parallel experiments', y: 71 },
96
- { lead: 'Sense', rest: ' how the context reacts', y: 90 },
97
- { lead: 'Respond', rest: ' by amplifying positive experiments', y: 109 },
197
+ {
198
+ lead: 'Probe',
199
+ rest: ' the context with parallel experiments',
200
+ wording: LINE_PROBE,
201
+ y: 71,
202
+ },
203
+ {
204
+ lead: 'Sense',
205
+ rest: ' how the context reacts',
206
+ wording: LINE_SENSE_REACTS,
207
+ y: 90,
208
+ },
209
+ {
210
+ lead: 'Respond',
211
+ rest: ' by amplifying positive experiments',
212
+ wording: LINE_RESPOND_AMPLIFY,
213
+ y: 109,
214
+ },
98
215
  ],
99
216
  },
100
217
  {
101
- heading: 'Complicated',
218
+ heading: HEADING_COMPLICATED,
219
+ prop: 'complicatedTitle',
102
220
  x: 779,
103
221
  hy: 31,
104
- subheading: 'Ordered system',
222
+ subheading: SUBHEADING_ORDERED,
105
223
  sy: 53,
106
224
  lines: [
107
- { lead: 'Sense', rest: ' the context with analytical methods', y: 71 },
108
- { lead: 'Analyse', rest: ' observations', y: 90 },
225
+ {
226
+ lead: 'Sense',
227
+ rest: ' the context with analytical methods',
228
+ wording: LINE_SENSE_ANALYTICAL,
229
+ y: 71,
230
+ },
231
+ { lead: 'Analyse', rest: ' observations', wording: LINE_ANALYSE, y: 90 },
109
232
  {
110
233
  lead: 'Respond',
111
234
  rest: ' by applying one of many good solutions',
235
+ wording: LINE_RESPOND_MANY_SOLUTIONS,
112
236
  y: 109,
113
237
  },
114
238
  ],
115
239
  },
116
240
  {
117
- heading: 'Chaotic',
241
+ heading: HEADING_CHAOTIC,
242
+ prop: 'chaoticTitle',
118
243
  x: 37,
119
244
  hy: 587,
120
- subheading: 'Un-ordered system',
245
+ subheading: SUBHEADING_UNORDERED,
121
246
  sy: 609,
122
247
  lines: [
123
248
  {
124
249
  lead: 'Act',
125
250
  rest: ' on the context to stabilize (it or yourself)',
251
+ wording: LINE_ACT,
126
252
  y: 627,
127
253
  },
128
- { lead: 'Sense', rest: ' how the context reacts', y: 646 },
129
- { lead: 'Respond', rest: ' by re-acting', y: 665 },
254
+ {
255
+ lead: 'Sense',
256
+ rest: ' how the context reacts',
257
+ wording: LINE_SENSE_REACTS,
258
+ y: 646,
259
+ },
260
+ {
261
+ lead: 'Respond',
262
+ rest: ' by re-acting',
263
+ wording: LINE_RESPOND_REACT,
264
+ y: 665,
265
+ },
130
266
  ],
131
267
  },
132
268
  {
133
- heading: 'Clear',
269
+ heading: HEADING_CLEAR,
270
+ prop: 'clearTitle',
134
271
  x: 779,
135
272
  hy: 587,
136
- subheading: 'Ordered system',
273
+ subheading: SUBHEADING_ORDERED,
137
274
  sy: 609,
138
275
  lines: [
139
- { lead: 'Sense', rest: ' the context with analytical methods', y: 627 },
140
- { lead: 'Categorize', rest: ' observations', y: 646 },
276
+ {
277
+ lead: 'Sense',
278
+ rest: ' the context with analytical methods',
279
+ wording: LINE_SENSE_ANALYTICAL,
280
+ y: 627,
281
+ },
282
+ {
283
+ lead: 'Categorize',
284
+ rest: ' observations',
285
+ wording: LINE_CATEGORIZE,
286
+ y: 646,
287
+ },
141
288
  {
142
289
  lead: 'Respond',
143
290
  rest: ' by applying tried and true practices',
291
+ wording: LINE_RESPOND_PRACTICES,
144
292
  y: 665,
145
293
  },
146
294
  ],
147
295
  },
148
296
  ];
149
- /** Teal annotation labels (centered): [text, x, y]. */
297
+ /* ── i18n: teal annotations and small exaptation sub-labels ────────────── */
298
+ const TEAL_ITERATE = [
299
+ 'com.labre.cynefin-estuarine.cynefin.teal.iterate',
300
+ 'iterate',
301
+ ];
302
+ const TEAL_STRATEGY_BY_DESIGN = [
303
+ 'com.labre.cynefin-estuarine.cynefin.teal.strategy-by-design',
304
+ 'strategy by design',
305
+ ];
306
+ const TEAL_RADICAL_INNOVATION = [
307
+ 'com.labre.cynefin-estuarine.cynefin.teal.radical-innovation',
308
+ 'radical innovation',
309
+ ];
310
+ const TEAL_BY_DESIGN = [
311
+ 'com.labre.cynefin-estuarine.cynefin.teal.by-design',
312
+ 'by design',
313
+ ];
314
+ const TEAL_EXTREME_REPURPOSING = [
315
+ 'com.labre.cynefin-estuarine.cynefin.teal.extreme-repurposing',
316
+ 'extreme repurposing',
317
+ ];
318
+ const TEAL_GOOD_PRACTICE = [
319
+ 'com.labre.cynefin-estuarine.cynefin.teal.good-practice',
320
+ 'good practice',
321
+ ];
322
+ const TEAL_BEST_PRACTICE = [
323
+ 'com.labre.cynefin-estuarine.cynefin.teal.best-practice',
324
+ 'best practice',
325
+ ];
326
+ /** Teal annotation labels (centered): [wording, x, y]. */
150
327
  export const TEAL_LABELS = [
151
- ['iterate', 510, 16],
152
- ['iterate', 533, 230],
153
- ['strategy by design', 257, 225],
154
- ['radical innovation', 268, 390],
155
- ['by design', 268, 406],
156
- ['extreme repurposing', 217, 496],
157
- ['good practice', 814, 196],
158
- ['best practice', 751, 459],
159
- ];
160
- /** Small exaptation sub-labels (centered): [text, x, y]. */
328
+ [TEAL_ITERATE, 510, 16],
329
+ [TEAL_ITERATE, 533, 230],
330
+ [TEAL_STRATEGY_BY_DESIGN, 257, 225],
331
+ [TEAL_RADICAL_INNOVATION, 268, 390],
332
+ [TEAL_BY_DESIGN, 268, 406],
333
+ [TEAL_EXTREME_REPURPOSING, 217, 496],
334
+ [TEAL_GOOD_PRACTICE, 814, 196],
335
+ [TEAL_BEST_PRACTICE, 751, 459],
336
+ ];
337
+ const SMALL_DISPOSITIONAL = [
338
+ 'com.labre.cynefin-estuarine.cynefin.small.dispositional-exaptation',
339
+ 'dispositional exaptation',
340
+ ];
341
+ const SMALL_STIMULATED = [
342
+ 'com.labre.cynefin-estuarine.cynefin.small.stimulated-exaptation',
343
+ 'stimulated exaptation',
344
+ ];
345
+ const SMALL_STRESS_BASED = [
346
+ 'com.labre.cynefin-estuarine.cynefin.small.stress-based-exaptation',
347
+ 'stress-based exaptation',
348
+ ];
349
+ /** Small exaptation sub-labels (centered): [wording, x, y]. */
161
350
  export const SMALL_LABELS = [
162
- ['dispositional exaptation', 257, 239],
163
- ['stimulated exaptation', 268, 419],
164
- ['stress-based exaptation', 217, 510],
351
+ [SMALL_DISPOSITIONAL, 257, 239],
352
+ [SMALL_STIMULATED, 268, 419],
353
+ [SMALL_STRESS_BASED, 217, 510],
354
+ ];
355
+ /* ── i18n: the two central markers ──────────────────────────────────── */
356
+ const MARKER_APORIA_NAME = [
357
+ 'com.labre.cynefin-estuarine.cynefin.marker.aporia',
358
+ 'Aporia',
359
+ ];
360
+ const MARKER_CONFUSION_NAME = [
361
+ 'com.labre.cynefin-estuarine.cynefin.marker.confusion',
362
+ 'Confusion',
363
+ ];
364
+ const MARKER_APORIA_NOTE = [
365
+ 'com.labre.cynefin-estuarine.cynefin.marker.aporia-note',
366
+ 'prepare to exit',
165
367
  ];
166
368
  export const MARKERS = [
167
369
  {
168
370
  letter: 'A',
169
371
  lx: 444,
170
372
  ly: 334,
171
- name: 'Aporia',
373
+ name: MARKER_APORIA_NAME,
172
374
  nx: 447,
173
375
  ny: 349,
174
- note: { text: 'prepare to exit', x: 449, y: 366 },
376
+ note: { wording: MARKER_APORIA_NOTE, x: 449, y: 366 },
377
+ },
378
+ {
379
+ letter: 'C',
380
+ lx: 531,
381
+ ly: 419,
382
+ name: MARKER_CONFUSION_NAME,
383
+ nx: 529,
384
+ ny: 436,
175
385
  },
176
- { letter: 'C', lx: 531, ly: 419, name: 'Confusion', nx: 529, ny: 436 },
386
+ ];
387
+ /**
388
+ * Every wording this file paints, for `translations.ts`'s manifest
389
+ * contribution. Deduplicated by key (`iterate` and the two shared lines are
390
+ * each listed once), which is what `mergeTranslationEntries` needs to see one
391
+ * entry per key rather than two identical ones.
392
+ */
393
+ export const CYNEFIN_CANVAS_WORDINGS = [
394
+ HEADING_COMPLEX,
395
+ HEADING_COMPLICATED,
396
+ HEADING_CHAOTIC,
397
+ HEADING_CLEAR,
398
+ SUBHEADING_ADAPTIVE,
399
+ SUBHEADING_ORDERED,
400
+ SUBHEADING_UNORDERED,
401
+ LINE_PROBE,
402
+ LINE_SENSE_REACTS,
403
+ LINE_RESPOND_AMPLIFY,
404
+ LINE_SENSE_ANALYTICAL,
405
+ LINE_ANALYSE,
406
+ LINE_RESPOND_MANY_SOLUTIONS,
407
+ LINE_ACT,
408
+ LINE_RESPOND_REACT,
409
+ LINE_CATEGORIZE,
410
+ LINE_RESPOND_PRACTICES,
411
+ TEAL_ITERATE,
412
+ TEAL_STRATEGY_BY_DESIGN,
413
+ TEAL_RADICAL_INNOVATION,
414
+ TEAL_BY_DESIGN,
415
+ TEAL_EXTREME_REPURPOSING,
416
+ TEAL_GOOD_PRACTICE,
417
+ TEAL_BEST_PRACTICE,
418
+ SMALL_DISPOSITIONAL,
419
+ SMALL_STIMULATED,
420
+ SMALL_STRESS_BASED,
421
+ MARKER_APORIA_NAME,
422
+ MARKER_CONFUSION_NAME,
423
+ MARKER_APORIA_NOTE,
177
424
  ];
@@ -7,6 +7,22 @@ import type { CynefinElementModel } from '@formicoidea/labre-core/model';
7
7
  * (heading + Probe/Sense/Respond decisions), the teal annotation labels and the
8
8
  * central Aporia (A) / Confusion (C) markers. Drawn in the fixed reference space
9
9
  * and scaled uniformly to the element bounds.
10
+ *
11
+ * ## i18n
12
+ *
13
+ * Every word above is a `ChromeWording` (`./consts.ts`), resolved through the
14
+ * `CanvasRenderer` this function is handed as its 4th argument — the same
15
+ * `renderer.std` `framework-background/renderer.ts` reads for a declared
16
+ * background. `renderer` is optional (absent in a bare unit test that calls
17
+ * this function with three arguments), in which case every wording reads its
18
+ * English fallback, byte-identical to before these keys existed.
19
+ *
20
+ * The three-line decisions are the one wrinkle: with no host they are drawn as
21
+ * they always were — a bold lead word plus the roman remainder, two
22
+ * `fillText` calls — because that split is known to be exactly the English
23
+ * text. Once a host answers, the WHOLE sentence is drawn as one run: a
24
+ * translation is not guaranteed to keep the same first word, so guessing where
25
+ * to split it would be inventing typography the translator never asked for.
10
26
  */
11
27
  export declare const cynefin: ElementRenderer<CynefinElementModel>;
12
28
  export declare const CynefinRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
@@ -1,6 +1,8 @@
1
- import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { ElementRendererExtension, taggedPath2D, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { TranslationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
2
3
  import { FONT_FAMILY, refScale } from '../utils.js';
3
- import { COLORS, DARK_BACK_PATHS, DARK_FRONT_PATHS, DASH_RECTS, DOMAINS, HATCHES, MARKERS, REF_H, REF_W, SMALL_LABELS, TEAL_LABELS, TEAL_PATH, TEAL_WIDTH, } from './consts.js';
4
+ import { COLORS, DARK_BACK_PATHS, DARK_FRONT_PATHS, DASH_RECTS, DOMAINS, HATCHES, HEADING_SIZE, MARKERS, REF_H, REF_W, SMALL_LABELS, TEAL_LABELS, TEAL_PATH, TEAL_WIDTH, } from './consts.js';
5
+ import { cynefinHeadingText } from './labels.js';
4
6
  /**
5
7
  * Canvas renderer for the Liminal Cynefin diagram — reproduces the official SVG:
6
8
  * the dark hand-drawn boundary, the teal "iterate" curve, the dashed
@@ -8,8 +10,29 @@ import { COLORS, DARK_BACK_PATHS, DARK_FRONT_PATHS, DASH_RECTS, DOMAINS, HATCHES
8
10
  * (heading + Probe/Sense/Respond decisions), the teal annotation labels and the
9
11
  * central Aporia (A) / Confusion (C) markers. Drawn in the fixed reference space
10
12
  * and scaled uniformly to the element bounds.
13
+ *
14
+ * ## i18n
15
+ *
16
+ * Every word above is a `ChromeWording` (`./consts.ts`), resolved through the
17
+ * `CanvasRenderer` this function is handed as its 4th argument — the same
18
+ * `renderer.std` `framework-background/renderer.ts` reads for a declared
19
+ * background. `renderer` is optional (absent in a bare unit test that calls
20
+ * this function with three arguments), in which case every wording reads its
21
+ * English fallback, byte-identical to before these keys existed.
22
+ *
23
+ * The three-line decisions are the one wrinkle: with no host they are drawn as
24
+ * they always were — a bold lead word plus the roman remainder, two
25
+ * `fillText` calls — because that split is known to be exactly the English
26
+ * text. Once a host answers, the WHOLE sentence is drawn as one run: a
27
+ * translation is not guaranteed to keep the same first word, so guessing where
28
+ * to split it would be inventing typography the translator never asked for.
11
29
  */
12
- export const cynefin = (model, ctx, matrix) => {
30
+ export const cynefin = (model, ctx, matrix, renderer) => {
31
+ const tr = (wording) => renderer ? translateKey(renderer.std, ...wording) : wording[1];
32
+ // The host's catalogue, or `null` with no host — used to decide whether a
33
+ // decision line is drawn bold-lead/roman-rest (English, exactly as before)
34
+ // or as one translated run. See the docstring above.
35
+ const hasHost = Boolean(renderer?.std.getOptional(TranslationProvider));
13
36
  const [, , w, h] = model.deserializedXYWH;
14
37
  const cx = w / 2;
15
38
  const cy = h / 2;
@@ -26,7 +49,7 @@ export const cynefin = (model, ctx, matrix) => {
26
49
  for (const [d, lw, miter] of DARK_BACK_PATHS) {
27
50
  ctx.lineJoin = miter ? 'miter' : 'round';
28
51
  ctx.lineWidth = lw;
29
- ctx.stroke(new Path2D(d));
52
+ ctx.stroke(taggedPath2D(d));
30
53
  }
31
54
  ctx.lineJoin = 'round';
32
55
  // ── Dashed boundary (oriented square pavings) ───────────────────────
@@ -42,14 +65,14 @@ export const cynefin = (model, ctx, matrix) => {
42
65
  if (model.showLiminalLine) {
43
66
  ctx.strokeStyle = COLORS.teal;
44
67
  ctx.lineWidth = TEAL_WIDTH;
45
- ctx.stroke(new Path2D(TEAL_PATH));
68
+ ctx.stroke(taggedPath2D(TEAL_PATH));
46
69
  }
47
70
  // ── Dark boundary strokes (over the teal curve) ─────────────────────
48
71
  ctx.strokeStyle = COLORS.boundary;
49
72
  for (const [d, lw, miter] of DARK_FRONT_PATHS) {
50
73
  ctx.lineJoin = miter ? 'miter' : 'round';
51
74
  ctx.lineWidth = lw;
52
- ctx.stroke(new Path2D(d));
75
+ ctx.stroke(taggedPath2D(d));
53
76
  }
54
77
  ctx.lineJoin = 'round';
55
78
  // ── Cliff hatching ──────────────────────────────────────────────────
@@ -65,52 +88,66 @@ export const cynefin = (model, ctx, matrix) => {
65
88
  if (model.showTitles) {
66
89
  ctx.textAlign = 'left';
67
90
  ctx.fillStyle = COLORS.heading;
68
- ctx.font = `700 30px ${FONT_FAMILY}`;
69
- for (const d of DOMAINS)
70
- ctx.fillText(d.heading, d.x, d.hy);
91
+ ctx.font = `700 ${HEADING_SIZE}px ${FONT_FAMILY}`;
92
+ // The user's own word when they have renamed this domain, the catalogue's
93
+ // otherwise — the same precedence a declared background applies.
94
+ for (const d of DOMAINS) {
95
+ ctx.fillText(cynefinHeadingText(model, d, tr), d.x, d.hy);
96
+ }
71
97
  ctx.textAlign = 'center';
72
98
  for (const m of MARKERS) {
73
99
  ctx.fillStyle = COLORS.body;
74
100
  ctx.font = `700 38px ${FONT_FAMILY}`;
101
+ // The letter itself is notation, not a word — never translated.
75
102
  ctx.fillText(m.letter, m.lx, m.ly);
76
103
  ctx.font = `13.5px ${FONT_FAMILY}`;
77
- ctx.fillText(m.name, m.nx, m.ny);
104
+ ctx.fillText(tr(m.name), m.nx, m.ny);
78
105
  }
79
106
  }
80
107
  // ── Explanatory text: subheadings, decisions, annotations, notes ────
81
108
  if (model.showDescriptions) {
82
- // Subheadings (h2) + bold-lead decision lines
109
+ // Subheadings (h2) + decision lines
83
110
  ctx.textAlign = 'left';
84
111
  for (const d of DOMAINS) {
85
112
  ctx.fillStyle = COLORS.heading;
86
113
  ctx.font = `700 15px ${FONT_FAMILY}`;
87
- ctx.fillText(d.subheading, d.x, d.sy);
114
+ ctx.fillText(tr(d.subheading), d.x, d.sy);
88
115
  ctx.fillStyle = COLORS.body;
89
- for (const { lead, rest, y } of d.lines) {
90
- ctx.font = `700 13.5px ${FONT_FAMILY}`;
91
- ctx.fillText(lead, d.x, y);
92
- const leadW = ctx.measureText(lead).width;
93
- ctx.font = `13.5px ${FONT_FAMILY}`;
94
- ctx.fillText(rest, d.x + leadW, y);
116
+ for (const { lead, rest, wording, y } of d.lines) {
117
+ if (hasHost) {
118
+ // A translated sentence: one run, no assumed bold-lead boundary.
119
+ ctx.font = `13.5px ${FONT_FAMILY}`;
120
+ ctx.fillText(tr(wording), d.x, y);
121
+ }
122
+ else {
123
+ // No host: exactly the bold-lead / roman-rest split that shipped
124
+ // before these keys existed.
125
+ ctx.font = `700 13.5px ${FONT_FAMILY}`;
126
+ ctx.fillText(lead, d.x, y);
127
+ const leadW = ctx.measureText(lead).width;
128
+ ctx.font = `13.5px ${FONT_FAMILY}`;
129
+ ctx.fillText(rest, d.x + leadW, y);
130
+ }
95
131
  }
96
132
  }
97
133
  ctx.textAlign = 'center';
98
134
  // Teal annotation labels
99
135
  ctx.fillStyle = COLORS.teal;
100
136
  ctx.font = `700 15px ${FONT_FAMILY}`;
101
- for (const [t, x, y] of TEAL_LABELS)
102
- ctx.fillText(t, x, y);
137
+ for (const [wording, x, y] of TEAL_LABELS)
138
+ ctx.fillText(tr(wording), x, y);
103
139
  // Small exaptation sub-labels
104
140
  ctx.fillStyle = COLORS.body;
105
141
  ctx.font = `10.5px ${FONT_FAMILY}`;
106
- for (const [t, x, y] of SMALL_LABELS)
107
- ctx.fillText(t, x, y);
142
+ for (const [wording, x, y] of SMALL_LABELS) {
143
+ ctx.fillText(tr(wording), x, y);
144
+ }
108
145
  // Marker notes ("prepare to exit")
109
146
  ctx.fillStyle = COLORS.teal;
110
147
  ctx.font = `700 15px ${FONT_FAMILY}`;
111
148
  for (const m of MARKERS) {
112
149
  if (m.note)
113
- ctx.fillText(m.note.text, m.note.x, m.note.y);
150
+ ctx.fillText(tr(m.note.wording), m.note.x, m.note.y);
114
151
  }
115
152
  }
116
153
  };
@@ -1,11 +1,20 @@
1
+ import type { EditableBackgroundLabel } from '@formicoidea/labre-core/blocks/surface';
2
+ import { FrameworkBackgroundView } from '@formicoidea/labre-core/blocks/surface';
1
3
  import type { CynefinElementModel } from '@formicoidea/labre-core/model';
2
- import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
4
  /**
4
5
  * View for the Cynefin background. Registering it ensures `gfx.view.get(model)`
5
6
  * returns a view (required so move / select interactions work).
7
+ *
8
+ * Extends {@link FrameworkBackgroundView} rather than
9
+ * `DeclaredBackgroundView`: this diagram is a figurative reproduction of the
10
+ * official SVG and has no `FrameworkBackgroundDef` behind it, so it says where
11
+ * its labels are in its own reference space. The gesture itself — where a
12
+ * double-click is allowed to land, the in-place `<input>`, the commit that
13
+ * refuses to write an untouched value — is the shared one (issue #355).
6
14
  */
7
- export declare class CynefinView extends GfxElementModelView<CynefinElementModel> {
15
+ export declare class CynefinView extends FrameworkBackgroundView<CynefinElementModel> {
8
16
  static type: string;
17
+ protected labelAt(lx: number, ly: number, w: number, h: number): EditableBackgroundLabel | null;
9
18
  }
10
19
  /**
11
20
  * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
@@ -1,10 +1,25 @@
1
- import { GfxElementModelView, GfxViewInteractionExtension, } from '@formicoidea/labre-core/std/gfx';
1
+ import { FrameworkBackgroundView, hitTestBackgroundLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { translateKey, } from '@formicoidea/labre-core/shared/services';
3
+ import { GfxViewInteractionExtension } from '@formicoidea/labre-core/std/gfx';
4
+ import { cynefinLabelHits } from './labels.js';
2
5
  /**
3
6
  * View for the Cynefin background. Registering it ensures `gfx.view.get(model)`
4
7
  * returns a view (required so move / select interactions work).
8
+ *
9
+ * Extends {@link FrameworkBackgroundView} rather than
10
+ * `DeclaredBackgroundView`: this diagram is a figurative reproduction of the
11
+ * official SVG and has no `FrameworkBackgroundDef` behind it, so it says where
12
+ * its labels are in its own reference space. The gesture itself — where a
13
+ * double-click is allowed to land, the in-place `<input>`, the commit that
14
+ * refuses to write an untouched value — is the shared one (issue #355).
5
15
  */
6
- export class CynefinView extends GfxElementModelView {
16
+ export class CynefinView extends FrameworkBackgroundView {
7
17
  static { this.type = 'cynefin'; }
18
+ labelAt(lx, ly, w, h) {
19
+ const translate = (wording) => translateKey(this.gfx.std, ...wording);
20
+ const hit = hitTestBackgroundLabel(cynefinLabelHits(this.model, w, h, translate), lx, ly);
21
+ return hit ? { prop: hit.prop, text: hit.text } : null;
22
+ }
8
23
  }
9
24
  /**
10
25
  * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
@@ -0,0 +1,25 @@
1
+ import type { BackgroundLabelHit } from '@formicoidea/labre-core/blocks/surface';
2
+ import type { CynefinElementModel } from '@formicoidea/labre-core/model';
3
+ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
4
+ import type { DomainBlock } from './consts.js';
5
+ /** Resolves a wording against the host's catalogue, or falls back to English. */
6
+ export type CynefinWordingResolver = (wording: ChromeWording) => string;
7
+ /**
8
+ * What a domain heading SAYS: the user's own word, else the vocabulary.
9
+ *
10
+ * The very precedence `backgroundLabelText` applies to a declared background —
11
+ * user text wins over the catalogue, the catalogue over the shipped English —
12
+ * spelled out here because this diagram has no declaration to apply it for it.
13
+ * An empty string is a real answer (the user erased the word), which is why the
14
+ * test is against `undefined` and not against falsiness.
15
+ */
16
+ export declare function cynefinHeadingText(model: CynefinElementModel, block: DomainBlock, translate: CynefinWordingResolver): string;
17
+ /**
18
+ * Clickable boxes of the four domain headings, in ELEMENT-LOCAL coordinates.
19
+ *
20
+ * Built from the same reference positions and the same uniform letterbox fit
21
+ * the renderer draws them with, so the target tracks the word at any size, and
22
+ * empty while the headings are hidden — a label that is not painted may not be
23
+ * aimed at either.
24
+ */
25
+ export declare function cynefinLabelHits(model: CynefinElementModel, w: number, h: number, translate: CynefinWordingResolver): BackgroundLabelHit[];