@formicoidea/labre-framework-ddd-event-storming 0.34.2 → 0.36.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 +10 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/morph.d.ts +139 -0
- package/dist/morph.js +280 -0
- package/dist/view.js +30 -2
- package/package.json +3 -3
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
|
*
|
|
@@ -35,6 +35,7 @@ const SPECS = [
|
|
|
35
35
|
// A NEW telemetry value: every other entry below keeps the `element` string
|
|
36
36
|
// it has emitted since ADR 0008.
|
|
37
37
|
element: 'board',
|
|
38
|
+
board: true,
|
|
38
39
|
icon: boardSwatch,
|
|
39
40
|
run: std => createEventStormingBoard(std.get(GfxControllerIdentifier)),
|
|
40
41
|
},
|
|
@@ -98,6 +99,10 @@ export const eventStormingCommands = SPECS.map((spec, order) => ({
|
|
|
98
99
|
defaultKeys: { mac: [], other: [] },
|
|
99
100
|
availability: 'always',
|
|
100
101
|
run: spec.run,
|
|
101
|
-
telemetry: {
|
|
102
|
+
telemetry: {
|
|
103
|
+
framework: 'ddd-event-storming',
|
|
104
|
+
element: spec.element,
|
|
105
|
+
board: spec.board,
|
|
106
|
+
},
|
|
102
107
|
}));
|
|
103
108
|
export const eventStormingCommandIcons = Object.fromEntries(SPECS.map(spec => [spec.iconKey, spec.icon]));
|
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';
|
package/dist/morph.d.ts
ADDED
|
@@ -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.
|
|
4
|
+
"version": "0.36.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.
|
|
35
|
-
"@formicoidea/labre-ddd-shared": "0.
|
|
34
|
+
"@formicoidea/labre-core": "0.36.0",
|
|
35
|
+
"@formicoidea/labre-ddd-shared": "0.36.0",
|
|
36
36
|
"lit": "^3.2.0"
|
|
37
37
|
}
|
|
38
38
|
}
|