@formicoidea/labre-framework-wardley 0.31.0 → 0.33.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 +63 -20
- package/dist/actions.js +190 -57
- package/dist/audit-criteria.d.ts +31 -0
- package/dist/audit-criteria.js +90 -0
- package/dist/background.d.ts +14 -0
- package/dist/background.js +338 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +352 -0
- package/dist/consts.d.ts +10 -4
- package/dist/consts.js +10 -9
- package/dist/descriptor.d.ts +8 -3
- package/dist/descriptor.js +6 -3
- package/dist/element-renderer.d.ts +8 -5
- package/dist/element-renderer.js +12 -147
- package/dist/element-view.d.ts +8 -4
- package/dist/element-view.js +30 -22
- package/dist/export.d.ts +211 -0
- package/dist/export.js +655 -0
- package/dist/gradient.d.ts +6 -11
- package/dist/gradient.js +59 -48
- package/dist/import.d.ts +116 -0
- package/dist/import.js +905 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.js +31 -1
- package/dist/interchange.d.ts +80 -0
- package/dist/interchange.js +138 -0
- package/dist/legend.js +8 -0
- package/dist/natures.d.ts +50 -0
- package/dist/natures.js +93 -0
- package/dist/node/node-renderer.js +1 -1
- package/dist/nudges.d.ts +41 -0
- package/dist/nudges.js +69 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +87 -0
- package/dist/reading.d.ts +3 -0
- package/dist/reading.js +129 -0
- package/dist/roles.d.ts +50 -0
- package/dist/roles.js +132 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +286 -0
- package/dist/templates/index.js +66 -10
- package/dist/templates/maps.js +146 -22
- package/dist/toolbar/config.js +3 -1
- package/dist/toolbar/icons.d.ts +20 -0
- package/dist/toolbar/icons.js +34 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/toolbar/wardley-menu.d.ts +8 -15
- package/dist/toolbar/wardley-menu.js +8 -136
- package/dist/toolbar/wardley-senior-button.js +12 -6
- package/dist/translations.d.ts +16 -0
- package/dist/translations.js +24 -0
- package/dist/view.d.ts +17 -0
- package/dist/view.js +127 -13
- package/package.json +2 -2
- package/dist/label-layout.d.ts +0 -20
- package/dist/label-layout.js +0 -72
- package/dist/shortcuts.d.ts +0 -2
- package/dist/shortcuts.js +0 -37
package/dist/actions.d.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import { type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* invoked from both the toolbar menu ({@link EdgelessWardleyMenu}) and the
|
|
6
|
-
* wardley keyboard shortcuts, so they only depend on the {@link GfxController}.
|
|
7
|
-
*/
|
|
8
|
-
/** Where the action was triggered from — feeds the telemetry payload. */
|
|
9
|
-
export interface WardleyActionSource {
|
|
10
|
-
segment: string;
|
|
11
|
-
module: string;
|
|
12
|
-
}
|
|
13
|
-
export declare const WARDLEY_SHORTCUT_SOURCE: WardleyActionSource;
|
|
1
|
+
import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
+
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
3
|
+
import { type GfxController } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
import { type WardleyExportBoard } from './export.js';
|
|
14
5
|
/**
|
|
15
6
|
* The single-circle node flavours: one connectable ellipse + a label to its
|
|
16
7
|
* right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
|
|
@@ -38,17 +29,24 @@ declare const NODE_PRESETS: {
|
|
|
38
29
|
readonly label: "Component";
|
|
39
30
|
};
|
|
40
31
|
};
|
|
41
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The subset of {@link WardleyNodeKind} that the single-circle presets above
|
|
34
|
+
* cover. Renamed away from `WardleyNodeKind` on purpose: the model declares a
|
|
35
|
+
* type of that name with SEVEN values, and two homonyms of different
|
|
36
|
+
* cardinality — one of them now the source of the semantic vocabulary — is a
|
|
37
|
+
* trap. `WARDLEY_ROLE[kind]` below only compiles while this stays a subset.
|
|
38
|
+
*/
|
|
39
|
+
export type WardleySingleCircleKind = keyof typeof NODE_PRESETS;
|
|
42
40
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
43
|
-
export declare function createWardleyBackground(gfx: GfxController, variant?: WardleyBgVariant
|
|
41
|
+
export declare function createWardleyBackground(gfx: GfxController, variant?: WardleyBgVariant): void;
|
|
44
42
|
/**
|
|
45
43
|
* Create a single-circle node (component / anchor / ecosystem / method):
|
|
46
44
|
* one connectable native ellipse + a label to its right, grouped so they
|
|
47
45
|
* move together (enter the group to reposition / edit the label).
|
|
48
46
|
*/
|
|
49
|
-
export declare function createWardleyNode(gfx: GfxController, kind:
|
|
47
|
+
export declare function createWardleyNode(gfx: GfxController, kind: WardleySingleCircleKind): void;
|
|
50
48
|
/** Create an inertia bar (filled black rect). */
|
|
51
|
-
export declare function createWardleyInertia(gfx: GfxController
|
|
49
|
+
export declare function createWardleyInertia(gfx: GfxController): void;
|
|
52
50
|
/**
|
|
53
51
|
* Create a pipeline: a wide thin native rect body (white semi-transparent,
|
|
54
52
|
* NON-connectable) + a node-sized square handle straddling its top edge (the
|
|
@@ -56,7 +54,7 @@ export declare function createWardleyInertia(gfx: GfxController, source?: Wardle
|
|
|
56
54
|
* label are grouped, then grouped again with the body so the whole pipeline
|
|
57
55
|
* moves as one. Pure composition of native elements — no custom type / view.
|
|
58
56
|
*/
|
|
59
|
-
export declare function createWardleyPipeline(gfx: GfxController
|
|
57
|
+
export declare function createWardleyPipeline(gfx: GfxController): void;
|
|
60
58
|
/**
|
|
61
59
|
* Create a market: a large thin-bordered circle (the connectable market node)
|
|
62
60
|
* containing 3 small thick-bordered component nodes wired into a triangle by
|
|
@@ -64,11 +62,56 @@ export declare function createWardleyPipeline(gfx: GfxController, source?: Wardl
|
|
|
64
62
|
* the node centers and follow on move/resize). A label sits to the right and
|
|
65
63
|
* everything is grouped into one object.
|
|
66
64
|
*/
|
|
67
|
-
export declare function createWardleyMarket(gfx: GfxController
|
|
65
|
+
export declare function createWardleyMarket(gfx: GfxController): void;
|
|
66
|
+
/**
|
|
67
|
+
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
68
|
+
*
|
|
69
|
+
* Wardley's first interchange command, and its whole implementation: the four
|
|
70
|
+
* steps live in {@link runInterchangeImportFile} and the reading lives in the
|
|
71
|
+
* declared capability, so this framework contributes a declaration and a
|
|
72
|
+
* label rather than a pipeline (`docs/adr/0012`, P1 and P3).
|
|
73
|
+
*
|
|
74
|
+
* It takes a `BlockStdScope` and not the `GfxController` the rest of this file
|
|
75
|
+
* runs on, because an import is not a drawing gesture: it opens a picker,
|
|
76
|
+
* writes a whole board in one undo step, moves the viewport and notifies —
|
|
77
|
+
* none of which a `GfxController` alone can do.
|
|
78
|
+
*/
|
|
79
|
+
export declare function importWardleySvgFile(std: BlockStdScope): Promise<void>;
|
|
68
80
|
/**
|
|
69
81
|
* Activate the native connector tool, pre-styled for a Wardley link (grey,
|
|
70
82
|
* solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
|
|
71
83
|
* user then draws from one node to another (endpoints attach to centers).
|
|
72
84
|
*/
|
|
73
|
-
export declare function activateWardleyConnector(gfx: GfxController, kind: 'link' | 'arrow'
|
|
85
|
+
export declare function activateWardleyConnector(gfx: GfxController, kind: 'link' | 'arrow'): void;
|
|
86
|
+
/** The maps on the surface — what the export command is offered against. */
|
|
87
|
+
export declare function wardleyMapsOnBoard(std: BlockStdScope): WardleyBackgroundElementModel[];
|
|
88
|
+
/**
|
|
89
|
+
* Everything on the surface the exporter speaks about, in document order.
|
|
90
|
+
*
|
|
91
|
+
* The half that needs an editor, and only that half: reading the surface. The
|
|
92
|
+
* picking is {@link wardleyBoardFrom}, which the interchange capability calls
|
|
93
|
+
* with the same elements and no `std` at all (`docs/adr/0012`, P3).
|
|
94
|
+
*/
|
|
95
|
+
export declare function wardleyBoardOf(std: BlockStdScope): WardleyExportBoard;
|
|
96
|
+
/**
|
|
97
|
+
* Serialize the board as an OWM document and hand it to the browser.
|
|
98
|
+
*
|
|
99
|
+
* Three steps, and only the first and the last know what an editor is: read the
|
|
100
|
+
* surface, run the DECLARED capability, download what it produced. The middle
|
|
101
|
+
* step is not re-implemented here — the document, the filename and the content
|
|
102
|
+
* type all come out of `WARDLEY_OWM_EXPORT.run`, so the command and the
|
|
103
|
+
* registry cannot describe the same map differently. There is one door, and the
|
|
104
|
+
* registry is the label on it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function exportOwmFile(std: BlockStdScope): void;
|
|
107
|
+
/**
|
|
108
|
+
* Read an `.owm` the user picks, draw it, and say what it cost.
|
|
109
|
+
*
|
|
110
|
+
* The whole of the import glue, and it is one line: the generic pipeline picks
|
|
111
|
+
* the file from the format's own declaration, runs the capability, mints the
|
|
112
|
+
* surface ids and repairs the link endpoints that named the file's names,
|
|
113
|
+
* brings the map into view and reports. Nothing in it is about Wardley except
|
|
114
|
+
* the capability handed to it.
|
|
115
|
+
*/
|
|
116
|
+
export declare function importOwmFile(std: BlockStdScope): Promise<void>;
|
|
74
117
|
export {};
|
package/dist/actions.js
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
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, } from '@formicoidea/labre-core/model';
|
|
5
|
-
import {
|
|
4
|
+
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
5
|
+
import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
6
|
+
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
6
7
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
7
|
-
import {
|
|
8
|
+
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
9
|
+
import { WARDLEY_BACKGROUND } from './background.js';
|
|
10
|
+
import { WARDLEY_SVG_IMPORT } from './interchange.js';
|
|
11
|
+
import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
|
|
12
|
+
import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
|
|
8
13
|
import { ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_DEFAULT, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, WARDLEY_RED, } from './node/consts.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import { WARDLEY_ROLE } from './roles.js';
|
|
15
|
+
/**
|
|
16
|
+
* Standalone creation/activation actions for the Wardley toolbox — the
|
|
17
|
+
* BEHAVIOUR layer, shared by every surface. They only depend on the
|
|
18
|
+
* {@link GfxController}.
|
|
19
|
+
*
|
|
20
|
+
* They no longer emit telemetry: since PF3 the single emission point is the
|
|
21
|
+
* command registry's `runCommand` (`docs/adr/0008`), which is the only function
|
|
22
|
+
* every surface goes through. `WardleyActionSource` is gone with it — the
|
|
23
|
+
* segment/module discrimination it carried is now `CommandInvocation.surface`.
|
|
24
|
+
*/
|
|
17
25
|
/**
|
|
18
26
|
* Per-variant default label overrides applied at creation (all remain editable
|
|
19
27
|
* afterwards via the inline editor / toggles). The gradient itself is driven by
|
|
20
28
|
* `variant` in the renderer.
|
|
29
|
+
*
|
|
30
|
+
* TODO(PF2 follow-up): these write ENGLISH PROSE into the document. Since PF2
|
|
31
|
+
* the ten label props default to `undefined` precisely so the declaration's
|
|
32
|
+
* i18n keys are reachable — but a map created as `opportunity` or `benefit`
|
|
33
|
+
* lands with "Opportunity" / "Benefit" / "Investment" already persisted as if
|
|
34
|
+
* the user had typed them, so those three are un-localisable for the life of
|
|
35
|
+
* the document. Only `classic` and `evolution-gradient` are fully localisable
|
|
36
|
+
* today.
|
|
37
|
+
*
|
|
38
|
+
* The fix is to make the variant part of the declaration — one axis/end-label
|
|
39
|
+
* set per variant, each naming its own `labelKey` — rather than a bag of prop
|
|
40
|
+
* overrides applied at creation. Out of scope here (it changes what a variant
|
|
41
|
+
* IS); duplicated verbatim in `templates/index.ts`, and both copies go away
|
|
42
|
+
* together.
|
|
21
43
|
*/
|
|
22
44
|
const BACKGROUND_VARIANT_DEFAULTS = {
|
|
23
45
|
classic: {},
|
|
@@ -55,14 +77,6 @@ const NODE_PRESETS = {
|
|
|
55
77
|
// Method: the FILL color encodes the chosen method (editable).
|
|
56
78
|
method: { d: METHOD_SIZE, fill: METHOD_FILL, label: METHOD_LABEL },
|
|
57
79
|
};
|
|
58
|
-
function track(gfx, source, event, element) {
|
|
59
|
-
gfx.std.getOptional(TelemetryProvider)?.track(event, {
|
|
60
|
-
framework: 'wardley',
|
|
61
|
-
element,
|
|
62
|
-
page: 'whiteboard editor',
|
|
63
|
-
...source,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
80
|
function finish(gfx, id) {
|
|
67
81
|
gfx.doc.captureSync();
|
|
68
82
|
gfx.tool.setTool(DefaultTool);
|
|
@@ -78,10 +92,18 @@ function group(gfx, ids) {
|
|
|
78
92
|
return result.groupId || ids[0];
|
|
79
93
|
}
|
|
80
94
|
/** Add a native ellipse wardley node centred on (cx, cy). */
|
|
81
|
-
function addEllipseNode(surface, kind, cx, cy, d, fillColor, strokeWidth = NODE_STROKE_WIDTH
|
|
95
|
+
function addEllipseNode(surface, kind, cx, cy, d, fillColor, strokeWidth = NODE_STROKE_WIDTH,
|
|
96
|
+
// The market's three inner dots are the GLYPH's own wiring, not artefacts the
|
|
97
|
+
// user placed — the same reason its triangle connectors carry no role. They
|
|
98
|
+
// sit inside the market circle by construction, so a role would make every
|
|
99
|
+
// market composite report an overlap with itself (W3).
|
|
100
|
+
neutral = false) {
|
|
82
101
|
return surface.addElement({
|
|
83
102
|
type: 'wardleyNode',
|
|
84
103
|
kind,
|
|
104
|
+
// Semantic identity (PF1): posted next to `kind`, which stays untouched
|
|
105
|
+
// and keeps driving the rendering.
|
|
106
|
+
role: neutral ? undefined : WARDLEY_ROLE[kind],
|
|
85
107
|
shapeType: 'ellipse',
|
|
86
108
|
filled: true,
|
|
87
109
|
fillColor,
|
|
@@ -97,6 +119,11 @@ function addLabel(surface, text, x, y, textAlign = 'left') {
|
|
|
97
119
|
return surface.addElement({
|
|
98
120
|
type: 'text',
|
|
99
121
|
text,
|
|
122
|
+
// Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
|
|
123
|
+
// text element like any other, so its ROLE is the only thing that tells W3
|
|
124
|
+
// it must not land on top of a node. A free text the user typed elsewhere
|
|
125
|
+
// stays neutral and is never evaluated.
|
|
126
|
+
role: WARDLEY_ROLE.label,
|
|
100
127
|
fontFamily: FontFamily.Inter,
|
|
101
128
|
fontSize: LABEL_FONT_SIZE,
|
|
102
129
|
color: NODE_STROKE,
|
|
@@ -105,23 +132,34 @@ function addLabel(surface, text, x, y, textAlign = 'left') {
|
|
|
105
132
|
});
|
|
106
133
|
}
|
|
107
134
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
108
|
-
export function createWardleyBackground(gfx, variant = 'classic'
|
|
135
|
+
export function createWardleyBackground(gfx, variant = 'classic') {
|
|
109
136
|
if (!gfx.surface)
|
|
110
137
|
return;
|
|
111
|
-
|
|
138
|
+
// A second map matches the biggest one already on the board rather than
|
|
139
|
+
// shrinking beside it. The reference size and the locked 16:9 proportion are
|
|
140
|
+
// the declaration's (`geometry`), not this function's.
|
|
141
|
+
let atLeastWidth = 0;
|
|
142
|
+
let atLeastHeight = 0;
|
|
112
143
|
for (const el of gfx.surface.getElementsByType('wardley')) {
|
|
113
144
|
const [, , ew, eh] = el.deserializedXYWH;
|
|
114
|
-
|
|
145
|
+
atLeastWidth = Math.max(atLeastWidth, ew);
|
|
146
|
+
atLeastHeight = Math.max(atLeastHeight, eh);
|
|
115
147
|
}
|
|
116
|
-
const height = (
|
|
148
|
+
const { width, height } = backgroundSize(WARDLEY_BACKGROUND, atLeastWidth, atLeastHeight);
|
|
117
149
|
const { centerX, centerY } = gfx.viewport;
|
|
118
150
|
const id = gfx.surface.addElement({
|
|
119
|
-
type:
|
|
151
|
+
type: WARDLEY_BACKGROUND.type,
|
|
152
|
+
// The map is a first-class role: validation rules position artefacts
|
|
153
|
+
// against `wardley:map`, never against the `wardley` element type. The
|
|
154
|
+
// declaration owns it, so a templated map and a hand-drawn one agree.
|
|
155
|
+
role: WARDLEY_BACKGROUND.role,
|
|
156
|
+
// A map is a frame you place things on, not a shape you nudge — the
|
|
157
|
+
// declaration decides, and the toolbar toggle takes over from there.
|
|
158
|
+
resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
|
|
120
159
|
variant,
|
|
121
160
|
...BACKGROUND_VARIANT_DEFAULTS[variant],
|
|
122
161
|
xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
|
|
123
162
|
});
|
|
124
|
-
track(gfx, source, 'FrameworkElementAdded', `background:${variant}`);
|
|
125
163
|
finish(gfx, id);
|
|
126
164
|
}
|
|
127
165
|
/**
|
|
@@ -129,7 +167,7 @@ export function createWardleyBackground(gfx, variant = 'classic', source = TOOLB
|
|
|
129
167
|
* one connectable native ellipse + a label to its right, grouped so they
|
|
130
168
|
* move together (enter the group to reposition / edit the label).
|
|
131
169
|
*/
|
|
132
|
-
export function createWardleyNode(gfx, kind
|
|
170
|
+
export function createWardleyNode(gfx, kind) {
|
|
133
171
|
const surface = gfx.surface;
|
|
134
172
|
if (!surface)
|
|
135
173
|
return;
|
|
@@ -137,11 +175,10 @@ export function createWardleyNode(gfx, kind, source = TOOLBOX_SOURCE) {
|
|
|
137
175
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
138
176
|
const nodeId = addEllipseNode(surface, kind, cx, cy, d, fill);
|
|
139
177
|
const labelId = addLabel(surface, label, cx + d / 2 + LABEL_GAP, cy - LABEL_H / 2);
|
|
140
|
-
track(gfx, source, 'FrameworkElementAdded', `node:${kind}`);
|
|
141
178
|
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
142
179
|
}
|
|
143
180
|
/** Create an inertia bar (filled black rect). */
|
|
144
|
-
export function createWardleyInertia(gfx
|
|
181
|
+
export function createWardleyInertia(gfx) {
|
|
145
182
|
if (!gfx.surface)
|
|
146
183
|
return;
|
|
147
184
|
const { w, h } = INERTIA_SIZE;
|
|
@@ -149,6 +186,9 @@ export function createWardleyInertia(gfx, source = TOOLBOX_SOURCE) {
|
|
|
149
186
|
const id = gfx.surface.addElement({
|
|
150
187
|
type: 'shape',
|
|
151
188
|
shapeType: 'rect',
|
|
189
|
+
// The inertia bar has no element type of its own — it IS a plain filled
|
|
190
|
+
// rect — so the role is the whole of its semantics (PF13.5).
|
|
191
|
+
role: WARDLEY_ROLE.inertia,
|
|
152
192
|
filled: true,
|
|
153
193
|
fillColor: INERTIA_COLOR,
|
|
154
194
|
strokeColor: INERTIA_COLOR,
|
|
@@ -160,7 +200,6 @@ export function createWardleyInertia(gfx, source = TOOLBOX_SOURCE) {
|
|
|
160
200
|
textFitMode: TextFitMode.Overflow,
|
|
161
201
|
xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
|
|
162
202
|
});
|
|
163
|
-
track(gfx, source, 'FrameworkElementAdded', 'node:inertia');
|
|
164
203
|
finish(gfx, id);
|
|
165
204
|
}
|
|
166
205
|
/**
|
|
@@ -170,7 +209,7 @@ export function createWardleyInertia(gfx, source = TOOLBOX_SOURCE) {
|
|
|
170
209
|
* label are grouped, then grouped again with the body so the whole pipeline
|
|
171
210
|
* moves as one. Pure composition of native elements — no custom type / view.
|
|
172
211
|
*/
|
|
173
|
-
export function createWardleyPipeline(gfx
|
|
212
|
+
export function createWardleyPipeline(gfx) {
|
|
174
213
|
if (!gfx.surface)
|
|
175
214
|
return;
|
|
176
215
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
@@ -182,6 +221,7 @@ export function createWardleyPipeline(gfx, source = TOOLBOX_SOURCE) {
|
|
|
182
221
|
const bodyId = gfx.surface.addElement({
|
|
183
222
|
type: 'wardleyNode',
|
|
184
223
|
kind: 'pipeline',
|
|
224
|
+
role: WARDLEY_ROLE.pipeline,
|
|
185
225
|
shapeType: 'rect',
|
|
186
226
|
filled: true,
|
|
187
227
|
fillColor: PIPELINE_FILL,
|
|
@@ -197,6 +237,7 @@ export function createWardleyPipeline(gfx, source = TOOLBOX_SOURCE) {
|
|
|
197
237
|
const handleId = gfx.surface.addElement({
|
|
198
238
|
type: 'wardleyNode',
|
|
199
239
|
kind: 'handle',
|
|
240
|
+
role: WARDLEY_ROLE.handle,
|
|
200
241
|
shapeType: 'rect',
|
|
201
242
|
filled: true,
|
|
202
243
|
fillColor: NODE_FILL,
|
|
@@ -211,7 +252,6 @@ export function createWardleyPipeline(gfx, source = TOOLBOX_SOURCE) {
|
|
|
211
252
|
const labelId = addLabel(gfx.surface, PIPELINE_LABEL, cx - 60, top - d / 2 - LABEL_H - LABEL_GAP, 'center');
|
|
212
253
|
// Nested groups: (handle + label), then (body + that group).
|
|
213
254
|
const innerId = group(gfx, [handleId, labelId]);
|
|
214
|
-
track(gfx, source, 'FrameworkElementAdded', 'node:pipeline');
|
|
215
255
|
finish(gfx, group(gfx, [bodyId, innerId]));
|
|
216
256
|
}
|
|
217
257
|
/**
|
|
@@ -221,7 +261,7 @@ export function createWardleyPipeline(gfx, source = TOOLBOX_SOURCE) {
|
|
|
221
261
|
* the node centers and follow on move/resize). A label sits to the right and
|
|
222
262
|
* everything is grouped into one object.
|
|
223
263
|
*/
|
|
224
|
-
export function createWardleyMarket(gfx
|
|
264
|
+
export function createWardleyMarket(gfx) {
|
|
225
265
|
const surface = gfx.surface;
|
|
226
266
|
if (!surface)
|
|
227
267
|
return;
|
|
@@ -237,7 +277,7 @@ export function createWardleyMarket(gfx, source = TOOLBOX_SOURCE) {
|
|
|
237
277
|
[rho * sin60, rho / 2],
|
|
238
278
|
[-rho * sin60, rho / 2],
|
|
239
279
|
];
|
|
240
|
-
const dotIds = verts.map(([vx, vy]) => addEllipseNode(surface, 'component', cx + vx, cy + vy, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH));
|
|
280
|
+
const dotIds = verts.map(([vx, vy]) => addEllipseNode(surface, 'component', cx + vx, cy + vy, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH, true));
|
|
241
281
|
// Triangle: 3 attached connectors (auto-route center-to-center, clipped).
|
|
242
282
|
const connIds = [
|
|
243
283
|
[dotIds[0], dotIds[1]],
|
|
@@ -255,35 +295,128 @@ export function createWardleyMarket(gfx, source = TOOLBOX_SOURCE) {
|
|
|
255
295
|
rearEndpointStyle: PointStyle.None,
|
|
256
296
|
}));
|
|
257
297
|
const labelId = addLabel(surface, MARKET_LABEL, cx + R + LABEL_GAP, cy - LABEL_H / 2);
|
|
258
|
-
track(gfx, source, 'FrameworkElementAdded', 'node:market');
|
|
259
298
|
finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
|
|
260
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
302
|
+
*
|
|
303
|
+
* Wardley's first interchange command, and its whole implementation: the four
|
|
304
|
+
* steps live in {@link runInterchangeImportFile} and the reading lives in the
|
|
305
|
+
* declared capability, so this framework contributes a declaration and a
|
|
306
|
+
* label rather than a pipeline (`docs/adr/0012`, P1 and P3).
|
|
307
|
+
*
|
|
308
|
+
* It takes a `BlockStdScope` and not the `GfxController` the rest of this file
|
|
309
|
+
* runs on, because an import is not a drawing gesture: it opens a picker,
|
|
310
|
+
* writes a whole board in one undo step, moves the viewport and notifies —
|
|
311
|
+
* none of which a `GfxController` alone can do.
|
|
312
|
+
*/
|
|
313
|
+
export async function importWardleySvgFile(std) {
|
|
314
|
+
await runInterchangeImportFile(std, WARDLEY_SVG_IMPORT);
|
|
315
|
+
}
|
|
261
316
|
/**
|
|
262
317
|
* Activate the native connector tool, pre-styled for a Wardley link (grey,
|
|
263
318
|
* solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
|
|
264
319
|
* user then draws from one node to another (endpoints attach to centers).
|
|
265
320
|
*/
|
|
266
|
-
export function activateWardleyConnector(gfx, kind
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
321
|
+
export function activateWardleyConnector(gfx, kind) {
|
|
322
|
+
gfx.tool.setTool(ConnectorTool, {
|
|
323
|
+
mode: ConnectorMode.Straight,
|
|
324
|
+
// The value-chain link IS the "depends on" edge of a Wardley map; the
|
|
325
|
+
// change arrow is a movement annotation and gets a role OF ITS OWN
|
|
326
|
+
// (PF13.4, reversing #71 — see `roles.ts`). Two roles, never one
|
|
327
|
+
// specialising the other: W1 is about where an arrow points and must never
|
|
328
|
+
// fall on a dependency.
|
|
329
|
+
role: kind === 'link' ? WARDLEY_ROLE.dependency : WARDLEY_ROLE.changeArrow,
|
|
330
|
+
// The look rides on the activation, never through the last-props store:
|
|
331
|
+
// the plain connector tool must keep the user's own style (#144 M1).
|
|
332
|
+
style: kind === 'arrow'
|
|
333
|
+
? {
|
|
334
|
+
stroke: WARDLEY_RED,
|
|
335
|
+
strokeStyle: StrokeStyle.Dash,
|
|
336
|
+
strokeWidth: LINK_STROKE_WIDTH,
|
|
337
|
+
frontEndpointStyle: PointStyle.None,
|
|
338
|
+
rearEndpointStyle: PointStyle.Triangle,
|
|
339
|
+
}
|
|
340
|
+
: {
|
|
341
|
+
stroke: LINK_GREY,
|
|
342
|
+
strokeStyle: StrokeStyle.Solid,
|
|
343
|
+
strokeWidth: LINK_STROKE_WIDTH,
|
|
344
|
+
frontEndpointStyle: PointStyle.None,
|
|
345
|
+
rearEndpointStyle: PointStyle.None,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
287
348
|
// The wardley palette stays open (native sub-menu behaviour): it only
|
|
288
349
|
// closes on re-click of the senior button, another senior tool, or Escape.
|
|
289
350
|
}
|
|
351
|
+
/* ── Interchange: the OWM DSL, out and in (`docs/adr/0012`) ───────────── */
|
|
352
|
+
/**
|
|
353
|
+
* The one wording this file owns: what a WRITER could not say.
|
|
354
|
+
*
|
|
355
|
+
* The import's wordings live in the pipeline that writes them
|
|
356
|
+
* (`affine-block-surface`, `extensions/interchange-import.ts`) — one set of
|
|
357
|
+
* keys for every format, with the format's own name composed into them, so
|
|
358
|
+
* "OWM file imported" needs no key of its own. An export's losses are the
|
|
359
|
+
* capability's own sentences and have nowhere else to go.
|
|
360
|
+
*/
|
|
361
|
+
const EXPORT_WARNINGS_KEY = 'com.labre.commands.wardley.exportOwm.warnings';
|
|
362
|
+
const EXPORT_WARNINGS_FALLBACK = 'What this export could not write down';
|
|
363
|
+
/** The maps on the surface — what the export command is offered against. */
|
|
364
|
+
export function wardleyMapsOnBoard(std) {
|
|
365
|
+
const surface = std.get(GfxControllerIdentifier).surface;
|
|
366
|
+
return (surface?.elementModels ?? []).filter((model) => model instanceof WardleyBackgroundElementModel);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Everything on the surface the exporter speaks about, in document order.
|
|
370
|
+
*
|
|
371
|
+
* The half that needs an editor, and only that half: reading the surface. The
|
|
372
|
+
* picking is {@link wardleyBoardFrom}, which the interchange capability calls
|
|
373
|
+
* with the same elements and no `std` at all (`docs/adr/0012`, P3).
|
|
374
|
+
*/
|
|
375
|
+
export function wardleyBoardOf(std) {
|
|
376
|
+
return wardleyBoardFrom(std.get(GfxControllerIdentifier).surface?.elementModels ?? []);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Serialize the board as an OWM document and hand it to the browser.
|
|
380
|
+
*
|
|
381
|
+
* Three steps, and only the first and the last know what an editor is: read the
|
|
382
|
+
* surface, run the DECLARED capability, download what it produced. The middle
|
|
383
|
+
* step is not re-implemented here — the document, the filename and the content
|
|
384
|
+
* type all come out of `WARDLEY_OWM_EXPORT.run`, so the command and the
|
|
385
|
+
* registry cannot describe the same map differently. There is one door, and the
|
|
386
|
+
* registry is the label on it.
|
|
387
|
+
*/
|
|
388
|
+
export function exportOwmFile(std) {
|
|
389
|
+
const elements = std.get(GfxControllerIdentifier).surface?.elementModels ?? [];
|
|
390
|
+
const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
|
|
391
|
+
const { text, filename, mime, warnings } = WARDLEY_OWM_EXPORT.run(elements, {
|
|
392
|
+
name: wardleySafeFilename(title),
|
|
393
|
+
});
|
|
394
|
+
// The charset is the browser's business, not the format's: `mime` is what an
|
|
395
|
+
// `.owm` IS, and this is how a blob is told to carry it.
|
|
396
|
+
downloadBlob(new Blob([text], { type: `${mime};charset=utf-8` }), filename);
|
|
397
|
+
// A warning is never an error: the file downloaded, and it is valid; what it
|
|
398
|
+
// could not say is what this names. A board with two maps on it, a component
|
|
399
|
+
// with no name, an evolution arrow that also climbs the value chain — each is
|
|
400
|
+
// a sentence the format has no way to write down, and the person who clicked
|
|
401
|
+
// Export is the one entitled to hear about it.
|
|
402
|
+
if (!warnings || warnings.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
std.getOptional(NotificationProvider)?.notify({
|
|
405
|
+
title: translateKey(std, EXPORT_WARNINGS_KEY, EXPORT_WARNINGS_FALLBACK),
|
|
406
|
+
message: warnings.join('\n'),
|
|
407
|
+
accent: 'warning',
|
|
408
|
+
duration: 8000,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Read an `.owm` the user picks, draw it, and say what it cost.
|
|
413
|
+
*
|
|
414
|
+
* The whole of the import glue, and it is one line: the generic pipeline picks
|
|
415
|
+
* the file from the format's own declaration, runs the capability, mints the
|
|
416
|
+
* surface ids and repairs the link endpoints that named the file's names,
|
|
417
|
+
* brings the map into view and reports. Nothing in it is about Wardley except
|
|
418
|
+
* the capability handed to it.
|
|
419
|
+
*/
|
|
420
|
+
export async function importOwmFile(std) {
|
|
421
|
+
await runInterchangeImportFile(std, WARDLEY_OWM_IMPORT);
|
|
422
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AuditCriterion } from '@formicoidea/labre-core/shared/services';
|
|
2
|
+
/**
|
|
3
|
+
* The three Wardley audit criteria, A1–A3, as DATA (PF14.1).
|
|
4
|
+
*
|
|
5
|
+
* ## Why these three are criteria and not rules
|
|
6
|
+
*
|
|
7
|
+
* A validation rule is decided by geometry: an element is inside a frame or it
|
|
8
|
+
* is not, an arrow runs with an axis or against it. The engine can settle it in
|
|
9
|
+
* microseconds and be right every time, so it does — that is levels 1 and 2.
|
|
10
|
+
*
|
|
11
|
+
* These three cannot be settled that way, and no amount of engineering will
|
|
12
|
+
* change that. "Is this component positioned for the right reason" is a
|
|
13
|
+
* question about the author's argument; "does this map do its job" is a question
|
|
14
|
+
* about a conversation. They are the questions a Wardley coach asks in a review,
|
|
15
|
+
* and they are exactly what an assistant with the map's facts in hand can have
|
|
16
|
+
* an opinion about — an OPINION, hence `audit` severity, hence invisible on the
|
|
17
|
+
* canvas and never a wall.
|
|
18
|
+
*
|
|
19
|
+
* They are versioned like a rule and shipped like one, because the day a
|
|
20
|
+
* criterion's wording changes is a day the answers before and after are no
|
|
21
|
+
* longer comparable, and a host pinning behaviour needs to be able to say so.
|
|
22
|
+
*
|
|
23
|
+
* ## The prompts
|
|
24
|
+
*
|
|
25
|
+
* The prompt is the criterion, in words, addressed to the assistant and never
|
|
26
|
+
* rendered — see {@link AuditCriterion.prompt}. They are written as review
|
|
27
|
+
* instructions: what to look at, and what would make the map fail. They name no
|
|
28
|
+
* model, no vendor and no output format: the assistant owns how it answers, the
|
|
29
|
+
* library owns what is asked.
|
|
30
|
+
*/
|
|
31
|
+
export declare const WARDLEY_AUDIT_CRITERIA: readonly AuditCriterion[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three Wardley audit criteria, A1–A3, as DATA (PF14.1).
|
|
3
|
+
*
|
|
4
|
+
* ## Why these three are criteria and not rules
|
|
5
|
+
*
|
|
6
|
+
* A validation rule is decided by geometry: an element is inside a frame or it
|
|
7
|
+
* is not, an arrow runs with an axis or against it. The engine can settle it in
|
|
8
|
+
* microseconds and be right every time, so it does — that is levels 1 and 2.
|
|
9
|
+
*
|
|
10
|
+
* These three cannot be settled that way, and no amount of engineering will
|
|
11
|
+
* change that. "Is this component positioned for the right reason" is a
|
|
12
|
+
* question about the author's argument; "does this map do its job" is a question
|
|
13
|
+
* about a conversation. They are the questions a Wardley coach asks in a review,
|
|
14
|
+
* and they are exactly what an assistant with the map's facts in hand can have
|
|
15
|
+
* an opinion about — an OPINION, hence `audit` severity, hence invisible on the
|
|
16
|
+
* canvas and never a wall.
|
|
17
|
+
*
|
|
18
|
+
* They are versioned like a rule and shipped like one, because the day a
|
|
19
|
+
* criterion's wording changes is a day the answers before and after are no
|
|
20
|
+
* longer comparable, and a host pinning behaviour needs to be able to say so.
|
|
21
|
+
*
|
|
22
|
+
* ## The prompts
|
|
23
|
+
*
|
|
24
|
+
* The prompt is the criterion, in words, addressed to the assistant and never
|
|
25
|
+
* rendered — see {@link AuditCriterion.prompt}. They are written as review
|
|
26
|
+
* instructions: what to look at, and what would make the map fail. They name no
|
|
27
|
+
* model, no vendor and no output format: the assistant owns how it answers, the
|
|
28
|
+
* library owns what is asked.
|
|
29
|
+
*/
|
|
30
|
+
export const WARDLEY_AUDIT_CRITERIA = [
|
|
31
|
+
{
|
|
32
|
+
id: 'wardley.A1',
|
|
33
|
+
framework: 'wardley',
|
|
34
|
+
labelKey: 'com.labre.wardley.audit.A1',
|
|
35
|
+
fallback: 'Positioning is justified',
|
|
36
|
+
version: 1,
|
|
37
|
+
prompt: [
|
|
38
|
+
'Assess whether each component is placed on the evolution axis for the',
|
|
39
|
+
'right reason. Evolution measures how CONTEXTUALLY UBIQUITOUS and',
|
|
40
|
+
'well-understood a component is to the users of THIS value chain — not',
|
|
41
|
+
'how modern, sophisticated or technically mature the technology behind',
|
|
42
|
+
'it is. A component built on cutting-edge technology can be a commodity',
|
|
43
|
+
'to its users; a component built on decades-old technology can still be',
|
|
44
|
+
'in genesis for this organisation. Flag components whose position looks',
|
|
45
|
+
'argued from technical maturity, vendor age, or how recently the team',
|
|
46
|
+
'adopted them, rather than from how their users perceive and consume',
|
|
47
|
+
'them. Use the zone each component sits in, the roles it carries and the',
|
|
48
|
+
'components it depends on. Say which component, in which zone, and what',
|
|
49
|
+
'about its position does not follow from the value chain around it.',
|
|
50
|
+
].join(' '),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'wardley.A2',
|
|
54
|
+
framework: 'wardley',
|
|
55
|
+
labelKey: 'com.labre.wardley.audit.A2',
|
|
56
|
+
fallback: 'The value chain is legible',
|
|
57
|
+
version: 1,
|
|
58
|
+
prompt: [
|
|
59
|
+
'Assess whether the value chain can actually be read. The map exists to',
|
|
60
|
+
'be discussed, agreed on and decided from, by people who did not draw',
|
|
61
|
+
'it. Follow the dependency edges from the anchor (the user and their',
|
|
62
|
+
'need) down through the chain: does every component have a reason to be',
|
|
63
|
+
'there that the chain itself states? Look for anchors that are missing or',
|
|
64
|
+
'that name a system rather than a user need, components that hang off no',
|
|
65
|
+
'chain, chains that fork into detail nobody would decide from, and',
|
|
66
|
+
'dependencies whose direction says the opposite of what the layout',
|
|
67
|
+
'implies. Say what a reader would fail to understand, and where.',
|
|
68
|
+
].join(' '),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'wardley.A3',
|
|
72
|
+
framework: 'wardley',
|
|
73
|
+
labelKey: 'com.labre.wardley.audit.A3',
|
|
74
|
+
fallback: 'The model applies here',
|
|
75
|
+
version: 1,
|
|
76
|
+
prompt: [
|
|
77
|
+
'Assess whether Wardley mapping is the right instrument for what this map',
|
|
78
|
+
'describes. The model assumes a COMPETITIVE landscape: evolution is',
|
|
79
|
+
'driven by supply and demand competition, and the strategic play is',
|
|
80
|
+
'against other actors who could serve the same need. It says little about',
|
|
81
|
+
'a domain with no competitive pressure — an internal process nobody else',
|
|
82
|
+
'could supply, a regulatory obligation with a single possible',
|
|
83
|
+
'implementation, an organisational chart. Judge from the anchor, the user',
|
|
84
|
+
'need it names, and the nature of the components in the chain. If the',
|
|
85
|
+
'subject is not a competitive landscape, say so plainly and say which',
|
|
86
|
+
'other instrument would fit — that is more useful than three careful',
|
|
87
|
+
'observations about a map that should not exist.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
export declare const WARDLEY_BACKGROUND: FrameworkBackgroundDef;
|
|
3
|
+
/**
|
|
4
|
+
* The props the in-place label editor is allowed to write.
|
|
5
|
+
*
|
|
6
|
+
* A closed list, not `string`: the hit test reports whatever `prop` a
|
|
7
|
+
* declaration names, and since #73 an element preserves keys it does not
|
|
8
|
+
* declare — so a typo in the declaration would happily persist a junk key onto
|
|
9
|
+
* every map it was double-clicked on. This is the gate between "the
|
|
10
|
+
* declaration says so" and "the document gets it".
|
|
11
|
+
*/
|
|
12
|
+
export declare const WARDLEY_LABEL_PROPS: readonly ["xAxisTitle", "yAxisTitle", "evolutionStart", "evolutionEnd", "visibilityHigh", "visibilityLow", "phase0", "phase1", "phase2", "phase3"];
|
|
13
|
+
export type WardleyLabelProp = (typeof WARDLEY_LABEL_PROPS)[number];
|
|
14
|
+
export declare function isWardleyLabelProp(prop: string): prop is WardleyLabelProp;
|