@formicoidea/labre-framework-wardley 0.58.0 → 0.60.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
@@ -4,6 +4,7 @@ import type { BlockStdScope } from '@formicoidea/labre-core/std';
4
4
  import { type GfxController, type GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
5
5
  import { type WardleyExportBoard } from './export.js';
6
6
  import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
7
+ import { WARDLEY_AXIS_SEED } from './toolbox.js';
7
8
  /**
8
9
  * Standalone creation/activation actions for the Wardley toolbox — the
9
10
  * BEHAVIOUR layer, shared by every surface. They only depend on the
@@ -14,25 +15,7 @@ import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
14
15
  * every surface goes through. `WardleyActionSource` is gone with it — the
15
16
  * segment/module discrimination it carried is now `CommandInvocation.surface`.
16
17
  */
17
- /**
18
- * The three axis words a translated variant's background is seeded with —
19
- * declared once, so the resolver below and the manifest
20
- * (`../translations.ts`) read the very same fallback.
21
- */
22
- export declare const WARDLEY_AXIS_SEED: {
23
- readonly opportunity: {
24
- readonly key: "com.labre.wardley.seed.axis-opportunity";
25
- readonly fallback: "Opportunity";
26
- };
27
- readonly benefit: {
28
- readonly key: "com.labre.wardley.seed.axis-benefit";
29
- readonly fallback: "Benefit";
30
- };
31
- readonly investment: {
32
- readonly key: "com.labre.wardley.seed.axis-investment";
33
- readonly fallback: "Investment";
34
- };
35
- };
18
+ export { WARDLEY_AXIS_SEED };
36
19
  /**
37
20
  * The single-circle node flavours: one connectable ellipse + a label to its
38
21
  * right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
@@ -116,6 +99,7 @@ export declare function createWardleyPorter(gfx: GfxController): void;
116
99
  * SemiBold, which no other Wardley label is: these two are annotations laid
117
100
  * over a map that is already full of names, and the weight is what keeps the
118
101
  * word readable as a remark ABOUT the map rather than as one more component.
102
+ * Both rules are the builder's (`wardleyNodeElements`).
119
103
  */
120
104
  export declare function createWardleyAccelerator(gfx: GfxController, kind: 'accelerator' | 'decelerator'): void;
121
105
  /**
package/dist/actions.js CHANGED
@@ -2,7 +2,7 @@ import { backgroundSize, containingFrame, DefaultTool, indexOverBackgrounds, run
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
3
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
4
  import { PolygonTool } from '@formicoidea/labre-core/gfx/shape';
5
- import { ConnectorMode, FontWeight, FrameworkBackgroundElementModel, TextElementModel, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
5
+ import { ConnectorMode, FrameworkBackgroundElementModel, TextElementModel, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
6
6
  import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
7
7
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
8
8
  import { Bound } from '@formicoidea/labre-core/global/gfx';
@@ -12,9 +12,10 @@ import { WARDLEY_SVG_IMPORT } from './interchange.js';
12
12
  import { wardleySafeFilename } from './export.js';
13
13
  import { wardleyBoardFrom } from './export-board.js';
14
14
  import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
15
- import { INERTIA_SIZE, PORTER_DEFAULT_LETTER } from './node/consts.js';
16
- import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, WARDLEY_EDGE_STYLE, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps, wardleyLabelBoxFor, wardleyLabelProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, wardleyNodeLabelKey, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
15
+ import { INERTIA_SIZE, PORTER_SIZE } from './node/consts.js';
16
+ import { wardleyAreaBox, wardleyAreaProps, WARDLEY_AREA_SIZE, WARDLEY_EDGE_STYLE, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeLabelKey, } from './presets.js';
17
17
  import { WARDLEY_ROLE } from './roles.js';
18
+ import { WARDLEY_AXIS_SEED, wardleyAreaElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
18
19
  /**
19
20
  * Standalone creation/activation actions for the Wardley toolbox — the
20
21
  * BEHAVIOUR layer, shared by every surface. They only depend on the
@@ -25,61 +26,27 @@ import { WARDLEY_ROLE } from './roles.js';
25
26
  * every surface goes through. `WardleyActionSource` is gone with it — the
26
27
  * segment/module discrimination it carried is now `CommandInvocation.surface`.
27
28
  */
29
+ // Declared with the editor-free toolbox (`docs/adr/0045`), which writes the
30
+ // English fallbacks; re-exported here unchanged.
31
+ export { WARDLEY_AXIS_SEED };
28
32
  /**
29
- * The three axis words a translated variant's background is seeded with —
30
- * declared once, so the resolver below and the manifest
31
- * (`../translations.ts`) read the very same fallback.
32
- */
33
- export const WARDLEY_AXIS_SEED = {
34
- opportunity: {
35
- key: 'com.labre.wardley.seed.axis-opportunity',
36
- fallback: 'Opportunity',
37
- },
38
- benefit: {
39
- key: 'com.labre.wardley.seed.axis-benefit',
40
- fallback: 'Benefit',
41
- },
42
- investment: {
43
- key: 'com.labre.wardley.seed.axis-investment',
44
- fallback: 'Investment',
45
- },
46
- };
47
- /**
48
- * Per-variant default label overrides applied at creation (all remain editable
49
- * afterwards via the inline editor / toggles). The gradient itself is driven by
50
- * `variant` in the renderer.
33
+ * The three axis words a translated variant's background is seeded with,
34
+ * resolved through the host's catalogue.
51
35
  *
52
36
  * Resolved through the translation seam since the i18n pass (#278): the ten
53
37
  * label props still default to `undefined` on the declaration itself, but
54
38
  * `opportunity` and `benefit` are the two variants whose Y axis is PROSE the
55
- * gesture bakes into the document at creation — `WARDLEY_AXIS_SEED` above is
56
- * what a host's catalogue is offered for the three words, resolved HERE, once,
57
- * exactly like every other seed in this file. `classic` and
58
- * `evolution-gradient` keep the classic labels (Value Chain / Uncharted /
59
- * Industrialized…), which are declaration `labelKey`s and were already fully
60
- * localisable — this function names no override for them.
39
+ * gesture bakes into the document at creation — `WARDLEY_AXIS_SEED` is what a
40
+ * host's catalogue is offered for the three words, resolved HERE, once,
41
+ * exactly like every other seed in this file. Which variant writes which word
42
+ * is the builder's (`wardleyMapElement`).
61
43
  */
62
- function backgroundVariantDefaults(gfx) {
44
+ function axisSeeds(gfx) {
63
45
  const axis = (def) => translateKey(gfx.std, def.key, def.fallback);
64
46
  return {
65
- classic: {},
66
- // The Y axis becomes "Opportunity"; phase labels keep the classic defaults.
67
- opportunity: {
68
- yAxisTitle: axis(WARDLEY_AXIS_SEED.opportunity),
69
- showVisibilityLabels: false,
70
- showCornerLabels: false,
71
- },
72
- // The Y axis splits into Benefit (top) / Investment (bottom) around a zero
73
- // line drawn by the renderer.
74
- benefit: {
75
- yAxisTitle: '',
76
- visibilityHigh: axis(WARDLEY_AXIS_SEED.benefit),
77
- visibilityLow: axis(WARDLEY_AXIS_SEED.investment),
78
- showCornerLabels: false,
79
- },
80
- // Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
81
- // the grey gradient differs.
82
- 'evolution-gradient': {},
47
+ opportunity: axis(WARDLEY_AXIS_SEED.opportunity),
48
+ benefit: axis(WARDLEY_AXIS_SEED.benefit),
49
+ investment: axis(WARDLEY_AXIS_SEED.investment),
83
50
  };
84
51
  }
85
52
  function finish(gfx, id) {
@@ -97,26 +64,63 @@ function group(gfx, ids) {
97
64
  return result.groupId || ids[0];
98
65
  }
99
66
  /**
100
- * Add a wardley node of one kind, at its canonical size, centred on (cx, cy).
101
- *
102
- * The APPEARANCE is {@link wardleyNodeProps}' and nothing else: the palette and
103
- * the "Change type" dropdown read that one description, so a market drawn here
104
- * and a component morphed into one are the same element (`presets.ts`).
67
+ * Write what a toolbox builder returned (`toolbox.ts`, `docs/adr/0045`) and
68
+ * return the id of the LAST element — the outermost group of a composite, or
69
+ * the one element of a single artefact — which is what the gesture selects.
105
70
  *
106
- * `extra` is for what a preset cannot carry — the Porter's-forces letter, which
107
- * is CONTENT the author then edits, not a look. `wardleyMorphProps` strips
108
- * `text` for exactly that reason, so a preset that wrote one would be a preset
109
- * the morph has to un-write.
71
+ * The builder is the ONE assembly of an artefact, shared with a host that has
72
+ * no editor; what stays here is what only an editor has. Local ids are minted
73
+ * in order, so a connector's ends and a group's members, which always come
74
+ * after the elements they name, resolve to the real ids. A group goes through
75
+ * {@link createGroupCommand} as it always has, which writes the group's
76
+ * translated title and its layer: the builder only says who the members are.
110
77
  */
111
- function addNode(surface, kind, cx, cy, extra = {}) {
112
- return surface.addElement({
113
- ...wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }),
114
- ...extra,
115
- });
78
+ function place(gfx, elements) {
79
+ const surface = gfx.surface;
80
+ if (!surface)
81
+ return;
82
+ const minted = new Map();
83
+ const real = (local) => minted.get(local) ?? local;
84
+ let last;
85
+ for (const { id: local, ...props } of elements) {
86
+ if (props.type === 'group') {
87
+ const children = Object.keys(props.children);
88
+ last = group(gfx, children.map(real));
89
+ }
90
+ else {
91
+ for (const end of ['source', 'target']) {
92
+ const ref = props[end];
93
+ if (ref?.id !== undefined)
94
+ props[end] = { ...ref, id: real(ref.id) };
95
+ }
96
+ last = surface.addElement(props);
97
+ }
98
+ if (typeof local === 'string' && last)
99
+ minted.set(local, last);
100
+ }
101
+ return last;
102
+ }
103
+ /** The point that centres a box of `w`×`h` on the viewport. */
104
+ function viewportTopLeft(gfx, w, h) {
105
+ const { centerX, centerY } = gfx.viewport;
106
+ return { x: centerX - w / 2, y: centerY - h / 2 };
107
+ }
108
+ /**
109
+ * A labelled kind's name prompt, resolved HERE, once — the prompt a node
110
+ * nobody has named still carries, exactly like a BPMN node's caption.
111
+ * `gfx.std` is how this file reaches the translation seam: these creation
112
+ * sites take `GfxController`, not `BlockStdScope`, directly (`WardleyView`
113
+ * reads the same member).
114
+ */
115
+ function seedName(gfx, kind) {
116
+ return translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]);
116
117
  }
117
- /** Add a native free-text label (same Inter family as the axis labels). */
118
- function addLabel(surface, text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
119
- return surface.addElement(wardleyLabelProps(text, x, y, textAlign, fontWeight));
118
+ /** Place a node + name composite of one kind, centred on the viewport. */
119
+ function placeNode(gfx, kind) {
120
+ const { w, h } = WARDLEY_NODE_SIZE[kind];
121
+ const id = place(gfx, wardleyNodeElements(kind, viewportTopLeft(gfx, w, h), seedName(gfx, kind)));
122
+ if (id)
123
+ finish(gfx, id);
120
124
  }
121
125
  /** Create a wardley map background of the given variant, viewport-centered. */
122
126
  export function createWardleyBackground(gfx, variant = 'classic') {
@@ -132,22 +136,12 @@ export function createWardleyBackground(gfx, variant = 'classic') {
132
136
  atLeastWidth = Math.max(atLeastWidth, ew);
133
137
  atLeastHeight = Math.max(atLeastHeight, eh);
134
138
  }
135
- const { width, height } = backgroundSize(WARDLEY_BACKGROUND, atLeastWidth, atLeastHeight);
136
- const { centerX, centerY } = gfx.viewport;
137
- const id = gfx.surface.addElement({
138
- type: WARDLEY_BACKGROUND.type,
139
- // The map is a first-class role: validation rules position artefacts
140
- // against `wardley:map`, never against the `wardley` element type. The
141
- // declaration owns it, so a templated map and a hand-drawn one agree.
142
- role: WARDLEY_BACKGROUND.role,
143
- // A map is a frame you place things on, not a shape you nudge — the
144
- // declaration decides, and the toolbar toggle takes over from there.
145
- resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
146
- variant,
147
- ...backgroundVariantDefaults(gfx)[variant],
148
- xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
149
- });
150
- finish(gfx, id);
139
+ const size = backgroundSize(WARDLEY_BACKGROUND, atLeastWidth, atLeastHeight);
140
+ const id = place(gfx, [
141
+ wardleyMapElement(variant, viewportTopLeft(gfx, size.width, size.height), axisSeeds(gfx), undefined, size),
142
+ ]);
143
+ if (id)
144
+ finish(gfx, id);
151
145
  }
152
146
  /**
153
147
  * Create a single-circle node (component / anchor / ecosystem / method):
@@ -155,30 +149,14 @@ export function createWardleyBackground(gfx, variant = 'classic') {
155
149
  * move together (enter the group to reposition / edit the label).
156
150
  */
157
151
  export function createWardleyNode(gfx, kind) {
158
- const surface = gfx.surface;
159
- if (!surface)
160
- return;
161
- const { centerX: cx, centerY: cy } = gfx.viewport;
162
- const nodeId = addNode(surface, kind, cx, cy);
163
- const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
164
- const labelId = addLabel(surface,
165
- // Resolved HERE, once — the prompt a node nobody has named still carries,
166
- // exactly like a BPMN node's caption. `gfx.std` is how this file reaches
167
- // the translation seam: these creation sites take `GfxController`, not
168
- // `BlockStdScope`, directly (`WardleyView` reads the same member).
169
- translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign);
170
- finish(gfx, group(gfx, [nodeId, labelId]));
152
+ placeNode(gfx, kind);
171
153
  }
172
154
  /** Create an inertia bar (filled black rect). */
173
155
  export function createWardleyInertia(gfx) {
174
- if (!gfx.surface)
175
- return;
176
156
  const { w, h } = INERTIA_SIZE;
177
- const { centerX, centerY } = gfx.viewport;
178
- const id = gfx.surface.addElement(wardleyInertiaProps({
179
- xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
180
- }));
181
- finish(gfx, id);
157
+ const id = place(gfx, [wardleyInertiaElement(viewportTopLeft(gfx, w, h))]);
158
+ if (id)
159
+ finish(gfx, id);
182
160
  }
183
161
  /**
184
162
  * Create a pipeline: a wide thin native rect body (white semi-transparent,
@@ -188,20 +166,10 @@ export function createWardleyInertia(gfx) {
188
166
  * moves as one. Pure composition of native elements — no custom type / view.
189
167
  */
190
168
  export function createWardleyPipeline(gfx) {
191
- if (!gfx.surface)
192
- return;
193
- const { centerX: cx, centerY: cy } = gfx.viewport;
194
- // Body: a WardleyNode rect, made non-connectable by `kind: 'pipeline'`.
195
- const bodyId = addNode(gfx.surface, 'pipeline', cx, cy);
196
- // Handle: a node-sized WardleyNode square straddling the top edge. Inherits
197
- // `centerAnchorOnly` so connectors attach to its center only.
198
- const handleId = gfx.surface.addElement(wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }));
199
- // Label centered horizontally on the pipeline, sitting ABOVE the handle.
200
- const { x, y, textAlign } = wardleyLabelBoxFor('pipeline', cx, cy);
201
- const labelId = addLabel(gfx.surface, translateKey(gfx.std, wardleyNodeLabelKey('pipeline'), WARDLEY_NODE_LABEL.pipeline), x, y, textAlign);
202
- // Nested groups: (handle + label), then (body + that group).
203
- const innerId = group(gfx, [handleId, labelId]);
204
- finish(gfx, group(gfx, [bodyId, innerId]));
169
+ const { w, h } = WARDLEY_NODE_SIZE.pipeline;
170
+ const id = place(gfx, wardleyPipelineElements(viewportTopLeft(gfx, w, h), seedName(gfx, 'pipeline')));
171
+ if (id)
172
+ finish(gfx, id);
205
173
  }
206
174
  /**
207
175
  * Create a market: a large thin-bordered circle (the connectable market node)
@@ -211,19 +179,10 @@ export function createWardleyPipeline(gfx) {
211
179
  * everything is grouped into one object.
212
180
  */
213
181
  export function createWardleyMarket(gfx) {
214
- const surface = gfx.surface;
215
- if (!surface)
216
- return;
217
- const { centerX: cx, centerY: cy } = gfx.viewport;
218
- // Outer circle = the market node (connectable, center-only).
219
- const circleId = addNode(surface, 'market', cx, cy);
220
- // 3 inner component nodes (thick border, no label) at the triangle vertices.
221
- const dotIds = wardleyMarketDotBoxes(cx, cy).map(xywh => surface.addElement(wardleyMarketDotProps({ xywh })));
222
- // Triangle: 3 attached connectors (auto-route center-to-center, clipped).
223
- const connIds = wardleyMarketLinkPairs(dotIds).map(([a, b]) => surface.addElement(wardleyMarketLinkProps(a, b)));
224
- const { x, y, textAlign } = wardleyLabelBoxFor('market', cx, cy);
225
- const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey('market'), WARDLEY_NODE_LABEL.market), x, y, textAlign);
226
- finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
182
+ const { w, h } = WARDLEY_NODE_SIZE.market;
183
+ const id = place(gfx, wardleyMarketElements(viewportTopLeft(gfx, w, h), seedName(gfx, 'market')));
184
+ if (id)
185
+ finish(gfx, id);
227
186
  }
228
187
  /**
229
188
  * Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
@@ -243,13 +202,9 @@ export function createWardleyMarket(gfx) {
243
202
  * outside the rim — exactly as the market's triangle is its own.
244
203
  */
245
204
  export function createWardleyPorter(gfx) {
246
- const surface = gfx.surface;
247
- if (!surface)
248
- return;
249
- const { centerX: cx, centerY: cy } = gfx.viewport;
250
- const circleId = addNode(surface, 'porter', cx, cy, wardleyPorterLetterProps(PORTER_DEFAULT_LETTER));
251
- const arrowIds = wardleyPorterArrows(cx, cy).map(arrow => surface.addElement(wardleyPorterArrowProps(arrow)));
252
- finish(gfx, group(gfx, [circleId, ...arrowIds]));
205
+ const id = place(gfx, wardleyPorterElements(viewportTopLeft(gfx, PORTER_SIZE, PORTER_SIZE)));
206
+ if (id)
207
+ finish(gfx, id);
253
208
  }
254
209
  /**
255
210
  * Create an accelerator or a decelerator: one fat native POLYGON and the words
@@ -274,18 +229,10 @@ export function createWardleyPorter(gfx) {
274
229
  * SemiBold, which no other Wardley label is: these two are annotations laid
275
230
  * over a map that is already full of names, and the weight is what keeps the
276
231
  * word readable as a remark ABOUT the map rather than as one more component.
232
+ * Both rules are the builder's (`wardleyNodeElements`).
277
233
  */
278
234
  export function createWardleyAccelerator(gfx, kind) {
279
- const surface = gfx.surface;
280
- if (!surface)
281
- return;
282
- const { centerX: cx, centerY: cy } = gfx.viewport;
283
- const nodeId = addNode(surface, kind, cx, cy);
284
- // Which side, and which alignment, are the placement's answer: the
285
- // decelerator's mirrored label is one of the two rules `presets.ts` states.
286
- const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
287
- const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign, FontWeight.SemiBold);
288
- finish(gfx, group(gfx, [nodeId, labelId]));
235
+ placeNode(gfx, kind);
289
236
  }
290
237
  /**
291
238
  * Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
@@ -311,8 +258,15 @@ export function createWardleyArea(gfx, shape) {
311
258
  if (!surface)
312
259
  return;
313
260
  const { centerX: cx, centerY: cy } = gfx.viewport;
314
- const xywh = wardleyAreaBox(shape, cx, cy);
315
- const id = surface.addElement(wardleyAreaProps(shape, { xywh }));
261
+ // The rectangle is a toolbox item (`wardleyAreaElement`); the polygon is
262
+ // drawn corner by corner from the palette ({@link activateWardleyAreaPolygon})
263
+ // and placed whole only by a caller of this function.
264
+ const { w, h } = WARDLEY_AREA_SIZE.rect;
265
+ const id = shape === 'rect'
266
+ ? place(gfx, [wardleyAreaElement(viewportTopLeft(gfx, w, h))])
267
+ : surface.addElement(wardleyAreaProps(shape, { xywh: wardleyAreaBox(shape, cx, cy) }));
268
+ if (!id)
269
+ return;
316
270
  lowerWardleyArea(gfx, id);
317
271
  finish(gfx, id);
318
272
  }
package/dist/import.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { ConnectorMode, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model/headless';
2
2
  import { WARDLEY_BACKGROUND } from './background.js';
3
3
  import { OWM_DEFAULT_MAP_HEIGHT, OWM_DEFAULT_MAP_WIDTH, OWM_LABEL_HEIGHT, OWM_LABEL_WIDTH, OWM_SCOPE, OWM_TAIL_ATTR, OWM_KEYWORDS, OWM_TITLE_ATTR, owmDefaultPlot, owmPointOf, WARDLEY_OWM_FORMAT_ID, } from './export.js';
4
- import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LABEL_FONT_SIZE, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, ECOSYSTEM_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, WARDLEY_RED, } from './node/consts.js';
4
+ import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, ECOSYSTEM_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, WARDLEY_RED, } from './node/consts.js';
5
5
  import { WARDLEY_NODE_SIZE, wardleyInertiaProps, wardleyNodeProps, } from './presets.js';
6
6
  import { WARDLEY_ROLE } from './roles.js';
7
+ import { wardleyChangeArrowElement, wardleyDependencyElement, } from './toolbox.js';
7
8
  /**
8
9
  * The OWM import remarks whose wording is FIXED, as `[key, English]` pairs.
9
10
  *
@@ -778,36 +779,16 @@ export function layoutWardleyStatements(statements, options) {
778
779
  });
779
780
  elements.push(label(evolution.becomes, cx + NODE_SIZE / 2 + LABEL_GAP, cy - OWM_LABEL_HEIGHT / 2, TextAlign.Left, WARDLEY_RED));
780
781
  }
782
+ // The two edges are the toolbar's own (`toolbox.ts`, `docs/adr/0045`): an
783
+ // arrow read from a file and one drawn with the tool are one assembly.
781
784
  evolutions.forEach((evolution, index) => {
782
- elements.push({
783
- type: 'connector',
784
- mode: ConnectorMode.Straight,
785
- role: WARDLEY_ROLE.changeArrow,
786
- stroke: WARDLEY_RED,
787
- strokeStyle: StrokeStyle.Dash,
788
- strokeWidth: LINK_STROKE_WIDTH,
789
- frontEndpointStyle: PointStyle.None,
790
- rearEndpointStyle: PointStyle.Triangle,
791
- source: { id: identity(evolution) },
792
- target: { id: twinHandles[index] },
793
- });
785
+ elements.push(wardleyChangeArrowElement(identity(evolution), twinHandles[index]));
794
786
  });
795
787
  for (const link of links) {
796
- elements.push({
797
- type: 'connector',
798
- mode: ConnectorMode.Straight,
799
- // The value-chain link. `source` is the CONSUMER and `target` is what it
800
- // needs — the verb of `wardley:dependency` (ADR 0010), which is exactly
801
- // what `A->B` says in this DSL. Never inverted.
802
- role: WARDLEY_ROLE.dependency,
803
- stroke: LINK_GREY,
804
- strokeStyle: StrokeStyle.Solid,
805
- strokeWidth: LINK_STROKE_WIDTH,
806
- frontEndpointStyle: PointStyle.None,
807
- rearEndpointStyle: PointStyle.None,
808
- source: { id: link.from },
809
- target: { id: link.to },
810
- });
788
+ // The value-chain link. `source` is the CONSUMER and `target` is what it
789
+ // needs — the verb of `wardley:dependency` (ADR 0010), which is exactly
790
+ // what `A->B` says in this DSL. Never inverted.
791
+ elements.push(wardleyDependencyElement(link.from, link.to));
811
792
  }
812
793
  for (const inertia of inertias) {
813
794
  const [cx, cy] = owmPointOf(plot, inertia.visibility, inertia.evolution);
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export { WARDLEY_BACKGROUND } from './background.js';
27
27
  */
28
28
  export { exportWardleyOwm, exportWardleyOwmWithWarnings, OWM_SCOPE, type OwmPlot, owmCoordsOf, owmPlotOf, owmPointOf, WARDLEY_OWM_FORMAT_ID, type WardleyExportBoard, wardleySafeFilename, } from './export.js';
29
29
  export { wardleyBoardFrom } from './export-board.js';
30
+ export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, type WardleyMapTexts, wardleyMarketElements, type WardleyNodeItemKind, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
30
31
  export { importWardleyOwm } from './import.js';
31
32
  export { importWardleySvg } from './svg-import.js';
32
33
  export { wardleyBoardOf, wardleyExportElementsOf, wardleyMapsSelected, } from './actions.js';
package/dist/index.js CHANGED
@@ -39,6 +39,8 @@ export { WARDLEY_BACKGROUND } from './background.js';
39
39
  */
40
40
  export { exportWardleyOwm, exportWardleyOwmWithWarnings, OWM_SCOPE, owmCoordsOf, owmPlotOf, owmPointOf, WARDLEY_OWM_FORMAT_ID, wardleySafeFilename, } from './export.js';
41
41
  export { wardleyBoardFrom } from './export-board.js';
42
+ // The toolbar as pure builders (`docs/adr/0045`), also on `./interchange`.
43
+ export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
42
44
  export { importWardleyOwm } from './import.js';
43
45
  // The SVG reader that recognises a map (ADR 0032), pure for the same reason:
44
46
  // labre-mcp calls the very function the command does.
@@ -62,3 +62,4 @@ export declare const WARDLEY_SVG_FORMAT: InterchangeFormat;
62
62
  * {@link WARDLEY_OWM_IMPORT} beside it stays the reference Wardley import.
63
63
  */
64
64
  export declare const WARDLEY_SVG_IMPORT: InterchangeImportCapability;
65
+ export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, type WardleyMapTexts, wardleyMarketElements, type WardleyNodeItemKind, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
@@ -89,3 +89,6 @@ export const WARDLEY_SVG_IMPORT = {
89
89
  direction: 'import',
90
90
  run: importWardleySvg,
91
91
  };
92
+ // The toolbar as pure builders (`docs/adr/0045`): what a host with no editor
93
+ // places to draw the very elements a user places from the toolbar.
94
+ export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
@@ -0,0 +1,94 @@
1
+ import type { FrameworkToolbox, SerializedElementProps, ToolboxPoint } from '@formicoidea/labre-core/interchange';
2
+ import type { WardleyBgVariant } from '@formicoidea/labre-core/model';
3
+ /**
4
+ * Wardley's toolbar as pure builders (`docs/adr/0045`): what the sub-menu's
5
+ * creation actions (`actions.ts`) place, with no editor behind them. The
6
+ * actions call these, so an artefact a host builds and one a user places from
7
+ * the toolbar are the same elements.
8
+ *
9
+ * Each builder is the TOOLBAR's artefact, not the OWM reader's: the reader
10
+ * (`import.ts`) lays a whole file out with a wider label box and a larger
11
+ * anchor, and draws no group. Every prop that is not geometry is the same on
12
+ * both paths (`toolbox.unit.spec.ts` holds the two to it).
13
+ */
14
+ /**
15
+ * The three axis words a translated variant's background is seeded with —
16
+ * declared once, so the builder below, the editor action that resolves them
17
+ * and the manifest (`translations.ts`) read the very same fallback. Moved here
18
+ * from `actions.ts`, which re-exports it unchanged.
19
+ */
20
+ export declare const WARDLEY_AXIS_SEED: {
21
+ readonly opportunity: {
22
+ readonly key: "com.labre.wardley.seed.axis-opportunity";
23
+ readonly fallback: "Opportunity";
24
+ };
25
+ readonly benefit: {
26
+ readonly key: "com.labre.wardley.seed.axis-benefit";
27
+ readonly fallback: "Benefit";
28
+ };
29
+ readonly investment: {
30
+ readonly key: "com.labre.wardley.seed.axis-investment";
31
+ readonly fallback: "Investment";
32
+ };
33
+ };
34
+ /** The axis words a map variant writes, by seed name; English when absent. */
35
+ export type WardleyMapTexts = Partial<Record<keyof typeof WARDLEY_AXIS_SEED, string>>;
36
+ /**
37
+ * A map background of one variant at `at`, at the declaration's reference
38
+ * size unless `size` says otherwise (the editor passes the size of the
39
+ * biggest map already on the board, which only an editor can read).
40
+ */
41
+ export declare function wardleyMapElement(variant: WardleyBgVariant, at: ToolboxPoint, texts?: WardleyMapTexts, local?: string, size?: {
42
+ width: number;
43
+ height: number;
44
+ }): SerializedElementProps;
45
+ /** The kinds drawn as one node and the name beside it, grouped. */
46
+ export type WardleyNodeItemKind = 'component' | 'method' | 'ecosystem' | 'anchor' | 'accelerator' | 'decelerator';
47
+ /**
48
+ * A single node at `at` (the top-left of the node's own box) and its name
49
+ * beside it, grouped: a component, a method, an ecosystem, an anchor, or one
50
+ * of the two climate arrows, whose name sits on the side their shaft is on.
51
+ */
52
+ export declare function wardleyNodeElements(kind: WardleyNodeItemKind, at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
53
+ /**
54
+ * A market at `at` (the top-left of its 30-unit circle): the circle, its
55
+ * three role-less dots wired into a triangle by three attached connectors,
56
+ * and its name, all in one group.
57
+ */
58
+ export declare function wardleyMarketElements(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
59
+ /**
60
+ * A pipeline at `at` (the top-left of its 120×25 body): the body, the
61
+ * `handle` square astride its top edge (the only place a connector lands),
62
+ * the name above it, and two nested groups — (handle + name), then (body +
63
+ * that group) — as `createWardleyPipeline` has always built it.
64
+ */
65
+ export declare function wardleyPipelineElements(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
66
+ /**
67
+ * A Porter's-forces glyph at `at` (the top-left of its 60-unit circle): the
68
+ * circle carrying ONE letter as its own inner text (R, L or E; R when
69
+ * absent) and the four role-less red arrows pushing on it, grouped.
70
+ */
71
+ export declare function wardleyPorterElements(at: ToolboxPoint, letter?: string, local?: string): SerializedElementProps[];
72
+ /** An inertia bar at `at`: a plain filled rect whose role is its meaning. */
73
+ export declare function wardleyInertiaElement(at: ToolboxPoint, local?: string): SerializedElementProps;
74
+ /**
75
+ * A rectangular AREA at `at`, its name its own inner text (the toolbar
76
+ * creates it empty). The editor then lowers it under the components it
77
+ * covers (`lowerWardleyArea`), a depth relative to what is already on the
78
+ * board: a host that writes one sets that depth itself.
79
+ */
80
+ export declare function wardleyAreaElement(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps;
81
+ /**
82
+ * The value-chain link the Link tool draws: `from` is the CONSUMER and `to`
83
+ * what it needs (ADR 0010), never inverted.
84
+ */
85
+ export declare function wardleyDependencyElement(from: string, to: string, label?: string): SerializedElementProps;
86
+ /** The evolution arrow the Arrow tool draws, from a component to its twin. */
87
+ export declare function wardleyChangeArrowElement(from: string, to: string, label?: string): SerializedElementProps;
88
+ /**
89
+ * Every item of the Wardley toolbar, in the toolbar's order. The polygon
90
+ * AREA is not here: it is a drawing tool, the author clicks its corners, and
91
+ * a prefabricated outline is precisely what the toolbar refuses to place
92
+ * (`activateWardleyAreaPolygon`).
93
+ */
94
+ export declare const WARDLEY_TOOLBOX: FrameworkToolbox;
@@ -0,0 +1,348 @@
1
+ import { ConnectorMode, FontWeight } from '@formicoidea/labre-core/model/headless';
2
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
3
+ import { WARDLEY_BACKGROUND } from './background.js';
4
+ import { wardleyCommandsManifest } from './commands-manifest.js';
5
+ import { INERTIA_SIZE, PORTER_DEFAULT_LETTER, PORTER_SIZE, } from './node/consts.js';
6
+ import { WARDLEY_AREA_SIZE, wardleyAreaProps, wardleyCanonicalBox, WARDLEY_EDGE_STYLE, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps, wardleyLabelBoxFor, wardleyLabelProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
7
+ import { WARDLEY_ROLE } from './roles.js';
8
+ /**
9
+ * Wardley's toolbar as pure builders (`docs/adr/0045`): what the sub-menu's
10
+ * creation actions (`actions.ts`) place, with no editor behind them. The
11
+ * actions call these, so an artefact a host builds and one a user places from
12
+ * the toolbar are the same elements.
13
+ *
14
+ * Each builder is the TOOLBAR's artefact, not the OWM reader's: the reader
15
+ * (`import.ts`) lays a whole file out with a wider label box and a larger
16
+ * anchor, and draws no group. Every prop that is not geometry is the same on
17
+ * both paths (`toolbox.unit.spec.ts` holds the two to it).
18
+ */
19
+ /**
20
+ * The three axis words a translated variant's background is seeded with —
21
+ * declared once, so the builder below, the editor action that resolves them
22
+ * and the manifest (`translations.ts`) read the very same fallback. Moved here
23
+ * from `actions.ts`, which re-exports it unchanged.
24
+ */
25
+ export const WARDLEY_AXIS_SEED = {
26
+ opportunity: {
27
+ key: 'com.labre.wardley.seed.axis-opportunity',
28
+ fallback: 'Opportunity',
29
+ },
30
+ benefit: {
31
+ key: 'com.labre.wardley.seed.axis-benefit',
32
+ fallback: 'Benefit',
33
+ },
34
+ investment: {
35
+ key: 'com.labre.wardley.seed.axis-investment',
36
+ fallback: 'Investment',
37
+ },
38
+ };
39
+ const given = (text, fallback) => text?.trim() || fallback;
40
+ /**
41
+ * Per-variant default labels written at creation (all remain editable
42
+ * afterwards). `opportunity` and `benefit` are the two variants whose Y axis
43
+ * is PROSE baked into the document; `classic` and `evolution-gradient` keep
44
+ * the declaration's own labels, which are `labelKey`s and need no seed.
45
+ */
46
+ function mapVariantProps(variant, texts) {
47
+ const axis = (seed) => given(texts[seed], WARDLEY_AXIS_SEED[seed].fallback);
48
+ switch (variant) {
49
+ case 'opportunity':
50
+ // The Y axis becomes "Opportunity"; phase labels keep the classic ones.
51
+ return {
52
+ yAxisTitle: axis('opportunity'),
53
+ showVisibilityLabels: false,
54
+ showCornerLabels: false,
55
+ };
56
+ case 'benefit':
57
+ // The Y axis splits into Benefit (top) / Investment (bottom) around a
58
+ // zero line drawn by the renderer.
59
+ return {
60
+ yAxisTitle: '',
61
+ visibilityHigh: axis('benefit'),
62
+ visibilityLow: axis('investment'),
63
+ showCornerLabels: false,
64
+ };
65
+ default:
66
+ return {};
67
+ }
68
+ }
69
+ /**
70
+ * A map background of one variant at `at`, at the declaration's reference
71
+ * size unless `size` says otherwise (the editor passes the size of the
72
+ * biggest map already on the board, which only an editor can read).
73
+ */
74
+ export function wardleyMapElement(variant, at, texts = {}, local = 'map', size = WARDLEY_BACKGROUND.geometry) {
75
+ return {
76
+ type: WARDLEY_BACKGROUND.type,
77
+ id: local,
78
+ // The map is a first-class role: validation rules position artefacts
79
+ // against `wardley:map`, never against the `wardley` element type.
80
+ role: WARDLEY_BACKGROUND.role,
81
+ // A map is a frame you place things on, not a shape you nudge.
82
+ resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
83
+ variant,
84
+ ...mapVariantProps(variant, texts),
85
+ xywh: new Bound(at.x, at.y, size.width, size.height).serialize(),
86
+ };
87
+ }
88
+ /** The centre of a box of size `w`×`h` whose top-left is `at`. */
89
+ const centreOf = (at, w, h) => ({
90
+ cx: at.x + w / 2,
91
+ cy: at.y + h / 2,
92
+ });
93
+ /** The name beside a labelled kind centred on (cx, cy). */
94
+ function labelOf(kind, name, cx, cy, id) {
95
+ const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
96
+ return {
97
+ ...wardleyLabelProps(given(name, WARDLEY_NODE_LABEL[kind]), x, y, textAlign,
98
+ // The two climate arrows are the only SemiBold names: an annotation laid
99
+ // over a map already full of names (`createWardleyAccelerator`).
100
+ kind === 'accelerator' || kind === 'decelerator'
101
+ ? FontWeight.SemiBold
102
+ : FontWeight.Regular),
103
+ id,
104
+ };
105
+ }
106
+ const groupOf = (id, members) => ({
107
+ type: 'group',
108
+ id,
109
+ children: Object.fromEntries(members.map(m => [m.id, true])),
110
+ });
111
+ /**
112
+ * A single node at `at` (the top-left of the node's own box) and its name
113
+ * beside it, grouped: a component, a method, an ecosystem, an anchor, or one
114
+ * of the two climate arrows, whose name sits on the side their shaft is on.
115
+ */
116
+ export function wardleyNodeElements(kind, at, name, local = kind) {
117
+ const { w, h } = WARDLEY_NODE_SIZE[kind];
118
+ const { cx, cy } = centreOf(at, w, h);
119
+ const members = [
120
+ {
121
+ ...wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }),
122
+ id: `${local}:node`,
123
+ },
124
+ labelOf(kind, name, cx, cy, `${local}:label`),
125
+ ];
126
+ return [...members, groupOf(`${local}:group`, members)];
127
+ }
128
+ /**
129
+ * A market at `at` (the top-left of its 30-unit circle): the circle, its
130
+ * three role-less dots wired into a triangle by three attached connectors,
131
+ * and its name, all in one group.
132
+ */
133
+ export function wardleyMarketElements(at, name, local = 'market') {
134
+ const { w, h } = WARDLEY_NODE_SIZE.market;
135
+ const { cx, cy } = centreOf(at, w, h);
136
+ const circle = {
137
+ ...wardleyNodeProps('market', {
138
+ xywh: wardleyCanonicalBox('market', cx, cy),
139
+ }),
140
+ id: `${local}:node`,
141
+ };
142
+ const dots = wardleyMarketDotBoxes(cx, cy).map((xywh, index) => ({
143
+ ...wardleyMarketDotProps({ xywh }),
144
+ id: `${local}:dot-${index + 1}`,
145
+ }));
146
+ const links = wardleyMarketLinkPairs(dots.map(dot => dot.id)).map(([a, b], index) => ({
147
+ ...wardleyMarketLinkProps(a, b),
148
+ id: `${local}:link-${index + 1}`,
149
+ }));
150
+ const members = [
151
+ circle,
152
+ ...dots,
153
+ ...links,
154
+ labelOf('market', name, cx, cy, `${local}:label`),
155
+ ];
156
+ return [...members, groupOf(`${local}:group`, members)];
157
+ }
158
+ /**
159
+ * A pipeline at `at` (the top-left of its 120×25 body): the body, the
160
+ * `handle` square astride its top edge (the only place a connector lands),
161
+ * the name above it, and two nested groups — (handle + name), then (body +
162
+ * that group) — as `createWardleyPipeline` has always built it.
163
+ */
164
+ export function wardleyPipelineElements(at, name, local = 'pipeline') {
165
+ const { w, h } = WARDLEY_NODE_SIZE.pipeline;
166
+ const { cx, cy } = centreOf(at, w, h);
167
+ const body = {
168
+ ...wardleyNodeProps('pipeline', {
169
+ xywh: wardleyCanonicalBox('pipeline', cx, cy),
170
+ }),
171
+ id: `${local}:node`,
172
+ };
173
+ const handle = {
174
+ ...wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }),
175
+ id: `${local}:handle`,
176
+ };
177
+ const label = labelOf('pipeline', name, cx, cy, `${local}:label`);
178
+ const inner = groupOf(`${local}:handle-group`, [handle, label]);
179
+ return [body, handle, label, inner, groupOf(`${local}:group`, [body, inner])];
180
+ }
181
+ /**
182
+ * A Porter's-forces glyph at `at` (the top-left of its 60-unit circle): the
183
+ * circle carrying ONE letter as its own inner text (R, L or E; R when
184
+ * absent) and the four role-less red arrows pushing on it, grouped.
185
+ */
186
+ export function wardleyPorterElements(at, letter, local = 'porter') {
187
+ const { cx, cy } = centreOf(at, PORTER_SIZE, PORTER_SIZE);
188
+ const members = [
189
+ {
190
+ ...wardleyNodeProps('porter', {
191
+ xywh: wardleyCanonicalBox('porter', cx, cy),
192
+ }),
193
+ ...wardleyPorterLetterProps(given(letter, PORTER_DEFAULT_LETTER)),
194
+ id: `${local}:node`,
195
+ },
196
+ ...wardleyPorterArrows(cx, cy).map((arrow, index) => ({
197
+ ...wardleyPorterArrowProps(arrow),
198
+ id: `${local}:arrow-${index + 1}`,
199
+ })),
200
+ ];
201
+ return [...members, groupOf(`${local}:group`, members)];
202
+ }
203
+ /** An inertia bar at `at`: a plain filled rect whose role is its meaning. */
204
+ export function wardleyInertiaElement(at, local = 'inertia') {
205
+ const { w, h } = INERTIA_SIZE;
206
+ return {
207
+ ...wardleyInertiaProps({
208
+ xywh: new Bound(at.x, at.y, w, h).serialize(),
209
+ }),
210
+ id: local,
211
+ };
212
+ }
213
+ /**
214
+ * A rectangular AREA at `at`, its name its own inner text (the toolbar
215
+ * creates it empty). The editor then lowers it under the components it
216
+ * covers (`lowerWardleyArea`), a depth relative to what is already on the
217
+ * board: a host that writes one sets that depth itself.
218
+ */
219
+ export function wardleyAreaElement(at, name, local = 'area') {
220
+ const { w, h } = WARDLEY_AREA_SIZE.rect;
221
+ const trimmed = name?.trim();
222
+ return {
223
+ ...wardleyAreaProps('rect', {
224
+ xywh: new Bound(at.x, at.y, w, h).serialize(),
225
+ }),
226
+ ...(trimmed ? { text: trimmed } : {}),
227
+ id: local,
228
+ };
229
+ }
230
+ function edge(kind, from, to, label) {
231
+ return {
232
+ type: 'connector',
233
+ mode: ConnectorMode.Straight,
234
+ // Two roles, never one specialising the other: W1 is about where an
235
+ // arrow points and must never fall on a dependency.
236
+ role: kind === 'link' ? WARDLEY_ROLE.dependency : WARDLEY_ROLE.changeArrow,
237
+ ...WARDLEY_EDGE_STYLE[kind],
238
+ source: { id: from },
239
+ target: { id: to },
240
+ ...(label?.trim() ? { text: label.trim() } : {}),
241
+ };
242
+ }
243
+ /**
244
+ * The value-chain link the Link tool draws: `from` is the CONSUMER and `to`
245
+ * what it needs (ADR 0010), never inverted.
246
+ */
247
+ export function wardleyDependencyElement(from, to, label) {
248
+ return edge('link', from, to, label);
249
+ }
250
+ /** The evolution arrow the Arrow tool draws, from a component to its twin. */
251
+ export function wardleyChangeArrowElement(from, to, label) {
252
+ return edge('arrow', from, to, label);
253
+ }
254
+ /** The toolbar command's label, so a host names an item as the toolbar does. */
255
+ function label(commandId) {
256
+ const row = wardleyCommandsManifest.find(r => r.id === commandId);
257
+ return {
258
+ labelKey: row?.labelKey ?? commandId,
259
+ labelFallback: row?.labelFallback ?? commandId,
260
+ };
261
+ }
262
+ const mapItem = (id, commandId, variant, texts) => ({
263
+ id,
264
+ ...label(commandId),
265
+ texts,
266
+ size: {
267
+ w: WARDLEY_BACKGROUND.geometry.width,
268
+ h: WARDLEY_BACKGROUND.geometry.height,
269
+ },
270
+ build: (at, slots, local) => [wardleyMapElement(variant, at, slots, local)],
271
+ });
272
+ const nodeItem = (kind, commandId) => ({
273
+ id: kind,
274
+ ...label(commandId),
275
+ texts: ['name'],
276
+ size: WARDLEY_NODE_SIZE[kind],
277
+ build: (at, texts, local) => wardleyNodeElements(kind, at, texts?.name, local),
278
+ });
279
+ const dependency = {
280
+ id: 'dependency',
281
+ ...label('wardley.linkTool'),
282
+ build: wardleyDependencyElement,
283
+ };
284
+ const changeArrow = {
285
+ id: 'change-arrow',
286
+ ...label('wardley.evolutionArrow'),
287
+ build: wardleyChangeArrowElement,
288
+ };
289
+ /**
290
+ * Every item of the Wardley toolbar, in the toolbar's order. The polygon
291
+ * AREA is not here: it is a drawing tool, the author clicks its corners, and
292
+ * a prefabricated outline is precisely what the toolbar refuses to place
293
+ * (`activateWardleyAreaPolygon`).
294
+ */
295
+ export const WARDLEY_TOOLBOX = {
296
+ framework: 'wardley',
297
+ items: [
298
+ mapItem('map', 'wardley.addBackground', 'classic', []),
299
+ mapItem('opportunity-map', 'wardley.addOpportunityBackground', 'opportunity', ['opportunity']),
300
+ mapItem('benefit-map', 'wardley.addBenefitBackground', 'benefit', [
301
+ 'benefit',
302
+ 'investment',
303
+ ]),
304
+ mapItem('evolution-map', 'wardley.addEvolutionBackground', 'evolution-gradient', []),
305
+ nodeItem('component', 'wardley.addComponent'),
306
+ nodeItem('method', 'wardley.addMethod'),
307
+ {
308
+ id: 'market',
309
+ ...label('wardley.addMarket'),
310
+ texts: ['name'],
311
+ size: WARDLEY_NODE_SIZE.market,
312
+ build: (at, texts, local) => wardleyMarketElements(at, texts?.name, local),
313
+ },
314
+ nodeItem('ecosystem', 'wardley.addEcosystem'),
315
+ nodeItem('anchor', 'wardley.addAnchor'),
316
+ {
317
+ id: 'pipeline',
318
+ ...label('wardley.addPipeline'),
319
+ texts: ['name'],
320
+ size: WARDLEY_NODE_SIZE.pipeline,
321
+ build: (at, texts, local) => wardleyPipelineElements(at, texts?.name, local),
322
+ },
323
+ {
324
+ id: 'inertia',
325
+ ...label('wardley.addInertia'),
326
+ texts: [],
327
+ size: INERTIA_SIZE,
328
+ build: (at, _texts, local) => [wardleyInertiaElement(at, local)],
329
+ },
330
+ {
331
+ id: 'porter',
332
+ ...label('wardley.addPorter'),
333
+ texts: ['letter'],
334
+ size: WARDLEY_NODE_SIZE.porter,
335
+ build: (at, texts, local) => wardleyPorterElements(at, texts?.letter, local),
336
+ },
337
+ nodeItem('accelerator', 'wardley.addAccelerator'),
338
+ nodeItem('decelerator', 'wardley.addDecelerator'),
339
+ {
340
+ id: 'area',
341
+ ...label('wardley.addAreaRect'),
342
+ texts: ['name'],
343
+ size: WARDLEY_AREA_SIZE.rect,
344
+ build: (at, texts, local) => [wardleyAreaElement(at, texts?.name, local)],
345
+ },
346
+ ],
347
+ links: [dependency, changeArrow],
348
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-wardley",
3
3
  "description": "Labre wardley framework for @formicoidea/labre-core.",
4
- "version": "0.58.0",
4
+ "version": "0.60.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -35,7 +35,7 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@formicoidea/labre-core": "0.58.0",
38
+ "@formicoidea/labre-core": "0.60.0",
39
39
  "lit": "^3.2.0"
40
40
  }
41
41
  }