@formicoidea/labre-framework-wardley 0.23.0 → 0.23.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.
Files changed (61) hide show
  1. package/dist/consts.d.ts +72 -0
  2. package/{src/consts.ts → dist/consts.js} +63 -72
  3. package/dist/descriptor.d.ts +7 -0
  4. package/{src/descriptor.ts → dist/descriptor.js} +1 -1
  5. package/dist/effects.d.ts +10 -0
  6. package/dist/effects.js +7 -0
  7. package/dist/element-renderer.d.ts +15 -0
  8. package/dist/element-renderer.js +160 -0
  9. package/dist/element-view.d.ts +21 -0
  10. package/dist/element-view.js +122 -0
  11. package/dist/gradient.d.ts +18 -0
  12. package/dist/gradient.js +112 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +2 -0
  15. package/dist/label-layout.d.ts +21 -0
  16. package/dist/label-layout.js +73 -0
  17. package/dist/legend.d.ts +12 -0
  18. package/dist/legend.js +333 -0
  19. package/dist/node/consts.d.ts +107 -0
  20. package/{src/node/consts.ts → dist/node/consts.js} +12 -20
  21. package/dist/node/label-editor.d.ts +28 -0
  22. package/dist/node/label-editor.js +216 -0
  23. package/dist/node/node-renderer.d.ts +17 -0
  24. package/dist/node/node-renderer.js +106 -0
  25. package/{src/node/node-view.ts → dist/node/node-view.d.ts} +3 -3
  26. package/dist/node/node-view.js +10 -0
  27. package/dist/templates/index.d.ts +3 -0
  28. package/dist/templates/index.js +172 -0
  29. package/dist/templates/maps.d.ts +3 -0
  30. package/dist/templates/maps.js +247 -0
  31. package/dist/toolbar/config.d.ts +75 -0
  32. package/dist/toolbar/config.js +206 -0
  33. package/dist/toolbar/icons.d.ts +31 -0
  34. package/{src/toolbar/icons.ts → dist/toolbar/icons.js} +51 -66
  35. package/dist/toolbar/node-config.d.ts +2 -0
  36. package/{src/toolbar/node-config.ts → dist/toolbar/node-config.js} +7 -14
  37. package/dist/toolbar/senior-tool.d.ts +2 -0
  38. package/{src/toolbar/senior-tool.ts → dist/toolbar/senior-tool.js} +5 -5
  39. package/dist/toolbar/wardley-menu.d.ts +53 -0
  40. package/dist/toolbar/wardley-menu.js +408 -0
  41. package/dist/toolbar/wardley-senior-button.d.ts +18 -0
  42. package/dist/toolbar/wardley-senior-button.js +146 -0
  43. package/dist/toolbar/wardley-tool-button.d.ts +10 -0
  44. package/dist/toolbar/wardley-tool-button.js +123 -0
  45. package/dist/view.d.ts +7 -0
  46. package/dist/view.js +36 -0
  47. package/package.json +15 -6
  48. package/src/effects.ts +0 -17
  49. package/src/element-renderer.ts +0 -242
  50. package/src/element-view.ts +0 -143
  51. package/src/gradient.ts +0 -137
  52. package/src/index.ts +0 -1
  53. package/src/label-layout.ts +0 -126
  54. package/src/legend.ts +0 -438
  55. package/src/node/node-renderer.ts +0 -142
  56. package/src/templates/index.ts +0 -236
  57. package/src/templates/maps.ts +0 -283
  58. package/src/toolbar/config.ts +0 -280
  59. package/src/toolbar/wardley-menu.ts +0 -552
  60. package/src/toolbar/wardley-senior-button.ts +0 -154
  61. package/src/view.ts +0 -39
@@ -0,0 +1,408 @@
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 { 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
+ import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
8
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
9
+ 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';
12
+ 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
+ /**
55
+ * 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.
58
+ */
59
+ export class EdgelessWardleyMenu extends EdgelessToolbarToolMixin(LitElement) {
60
+ constructor() {
61
+ super(...arguments);
62
+ this.type = EmptyTool;
63
+ }
64
+ static { this.styles = css `
65
+ :host {
66
+ position: absolute;
67
+ display: flex;
68
+ z-index: -1;
69
+ }
70
+ .menu-content {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ }
75
+ .button-group-container {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 14px;
79
+ fill: var(--affine-icon-color);
80
+ }
81
+ .button-group-container svg {
82
+ width: 24px;
83
+ height: 24px;
84
+ }
85
+ `; }
86
+ _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
+ });
132
+ }
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
+ _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]));
228
+ }
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
+ _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
+ });
318
+ }
319
+ render() {
320
+ return html `
321
+ <edgeless-slide-menu>
322
+ <div class="menu-content">
323
+ <div class="button-group-container">
324
+ <edgeless-tool-icon-button
325
+ .tooltip=${'Wardley map background'}
326
+ @click=${() => this._createBackground('classic')}
327
+ >
328
+ ${wardleyBackgroundIcon}
329
+ </edgeless-tool-icon-button>
330
+ <edgeless-tool-icon-button
331
+ .tooltip=${'Opportunity background (gradient)'}
332
+ @click=${() => this._createBackground('opportunity')}
333
+ >
334
+ ${wardleyOpportunityIcon}
335
+ </edgeless-tool-icon-button>
336
+ <edgeless-tool-icon-button
337
+ .tooltip=${'Benefit / Investment background (gradient)'}
338
+ @click=${() => this._createBackground('benefit')}
339
+ >
340
+ ${wardleyBenefitIcon}
341
+ </edgeless-tool-icon-button>
342
+ <edgeless-tool-icon-button
343
+ .tooltip=${'Evolution background (Wardley presentation)'}
344
+ @click=${() => this._createBackground('evolution-gradient')}
345
+ >
346
+ ${wardleyEvolutionGradientIcon}
347
+ </edgeless-tool-icon-button>
348
+ <edgeless-tool-icon-button
349
+ .tooltip=${'Component'}
350
+ @click=${() => this._createNode('component')}
351
+ >
352
+ ${wardleyComponentIcon}
353
+ </edgeless-tool-icon-button>
354
+ <edgeless-tool-icon-button
355
+ .tooltip=${'Component + method'}
356
+ @click=${() => this._createNode('method')}
357
+ >
358
+ ${wardleyMethodIcon}
359
+ </edgeless-tool-icon-button>
360
+ <edgeless-tool-icon-button
361
+ .tooltip=${'Market'}
362
+ @click=${this._createMarket}
363
+ >
364
+ ${wardleyMarketIcon}
365
+ </edgeless-tool-icon-button>
366
+ <edgeless-tool-icon-button
367
+ .tooltip=${'Ecosystem'}
368
+ @click=${() => this._createNode('ecosystem')}
369
+ >
370
+ ${wardleyEcosystemIcon}
371
+ </edgeless-tool-icon-button>
372
+ <edgeless-tool-icon-button
373
+ .tooltip=${'Anchor'}
374
+ @click=${() => this._createNode('anchor')}
375
+ >
376
+ ${wardleyAnchorIcon}
377
+ </edgeless-tool-icon-button>
378
+ <edgeless-tool-icon-button
379
+ .tooltip=${'Pipeline'}
380
+ @click=${this._createPipeline}
381
+ >
382
+ ${wardleyPipelineIcon}
383
+ </edgeless-tool-icon-button>
384
+ <edgeless-tool-icon-button
385
+ .tooltip=${'Link'}
386
+ @click=${() => this._activateConnector('link')}
387
+ >
388
+ ${wardleyLinkIcon}
389
+ </edgeless-tool-icon-button>
390
+ <edgeless-tool-icon-button
391
+ .tooltip=${'Arrow (evolution)'}
392
+ @click=${() => this._activateConnector('arrow')}
393
+ >
394
+ ${wardleyArrowIcon}
395
+ </edgeless-tool-icon-button>
396
+ <edgeless-tool-icon-button
397
+ .tooltip=${'Inertia'}
398
+ @click=${this._createInertia}
399
+ >
400
+ ${wardleyInertiaIcon}
401
+ </edgeless-tool-icon-button>
402
+ </div>
403
+ </div>
404
+ </edgeless-slide-menu>
405
+ `;
406
+ }
407
+ }
408
+ //# sourceMappingURL=wardley-menu.js.map
@@ -0,0 +1,18 @@
1
+ import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
2
+ import { LitElement } from 'lit';
3
+ declare const EdgelessWardleySeniorButton_base: typeof LitElement & import("@formicoidea/labre-core/global/utils").Constructor<import("@formicoidea/labre-core/widgets/edgeless-toolbar").EdgelessToolbarToolClass>;
4
+ /**
5
+ * Main toolbar button (colored proposal-B icon) that opens the Wardley toolbox
6
+ * sub-menu above the toolbar. Styled like the other senior tools: the tile fills
7
+ * the 96×64 slot, is anchored to the bottom so it "rises from below", and grows
8
+ * slightly on hover.
9
+ */
10
+ export declare class EdgelessWardleySeniorButton extends EdgelessWardleySeniorButton_base {
11
+ static styles: import("lit").CSSResult;
12
+ enableActiveBackground: boolean;
13
+ type: typeof EmptyTool;
14
+ private _toggleMenu;
15
+ render(): import("lit-html").TemplateResult<1>;
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=wardley-senior-button.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
+ import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
+ import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
+ import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
5
+ import { css, html, LitElement } from 'lit';
6
+ import { wardleyToolbarIcon } from './icons';
7
+ /**
8
+ * Main toolbar button (colored proposal-B icon) that opens the Wardley toolbox
9
+ * sub-menu above the toolbar. Styled like the other senior tools: the tile fills
10
+ * the 96×64 slot, is anchored to the bottom so it "rises from below", and grows
11
+ * slightly on hover.
12
+ */
13
+ export class EdgelessWardleySeniorButton extends EdgelessToolbarToolMixin(SignalWatcher(LitElement)) {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.enableActiveBackground = true;
17
+ // `EmptyTool` is only a sentinel for the mixin's abstract `type`; we never
18
+ // activate it. The menu opens as a palette over the default (selection) tool
19
+ // so the canvas stays fully interactive while it is open — mirroring the
20
+ // native Note/Shape sub-menus.
21
+ this.type = EmptyTool;
22
+ }
23
+ static { this.styles = css `
24
+ :host,
25
+ .wardley-button {
26
+ display: block;
27
+ width: 100%;
28
+ height: 100%;
29
+ }
30
+ /* Make this 96px button the containing block of the popup's clip wrapper
31
+ (it is appended to our shadow root) so the sub-menu anchors to THIS
32
+ button — not the whole toolbar — and can be centered over it. */
33
+ :host {
34
+ position: relative;
35
+ }
36
+ .wardley-root {
37
+ width: 100%;
38
+ height: 64px;
39
+ position: relative;
40
+ overflow: hidden;
41
+ cursor: pointer;
42
+ display: flex;
43
+ align-items: flex-end;
44
+ justify-content: center;
45
+ }
46
+ .wardley-card {
47
+ --y: -4px;
48
+ --s: 1;
49
+ position: absolute;
50
+ bottom: 0;
51
+ width: 54px;
52
+ height: 54px;
53
+ transform: translateY(var(--y)) scale(var(--s)); /* base */
54
+ translate: var(--active-x, 0) var(--active-y, 0); /* actif */
55
+ rotate: var(--active-r, -2deg);
56
+ scale: var(--active-s, 1);
57
+ transition: transform 0.3s ease, translate 0.3s ease,
58
+ rotate 0.3s ease, scale 0.3s ease;
59
+ }
60
+ .wardley-card svg {
61
+ display: block;
62
+ width: 100%;
63
+ height: 100%;
64
+ }
65
+ .wardley-root:hover .wardley-card {
66
+ --y: -10px;
67
+ --s: 1.07;
68
+ }
69
+ `; }
70
+ _toggleMenu() {
71
+ // Toggle on popper presence (not tool-active state): the menu stays open on
72
+ // click-outside and only closes on re-click, another senior tool, or Escape.
73
+ if (this.popper) {
74
+ this.popper.dispose();
75
+ this.popper = null;
76
+ return;
77
+ }
78
+ this.setEdgelessTool(DefaultTool);
79
+ const menu = this.createPopper('edgeless-wardley-menu', this);
80
+ menu.element.edgeless = this.edgeless;
81
+ // Anchor the sub-menu to THIS button (the clip wrapper is now button-
82
+ // relative thanks to `:host{position:relative}`): make the menu an in-flow
83
+ // flex item sized to its content, centered over the button. Now that other
84
+ // senior tools (EDGY, Cynefin/Estuarine) sit to the right of Wardley, there
85
+ // is room on both sides, so the menu no longer needs to be pinned to the
86
+ // right edge. Native sub-menus are untouched.
87
+ const el = menu.element;
88
+ const wrap = el.parentElement;
89
+ if (wrap) {
90
+ wrap.style.overflow = 'visible';
91
+ wrap.style.justifyContent = 'center';
92
+ }
93
+ Object.assign(el.style, {
94
+ position: 'static',
95
+ width: 'max-content',
96
+ maxWidth: 'calc(100vw - 16px)',
97
+ marginLeft: '0',
98
+ });
99
+ // The Wardley menu is wide (~13 items). After layout, right-align its right
100
+ // edge to the right edge of the rightmost senior tool (the right end of the
101
+ // senior toolbar), so it fills the space to the right instead of sitting
102
+ // centered with a gap. The menu then extends leftwards.
103
+ requestAnimationFrame(() => {
104
+ const rect = el.getBoundingClientRect();
105
+ // Right edge of the rightmost senior tool slot (scan across shadow roots).
106
+ let target = 0;
107
+ const seen = new Set();
108
+ const scan = (root) => {
109
+ root.querySelectorAll('*').forEach(node => {
110
+ const cls = node.className;
111
+ if (typeof cls === 'string' &&
112
+ cls.split(' ').includes('senior-tool-item')) {
113
+ const b = node.getBoundingClientRect();
114
+ if (b.width > 0)
115
+ target = Math.max(target, b.right);
116
+ }
117
+ const sr = node.shadowRoot;
118
+ if (sr && !seen.has(sr)) {
119
+ seen.add(sr);
120
+ scan(sr);
121
+ }
122
+ });
123
+ };
124
+ scan(document);
125
+ if (target > 0) {
126
+ const dx = Math.round(target - rect.right);
127
+ if (dx)
128
+ el.style.transform = `translateX(${dx}px)`;
129
+ }
130
+ });
131
+ }
132
+ render() {
133
+ return html `<edgeless-toolbar-button
134
+ class="wardley-button"
135
+ .tooltip=${this.popper ? '' : 'Wardley map'}
136
+ .tooltipOffset=${4}
137
+ .active=${!!this.popper}
138
+ @click=${this._toggleMenu}
139
+ >
140
+ <div class="wardley-root">
141
+ <div class="wardley-card">${wardleyToolbarIcon}</div>
142
+ </div>
143
+ </edgeless-toolbar-button>`;
144
+ }
145
+ }
146
+ //# sourceMappingURL=wardley-senior-button.js.map
@@ -0,0 +1,10 @@
1
+ import type { GfxController } from '@blocksuite/std/gfx';
2
+ import { LitElement } from 'lit';
3
+ /** Toolbar button that drops a Wardley map background at the viewport center. */
4
+ export declare class EdgelessWardleyToolButton extends LitElement {
5
+ static styles: import("lit").CSSResult;
6
+ private _addWardley;
7
+ render(): import("lit-html").TemplateResult<1>;
8
+ accessor gfx: GfxController;
9
+ }
10
+ //# sourceMappingURL=wardley-tool-button.d.ts.map