@formicoidea/labre-framework-wardley 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/actions.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
2
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
2
3
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
4
  import { type GfxController } from '@formicoidea/labre-core/std/gfx';
4
5
  import { type WardleyExportBoard } from './export.js';
5
- import { type WardleyArtefactKind } from './presets.js';
6
+ import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
6
7
  /**
7
8
  * The single-circle node flavours: one connectable ellipse + a label to its
8
9
  * right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
@@ -10,12 +11,15 @@ import { type WardleyArtefactKind } from './presets.js';
10
11
  *
11
12
  * A subset of {@link WardleyArtefactKind}, and named away from
12
13
  * `WardleyNodeKind` on purpose: the model declares a type of that name with
13
- * SEVEN values, and two homonyms of different cardinality — one of them the
14
- * source of the semantic vocabulary — is a trap. The market and the pipeline
15
- * are composites with creation functions of their own; the handle is not an
16
- * artefact at all.
14
+ * eleven values, and two homonyms of different cardinality — one of them the
15
+ * source of the semantic vocabulary — is a trap. The market, the pipeline and
16
+ * the Porter's-forces glyph are composites with creation functions of their
17
+ * own; the accelerator and the decelerator are not circles at all and place
18
+ * their label on the side their arrow points from
19
+ * ({@link createWardleyAccelerator}); the area is a zone with no label element
20
+ * at all ({@link createWardleyArea}); the handle is not an artefact.
17
21
  */
18
- export type WardleySingleCircleKind = Exclude<WardleyArtefactKind, 'market' | 'pipeline'>;
22
+ export type WardleySingleCircleKind = Exclude<WardleyArtefactKind, 'market' | 'pipeline' | 'porter' | 'accelerator' | 'decelerator' | 'area'>;
19
23
  /** Create a wardley map background of the given variant, viewport-centered. */
20
24
  export declare function createWardleyBackground(gfx: GfxController, variant?: WardleyBgVariant): void;
21
25
  /**
@@ -42,6 +46,104 @@ export declare function createWardleyPipeline(gfx: GfxController): void;
42
46
  * everything is grouped into one object.
43
47
  */
44
48
  export declare function createWardleyMarket(gfx: GfxController): void;
49
+ /**
50
+ * Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
51
+ * from the four cardinal directions by solid red arrows, all grouped as one
52
+ * object.
53
+ *
54
+ * It marks an EXTERNAL competition force acting on the map — which is why the
55
+ * circle carries `wardley:porter` and nothing under `wardley:component`: it is
56
+ * not a link in the value chain, and no rule written about the chain has
57
+ * anything to say about a pressure applied from outside it.
58
+ *
59
+ * No label, and that is the notation rather than an omission: the letter says
60
+ * which force this is (R relative competition, L struggle for survival, E
61
+ * struggle to establish) and it is the circle's OWN inner text, so a
62
+ * double-click opens the native shape editor on it (`node/node-view.ts`). The
63
+ * four arrows are the glyph's own wiring — role-less filled red polygons,
64
+ * outside the rim — exactly as the market's triangle is its own.
65
+ */
66
+ export declare function createWardleyPorter(gfx: GfxController): void;
67
+ /**
68
+ * Create an accelerator or a decelerator: one fat native POLYGON and the words
69
+ * that say what it is, grouped like every other artefact.
70
+ *
71
+ * Not a link in the value chain, and not a pressure on it either: an annotation
72
+ * of the CLIMATE, saying that something is speeding evolution up (an arrow
73
+ * pointing right, towards commodity) or holding it back (the mirror, pointing
74
+ * left). Which way it points IS the notation, so the two are separate kinds
75
+ * with outlines of their own rather than one kind with a `rotate`.
76
+ *
77
+ * ## Where the words go, and why it is not always the right
78
+ *
79
+ * Every other single artefact on this canvas wears its name on its right,
80
+ * because a circle has no direction to disagree with. An arrow does. The
81
+ * reference puts the words on the side the arrow's SHAFT is on — right of an
82
+ * accelerator, left of a decelerator — so the reading runs into the arrow
83
+ * rather than across it, and the label is aligned towards it (left-aligned on
84
+ * the right, right-aligned on the left) so the words end against the shaft
85
+ * whatever their length.
86
+ *
87
+ * SemiBold, which no other Wardley label is: these two are annotations laid
88
+ * over a map that is already full of names, and the weight is what keeps the
89
+ * word readable as a remark ABOUT the map rather than as one more component.
90
+ */
91
+ export declare function createWardleyAccelerator(gfx: GfxController, kind: 'accelerator' | 'decelerator'): void;
92
+ /**
93
+ * Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
94
+ * polygon whose corners the author then moves.
95
+ *
96
+ * One element and no group, which makes it the plainest creation site in this
97
+ * file — and the difference is the name. Every other artefact wears its name as
98
+ * a text element beside it, so every other artefact is a group of two. A zone's
99
+ * name belongs INSIDE the zone, so it is the shape's own inner text: created
100
+ * empty, opened by a double-click on the zone (`node/node-view.ts`), and set in
101
+ * a fit mode that never resizes the boundary around it.
102
+ *
103
+ * ## Why it is sent to the back
104
+ *
105
+ * A zone is drawn precisely OVER the components it groups, and the surface
106
+ * paints in index order: an area added last would sit on top of everything it
107
+ * surrounds — hiding it under a wash, and worse, intercepting every click meant
108
+ * for a component inside it. So the element is lowered the moment it exists,
109
+ * to the depth {@link wardleyAreaIndexOver} works out.
110
+ */
111
+ export declare function createWardleyArea(gfx: GfxController, shape: WardleyAreaShape): void;
112
+ /**
113
+ * One already-placed element, as {@link wardleyAreaIndexOver} needs to read it.
114
+ *
115
+ * Three fields and no model, so the placement can be reasoned about — and
116
+ * tested — without a document: what it is, where it is, and how deep.
117
+ */
118
+ export interface WardleyStackedElement {
119
+ /** Fractional index. Sorts lexicographically, which IS the paint order. */
120
+ index: string;
121
+ /** Serialized box. */
122
+ xywh: string;
123
+ /** Whether this is a framework BACKGROUND — a map, a pool, a C4 board… */
124
+ isBackground: boolean;
125
+ }
126
+ /**
127
+ * How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
128
+ * covers, and below everything else. `null` means the back of the surface.
129
+ *
130
+ * "The back" alone was the first answer and it was wrong, as the recette of
131
+ * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
132
+ * zone sent behind the whole surface went behind the map and vanished. What a
133
+ * zone must be under is the artefacts it groups; what it must be over is the
134
+ * canvas they are drawn on. Those are two different depths, and only the first
135
+ * one is "the back".
136
+ *
137
+ * So: find the topmost background the zone actually OVERLAPS — the map it was
138
+ * drawn on, and not some other framework's board parked elsewhere on the same
139
+ * canvas — and mint a key between it and whatever sits directly above it. With
140
+ * no background under the zone at all there is nothing to clear, and the back
141
+ * of the surface is right again.
142
+ *
143
+ * `siblings` need not be sorted: fractional indexes sort lexicographically, so
144
+ * this sorts them itself and reads the paint order straight off the strings.
145
+ */
146
+ export declare function wardleyAreaIndexOver(siblings: readonly WardleyStackedElement[], box: Bound): string | null;
45
147
  /**
46
148
  * Read an SVG the user picks as a SKETCH, and say what it cost.
47
149
  *
package/dist/actions.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { backgroundSize, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
3
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
4
+ import { ConnectorMode, FontFamily, FontWeight, FrameworkBackgroundElementModel, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
5
5
  import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
6
6
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
7
7
  import { Bound } from '@formicoidea/labre-core/global/gfx';
8
- import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
8
+ import { generateKeyBetween, GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
9
9
  import { WARDLEY_BACKGROUND } from './background.js';
10
10
  import { WARDLEY_SVG_IMPORT } from './interchange.js';
11
11
  import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
12
12
  import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
13
- import { HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, NODE_STROKE, WARDLEY_RED, } from './node/consts.js';
14
- import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, } from './presets.js';
13
+ import { HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, NODE_STROKE, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
14
+ import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
15
15
  import { WARDLEY_ROLE } from './roles.js';
16
16
  /**
17
17
  * Standalone creation/activation actions for the Wardley toolbox — the
@@ -64,6 +64,14 @@ const BACKGROUND_VARIANT_DEFAULTS = {
64
64
  };
65
65
  /** Height of the native free-text labels (Inter, size 18). */
66
66
  const LABEL_H = LABEL_FONT_SIZE + 8;
67
+ /**
68
+ * Width of a label box, whatever it reads.
69
+ *
70
+ * A number rather than a measurement, which is why a RIGHT-aligned label has to
71
+ * subtract it: the box does not shrink to the words, so the only way to make
72
+ * the words end on a given edge is to start the box a full width before it.
73
+ */
74
+ const LABEL_W = 120;
67
75
  function finish(gfx, id) {
68
76
  gfx.doc.captureSync();
69
77
  gfx.tool.setTool(DefaultTool);
@@ -81,18 +89,27 @@ function group(gfx, ids) {
81
89
  /**
82
90
  * Add a wardley node of one kind, at its canonical size, centred on (cx, cy).
83
91
  *
84
- * The props are {@link wardleyNodeProps}' and nothing else: the palette and the
85
- * "Change type" dropdown read that one description, so a market drawn here and
86
- * a component morphed into one are the same element (`presets.ts`).
92
+ * The APPEARANCE is {@link wardleyNodeProps}' and nothing else: the palette and
93
+ * the "Change type" dropdown read that one description, so a market drawn here
94
+ * and a component morphed into one are the same element (`presets.ts`).
95
+ *
96
+ * `extra` is for what a preset cannot carry — the Porter's-forces letter, which
97
+ * is CONTENT the author then edits, not a look. `wardleyMorphProps` strips
98
+ * `text` for exactly that reason, so a preset that wrote one would be a preset
99
+ * the morph has to un-write.
87
100
  */
88
- function addNode(surface, kind, cx, cy) {
89
- return surface.addElement(wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }));
101
+ function addNode(surface, kind, cx, cy, extra = {}) {
102
+ return surface.addElement({
103
+ ...wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }),
104
+ ...extra,
105
+ });
90
106
  }
91
107
  /** Add a native free-text label (same Inter family as the axis labels). */
92
- function addLabel(surface, text, x, y, textAlign = 'left') {
108
+ function addLabel(surface, text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
93
109
  return surface.addElement({
94
110
  type: 'text',
95
111
  text,
112
+ fontWeight,
96
113
  // Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
97
114
  // text element like any other, so its ROLE is the only thing that tells W3
98
115
  // it must not land on top of a node. A free text the user typed elsewhere
@@ -102,7 +119,7 @@ function addLabel(surface, text, x, y, textAlign = 'left') {
102
119
  fontSize: LABEL_FONT_SIZE,
103
120
  color: NODE_STROKE,
104
121
  textAlign,
105
- xywh: new Bound(x, y, 120, LABEL_H).serialize(),
122
+ xywh: new Bound(x, y, LABEL_W, LABEL_H).serialize(),
106
123
  });
107
124
  }
108
125
  /** Create a wardley map background of the given variant, viewport-centered. */
@@ -222,6 +239,150 @@ export function createWardleyMarket(gfx) {
222
239
  const labelId = addLabel(surface, WARDLEY_NODE_LABEL.market, cx + R + LABEL_GAP, cy - LABEL_H / 2);
223
240
  finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
224
241
  }
242
+ /**
243
+ * Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
244
+ * from the four cardinal directions by solid red arrows, all grouped as one
245
+ * object.
246
+ *
247
+ * It marks an EXTERNAL competition force acting on the map — which is why the
248
+ * circle carries `wardley:porter` and nothing under `wardley:component`: it is
249
+ * not a link in the value chain, and no rule written about the chain has
250
+ * anything to say about a pressure applied from outside it.
251
+ *
252
+ * No label, and that is the notation rather than an omission: the letter says
253
+ * which force this is (R relative competition, L struggle for survival, E
254
+ * struggle to establish) and it is the circle's OWN inner text, so a
255
+ * double-click opens the native shape editor on it (`node/node-view.ts`). The
256
+ * four arrows are the glyph's own wiring — role-less filled red polygons,
257
+ * outside the rim — exactly as the market's triangle is its own.
258
+ */
259
+ export function createWardleyPorter(gfx) {
260
+ const surface = gfx.surface;
261
+ if (!surface)
262
+ return;
263
+ const { centerX: cx, centerY: cy } = gfx.viewport;
264
+ const circleId = addNode(surface, 'porter', cx, cy, wardleyPorterLetterProps(PORTER_DEFAULT_LETTER));
265
+ const arrowIds = wardleyPorterArrows(cx, cy).map(arrow => surface.addElement(wardleyPorterArrowProps(arrow)));
266
+ finish(gfx, group(gfx, [circleId, ...arrowIds]));
267
+ }
268
+ /**
269
+ * Create an accelerator or a decelerator: one fat native POLYGON and the words
270
+ * that say what it is, grouped like every other artefact.
271
+ *
272
+ * Not a link in the value chain, and not a pressure on it either: an annotation
273
+ * of the CLIMATE, saying that something is speeding evolution up (an arrow
274
+ * pointing right, towards commodity) or holding it back (the mirror, pointing
275
+ * left). Which way it points IS the notation, so the two are separate kinds
276
+ * with outlines of their own rather than one kind with a `rotate`.
277
+ *
278
+ * ## Where the words go, and why it is not always the right
279
+ *
280
+ * Every other single artefact on this canvas wears its name on its right,
281
+ * because a circle has no direction to disagree with. An arrow does. The
282
+ * reference puts the words on the side the arrow's SHAFT is on — right of an
283
+ * accelerator, left of a decelerator — so the reading runs into the arrow
284
+ * rather than across it, and the label is aligned towards it (left-aligned on
285
+ * the right, right-aligned on the left) so the words end against the shaft
286
+ * whatever their length.
287
+ *
288
+ * SemiBold, which no other Wardley label is: these two are annotations laid
289
+ * over a map that is already full of names, and the weight is what keeps the
290
+ * word readable as a remark ABOUT the map rather than as one more component.
291
+ */
292
+ export function createWardleyAccelerator(gfx, kind) {
293
+ const surface = gfx.surface;
294
+ if (!surface)
295
+ return;
296
+ const { w } = WARDLEY_NODE_SIZE[kind];
297
+ const { centerX: cx, centerY: cy } = gfx.viewport;
298
+ const nodeId = addNode(surface, kind, cx, cy);
299
+ const rightwards = kind === 'accelerator';
300
+ const labelId = addLabel(surface, WARDLEY_NODE_LABEL[kind],
301
+ // A label box is a fixed LABEL_W wide whatever it reads, so a right-aligned
302
+ // one has to start a box-width before the edge the words must end on.
303
+ rightwards ? cx + w / 2 + LABEL_GAP : cx - w / 2 - LABEL_GAP - LABEL_W, cy - LABEL_H / 2, rightwards ? 'left' : 'right', FontWeight.SemiBold);
304
+ finish(gfx, group(gfx, [nodeId, labelId]));
305
+ }
306
+ /**
307
+ * Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
308
+ * polygon whose corners the author then moves.
309
+ *
310
+ * One element and no group, which makes it the plainest creation site in this
311
+ * file — and the difference is the name. Every other artefact wears its name as
312
+ * a text element beside it, so every other artefact is a group of two. A zone's
313
+ * name belongs INSIDE the zone, so it is the shape's own inner text: created
314
+ * empty, opened by a double-click on the zone (`node/node-view.ts`), and set in
315
+ * a fit mode that never resizes the boundary around it.
316
+ *
317
+ * ## Why it is sent to the back
318
+ *
319
+ * A zone is drawn precisely OVER the components it groups, and the surface
320
+ * paints in index order: an area added last would sit on top of everything it
321
+ * surrounds — hiding it under a wash, and worse, intercepting every click meant
322
+ * for a component inside it. So the element is lowered the moment it exists,
323
+ * to the depth {@link wardleyAreaIndexOver} works out.
324
+ */
325
+ export function createWardleyArea(gfx, shape) {
326
+ const surface = gfx.surface;
327
+ if (!surface)
328
+ return;
329
+ const { centerX: cx, centerY: cy } = gfx.viewport;
330
+ const xywh = wardleyAreaBox(shape, cx, cy);
331
+ const id = surface.addElement(wardleyAreaProps(shape, { xywh }));
332
+ const model = surface.getElementById(id);
333
+ if (model) {
334
+ const over = wardleyAreaIndexOver(stackedElementsOf(surface, id), Bound.deserialize(xywh));
335
+ // `null` is "nothing under it": the back of the surface, minted by the
336
+ // layer itself so an empty board gets the layer's own initial index rather
337
+ // than a key invented here.
338
+ model.index = over ?? gfx.layer.getReorderedIndex(model, 'back');
339
+ }
340
+ finish(gfx, id);
341
+ }
342
+ /** The top-level canvas elements of a surface, minus the one being placed. */
343
+ function stackedElementsOf(surface, exclude) {
344
+ return surface.elementModels
345
+ .filter(model => model.id !== exclude && model.group === null)
346
+ .map(model => ({
347
+ index: model.index,
348
+ xywh: model.xywh,
349
+ isBackground: model instanceof FrameworkBackgroundElementModel,
350
+ }));
351
+ }
352
+ /**
353
+ * How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
354
+ * covers, and below everything else. `null` means the back of the surface.
355
+ *
356
+ * "The back" alone was the first answer and it was wrong, as the recette of
357
+ * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
358
+ * zone sent behind the whole surface went behind the map and vanished. What a
359
+ * zone must be under is the artefacts it groups; what it must be over is the
360
+ * canvas they are drawn on. Those are two different depths, and only the first
361
+ * one is "the back".
362
+ *
363
+ * So: find the topmost background the zone actually OVERLAPS — the map it was
364
+ * drawn on, and not some other framework's board parked elsewhere on the same
365
+ * canvas — and mint a key between it and whatever sits directly above it. With
366
+ * no background under the zone at all there is nothing to clear, and the back
367
+ * of the surface is right again.
368
+ *
369
+ * `siblings` need not be sorted: fractional indexes sort lexicographically, so
370
+ * this sorts them itself and reads the paint order straight off the strings.
371
+ */
372
+ export function wardleyAreaIndexOver(siblings, box) {
373
+ const stack = [...siblings].sort((a, b) => a.index < b.index ? -1 : a.index > b.index ? 1 : 0);
374
+ // The LAST match, which is the topmost: a board with two maps on it gets the
375
+ // zone above the one it is actually drawn over, whichever was drawn first.
376
+ const under = stack.reduce((found, element, at) => element.isBackground &&
377
+ Bound.deserialize(element.xywh).isOverlapWithBound(box)
378
+ ? at
379
+ : found, -1);
380
+ if (under < 0)
381
+ return null;
382
+ // `null` for the upper bound when the background is the topmost element
383
+ // there is — `generateKeyBetween` reads that as "append after".
384
+ return generateKeyBetween(stack[under].index, stack[under + 1]?.index ?? null);
385
+ }
225
386
  /**
226
387
  * Read an SVG the user picks as a SKETCH, and say what it cost.
227
388
  *
@@ -119,6 +119,46 @@ export const wardleyCommandsManifest = [
119
119
  scope: 'edgeless',
120
120
  defaultKeys: { mac: ['w', 'i'], other: ['w', 'i'] },
121
121
  },
122
+ {
123
+ id: 'wardley.addPorter',
124
+ owner: 'wardley',
125
+ labelKey: 'com.labre.commands.wardley.addPorter',
126
+ labelFallback: "Porter's forces",
127
+ scope: 'edgeless',
128
+ defaultKeys: { mac: [], other: [] },
129
+ },
130
+ {
131
+ id: 'wardley.addAccelerator',
132
+ owner: 'wardley',
133
+ labelKey: 'com.labre.commands.wardley.addAccelerator',
134
+ labelFallback: 'Accelerator',
135
+ scope: 'edgeless',
136
+ defaultKeys: { mac: [], other: [] },
137
+ },
138
+ {
139
+ id: 'wardley.addDecelerator',
140
+ owner: 'wardley',
141
+ labelKey: 'com.labre.commands.wardley.addDecelerator',
142
+ labelFallback: 'Decelerator',
143
+ scope: 'edgeless',
144
+ defaultKeys: { mac: [], other: [] },
145
+ },
146
+ {
147
+ id: 'wardley.addAreaRect',
148
+ owner: 'wardley',
149
+ labelKey: 'com.labre.commands.wardley.addAreaRect',
150
+ labelFallback: 'Area (rectangle)',
151
+ scope: 'edgeless',
152
+ defaultKeys: { mac: [], other: [] },
153
+ },
154
+ {
155
+ id: 'wardley.addAreaPolygon',
156
+ owner: 'wardley',
157
+ labelKey: 'com.labre.commands.wardley.addAreaPolygon',
158
+ labelFallback: 'Area (polygon)',
159
+ scope: 'edgeless',
160
+ defaultKeys: { mac: [], other: [] },
161
+ },
122
162
  {
123
163
  id: 'wardley.importOwm',
124
164
  owner: 'wardley',
@@ -1,8 +1,25 @@
1
1
  import type { CommandDescriptor } from '@formicoidea/labre-core/std';
2
2
  import type { TemplateResult } from 'lit';
3
3
  /**
4
- * The Wardley registry: the thirteen toolbox entries, then the two directions
4
+ * The Wardley registry: the eighteen toolbox entries, then the two directions
5
5
  * of the OWM DSL and the SVG fallback (`docs/adr/0012`).
6
+ *
7
+ * ## Nineteen nominations, and the curation decision behind them
8
+ *
9
+ * `addPorter` spent the last seat of ADR 0014 R4's `SENIOR_MENU_CAP + 1`
10
+ * budget, and `addAccelerator` / `addDecelerator` are the two past it. The PO
11
+ * made the call on 2026-09-03 (Amendment 2026-09-03 under R4): **every Wardley
12
+ * artefact nominates the row**. The row itself is unchanged — thirteen
13
+ * arbitrated seats plus "More artefacts…", the cap of fourteen it has had since
14
+ * the OWM pair tipped this framework over — so what the decision buys is that
15
+ * a newly-drawn artefact is ELIGIBLE for a seat, and recency and frequency
16
+ * decide who gets one. Nothing becomes unreachable: the catalogue lists
17
+ * everything, which is the registry's own invariant.
18
+ *
19
+ * `addAreaRect` and `addAreaPolygon` are the two after those, and they land
20
+ * under that same amendment rather than reopening it: they nominate like every
21
+ * other Wardley artefact, the row is still thirteen arbitrated seats plus "More
22
+ * artefacts…", and the catalogue grows by a section of its own.
6
23
  */
7
24
  export declare const wardleyCommands: CommandDescriptor[];
8
25
  /** `iconKey` → template. Never travels through either manifest (ADR 0008). */
package/dist/commands.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
- import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
2
+ import { activateWardleyConnector, createWardleyAccelerator, createWardleyArea, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, createWardleyPorter, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
3
3
  import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
4
- import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './toolbar/icons.js';
4
+ import { wardleyAcceleratorIcon, wardleyAnchorIcon, wardleyAreaPolygonIcon, wardleyAreaRectIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyDeceleratorIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, wardleyPorterIcon, } from './toolbar/icons.js';
5
5
  const SPECS = [
6
6
  {
7
7
  id: 'addBackground',
@@ -12,6 +12,7 @@ const SPECS = [
12
12
  category: 'backgrounds',
13
13
  kind: 'artefact',
14
14
  element: 'background:classic',
15
+ board: true,
15
16
  run: gfx => createWardleyBackground(gfx, 'classic'),
16
17
  },
17
18
  {
@@ -21,6 +22,7 @@ const SPECS = [
21
22
  category: 'backgrounds',
22
23
  kind: 'artefact',
23
24
  element: 'background:opportunity',
25
+ board: true,
24
26
  run: gfx => createWardleyBackground(gfx, 'opportunity'),
25
27
  },
26
28
  {
@@ -30,6 +32,7 @@ const SPECS = [
30
32
  category: 'backgrounds',
31
33
  kind: 'artefact',
32
34
  element: 'background:benefit',
35
+ board: true,
33
36
  run: gfx => createWardleyBackground(gfx, 'benefit'),
34
37
  },
35
38
  {
@@ -39,6 +42,7 @@ const SPECS = [
39
42
  category: 'backgrounds',
40
43
  kind: 'artefact',
41
44
  element: 'background:evolution-gradient',
45
+ board: true,
42
46
  run: gfx => createWardleyBackground(gfx, 'evolution-gradient'),
43
47
  },
44
48
  {
@@ -137,6 +141,69 @@ const SPECS = [
137
141
  element: 'node:inertia',
138
142
  run: createWardleyInertia,
139
143
  },
144
+ // Last of the nodes, and last of the row: a Porter's force is the only
145
+ // artefact on this palette that is not part of the map — it is what presses
146
+ // on it from outside — so it reads as an annotation you reach for once the
147
+ // chain is drawn. Keyless: the `w` chord already seats eight artefacts, and
148
+ // a framework binds past that by host override rather than by default.
149
+ {
150
+ id: 'addPorter',
151
+ label: "Porter's forces",
152
+ iconKey: 'wardley.porter',
153
+ category: 'nodes',
154
+ kind: 'artefact',
155
+ element: 'node:porter',
156
+ run: createWardleyPorter,
157
+ },
158
+ // The two climate annotations, after the force and for the same reason: they
159
+ // are not links in the value chain either, and they are reached for once the
160
+ // chain is drawn and the question becomes how fast it is moving. Keyless,
161
+ // like the porter — the `w` chord seats eight artefacts and a framework binds
162
+ // past that by host override.
163
+ {
164
+ id: 'addAccelerator',
165
+ label: 'Accelerator',
166
+ iconKey: 'wardley.accelerator',
167
+ category: 'nodes',
168
+ kind: 'artefact',
169
+ element: 'node:accelerator',
170
+ run: gfx => createWardleyAccelerator(gfx, 'accelerator'),
171
+ },
172
+ {
173
+ id: 'addDecelerator',
174
+ label: 'Decelerator',
175
+ iconKey: 'wardley.decelerator',
176
+ category: 'nodes',
177
+ kind: 'artefact',
178
+ element: 'node:decelerator',
179
+ run: gfx => createWardleyAccelerator(gfx, 'decelerator'),
180
+ },
181
+ // The zones, last of the toolbox and after the connectors: an area is drawn
182
+ // around a chain that already exists, so it is the last gesture of a map and
183
+ // reads as the last entry of the row. Two entries and ONE kind — the shape is
184
+ // the choice, and offering it as two buttons is what saves the author a trip
185
+ // through "switch type" for the commonest case.
186
+ //
187
+ // Keyless, like the porter and the two climate arrows: the `w` chord seats
188
+ // eight artefacts and a framework binds past that by host override.
189
+ {
190
+ id: 'addAreaRect',
191
+ label: 'Area (rectangle)',
192
+ iconKey: 'wardley.area-rect',
193
+ category: 'areas',
194
+ kind: 'artefact',
195
+ element: 'node:area-rect',
196
+ run: gfx => createWardleyArea(gfx, 'rect'),
197
+ },
198
+ {
199
+ id: 'addAreaPolygon',
200
+ label: 'Area (polygon)',
201
+ iconKey: 'wardley.area-polygon',
202
+ category: 'areas',
203
+ kind: 'artefact',
204
+ element: 'node:area-polygon',
205
+ run: gfx => createWardleyArea(gfx, 'polygon'),
206
+ },
140
207
  ];
141
208
  /**
142
209
  * The sentence a tool shows under its label — the role's own gesture hint
@@ -171,7 +238,7 @@ const toolboxCommands = SPECS.map((spec, order) => ({
171
238
  : { mac: [], other: [] },
172
239
  availability: 'always',
173
240
  run: std => spec.run(std.get(GfxControllerIdentifier)),
174
- telemetry: { framework: 'wardley', element: spec.element },
241
+ telemetry: { framework: 'wardley', element: spec.element, board: spec.board },
175
242
  }));
176
243
  /**
177
244
  * The IMPORT — the first Wardley command whose subject is a whole map rather
@@ -183,9 +250,9 @@ const toolboxCommands = SPECS.map((spec, order) => ({
183
250
  * the sub-menu is the first thing a user opens, and "start from the map
184
251
  * somebody sent me" belongs in that row beside "start from a component" —
185
252
  * asking them to find the catalogue sidepanel first is the friction the
186
- * decision names. It is Wardley's fourteenth nomination, which is exactly the
187
- * cap, so nothing about the sub-menu's arbitration changes and no button is
188
- * pushed out of the row.
253
+ * decision names. It is the `+1` of ADR 0014's R4 — the single deliberate
254
+ * over-nomination the PO signed off on, and the reason this framework's
255
+ * nomination budget is `SENIOR_MENU_CAP + 1` rather than the cap itself.
189
256
  *
190
257
  * No `'contextual-toolbar'`: a contextual toolbar is a statement about a
191
258
  * SELECTION, and the moment this command is most wanted is on a board with
@@ -323,8 +390,25 @@ const importSvgCommand = {
323
390
  telemetry: { framework: 'wardley', element: 'board:import-svg' },
324
391
  };
325
392
  /**
326
- * The Wardley registry: the thirteen toolbox entries, then the two directions
393
+ * The Wardley registry: the eighteen toolbox entries, then the two directions
327
394
  * of the OWM DSL and the SVG fallback (`docs/adr/0012`).
395
+ *
396
+ * ## Nineteen nominations, and the curation decision behind them
397
+ *
398
+ * `addPorter` spent the last seat of ADR 0014 R4's `SENIOR_MENU_CAP + 1`
399
+ * budget, and `addAccelerator` / `addDecelerator` are the two past it. The PO
400
+ * made the call on 2026-09-03 (Amendment 2026-09-03 under R4): **every Wardley
401
+ * artefact nominates the row**. The row itself is unchanged — thirteen
402
+ * arbitrated seats plus "More artefacts…", the cap of fourteen it has had since
403
+ * the OWM pair tipped this framework over — so what the decision buys is that
404
+ * a newly-drawn artefact is ELIGIBLE for a seat, and recency and frequency
405
+ * decide who gets one. Nothing becomes unreachable: the catalogue lists
406
+ * everything, which is the registry's own invariant.
407
+ *
408
+ * `addAreaRect` and `addAreaPolygon` are the two after those, and they land
409
+ * under that same amendment rather than reopening it: they nominate like every
410
+ * other Wardley artefact, the row is still thirteen arbitrated seats plus "More
411
+ * artefacts…", and the catalogue grows by a section of its own.
328
412
  */
329
413
  export const wardleyCommands = [
330
414
  ...toolboxCommands,
@@ -347,6 +431,11 @@ export const wardleyCommandIcons = {
347
431
  'wardley.link': wardleyLinkIcon,
348
432
  'wardley.arrow': wardleyArrowIcon,
349
433
  'wardley.inertia': wardleyInertiaIcon,
434
+ 'wardley.porter': wardleyPorterIcon,
435
+ 'wardley.accelerator': wardleyAcceleratorIcon,
436
+ 'wardley.decelerator': wardleyDeceleratorIcon,
437
+ 'wardley.area-rect': wardleyAreaRectIcon,
438
+ 'wardley.area-polygon': wardleyAreaPolygonIcon,
350
439
  'wardley.import-owm': wardleyImportOwmIcon,
351
440
  'wardley.export-owm': wardleyExportOwmIcon,
352
441
  'wardley.import-svg': wardleyImportSvgIcon,