@formicoidea/labre-framework-wardley 0.41.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
@@ -108,7 +108,9 @@ export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyM
108
108
  * dots crammed into it. So the target's canonical size is applied, CENTRED on
109
109
  * where the artefact already stands: the thing does not move, it becomes the
110
110
  * size its notation says it is. The label keeps its own place, which is the
111
- * 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.
112
114
  *
113
115
  * ## And why the rest is here rather than in the patch
114
116
  *
package/dist/morph.js CHANGED
@@ -1,8 +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
3
  import { translateKey } from '@formicoidea/labre-core/shared/services';
4
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
4
5
  import { wardleyCommandIcons, wardleyCommands } from './commands.js';
5
- import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, wardleyMorphClears, wardleyMorphProps, wardleyNodeLabelKey, } 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';
6
7
  import { WARDLEY_ROLE } from './roles.js';
7
8
  /**
8
9
  * What a Wardley artefact may BECOME — the declaration behind the "Change type"
@@ -338,6 +339,66 @@ function rewriteLabel(group, from, to, std) {
338
339
  label.text.insert(0, next);
339
340
  });
340
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
+ }
341
402
  /**
342
403
  * Everything the morph owes a Wardley composite beyond the props patch — the
343
404
  * `afterMorph` half of the spec, run inside the generic module's one
@@ -353,7 +414,9 @@ function rewriteLabel(group, from, to, std) {
353
414
  * dots crammed into it. So the target's canonical size is applied, CENTRED on
354
415
  * where the artefact already stands: the thing does not move, it becomes the
355
416
  * size its notation says it is. The label keeps its own place, which is the
356
- * 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.
357
420
  *
358
421
  * ## And why the rest is here rather than in the patch
359
422
  *
@@ -383,6 +446,7 @@ export function wardleyMorphComposite(selected, from, to, std) {
383
446
  addMarketGlyph(selected, cx, cy);
384
447
  if (to === 'pipeline')
385
448
  addHandle(selected, carrier, cx, cy);
449
+ relocateLabel(selected, from, to, cx, cy);
386
450
  rewriteLabel(selected, from, to, std);
387
451
  }
388
452
  /**
@@ -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.
@@ -129,6 +129,29 @@ export declare const WARDLEY_LABEL_H: number;
129
129
  * the words end on a given edge is to start the box a full width before it.
130
130
  */
131
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;
132
155
  /**
133
156
  * The NAME beside an artefact, as props — a native free-text element.
134
157
  *
@@ -228,6 +251,32 @@ export declare function wardleyMarketLinkProps(sourceId: string, targetId: strin
228
251
  };
229
252
  /** The pairs the triangle wires, given the three dots in creation order. */
230
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
+ };
231
280
  /** One of the four arrows: its own axis-aligned box, and the outline in it. */
232
281
  export interface WardleyPorterArrow {
233
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
@@ -198,6 +198,40 @@ export const WARDLEY_LABEL_H = LABEL_FONT_SIZE + 8;
198
198
  * the words end on a given edge is to start the box a full width before it.
199
199
  */
200
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
+ }
201
235
  /**
202
236
  * The NAME beside an artefact, as props — a native free-text element.
203
237
  *
@@ -402,6 +436,33 @@ export function wardleyMarketLinkPairs(dots) {
402
436
  [dots[2], dots[0]],
403
437
  ];
404
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
+ };
405
466
  /**
406
467
  * The four arrow outlines, north first and then clockwise.
407
468
  *
@@ -1,11 +1,12 @@
1
1
  import { type TemplateCategory } from '@formicoidea/labre-core/gfx/template';
2
2
  import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
3
3
  /**
4
- * The two hand-drawn palette swatches' own tile names (`Template.nameKey`):
5
- * neither derives from a command, since `linkTool` / `evolutionArrow`
6
- * activate a tool rather than draw anything (see `tpl` below and its
7
- * docstring).
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
8
  */
9
9
  export declare const WARDLEY_TEMPLATE_NAME_LINK: ChromeWording;
10
10
  export declare const WARDLEY_TEMPLATE_NAME_EVOLUTION_ARROW: ChromeWording;
11
+ export declare const WARDLEY_TEMPLATE_NAME_AREA_POLYGON: ChromeWording;
11
12
  export declare const wardleyTemplateCategory: TemplateCategory;
@@ -5,6 +5,7 @@ import { lowerWardleyArea } from '../actions.js';
5
5
  import { wardleyCommands } from '../commands.js';
6
6
  import { COLORS } from '../consts.js';
7
7
  import { INERTIA_COLOR, LINK_GREY, LINK_STROKE_WIDTH, NODE_FILL, NODE_STROKE, WARDLEY_RED, } from '../node/consts.js';
8
+ import { WARDLEY_AREA_SIZE, wardleyAreaProps } from '../presets.js';
8
9
  import { WARDLEY_ROLE } from '../roles.js';
9
10
  import { wardleyMaps } from './maps.js';
10
11
  /**
@@ -74,10 +75,10 @@ const arrowPreview = (rightwards) => `<svg ${ATTRS} fill="none"><path d="${right
74
75
  /** The zone, in its Peace wash — the pair differs on the outline and on nothing else. */
75
76
  const areaPreview = (outline) => `<svg ${ATTRS} fill="none">${outline}</svg>`;
76
77
  /**
77
- * The two hand-drawn palette swatches' own tile names (`Template.nameKey`):
78
- * neither derives from a command, since `linkTool` / `evolutionArrow`
79
- * activate a tool rather than draw anything (see `tpl` below and its
80
- * docstring).
78
+ * The three hand-drawn palette swatches' own tile names (`Template.nameKey`):
79
+ * none derives from a command, since `linkTool` / `evolutionArrow` /
80
+ * `addAreaPolygon` activate a tool rather than draw anything (see `tpl` below
81
+ * and its docstring).
81
82
  */
82
83
  export const WARDLEY_TEMPLATE_NAME_LINK = [
83
84
  'com.labre.wardley.template.link',
@@ -87,6 +88,10 @@ export const WARDLEY_TEMPLATE_NAME_EVOLUTION_ARROW = [
87
88
  'com.labre.wardley.template.evolution-arrow',
88
89
  'Evolution arrow',
89
90
  ];
91
+ export const WARDLEY_TEMPLATE_NAME_AREA_POLYGON = [
92
+ 'com.labre.wardley.template.area-polygon',
93
+ 'Area (polygon)',
94
+ ];
90
95
  /** A hand-composed template — what is left once the artefacts are derived. */
91
96
  function tpl(name, preview, elements, nameKey) {
92
97
  return {
@@ -98,7 +103,7 @@ function tpl(name, preview, elements, nameKey) {
98
103
  };
99
104
  }
100
105
  /**
101
- * A zone, plus the one thing a snapshot cannot say.
106
+ * A zone card, plus the one thing a snapshot cannot say.
102
107
  *
103
108
  * `createWardleyArea` lowers the zone the moment it exists, to just above the
104
109
  * framework backgrounds it covers — otherwise the wash sits on top of every
@@ -106,9 +111,9 @@ function tpl(name, preview, elements, nameKey) {
106
111
  * ALREADY ON THE BOARD, which a snapshot knows nothing about, so the panel
107
112
  * replays it on the freshly inserted element instead.
108
113
  */
109
- function areaTemplate(id, preview) {
114
+ function areaTemplate(template) {
110
115
  return {
111
- ...templateFromCommand(byId(id), preview),
116
+ ...template,
112
117
  afterInsert: (std, insertedIds) => lowerWardleyArea(std.get(GfxControllerIdentifier), insertedIds[0]),
113
118
  };
114
119
  }
@@ -147,7 +152,17 @@ export const wardleyTemplateCategory = {
147
152
  tpl('Evolution arrow', `<svg ${ATTRS} fill="none"><path d="M24 40 H100" stroke="${WARDLEY_RED}" stroke-width="2.4" stroke-dasharray="6 4"/><path d="M98 33 L112 40 L98 47 Z" fill="${WARDLEY_RED}"/></svg>`, {
148
153
  a: connect({ position: [0, 0] }, { position: [160, 0] }, { evolution: true, typed: false }),
149
154
  }, WARDLEY_TEMPLATE_NAME_EVOLUTION_ARROW[0]),
150
- areaTemplate('wardley.addAreaRect', areaPreview('<rect x="24" y="16" width="87" height="48" rx="2" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5"/>')),
151
- areaTemplate('wardley.addAreaPolygon', areaPreview('<path d="M67 12 L110 43 L94 68 H40 L24 43 Z" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5" stroke-linejoin="round"/>')),
155
+ areaTemplate(templateFromCommand(byId('wardley.addAreaRect'), areaPreview('<rect x="24" y="16" width="87" height="48" rx="2" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5"/>'))),
156
+ // Hand-composed, like the two connector swatches and for the same reason:
157
+ // `wardley.addAreaPolygon` arms the polygon tool (#343) and draws nothing,
158
+ // so there is no action to record. A panel card cannot arm a tool — it
159
+ // inserts a snapshot — so the card keeps offering the zone ready-made, and
160
+ // the author who wants to place the corners uses the sub-menu. The props
161
+ // still come from the pack, so the two zones cannot disagree.
162
+ areaTemplate(tpl(WARDLEY_TEMPLATE_NAME_AREA_POLYGON[1], areaPreview('<path d="M67 12 L110 43 L94 68 H40 L24 43 Z" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5" stroke-linejoin="round"/>'), {
163
+ area: wardleyAreaProps('polygon', {
164
+ xywh: `[0,0,${WARDLEY_AREA_SIZE.polygon.w},${WARDLEY_AREA_SIZE.polygon.h}]`,
165
+ }),
166
+ }, WARDLEY_TEMPLATE_NAME_AREA_POLYGON[0])),
152
167
  ],
153
168
  };
@@ -4,7 +4,7 @@ import { type ChromeWording } from '@formicoidea/labre-core/shared/services';
4
4
  * The seeds the two worked maps write — declared once so the builders below
5
5
  * and the manifest (`../translations.ts`) read the very same fallback, never
6
6
  * a restated literal. `com.labre.wardley.example.<map>.<slug>`, per
7
- * `docs/adr/0016`.
7
+ * `docs/adr/0023`.
8
8
  */
9
9
  export declare const TEA_SHOP_SEED: {
10
10
  readonly title: {
@@ -222,7 +222,7 @@ const mapPreview = (extra) => `<svg ${ATTRS} fill="none"><path d="M22 12 V64 H12
222
222
  * The seeds the two worked maps write — declared once so the builders below
223
223
  * and the manifest (`../translations.ts`) read the very same fallback, never
224
224
  * a restated literal. `com.labre.wardley.example.<map>.<slug>`, per
225
- * `docs/adr/0016`.
225
+ * `docs/adr/0023`.
226
226
  */
227
227
  export const TEA_SHOP_SEED = {
228
228
  title: {
@@ -310,7 +310,7 @@ function seedText(std, def) {
310
310
  * `build` takes the OPTIONAL inserting editor: called with none, at module
311
311
  * load, for the English `content`; called again with the real `std` as
312
312
  * {@link Template.localize}, so the two builds read the very same layout and
313
- * differ only in the words a seed resolves to (`docs/adr/0016`).
313
+ * differ only in the words a seed resolves to (`docs/adr/0023`).
314
314
  */
315
315
  function tpl(name, preview, build, nameKey) {
316
316
  return {
@@ -1,4 +1,4 @@
1
- import { type ChromeWording, type ToolbarContext } from '@formicoidea/labre-core/shared/services';
1
+ import { type ChromeWording, type ToolbarContext, type ToolbarModuleConfig } from '@formicoidea/labre-core/shared/services';
2
2
  import { type TemplateResult } from 'lit';
3
3
  /** Every wording above, for `translations.ts`'s manifest contribution. */
4
4
  export declare const WARDLEY_TOOLBAR_WORDINGS: readonly ChromeWording[];
@@ -73,12 +73,37 @@ export declare const wardleyToolbarConfig: {
73
73
  active(ctx: ToolbarContext): boolean;
74
74
  run(ctx: ToolbarContext): void;
75
75
  }];
76
- }, {
77
- readonly id: "d.legend";
78
- readonly tooltipWording: ChromeWording;
79
- readonly icon: TemplateResult<2>;
80
- readonly run: (ctx: ToolbarContext) => void;
81
- }];
76
+ }, import("@formicoidea/labre-core/shared/services").ToolbarActionGenerator];
82
77
  readonly when: (ctx: ToolbarContext) => boolean;
83
78
  };
84
79
  export declare const wardleyToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
80
+ /**
81
+ * The map's flag-gated row, WHOLE: the Legend button and the Validation
82
+ * dropdown, in one module.
83
+ *
84
+ * ## Why they cannot be two modules
85
+ *
86
+ * `renderToolbar` merges exactly four slots per element — `<flavour>`,
87
+ * `custom:<flavour>` and the two `affine:surface:*` wildcards — and
88
+ * `ToolbarModuleExtension` binds by DI variant, so a second module claiming
89
+ * `custom:affine:surface:wardley` would throw
90
+ * `DuplicateServiceDefinitionError` before the editor finished setting up. Two
91
+ * slots, and Wardley has three things to put on a selected map: the axis and
92
+ * label toggles (always-on, `<flavour>`, above), the legend and the level of
93
+ * requirement. The precedent is C4's `c4BoardToolingToolbarConfig`.
94
+ *
95
+ * ## Why the legend moved OUT of the always-on half
96
+ *
97
+ * Because GENERATING one is tooling, and `docs/adr/0009` gates tooling: the
98
+ * flag off takes away the gesture that writes legend elements, exactly as it
99
+ * takes away the sub-menu that draws components. What the gesture already
100
+ * wrote is document CONTENT and keeps being painted — the box is plain shapes,
101
+ * text and `wardleyNode`s with no role, all drawn by the always-on renderers.
102
+ * The axis and label toggles stay always-on for the opposite reason: a stored
103
+ * map must keep its axes whatever the flag says.
104
+ *
105
+ * Sorting keeps the row readable across the merge — `a.` … `d.legend` from the
106
+ * two modules, then `z.validation` — whatever order they were registered in.
107
+ */
108
+ export declare const wardleyBoardToolingToolbarConfig: ToolbarModuleConfig;
109
+ export declare const wardleyBoardToolingToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;