@formicoidea/labre-ddd-shared 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './shared/consts.js';
2
+ export * from './shared/legend-auto.js';
2
3
  export * from './shared/prefabs.js';
3
4
  export * from './shared/utils.js';
4
- export { DddMenuBase } from './toolbar/menu-base.js';
5
5
  export * from './toolbar/icons.js';
6
6
  export { DddSeniorButtonBase } from './toolbar/senior-buttons.js';
7
7
  export { CD_TEMPLATES, CM_TEMPLATES, ES_TEMPLATES, } from './templates/components.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './shared/consts.js';
2
+ export * from './shared/legend-auto.js';
2
3
  export * from './shared/prefabs.js';
3
4
  export * from './shared/utils.js';
4
- export { DddMenuBase } from './toolbar/menu-base.js';
5
5
  export * from './toolbar/icons.js';
6
6
  export { DddSeniorButtonBase } from './toolbar/senior-buttons.js';
7
7
  export { CD_TEMPLATES, CM_TEMPLATES, ES_TEMPLATES, } from './templates/components.js';
@@ -13,7 +13,31 @@ export declare const SHADOW_COLOR = "#0000002e";
13
13
  export declare const STICKY_FONT = "blocksuite:surface:Kalam";
14
14
  export declare const STICKY_FONT_SIZE = 20;
15
15
  export declare const STICKY_RADIUS = 6;
16
- /** Event Storming sticky palette — DDD Crew / Brandolini colour code. */
16
+ /**
17
+ * Event Storming sticky palette — DDD Crew / Brandolini colour code.
18
+ *
19
+ * ## The aggregate (WS5)
20
+ *
21
+ * Brandolini's notation has a pale-yellow sticky for the AGGREGATE — the thing a
22
+ * command lands on and the thing that raises the event — and without it the
23
+ * canonical sentence `Command → Aggregate → Domain event` cannot be drawn at
24
+ * all, let alone checked. It was missing here until WS5.
25
+ *
26
+ * Its colour is the one judgement call in this table. The plan's indicative
27
+ * `#FDF0A0` measures **ΔE 3.5** from the actor's `#FFF1A8` — two stickies
28
+ * nobody could tell apart on a wall, which is exactly the failure the notation
29
+ * already has and the tool should not inherit. `#FAF2C9` is the same pale
30
+ * yellow taken two steps towards cream: ΔE 16.3 from the actor, 50.2 from the
31
+ * constraint, and still 21.7 from the white board it sits on, so it reads as
32
+ * "pale" rather than as "blank". The three yellows are then a LADDER — the
33
+ * constraint saturated, the actor light, the aggregate palest — which is how a
34
+ * reader tells them apart at a glance without a legend.
35
+ *
36
+ * Size carries the rest of the distinction, as it does on a real wall: the
37
+ * aggregate is created at 160 against the standard 120 (see
38
+ * `ddd-event-storming/src/commands.ts`). The palette says which; the format
39
+ * says how big.
40
+ */
17
41
  export declare const ES_STICKIES: readonly [{
18
42
  readonly kind: "domainEvent";
19
43
  readonly label: "Domain event";
@@ -24,6 +48,11 @@ export declare const ES_STICKIES: readonly [{
24
48
  readonly label: "Command";
25
49
  readonly fill: "#5BA3DB";
26
50
  readonly text: "#06304d";
51
+ }, {
52
+ readonly kind: "aggregate";
53
+ readonly label: "Aggregate";
54
+ readonly fill: "#FAF2C9";
55
+ readonly text: "#5a4b00";
27
56
  }, {
28
57
  readonly kind: "actor";
29
58
  readonly label: "Actor";
@@ -13,18 +13,59 @@ export const SHADOW_COLOR = '#0000002e';
13
13
  export const STICKY_FONT = 'blocksuite:surface:Kalam';
14
14
  export const STICKY_FONT_SIZE = 20;
15
15
  export const STICKY_RADIUS = 6;
16
- /** Event Storming sticky palette — DDD Crew / Brandolini colour code. */
16
+ /**
17
+ * Event Storming sticky palette — DDD Crew / Brandolini colour code.
18
+ *
19
+ * ## The aggregate (WS5)
20
+ *
21
+ * Brandolini's notation has a pale-yellow sticky for the AGGREGATE — the thing a
22
+ * command lands on and the thing that raises the event — and without it the
23
+ * canonical sentence `Command → Aggregate → Domain event` cannot be drawn at
24
+ * all, let alone checked. It was missing here until WS5.
25
+ *
26
+ * Its colour is the one judgement call in this table. The plan's indicative
27
+ * `#FDF0A0` measures **ΔE 3.5** from the actor's `#FFF1A8` — two stickies
28
+ * nobody could tell apart on a wall, which is exactly the failure the notation
29
+ * already has and the tool should not inherit. `#FAF2C9` is the same pale
30
+ * yellow taken two steps towards cream: ΔE 16.3 from the actor, 50.2 from the
31
+ * constraint, and still 21.7 from the white board it sits on, so it reads as
32
+ * "pale" rather than as "blank". The three yellows are then a LADDER — the
33
+ * constraint saturated, the actor light, the aggregate palest — which is how a
34
+ * reader tells them apart at a glance without a legend.
35
+ *
36
+ * Size carries the rest of the distinction, as it does on a real wall: the
37
+ * aggregate is created at 160 against the standard 120 (see
38
+ * `ddd-event-storming/src/commands.ts`). The palette says which; the format
39
+ * says how big.
40
+ */
17
41
  export const ES_STICKIES = [
18
- { kind: 'domainEvent', label: 'Domain event', fill: '#F5963B', text: '#5a3000' },
42
+ {
43
+ kind: 'domainEvent',
44
+ label: 'Domain event',
45
+ fill: '#F5963B',
46
+ text: '#5a3000',
47
+ },
19
48
  { kind: 'command', label: 'Command', fill: '#5BA3DB', text: '#06304d' },
49
+ // Placed after the command, in the order the grammar reads: an actor issues a
50
+ // command, a command lands on an aggregate, an aggregate raises an event.
51
+ { kind: 'aggregate', label: 'Aggregate', fill: '#FAF2C9', text: '#5a4b00' },
20
52
  { kind: 'actor', label: 'Actor', fill: '#FFF1A8', text: '#5a4b00' },
21
53
  { kind: 'constraint', label: 'Constraint', fill: '#FFD84D', text: '#5a4b00' },
22
54
  { kind: 'policy', label: 'Policy', fill: '#C9A8E0', text: '#3d1f57' },
23
55
  { kind: 'readModel', label: 'Read model', fill: '#7ED38A', text: '#14502a' },
24
- { kind: 'system', label: 'External system', fill: '#F6A6C0', text: '#5e1230' },
56
+ {
57
+ kind: 'system',
58
+ label: 'External system',
59
+ fill: '#F6A6C0',
60
+ text: '#5e1230',
61
+ },
25
62
  ];
26
63
  /** Hotspot — neon-pink diamond. */
27
- export const ES_HOTSPOT = { label: 'Hotspot', fill: '#FF1E8E', text: '#ffffff' };
64
+ export const ES_HOTSPOT = {
65
+ label: 'Hotspot',
66
+ fill: '#FF1E8E',
67
+ text: '#ffffff',
68
+ };
28
69
  /** Context Map bounded-context bubble. */
29
70
  export const CM_BUBBLE = {
30
71
  fill: '#e6f0fa',
@@ -41,15 +82,69 @@ export const CM_BUBBLE = {
41
82
  * Ways, Big Ball of Mud).
42
83
  */
43
84
  export const CM_RELATIONSHIPS = [
44
- { kind: 'partnership', label: 'Partnership', abbrev: 'PS', upDown: false, dashed: false },
45
- { kind: 'sharedKernel', label: 'Shared Kernel', abbrev: 'SK', upDown: false, dashed: false },
46
- { kind: 'customerSupplier', label: 'Customer / Supplier', abbrev: 'C/S', upDown: true, dashed: false },
47
- { kind: 'conformist', label: 'Conformist', abbrev: 'CF', upDown: true, dashed: false },
48
- { kind: 'acl', label: 'Anticorruption Layer', abbrev: 'ACL', upDown: true, dashed: false },
49
- { kind: 'ohs', label: 'Open Host Service', abbrev: 'OHS', upDown: true, dashed: false },
50
- { kind: 'publishedLanguage', label: 'Published Language', abbrev: 'PL', upDown: true, dashed: false },
51
- { kind: 'separateWays', label: 'Separate Ways', abbrev: 'SW', upDown: false, dashed: true },
52
- { kind: 'bbom', label: 'Big Ball of Mud', abbrev: 'BBoM', upDown: false, dashed: true },
85
+ {
86
+ kind: 'partnership',
87
+ label: 'Partnership',
88
+ abbrev: 'PS',
89
+ upDown: false,
90
+ dashed: false,
91
+ },
92
+ {
93
+ kind: 'sharedKernel',
94
+ label: 'Shared Kernel',
95
+ abbrev: 'SK',
96
+ upDown: false,
97
+ dashed: false,
98
+ },
99
+ {
100
+ kind: 'customerSupplier',
101
+ label: 'Customer / Supplier',
102
+ abbrev: 'C/S',
103
+ upDown: true,
104
+ dashed: false,
105
+ },
106
+ {
107
+ kind: 'conformist',
108
+ label: 'Conformist',
109
+ abbrev: 'CF',
110
+ upDown: true,
111
+ dashed: false,
112
+ },
113
+ {
114
+ kind: 'acl',
115
+ label: 'Anticorruption Layer',
116
+ abbrev: 'ACL',
117
+ upDown: true,
118
+ dashed: false,
119
+ },
120
+ {
121
+ kind: 'ohs',
122
+ label: 'Open Host Service',
123
+ abbrev: 'OHS',
124
+ upDown: true,
125
+ dashed: false,
126
+ },
127
+ {
128
+ kind: 'publishedLanguage',
129
+ label: 'Published Language',
130
+ abbrev: 'PL',
131
+ upDown: true,
132
+ dashed: false,
133
+ },
134
+ {
135
+ kind: 'separateWays',
136
+ label: 'Separate Ways',
137
+ abbrev: 'SW',
138
+ upDown: false,
139
+ dashed: true,
140
+ },
141
+ {
142
+ kind: 'bbom',
143
+ label: 'Big Ball of Mud',
144
+ abbrev: 'BBoM',
145
+ upDown: false,
146
+ dashed: true,
147
+ },
53
148
  ];
54
149
  /**
55
150
  * Context Map cloud (Big Ball of Mud / general-purpose boundary). The canvas
@@ -64,7 +159,10 @@ function buildCloud() {
64
159
  for (let i = 0; i < N; i++) {
65
160
  const t = (i / N) * Math.PI * 2;
66
161
  const r = 0.4 + 0.05 * Math.sin(6 * t) + 0.03 * Math.sin(3 * t + 0.8);
67
- pts.push([clamp(0.5 + r * Math.cos(t)), clamp(0.5 + r * 0.6 * Math.sin(t))]);
162
+ pts.push([
163
+ clamp(0.5 + r * Math.cos(t)),
164
+ clamp(0.5 + r * 0.6 * Math.sin(t)),
165
+ ]);
68
166
  }
69
167
  return pts;
70
168
  }
@@ -72,7 +170,12 @@ export const CLOUD_VERTICES = buildCloud();
72
170
  export const CLOUD = { w: 180, h: 120, fill: '#f0eef6', stroke: '#6d6e71' };
73
171
  /** Team Topologies interaction modes (placeable markers + Notation legend). */
74
172
  export const TEAM_TOPOLOGIES = [
75
- { kind: 'collaboration', label: 'Collaboration', letter: 'C', fill: '#99ff99' },
173
+ {
174
+ kind: 'collaboration',
175
+ label: 'Collaboration',
176
+ letter: 'C',
177
+ fill: '#99ff99',
178
+ },
76
179
  { kind: 'xaas', label: 'X-as-a-Service', letter: 'X', fill: '#66b2ff' },
77
180
  { kind: 'facilitating', label: 'Facilitating', letter: 'F', fill: '#ffd84d' },
78
181
  ];
@@ -0,0 +1,118 @@
1
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
2
+ import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
+ import { type GfxController, type RoleDefs, type RoleId } from '@formicoidea/labre-core/std/gfx';
4
+ import { type LegendRow, type LegendSection } from './prefabs.js';
5
+ /**
6
+ * The AUTOMATIC legend, shared by the three DDD backgrounds.
7
+ *
8
+ * Wardley's `createWardleyLegend` is the gesture this replicates — select the
9
+ * background, press one button, get a legend of what is actually drawn on it,
10
+ * dropped bottom-left and grouped so it can be moved and edited like anything
11
+ * else. Two things are deliberately NOT replicated:
12
+ *
13
+ * - **detection is by ROLE, never by `instanceof` or by fill colour.** A role is
14
+ * the semantic identity of an artefact (`framework/std`'s `role.ts`); a shape
15
+ * type is not, and a colour is a restyle away from lying. Reading roles also
16
+ * means a legend and a validation rule agree on what is on the board, because
17
+ * they read the same field;
18
+ * - **the glyphs are the DDD house prefab** ({@link addLegend}), not hand-drawn
19
+ * element-by-element replicas of the artefacts. The three DDD frameworks
20
+ * already document themselves with swatch rows, and a second visual language
21
+ * for the same job would be one too many.
22
+ *
23
+ * A framework contributes nothing but a TABLE: which role puts which row in the
24
+ * legend. Everything else — scanning the perimeter, resolving specialisations,
25
+ * dropping empty sections, placing and grouping the box — happens here once.
26
+ */
27
+ /**
28
+ * One candidate row: `row` is listed only when `role` — or a role that
29
+ * specialises it — is carried by an element inside the background's perimeter.
30
+ */
31
+ export interface AutoLegendEntry {
32
+ role: RoleId;
33
+ row: LegendRow;
34
+ /**
35
+ * Match `role` and `role` ALONE, without the specialisation walk.
36
+ *
37
+ * The default — an entry lights up for any of its children — is what a legend
38
+ * usually wants: one "Relation" row covers the twenty-two verbs, one
39
+ * "Sub-domain" row would cover the five kinds. It is also what a validation
40
+ * rule wants, and for the same reason: a statement about a family is a
41
+ * statement about every member of it.
42
+ *
43
+ * A legend sometimes wants the opposite, because it documents what is DRAWN
44
+ * rather than what is meant. EDGY's four base kinds are the case: Content
45
+ * specialises Object, so an entry on `edgy:object` would put an "Object" row
46
+ * on a board where nobody ever dropped a bare Object — the row would name a
47
+ * white square that is nowhere on the diagram. `exact` makes the base and its
48
+ * specialisations two separate statements, so each is listed when, and only
49
+ * when, it is the thing the user actually put down.
50
+ *
51
+ * Reach for it when a row's swatch would be a lie on a board carrying only
52
+ * children — a distinct colour, a distinct shape, a distinct wording. Leave it
53
+ * off when the parent row is a fair summary of the whole family.
54
+ */
55
+ exact?: boolean;
56
+ }
57
+ export interface AutoLegendSectionSpec {
58
+ /** Sub-title, dropped along with the section when none of its rows appear. */
59
+ title?: string;
60
+ entries: readonly AutoLegendEntry[];
61
+ }
62
+ export interface AutoLegendSpec {
63
+ /**
64
+ * Box title. The three DDD tools all say "Legend" (PO recette, 26/08/2026:
65
+ * the boxes used to be titled in French, which was the one label in the
66
+ * library that was — identifiers and fallback wordings are English here, and
67
+ * the day a host ships a locale pack it translates a key, not a leftover).
68
+ */
69
+ title: string;
70
+ width?: number;
71
+ /**
72
+ * The framework's role vocabulary, so a present role is matched against an
73
+ * entry's role THROUGH the specialisation chain: an entry written on a parent
74
+ * role lists itself as soon as any of its children is on the board — unless
75
+ * it asks for {@link AutoLegendEntry.exact}, which is the whole of the
76
+ * exception.
77
+ */
78
+ roles: RoleDefs;
79
+ sections: readonly AutoLegendSectionSpec[];
80
+ }
81
+ /**
82
+ * The framework's OWN wording for a role, from the vocabulary that declares it.
83
+ *
84
+ * A legend row about `es:flow` says "Flow" because that is what the role def
85
+ * says it is called — the table below never restates a label the vocabulary
86
+ * already owns, so renaming a role renames its legend row.
87
+ */
88
+ export declare function roleLabel(roles: RoleDefs, id: RoleId): string;
89
+ /**
90
+ * Every role carried by an element inside `bound`.
91
+ *
92
+ * Neutral elements (no role) contribute nothing, which is the whole
93
+ * compatibility promise: a board drawn before the vocabularies existed produces
94
+ * an empty set and therefore a legend with no rows, exactly like an empty board.
95
+ */
96
+ export declare function rolesInBound(gfx: GfxController, bound: Bound): Set<RoleId>;
97
+ /**
98
+ * The sections to draw for a given set of present roles: every entry whose role
99
+ * is present (directly or through a specialisation, unless the entry asks for
100
+ * {@link AutoLegendEntry.exact}), in declaration order, with empty sections —
101
+ * sub-title included — dropped.
102
+ *
103
+ * When nothing is recognised the result is an EMPTY array, and the caller still
104
+ * draws the box: that is Wardley's behaviour (an empty map yields a framed
105
+ * "Legend" title and no rows), and it is the honest one — a legend of a board
106
+ * with nothing on it lists nothing, rather than inventing the full notation the
107
+ * user has not used.
108
+ */
109
+ export declare function autoLegendSections(present: ReadonlySet<RoleId>, spec: AutoLegendSpec): LegendSection[];
110
+ /**
111
+ * Build the legend of what is drawn inside `background` and drop it bottom-left
112
+ * of it, grouped and selected.
113
+ *
114
+ * Returns the group id, or `undefined` when there is no surface to draw on.
115
+ */
116
+ export declare function createAutoLegend(std: BlockStdScope, background: {
117
+ xywh: string;
118
+ }, spec: AutoLegendSpec): string | undefined;
@@ -0,0 +1,79 @@
1
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
2
+ import { GfxControllerIdentifier, roleIsA, } from '@formicoidea/labre-core/std/gfx';
3
+ import { addLegend, measureLegend, } from './prefabs.js';
4
+ /**
5
+ * The framework's OWN wording for a role, from the vocabulary that declares it.
6
+ *
7
+ * A legend row about `es:flow` says "Flow" because that is what the role def
8
+ * says it is called — the table below never restates a label the vocabulary
9
+ * already owns, so renaming a role renames its legend row.
10
+ */
11
+ export function roleLabel(roles, id) {
12
+ return roles[id]?.labelFallback ?? id;
13
+ }
14
+ /**
15
+ * Distance from the background's left edge / bottom edge, in model units.
16
+ * Wardley's numbers, kept identical so the two gestures put the box in the same
17
+ * place relative to their backgrounds.
18
+ */
19
+ const INSET_X = 50;
20
+ const INSET_BOTTOM = 56;
21
+ /**
22
+ * Every role carried by an element inside `bound`.
23
+ *
24
+ * Neutral elements (no role) contribute nothing, which is the whole
25
+ * compatibility promise: a board drawn before the vocabularies existed produces
26
+ * an empty set and therefore a legend with no rows, exactly like an empty board.
27
+ */
28
+ export function rolesInBound(gfx, bound) {
29
+ const present = new Set();
30
+ for (const el of gfx.getElementsByBound(bound, { type: 'canvas' })) {
31
+ if (el.role !== undefined)
32
+ present.add(el.role);
33
+ }
34
+ return present;
35
+ }
36
+ /**
37
+ * The sections to draw for a given set of present roles: every entry whose role
38
+ * is present (directly or through a specialisation, unless the entry asks for
39
+ * {@link AutoLegendEntry.exact}), in declaration order, with empty sections —
40
+ * sub-title included — dropped.
41
+ *
42
+ * When nothing is recognised the result is an EMPTY array, and the caller still
43
+ * draws the box: that is Wardley's behaviour (an empty map yields a framed
44
+ * "Legend" title and no rows), and it is the honest one — a legend of a board
45
+ * with nothing on it lists nothing, rather than inventing the full notation the
46
+ * user has not used.
47
+ */
48
+ export function autoLegendSections(present, spec) {
49
+ const sections = [];
50
+ for (const section of spec.sections) {
51
+ const rows = section.entries
52
+ .filter(entry => entry.exact
53
+ ? present.has(entry.role)
54
+ : [...present].some(role => roleIsA(role, entry.role, spec.roles)))
55
+ .map(entry => entry.row);
56
+ if (rows.length)
57
+ sections.push({ title: section.title, rows });
58
+ }
59
+ return sections;
60
+ }
61
+ /**
62
+ * Build the legend of what is drawn inside `background` and drop it bottom-left
63
+ * of it, grouped and selected.
64
+ *
65
+ * Returns the group id, or `undefined` when there is no surface to draw on.
66
+ */
67
+ export function createAutoLegend(std, background, spec) {
68
+ const gfx = std.get(GfxControllerIdentifier);
69
+ const surface = gfx.surface;
70
+ if (!surface)
71
+ return undefined;
72
+ const bound = Bound.deserialize(background.xywh);
73
+ const sections = autoLegendSections(rolesInBound(gfx, bound), spec);
74
+ const { height } = measureLegend(sections, spec.width);
75
+ std.store.captureSync();
76
+ const id = addLegend(surface, std, bound.x + INSET_X, bound.y + bound.h - INSET_BOTTOM - height, { title: spec.title, sections, width: spec.width });
77
+ gfx.selection.set({ elements: [id], editing: false });
78
+ return id;
79
+ }
@@ -1,5 +1,5 @@
1
1
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
2
- import type { GfxController } from '@formicoidea/labre-core/std/gfx';
2
+ import { type GfxController } from '@formicoidea/labre-core/std/gfx';
3
3
  /**
4
4
  * Irreducible reusable units composed by all three DDD sub-menus. Each helper
5
5
  * wraps native `surface.addElement` (shape / text / connector / group); nothing
@@ -7,6 +7,14 @@ import type { GfxController } from '@formicoidea/labre-core/std/gfx';
7
7
  * stickies, Context Map bubbles + relationships and Core Domain dots + arrows.
8
8
  */
9
9
  type Surface = NonNullable<GfxController['surface']>;
10
+ /**
11
+ * Build one DDD artefact at the viewport centre and select it, leaving the
12
+ * palette open so several can be added in a row. This is the old
13
+ * `DddMenuBase.finish` — moved here when PF3 turned the three menus into pure
14
+ * renderers over the command registry, since the placement is what the command
15
+ * bodies need and the menu no longer has any.
16
+ */
17
+ export declare function placeDddElement(std: BlockStdScope, build: (surface: Surface, cx: number, cy: number) => string): void;
10
18
  /** Group several element ids; returns the group id (or the first id on failure). */
11
19
  export declare function groupIds(std: BlockStdScope, ids: string[]): string;
12
20
  /**
@@ -22,15 +30,23 @@ export declare function addSticky(surface: Surface, std: BlockStdScope, cx: numb
22
30
  label: string;
23
31
  shapeType?: 'rect' | 'diamond';
24
32
  size?: number;
33
+ /**
34
+ * Stamped on the FACE, never on the shadow: the face is the artefact — it
35
+ * is what carries the words, what a connector attaches to and what the
36
+ * grammar rules read. The shadow is ink.
37
+ */
38
+ role?: string;
25
39
  }): string;
26
40
  /**
27
41
  * A Context Map bounded-context bubble: a rounded pill whose label is the
28
42
  * pill's own shape text in `overflow` fit mode (fixed pill, fixed font — a
29
43
  * long context name may spill out rather than deform the map).
30
44
  */
31
- export declare function addBubble(surface: Surface, cx: number, cy: number, label: string): string;
45
+ export declare function addBubble(surface: Surface, cx: number, cy: number, label: string, role?: string): string;
32
46
  /** A Core Domain dot (sub-domain / bounded context); optional label to its right. */
33
- export declare function addDot(surface: Surface, std: BlockStdScope, cx: number, cy: number, fill: string, label?: string): string;
47
+ export declare function addDot(surface: Surface, std: BlockStdScope, cx: number, cy: number, fill: string, label?: string,
48
+ /** Stamped on the ELLIPSE, never on the group: the dot is the artefact. */
49
+ role?: string): string;
34
50
  /** A free-floating label. */
35
51
  export declare function addLabel(surface: Surface, cx: number, cy: number, label: string, align?: 'left' | 'center', color?: string): string;
36
52
  /** A relationship connector (the single connector unit reused for all patterns). */
@@ -39,12 +55,22 @@ export declare function addConnector(surface: Surface, x1: number, y1: number, x
39
55
  dashed?: boolean;
40
56
  stroke?: string;
41
57
  strokeWidth?: number;
58
+ /** Edge role; `undefined` leaves the connector a neutral line. */
59
+ role?: string;
42
60
  }): string;
43
- /** A Team Topologies interaction-mode marker: a coloured square + centred letter. */
61
+ /**
62
+ * A Team Topologies interaction-mode marker: a coloured square + centred letter.
63
+ *
64
+ * The role rides on the SQUARE, never on the group — the same call as
65
+ * {@link addDot}: the square is the artefact, the letter is its glyph and the
66
+ * word beside it is a caption. `undefined` writes nothing, so a caller that
67
+ * passes none keeps producing the neutral drawing it always did.
68
+ */
44
69
  export declare function addMarker(surface: Surface, std: BlockStdScope, cx: number, cy: number, opts: {
45
70
  fill: string;
46
71
  letter: string;
47
72
  label?: string;
73
+ role?: string;
48
74
  }): string;
49
75
  /** A cloud (Big Ball of Mud / general-purpose boundary): a smoothed closed bezier polygon. */
50
76
  export declare function addCloud(surface: Surface, std: BlockStdScope, cx: number, cy: number, label?: string): string;
@@ -63,13 +89,32 @@ export interface LegendRow {
63
89
  color: string;
64
90
  letter?: string;
65
91
  label: string;
92
+ /**
93
+ * `line` swatches only: draw the sample as two short segments rather than one
94
+ * bar, so a dashed pattern (Separate Ways, Big Ball of Mud, a Core Domain
95
+ * movement) reads in the legend the way it reads on the board. A legend that
96
+ * showed a solid bar for a dashed link would be telling a small lie about the
97
+ * notation it documents.
98
+ */
99
+ dashed?: boolean;
66
100
  }
67
101
  export interface LegendSection {
68
102
  title?: string;
69
103
  rows: LegendRow[];
70
104
  }
71
105
  /**
72
- * A boxed legend: a bordered container with a bold "Légende" title and bold
106
+ * How big the box {@link addLegend} would draw for these sections is — without
107
+ * drawing it. The auto-legend needs the height to drop the box bottom-left of a
108
+ * background, and measuring is the only honest way to get it: the box grows with
109
+ * the number of sub-titles and rows, which is exactly what the detection pass
110
+ * decides.
111
+ */
112
+ export declare function measureLegend(sections: readonly LegendSection[], width?: number): {
113
+ width: number;
114
+ height: number;
115
+ };
116
+ /**
117
+ * A boxed legend: a bordered container with a bold "Legend" title and bold
73
118
  * section sub-titles, each row a swatch (dot / square+letter / line) + label.
74
119
  * Shared by the Core Domain and Context Map tools. Returns the grouped id.
75
120
  */
@@ -1,18 +1,40 @@
1
+ import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
1
2
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
2
3
  import { ConnectorMode, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
3
4
  import { Bound } from '@formicoidea/labre-core/global/gfx';
5
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
4
6
  import * as Y from 'yjs';
5
7
  import { CLOUD, CLOUD_VERTICES, CM_BUBBLE, DOT_SIZE, LABEL_COLOR, LABEL_FONT, LABEL_FONT_SIZE, MARKER_SIZE, SHADOW_COLOR, SHADOW_OFFSET, STICKY_FONT, STICKY_FONT_SIZE, STICKY_RADIUS, STICKY_SIZE, } from './consts.js';
6
8
  const NO_STROKE = '#00000000';
9
+ /**
10
+ * Build one DDD artefact at the viewport centre and select it, leaving the
11
+ * palette open so several can be added in a row. This is the old
12
+ * `DddMenuBase.finish` — moved here when PF3 turned the three menus into pure
13
+ * renderers over the command registry, since the placement is what the command
14
+ * bodies need and the menu no longer has any.
15
+ */
16
+ export function placeDddElement(std, build) {
17
+ const gfx = std.get(GfxControllerIdentifier);
18
+ const surface = gfx.surface;
19
+ if (!surface)
20
+ return;
21
+ const { centerX, centerY } = gfx.viewport;
22
+ const id = build(surface, centerX, centerY);
23
+ gfx.doc.captureSync();
24
+ gfx.tool.setTool(DefaultTool);
25
+ gfx.selection.set({ elements: [id], editing: false });
26
+ }
7
27
  /** Group several element ids; returns the group id (or the first id on failure). */
8
28
  export function groupIds(std, ids) {
9
29
  const [, result] = std.command.exec(createGroupCommand, { elements: ids });
10
30
  return result.groupId || ids[0];
11
31
  }
12
32
  function addShape(surface, x, y, w, h, opts) {
13
- const { shapeType = 'rect', fill, stroke = NO_STROKE, strokeWidth = 0, radius = 0, label } = opts;
33
+ const { shapeType = 'rect', fill, stroke = NO_STROKE, strokeWidth = 0, radius = 0, label, role, } = opts;
14
34
  return surface.addElement({
15
35
  type: 'shape',
36
+ // `undefined` writes nothing: a neutral artefact keeps no `role` key.
37
+ role,
16
38
  shapeType,
17
39
  filled: true,
18
40
  fillColor: fill,
@@ -54,7 +76,7 @@ function addText(surface, x, y, w, text, color, fontFamily, fontSize, textAlign
54
76
  * text element; they keep working untouched.)
55
77
  */
56
78
  export function addSticky(surface, std, cx, cy, opts) {
57
- const { fill, text, label, shapeType = 'rect', size = STICKY_SIZE } = opts;
79
+ const { fill, text, label, shapeType = 'rect', size = STICKY_SIZE, role, } = opts;
58
80
  const half = size / 2;
59
81
  const radius = shapeType === 'rect' ? STICKY_RADIUS : 0;
60
82
  const shadow = addShape(surface, cx - half + SHADOW_OFFSET, cy - half + SHADOW_OFFSET, size, size, {
@@ -66,6 +88,7 @@ export function addSticky(surface, std, cx, cy, opts) {
66
88
  shapeType,
67
89
  fill,
68
90
  radius,
91
+ role,
69
92
  label: {
70
93
  text: label,
71
94
  color: text,
@@ -81,13 +104,14 @@ export function addSticky(surface, std, cx, cy, opts) {
81
104
  * pill's own shape text in `overflow` fit mode (fixed pill, fixed font — a
82
105
  * long context name may spill out rather than deform the map).
83
106
  */
84
- export function addBubble(surface, cx, cy, label) {
107
+ export function addBubble(surface, cx, cy, label, role) {
85
108
  const { w, h, radius, fill, stroke, text } = CM_BUBBLE;
86
109
  return addShape(surface, cx - w / 2, cy - h / 2, w, h, {
87
110
  fill,
88
111
  stroke,
89
112
  strokeWidth: 1.5,
90
113
  radius,
114
+ role,
91
115
  label: {
92
116
  text: label,
93
117
  color: text,
@@ -98,13 +122,16 @@ export function addBubble(surface, cx, cy, label) {
98
122
  });
99
123
  }
100
124
  /** A Core Domain dot (sub-domain / bounded context); optional label to its right. */
101
- export function addDot(surface, std, cx, cy, fill, label) {
125
+ export function addDot(surface, std, cx, cy, fill, label,
126
+ /** Stamped on the ELLIPSE, never on the group: the dot is the artefact. */
127
+ role) {
102
128
  const d = DOT_SIZE;
103
129
  const dot = addShape(surface, cx - d / 2, cy - d / 2, d, d, {
104
130
  shapeType: 'ellipse',
105
131
  fill,
106
132
  stroke: '#1f2328',
107
133
  strokeWidth: 1.5,
134
+ role,
108
135
  });
109
136
  if (!label)
110
137
  return dot;
@@ -117,9 +144,10 @@ export function addLabel(surface, cx, cy, label, align = 'center', color = LABEL
117
144
  }
118
145
  /** A relationship connector (the single connector unit reused for all patterns). */
119
146
  export function addConnector(surface, x1, y1, x2, y2, opts = {}) {
120
- const { rearArrow = false, dashed = false, stroke = LABEL_COLOR, strokeWidth = 2 } = opts;
147
+ const { rearArrow = false, dashed = false, stroke = LABEL_COLOR, strokeWidth = 2, role, } = opts;
121
148
  return surface.addElement({
122
149
  type: 'connector',
150
+ role,
123
151
  mode: ConnectorMode.Straight,
124
152
  stroke,
125
153
  strokeWidth,
@@ -130,15 +158,23 @@ export function addConnector(surface, x1, y1, x2, y2, opts = {}) {
130
158
  target: { position: [x2, y2] },
131
159
  });
132
160
  }
133
- /** A Team Topologies interaction-mode marker: a coloured square + centred letter. */
161
+ /**
162
+ * A Team Topologies interaction-mode marker: a coloured square + centred letter.
163
+ *
164
+ * The role rides on the SQUARE, never on the group — the same call as
165
+ * {@link addDot}: the square is the artefact, the letter is its glyph and the
166
+ * word beside it is a caption. `undefined` writes nothing, so a caller that
167
+ * passes none keeps producing the neutral drawing it always did.
168
+ */
134
169
  export function addMarker(surface, std, cx, cy, opts) {
135
- const { fill, letter, label } = opts;
170
+ const { fill, letter, label, role } = opts;
136
171
  const s = MARKER_SIZE;
137
172
  const box = addShape(surface, cx - s / 2, cy - s / 2, s, s, {
138
173
  fill,
139
174
  stroke: '#1f2328',
140
175
  strokeWidth: 1.5,
141
176
  radius: 4,
177
+ role,
142
178
  });
143
179
  const glyph = addText(surface, cx - s / 2, cy - 9, s, letter, '#1f2328', LABEL_FONT, 15);
144
180
  const ids = [box, glyph];
@@ -193,25 +229,48 @@ export function addRelationship(surface, std, cx, cy, preset) {
193
229
  return groupIds(std, ids);
194
230
  }
195
231
  /**
196
- * A boxed legend: a bordered container with a bold "Légende" title and bold
197
- * section sub-titles, each row a swatch (dot / square+letter / line) + label.
198
- * Shared by the Core Domain and Context Map tools. Returns the grouped id.
232
+ * The legend box's geometry, in one place because two callers need it: the one
233
+ * that DRAWS the box and the one that has to know how tall it will be before
234
+ * drawing it (see {@link measureLegend}).
199
235
  */
200
- export function addLegend(surface, std, x, y, opts) {
201
- const W = opts.width ?? 260;
202
- const PAD = 16;
203
- const TITLE_H = 32;
204
- const SUB_H = 26;
205
- const ROW_H = 28;
206
- const SW = 16;
236
+ const LEGEND_METRICS = {
237
+ DEFAULT_W: 260,
238
+ PAD: 16,
239
+ TITLE_H: 32,
240
+ SUB_H: 26,
241
+ ROW_H: 28,
242
+ /** Swatch side (a `line` swatch is this long). */
243
+ SW: 16,
244
+ };
245
+ /**
246
+ * How big the box {@link addLegend} would draw for these sections is — without
247
+ * drawing it. The auto-legend needs the height to drop the box bottom-left of a
248
+ * background, and measuring is the only honest way to get it: the box grows with
249
+ * the number of sub-titles and rows, which is exactly what the detection pass
250
+ * decides.
251
+ */
252
+ export function measureLegend(sections, width) {
253
+ const { DEFAULT_W, PAD, TITLE_H, SUB_H, ROW_H } = LEGEND_METRICS;
207
254
  let subs = 0;
208
255
  let rows = 0;
209
- for (const s of opts.sections) {
256
+ for (const s of sections) {
210
257
  if (s.title)
211
258
  subs++;
212
259
  rows += s.rows.length;
213
260
  }
214
- const H = PAD * 2 + TITLE_H + subs * SUB_H + rows * ROW_H;
261
+ return {
262
+ width: width ?? DEFAULT_W,
263
+ height: PAD * 2 + TITLE_H + subs * SUB_H + rows * ROW_H,
264
+ };
265
+ }
266
+ /**
267
+ * A boxed legend: a bordered container with a bold "Legend" title and bold
268
+ * section sub-titles, each row a swatch (dot / square+letter / line) + label.
269
+ * Shared by the Core Domain and Context Map tools. Returns the grouped id.
270
+ */
271
+ export function addLegend(surface, std, x, y, opts) {
272
+ const { PAD, TITLE_H, SUB_H, ROW_H, SW } = LEGEND_METRICS;
273
+ const { width: W, height: H } = measureLegend(opts.sections, opts.width);
215
274
  const ids = [
216
275
  addShape(surface, x, y, W, H, {
217
276
  fill: '#ffffff',
@@ -232,13 +291,36 @@ export function addLegend(surface, std, x, y, opts) {
232
291
  const sx = x + PAD;
233
292
  const midY = cy + ROW_H / 2;
234
293
  if (row.swatch === 'dot') {
235
- ids.push(addShape(surface, sx, midY - SW / 2, SW, SW, { shapeType: 'ellipse', fill: row.color, stroke: '#1f2328', strokeWidth: 1 }));
294
+ ids.push(addShape(surface, sx, midY - SW / 2, SW, SW, {
295
+ shapeType: 'ellipse',
296
+ fill: row.color,
297
+ stroke: '#1f2328',
298
+ strokeWidth: 1,
299
+ }));
236
300
  }
237
301
  else if (row.swatch === 'square') {
238
- ids.push(addShape(surface, sx, midY - SW / 2, SW, SW, { fill: row.color, stroke: '#1f2328', strokeWidth: 1, radius: 3 }));
302
+ ids.push(addShape(surface, sx, midY - SW / 2, SW, SW, {
303
+ fill: row.color,
304
+ stroke: '#1f2328',
305
+ strokeWidth: 1,
306
+ radius: 3,
307
+ }));
239
308
  if (row.letter)
240
309
  ids.push(addText(surface, sx, midY - 8, SW, row.letter, '#1f2328', LABEL_FONT, 11));
241
310
  }
311
+ else if (row.dashed) {
312
+ // Two 6-unit segments with a 4-unit gap: the same 16 units as a solid
313
+ // bar, read as a dash.
314
+ const seg = 6;
315
+ ids.push(addShape(surface, sx, midY - 2, seg, 4, {
316
+ fill: row.color,
317
+ radius: 1,
318
+ }));
319
+ ids.push(addShape(surface, sx + SW - seg, midY - 2, seg, 4, {
320
+ fill: row.color,
321
+ radius: 1,
322
+ }));
323
+ }
242
324
  else {
243
325
  ids.push(addShape(surface, sx, midY - 2, SW, 4, { fill: row.color, radius: 1 }));
244
326
  }
@@ -10,7 +10,12 @@ import { CD_SUBDOMAINS, CLOUD, CLOUD_VERTICES, CM_BUBBLE, CM_RELATIONSHIPS, ES_H
10
10
  const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
11
11
  const NO_STROKE = '#00000000';
12
12
  function tpl(name, preview, elements) {
13
- return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
13
+ return {
14
+ name,
15
+ type: 'template',
16
+ preview,
17
+ content: makeTemplateSnapshot(elements, name),
18
+ };
14
19
  }
15
20
  function stickyEl(fill, text, label, shapeType) {
16
21
  return {
@@ -38,7 +43,9 @@ const mk = (c, l) => `<svg ${ATTRS}><rect x="48" y="20" width="40" height="40" r
38
43
  const line = (dashed, arrow) => `<svg ${ATTRS} fill="none"><path d="M20 40 H${arrow ? 100 : 115}" stroke="#1f2328" stroke-width="2" stroke-dasharray="${dashed ? '5 4' : '0'}"/>${arrow ? '<path d="M98 32 L114 40 L98 48" stroke="#1f2328" stroke-width="2"/>' : ''}</svg>`;
39
44
  /** Event Storming: one template per sticky kind + the hotspot. */
40
45
  export const ES_TEMPLATES = [
41
- ...ES_STICKIES.map(p => tpl(`Event Storming — ${p.label}`, sq(p.fill), { s: stickyEl(p.fill, p.text, p.label, 'rect') })),
46
+ ...ES_STICKIES.map(p => tpl(`Event Storming — ${p.label}`, sq(p.fill), {
47
+ s: stickyEl(p.fill, p.text, p.label, 'rect'),
48
+ })),
42
49
  tpl('Event Storming — Hotspot', dia(ES_HOTSPOT.fill), {
43
50
  s: stickyEl(ES_HOTSPOT.fill, ES_HOTSPOT.text, ES_HOTSPOT.label, 'diamond'),
44
51
  }),
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Contextual-toolbar glyph — "generate the legend of what is on this
3
+ * background". One icon for the three DDD backgrounds because it is one
4
+ * gesture; the Core Domain chart has drawn this exact box-and-rows since its
5
+ * legend button shipped, and the two boards now borrow it rather than each
6
+ * redrawing a lookalike.
7
+ *
8
+ * `html` rather than `svg`: a toolbar action's `icon` is rendered as a standalone
9
+ * template, unlike the senior-button glyphs above which are interpolated into an
10
+ * outer `<svg>`.
11
+ */
12
+ export declare const dddLegendIcon: import("lit-html").TemplateResult<1>;
1
13
  /** Senior-button glyph — Event Storming (overlapping stickies). */
2
14
  export declare const eventStormingToolbarIcon: import("lit-html").TemplateResult<2>;
3
15
  /** Senior-button glyph — Core Domain Chart (axes + bands). */
@@ -1,4 +1,35 @@
1
- import { svg } from 'lit';
1
+ import { html, svg } from 'lit';
2
+ /**
3
+ * Contextual-toolbar glyph — "generate the legend of what is on this
4
+ * background". One icon for the three DDD backgrounds because it is one
5
+ * gesture; the Core Domain chart has drawn this exact box-and-rows since its
6
+ * legend button shipped, and the two boards now borrow it rather than each
7
+ * redrawing a lookalike.
8
+ *
9
+ * `html` rather than `svg`: a toolbar action's `icon` is rendered as a standalone
10
+ * template, unlike the senior-button glyphs above which are interpolated into an
11
+ * outer `<svg>`.
12
+ */
13
+ export const dddLegendIcon = html `<svg
14
+ width="24"
15
+ height="24"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ >
20
+ <rect
21
+ x="3"
22
+ y="4"
23
+ width="18"
24
+ height="16"
25
+ rx="2"
26
+ stroke="currentColor"
27
+ stroke-width="1.6"
28
+ />
29
+ <circle cx="7" cy="9" r="1.6" fill="currentColor" />
30
+ <circle cx="7" cy="14" r="1.6" fill="currentColor" />
31
+ <path d="M11 9 H18 M11 14 H18" stroke="currentColor" stroke-width="1.4" />
32
+ </svg>`;
2
33
  /** Senior-button glyph — Event Storming (overlapping stickies). */
3
34
  export const eventStormingToolbarIcon = svg `<svg width="100%" height="100%" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
4
35
  <rect x="10" y="14" width="22" height="22" rx="2" transform="rotate(-6 21 25)" fill="#F5963B"/>
@@ -13,6 +13,8 @@ export declare abstract class DddSeniorButtonBase extends DddSeniorButtonBase_ba
13
13
  type: typeof EmptyTool;
14
14
  protected abstract menuTag: keyof HTMLElementTagNameMap;
15
15
  protected abstract label: string;
16
+ /** i18n key resolved through the host catalogue; `label` is the fallback. */
17
+ protected abstract labelKey: string;
16
18
  protected abstract icon: TemplateResult;
17
19
  private _toggleMenu;
18
20
  render(): TemplateResult<1>;
@@ -1,5 +1,6 @@
1
1
  import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
+ import { translateKey } from '@formicoidea/labre-core/shared/services';
3
4
  import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
5
  import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
5
6
  import { css, html, LitElement } from 'lit';
@@ -65,7 +66,9 @@ export class DddSeniorButtonBase extends EdgelessToolbarToolMixin(SignalWatcher(
65
66
  render() {
66
67
  return html `<edgeless-toolbar-button
67
68
  class="ddd-button"
68
- .tooltip=${this.popper ? '' : this.label}
69
+ .tooltip=${this.popper
70
+ ? ''
71
+ : translateKey(this.edgeless.std, this.labelKey, this.label)}
69
72
  .tooltipOffset=${4}
70
73
  .active=${!!this.popper}
71
74
  @click=${this._toggleMenu}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-ddd-shared",
3
3
  "description": "Labre ddd-shared — shared building blocks for @formicoidea/labre-core frameworks.",
4
- "version": "0.31.0",
4
+ "version": "0.33.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -19,7 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@formicoidea/labre-core": "0.31.0",
22
+ "@formicoidea/labre-core": "0.33.0",
23
23
  "lit": "^3.2.0"
24
24
  }
25
25
  }
@@ -1,23 +0,0 @@
1
- import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
2
- import { LitElement } from 'lit';
3
- declare const DddMenuBase_base: typeof LitElement & import("@formicoidea/labre-core/_pkgs/global/utils").Constructor<import("@formicoidea/labre-core/_pkgs/affine-widget-edgeless-toolbar").EdgelessToolbarToolClass>;
4
- /**
5
- * Shared base for the three DDD popover menus: the menu shell styles, the
6
- * "create then return to selection (palette stays open)" finish helper and
7
- * the telemetry track helper. Each subclass adds its own create* methods (built
8
- * from the shared prefab builders) and its own render().
9
- */
10
- export declare abstract class DddMenuBase extends DddMenuBase_base {
11
- static styles: import("lit").CSSResult;
12
- type: typeof EmptyTool;
13
- /** Framework id for telemetry. */
14
- protected abstract framework: 'event-storming' | 'core-domain' | 'context-map';
15
- protected get surface(): import("@formicoidea/labre-core/std/gfx").SurfaceBlockModel | null;
16
- protected get center(): {
17
- cx: number;
18
- cy: number;
19
- };
20
- protected finish(id: string): void;
21
- protected track(event: 'FrameworkElementAdded' | 'FrameworkToolPicked', element: string): void;
22
- }
23
- export {};
@@ -1,62 +0,0 @@
1
- import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
- import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
- import { TelemetryProvider } from '@formicoidea/labre-core/shared/services';
4
- import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
5
- import { css, LitElement } from 'lit';
6
- /**
7
- * Shared base for the three DDD popover menus: the menu shell styles, the
8
- * "create then return to selection (palette stays open)" finish helper and
9
- * the telemetry track helper. Each subclass adds its own create* methods (built
10
- * from the shared prefab builders) and its own render().
11
- */
12
- export class DddMenuBase extends EdgelessToolbarToolMixin(LitElement) {
13
- constructor() {
14
- super(...arguments);
15
- this.type = EmptyTool;
16
- }
17
- static { this.styles = css `
18
- :host {
19
- display: flex;
20
- z-index: -1;
21
- justify-content: center;
22
- }
23
- .menu-content {
24
- display: flex;
25
- align-items: center;
26
- justify-content: center;
27
- }
28
- .button-group-container {
29
- display: flex;
30
- align-items: center;
31
- gap: 10px;
32
- fill: var(--affine-icon-color);
33
- }
34
- .button-group-container svg {
35
- width: 24px;
36
- height: 24px;
37
- }
38
- `; }
39
- get surface() {
40
- return this.gfx.surface;
41
- }
42
- get center() {
43
- const { centerX, centerY } = this.gfx.viewport;
44
- return { cx: centerX, cy: centerY };
45
- }
46
- finish(id) {
47
- const { gfx } = this;
48
- gfx.doc.captureSync();
49
- gfx.tool.setTool(DefaultTool);
50
- gfx.selection.set({ elements: [id], editing: false });
51
- // Palette stays open so several objects can be added in a row.
52
- }
53
- track(event, element) {
54
- this.edgeless.std.getOptional(TelemetryProvider)?.track(event, {
55
- framework: this.framework,
56
- element,
57
- page: 'whiteboard editor',
58
- segment: 'ddd toolbox',
59
- module: `${this.framework} menu`,
60
- });
61
- }
62
- }