@formicoidea/labre-framework-wardley 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +384 -0
- package/dist/actions.d.ts +80 -13
- package/dist/actions.js +167 -87
- package/dist/commands.js +52 -31
- package/dist/element-view.d.ts +20 -34
- package/dist/element-view.js +23 -139
- package/dist/import.d.ts +28 -0
- package/dist/import.js +66 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/legend.d.ts +107 -12
- package/dist/legend.js +485 -471
- package/dist/morph.d.ts +15 -3
- package/dist/morph.js +90 -10
- package/dist/natures.js +19 -0
- package/dist/node/consts.d.ts +15 -13
- package/dist/node/consts.js +15 -13
- package/dist/node/node-renderer.js +12 -1
- package/dist/presets.d.ts +71 -1
- package/dist/presets.js +83 -1
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.js +40 -8
- package/dist/templates/maps.d.ts +94 -0
- package/dist/templates/maps.js +148 -27
- package/dist/toolbar/config.d.ts +34 -7
- package/dist/toolbar/config.js +109 -34
- package/dist/toolbar/node-config.d.ts +34 -2
- package/dist/toolbar/node-config.js +112 -9
- package/dist/toolbar/wardley-senior-button.d.ts +8 -0
- package/dist/toolbar/wardley-senior-button.js +12 -2
- package/dist/translations.js +81 -1
- package/dist/view.js +19 -15
- package/package.json +2 -2
package/dist/actions.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { backgroundSize, DefaultTool, indexOverBackgrounds, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
|
|
1
|
+
import { backgroundSize, containingFrame, DefaultTool, indexOverBackgrounds, 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 {
|
|
4
|
+
import { PolygonTool } from '@formicoidea/labre-core/gfx/shape';
|
|
5
|
+
import { ConnectorMode, FontWeight, FrameworkBackgroundElementModel, TextElementModel, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
5
6
|
import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
6
7
|
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
7
8
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
8
|
-
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
9
|
+
import { GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
|
|
9
10
|
import { WARDLEY_BACKGROUND } from './background.js';
|
|
10
11
|
import { WARDLEY_SVG_IMPORT } from './interchange.js';
|
|
11
12
|
import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
|
|
12
13
|
import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
|
|
13
|
-
import {
|
|
14
|
-
import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps,
|
|
14
|
+
import { INERTIA_SIZE, PORTER_DEFAULT_LETTER } from './node/consts.js';
|
|
15
|
+
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
16
|
import { WARDLEY_ROLE } from './roles.js';
|
|
16
17
|
/**
|
|
17
18
|
* Standalone creation/activation actions for the Wardley toolbox — the
|
|
@@ -24,48 +25,62 @@ import { WARDLEY_ROLE } from './roles.js';
|
|
|
24
25
|
* segment/module discrimination it carried is now `CommandInvocation.surface`.
|
|
25
26
|
*/
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
* TODO(PF2 follow-up): these write ENGLISH PROSE into the document. Since PF2
|
|
32
|
-
* the ten label props default to `undefined` precisely so the declaration's
|
|
33
|
-
* i18n keys are reachable — but a map created as `opportunity` or `benefit`
|
|
34
|
-
* lands with "Opportunity" / "Benefit" / "Investment" already persisted as if
|
|
35
|
-
* the user had typed them, so those three are un-localisable for the life of
|
|
36
|
-
* the document. Only `classic` and `evolution-gradient` are fully localisable
|
|
37
|
-
* today.
|
|
38
|
-
*
|
|
39
|
-
* The fix is to make the variant part of the declaration — one axis/end-label
|
|
40
|
-
* set per variant, each naming its own `labelKey` — rather than a bag of prop
|
|
41
|
-
* overrides applied at creation. Out of scope here (it changes what a variant
|
|
42
|
-
* IS). This is now the ONLY copy: the four background templates are derived
|
|
43
|
-
* from these four commands, so the table goes away in one place.
|
|
28
|
+
* The three axis words a translated variant's background is seeded with —
|
|
29
|
+
* declared once, so the resolver below and the manifest
|
|
30
|
+
* (`../translations.ts`) read the very same fallback.
|
|
44
31
|
*/
|
|
45
|
-
const
|
|
46
|
-
classic: {},
|
|
47
|
-
// The Y axis becomes "Opportunity"; phase labels keep the classic defaults.
|
|
32
|
+
export const WARDLEY_AXIS_SEED = {
|
|
48
33
|
opportunity: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
showCornerLabels: false,
|
|
34
|
+
key: 'com.labre.wardley.seed.axis-opportunity',
|
|
35
|
+
fallback: 'Opportunity',
|
|
52
36
|
},
|
|
53
|
-
// The Y axis splits into Benefit (top) / Investment (bottom) around a zero
|
|
54
|
-
// line drawn by the renderer.
|
|
55
37
|
benefit: {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
38
|
+
key: 'com.labre.wardley.seed.axis-benefit',
|
|
39
|
+
fallback: 'Benefit',
|
|
40
|
+
},
|
|
41
|
+
investment: {
|
|
42
|
+
key: 'com.labre.wardley.seed.axis-investment',
|
|
43
|
+
fallback: 'Investment',
|
|
60
44
|
},
|
|
61
|
-
// Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
|
|
62
|
-
// the grey gradient differs.
|
|
63
|
-
'evolution-gradient': {},
|
|
64
45
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Per-variant default label overrides applied at creation (all remain editable
|
|
48
|
+
* afterwards via the inline editor / toggles). The gradient itself is driven by
|
|
49
|
+
* `variant` in the renderer.
|
|
50
|
+
*
|
|
51
|
+
* Resolved through the translation seam since the i18n pass (#278): the ten
|
|
52
|
+
* label props still default to `undefined` on the declaration itself, but
|
|
53
|
+
* `opportunity` and `benefit` are the two variants whose Y axis is PROSE the
|
|
54
|
+
* gesture bakes into the document at creation — `WARDLEY_AXIS_SEED` above is
|
|
55
|
+
* what a host's catalogue is offered for the three words, resolved HERE, once,
|
|
56
|
+
* exactly like every other seed in this file. `classic` and
|
|
57
|
+
* `evolution-gradient` keep the classic labels (Value Chain / Uncharted /
|
|
58
|
+
* Industrialized…), which are declaration `labelKey`s and were already fully
|
|
59
|
+
* localisable — this function names no override for them.
|
|
60
|
+
*/
|
|
61
|
+
function backgroundVariantDefaults(gfx) {
|
|
62
|
+
const axis = (def) => translateKey(gfx.std, def.key, def.fallback);
|
|
63
|
+
return {
|
|
64
|
+
classic: {},
|
|
65
|
+
// The Y axis becomes "Opportunity"; phase labels keep the classic defaults.
|
|
66
|
+
opportunity: {
|
|
67
|
+
yAxisTitle: axis(WARDLEY_AXIS_SEED.opportunity),
|
|
68
|
+
showVisibilityLabels: false,
|
|
69
|
+
showCornerLabels: false,
|
|
70
|
+
},
|
|
71
|
+
// The Y axis splits into Benefit (top) / Investment (bottom) around a zero
|
|
72
|
+
// line drawn by the renderer.
|
|
73
|
+
benefit: {
|
|
74
|
+
yAxisTitle: '',
|
|
75
|
+
visibilityHigh: axis(WARDLEY_AXIS_SEED.benefit),
|
|
76
|
+
visibilityLow: axis(WARDLEY_AXIS_SEED.investment),
|
|
77
|
+
showCornerLabels: false,
|
|
78
|
+
},
|
|
79
|
+
// Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
|
|
80
|
+
// the grey gradient differs.
|
|
81
|
+
'evolution-gradient': {},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
69
84
|
function finish(gfx, id) {
|
|
70
85
|
gfx.doc.captureSync();
|
|
71
86
|
gfx.tool.setTool(DefaultTool);
|
|
@@ -128,7 +143,7 @@ export function createWardleyBackground(gfx, variant = 'classic') {
|
|
|
128
143
|
// declaration decides, and the toolbar toggle takes over from there.
|
|
129
144
|
resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
|
|
130
145
|
variant,
|
|
131
|
-
...
|
|
146
|
+
...backgroundVariantDefaults(gfx)[variant],
|
|
132
147
|
xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
|
|
133
148
|
});
|
|
134
149
|
finish(gfx, id);
|
|
@@ -142,10 +157,15 @@ export function createWardleyNode(gfx, kind) {
|
|
|
142
157
|
const surface = gfx.surface;
|
|
143
158
|
if (!surface)
|
|
144
159
|
return;
|
|
145
|
-
const { w } = WARDLEY_NODE_SIZE[kind];
|
|
146
160
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
147
161
|
const nodeId = addNode(surface, kind, cx, cy);
|
|
148
|
-
const
|
|
162
|
+
const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
|
|
163
|
+
const labelId = addLabel(surface,
|
|
164
|
+
// Resolved HERE, once — the prompt a node nobody has named still carries,
|
|
165
|
+
// exactly like a BPMN node's caption. `gfx.std` is how this file reaches
|
|
166
|
+
// the translation seam: these creation sites take `GfxController`, not
|
|
167
|
+
// `BlockStdScope`, directly (`WardleyView` reads the same member).
|
|
168
|
+
translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign);
|
|
149
169
|
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
150
170
|
}
|
|
151
171
|
/** Create an inertia bar (filled black rect). */
|
|
@@ -170,15 +190,14 @@ export function createWardleyPipeline(gfx) {
|
|
|
170
190
|
if (!gfx.surface)
|
|
171
191
|
return;
|
|
172
192
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
173
|
-
const d = HANDLE_SIZE;
|
|
174
|
-
const top = cy - WARDLEY_NODE_SIZE.pipeline.h / 2;
|
|
175
193
|
// Body: a WardleyNode rect, made non-connectable by `kind: 'pipeline'`.
|
|
176
194
|
const bodyId = addNode(gfx.surface, 'pipeline', cx, cy);
|
|
177
195
|
// Handle: a node-sized WardleyNode square straddling the top edge. Inherits
|
|
178
196
|
// `centerAnchorOnly` so connectors attach to its center only.
|
|
179
197
|
const handleId = gfx.surface.addElement(wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }));
|
|
180
198
|
// Label centered horizontally on the pipeline, sitting ABOVE the handle.
|
|
181
|
-
const
|
|
199
|
+
const { x, y, textAlign } = wardleyLabelBoxFor('pipeline', cx, cy);
|
|
200
|
+
const labelId = addLabel(gfx.surface, translateKey(gfx.std, wardleyNodeLabelKey('pipeline'), WARDLEY_NODE_LABEL.pipeline), x, y, textAlign);
|
|
182
201
|
// Nested groups: (handle + label), then (body + that group).
|
|
183
202
|
const innerId = group(gfx, [handleId, labelId]);
|
|
184
203
|
finish(gfx, group(gfx, [bodyId, innerId]));
|
|
@@ -195,14 +214,14 @@ export function createWardleyMarket(gfx) {
|
|
|
195
214
|
if (!surface)
|
|
196
215
|
return;
|
|
197
216
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
198
|
-
const R = WARDLEY_NODE_SIZE.market.w / 2;
|
|
199
217
|
// Outer circle = the market node (connectable, center-only).
|
|
200
218
|
const circleId = addNode(surface, 'market', cx, cy);
|
|
201
219
|
// 3 inner component nodes (thick border, no label) at the triangle vertices.
|
|
202
220
|
const dotIds = wardleyMarketDotBoxes(cx, cy).map(xywh => surface.addElement(wardleyMarketDotProps({ xywh })));
|
|
203
221
|
// Triangle: 3 attached connectors (auto-route center-to-center, clipped).
|
|
204
222
|
const connIds = wardleyMarketLinkPairs(dotIds).map(([a, b]) => surface.addElement(wardleyMarketLinkProps(a, b)));
|
|
205
|
-
const
|
|
223
|
+
const { x, y, textAlign } = wardleyLabelBoxFor('market', cx, cy);
|
|
224
|
+
const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey('market'), WARDLEY_NODE_LABEL.market), x, y, textAlign);
|
|
206
225
|
finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
|
|
207
226
|
}
|
|
208
227
|
/**
|
|
@@ -259,14 +278,12 @@ export function createWardleyAccelerator(gfx, kind) {
|
|
|
259
278
|
const surface = gfx.surface;
|
|
260
279
|
if (!surface)
|
|
261
280
|
return;
|
|
262
|
-
const { w } = WARDLEY_NODE_SIZE[kind];
|
|
263
281
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
264
282
|
const nodeId = addNode(surface, kind, cx, cy);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
rightwards ? cx + w / 2 + LABEL_GAP : cx - w / 2 - LABEL_GAP - LABEL_W, cy - LABEL_H / 2, rightwards ? 'left' : 'right', FontWeight.SemiBold);
|
|
283
|
+
// Which side, and which alignment, are the placement's answer: the
|
|
284
|
+
// decelerator's mirrored label is one of the two rules `presets.ts` states.
|
|
285
|
+
const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
|
|
286
|
+
const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign, FontWeight.SemiBold);
|
|
270
287
|
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
271
288
|
}
|
|
272
289
|
/**
|
|
@@ -298,6 +315,40 @@ export function createWardleyArea(gfx, shape) {
|
|
|
298
315
|
lowerWardleyArea(gfx, id);
|
|
299
316
|
finish(gfx, id);
|
|
300
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Arm the interactive polygon tool to draw a ZONE, corner by corner.
|
|
320
|
+
*
|
|
321
|
+
* The polygonal half of the kind is a TOOL and the rectangular half an artefact,
|
|
322
|
+
* and the asymmetry is the gesture rather than an inconsistency: a rectangle has
|
|
323
|
+
* one shape whatever its size, so placing one and dragging its handles is the
|
|
324
|
+
* whole of drawing it — while a polygon IS its corners, and a prefabricated
|
|
325
|
+
* pentagon parked in the middle of the map is never the outline anybody wanted
|
|
326
|
+
* (PO decision of 2026-09-16). The author clicks each corner and closes on the
|
|
327
|
+
* first one; Escape cancels and fewer than three corners draws nothing, both of
|
|
328
|
+
* which the tool already decides.
|
|
329
|
+
*
|
|
330
|
+
* What comes back is a Wardley area and not a plain polygon, because the tool
|
|
331
|
+
* creates the element from the props handed here — same fill, rim, role and
|
|
332
|
+
* inner-text settings as {@link createWardleyArea} writes, from the same
|
|
333
|
+
* description — and then lowers it the way that function does.
|
|
334
|
+
*/
|
|
335
|
+
export function activateWardleyAreaPolygon(gfx) {
|
|
336
|
+
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
337
|
+
gfx.tool.setTool(PolygonTool, {
|
|
338
|
+
// The box and the default pentagon in here are a placeholder: the tool
|
|
339
|
+
// spreads this bag before the outline the author drew, so both are
|
|
340
|
+
// overwritten. They are still stated rather than stripped, because what a
|
|
341
|
+
// zone IS has exactly one description (`presets.ts`) and taking two keys
|
|
342
|
+
// out of it here would be a second one.
|
|
343
|
+
props: wardleyAreaProps('polygon', {
|
|
344
|
+
xywh: wardleyAreaBox('polygon', cx, cy),
|
|
345
|
+
}),
|
|
346
|
+
// A zone is lowered the moment it exists, for the reason
|
|
347
|
+
// {@link createWardleyArea} gives at length: drawn last it would paint over
|
|
348
|
+
// every component it groups and intercept their clicks.
|
|
349
|
+
onCreated: lowerWardleyArea,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
301
352
|
/**
|
|
302
353
|
* Send one already-placed area to the depth it belongs at — see
|
|
303
354
|
* {@link createWardleyArea} for why a zone is lowered at all.
|
|
@@ -368,22 +419,10 @@ export function activateWardleyConnector(gfx, kind) {
|
|
|
368
419
|
// fall on a dependency.
|
|
369
420
|
role: kind === 'link' ? WARDLEY_ROLE.dependency : WARDLEY_ROLE.changeArrow,
|
|
370
421
|
// The look rides on the activation, never through the last-props store:
|
|
371
|
-
// the plain connector tool must keep the user's own style (#144 M1).
|
|
372
|
-
style
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
strokeStyle: StrokeStyle.Dash,
|
|
376
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
377
|
-
frontEndpointStyle: PointStyle.None,
|
|
378
|
-
rearEndpointStyle: PointStyle.Triangle,
|
|
379
|
-
}
|
|
380
|
-
: {
|
|
381
|
-
stroke: LINK_GREY,
|
|
382
|
-
strokeStyle: StrokeStyle.Solid,
|
|
383
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
384
|
-
frontEndpointStyle: PointStyle.None,
|
|
385
|
-
rearEndpointStyle: PointStyle.None,
|
|
386
|
-
},
|
|
422
|
+
// the plain connector tool must keep the user's own style (#144 M1). The
|
|
423
|
+
// style itself is {@link WARDLEY_EDGE_STYLE}, so the legend row this tool
|
|
424
|
+
// subscribes pictures the very line the tool draws.
|
|
425
|
+
style: { ...WARDLEY_EDGE_STYLE[kind] },
|
|
387
426
|
});
|
|
388
427
|
// The wardley palette stays open (native sub-menu behaviour): it only
|
|
389
428
|
// closes on re-click of the senior button, another senior tool, or Escape.
|
|
@@ -400,33 +439,74 @@ export function activateWardleyConnector(gfx, kind) {
|
|
|
400
439
|
*/
|
|
401
440
|
const EXPORT_WARNINGS_KEY = 'com.labre.commands.wardley.exportOwm.warnings';
|
|
402
441
|
const EXPORT_WARNINGS_FALLBACK = 'What this export could not write down';
|
|
403
|
-
/**
|
|
404
|
-
export
|
|
405
|
-
|
|
406
|
-
|
|
442
|
+
/**
|
|
443
|
+
* The Wardley maps in the SELECTION — what the export is offered against.
|
|
444
|
+
*
|
|
445
|
+
* No read-only filter, for `c4BoardsForExport`'s reason: an export writes
|
|
446
|
+
* nothing, and a map published read-only is precisely the one somebody wants to
|
|
447
|
+
* take away.
|
|
448
|
+
*/
|
|
449
|
+
export function wardleyMapsSelected(std) {
|
|
450
|
+
return std
|
|
451
|
+
.get(GfxControllerIdentifier)
|
|
452
|
+
.selection.selectedElements.filter((model) => model instanceof WardleyBackgroundElementModel);
|
|
407
453
|
}
|
|
408
454
|
/**
|
|
409
|
-
*
|
|
455
|
+
* The elements the export speaks about, in DOCUMENT order: the selected map(s)
|
|
456
|
+
* and what they wholly contain.
|
|
410
457
|
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
458
|
+
* Membership is `containingFrame` on the map's element bound — whole
|
|
459
|
+
* containment, ties to the smaller id — which is the one answer the validation
|
|
460
|
+
* engine and the legend already give (PF2.4). The candidates are EVERY map on
|
|
461
|
+
* the surface and not just the selected ones, so an artefact inside two
|
|
462
|
+
* overlapping maps is attributed here exactly as the engine attributes it.
|
|
463
|
+
*/
|
|
464
|
+
export function wardleyExportElementsOf(std) {
|
|
465
|
+
const elements = std.get(GfxControllerIdentifier).surface?.elementModels ?? [];
|
|
466
|
+
const allMaps = elements.filter((model) => model instanceof WardleyBackgroundElementModel);
|
|
467
|
+
const selected = new Set(wardleyMapsSelected(std));
|
|
468
|
+
return elements.filter(element => {
|
|
469
|
+
if (element instanceof WardleyBackgroundElementModel) {
|
|
470
|
+
return selected.has(element);
|
|
471
|
+
}
|
|
472
|
+
// ponytail: a label is the NAME of a node, not an artefact of its own, so it
|
|
473
|
+
// crosses UNSCOPED. `matchLabels` (export.ts) only binds one within
|
|
474
|
+
// LABEL_MATCH_TOLERANCE of a kept node and drops the rest, while a long name
|
|
475
|
+
// beside a node near the right edge legitimately hangs over the map bound —
|
|
476
|
+
// scoping labels by containment would export that node unnamed.
|
|
477
|
+
if (element instanceof TextElementModel &&
|
|
478
|
+
element.role === WARDLEY_ROLE.label) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
const map = containingFrame(element.elementBound, allMaps, candidate => candidate.elementBound);
|
|
482
|
+
return map !== null && selected.has(map);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* What the exporter speaks about, as a picked board.
|
|
487
|
+
*
|
|
488
|
+
* The half that needs an editor, and only that half: reading the surface and
|
|
489
|
+
* scoping it to the selection. The picking is {@link wardleyBoardFrom}, which
|
|
490
|
+
* the interchange capability calls with the same elements and no `std` at all
|
|
491
|
+
* (`docs/adr/0012`, P3).
|
|
414
492
|
*/
|
|
415
493
|
export function wardleyBoardOf(std) {
|
|
416
|
-
return wardleyBoardFrom(std
|
|
494
|
+
return wardleyBoardFrom(wardleyExportElementsOf(std));
|
|
417
495
|
}
|
|
418
496
|
/**
|
|
419
|
-
* Serialize the
|
|
497
|
+
* Serialize the selected map as an OWM document and hand it to the browser.
|
|
420
498
|
*
|
|
421
499
|
* Three steps, and only the first and the last know what an editor is: read the
|
|
422
|
-
*
|
|
423
|
-
* step is not re-implemented here — the document, the
|
|
424
|
-
* type all come out of `WARDLEY_OWM_EXPORT.run`, so
|
|
425
|
-
* registry cannot describe the same map differently.
|
|
426
|
-
*
|
|
500
|
+
* selected map's perimeter, run the DECLARED capability, download what it
|
|
501
|
+
* produced. The middle step is not re-implemented here — the document, the
|
|
502
|
+
* filename and the content type all come out of `WARDLEY_OWM_EXPORT.run`, so
|
|
503
|
+
* the command and the registry cannot describe the same map differently.
|
|
504
|
+
*
|
|
505
|
+
* Selecting SEVERAL maps still produces one file: an OWM document is one map,
|
|
506
|
+
* and `exportWardleyOwmWithWarnings` says so out loud in its warnings.
|
|
427
507
|
*/
|
|
428
508
|
export function exportOwmFile(std) {
|
|
429
|
-
const elements = std
|
|
509
|
+
const elements = wardleyExportElementsOf(std);
|
|
430
510
|
const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
|
|
431
511
|
const { text, filename, mime, warnings } = WARDLEY_OWM_EXPORT.run(elements, {
|
|
432
512
|
name: wardleySafeFilename(title),
|
package/dist/commands.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
-
import { activateWardleyConnector, createWardleyAccelerator, createWardleyArea, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, createWardleyPorter, exportOwmFile, importOwmFile, importWardleySvgFile,
|
|
2
|
+
import { activateWardleyAreaPolygon, activateWardleyConnector, createWardleyAccelerator, createWardleyArea, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, createWardleyPorter, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsSelected, } from './actions.js';
|
|
3
|
+
import { WARDLEY_LEGEND_BOX, wardleyLegendEntry, } from './legend.js';
|
|
3
4
|
import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.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
|
+
import { wardleyAcceleratorIcon, wardleyAnchorIcon, wardleyAreaPolygonIcon, wardleyAreaRectIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyDeceleratorIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, wardleyPorterIcon, wardleyToolbarIcon, } from './toolbar/icons.js';
|
|
5
6
|
const SPECS = [
|
|
6
7
|
{
|
|
7
8
|
id: 'addBackground',
|
|
@@ -47,6 +48,7 @@ const SPECS = [
|
|
|
47
48
|
},
|
|
48
49
|
{
|
|
49
50
|
id: 'addComponent',
|
|
51
|
+
legendRole: WARDLEY_ROLE.component,
|
|
50
52
|
label: 'Component',
|
|
51
53
|
labelKey: 'com.labre.keyboardShortcuts.wardley.addComponent',
|
|
52
54
|
key: 'c',
|
|
@@ -58,6 +60,7 @@ const SPECS = [
|
|
|
58
60
|
},
|
|
59
61
|
{
|
|
60
62
|
id: 'addMethod',
|
|
63
|
+
legendRole: WARDLEY_ROLE.method,
|
|
61
64
|
label: 'Component + method',
|
|
62
65
|
labelKey: 'com.labre.keyboardShortcuts.wardley.addMethod',
|
|
63
66
|
key: 'm',
|
|
@@ -69,6 +72,7 @@ const SPECS = [
|
|
|
69
72
|
},
|
|
70
73
|
{
|
|
71
74
|
id: 'addMarket',
|
|
75
|
+
legendRole: WARDLEY_ROLE.market,
|
|
72
76
|
label: 'Market',
|
|
73
77
|
iconKey: 'wardley.market',
|
|
74
78
|
category: 'nodes',
|
|
@@ -78,6 +82,7 @@ const SPECS = [
|
|
|
78
82
|
},
|
|
79
83
|
{
|
|
80
84
|
id: 'addEcosystem',
|
|
85
|
+
legendRole: WARDLEY_ROLE.ecosystem,
|
|
81
86
|
label: 'Ecosystem',
|
|
82
87
|
iconKey: 'wardley.ecosystem',
|
|
83
88
|
category: 'nodes',
|
|
@@ -87,6 +92,7 @@ const SPECS = [
|
|
|
87
92
|
},
|
|
88
93
|
{
|
|
89
94
|
id: 'addAnchor',
|
|
95
|
+
legendRole: WARDLEY_ROLE.anchor,
|
|
90
96
|
label: 'Anchor',
|
|
91
97
|
key: 'a',
|
|
92
98
|
iconKey: 'wardley.anchor',
|
|
@@ -97,6 +103,7 @@ const SPECS = [
|
|
|
97
103
|
},
|
|
98
104
|
{
|
|
99
105
|
id: 'addPipeline',
|
|
106
|
+
legendRole: WARDLEY_ROLE.pipeline,
|
|
100
107
|
label: 'Pipeline',
|
|
101
108
|
labelKey: 'com.labre.keyboardShortcuts.wardley.addPipeline',
|
|
102
109
|
key: 'p',
|
|
@@ -108,6 +115,7 @@ const SPECS = [
|
|
|
108
115
|
},
|
|
109
116
|
{
|
|
110
117
|
id: 'linkTool',
|
|
118
|
+
legendRole: WARDLEY_ROLE.dependency,
|
|
111
119
|
label: 'Link',
|
|
112
120
|
labelKey: 'com.labre.keyboardShortcuts.wardley.linkTool',
|
|
113
121
|
key: 'l',
|
|
@@ -120,6 +128,7 @@ const SPECS = [
|
|
|
120
128
|
},
|
|
121
129
|
{
|
|
122
130
|
id: 'evolutionArrow',
|
|
131
|
+
legendRole: WARDLEY_ROLE.changeArrow,
|
|
123
132
|
label: 'Arrow (evolution)',
|
|
124
133
|
labelKey: 'com.labre.keyboardShortcuts.wardley.evolutionArrow',
|
|
125
134
|
key: 'e',
|
|
@@ -132,6 +141,7 @@ const SPECS = [
|
|
|
132
141
|
},
|
|
133
142
|
{
|
|
134
143
|
id: 'addInertia',
|
|
144
|
+
legendRole: WARDLEY_ROLE.inertia,
|
|
135
145
|
label: 'Inertia',
|
|
136
146
|
labelKey: 'com.labre.keyboardShortcuts.wardley.addInertia',
|
|
137
147
|
key: 'i',
|
|
@@ -148,6 +158,7 @@ const SPECS = [
|
|
|
148
158
|
// a framework binds past that by host override rather than by default.
|
|
149
159
|
{
|
|
150
160
|
id: 'addPorter',
|
|
161
|
+
legendRole: WARDLEY_ROLE.porter,
|
|
151
162
|
label: "Porter's forces",
|
|
152
163
|
iconKey: 'wardley.porter',
|
|
153
164
|
category: 'nodes',
|
|
@@ -162,6 +173,7 @@ const SPECS = [
|
|
|
162
173
|
// past that by host override.
|
|
163
174
|
{
|
|
164
175
|
id: 'addAccelerator',
|
|
176
|
+
legendRole: WARDLEY_ROLE.accelerator,
|
|
165
177
|
label: 'Accelerator',
|
|
166
178
|
iconKey: 'wardley.accelerator',
|
|
167
179
|
category: 'nodes',
|
|
@@ -171,6 +183,7 @@ const SPECS = [
|
|
|
171
183
|
},
|
|
172
184
|
{
|
|
173
185
|
id: 'addDecelerator',
|
|
186
|
+
legendRole: WARDLEY_ROLE.decelerator,
|
|
174
187
|
label: 'Decelerator',
|
|
175
188
|
iconKey: 'wardley.decelerator',
|
|
176
189
|
category: 'nodes',
|
|
@@ -182,12 +195,14 @@ const SPECS = [
|
|
|
182
195
|
// around a chain that already exists, so it is the last gesture of a map and
|
|
183
196
|
// reads as the last entry of the row. Two entries and ONE kind — the shape is
|
|
184
197
|
// the choice, and offering it as two buttons is what saves the author a trip
|
|
185
|
-
// through "switch type" for the commonest case.
|
|
198
|
+
// through "switch type" for the commonest case. One kind, but two GESTURES:
|
|
199
|
+
// the rectangle is placed, the polygon is drawn corner by corner.
|
|
186
200
|
//
|
|
187
201
|
// Keyless, like the porter and the two climate arrows: the `w` chord seats
|
|
188
202
|
// eight artefacts and a framework binds past that by host override.
|
|
189
203
|
{
|
|
190
204
|
id: 'addAreaRect',
|
|
205
|
+
legendRole: WARDLEY_ROLE.area,
|
|
191
206
|
label: 'Area (rectangle)',
|
|
192
207
|
iconKey: 'wardley.area-rect',
|
|
193
208
|
category: 'areas',
|
|
@@ -197,12 +212,20 @@ const SPECS = [
|
|
|
197
212
|
},
|
|
198
213
|
{
|
|
199
214
|
id: 'addAreaPolygon',
|
|
215
|
+
legendRole: WARDLEY_ROLE.area,
|
|
200
216
|
label: 'Area (polygon)',
|
|
201
217
|
iconKey: 'wardley.area-polygon',
|
|
202
218
|
category: 'areas',
|
|
203
|
-
|
|
219
|
+
// A TOOL, and the only entry of this row that is one: a polygon IS its
|
|
220
|
+
// corners, so the author draws them rather than receiving a pentagon to
|
|
221
|
+
// reshape (PO decision of 2026-09-16, #343). `element` is untouched — the
|
|
222
|
+
// artefact this gesture produces is the same one — and the change of kind
|
|
223
|
+
// is what moves its event from `FrameworkElementAdded` to
|
|
224
|
+
// `FrameworkToolPicked`, which is the truth about a tool: it is picked, and
|
|
225
|
+
// the drawing may still be abandoned with Escape.
|
|
226
|
+
kind: 'tool',
|
|
204
227
|
element: 'node:area-polygon',
|
|
205
|
-
run:
|
|
228
|
+
run: activateWardleyAreaPolygon,
|
|
206
229
|
},
|
|
207
230
|
];
|
|
208
231
|
/**
|
|
@@ -239,6 +262,13 @@ const toolboxCommands = SPECS.map((spec, order) => ({
|
|
|
239
262
|
availability: 'always',
|
|
240
263
|
run: std => spec.run(std.get(GfxControllerIdentifier)),
|
|
241
264
|
telemetry: { framework: 'wardley', element: spec.element, board: spec.board },
|
|
265
|
+
// The legend, SUBSCRIBED rather than tabulated (`docs/adr/0009`'s sibling):
|
|
266
|
+
// the command that draws the artefact names the row that pictures it, and
|
|
267
|
+
// the sheet's own command carries the box the rows are drawn in. Which
|
|
268
|
+
// sub-title a row files under is `category`, so the sections are the very
|
|
269
|
+
// groups the catalogue already shows.
|
|
270
|
+
...(spec.legendRole ? { legend: wardleyLegendEntry(spec.legendRole) } : {}),
|
|
271
|
+
...(spec.id === 'addBackground' ? { legendBox: WARDLEY_LEGEND_BOX } : {}),
|
|
242
272
|
}));
|
|
243
273
|
/**
|
|
244
274
|
* The IMPORT — the first Wardley command whose subject is a whole map rather
|
|
@@ -290,30 +320,18 @@ const importCommand = {
|
|
|
290
320
|
/**
|
|
291
321
|
* The EXPORT — the other direction of the same format.
|
|
292
322
|
*
|
|
293
|
-
* It
|
|
294
|
-
*
|
|
295
|
-
* export is what you do to a board
|
|
296
|
-
*
|
|
297
|
-
* command missing from the catalogue is unreachable the moment its framework
|
|
298
|
-
* overflows the fourteen slots — plus the palette and the agent. No
|
|
299
|
-
* `'contextual-toolbar'` either, and that is a declaration rather than an
|
|
300
|
-
* oversight: a contextual-toolbar surface is rendered by an element's own
|
|
301
|
-
* `ToolbarModuleConfig`, and declaring one nothing invokes would put an entry
|
|
302
|
-
* in the manifest that no toolbar draws.
|
|
323
|
+
* It mirrors `bpmn.exportXml` and `c4.exportMermaid` (R5 of
|
|
324
|
+
* `docs/add-a-framework/02-framework-rules.md`): an import is where a board
|
|
325
|
+
* comes FROM, so it sits in the sub-menu; an export is what you do to a board
|
|
326
|
+
* you already have, so it sits in the selected map's "⋮" (`toolbar/config.ts`).
|
|
303
327
|
*
|
|
304
|
-
* `'
|
|
305
|
-
*
|
|
306
|
-
* read-only document —
|
|
307
|
-
* away. What it DOES need is a plot to measure coordinates against, and that is
|
|
308
|
-
* a fact about the surface rather than about the selection: a Wardley node has
|
|
309
|
-
* no `visibility` prop, so its position on the plot IS its coordinate, and with
|
|
310
|
-
* no map there is nothing to invert. `'selection'` would be BPMN's shape copied
|
|
311
|
-
* for the look of it — that command's precondition genuinely is a selected
|
|
312
|
-
* pool, and this one's is not.
|
|
328
|
+
* An export READS, so `availability: 'selection'` with a `when` on the selected
|
|
329
|
+
* MAP is the whole precondition: it is offered on a locked map and on a
|
|
330
|
+
* read-only document — precisely the map somebody wants to take away.
|
|
313
331
|
*
|
|
314
|
-
* **
|
|
315
|
-
*
|
|
316
|
-
* in its warnings.
|
|
332
|
+
* **Scope: the selected map's perimeter.** The file holds that map and what it
|
|
333
|
+
* wholly contains ({@link wardleyExportElementsOf}). Selecting several maps
|
|
334
|
+
* still writes one map, and the export says so out loud in its warnings.
|
|
317
335
|
*/
|
|
318
336
|
const exportCommand = {
|
|
319
337
|
id: 'wardley.exportOwm',
|
|
@@ -322,17 +340,17 @@ const exportCommand = {
|
|
|
322
340
|
labelKey: 'com.labre.commands.wardley.exportOwm',
|
|
323
341
|
labelFallback: 'Export Wardley map (OWM)',
|
|
324
342
|
descriptionKey: 'com.labre.commands.wardley.exportOwm.description',
|
|
325
|
-
descriptionFallback: 'Download the map as an OnlineWardleyMaps .owm file, ready to open in any Wardley mapping tool.',
|
|
343
|
+
descriptionFallback: 'Download the selected map as an OnlineWardleyMaps .owm file — the components inside its perimeter, ready to open in any Wardley mapping tool.',
|
|
326
344
|
category: 'interchange',
|
|
327
345
|
iconKey: 'wardley.export-owm',
|
|
328
|
-
surfaces: ['catalogue', 'palette', 'agent'],
|
|
346
|
+
surfaces: ['catalogue', 'contextual-toolbar', 'palette', 'agent'],
|
|
329
347
|
order: SPECS.length + 1,
|
|
330
348
|
scope: 'edgeless',
|
|
331
349
|
defaultKeys: { mac: [], other: [] },
|
|
332
|
-
availability: '
|
|
350
|
+
availability: 'selection',
|
|
333
351
|
run: exportOwmFile,
|
|
334
352
|
telemetry: { framework: 'wardley', element: 'board:export-owm' },
|
|
335
|
-
when: std =>
|
|
353
|
+
when: std => wardleyMapsSelected(std).length > 0,
|
|
336
354
|
};
|
|
337
355
|
/**
|
|
338
356
|
* The SVG FALLBACK import — the visual tier, named as such before the picker
|
|
@@ -439,4 +457,7 @@ export const wardleyCommandIcons = {
|
|
|
439
457
|
'wardley.import-owm': wardleyImportOwmIcon,
|
|
440
458
|
'wardley.export-owm': wardleyExportOwmIcon,
|
|
441
459
|
'wardley.import-svg': wardleyImportSvgIcon,
|
|
460
|
+
// The senior button's 56×56 glyph, so `FrameworkDescriptor.iconKey` resolves
|
|
461
|
+
// through `getCommandIcon`.
|
|
462
|
+
'wardley.toolbar': wardleyToolbarIcon,
|
|
442
463
|
};
|