@formicoidea/labre-framework-wardley 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.
package/dist/morph.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
3
+ import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
4
  import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
5
  /**
5
6
  * What a Wardley artefact may BECOME — the declaration behind the "Change type"
@@ -81,8 +82,17 @@ export declare function wardleyNodeOfComponent(model: GfxPrimitiveElementModel):
81
82
  *
82
83
  * Pure and total over every string: the input is a canvas text element somebody
83
84
  * may have typed anything into.
85
+ *
86
+ * `std` is OPTIONAL: "untouched" means equal to the English prompt OR to the
87
+ * host's own resolved wording for `from`'s kind, and the target's prompt is
88
+ * likewise resolved through the host when one is given — mirroring
89
+ * `c4MorphedTypeLine`'s fix for the identical class of gap. With no `std`
90
+ * this behaves exactly as it always has (English only). The morph toolbar's
91
+ * `afterMorph` callback (`MorphSpec`, `packages/affine/blocks/surface`) now
92
+ * hands its callee the inserting editor's `std`, so `rewriteLabel` below
93
+ * passes it straight through.
84
94
  */
85
- export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyMorphKind, rawText: string | null | undefined): string | null;
95
+ export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyMorphKind, rawText: string | null | undefined, std?: BlockStdScope): string | null;
86
96
  /**
87
97
  * Everything the morph owes a Wardley composite beyond the props patch — the
88
98
  * `afterMorph` half of the spec, run inside the generic module's one
@@ -98,7 +108,9 @@ export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyM
98
108
  * dots crammed into it. So the target's canonical size is applied, CENTRED on
99
109
  * where the artefact already stands: the thing does not move, it becomes the
100
110
  * size its notation says it is. The label keeps its own place, which is the
101
- * user's.
111
+ * user's — with the one exception {@link relocateLabel} states: a name still
112
+ * standing where creation put it follows the target kind when that kind carries
113
+ * its name somewhere else, or the new drawing lands on top of the old words.
102
114
  *
103
115
  * ## And why the rest is here rather than in the patch
104
116
  *
@@ -110,7 +122,7 @@ export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyM
110
122
  * LEAVING comes apart before the one it is arriving at is built, so a
111
123
  * market → pipeline never has three dots and a handle at once.
112
124
  */
113
- export declare function wardleyMorphComposite(selected: GfxPrimitiveElementModel, from: WardleyMorphKind, to: WardleyMorphKind): void;
125
+ export declare function wardleyMorphComposite(selected: GfxPrimitiveElementModel, from: WardleyMorphKind, to: WardleyMorphKind, std?: BlockStdScope): void;
114
126
  /**
115
127
  * Wardley's morph declaration, handed to the generic `morphToolbarConfig`.
116
128
  *
package/dist/morph.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorElementModel, GroupElementModel, TextElementModel, WardleyNodeElementModel, } from '@formicoidea/labre-core/model';
3
+ import { translateKey } from '@formicoidea/labre-core/shared/services';
4
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
3
5
  import { wardleyCommandIcons, wardleyCommands } from './commands.js';
4
- import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, wardleyMorphClears, wardleyMorphProps, } from './presets.js';
6
+ import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, WARDLEY_LABEL_H, WARDLEY_LABEL_W, wardleyLabelBoxFor, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, wardleyMorphClears, wardleyMorphProps, wardleyNodeLabelKey, } from './presets.js';
5
7
  import { WARDLEY_ROLE } from './roles.js';
6
8
  /**
7
9
  * What a Wardley artefact may BECOME — the declaration behind the "Change type"
@@ -148,11 +150,26 @@ export function wardleyNodeOfComponent(model) {
148
150
  *
149
151
  * Pure and total over every string: the input is a canvas text element somebody
150
152
  * may have typed anything into.
153
+ *
154
+ * `std` is OPTIONAL: "untouched" means equal to the English prompt OR to the
155
+ * host's own resolved wording for `from`'s kind, and the target's prompt is
156
+ * likewise resolved through the host when one is given — mirroring
157
+ * `c4MorphedTypeLine`'s fix for the identical class of gap. With no `std`
158
+ * this behaves exactly as it always has (English only). The morph toolbar's
159
+ * `afterMorph` callback (`MorphSpec`, `packages/affine/blocks/surface`) now
160
+ * hands its callee the inserting editor's `std`, so `rewriteLabel` below
161
+ * passes it straight through.
151
162
  */
152
- export function wardleyMorphedLabel(from, to, rawText) {
153
- return (rawText ?? '').trim() === WARDLEY_NODE_LABEL[from]
154
- ? WARDLEY_NODE_LABEL[to]
155
- : null;
163
+ export function wardleyMorphedLabel(from, to, rawText, std) {
164
+ const text = (rawText ?? '').trim();
165
+ const fromPrompt = std
166
+ ? translateKey(std, wardleyNodeLabelKey(from), WARDLEY_NODE_LABEL[from])
167
+ : WARDLEY_NODE_LABEL[from];
168
+ if (text !== WARDLEY_NODE_LABEL[from] && text !== fromPrompt)
169
+ return null;
170
+ return std
171
+ ? translateKey(std, wardleyNodeLabelKey(to), WARDLEY_NODE_LABEL[to])
172
+ : WARDLEY_NODE_LABEL[to];
156
173
  }
157
174
  /** The centre of an element, from the box it currently occupies. */
158
175
  function centreOf(model) {
@@ -309,12 +326,12 @@ function removeHandle(group, carrier) {
309
326
  * inside the caller's `captureSync`, so the rewrite is part of the same single
310
327
  * ctrl+z as the kind that made it necessary.
311
328
  */
312
- function rewriteLabel(group, from, to) {
329
+ function rewriteLabel(group, from, to, std) {
313
330
  const label = labelOfComposite(group);
314
331
  if (!label || label.isLocked())
315
332
  return;
316
333
  const text = label.text.toString().trim();
317
- const next = wardleyMorphedLabel(from, to, text);
334
+ const next = wardleyMorphedLabel(from, to, text, std);
318
335
  if (next === null || next === text)
319
336
  return;
320
337
  label.surface.store.transact(() => {
@@ -322,6 +339,66 @@ function rewriteLabel(group, from, to) {
322
339
  label.text.insert(0, next);
323
340
  });
324
341
  }
342
+ /**
343
+ * The offset from a label box's left edge to the point its alignment holds
344
+ * fixed — the edge the words grow from.
345
+ */
346
+ function anchorOffset(w, textAlign) {
347
+ return textAlign === 'center' ? w / 2 : textAlign === 'right' ? w : 0;
348
+ }
349
+ /**
350
+ * Carry the name to where the TARGET kind holds it — and only when it is still
351
+ * where the SOURCE kind's creation put it.
352
+ *
353
+ * ## The bug this repairs
354
+ *
355
+ * A component wears its name to the right of an 18-pixel circle. A pipeline is
356
+ * a 120-wide bar: written onto the same artefact, it is drawn straight OVER
357
+ * those words, and a filled shape answers the pointer on its whole interior
358
+ * while a text answers only on its box — so which of the two got the click
359
+ * depended on where in the label you aimed. The name could not be edited, and
360
+ * dragging it moved the pipeline. Rule W3 forbids precisely that overlap.
361
+ *
362
+ * ## Why only across a change of PLACE
363
+ *
364
+ * The timid rule {@link rewriteLabel} states about the words, applied to the
365
+ * placement: what the author put somewhere is theirs. The three circles all
366
+ * carry their name beside them, so a component growing into a market leaves the
367
+ * words exactly where they were — the circle grows underneath them and that is
368
+ * the documented behaviour. Only the crossing to or from the pipeline changes
369
+ * the PLACE, and only that moves anything.
370
+ *
371
+ * The test is on the point the label's own alignment keeps fixed rather than on
372
+ * its top-left, because typing into a label resizes its box around exactly that
373
+ * point (`edgeless-text-editor`): a renamed label is still where creation put
374
+ * it, and a MOVED one is not.
375
+ */
376
+ function relocateLabel(group, from, to, cx, cy) {
377
+ const source = wardleyLabelBoxFor(from, cx, cy);
378
+ const target = wardleyLabelBoxFor(to, cx, cy);
379
+ if (source.place === target.place)
380
+ return;
381
+ const label = labelOfComposite(group);
382
+ if (!label || label.isLocked())
383
+ return;
384
+ const [x, y, w, h] = label.deserializedXYWH;
385
+ const sourceAnchorX = source.x + anchorOffset(WARDLEY_LABEL_W, source.textAlign);
386
+ // A pixel of tolerance on each axis: a renamed label's line is measured from
387
+ // the font and lands within a hair of the canonical height this compares to.
388
+ if (Math.abs(x + anchorOffset(w, source.textAlign) - sourceAnchorX) > 1) {
389
+ return;
390
+ }
391
+ if (Math.abs(y + h / 2 - (source.y + WARDLEY_LABEL_H / 2)) > 1)
392
+ return;
393
+ label.surface.updateElement(label.id, {
394
+ // The BOX keeps its own size — the words in it may be the author's, and one
395
+ // shrunk back to the canonical width would clip them.
396
+ xywh: new Bound(target.x +
397
+ anchorOffset(WARDLEY_LABEL_W, target.textAlign) -
398
+ anchorOffset(w, target.textAlign), target.y + WARDLEY_LABEL_H / 2 - h / 2, w, h).serialize(),
399
+ textAlign: target.textAlign,
400
+ });
401
+ }
325
402
  /**
326
403
  * Everything the morph owes a Wardley composite beyond the props patch — the
327
404
  * `afterMorph` half of the spec, run inside the generic module's one
@@ -337,7 +414,9 @@ function rewriteLabel(group, from, to) {
337
414
  * dots crammed into it. So the target's canonical size is applied, CENTRED on
338
415
  * where the artefact already stands: the thing does not move, it becomes the
339
416
  * size its notation says it is. The label keeps its own place, which is the
340
- * user's.
417
+ * user's — with the one exception {@link relocateLabel} states: a name still
418
+ * standing where creation put it follows the target kind when that kind carries
419
+ * its name somewhere else, or the new drawing lands on top of the old words.
341
420
  *
342
421
  * ## And why the rest is here rather than in the patch
343
422
  *
@@ -349,7 +428,7 @@ function rewriteLabel(group, from, to) {
349
428
  * LEAVING comes apart before the one it is arriving at is built, so a
350
429
  * market → pipeline never has three dots and a handle at once.
351
430
  */
352
- export function wardleyMorphComposite(selected, from, to) {
431
+ export function wardleyMorphComposite(selected, from, to, std) {
353
432
  if (!(selected instanceof GroupElementModel))
354
433
  return;
355
434
  const carrier = wardleyNodeOfComponent(selected);
@@ -367,7 +446,8 @@ export function wardleyMorphComposite(selected, from, to) {
367
446
  addMarketGlyph(selected, cx, cy);
368
447
  if (to === 'pipeline')
369
448
  addHandle(selected, carrier, cx, cy);
370
- rewriteLabel(selected, from, to);
449
+ relocateLabel(selected, from, to, cx, cy);
450
+ rewriteLabel(selected, from, to, std);
371
451
  }
372
452
  /**
373
453
  * Wardley's morph declaration, handed to the generic `morphToolbarConfig`.
package/dist/natures.js CHANGED
@@ -86,11 +86,14 @@ export const WARDLEY_TAG_DEFS = {
86
86
  packId: 'wardley-core',
87
87
  framework: 'wardley',
88
88
  label: 'Wardley',
89
+ labelKey: 'com.labre.wardley.tag-pack.label',
89
90
  tags: [
90
91
  {
91
92
  id: WARDLEY_NATURE_TAG_ID,
92
93
  label: 'Nature',
94
+ labelKey: 'com.labre.wardley.tag.nature',
93
95
  description: 'What kind of thing this component is: an activity, data, a practice or knowledge.',
96
+ descriptionKey: 'com.labre.wardley.tag.nature.description',
94
97
  cardinality: 'single',
95
98
  appliesTo: [WARDLEY_ROLE.component],
96
99
  // No `order`, on either tag of this pack: an absent order sorts by SEED
@@ -103,29 +106,39 @@ export const WARDLEY_TAG_DEFS = {
103
106
  {
104
107
  id: WARDLEY_NATURE.activity,
105
108
  label: 'Activity',
109
+ labelKey: 'com.labre.wardley.tag.nature.activity',
106
110
  description: 'Something that is DONE — a step, a service, a process.',
111
+ descriptionKey: 'com.labre.wardley.tag.nature.activity.description',
107
112
  },
108
113
  {
109
114
  id: WARDLEY_NATURE.data,
110
115
  label: 'Data',
116
+ labelKey: 'com.labre.wardley.tag.nature.data',
111
117
  description: 'Something that is RECORDED — a dataset, a register.',
118
+ descriptionKey: 'com.labre.wardley.tag.nature.data.description',
112
119
  },
113
120
  {
114
121
  id: WARDLEY_NATURE.practice,
115
122
  label: 'Practice',
123
+ labelKey: 'com.labre.wardley.tag.nature.practice',
116
124
  description: 'A way of doing — a method, a convention, an operating model.',
125
+ descriptionKey: 'com.labre.wardley.tag.nature.practice.description',
117
126
  },
118
127
  {
119
128
  id: WARDLEY_NATURE.knowledge,
120
129
  label: 'Knowledge',
130
+ labelKey: 'com.labre.wardley.tag.nature.knowledge',
121
131
  description: 'Something that is KNOWN — a model, a theory, a rule.',
132
+ descriptionKey: 'com.labre.wardley.tag.nature.knowledge.description',
122
133
  },
123
134
  ],
124
135
  },
125
136
  {
126
137
  id: WARDLEY_COMPETITION_TAG_ID,
127
138
  label: 'Competition',
139
+ labelKey: 'com.labre.wardley.tag.competition',
128
140
  description: "Which of Porter's forces this glyph marks: relative competition, a struggle for survival, or a struggle to establish.",
141
+ descriptionKey: 'com.labre.wardley.tag.competition.description',
129
142
  cardinality: 'single',
130
143
  appliesTo: [WARDLEY_ROLE.porter],
131
144
  values: [
@@ -135,17 +148,23 @@ export const WARDLEY_TAG_DEFS = {
135
148
  // shows: a reader picking a value here is choosing what the circle
136
149
  // will say, not filling in a form about it.
137
150
  label: 'Relative competition (R)',
151
+ labelKey: 'com.labre.wardley.tag.competition.relative',
138
152
  description: 'Established players competing against one another on a market that exists.',
153
+ descriptionKey: 'com.labre.wardley.tag.competition.relative.description',
139
154
  },
140
155
  {
141
156
  id: WARDLEY_COMPETITION.struggle,
142
157
  label: 'Struggle for survival (L)',
158
+ labelKey: 'com.labre.wardley.tag.competition.struggle',
143
159
  description: 'Pressure on a player to stay in a market it is already in.',
160
+ descriptionKey: 'com.labre.wardley.tag.competition.struggle.description',
144
161
  },
145
162
  {
146
163
  id: WARDLEY_COMPETITION.establish,
147
164
  label: 'Struggle to establish (E)',
165
+ labelKey: 'com.labre.wardley.tag.competition.establish',
148
166
  description: 'Pressure on a newcomer trying to get into a market.',
167
+ descriptionKey: 'com.labre.wardley.tag.competition.establish.description',
149
168
  },
150
169
  ],
151
170
  },
@@ -57,29 +57,31 @@ export declare const LINK_STROKE_WIDTH = 2;
57
57
  /**
58
58
  * Wardley red ("future"/evolution) — matches the validated arrow icon.
59
59
  *
60
- * An IDENTITY colour: the legend recognises a stored evolution link by it
61
- * (`legend.ts`, `el.stroke === WARDLEY_RED`). It stays a literal and must never
62
- * be derived from `NOTATION_NEUTRALS` or any other scale — a change of value
63
- * would orphan every link already drawn with it.
60
+ * A STORED colour: every evolution arrow ever drawn carries this exact value,
61
+ * so it stays a literal and must never be derived from `NOTATION_NEUTRALS` or
62
+ * any other scale — a change of value would leave the maps already drawn with
63
+ * it a different red from the ones drawn after. (It is no longer how an arrow
64
+ * is RECOGNISED: the legend and the rules read `wardley:change-arrow`, so a
65
+ * connector somebody restyled red is not one.)
64
66
  */
65
67
  export declare const WARDLEY_RED = "#d6455d";
66
68
  /**
67
69
  * Dependency link grey.
68
70
  *
69
- * An IDENTITY colour: the legend recognises a stored dependency link by it
70
- * (`legend.ts`, `el.stroke === LINK_GREY`). It stays a literal and must never
71
- * be derived from `NOTATION_NEUTRALS` — a change of value would orphan every
72
- * link already drawn with it.
71
+ * A STORED colour, like {@link WARDLEY_RED} above and for its reason: every
72
+ * dependency link ever drawn carries this exact value, so it stays a literal
73
+ * and must never be derived from `NOTATION_NEUTRALS`. What MAKES a connector a
74
+ * dependency is its role, not its grey.
73
75
  */
74
76
  export declare const LINK_GREY = "#666666";
75
77
  /**
76
78
  * Inertia bar color + size.
77
79
  *
78
- * An IDENTITY colour: the legend recognises a stored inertia bar by it
79
- * (`legend.ts`, `el.fillColor === INERTIA_COLOR`). It happens to equal the
80
- * scale's ink today, but it stays a literal and must never be derived from
81
- * `NOTATION_NEUTRALS`: a change of the shared ink would orphan every inertia bar
82
- * already on a map.
80
+ * A STORED colour, like the two above: every inertia bar ever drawn carries
81
+ * this exact value. It happens to equal the scale's ink today, but it stays a
82
+ * literal and must never be derived from `NOTATION_NEUTRALS` — a change of the
83
+ * shared ink would repaint the bars already on a map. A rect somebody tinted
84
+ * `#1f2328` is NOT an inertia: `wardley:inertia` is.
83
85
  */
84
86
  export declare const INERTIA_COLOR = "#1f2328";
85
87
  export declare const INERTIA_SIZE: {
@@ -55,29 +55,31 @@ export const LINK_STROKE_WIDTH = 2;
55
55
  /**
56
56
  * Wardley red ("future"/evolution) — matches the validated arrow icon.
57
57
  *
58
- * An IDENTITY colour: the legend recognises a stored evolution link by it
59
- * (`legend.ts`, `el.stroke === WARDLEY_RED`). It stays a literal and must never
60
- * be derived from `NOTATION_NEUTRALS` or any other scale — a change of value
61
- * would orphan every link already drawn with it.
58
+ * A STORED colour: every evolution arrow ever drawn carries this exact value,
59
+ * so it stays a literal and must never be derived from `NOTATION_NEUTRALS` or
60
+ * any other scale — a change of value would leave the maps already drawn with
61
+ * it a different red from the ones drawn after. (It is no longer how an arrow
62
+ * is RECOGNISED: the legend and the rules read `wardley:change-arrow`, so a
63
+ * connector somebody restyled red is not one.)
62
64
  */
63
65
  export const WARDLEY_RED = '#d6455d';
64
66
  /**
65
67
  * Dependency link grey.
66
68
  *
67
- * An IDENTITY colour: the legend recognises a stored dependency link by it
68
- * (`legend.ts`, `el.stroke === LINK_GREY`). It stays a literal and must never
69
- * be derived from `NOTATION_NEUTRALS` — a change of value would orphan every
70
- * link already drawn with it.
69
+ * A STORED colour, like {@link WARDLEY_RED} above and for its reason: every
70
+ * dependency link ever drawn carries this exact value, so it stays a literal
71
+ * and must never be derived from `NOTATION_NEUTRALS`. What MAKES a connector a
72
+ * dependency is its role, not its grey.
71
73
  */
72
74
  export const LINK_GREY = '#666666';
73
75
  /**
74
76
  * Inertia bar color + size.
75
77
  *
76
- * An IDENTITY colour: the legend recognises a stored inertia bar by it
77
- * (`legend.ts`, `el.fillColor === INERTIA_COLOR`). It happens to equal the
78
- * scale's ink today, but it stays a literal and must never be derived from
79
- * `NOTATION_NEUTRALS`: a change of the shared ink would orphan every inertia bar
80
- * already on a map.
78
+ * A STORED colour, like the two above: every inertia bar ever drawn carries
79
+ * this exact value. It happens to equal the scale's ink today, but it stays a
80
+ * literal and must never be derived from `NOTATION_NEUTRALS` — a change of the
81
+ * shared ink would repaint the bars already on a map. A rect somebody tinted
82
+ * `#1f2328` is NOT an inertia: `wardley:inertia` is.
81
83
  */
82
84
  export const INERTIA_COLOR = '#1f2328';
83
85
  export const INERTIA_SIZE = { w: 8, h: 44 };
@@ -30,13 +30,24 @@ export const wardleyNode = (model, ctx, matrix, renderer, rc, bound) => {
30
30
  return;
31
31
  const strokeWidth = model.strokeWidth || 1;
32
32
  const R = Math.min(w, h) / 2 - strokeWidth / 2;
33
+ // A node dragged smaller than its own stroke leaves the glyph no room: every
34
+ // radius below is a fraction of `R`, and `arc` throws `IndexSizeError` on a
35
+ // negative one rather than clamping — which, since the surface render loop
36
+ // wraps no renderer in a `try`, aborts the rest of the frame. The hatch step
37
+ // is a fraction of `R` too, so `R === 0` would also spin its loop forever.
38
+ // The native circle underneath is already painted; the glyph simply stops.
39
+ if (!(R > 0))
40
+ return;
33
41
  const color = renderer.getColorValue(model.strokeColor, DefaultTheme.shapeStrokeColor, true);
34
42
  ctx.setTransform(glyphMatrix);
35
43
  // ── Anchor: person glyph (clipped to the circle) ────────────────────
36
44
  if (model.kind === 'anchor') {
37
45
  ctx.save();
38
46
  ctx.beginPath();
39
- ctx.arc(cx, cy, R - strokeWidth / 2, 0, Math.PI * 2);
47
+ // The clip is inset by half a stroke again, which can land under zero on a
48
+ // node barely wider than its border: clipped to nothing, the glyph paints
49
+ // nothing, which is the honest picture at that size.
50
+ ctx.arc(cx, cy, Math.max(0, R - strokeWidth / 2), 0, Math.PI * 2);
40
51
  ctx.clip();
41
52
  ctx.lineWidth = strokeWidth;
42
53
  ctx.strokeStyle = color;
package/dist/presets.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FontWeight, type WardleyNodeKind } from '@formicoidea/labre-core/model';
1
+ import { FontWeight, PointStyle, StrokeStyle, type WardleyNodeKind } from '@formicoidea/labre-core/model';
2
2
  /**
3
3
  * What a Wardley artefact IS, as props — the ONE description the creation sites
4
4
  * and the morph both read.
@@ -72,6 +72,27 @@ export type WardleyLabelledKind = Exclude<WardleyArtefactKind, 'porter' | 'area'
72
72
  * still carries, and the only string a morph is ever allowed to rewrite.
73
73
  */
74
74
  export declare const WARDLEY_NODE_LABEL: Record<WardleyLabelledKind, string>;
75
+ /**
76
+ * The i18n key {@link WARDLEY_NODE_LABEL} is the English default of —
77
+ * resolved at placement (`createWardleyNode` / `createWardleyPipeline` /
78
+ * `createWardleyMarket`), never afterwards. "Pipeline" is the PO's glossary
79
+ * term for Wardley: the French proposal IS the English word.
80
+ *
81
+ * ## The morph's placeholder detection, translated
82
+ *
83
+ * `wardleyMorphedLabel` (`morph.ts`) decides whether to refresh an artefact's
84
+ * name across a morph, and accepts an OPTIONAL `std`: given one, it
85
+ * recognises the stored text as "untouched" whether it reads the English
86
+ * prompt or the host's own resolved wording, and resolves the TARGET kind's
87
+ * prompt through the host too (the same fix `c4MorphedTypeLine` carries,
88
+ * `gfx/c4/src/type-line.ts`). `MorphSpec.afterMorph`
89
+ * (`packages/affine/blocks/surface`) now hands its callee the morph
90
+ * toolbar's own `std`, so `rewriteLabel` passes it straight through: on a
91
+ * host running a French catalogue, morphing an untouched TRANSLATED node
92
+ * rewrites its name to the target kind's own translated prompt, exactly as a
93
+ * standalone playground already rewrote the English one.
94
+ */
95
+ export declare const wardleyNodeLabelKey: (kind: WardleyLabelledKind) => string;
75
96
  /** A box centred on a point — how every Wardley creation site places a node. */
76
97
  export declare function wardleyCenteredBox(cx: number, cy: number, w: number, h: number): string;
77
98
  /** The canonical box of one kind, centred where the artefact already stands. */
@@ -108,6 +129,29 @@ export declare const WARDLEY_LABEL_H: number;
108
129
  * the words end on a given edge is to start the box a full width before it.
109
130
  */
110
131
  export declare const WARDLEY_LABEL_W = 120;
132
+ /**
133
+ * Where a kind carries its NAME, and how the words sit in the box.
134
+ *
135
+ * The one description of a placement the four creation sites and the morph all
136
+ * read. Restated at each site it had already started to drift — and a morph
137
+ * that rewrites the BOX of an artefact without knowing where that kind holds
138
+ * its name leaves the words under the new drawing, which is W3's overlap and a
139
+ * label the pointer can no longer reach.
140
+ *
141
+ * `place` is the coarse half of the answer, and the only one a morph acts on:
142
+ * the three circles and the two arrows all wear their name BESIDE them, so an
143
+ * artefact that grows from one to the next leaves the words exactly where they
144
+ * are; the pipeline is the one kind that holds it ABOVE, and crossing between
145
+ * the two is what has to move something.
146
+ */
147
+ export type WardleyLabelPlacement = {
148
+ x: number;
149
+ y: number;
150
+ textAlign: 'left' | 'center' | 'right';
151
+ place: 'beside' | 'above';
152
+ };
153
+ /** Where a node of one kind, centred on (cx, cy), carries its label. */
154
+ export declare function wardleyLabelBoxFor(kind: WardleyLabelledKind, cx: number, cy: number): WardleyLabelPlacement;
111
155
  /**
112
156
  * The NAME beside an artefact, as props — a native free-text element.
113
157
  *
@@ -207,6 +251,32 @@ export declare function wardleyMarketLinkProps(sourceId: string, targetId: strin
207
251
  };
208
252
  /** The pairs the triangle wires, given the three dots in creation order. */
209
253
  export declare function wardleyMarketLinkPairs<T>(dots: readonly T[]): [T, T][];
254
+ /**
255
+ * How a Wardley connector LOOKS, per tool: the value-chain link is grey, solid
256
+ * and headless, the evolution arrow red, dashed and tipped with a filled
257
+ * triangle.
258
+ *
259
+ * Here beside the node presets, and for their reason: the style rides on the
260
+ * tool activation (`activateWardleyConnector`) and is pictured by the legend
261
+ * row that tool subscribes, so restating it in either place would be two
262
+ * descriptions of one notation that agreed the day they were written.
263
+ */
264
+ export declare const WARDLEY_EDGE_STYLE: {
265
+ readonly link: {
266
+ readonly stroke: "#666666";
267
+ readonly strokeStyle: StrokeStyle.Solid;
268
+ readonly strokeWidth: 2;
269
+ readonly frontEndpointStyle: PointStyle.None;
270
+ readonly rearEndpointStyle: PointStyle.None;
271
+ };
272
+ readonly arrow: {
273
+ readonly stroke: "#d6455d";
274
+ readonly strokeStyle: StrokeStyle.Dash;
275
+ readonly strokeWidth: 2;
276
+ readonly frontEndpointStyle: PointStyle.None;
277
+ readonly rearEndpointStyle: PointStyle.Triangle;
278
+ };
279
+ };
210
280
  /** One of the four arrows: its own axis-aligned box, and the outline in it. */
211
281
  export interface WardleyPorterArrow {
212
282
  /** Serialized `xywh` of the box the polygon is drawn inside. */
package/dist/presets.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, LABEL_DEFAULT, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
3
+ import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, LABEL_DEFAULT, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
4
4
  import { WARDLEY_ROLE } from './roles.js';
5
5
  /**
6
6
  * The size a kind is BORN at, which for Wardley is the notation rather than a
@@ -65,6 +65,27 @@ export const WARDLEY_NODE_LABEL = {
65
65
  accelerator: ACCELERATOR_LABEL,
66
66
  decelerator: DECELERATOR_LABEL,
67
67
  };
68
+ /**
69
+ * The i18n key {@link WARDLEY_NODE_LABEL} is the English default of —
70
+ * resolved at placement (`createWardleyNode` / `createWardleyPipeline` /
71
+ * `createWardleyMarket`), never afterwards. "Pipeline" is the PO's glossary
72
+ * term for Wardley: the French proposal IS the English word.
73
+ *
74
+ * ## The morph's placeholder detection, translated
75
+ *
76
+ * `wardleyMorphedLabel` (`morph.ts`) decides whether to refresh an artefact's
77
+ * name across a morph, and accepts an OPTIONAL `std`: given one, it
78
+ * recognises the stored text as "untouched" whether it reads the English
79
+ * prompt or the host's own resolved wording, and resolves the TARGET kind's
80
+ * prompt through the host too (the same fix `c4MorphedTypeLine` carries,
81
+ * `gfx/c4/src/type-line.ts`). `MorphSpec.afterMorph`
82
+ * (`packages/affine/blocks/surface`) now hands its callee the morph
83
+ * toolbar's own `std`, so `rewriteLabel` passes it straight through: on a
84
+ * host running a French catalogue, morphing an untouched TRANSLATED node
85
+ * rewrites its name to the target kind's own translated prompt, exactly as a
86
+ * standalone playground already rewrote the English one.
87
+ */
88
+ export const wardleyNodeLabelKey = (kind) => `com.labre.wardley.seed.${kind}`;
68
89
  /**
69
90
  * The fill a kind is born with. White for every circle that means "a thing on
70
91
  * the value chain"; the method's grey is the one that carries meaning — it
@@ -177,6 +198,40 @@ export const WARDLEY_LABEL_H = LABEL_FONT_SIZE + 8;
177
198
  * the words end on a given edge is to start the box a full width before it.
178
199
  */
179
200
  export const WARDLEY_LABEL_W = 120;
201
+ /** Where a node of one kind, centred on (cx, cy), carries its label. */
202
+ export function wardleyLabelBoxFor(kind, cx, cy) {
203
+ const { w } = WARDLEY_NODE_SIZE[kind];
204
+ // A pipeline is a 120-wide bar, and a name written to the right of it would
205
+ // stand a bar's length away from the thing it names. So the words go above
206
+ // the handle, centred on the body.
207
+ if (kind === 'pipeline') {
208
+ const top = cy - WARDLEY_NODE_SIZE.pipeline.h / 2;
209
+ return {
210
+ x: cx - WARDLEY_LABEL_W / 2,
211
+ y: top - HANDLE_SIZE / 2 - WARDLEY_LABEL_H - LABEL_GAP,
212
+ textAlign: 'center',
213
+ place: 'above',
214
+ };
215
+ }
216
+ // A decelerator points LEFT, and the reference puts the words on the side the
217
+ // arrow's shaft is on so the reading runs into the arrow rather than across
218
+ // it. A label box is a fixed width whatever it reads, so a right-aligned one
219
+ // has to start a box-width before the edge its words must end on.
220
+ if (kind === 'decelerator') {
221
+ return {
222
+ x: cx - w / 2 - LABEL_GAP - WARDLEY_LABEL_W,
223
+ y: cy - WARDLEY_LABEL_H / 2,
224
+ textAlign: 'right',
225
+ place: 'beside',
226
+ };
227
+ }
228
+ return {
229
+ x: cx + w / 2 + LABEL_GAP,
230
+ y: cy - WARDLEY_LABEL_H / 2,
231
+ textAlign: 'left',
232
+ place: 'beside',
233
+ };
234
+ }
180
235
  /**
181
236
  * The NAME beside an artefact, as props — a native free-text element.
182
237
  *
@@ -381,6 +436,33 @@ export function wardleyMarketLinkPairs(dots) {
381
436
  [dots[2], dots[0]],
382
437
  ];
383
438
  }
439
+ /* ── The two typed edges ──────────────────────────────────────────────── */
440
+ /**
441
+ * How a Wardley connector LOOKS, per tool: the value-chain link is grey, solid
442
+ * and headless, the evolution arrow red, dashed and tipped with a filled
443
+ * triangle.
444
+ *
445
+ * Here beside the node presets, and for their reason: the style rides on the
446
+ * tool activation (`activateWardleyConnector`) and is pictured by the legend
447
+ * row that tool subscribes, so restating it in either place would be two
448
+ * descriptions of one notation that agreed the day they were written.
449
+ */
450
+ export const WARDLEY_EDGE_STYLE = {
451
+ link: {
452
+ stroke: LINK_GREY,
453
+ strokeStyle: StrokeStyle.Solid,
454
+ strokeWidth: LINK_STROKE_WIDTH,
455
+ frontEndpointStyle: PointStyle.None,
456
+ rearEndpointStyle: PointStyle.None,
457
+ },
458
+ arrow: {
459
+ stroke: WARDLEY_RED,
460
+ strokeStyle: StrokeStyle.Dash,
461
+ strokeWidth: LINK_STROKE_WIDTH,
462
+ frontEndpointStyle: PointStyle.None,
463
+ rearEndpointStyle: PointStyle.Triangle,
464
+ },
465
+ };
384
466
  /**
385
467
  * The four arrow outlines, north first and then clockwise.
386
468
  *
@@ -1,2 +1,12 @@
1
1
  import { type TemplateCategory } from '@formicoidea/labre-core/gfx/template';
2
+ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
3
+ /**
4
+ * The three hand-drawn palette swatches' own tile names (`Template.nameKey`):
5
+ * none derives from a command, since `linkTool` / `evolutionArrow` /
6
+ * `addAreaPolygon` activate a tool rather than draw anything (see `tpl` below
7
+ * and its docstring).
8
+ */
9
+ export declare const WARDLEY_TEMPLATE_NAME_LINK: ChromeWording;
10
+ export declare const WARDLEY_TEMPLATE_NAME_EVOLUTION_ARROW: ChromeWording;
11
+ export declare const WARDLEY_TEMPLATE_NAME_AREA_POLYGON: ChromeWording;
2
12
  export declare const wardleyTemplateCategory: TemplateCategory;