@formicoidea/labre-framework-ddd-event-storming 0.34.1 → 0.35.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/commands.js CHANGED
@@ -13,10 +13,10 @@ import { ES_STICKY_ROLE } from './roles.js';
13
13
  * stickies now carry their role, the flow arms the connector tool instead of
14
14
  * dropping a drawing, and the board is a new entry with a new `element` value.
15
15
  */
16
- const squareSwatch = (color) => svg `<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3" fill="${color}"/></svg>`;
17
- const diamondSwatch = (color) => svg `<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" transform="rotate(45 12 12)" fill="${color}"/></svg>`;
18
- const flowSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><path d="M4 12 H18" stroke="currentColor" stroke-width="2"/><path d="M16 8 L20 12 L16 16" stroke="currentColor" stroke-width="2" fill="none"/></svg>`;
19
- const boardSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><rect x="2.5" y="4" width="19" height="16" rx="2" fill="#ffffff" stroke="currentColor" stroke-width="1.6"/><rect x="5" y="7.5" width="4" height="4" rx="1" fill="#F5963B"/><rect x="10.5" y="7.5" width="4" height="4" rx="1" fill="#5BA3DB"/><rect x="16" y="7.5" width="4" height="4" rx="1" fill="#FAF2C9" stroke="#c9bd7f" stroke-width="0.6"/><path d="M4.5 16.5 H19.5" stroke="currentColor" stroke-width="1.2"/><path d="M17.5 14.8 L19.8 16.5 L17.5 18.2" stroke="currentColor" stroke-width="1.2"/></svg>`;
16
+ const squareSwatch = (color) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3" fill="${color}"/></svg>`;
17
+ const diamondSwatch = (color) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" transform="rotate(45 12 12)" fill="${color}"/></svg>`;
18
+ const flowSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M4 12 H18" stroke="currentColor" stroke-width="2"/><path d="M16 8 L20 12 L16 16" stroke="currentColor" stroke-width="2" fill="none"/></svg>`;
19
+ const boardSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="2.5" y="4" width="19" height="16" rx="2" fill="#ffffff" stroke="currentColor" stroke-width="1.6"/><rect x="5" y="7.5" width="4" height="4" rx="1" fill="#F5963B"/><rect x="10.5" y="7.5" width="4" height="4" rx="1" fill="#5BA3DB"/><rect x="16" y="7.5" width="4" height="4" rx="1" fill="#FAF2C9" stroke="#c9bd7f" stroke-width="0.6"/><path d="M4.5 16.5 H19.5" stroke="currentColor" stroke-width="1.2"/><path d="M17.5 14.8 L19.8 16.5 L17.5 18.2" stroke="currentColor" stroke-width="1.2"/></svg>`;
20
20
  /**
21
21
  * The aggregate is created BIG — 160 against the standard 120.
22
22
  *
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { eventStormingCommandIcons, eventStormingCommands, } from './commands.js';
2
2
  export { EVENT_STORMING_BACKGROUND } from './background.js';
3
+ export { ES_MORPH_FAMILIES, ES_STICKY_FACES, EVENT_STORMING_MORPH_SPEC, eventStormingFaceOfSticky, eventStormingMorphedLabel, eventStormingMorphProps, eventStormingStickyKind, } from './morph.js';
3
4
  export { EVENT_STORMING_NUDGES } from './nudges.js';
4
5
  export { EVENT_STORMING_PROFILES } from './profiles.js';
5
6
  export { ES_ROLE, ES_STICKY_ROLE, EVENT_STORMING_ROLES, type EventStormingRoleId, type EventStormingStickyKind, } from './roles.js';
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  export { eventStormingCommandIcons, eventStormingCommands, } from './commands.js';
2
2
  export { EVENT_STORMING_BACKGROUND } from './background.js';
3
+ // What a sticky may BECOME: the family and the spec the flag-gated morph module
4
+ // is registered with, the resolution from a selected group to the face the role
5
+ // lives on, and the two pure rules the integration suite drives directly.
6
+ export { ES_MORPH_FAMILIES, ES_STICKY_FACES, EVENT_STORMING_MORPH_SPEC, eventStormingFaceOfSticky, eventStormingMorphedLabel, eventStormingMorphProps, eventStormingStickyKind, } from './morph.js';
3
7
  export { EVENT_STORMING_NUDGES } from './nudges.js';
4
8
  export { EVENT_STORMING_PROFILES } from './profiles.js';
5
9
  export { ES_ROLE, ES_STICKY_ROLE, EVENT_STORMING_ROLES, } from './roles.js';
@@ -0,0 +1,139 @@
1
+ import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ShapeElementModel, ShapeType } from '@formicoidea/labre-core/model';
3
+ import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
+ import { type EventStormingStickyKind } from './roles.js';
5
+ /**
6
+ * What an Event Storming sticky may BECOME — the declaration behind the
7
+ * "Change type" dropdown on a selected sticky's contextual toolbar.
8
+ *
9
+ * ## One family, and why nine kinds is the honest number
10
+ *
11
+ * A workshop does not draw a domain event and a command in two different
12
+ * gestures; it draws a sticky and then argues about which colour it is. That
13
+ * argument IS Event Storming — Brandolini's whole method is a wall of
14
+ * identically-shaped paper whose colour is the claim — and the artefact never
15
+ * changes size, level or grammar rank when the claim does. So unlike BPMN's six
16
+ * families and C4's three, every sticky here is reachable from every other:
17
+ * splitting them would be inventing a hierarchy the notation does not have,
18
+ * and would mean deciding that "this orange one was really a command" needs a
19
+ * delete and a redraw while "this event was really a hotspot" does not.
20
+ *
21
+ * The **hotspot is in the family** for exactly that reason. It is the only
22
+ * member drawn as a diamond, which is a silhouette and not a rank: a hotspot is
23
+ * what the workshop writes when a sticky it already placed turns out to be a
24
+ * question rather than a claim, and that is the single most common morph the
25
+ * method actually performs. Its diamond is handled by {@link
26
+ * eventStormingMorphProps} like any other preset difference — and by
27
+ * {@link followTheFace}, since the sticky's shadow is drawn in the same
28
+ * silhouette as the face it sits behind.
29
+ *
30
+ * Declaration order is menu order and it is the GRAMMAR's order — the one
31
+ * `ES_STICKIES` declares and the senior sub-menu renders (an actor issues a
32
+ * command, a command lands on an aggregate, an aggregate raises an event) —
33
+ * with the hotspot last, where the palette also puts it. A user meets the same
34
+ * sequence in the menu that draws stickies and in the menu that re-says them.
35
+ */
36
+ /**
37
+ * Everything one sticky kind is worth on the FACE, derived from the same two
38
+ * preset tables the creation commands read.
39
+ *
40
+ * `ES_STICKIES` is eight squares and `ES_HOTSPOT` is the ninth, a diamond in
41
+ * its own preset — the split `roles.ts` already works around, and for the same
42
+ * reason: the hotspot is not a colour variant, so it is not in the colour
43
+ * table. Joined here once so that every consumer below (the family, the patch,
44
+ * the labels, the inverse role table) reads ONE list and no second table of
45
+ * nine can drift from the one the palette draws.
46
+ */
47
+ interface StickyFace {
48
+ kind: EventStormingStickyKind;
49
+ /** `fillColor` of the face. */
50
+ fill: string;
51
+ /** `color` — the handwriting on it. */
52
+ text: string;
53
+ /** The words a freshly-drawn sticky of this kind carries. */
54
+ label: string;
55
+ shapeType: ShapeType.Rect | ShapeType.Diamond;
56
+ }
57
+ export declare const ES_STICKY_FACES: readonly StickyFace[];
58
+ /** The one family: every sticky, in the order the grammar reads them. */
59
+ export declare const ES_MORPH_FAMILIES: readonly (readonly EventStormingStickyKind[])[];
60
+ /**
61
+ * The kind a shape carries, or `undefined` when it carries none.
62
+ *
63
+ * `undefined` for a sticky placed before WS5 is the whole compat story and it
64
+ * is deliberate: those carry no role, they are never evaluated by a rule
65
+ * (promesse #71), and nothing here backfills one. A morph that inferred a kind
66
+ * from a fill colour would be writing a semantic claim the workshop never
67
+ * made — so a pre-WS5 sticky is simply not offered the menu, exactly as it is
68
+ * not offered a verdict.
69
+ */
70
+ export declare function eventStormingStickyKind(model: GfxPrimitiveElementModel): EventStormingStickyKind | undefined;
71
+ /**
72
+ * The FACE of a selected sticky — `undefined` when the group is not one.
73
+ *
74
+ * A sticky is a native `group` of two shapes: a neutral offset rectangle that
75
+ * is the drop shadow, and the coloured face that carries the role, the words
76
+ * and every connector endpoint. One click selects the group, so this is the
77
+ * same indirection `c4NodeOfComponent` performs, and the same gate: the face is
78
+ * identified by its ROLE and not by its position in the group, so the shadow
79
+ * (which is ink and carries none) is skipped, a plain lasso round two
80
+ * rectangles resolves to nothing, another framework's composite resolves to
81
+ * nothing, and a legacy sticky whose label is a separate text element resolves
82
+ * to nothing because its face carries no role either.
83
+ *
84
+ * TWO faces is a refusal rather than a first-wins pick, for the reason C4
85
+ * gives: a group of two stickies is a group somebody made of two stickies, and
86
+ * morphing "it" would mean choosing one of them by document order.
87
+ */
88
+ export declare function eventStormingFaceOfSticky(model: GfxPrimitiveElementModel): ShapeElementModel | undefined;
89
+ /**
90
+ * What one kind is worth to a face that ALREADY EXISTS.
91
+ *
92
+ * Five keys, and they are exactly the five that differ between two stickies:
93
+ * the role, the two colours, the silhouette and its rounding. Every other prop
94
+ * `addSticky` writes — `filled`, `strokeColor`, `strokeWidth`, `shapeStyle`,
95
+ * `roughness`, and the whole typography of the contained label — is the SAME
96
+ * for all nine kinds, which the unit suite asserts rather than assumes: the day
97
+ * a tenth sticky lands with a preset of its own, that case is what says this
98
+ * table has to grow.
99
+ *
100
+ * ## Why the size is not in here, unlike C4 and BPMN
101
+ *
102
+ * Because on this notation size is not a preset, it is a statement the author
103
+ * made. The aggregate is BORN at 160 against the standard 120 (`commands.ts`),
104
+ * but a sticky a workshop stretched to fit six words is stretched on purpose,
105
+ * and `xywh` is the one thing a morph promises never to touch — the generic
106
+ * module strips it whatever a spec returns. The consequence is visible and
107
+ * accepted: an aggregate morphed to a command keeps its 160, and a command
108
+ * morphed to an aggregate stays 120. The paint says what it is; the size says
109
+ * how much room the author gave it.
110
+ */
111
+ export declare function eventStormingMorphProps(kind: EventStormingStickyKind): Record<string, unknown>;
112
+ /**
113
+ * The words a sticky should carry once its face has morphed — or `null` when
114
+ * the words are the AUTHOR's and must not be touched.
115
+ *
116
+ * The same timid rule as `c4MorphedTitle`, and it matters more here: a sticky
117
+ * nobody has written on still says "Domain event", and an orange one that
118
+ * became blue while still saying "Domain event" is a wall contradicting itself
119
+ * in the one place a reader looks. Exactly one case rewrites — the text is the
120
+ * source kind's own creation prompt, letter for letter. A sticky somebody wrote
121
+ * "Order placed" on is called that whatever it becomes, and so is one they
122
+ * cleared, one they added a word to, and one carrying another kind's prompt.
123
+ *
124
+ * Pure and total over every string: the input is a shape label a user may have
125
+ * typed anything into.
126
+ */
127
+ export declare function eventStormingMorphedLabel(from: EventStormingStickyKind, to: EventStormingStickyKind, rawText: string | null | undefined): string | null;
128
+ /**
129
+ * Event Storming's morph declaration, handed to the generic
130
+ * `morphToolbarConfig`.
131
+ *
132
+ * `modelType` is the native `GroupElementModel` and not a shape, because the
133
+ * group is what a click on a sticky selects — the same arrangement C4 has, and
134
+ * everything that makes it safe is in {@link eventStormingFaceOfSticky}: the
135
+ * toolbar's own homogeneity test proves only that every selection is A group,
136
+ * and the resolution proves that every one of them is one of OURS.
137
+ */
138
+ export declare const EVENT_STORMING_MORPH_SPEC: MorphSpec<EventStormingStickyKind>;
139
+ export {};
package/dist/morph.js ADDED
@@ -0,0 +1,280 @@
1
+ import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ES_HOTSPOT, ES_STICKIES, SHADOW_COLOR, STICKY_RADIUS, } from '@formicoidea/labre-ddd-shared';
3
+ import { GroupElementModel, ShapeElementModel, ShapeType, } from '@formicoidea/labre-core/model';
4
+ import { eventStormingCommandIcons, eventStormingCommands, } from './commands.js';
5
+ import { ES_STICKY_ROLE } from './roles.js';
6
+ export const ES_STICKY_FACES = [
7
+ ...ES_STICKIES.map((preset) => ({
8
+ kind: preset.kind,
9
+ fill: preset.fill,
10
+ text: preset.text,
11
+ label: preset.label,
12
+ shapeType: ShapeType.Rect,
13
+ })),
14
+ {
15
+ kind: 'hotspot',
16
+ fill: ES_HOTSPOT.fill,
17
+ text: ES_HOTSPOT.text,
18
+ label: ES_HOTSPOT.label,
19
+ shapeType: ShapeType.Diamond,
20
+ },
21
+ ];
22
+ const FACE_OF_KIND = new Map(ES_STICKY_FACES.map(face => [face.kind, face]));
23
+ /** The one family: every sticky, in the order the grammar reads them. */
24
+ export const ES_MORPH_FAMILIES = [ES_STICKY_FACES.map(face => face.kind)];
25
+ /**
26
+ * The rounding a silhouette takes — the same ternary `addSticky` applies, so a
27
+ * morphed diamond cannot end up with the square's soft corners.
28
+ */
29
+ const radiusOf = (shapeType) => shapeType === ShapeType.Rect ? STICKY_RADIUS : 0;
30
+ /* ── The kind, which is not a field ────────────────────────────────────── */
31
+ /**
32
+ * Role → kind, the inverse of the table `roles.ts` derives.
33
+ *
34
+ * A sticky has no `kind` prop and is not getting one: it is a native `shape`,
35
+ * and what makes it a domain EVENT rather than an orange square is the `role`
36
+ * on its face. That is already the single source every validation rule reads,
37
+ * so inverting it is cheaper and safer than writing a second identity beside
38
+ * it — a document where the two disagreed would be a document whose rules and
39
+ * whose toolbar told different stories about the same sticky.
40
+ */
41
+ const KIND_OF_ROLE = new Map(ES_STICKY_FACES.map(face => [ES_STICKY_ROLE[face.kind], face.kind]));
42
+ /**
43
+ * The kind a shape carries, or `undefined` when it carries none.
44
+ *
45
+ * `undefined` for a sticky placed before WS5 is the whole compat story and it
46
+ * is deliberate: those carry no role, they are never evaluated by a rule
47
+ * (promesse #71), and nothing here backfills one. A morph that inferred a kind
48
+ * from a fill colour would be writing a semantic claim the workshop never
49
+ * made — so a pre-WS5 sticky is simply not offered the menu, exactly as it is
50
+ * not offered a verdict.
51
+ */
52
+ export function eventStormingStickyKind(model) {
53
+ const role = model.role;
54
+ return role === undefined ? undefined : KIND_OF_ROLE.get(role);
55
+ }
56
+ /* ── Resolving the composite ───────────────────────────────────────────── */
57
+ /**
58
+ * The FACE of a selected sticky — `undefined` when the group is not one.
59
+ *
60
+ * A sticky is a native `group` of two shapes: a neutral offset rectangle that
61
+ * is the drop shadow, and the coloured face that carries the role, the words
62
+ * and every connector endpoint. One click selects the group, so this is the
63
+ * same indirection `c4NodeOfComponent` performs, and the same gate: the face is
64
+ * identified by its ROLE and not by its position in the group, so the shadow
65
+ * (which is ink and carries none) is skipped, a plain lasso round two
66
+ * rectangles resolves to nothing, another framework's composite resolves to
67
+ * nothing, and a legacy sticky whose label is a separate text element resolves
68
+ * to nothing because its face carries no role either.
69
+ *
70
+ * TWO faces is a refusal rather than a first-wins pick, for the reason C4
71
+ * gives: a group of two stickies is a group somebody made of two stickies, and
72
+ * morphing "it" would mean choosing one of them by document order.
73
+ */
74
+ export function eventStormingFaceOfSticky(model) {
75
+ if (!(model instanceof GroupElementModel))
76
+ return undefined;
77
+ let found;
78
+ for (const child of model.childElements) {
79
+ if (!(child instanceof ShapeElementModel))
80
+ continue;
81
+ if (eventStormingStickyKind(child) === undefined)
82
+ continue;
83
+ if (found)
84
+ return undefined;
85
+ found = child;
86
+ }
87
+ return found;
88
+ }
89
+ /* ── The patch ─────────────────────────────────────────────────────────── */
90
+ /**
91
+ * What one kind is worth to a face that ALREADY EXISTS.
92
+ *
93
+ * Five keys, and they are exactly the five that differ between two stickies:
94
+ * the role, the two colours, the silhouette and its rounding. Every other prop
95
+ * `addSticky` writes — `filled`, `strokeColor`, `strokeWidth`, `shapeStyle`,
96
+ * `roughness`, and the whole typography of the contained label — is the SAME
97
+ * for all nine kinds, which the unit suite asserts rather than assumes: the day
98
+ * a tenth sticky lands with a preset of its own, that case is what says this
99
+ * table has to grow.
100
+ *
101
+ * ## Why the size is not in here, unlike C4 and BPMN
102
+ *
103
+ * Because on this notation size is not a preset, it is a statement the author
104
+ * made. The aggregate is BORN at 160 against the standard 120 (`commands.ts`),
105
+ * but a sticky a workshop stretched to fit six words is stretched on purpose,
106
+ * and `xywh` is the one thing a morph promises never to touch — the generic
107
+ * module strips it whatever a spec returns. The consequence is visible and
108
+ * accepted: an aggregate morphed to a command keeps its 160, and a command
109
+ * morphed to an aggregate stays 120. The paint says what it is; the size says
110
+ * how much room the author gave it.
111
+ */
112
+ export function eventStormingMorphProps(kind) {
113
+ const face = FACE_OF_KIND.get(kind);
114
+ // Total over the union by construction — the map is built from the same list
115
+ // the type is derived from — and defensive only for a host that hands a
116
+ // string in from outside TypeScript.
117
+ if (!face)
118
+ return {};
119
+ return {
120
+ role: ES_STICKY_ROLE[kind],
121
+ fillColor: face.fill,
122
+ color: face.text,
123
+ shapeType: face.shapeType,
124
+ radius: radiusOf(face.shapeType),
125
+ };
126
+ }
127
+ /* ── What the rest of the sticky owes the change ───────────────────────── */
128
+ /** The words a freshly-drawn sticky of each kind carries. */
129
+ const STICKY_LABEL = Object.fromEntries(ES_STICKY_FACES.map(face => [face.kind, face.label]));
130
+ /**
131
+ * The words a sticky should carry once its face has morphed — or `null` when
132
+ * the words are the AUTHOR's and must not be touched.
133
+ *
134
+ * The same timid rule as `c4MorphedTitle`, and it matters more here: a sticky
135
+ * nobody has written on still says "Domain event", and an orange one that
136
+ * became blue while still saying "Domain event" is a wall contradicting itself
137
+ * in the one place a reader looks. Exactly one case rewrites — the text is the
138
+ * source kind's own creation prompt, letter for letter. A sticky somebody wrote
139
+ * "Order placed" on is called that whatever it becomes, and so is one they
140
+ * cleared, one they added a word to, and one carrying another kind's prompt.
141
+ *
142
+ * Pure and total over every string: the input is a shape label a user may have
143
+ * typed anything into.
144
+ */
145
+ export function eventStormingMorphedLabel(from, to, rawText) {
146
+ return (rawText ?? '').trim() === STICKY_LABEL[from]
147
+ ? STICKY_LABEL[to]
148
+ : null;
149
+ }
150
+ /**
151
+ * The shadow behind a face — the sibling shape drawn in {@link SHADOW_COLOR}.
152
+ *
153
+ * Identified by its paint and not by its index, because the group's child order
154
+ * is a painting decision `addSticky` happens to make and not a contract. It is
155
+ * excluded by identity from the face rather than by role, so a document where
156
+ * some hand-built group put a role on something unexpected still cannot make
157
+ * the morph rewrite the very element it just patched.
158
+ */
159
+ function shadowBehind(group, face) {
160
+ return group.childElements.find((child) => child instanceof ShapeElementModel &&
161
+ child !== face &&
162
+ child.fillColor === SHADOW_COLOR);
163
+ }
164
+ /**
165
+ * Keep the sticky's shadow and its words saying what the face now is — the
166
+ * `afterMorph` half of the spec, run inside the morph's own `captureSync` so
167
+ * one ctrl+z puts the whole sticky back.
168
+ *
169
+ * Two things the patch cannot reach, for two different reasons:
170
+ *
171
+ * - **The shadow** is a different ELEMENT. It is the only faux-drop-shadow in
172
+ * the repo — an offset rectangle behind the face rather than a canvas shadow
173
+ * — and it is drawn in the face's own silhouette. Morph a square sticky into
174
+ * a hotspot without it and the diamond floats over a rectangular smudge. Its
175
+ * POSITION is deliberately untouched: the offset is the author's geometry as
176
+ * much as the face's box is.
177
+ * - **The words** are `text`, which the generic module strips from every patch
178
+ * on principle. The rewrite is the placeholder→placeholder rule above.
179
+ *
180
+ * Both derive the target's appearance from {@link eventStormingMorphProps}
181
+ * rather than by reading the freshly-patched face: what the shadow must follow
182
+ * is what the KIND says, and a hook that read its sibling would quietly depend
183
+ * on the order the generic module happens to write in.
184
+ */
185
+ function followTheFace(model, from, to) {
186
+ if (!(model instanceof GroupElementModel))
187
+ return;
188
+ const face = eventStormingFaceOfSticky(model);
189
+ if (!face)
190
+ return;
191
+ const { shapeType, radius } = eventStormingMorphProps(to);
192
+ const shadow = shadowBehind(model, face);
193
+ if (shadow &&
194
+ !shadow.isLocked() &&
195
+ (shadow.shapeType !== shapeType || shadow.radius !== radius)) {
196
+ shadow.surface.updateElement(shadow.id, { shapeType, radius });
197
+ }
198
+ rewriteLabel(face, from, to);
199
+ }
200
+ /**
201
+ * Rewrite the face's own label, in place, if the rule says to.
202
+ *
203
+ * In place and in one transaction, like `rewriteTier`: the `Y.Text` instance is
204
+ * what a bound editor holds, so it is mutated rather than replaced. The
205
+ * comparison is against the TRIMMED text — the same string the rule was given —
206
+ * so a decision made on `'Command '` does not spend a transaction rewriting
207
+ * the padding away.
208
+ */
209
+ function rewriteLabel(face, from, to) {
210
+ const words = face.text;
211
+ if (!words)
212
+ return;
213
+ const text = words.toString().trim();
214
+ const next = eventStormingMorphedLabel(from, to, text);
215
+ if (next === null || next === text)
216
+ return;
217
+ face.surface.store.transact(() => {
218
+ words.delete(0, words.length);
219
+ words.insert(0, next);
220
+ });
221
+ }
222
+ /* ── The chrome ────────────────────────────────────────────────────────── */
223
+ /**
224
+ * The creation command that draws each kind, keyed BY that kind.
225
+ *
226
+ * Derived from `telemetry.element`, which is where the kind is already written
227
+ * down (`sticky:domainEvent`) and is documented as a historical value that must
228
+ * not be renamed (ADR 0008). Deriving rather than restating is what stops a
229
+ * second table of labels and icons drifting from the one the sub-menu, the
230
+ * catalogue and the agent read — including the one entry where it would bite:
231
+ * `system` is labelled "External system", and a restated table would sooner or
232
+ * later say "System".
233
+ */
234
+ const STICKY_COMMANDS = new Map(eventStormingCommands.flatMap(command => {
235
+ const element = command.telemetry?.element;
236
+ return element?.startsWith('sticky:')
237
+ ? [[element.slice('sticky:'.length), command]]
238
+ : [];
239
+ }));
240
+ /** A kind's wording: the creation command's own key and English. */
241
+ function labelOf(kind) {
242
+ const command = STICKY_COMMANDS.get(kind);
243
+ return {
244
+ key: command?.labelKey,
245
+ fallback: command?.labelFallback ?? kind,
246
+ };
247
+ }
248
+ /** A kind's icon: the creation command's own swatch, reused rather than redrawn. */
249
+ function iconOf(kind) {
250
+ const iconKey = STICKY_COMMANDS.get(kind)?.iconKey;
251
+ return ((iconKey && eventStormingCommandIcons[iconKey]) ||
252
+ eventStormingCommandIcons['ddd-event-storming.sticky.domainEvent']);
253
+ }
254
+ /**
255
+ * Event Storming's morph declaration, handed to the generic
256
+ * `morphToolbarConfig`.
257
+ *
258
+ * `modelType` is the native `GroupElementModel` and not a shape, because the
259
+ * group is what a click on a sticky selects — the same arrangement C4 has, and
260
+ * everything that makes it safe is in {@link eventStormingFaceOfSticky}: the
261
+ * toolbar's own homogeneity test proves only that every selection is A group,
262
+ * and the resolution proves that every one of them is one of OURS.
263
+ */
264
+ export const EVENT_STORMING_MORPH_SPEC = {
265
+ framework: 'ddd-event-storming',
266
+ families: ES_MORPH_FAMILIES,
267
+ modelType: GroupElementModel,
268
+ resolveTarget: eventStormingFaceOfSticky,
269
+ kindOf: eventStormingStickyKind,
270
+ // The FACE's role, which is the only role a sticky has: the group carries
271
+ // none and the shadow is ink. Unlike C4's, this table does not collapse —
272
+ // nine kinds, nine roles — so the telemetry's `fromRole`/`toRole` always name
273
+ // the two claims the workshop swapped between.
274
+ roleOf: kind => ES_STICKY_ROLE[kind],
275
+ propsOf: eventStormingMorphProps,
276
+ afterMorph: followTheFace,
277
+ labelOf,
278
+ iconOf,
279
+ label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
280
+ };
package/dist/view.js CHANGED
@@ -1,12 +1,13 @@
1
- import { QualityNudgeExtension, validationToolbarConfig, ValidationProfileExtension, ValidationRuleExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { morphToolbarConfig, QualityNudgeExtension, validationToolbarConfig, ValidationProfileExtension, ValidationRuleExtension, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ViewExtensionProvider, } from '@formicoidea/labre-core/ext-loader';
3
- import { ToolbarModuleExtension } from '@formicoidea/labre-core/shared/services';
3
+ import { ToolbarModuleExtension, toolbarModuleKey, } from '@formicoidea/labre-core/shared/services';
4
4
  import { BlockFlavourIdentifier, CommandExtension } from '@formicoidea/labre-core/std';
5
5
  import { RoleVocabularyExtension } from '@formicoidea/labre-core/std/gfx';
6
6
  import { eventStormingCommandIcons, eventStormingCommands } from './commands.js';
7
7
  import { eventStormingEffects } from './effects.js';
8
8
  import { EventStormingRendererExtension } from './element-renderer.js';
9
9
  import { EventStormingInteraction, EventStormingView } from './element-view.js';
10
+ import { EVENT_STORMING_MORPH_SPEC } from './morph.js';
10
11
  import { EVENT_STORMING_NUDGES } from './nudges.js';
11
12
  import { EVENT_STORMING_PROFILES } from './profiles.js';
12
13
  import { EVENT_STORMING_ROLES } from './roles.js';
@@ -82,6 +83,33 @@ export class DddEventStormingViewExtension extends ViewExtensionProvider {
82
83
  }));
83
84
  context.register(eventStormingSeniorTool);
84
85
  context.register(CommandExtension(eventStormingCommands, eventStormingCommandIcons));
86
+ // The "Change type" dropdown on a selected STICKY's contextual toolbar —
87
+ // the generic module, parameterized by Event Storming's own family.
88
+ //
89
+ // ## Why the key carries an owner
90
+ //
91
+ // A sticky is a native `group` (the shadow and the face), so the row the
92
+ // toolbar draws for it is the GROUP's row, merged by the registry from
93
+ // every module registered on that flavour. Both plain group keys were
94
+ // claimed long ago — `affine:surface:group` by the native operations
95
+ // (rename, ungroup), `custom:affine:surface:group` by Wardley's
96
+ // qualification dropdown — and C4's morph already took the
97
+ // `#c4-morph` variant. `toolbarModuleKey` is what lifts the ceiling of
98
+ // one module per flavour: this one is registered under
99
+ // `custom:affine:surface:group#ddd-event-storming-morph` and the registry
100
+ // hands it to the same row (`toolbar-service/registry.ts`).
101
+ //
102
+ // ## Why here
103
+ //
104
+ // In the flag-gated half, because a morph is TOOLING: a sticky placed
105
+ // while the flag was on keeps its role, its colours, its words, its
106
+ // shadow and its place in every rule when the flag goes off — it just
107
+ // stops being something the toolbar offers to say differently
108
+ // (`docs/adr/0009`).
109
+ context.register(ToolbarModuleExtension({
110
+ id: BlockFlavourIdentifier(toolbarModuleKey('custom:affine:surface:group', 'ddd-event-storming-morph')),
111
+ config: morphToolbarConfig(EVENT_STORMING_MORPH_SPEC),
112
+ }));
85
113
  }
86
114
  }
87
115
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-ddd-event-storming",
3
3
  "description": "Labre ddd-event-storming framework for @formicoidea/labre-core.",
4
- "version": "0.34.1",
4
+ "version": "0.35.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -31,8 +31,8 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@formicoidea/labre-core": "0.34.1",
35
- "@formicoidea/labre-ddd-shared": "0.34.1",
34
+ "@formicoidea/labre-core": "0.35.0",
35
+ "@formicoidea/labre-ddd-shared": "0.35.0",
36
36
  "lit": "^3.2.0"
37
37
  }
38
38
  }