@formicoidea/labre-framework-edgy 0.30.1 → 0.32.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.
@@ -1,7 +1,7 @@
1
1
  import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
2
2
  import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
3
- import { REF_H, REF_W } from '../consts';
4
- import { ACTIVITY_VERTICES, INNER_FONT_SIZE, NODE_FILL, NODE_STROKE, NODE_STROKE_WIDTH, OUTCOME_RADIUS, } from '../node/consts';
3
+ import { CROP, CROP_LABELED } from '../consts.js';
4
+ import { ACTIVITY_VERTICES, INNER_FONT_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, OUTCOME_RADIUS, } from '../node/consts.js';
5
5
  // EDGY facet palette (header / pale sub-card).
6
6
  const C = {
7
7
  identity: ['#1ec873', '#9fe6c2'],
@@ -85,6 +85,24 @@ function line(x1, y1, x2, y2, opts = {}) {
85
85
  target: { position: [x2, y2] },
86
86
  };
87
87
  }
88
+ /**
89
+ * Connector ATTACHED to two template elements (by key): endpoints clip to the
90
+ * element edges and follow moves — unlike `line()`, whose free positions are
91
+ * only right until the user drags something.
92
+ */
93
+ function attach(src, dst, opts = {}) {
94
+ return {
95
+ type: 'connector',
96
+ mode: opts.mode ?? ConnectorMode.Straight,
97
+ stroke: NODE_STROKE,
98
+ strokeWidth: 2,
99
+ strokeStyle: StrokeStyle.Solid,
100
+ frontEndpointStyle: PointStyle.None,
101
+ rearEndpointStyle: opts.arrow ? PointStyle.Triangle : PointStyle.None,
102
+ source: { id: src },
103
+ target: { id: dst },
104
+ };
105
+ }
88
106
  // ── Facets overview (six facets, sub-cards, facets/intersections) ─────
89
107
  function facetsOverview() {
90
108
  const out = {};
@@ -137,8 +155,8 @@ function journey() {
137
155
  use1: label(258, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
138
156
  use2: label(518, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
139
157
  use3: label(778, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
140
- l1: line(350, 248, 350, 392), l2: line(610, 248, 610, 392), l3: line(870, 248, 870, 392),
141
- l4: line(350, 488, 350, 540), l5: line(610, 488, 610, 540), l6: line(870, 488, 870, 540),
158
+ l1: attach('s1', 'c1'), l2: attach('s2', 'c2'), l3: attach('s3', 'c3'),
159
+ l4: attach('c1', 't1'), l5: attach('c2', 't2'), l6: attach('c3', 't3'),
142
160
  };
143
161
  }
144
162
  // ── Service blueprint (six swimlanes) ─────────────────────────────────
@@ -162,14 +180,14 @@ function blueprint() {
162
180
  csv: chev(280, 514, B, 'Customer service'), cfu: box(1010, 514, B, 'Customer follow-up'),
163
181
  pf: chev(600, 664, B, 'Process Form'), dec: chev(900, 664, B, 'Decision [Accept|Reject]'),
164
182
  crm: box(280, 814, B, 'CRM Application'), cms: box(600, 814, B, 'Case Management'), pay: box(900, 814, B, 'Payments System'),
165
- a1: line(380, 134, 380, 214, { arrow: true }),
166
- a2: line(480, 244, 600, 244, { arrow: true }),
167
- a3: line(800, 244, 1010, 244, { arrow: true }),
168
- a4: line(320, 274, 320, 364, { arrow: true }),
169
- a5: line(320, 424, 320, 514, { arrow: true }),
170
- a6: line(700, 724, 900, 694, { arrow: true }),
171
- a7: line(380, 874, 600, 844, { arrow: true }),
172
- a8: line(800, 844, 900, 844, { arrow: true }),
183
+ a1: attach('af', 'sf', { arrow: true }),
184
+ a2: attach('sf', 'fs', { arrow: true }),
185
+ a3: attach('fs', 'rd', { arrow: true }),
186
+ a4: attach('sf', 'fh', { arrow: true }),
187
+ a5: attach('fh', 'csv', { arrow: true }),
188
+ a6: attach('pf', 'dec', { arrow: true }),
189
+ a7: attach('crm', 'cms', { arrow: true }),
190
+ a8: attach('cms', 'pay', { arrow: true }),
173
191
  });
174
192
  return out;
175
193
  }
@@ -181,18 +199,147 @@ function orgChart() {
181
199
  org: u(420, 40, 180, 'Organisation'),
182
200
  a: u(120, 200, 200, 'Business Unit A'), b: u(410, 200, 200, 'Business Unit B'), c: u(700, 200, 200, 'Business Unit C'),
183
201
  a1: u(60, 360, 170, 'Group A-1'), a2: u(260, 360, 170, 'Group A-2'), c1: u(715, 360, 170, 'Group C-1'),
184
- e1: line(510, 104, 510, 150), e2: line(220, 150, 800, 150),
185
- e3: line(220, 150, 220, 200), e4: line(510, 150, 510, 200), e5: line(800, 150, 800, 200),
186
- e6: line(220, 264, 220, 320), e7: line(145, 320, 345, 320),
187
- e8: line(145, 320, 145, 360), e9: line(345, 320, 345, 360),
188
- e10: line(800, 264, 800, 360),
202
+ // Attached orthogonal connectors: the router draws the org-chart elbows
203
+ // and the links follow when units are moved around.
204
+ e1: attach('org', 'a', { mode: ConnectorMode.Orthogonal }),
205
+ e2: attach('org', 'b', { mode: ConnectorMode.Orthogonal }),
206
+ e3: attach('org', 'c', { mode: ConnectorMode.Orthogonal }),
207
+ e4: attach('a', 'a1', { mode: ConnectorMode.Orthogonal }),
208
+ e5: attach('a', 'a2', { mode: ConnectorMode.Orthogonal }),
209
+ e6: attach('c', 'c1', { mode: ConnectorMode.Orthogonal }),
189
210
  };
190
211
  }
212
+ // ── EDGY dynamic (the relational metamodel, on the facets background) ──
213
+ /**
214
+ * Background scale (from REF coords to model coords): large enough that the
215
+ * default-size nodes breathe inside each zone. The background is cropped to
216
+ * the circles (`cropToCircles`), so its xywh covers CROP × DYN_SCALE.
217
+ */
218
+ export const DYN_SCALE = 4.8;
219
+ /**
220
+ * The 24 canonical EDGY relations (source, target, verb, label position along
221
+ * the link) — exported for the unit tests. 7 per facet + 3 between the
222
+ * intersections. The optional 4th member mirrors the reference diagram's
223
+ * placements: verbs of intersection-outgoing links sit near the far element
224
+ * (`labelOffset.distance` ≈ .75), short peer links keep the middle.
225
+ */
226
+ export const EDGY_DYNAMIC_RELATIONS = [
227
+ // Identity
228
+ ['content', 'purpose', 'expresses'],
229
+ ['content', 'story', 'conveys', 0.75],
230
+ ['story', 'purpose', 'contextualises', 0.8],
231
+ ['organisation', 'purpose', 'pursues', 0.8],
232
+ ['organisation', 'story', 'authors', 0.6],
233
+ ['brand', 'purpose', 'represents', 0.8],
234
+ ['brand', 'story', 'evokes', 0.65],
235
+ // Architecture
236
+ ['organisation', 'process', 'performs', 0.8],
237
+ ['process', 'capability', 'realises', 0.75],
238
+ ['process', 'asset', 'requires'],
239
+ ['capability', 'asset', 'requires', 0.75],
240
+ ['organisation', 'capability', 'has', 0.6],
241
+ ['product', 'capability', 'requires', 0.75],
242
+ ['process', 'product', 'creates', 0.65],
243
+ // Experience
244
+ ['task', 'journey', 'is part of', 0.6],
245
+ ['task', 'channel', 'uses', 0.6],
246
+ ['journey', 'channel', 'traverses', 0.6],
247
+ ['product', 'task', 'serves', 0.9],
248
+ ['product', 'journey', 'features in', 0.8],
249
+ ['brand', 'task', 'supports', 0.8],
250
+ ['brand', 'journey', 'appears in', 0.9],
251
+ // Intersections
252
+ ['organisation', 'brand', 'builds', 0.85],
253
+ ['organisation', 'product', 'makes', 0.65],
254
+ ['product', 'brand', 'embodies', 0.8],
255
+ ];
256
+ /**
257
+ * The 12 elements, centred coordinates in REFERENCE coords (the fixed space
258
+ * of consts.ts — the same space as `VENN`), laid out like the reference
259
+ * "elements & relations" diagram: aligned top row, Story/Capability flanks,
260
+ * Brand/Product astride the white centre, Task/Journey/Channel triangle.
261
+ * Exported (with {@link dynToModel}) for the containment test.
262
+ */
263
+ /** Official pastel fills per facet (the `pictograms/Shape-*.svg` colors). */
264
+ const PASTEL = {
265
+ identity: '#80ffb7',
266
+ architecture: '#a6c0ff',
267
+ experience: '#ff99bd',
268
+ organisation: '#80eaff',
269
+ brand: '#ffd580',
270
+ product: '#e599ff',
271
+ };
272
+ export const EDGY_DYNAMIC_NODES = {
273
+ content: { kind: 'object', cx: 237.5, cy: 100, fill: PASTEL.identity },
274
+ purpose: { kind: 'outcome', cx: 282.5, cy: 100, fill: PASTEL.identity },
275
+ organisation: { kind: 'object', cx: 340, cy: 100, w: 175, fill: PASTEL.organisation },
276
+ process: { kind: 'activity', cx: 397.5, cy: 100, fill: PASTEL.architecture },
277
+ asset: { kind: 'object', cx: 442.5, cy: 100, fill: PASTEL.architecture },
278
+ story: { kind: 'activity', cx: 255, cy: 152.5, fill: PASTEL.identity },
279
+ capability: { kind: 'outcome', cx: 425, cy: 152.5, w: 150, fill: PASTEL.architecture },
280
+ brand: { kind: 'object', cx: 280, cy: 195, fill: PASTEL.brand },
281
+ product: { kind: 'object', cx: 400, cy: 195, fill: PASTEL.product },
282
+ task: { kind: 'outcome', cx: 310, cy: 257.5, fill: PASTEL.experience },
283
+ journey: { kind: 'activity', cx: 370, cy: 257.5, fill: PASTEL.experience },
284
+ channel: { kind: 'object', cx: 340, cy: 297.5, fill: PASTEL.experience },
285
+ };
286
+ /**
287
+ * Reference coords → template model coords: the background element sits at
288
+ * (0,0) and renders the cropped circles box, so a reference point maps to
289
+ * `(p - CROP.origin) × DYN_SCALE`.
290
+ */
291
+ export function dynToModel(refX, refY) {
292
+ return [(refX - CROP.x) * DYN_SCALE, (refY - CROP.y) * DYN_SCALE];
293
+ }
294
+ function dynamic() {
295
+ const out = {
296
+ bg: {
297
+ type: 'edgy',
298
+ showLabels: false,
299
+ showPictos: false,
300
+ cropToCircles: true,
301
+ xywh: `[0,0,${CROP.w * DYN_SCALE},${CROP.h * DYN_SCALE}]`,
302
+ },
303
+ };
304
+ for (const [key, { kind, cx, cy, w, fill }] of Object.entries(EDGY_DYNAMIC_NODES)) {
305
+ const nw = w ?? NODE_SIZE[kind].w;
306
+ const nh = NODE_SIZE[kind].h;
307
+ const [mx, my] = dynToModel(cx, cy);
308
+ const name = key.charAt(0).toUpperCase() + key.slice(1);
309
+ out[key] = enode(kind, mx - nw / 2, my - nh / 2, nw, nh, {
310
+ text: name,
311
+ fill,
312
+ });
313
+ }
314
+ EDGY_DYNAMIC_RELATIONS.forEach(([src, dst, verb, t], i) => {
315
+ out[`rel${i}`] = {
316
+ type: 'connector',
317
+ mode: ConnectorMode.Straight,
318
+ stroke: NODE_STROKE,
319
+ strokeWidth: 2,
320
+ strokeStyle: StrokeStyle.Solid,
321
+ frontEndpointStyle: PointStyle.None,
322
+ rearEndpointStyle: PointStyle.None,
323
+ source: { id: src },
324
+ target: { id: dst },
325
+ // Native connector label: the verb travels with the link. The x/y are
326
+ // re-centred on the path at the first layout, but the w/h ARE the label
327
+ // box — size it to the verb so the text lays out on one line. The
328
+ // distance slides the verb along the link like the reference diagram.
329
+ text: surfaceText(verb),
330
+ labelXYWH: [0, 0, verb.length * 9 + 24, 26],
331
+ labelOffset: { distance: t ?? 0.5 },
332
+ };
333
+ });
334
+ return out;
335
+ }
191
336
  const single = (el) => ({ a: el });
192
337
  const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
193
338
  function tpl(name, preview, elements) {
194
339
  return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
195
340
  }
341
+ /** Exported for direct insertion from the EDGY senior toolbar menu. */
342
+ export const edgyDynamicTemplate = tpl('EDGY dynamic', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/><path d="M55 30 H80 M56 31 L66 52 M79 31 L69 52" stroke="#fff" stroke-width="2"/><rect x="51" y="26" width="8" height="8" fill="#fff"/><circle cx="80" cy="30" r="4" fill="#fff"/><path d="M62 49 h6 l3 3 -3 3 h-6 z" fill="#fff"/></svg>`, dynamic());
196
343
  export const edgyTemplateCategory = {
197
344
  name: 'EDGY',
198
345
  templates: [
@@ -200,7 +347,8 @@ export const edgyTemplateCategory = {
200
347
  tpl('Customer journey', `<svg ${ATTRS} fill="none"><path d="M20 24 H110 L122 40 L110 56 H20 Z" fill="#f3a3c0"/><rect x="26" y="30" width="22" height="20" fill="none" stroke="#fff"/><rect x="54" y="30" width="22" height="20" fill="none" stroke="#fff"/><rect x="82" y="30" width="22" height="20" fill="none" stroke="#fff"/></svg>`, journey()),
201
348
  tpl('Service blueprint', `<svg ${ATTRS} fill="none"><rect x="8" y="14" width="119" height="16" fill="#fbd5e0"/><rect x="8" y="32" width="119" height="34" fill="#d4e9f8"/><rect x="20" y="18" width="22" height="9" fill="#f5246e" opacity="0.5"/><rect x="20" y="40" width="22" height="9" fill="#2f6ff0" opacity="0.4"/><rect x="60" y="40" width="22" height="9" fill="#2f6ff0" opacity="0.4"/></svg>`, blueprint()),
202
349
  tpl('Organisation chart', `<svg ${ATTRS} fill="none"><rect x="52" y="12" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="14" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="52" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="90" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><path d="M68 26 V32 M30 32 H106 M30 32 V38 M68 32 V38 M106 32 V38" stroke="#262626"/></svg>`, orgChart()),
203
- tpl('Facets diagram', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/></svg>`, single({ type: 'edgy', xywh: `[0,0,${REF_W * 1.5},${REF_H * 1.5}]` })),
350
+ tpl('Facets diagram', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/></svg>`, single({ type: 'edgy', cropToCircles: true, xywh: `[0,0,${CROP_LABELED.w * 1.5},${CROP_LABELED.h * 1.5}]` })),
351
+ edgyDynamicTemplate,
204
352
  tpl('People', `<svg ${ATTRS} fill="#262626"><circle cx="67" cy="32" r="9" fill="none" stroke="#262626" stroke-width="2.4"/><path d="M50 60 a17 17 0 0 1 34 0" fill="none" stroke="#262626" stroke-width="2.4"/></svg>`, { n: enode('people', 0, 0, 64, 64), l: label(-28, 70, 120, 24, 'People', { fontSize: 16 }) }),
205
353
  tpl('Outcome', `<svg ${ATTRS} fill="none"><rect x="20" y="24" width="95" height="34" rx="6" stroke="#262626" stroke-width="2"/></svg>`, single(enode('outcome', 0, 0, 130, 80, { text: 'Outcome' }))),
206
354
  tpl('Object', `<svg ${ATTRS} fill="none"><rect x="20" y="24" width="95" height="34" stroke="#262626" stroke-width="2"/></svg>`, single(enode('object', 0, 0, 130, 80, { text: 'Object' }))),
@@ -13,7 +13,30 @@ export declare const edgyToolbarConfig: {
13
13
  icon: TemplateResult;
14
14
  active(ctx: ToolbarContext): boolean;
15
15
  run(ctx: ToolbarContext): void;
16
+ }, {
17
+ id: string;
18
+ tooltip: string;
19
+ icon: TemplateResult;
20
+ active(ctx: ToolbarContext): boolean;
21
+ run(ctx: ToolbarContext): void;
16
22
  }];
17
23
  readonly when: (ctx: ToolbarContext) => boolean;
18
24
  };
19
25
  export declare const edgyToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
26
+ export declare const edgyBoardToolbarConfig: {
27
+ readonly actions: [{
28
+ id: string;
29
+ tooltip: string;
30
+ icon: TemplateResult;
31
+ active(ctx: ToolbarContext): boolean;
32
+ run(ctx: ToolbarContext): void;
33
+ }, {
34
+ id: string;
35
+ tooltip: string;
36
+ icon: TemplateResult;
37
+ active(ctx: ToolbarContext): boolean;
38
+ run(ctx: ToolbarContext): void;
39
+ }];
40
+ readonly when: (ctx: ToolbarContext) => boolean;
41
+ };
42
+ export declare const edgyBoardToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
@@ -1,5 +1,5 @@
1
1
  import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
2
- import { EdgyFacetsElementModel } from '@formicoidea/labre-core/model';
2
+ import { EdgyBoardElementModel, EdgyFacetsElementModel, } from '@formicoidea/labre-core/model';
3
3
  import { ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
4
4
  import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
5
5
  import { html } from 'lit';
@@ -30,37 +30,56 @@ const LabelsIcon = html `<svg
30
30
  <path d="M3 8.5l6-4.5 12 0 0 16-12 0-6-4.5z" />
31
31
  <circle cx="8" cy="12" r="1.4" />
32
32
  </svg>`;
33
+ /** Spot with rays — enable / disable the hover spotlight. */
34
+ const SpotlightIcon = html `<svg
35
+ width="24"
36
+ height="24"
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-width="1.6"
41
+ stroke-linecap="round"
42
+ >
43
+ <circle cx="12" cy="12" r="3.4" />
44
+ <path
45
+ d="M12 4v2.5M12 17.5V20M4 12h2.5M17.5 12H20M6.3 6.3l1.8 1.8M15.9 15.9l1.7 1.7M17.7 6.3l-1.8 1.8M8.1 15.9l-1.7 1.7"
46
+ />
47
+ </svg>`;
33
48
  /**
34
- * Build a toolbar toggle that flips a boolean flag on every selected facets
35
- * diagram: `active` reflects the current state, `run` flips it (with an undo
36
- * checkpoint).
49
+ * Build a toolbar toggle that flips a boolean flag on every selected element
50
+ * of the given class: `active` reflects the current state, `run` flips it
51
+ * (with an undo checkpoint).
37
52
  */
38
- function booleanToggle(id, tooltip, icon, prop) {
53
+ function booleanToggle(Model, id, tooltip, icon, prop) {
54
+ const models = (ctx) => ctx.getSurfaceModelsByType(Model);
39
55
  return {
40
56
  id,
41
57
  tooltip,
42
58
  icon,
43
59
  active(ctx) {
44
- const models = ctx.getSurfaceModelsByType(EdgyFacetsElementModel);
45
- return models.length > 0 && models.every(model => model[prop]);
60
+ const all = models(ctx);
61
+ return all.length > 0 && all.every(model => model[prop]);
46
62
  },
47
63
  run(ctx) {
48
- const models = ctx.getSurfaceModelsByType(EdgyFacetsElementModel);
49
- if (!models.length)
64
+ const all = models(ctx);
65
+ if (!all.length)
50
66
  return;
51
- const enable = !models.every(model => model[prop]);
67
+ const enable = !all.every(model => model[prop]);
52
68
  ctx.std.store.captureSync();
53
69
  const crud = ctx.std.get(EdgelessCRUDIdentifier);
54
- for (const model of models) {
55
- crud.updateElement(model.id, { [prop]: enable });
70
+ for (const model of all) {
71
+ crud.updateElement(model.id, {
72
+ [prop]: enable,
73
+ });
56
74
  }
57
75
  },
58
76
  };
59
77
  }
60
78
  export const edgyToolbarConfig = {
61
79
  actions: [
62
- booleanToggle('a.toggle-resize', 'Enable / lock resizing', ResizeIcon, 'resizeEnabled'),
63
- booleanToggle('b.toggle-labels', 'Show / hide facet labels', LabelsIcon, 'showLabels'),
80
+ booleanToggle(EdgyFacetsElementModel, 'a.toggle-resize', 'Enable / lock resizing', ResizeIcon, 'resizeEnabled'),
81
+ booleanToggle(EdgyFacetsElementModel, 'b.toggle-labels', 'Show / hide facet labels', LabelsIcon, 'showLabels'),
82
+ booleanToggle(EdgyFacetsElementModel, 'c.toggle-spotlight', 'Enable / disable hover spotlight', SpotlightIcon, 'spotlightEnabled'),
64
83
  ],
65
84
  when: ctx => ctx.getSurfaceModelsByType(EdgyFacetsElementModel).length > 0,
66
85
  };
@@ -68,3 +87,14 @@ export const edgyToolbarExtension = ToolbarModuleExtension({
68
87
  id: BlockFlavourIdentifier('affine:surface:edgy'),
69
88
  config: edgyToolbarConfig,
70
89
  });
90
+ export const edgyBoardToolbarConfig = {
91
+ actions: [
92
+ booleanToggle(EdgyBoardElementModel, 'a.toggle-resize', 'Enable / lock resizing', ResizeIcon, 'resizeEnabled'),
93
+ booleanToggle(EdgyBoardElementModel, 'b.toggle-spotlight', 'Enable / disable hover spotlight', SpotlightIcon, 'spotlightEnabled'),
94
+ ],
95
+ when: ctx => ctx.getSurfaceModelsByType(EdgyBoardElementModel).length > 0,
96
+ };
97
+ export const edgyBoardToolbarExtension = ToolbarModuleExtension({
98
+ id: BlockFlavourIdentifier('affine:surface:edgyBoard'),
99
+ config: edgyBoardToolbarConfig,
100
+ });
@@ -1,30 +1,12 @@
1
1
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
2
- import { LitElement } from 'lit';
3
- declare const EdgelessEdgyMenu_base: typeof LitElement & import("@formicoidea/labre-core/_pkgs/global/utils").Constructor<import("@formicoidea/labre-core/_pkgs/affine-widget-edgeless-toolbar").EdgelessToolbarToolClass>;
2
+ import { EdgelessCommandMenu } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
3
  /**
5
- * The popover above the toolbar for the EDGY toolbox. Items create the facets
6
- * diagram (the on-click Venn) and the four base-element prefab shapes — all
7
- * native shapes so they stay editable.
4
+ * The popover above the toolbar for the EDGY toolbox. Since PF3 it declares
5
+ * nothing: {@link EdgelessCommandMenu} renders `edgyCommands` for the
6
+ * `senior-menu` surface, and the same seven artefacts are now reachable from
7
+ * Settings › Shortcuts, which never saw them before (`docs/adr/0008`).
8
8
  */
9
- export declare class EdgelessEdgyMenu extends EdgelessEdgyMenu_base {
10
- static styles: import("lit").CSSResult;
9
+ export declare class EdgelessEdgyMenu extends EdgelessCommandMenu {
10
+ protected owner: "edgy";
11
11
  type: typeof EmptyTool;
12
- /** Create the Enterprise Design Facets diagram centred on the viewport. */
13
- private _createFacets;
14
- /** Add a native free-text label (Inter), used for the People node. */
15
- private _addLabel;
16
- private _group;
17
- /** Shared props for an EDGY node shape. */
18
- private _baseShapeProps;
19
- /** Create a box base element (outcome / object / activity) with inner text. */
20
- private _createBox;
21
- /**
22
- * Create the People base element: an (invisible) ellipse decorated with the
23
- * person glyph by the renderer, plus a native text label below, grouped.
24
- */
25
- private _createPeople;
26
- private _finish;
27
- private _track;
28
- render(): import("lit-html").TemplateResult<1>;
29
12
  }
30
- export {};
@@ -1,194 +1,15 @@
1
- import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
- import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
3
1
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
4
- import { FontFamily, ShapeStyle } from '@formicoidea/labre-core/model';
5
- import { TelemetryProvider } from '@formicoidea/labre-core/shared/services';
6
- import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
7
- import { Bound } from '@formicoidea/labre-core/global/gfx';
8
- import { css, html, LitElement } from 'lit';
9
- import { REF_H, REF_W } from '../consts';
10
- import { ACTIVITY_VERTICES, INNER_FONT_SIZE, LABEL_FONT_SIZE, LABEL_GAP, NODE_FILL, NODE_LABEL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, OUTCOME_RADIUS, } from '../node/consts';
11
- import { edgyActivityIcon, edgyFacetsIcon, edgyObjectIcon, edgyOutcomeIcon, edgyPeopleIcon, } from './icons';
12
- /** Default facets-diagram size (REF aspect, scaled up so it reads on canvas). */
13
- const FACETS_SCALE = 1.5;
14
- /** Height of the native People free-text label. */
15
- const LABEL_H = LABEL_FONT_SIZE + 8;
2
+ import { EdgelessCommandMenu } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
16
3
  /**
17
- * The popover above the toolbar for the EDGY toolbox. Items create the facets
18
- * diagram (the on-click Venn) and the four base-element prefab shapes — all
19
- * native shapes so they stay editable.
4
+ * The popover above the toolbar for the EDGY toolbox. Since PF3 it declares
5
+ * nothing: {@link EdgelessCommandMenu} renders `edgyCommands` for the
6
+ * `senior-menu` surface, and the same seven artefacts are now reachable from
7
+ * Settings › Shortcuts, which never saw them before (`docs/adr/0008`).
20
8
  */
21
- export class EdgelessEdgyMenu extends EdgelessToolbarToolMixin(LitElement) {
9
+ export class EdgelessEdgyMenu extends EdgelessCommandMenu {
22
10
  constructor() {
23
11
  super(...arguments);
12
+ this.owner = 'edgy';
24
13
  this.type = EmptyTool;
25
14
  }
26
- static { this.styles = css `
27
- :host {
28
- display: flex;
29
- z-index: -1;
30
- justify-content: center;
31
- }
32
- .menu-content {
33
- display: flex;
34
- align-items: center;
35
- justify-content: center;
36
- }
37
- .button-group-container {
38
- display: flex;
39
- align-items: center;
40
- gap: 14px;
41
- fill: var(--affine-icon-color);
42
- }
43
- .button-group-container svg {
44
- width: 24px;
45
- height: 24px;
46
- }
47
- `; }
48
- /** Create the Enterprise Design Facets diagram centred on the viewport. */
49
- _createFacets() {
50
- const { gfx } = this;
51
- if (!gfx.surface)
52
- return;
53
- const width = REF_W * FACETS_SCALE;
54
- const height = REF_H * FACETS_SCALE;
55
- const { centerX, centerY } = gfx.viewport;
56
- const id = gfx.surface.addElement({
57
- type: 'edgy',
58
- xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
59
- });
60
- this._track('facets');
61
- this._finish(id);
62
- }
63
- /** Add a native free-text label (Inter), used for the People node. */
64
- _addLabel(surface, text, x, y) {
65
- return surface.addElement({
66
- type: 'text',
67
- text,
68
- fontFamily: FontFamily.Inter,
69
- fontSize: LABEL_FONT_SIZE,
70
- color: NODE_STROKE,
71
- textAlign: 'center',
72
- xywh: new Bound(x, y, 120, LABEL_H).serialize(),
73
- });
74
- }
75
- _group(ids) {
76
- const [, result] = this.edgeless.std.command.exec(createGroupCommand, {
77
- elements: ids,
78
- });
79
- return result.groupId || ids[0];
80
- }
81
- /** Shared props for an EDGY node shape. */
82
- _baseShapeProps(kind) {
83
- return {
84
- type: 'edgyNode',
85
- kind,
86
- filled: true,
87
- fillColor: NODE_FILL,
88
- strokeColor: NODE_STROKE,
89
- shapeStyle: ShapeStyle.General,
90
- roughness: 0,
91
- };
92
- }
93
- /** Create a box base element (outcome / object / activity) with inner text. */
94
- _createBox(kind) {
95
- const surface = this.gfx.surface;
96
- if (!surface)
97
- return;
98
- const { w, h } = NODE_SIZE[kind];
99
- const { centerX: cx, centerY: cy } = this.gfx.viewport;
100
- const shapeType = kind === 'activity' ? 'polygon' : 'rect';
101
- const id = surface.addElement({
102
- ...this._baseShapeProps(kind),
103
- shapeType,
104
- strokeWidth: NODE_STROKE_WIDTH,
105
- radius: kind === 'outcome' ? OUTCOME_RADIUS : 0,
106
- vertices: kind === 'activity' ? ACTIVITY_VERTICES : null,
107
- text: NODE_LABEL[kind],
108
- color: NODE_STROKE,
109
- fontFamily: FontFamily.Inter,
110
- fontSize: INNER_FONT_SIZE,
111
- textAlign: 'center',
112
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
113
- });
114
- this._track(`node:${kind}`);
115
- this._finish(id);
116
- }
117
- /**
118
- * Create the People base element: an (invisible) ellipse decorated with the
119
- * person glyph by the renderer, plus a native text label below, grouped.
120
- */
121
- _createPeople() {
122
- const surface = this.gfx.surface;
123
- if (!surface)
124
- return;
125
- const { w, h } = NODE_SIZE.people;
126
- const { centerX: cx, centerY: cy } = this.gfx.viewport;
127
- const nodeId = surface.addElement({
128
- ...this._baseShapeProps('people'),
129
- shapeType: 'ellipse',
130
- // No visible outline — People is just the glyph; the ellipse is the bound.
131
- strokeWidth: 0,
132
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
133
- });
134
- const labelId = this._addLabel(surface, NODE_LABEL.people, cx - 60, cy + h / 2 + LABEL_GAP);
135
- this._track('node:people');
136
- this._finish(this._group([nodeId, labelId]));
137
- }
138
- _finish(id) {
139
- const { gfx } = this;
140
- gfx.doc.captureSync();
141
- gfx.tool.setTool(DefaultTool);
142
- gfx.selection.set({ elements: [id], editing: false });
143
- // Keep the palette open (native sub-menu behaviour).
144
- }
145
- _track(element) {
146
- this.edgeless.std.getOptional(TelemetryProvider)?.track('FrameworkElementAdded', {
147
- framework: 'edgy',
148
- element,
149
- page: 'whiteboard editor',
150
- segment: 'edgy toolbox',
151
- module: 'edgy menu',
152
- });
153
- }
154
- render() {
155
- return html `
156
- <edgeless-slide-menu>
157
- <div class="menu-content">
158
- <div class="button-group-container">
159
- <edgeless-tool-icon-button
160
- .tooltip=${'Enterprise Design facets'}
161
- @click=${this._createFacets}
162
- >
163
- ${edgyFacetsIcon}
164
- </edgeless-tool-icon-button>
165
- <edgeless-tool-icon-button
166
- .tooltip=${'People'}
167
- @click=${this._createPeople}
168
- >
169
- ${edgyPeopleIcon}
170
- </edgeless-tool-icon-button>
171
- <edgeless-tool-icon-button
172
- .tooltip=${'Outcome'}
173
- @click=${() => this._createBox('outcome')}
174
- >
175
- ${edgyOutcomeIcon}
176
- </edgeless-tool-icon-button>
177
- <edgeless-tool-icon-button
178
- .tooltip=${'Object'}
179
- @click=${() => this._createBox('object')}
180
- >
181
- ${edgyObjectIcon}
182
- </edgeless-tool-icon-button>
183
- <edgeless-tool-icon-button
184
- .tooltip=${'Activity'}
185
- @click=${() => this._createBox('activity')}
186
- >
187
- ${edgyActivityIcon}
188
- </edgeless-tool-icon-button>
189
- </div>
190
- </div>
191
- </edgeless-slide-menu>
192
- `;
193
- }
194
15
  }
@@ -3,7 +3,7 @@ import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
3
  import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
4
  import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
5
5
  import { css, html, LitElement } from 'lit';
6
- import { edgyToolbarIcon } from './icons';
6
+ import { edgyToolbarIcon } from './icons.js';
7
7
  /**
8
8
  * Main toolbar button (colored facets glyph) that opens the EDGY toolbox
9
9
  * sub-menu above the toolbar. Mirrors the Wardley senior button.
@@ -2,6 +2,10 @@
2
2
  export declare const edgyToolbarIcon: import("lit-html").TemplateResult<2>;
3
3
  /** Menu icon — the facets diagram (colored mini Venn). */
4
4
  export declare const edgyFacetsIcon: import("lit-html").TemplateResult<2>;
5
+ /** Menu icon — the dynamic diagram: mini Venn + three connected white shapes. */
6
+ export declare const edgyDynamicIcon: import("lit-html").TemplateResult<2>;
7
+ /** Menu icon — the blank board: rounded rect + connected facet-colored shapes. */
8
+ export declare const edgyBoardIcon: import("lit-html").TemplateResult<2>;
5
9
  /** People — person glyph (official Icon-People), uses currentColor. */
6
10
  export declare const edgyPeopleIcon: import("lit-html").TemplateResult<2>;
7
11
  /** Outcome — lightly rounded rectangle. */
@@ -13,6 +13,24 @@ export const edgyFacetsIcon = svg `<svg width="24" height="24" viewBox="0 0 24 2
13
13
  <circle cx="14.5" cy="10" r="6" fill="#034cee" opacity="0.92"/>
14
14
  <circle cx="12" cy="14.5" r="6" fill="#ff0056" opacity="0.92"/>
15
15
  </svg>`;
16
+ /** Menu icon — the dynamic diagram: mini Venn + three connected white shapes. */
17
+ export const edgyDynamicIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
18
+ <circle cx="9.5" cy="10" r="6" fill="#00ea4e" opacity="0.92"/>
19
+ <circle cx="14.5" cy="10" r="6" fill="#034cee" opacity="0.92"/>
20
+ <circle cx="12" cy="14.5" r="6" fill="#ff0056" opacity="0.92"/>
21
+ <path d="M8.5 8.5 L15.5 8.5 M8.5 8.5 L12 16 M15.5 8.5 L12 16" stroke="#fff" stroke-width="1.3"/>
22
+ <rect x="6.9" y="6.9" width="3.2" height="3.2" fill="#fff"/>
23
+ <circle cx="15.5" cy="8.5" r="1.7" fill="#fff"/>
24
+ <path d="M10.4 14.9 H12.9 L14 16 L12.9 17.1 H10.4 Z" fill="#fff"/>
25
+ </svg>`;
26
+ /** Menu icon — the blank board: rounded rect + connected facet-colored shapes. */
27
+ export const edgyBoardIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
28
+ <rect x="3" y="4.5" width="18" height="15" rx="2.5" stroke="currentColor" stroke-width="1.4"/>
29
+ <path d="M8.3 9 L15.7 9 M8.3 9 L12 15.2 M15.7 9 L12 15.2" stroke="currentColor" stroke-width="1.1"/>
30
+ <rect x="6.8" y="7.5" width="3" height="3" fill="#00ea4e"/>
31
+ <circle cx="15.7" cy="9" r="1.7" fill="#034cee"/>
32
+ <path d="M10.5 14.1 H12.8 L13.9 15.2 L12.8 16.3 H10.5 Z" fill="#ff0056"/>
33
+ </svg>`;
16
34
  /** People — person glyph (official Icon-People), uses currentColor. */
17
35
  export const edgyPeopleIcon = svg `<svg width="24" height="24" viewBox="0 0 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
18
36
  <path d="m16,19c-3.308,0-6-2.692-6-6v-4c0-3.308,2.692-6,6-6s6,2.692,6,6v4c0,3.308-2.692,6-6,6Zm0-14c-2.206,0-4,1.794-4,4v4c0,2.206,1.794,4,4,4s4-1.794,4-4v-4c0-2.206-1.794-4-4-4Z"/>