@formicoidea/labre-framework-wardley 0.29.1 → 0.30.1

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.
@@ -0,0 +1,74 @@
1
+ import { type WardleyBgVariant } from '@formicoidea/labre-core/model';
2
+ import type { GfxController } from '@formicoidea/labre-core/std/gfx';
3
+ /**
4
+ * Standalone creation/activation actions for the Wardley toolbox. They are
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;
14
+ /**
15
+ * The single-circle node flavours: one connectable ellipse + a label to its
16
+ * right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
17
+ * method inner circle) is drawn by the node renderer from `kind`.
18
+ */
19
+ declare const NODE_PRESETS: {
20
+ readonly component: {
21
+ readonly d: 18;
22
+ readonly fill: "#ffffff";
23
+ readonly label: string;
24
+ };
25
+ readonly anchor: {
26
+ readonly d: 18;
27
+ readonly fill: "#ffffff";
28
+ readonly label: string;
29
+ };
30
+ readonly ecosystem: {
31
+ readonly d: 40;
32
+ readonly fill: "#ffffff";
33
+ readonly label: "Ecosystem";
34
+ };
35
+ readonly method: {
36
+ readonly d: 35;
37
+ readonly fill: "#d9d9d9";
38
+ readonly label: "Component";
39
+ };
40
+ };
41
+ export type WardleyNodeKind = keyof typeof NODE_PRESETS;
42
+ /** Create a wardley map background of the given variant, viewport-centered. */
43
+ export declare function createWardleyBackground(gfx: GfxController, variant?: WardleyBgVariant, source?: WardleyActionSource): void;
44
+ /**
45
+ * Create a single-circle node (component / anchor / ecosystem / method):
46
+ * one connectable native ellipse + a label to its right, grouped so they
47
+ * move together (enter the group to reposition / edit the label).
48
+ */
49
+ export declare function createWardleyNode(gfx: GfxController, kind: WardleyNodeKind, source?: WardleyActionSource): void;
50
+ /** Create an inertia bar (filled black rect). */
51
+ export declare function createWardleyInertia(gfx: GfxController, source?: WardleyActionSource): void;
52
+ /**
53
+ * Create a pipeline: a wide thin native rect body (white semi-transparent,
54
+ * NON-connectable) + a node-sized square handle straddling its top edge (the
55
+ * only connection point, center anchor) + a native text label. The handle and
56
+ * label are grouped, then grouped again with the body so the whole pipeline
57
+ * moves as one. Pure composition of native elements — no custom type / view.
58
+ */
59
+ export declare function createWardleyPipeline(gfx: GfxController, source?: WardleyActionSource): void;
60
+ /**
61
+ * Create a market: a large thin-bordered circle (the connectable market node)
62
+ * containing 3 small thick-bordered component nodes wired into a triangle by
63
+ * native attached connectors (thin, dark, no arrows — they auto-route between
64
+ * the node centers and follow on move/resize). A label sits to the right and
65
+ * everything is grouped into one object.
66
+ */
67
+ export declare function createWardleyMarket(gfx: GfxController, source?: WardleyActionSource): void;
68
+ /**
69
+ * Activate the native connector tool, pre-styled for a Wardley link (grey,
70
+ * solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
71
+ * user then draws from one node to another (endpoints attach to centers).
72
+ */
73
+ export declare function activateWardleyConnector(gfx: GfxController, kind: 'link' | 'arrow', source?: WardleyActionSource): void;
74
+ export {};
@@ -0,0 +1,289 @@
1
+ import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
+ import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
+ import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
5
+ import { EditPropsStore, TelemetryProvider, } from '@formicoidea/labre-core/shared/services';
6
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
7
+ import { REF_WIDTH } from './consts';
8
+ 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';
9
+ const TOOLBOX_SOURCE = {
10
+ segment: 'wardley toolbox',
11
+ module: 'wardley menu',
12
+ };
13
+ export const WARDLEY_SHORTCUT_SOURCE = {
14
+ segment: 'wardley toolbox',
15
+ module: 'keyboard shortcut',
16
+ };
17
+ /**
18
+ * Per-variant default label overrides applied at creation (all remain editable
19
+ * afterwards via the inline editor / toggles). The gradient itself is driven by
20
+ * `variant` in the renderer.
21
+ */
22
+ const BACKGROUND_VARIANT_DEFAULTS = {
23
+ classic: {},
24
+ // The Y axis becomes "Opportunity"; phase labels keep the classic defaults.
25
+ opportunity: {
26
+ yAxisTitle: 'Opportunity',
27
+ showVisibilityLabels: false,
28
+ showCornerLabels: false,
29
+ },
30
+ // The Y axis splits into Benefit (top) / Investment (bottom) around a zero
31
+ // line drawn by the renderer.
32
+ benefit: {
33
+ yAxisTitle: '',
34
+ visibilityHigh: 'Benefit',
35
+ visibilityLow: 'Investment',
36
+ showCornerLabels: false,
37
+ },
38
+ // Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
39
+ // the grey gradient differs.
40
+ 'evolution-gradient': {},
41
+ };
42
+ /** Height of the native free-text labels (Inter, size 18). */
43
+ const LABEL_H = LABEL_FONT_SIZE + 8;
44
+ /**
45
+ * The single-circle node flavours: one connectable ellipse + a label to its
46
+ * right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
47
+ * method inner circle) is drawn by the node renderer from `kind`.
48
+ */
49
+ const NODE_PRESETS = {
50
+ component: { d: NODE_SIZE, fill: NODE_FILL, label: LABEL_DEFAULT.component },
51
+ anchor: { d: NODE_SIZE, fill: NODE_FILL, label: LABEL_DEFAULT.anchor },
52
+ // Ecosystem: glyph = double border + hatched donut; connectors attach to
53
+ // this outer circle's center.
54
+ ecosystem: { d: ECOSYSTEM_SIZE, fill: NODE_FILL, label: ECOSYSTEM_LABEL },
55
+ // Method: the FILL color encodes the chosen method (editable).
56
+ method: { d: METHOD_SIZE, fill: METHOD_FILL, label: METHOD_LABEL },
57
+ };
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
+ function finish(gfx, id) {
67
+ gfx.doc.captureSync();
68
+ gfx.tool.setTool(DefaultTool);
69
+ gfx.selection.set({ elements: [id], editing: false });
70
+ // The wardley palette stays open (native sub-menu behaviour) so several
71
+ // Wardley objects can be added in a row; the canvas stays selectable.
72
+ }
73
+ /** Group elements; returns the group id (or the first id if grouping failed). */
74
+ function group(gfx, ids) {
75
+ const [, result] = gfx.std.command.exec(createGroupCommand, {
76
+ elements: ids,
77
+ });
78
+ return result.groupId || ids[0];
79
+ }
80
+ /** Add a native ellipse wardley node centred on (cx, cy). */
81
+ function addEllipseNode(surface, kind, cx, cy, d, fillColor, strokeWidth = NODE_STROKE_WIDTH) {
82
+ return surface.addElement({
83
+ type: 'wardleyNode',
84
+ kind,
85
+ shapeType: 'ellipse',
86
+ filled: true,
87
+ fillColor,
88
+ strokeColor: NODE_STROKE,
89
+ strokeWidth,
90
+ shapeStyle: ShapeStyle.General,
91
+ roughness: 0,
92
+ xywh: new Bound(cx - d / 2, cy - d / 2, d, d).serialize(),
93
+ });
94
+ }
95
+ /** Add a native free-text label (same Inter family as the axis labels). */
96
+ function addLabel(surface, text, x, y, textAlign = 'left') {
97
+ return surface.addElement({
98
+ type: 'text',
99
+ text,
100
+ fontFamily: FontFamily.Inter,
101
+ fontSize: LABEL_FONT_SIZE,
102
+ color: NODE_STROKE,
103
+ textAlign,
104
+ xywh: new Bound(x, y, 120, LABEL_H).serialize(),
105
+ });
106
+ }
107
+ /** Create a wardley map background of the given variant, viewport-centered. */
108
+ export function createWardleyBackground(gfx, variant = 'classic', source = TOOLBOX_SOURCE) {
109
+ if (!gfx.surface)
110
+ return;
111
+ let width = REF_WIDTH;
112
+ for (const el of gfx.surface.getElementsByType('wardley')) {
113
+ const [, , ew, eh] = el.deserializedXYWH;
114
+ width = Math.max(width, ew, (eh * 16) / 9);
115
+ }
116
+ const height = (width * 9) / 16;
117
+ const { centerX, centerY } = gfx.viewport;
118
+ const id = gfx.surface.addElement({
119
+ type: 'wardley',
120
+ variant,
121
+ ...BACKGROUND_VARIANT_DEFAULTS[variant],
122
+ xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
123
+ });
124
+ track(gfx, source, 'FrameworkElementAdded', `background:${variant}`);
125
+ finish(gfx, id);
126
+ }
127
+ /**
128
+ * Create a single-circle node (component / anchor / ecosystem / method):
129
+ * one connectable native ellipse + a label to its right, grouped so they
130
+ * move together (enter the group to reposition / edit the label).
131
+ */
132
+ export function createWardleyNode(gfx, kind, source = TOOLBOX_SOURCE) {
133
+ const surface = gfx.surface;
134
+ if (!surface)
135
+ return;
136
+ const { d, fill, label } = NODE_PRESETS[kind];
137
+ const { centerX: cx, centerY: cy } = gfx.viewport;
138
+ const nodeId = addEllipseNode(surface, kind, cx, cy, d, fill);
139
+ const labelId = addLabel(surface, label, cx + d / 2 + LABEL_GAP, cy - LABEL_H / 2);
140
+ track(gfx, source, 'FrameworkElementAdded', `node:${kind}`);
141
+ finish(gfx, group(gfx, [nodeId, labelId]));
142
+ }
143
+ /** Create an inertia bar (filled black rect). */
144
+ export function createWardleyInertia(gfx, source = TOOLBOX_SOURCE) {
145
+ if (!gfx.surface)
146
+ return;
147
+ const { w, h } = INERTIA_SIZE;
148
+ const { centerX, centerY } = gfx.viewport;
149
+ const id = gfx.surface.addElement({
150
+ type: 'shape',
151
+ shapeType: 'rect',
152
+ filled: true,
153
+ fillColor: INERTIA_COLOR,
154
+ strokeColor: INERTIA_COLOR,
155
+ strokeWidth: 0,
156
+ shapeStyle: ShapeStyle.General,
157
+ roughness: 0,
158
+ radius: 0,
159
+ // the inertia bar has a canonical size: text overflows, never deforms
160
+ textFitMode: TextFitMode.Overflow,
161
+ xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
162
+ });
163
+ track(gfx, source, 'FrameworkElementAdded', 'node:inertia');
164
+ finish(gfx, id);
165
+ }
166
+ /**
167
+ * Create a pipeline: a wide thin native rect body (white semi-transparent,
168
+ * NON-connectable) + a node-sized square handle straddling its top edge (the
169
+ * only connection point, center anchor) + a native text label. The handle and
170
+ * label are grouped, then grouped again with the body so the whole pipeline
171
+ * moves as one. Pure composition of native elements — no custom type / view.
172
+ */
173
+ export function createWardleyPipeline(gfx, source = TOOLBOX_SOURCE) {
174
+ if (!gfx.surface)
175
+ return;
176
+ const { centerX: cx, centerY: cy } = gfx.viewport;
177
+ const W = PIPELINE_WIDTH;
178
+ const H = PIPELINE_HEIGHT;
179
+ const d = HANDLE_SIZE;
180
+ const top = cy - H / 2;
181
+ // Body: a WardleyNode rect, made non-connectable by `kind: 'pipeline'`.
182
+ const bodyId = gfx.surface.addElement({
183
+ type: 'wardleyNode',
184
+ kind: 'pipeline',
185
+ shapeType: 'rect',
186
+ filled: true,
187
+ fillColor: PIPELINE_FILL,
188
+ strokeColor: NODE_STROKE,
189
+ strokeWidth: NODE_STROKE_WIDTH,
190
+ shapeStyle: ShapeStyle.General,
191
+ roughness: 0,
192
+ radius: 0,
193
+ xywh: new Bound(cx - W / 2, top, W, H).serialize(),
194
+ });
195
+ // Handle: a node-sized WardleyNode square straddling the top edge. Inherits
196
+ // `centerAnchorOnly` so connectors attach to its center only.
197
+ const handleId = gfx.surface.addElement({
198
+ type: 'wardleyNode',
199
+ kind: 'handle',
200
+ shapeType: 'rect',
201
+ filled: true,
202
+ fillColor: NODE_FILL,
203
+ strokeColor: NODE_STROKE,
204
+ strokeWidth: NODE_STROKE_WIDTH,
205
+ shapeStyle: ShapeStyle.General,
206
+ roughness: 0,
207
+ radius: 0,
208
+ xywh: new Bound(cx - d / 2, top - d / 2, d, d).serialize(),
209
+ });
210
+ // Label centered horizontally on the pipeline, sitting ABOVE the handle.
211
+ const labelId = addLabel(gfx.surface, PIPELINE_LABEL, cx - 60, top - d / 2 - LABEL_H - LABEL_GAP, 'center');
212
+ // Nested groups: (handle + label), then (body + that group).
213
+ const innerId = group(gfx, [handleId, labelId]);
214
+ track(gfx, source, 'FrameworkElementAdded', 'node:pipeline');
215
+ finish(gfx, group(gfx, [bodyId, innerId]));
216
+ }
217
+ /**
218
+ * Create a market: a large thin-bordered circle (the connectable market node)
219
+ * containing 3 small thick-bordered component nodes wired into a triangle by
220
+ * native attached connectors (thin, dark, no arrows — they auto-route between
221
+ * the node centers and follow on move/resize). A label sits to the right and
222
+ * everything is grouped into one object.
223
+ */
224
+ export function createWardleyMarket(gfx, source = TOOLBOX_SOURCE) {
225
+ const surface = gfx.surface;
226
+ if (!surface)
227
+ return;
228
+ const { centerX: cx, centerY: cy } = gfx.viewport;
229
+ const R = MARKET_SIZE / 2;
230
+ const rho = MARKET_DOT_RING;
231
+ const sin60 = Math.sqrt(3) / 2;
232
+ // Outer circle = the market node (connectable, center-only).
233
+ const circleId = addEllipseNode(surface, 'market', cx, cy, MARKET_SIZE, NODE_FILL);
234
+ // 3 inner component nodes (thick border, no label) at the triangle vertices.
235
+ const verts = [
236
+ [0, -rho],
237
+ [rho * sin60, rho / 2],
238
+ [-rho * sin60, rho / 2],
239
+ ];
240
+ const dotIds = verts.map(([vx, vy]) => addEllipseNode(surface, 'component', cx + vx, cy + vy, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH));
241
+ // Triangle: 3 attached connectors (auto-route center-to-center, clipped).
242
+ const connIds = [
243
+ [dotIds[0], dotIds[1]],
244
+ [dotIds[1], dotIds[2]],
245
+ [dotIds[2], dotIds[0]],
246
+ ].map(([a, b]) => surface.addElement({
247
+ type: 'connector',
248
+ mode: ConnectorMode.Straight,
249
+ source: { id: a },
250
+ target: { id: b },
251
+ stroke: MARKET_LINK_COLOR,
252
+ strokeStyle: StrokeStyle.Solid,
253
+ strokeWidth: MARKET_LINK_WIDTH,
254
+ frontEndpointStyle: PointStyle.None,
255
+ rearEndpointStyle: PointStyle.None,
256
+ }));
257
+ const labelId = addLabel(surface, MARKET_LABEL, cx + R + LABEL_GAP, cy - LABEL_H / 2);
258
+ track(gfx, source, 'FrameworkElementAdded', 'node:market');
259
+ finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
260
+ }
261
+ /**
262
+ * Activate the native connector tool, pre-styled for a Wardley link (grey,
263
+ * solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
264
+ * user then draws from one node to another (endpoints attach to centers).
265
+ */
266
+ export function activateWardleyConnector(gfx, kind, source = TOOLBOX_SOURCE) {
267
+ const props = kind === 'arrow'
268
+ ? {
269
+ mode: ConnectorMode.Straight,
270
+ stroke: WARDLEY_RED,
271
+ strokeStyle: StrokeStyle.Dash,
272
+ strokeWidth: LINK_STROKE_WIDTH,
273
+ frontEndpointStyle: PointStyle.None,
274
+ rearEndpointStyle: PointStyle.Triangle,
275
+ }
276
+ : {
277
+ mode: ConnectorMode.Straight,
278
+ stroke: LINK_GREY,
279
+ strokeStyle: StrokeStyle.Solid,
280
+ strokeWidth: LINK_STROKE_WIDTH,
281
+ frontEndpointStyle: PointStyle.None,
282
+ rearEndpointStyle: PointStyle.None,
283
+ };
284
+ gfx.std.get(EditPropsStore).recordLastProps('connector', props);
285
+ track(gfx, source, 'FrameworkToolPicked', `connector:${kind}`);
286
+ gfx.tool.setTool(ConnectorTool, { mode: ConnectorMode.Straight });
287
+ // The wardley palette stays open (native sub-menu behaviour): it only
288
+ // closes on re-click of the senior button, another senior tool, or Escape.
289
+ }
@@ -1,6 +1,6 @@
1
1
  import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ARROW, CARD_RADIUS, COLORS, EVOLUTION_BOUNDARIES, FONT_FAMILY, FONTS, LINE, MARGIN, OFFSETS, } from './consts';
3
- import { BENEFIT_ZERO_FRAC, paintGradientBackground } from './gradient';
3
+ import { paintGradientBackground } from './gradient';
4
4
  function roundRectPath(ctx, x, y, w, h, r) {
5
5
  const rr = Math.min(r, w / 2, h / 2);
6
6
  ctx.beginPath();
@@ -60,15 +60,6 @@ export const wardley = (model, ctx, matrix) => {
60
60
  // Hidden when `showGradient` is false → plain white background.
61
61
  if (model.variant !== 'classic' && model.showGradient) {
62
62
  paintGradientBackground(ctx, model.variant, px0, px1, py0, py1);
63
- if (model.variant === 'benefit') {
64
- const zy = py1 - BENEFIT_ZERO_FRAC * ph;
65
- ctx.strokeStyle = COLORS.axis;
66
- ctx.lineWidth = 1.2;
67
- ctx.beginPath();
68
- ctx.moveTo(px0, zy);
69
- ctx.lineTo(px1, zy);
70
- ctx.stroke();
71
- }
72
63
  }
73
64
  // ── Optional evolution band tints ───────────────────────────────────
74
65
  if (model.banded) {
@@ -8,8 +8,6 @@
8
8
  */
9
9
  export declare const GRADIENT_GREEN = "#1f9e4d";
10
10
  export declare const GRADIENT_RED = "#d6455d";
11
- /** Benefit/investment zero-line height (fraction of plot height from bottom). */
12
- export declare const BENEFIT_ZERO_FRAC = 0.3;
13
11
  /**
14
12
  * Paint the curve-driven gradient over the plot rectangle [px0,px1]×[py0,py1]
15
13
  * in element-local coordinates. `classic` paints nothing.
package/dist/gradient.js CHANGED
@@ -48,8 +48,6 @@ const GRADIENT_GREY = '#7c8389';
48
48
  const GRADIENT_MAX_OPACITY = 0.45;
49
49
  /** Peak opacity for the grey evolution-gradient variant. */
50
50
  const GREY_MAX_OPACITY = 0.38;
51
- /** Benefit/investment zero-line height (fraction of plot height from bottom). */
52
- export const BENEFIT_ZERO_FRAC = 0.3;
53
51
  function rgba(hex, alpha) {
54
52
  const r = parseInt(hex.slice(1, 3), 16);
55
53
  const g = parseInt(hex.slice(3, 5), 16);
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export { wardleyShortcuts } from './shortcuts';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export { wardleyShortcuts } from './shortcuts';
@@ -0,0 +1,2 @@
1
+ import type { ShortcutDescriptor } from '@formicoidea/labre-core/std';
2
+ export declare const wardleyShortcuts: ShortcutDescriptor[];
@@ -0,0 +1,37 @@
1
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
+ import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyNode, createWardleyPipeline, WARDLEY_SHORTCUT_SOURCE, } from './actions';
3
+ /**
4
+ * Wardley canvas chords: press `w`, then the action key. Edgeless-scoped and
5
+ * registered by {@link WardleyViewExtension}, so they only exist when the
6
+ * edgeless editor is mounted AND the `wardley` block flag is enabled. The
7
+ * host can rebind or disable each id via `ShortcutOverrides`.
8
+ *
9
+ * This is the reference pattern for other frameworks: same shape with their
10
+ * own prefix letter, listed in `FRAMEWORK_SHORTCUT_GROUPS` (manifest) and
11
+ * registered via `ShortcutExtension` in their view extension (binding).
12
+ */
13
+ const wardleyShortcut = (id, key, run) => ({
14
+ id: `wardley.${id}`,
15
+ labelKey: `com.labre.keyboardShortcuts.wardley.${id}`,
16
+ defaultKeys: { mac: ['w', key], other: ['w', key] },
17
+ scope: 'edgeless',
18
+ owner: 'wardley',
19
+ handler: std => ctx => {
20
+ const gfx = std.get(GfxControllerIdentifier);
21
+ // Never fire while editing text on the canvas.
22
+ if (gfx.selection.editing)
23
+ return false;
24
+ ctx.get('defaultState').event.preventDefault();
25
+ run(gfx);
26
+ return true;
27
+ },
28
+ });
29
+ export const wardleyShortcuts = [
30
+ wardleyShortcut('addComponent', 'c', gfx => createWardleyNode(gfx, 'component', WARDLEY_SHORTCUT_SOURCE)),
31
+ wardleyShortcut('linkTool', 'l', gfx => activateWardleyConnector(gfx, 'link', WARDLEY_SHORTCUT_SOURCE)),
32
+ wardleyShortcut('evolutionArrow', 'a', gfx => activateWardleyConnector(gfx, 'arrow', WARDLEY_SHORTCUT_SOURCE)),
33
+ wardleyShortcut('addInertia', 'i', gfx => createWardleyInertia(gfx, WARDLEY_SHORTCUT_SOURCE)),
34
+ wardleyShortcut('addPipeline', 'p', gfx => createWardleyPipeline(gfx, WARDLEY_SHORTCUT_SOURCE)),
35
+ wardleyShortcut('addMethod', 'm', gfx => createWardleyNode(gfx, 'method', WARDLEY_SHORTCUT_SOURCE)),
36
+ wardleyShortcut('addBackground', 'b', gfx => createWardleyBackground(gfx, 'classic', WARDLEY_SHORTCUT_SOURCE)),
37
+ ];
@@ -3,50 +3,18 @@ import { LitElement } from 'lit';
3
3
  declare const EdgelessWardleyMenu_base: typeof LitElement & import("@formicoidea/labre-core/_pkgs/global/utils").Constructor<import("@formicoidea/labre-core/_pkgs/affine-widget-edgeless-toolbar").EdgelessToolbarToolClass>;
4
4
  /**
5
5
  * The popover that opens above the toolbar for the Wardley toolbox. Each item
6
- * creates a pre-formatted Wardley object. Nodes (component / anchor) are a
7
- * native ellipse + a native text label, grouped together.
6
+ * creates a pre-formatted Wardley object (see `../actions.ts` the same
7
+ * actions back the wardley keyboard shortcuts).
8
8
  */
9
9
  export declare class EdgelessWardleyMenu extends EdgelessWardleyMenu_base {
10
10
  static styles: import("lit").CSSResult;
11
11
  type: typeof EmptyTool;
12
12
  private _createBackground;
13
- /** Add a native ellipse wardley node centred on (cx, cy). */
14
- private _addEllipseNode;
15
- /** Add a native free-text label (same Inter family as the axis labels). */
16
- private _addLabel;
17
- /** Group elements; returns the group id (or the first id if grouping failed). */
18
- private _group;
19
- /**
20
- * Create a single-circle node (component / anchor / ecosystem / method):
21
- * one connectable native ellipse + a label to its right, grouped so they
22
- * move together (enter the group to reposition / edit the label).
23
- */
24
13
  private _createNode;
25
- private _createInertia;
26
- /**
27
- * Create a pipeline: a wide thin native rect body (white semi-transparent,
28
- * NON-connectable) + a node-sized square handle straddling its top edge (the
29
- * only connection point, center anchor) + a native text label. The handle and
30
- * label are grouped, then grouped again with the body so the whole pipeline
31
- * moves as one. Pure composition of native elements — no custom type / view.
32
- */
33
- private _createPipeline;
34
- /**
35
- * Create a market: a large thin-bordered circle (the connectable market node)
36
- * containing 3 small thick-bordered component nodes wired into a triangle by
37
- * native attached connectors (thin, dark, no arrows — they auto-route between
38
- * the node centers and follow on move/resize). A label sits to the right and
39
- * everything is grouped into one object.
40
- */
41
- private _createMarket;
42
- /**
43
- * Activate the native connector tool, pre-styled for a Wardley link (grey,
44
- * solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
45
- * user then draws from one node to another (endpoints attach to centers).
46
- */
14
+ private readonly _createInertia;
15
+ private readonly _createPipeline;
16
+ private readonly _createMarket;
47
17
  private _activateConnector;
48
- private _finish;
49
- private _track;
50
18
  render(): import("lit-html").TemplateResult<1>;
51
19
  }
52
20
  export {};
@@ -1,65 +1,26 @@
1
- import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
- import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
- import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
1
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
5
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
6
- import { EditPropsStore, TelemetryProvider, } from '@formicoidea/labre-core/shared/services';
7
2
  import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
8
- import { Bound } from '@formicoidea/labre-core/global/gfx';
9
3
  import { css, html, LitElement } from 'lit';
10
- import { REF_WIDTH } from '../consts';
11
- 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';
4
+ import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, } from '../actions';
12
5
  import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyEvolutionGradientIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './icons';
13
- /**
14
- * Per-variant default label overrides applied at creation (all remain editable
15
- * afterwards via the inline editor / toggles). The gradient itself is driven by
16
- * `variant` in the renderer.
17
- */
18
- const BACKGROUND_VARIANT_DEFAULTS = {
19
- classic: {},
20
- // The Y axis becomes "Opportunity"; phase labels keep the classic defaults.
21
- opportunity: {
22
- yAxisTitle: 'Opportunity',
23
- showVisibilityLabels: false,
24
- showCornerLabels: false,
25
- },
26
- // The Y axis splits into Benefit (top) / Investment (bottom) around a zero
27
- // line drawn by the renderer.
28
- benefit: {
29
- yAxisTitle: '',
30
- visibilityHigh: 'Benefit',
31
- visibilityLow: 'Investment',
32
- showCornerLabels: false,
33
- },
34
- // Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
35
- // the grey gradient differs.
36
- 'evolution-gradient': {},
37
- };
38
- /** Height of the native free-text labels (Inter, size 18). */
39
- const LABEL_H = LABEL_FONT_SIZE + 8;
40
- /**
41
- * The single-circle node flavours: one connectable ellipse + a label to its
42
- * right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
43
- * method inner circle) is drawn by the node renderer from `kind`.
44
- */
45
- const NODE_PRESETS = {
46
- component: { d: NODE_SIZE, fill: NODE_FILL, label: LABEL_DEFAULT.component },
47
- anchor: { d: NODE_SIZE, fill: NODE_FILL, label: LABEL_DEFAULT.anchor },
48
- // Ecosystem: glyph = double border + hatched donut; connectors attach to
49
- // this outer circle's center.
50
- ecosystem: { d: ECOSYSTEM_SIZE, fill: NODE_FILL, label: ECOSYSTEM_LABEL },
51
- // Method: the FILL color encodes the chosen method (editable).
52
- method: { d: METHOD_SIZE, fill: METHOD_FILL, label: METHOD_LABEL },
53
- };
54
6
  /**
55
7
  * The popover that opens above the toolbar for the Wardley toolbox. Each item
56
- * creates a pre-formatted Wardley object. Nodes (component / anchor) are a
57
- * native ellipse + a native text label, grouped together.
8
+ * creates a pre-formatted Wardley object (see `../actions.ts` the same
9
+ * actions back the wardley keyboard shortcuts).
58
10
  */
59
11
  export class EdgelessWardleyMenu extends EdgelessToolbarToolMixin(LitElement) {
60
12
  constructor() {
61
13
  super(...arguments);
62
14
  this.type = EmptyTool;
15
+ this._createInertia = () => {
16
+ createWardleyInertia(this.gfx);
17
+ };
18
+ this._createPipeline = () => {
19
+ createWardleyPipeline(this.gfx);
20
+ };
21
+ this._createMarket = () => {
22
+ createWardleyMarket(this.gfx);
23
+ };
63
24
  }
64
25
  static { this.styles = css `
65
26
  :host {
@@ -84,237 +45,13 @@ export class EdgelessWardleyMenu extends EdgelessToolbarToolMixin(LitElement) {
84
45
  }
85
46
  `; }
86
47
  _createBackground(variant = 'classic') {
87
- const { gfx } = this;
88
- if (!gfx.surface)
89
- return;
90
- let width = REF_WIDTH;
91
- for (const el of gfx.surface.getElementsByType('wardley')) {
92
- const [, , ew, eh] = el.deserializedXYWH;
93
- width = Math.max(width, ew, (eh * 16) / 9);
94
- }
95
- const height = (width * 9) / 16;
96
- const { centerX, centerY } = gfx.viewport;
97
- const id = gfx.surface.addElement({
98
- type: 'wardley',
99
- variant,
100
- ...BACKGROUND_VARIANT_DEFAULTS[variant],
101
- xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
102
- });
103
- this._track('FrameworkElementAdded', `background:${variant}`);
104
- this._finish(id);
105
- }
106
- /** Add a native ellipse wardley node centred on (cx, cy). */
107
- _addEllipseNode(surface, kind, cx, cy, d, fillColor, strokeWidth = NODE_STROKE_WIDTH) {
108
- return surface.addElement({
109
- type: 'wardleyNode',
110
- kind,
111
- shapeType: 'ellipse',
112
- filled: true,
113
- fillColor,
114
- strokeColor: NODE_STROKE,
115
- strokeWidth,
116
- shapeStyle: ShapeStyle.General,
117
- roughness: 0,
118
- xywh: new Bound(cx - d / 2, cy - d / 2, d, d).serialize(),
119
- });
120
- }
121
- /** Add a native free-text label (same Inter family as the axis labels). */
122
- _addLabel(surface, text, x, y, textAlign = 'left') {
123
- return surface.addElement({
124
- type: 'text',
125
- text,
126
- fontFamily: FontFamily.Inter,
127
- fontSize: LABEL_FONT_SIZE,
128
- color: NODE_STROKE,
129
- textAlign,
130
- xywh: new Bound(x, y, 120, LABEL_H).serialize(),
131
- });
48
+ createWardleyBackground(this.gfx, variant);
132
49
  }
133
- /** Group elements; returns the group id (or the first id if grouping failed). */
134
- _group(ids) {
135
- const [, result] = this.edgeless.std.command.exec(createGroupCommand, {
136
- elements: ids,
137
- });
138
- return result.groupId || ids[0];
139
- }
140
- /**
141
- * Create a single-circle node (component / anchor / ecosystem / method):
142
- * one connectable native ellipse + a label to its right, grouped so they
143
- * move together (enter the group to reposition / edit the label).
144
- */
145
50
  _createNode(kind) {
146
- const surface = this.gfx.surface;
147
- if (!surface)
148
- return;
149
- const { d, fill, label } = NODE_PRESETS[kind];
150
- const { centerX: cx, centerY: cy } = this.gfx.viewport;
151
- const nodeId = this._addEllipseNode(surface, kind, cx, cy, d, fill);
152
- const labelId = this._addLabel(surface, label, cx + d / 2 + LABEL_GAP, cy - LABEL_H / 2);
153
- this._track('FrameworkElementAdded', `node:${kind}`);
154
- this._finish(this._group([nodeId, labelId]));
155
- }
156
- _createInertia() {
157
- const { gfx } = this;
158
- if (!gfx.surface)
159
- return;
160
- const { w, h } = INERTIA_SIZE;
161
- const { centerX, centerY } = gfx.viewport;
162
- const id = gfx.surface.addElement({
163
- type: 'shape',
164
- shapeType: 'rect',
165
- filled: true,
166
- fillColor: INERTIA_COLOR,
167
- strokeColor: INERTIA_COLOR,
168
- strokeWidth: 0,
169
- shapeStyle: ShapeStyle.General,
170
- roughness: 0,
171
- radius: 0,
172
- xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
173
- });
174
- this._track('FrameworkElementAdded', 'node:inertia');
175
- this._finish(id);
176
- }
177
- /**
178
- * Create a pipeline: a wide thin native rect body (white semi-transparent,
179
- * NON-connectable) + a node-sized square handle straddling its top edge (the
180
- * only connection point, center anchor) + a native text label. The handle and
181
- * label are grouped, then grouped again with the body so the whole pipeline
182
- * moves as one. Pure composition of native elements — no custom type / view.
183
- */
184
- _createPipeline() {
185
- const { gfx } = this;
186
- if (!gfx.surface)
187
- return;
188
- const { centerX: cx, centerY: cy } = gfx.viewport;
189
- const W = PIPELINE_WIDTH;
190
- const H = PIPELINE_HEIGHT;
191
- const d = HANDLE_SIZE;
192
- const top = cy - H / 2;
193
- // Body: a WardleyNode rect, made non-connectable by `kind: 'pipeline'`.
194
- const bodyId = gfx.surface.addElement({
195
- type: 'wardleyNode',
196
- kind: 'pipeline',
197
- shapeType: 'rect',
198
- filled: true,
199
- fillColor: PIPELINE_FILL,
200
- strokeColor: NODE_STROKE,
201
- strokeWidth: NODE_STROKE_WIDTH,
202
- shapeStyle: ShapeStyle.General,
203
- roughness: 0,
204
- radius: 0,
205
- xywh: new Bound(cx - W / 2, top, W, H).serialize(),
206
- });
207
- // Handle: a node-sized WardleyNode square straddling the top edge. Inherits
208
- // `centerAnchorOnly` so connectors attach to its center only.
209
- const handleId = gfx.surface.addElement({
210
- type: 'wardleyNode',
211
- kind: 'handle',
212
- shapeType: 'rect',
213
- filled: true,
214
- fillColor: NODE_FILL,
215
- strokeColor: NODE_STROKE,
216
- strokeWidth: NODE_STROKE_WIDTH,
217
- shapeStyle: ShapeStyle.General,
218
- roughness: 0,
219
- radius: 0,
220
- xywh: new Bound(cx - d / 2, top - d / 2, d, d).serialize(),
221
- });
222
- // Label centered horizontally on the pipeline, sitting ABOVE the handle.
223
- const labelId = this._addLabel(gfx.surface, PIPELINE_LABEL, cx - 60, top - d / 2 - LABEL_H - LABEL_GAP, 'center');
224
- // Nested groups: (handle + label), then (body + that group).
225
- const innerId = this._group([handleId, labelId]);
226
- this._track('FrameworkElementAdded', 'node:pipeline');
227
- this._finish(this._group([bodyId, innerId]));
51
+ createWardleyNode(this.gfx, kind);
228
52
  }
229
- /**
230
- * Create a market: a large thin-bordered circle (the connectable market node)
231
- * containing 3 small thick-bordered component nodes wired into a triangle by
232
- * native attached connectors (thin, dark, no arrows — they auto-route between
233
- * the node centers and follow on move/resize). A label sits to the right and
234
- * everything is grouped into one object.
235
- */
236
- _createMarket() {
237
- const surface = this.gfx.surface;
238
- if (!surface)
239
- return;
240
- const { centerX: cx, centerY: cy } = this.gfx.viewport;
241
- const R = MARKET_SIZE / 2;
242
- const rho = MARKET_DOT_RING;
243
- const sin60 = Math.sqrt(3) / 2;
244
- // Outer circle = the market node (connectable, center-only).
245
- const circleId = this._addEllipseNode(surface, 'market', cx, cy, MARKET_SIZE, NODE_FILL);
246
- // 3 inner component nodes (thick border, no label) at the triangle vertices.
247
- const verts = [
248
- [0, -rho],
249
- [rho * sin60, rho / 2],
250
- [-rho * sin60, rho / 2],
251
- ];
252
- const dotIds = verts.map(([vx, vy]) => this._addEllipseNode(surface, 'component', cx + vx, cy + vy, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH));
253
- // Triangle: 3 attached connectors (auto-route center-to-center, clipped).
254
- const connIds = [
255
- [dotIds[0], dotIds[1]],
256
- [dotIds[1], dotIds[2]],
257
- [dotIds[2], dotIds[0]],
258
- ].map(([a, b]) => surface.addElement({
259
- type: 'connector',
260
- mode: ConnectorMode.Straight,
261
- source: { id: a },
262
- target: { id: b },
263
- stroke: MARKET_LINK_COLOR,
264
- strokeStyle: StrokeStyle.Solid,
265
- strokeWidth: MARKET_LINK_WIDTH,
266
- frontEndpointStyle: PointStyle.None,
267
- rearEndpointStyle: PointStyle.None,
268
- }));
269
- const labelId = this._addLabel(surface, MARKET_LABEL, cx + R + LABEL_GAP, cy - LABEL_H / 2);
270
- this._track('FrameworkElementAdded', 'node:market');
271
- this._finish(this._group([circleId, ...dotIds, ...connIds, labelId]));
272
- }
273
- /**
274
- * Activate the native connector tool, pre-styled for a Wardley link (grey,
275
- * solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
276
- * user then draws from one node to another (endpoints attach to centers).
277
- */
278
53
  _activateConnector(kind) {
279
- const props = kind === 'arrow'
280
- ? {
281
- mode: ConnectorMode.Straight,
282
- stroke: WARDLEY_RED,
283
- strokeStyle: StrokeStyle.Dash,
284
- strokeWidth: LINK_STROKE_WIDTH,
285
- frontEndpointStyle: PointStyle.None,
286
- rearEndpointStyle: PointStyle.Triangle,
287
- }
288
- : {
289
- mode: ConnectorMode.Straight,
290
- stroke: LINK_GREY,
291
- strokeStyle: StrokeStyle.Solid,
292
- strokeWidth: LINK_STROKE_WIDTH,
293
- frontEndpointStyle: PointStyle.None,
294
- rearEndpointStyle: PointStyle.None,
295
- };
296
- this.edgeless.std.get(EditPropsStore).recordLastProps('connector', props);
297
- this._track('FrameworkToolPicked', `connector:${kind}`);
298
- this.gfx.tool.setTool(ConnectorTool, { mode: ConnectorMode.Straight });
299
- // Keep the palette open (native sub-menu behaviour): it only closes on
300
- // re-click of the senior button, another senior tool, or Escape.
301
- }
302
- _finish(id) {
303
- const { gfx } = this;
304
- gfx.doc.captureSync();
305
- gfx.tool.setTool(DefaultTool);
306
- gfx.selection.set({ elements: [id], editing: false });
307
- // Keep the palette open (native sub-menu behaviour) so several Wardley
308
- // objects can be added in a row; the canvas stays selectable meanwhile.
309
- }
310
- _track(event, element) {
311
- this.edgeless.std.getOptional(TelemetryProvider)?.track(event, {
312
- framework: 'wardley',
313
- element,
314
- page: 'whiteboard editor',
315
- segment: 'wardley toolbox',
316
- module: 'wardley menu',
317
- });
54
+ activateWardleyConnector(this.gfx, kind);
318
55
  }
319
56
  render() {
320
57
  return html `
@@ -33,6 +33,8 @@ export class EdgelessWardleySeniorButton extends EdgelessToolbarToolMixin(Signal
33
33
  position: relative;
34
34
  overflow: hidden;
35
35
  cursor: pointer;
36
+ /* no double-tap-zoom / 300ms tap delay on touch screens */
37
+ touch-action: manipulation;
36
38
  display: flex;
37
39
  align-items: flex-end;
38
40
  justify-content: center;
@@ -56,7 +58,8 @@ export class EdgelessWardleySeniorButton extends EdgelessToolbarToolMixin(Signal
56
58
  width: 100%;
57
59
  height: 100%;
58
60
  }
59
- .wardley-root:hover .wardley-card {
61
+ .wardley-root:hover .wardley-card,
62
+ .wardley-root:active .wardley-card {
60
63
  --y: -10px;
61
64
  --s: 1.07;
62
65
  }
package/dist/view.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ViewExtensionProvider, } from '@formicoidea/labre-core/ext-loader';
2
2
  import { extendTemplateCategory } from '@formicoidea/labre-core/gfx/template';
3
+ import { ShortcutExtension } from '@formicoidea/labre-core/std';
3
4
  import { effects } from './effects';
5
+ import { wardleyShortcuts } from './shortcuts';
4
6
  import { wardleyTemplateCategory } from './templates';
5
7
  import { WardleyElementRendererExtension } from './element-renderer';
6
8
  import { WardleyInteraction, WardleyView } from './element-view';
@@ -30,6 +32,10 @@ export class WardleyViewExtension extends ViewExtensionProvider {
30
32
  context.register(wardleySeniorTool);
31
33
  context.register(wardleyToolbarExtension);
32
34
  context.register(wardleyNodeToolbarExtension);
35
+ // Edgeless-scoped chords (w+c, w+l, ...). Registering here inherits
36
+ // both the wardley flag gating and the edgeless-only availability;
37
+ // the installer is ShortcutKeymapExtension('edgeless') in root.
38
+ context.register(ShortcutExtension(wardleyShortcuts));
33
39
  }
34
40
  }
35
41
  }
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.29.1",
4
+ "version": "0.30.1",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -27,7 +27,7 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@formicoidea/labre-core": "0.29.1",
30
+ "@formicoidea/labre-core": "0.30.1",
31
31
  "lit": "^3.2.0"
32
32
  }
33
33
  }