@formicoidea/labre-framework-ddd-core-domain 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 +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/morph.d.ts +117 -0
- package/dist/morph.js +344 -0
- package/dist/view.js +30 -2
- package/package.json +3 -3
package/dist/commands.js
CHANGED
|
@@ -8,10 +8,10 @@ import { markerRole, subdomainRole } from './roles.js';
|
|
|
8
8
|
* The notation legend stays in the map-background context menu (toolbar
|
|
9
9
|
* config) — the element toolbar is out of scope for `docs/adr/0008`.
|
|
10
10
|
*/
|
|
11
|
-
const chartSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="17" height="17" fill="#4d9900" fill-opacity="0.5"/><rect x="4" y="3" width="6" height="17" fill="#9933ff" fill-opacity="0.5"/><path d="M4 20 V3 M4 20 H21" stroke="currentColor" stroke-width="1.8"/></svg>`;
|
|
12
|
-
const dotSwatch = (color) => svg `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="${color}" stroke="#1f2328" stroke-width="1.2"/></svg>`;
|
|
13
|
-
const movementSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><path d="M3 18 L16 7" stroke="${MOVEMENT_COLOR}" stroke-width="2" stroke-dasharray="3 3"/><path d="M12 6 L18 5 L17 11" stroke="${MOVEMENT_COLOR}" stroke-width="2" fill="none"/></svg>`;
|
|
14
|
-
const markerSwatch = (fill, letter) => svg `<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="3" fill="${fill}" stroke="#1f2328" stroke-width="1.2"/><text x="12" y="16" text-anchor="middle" font-size="11" font-family="sans-serif" fill="#1f2328">${letter}</text></svg>`;
|
|
11
|
+
const chartSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="17" height="17" fill="#4d9900" fill-opacity="0.5"/><rect x="4" y="3" width="6" height="17" fill="#9933ff" fill-opacity="0.5"/><path d="M4 20 V3 M4 20 H21" stroke="currentColor" stroke-width="1.8"/></svg>`;
|
|
12
|
+
const dotSwatch = (color) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="${color}" stroke="#1f2328" stroke-width="1.2"/></svg>`;
|
|
13
|
+
const movementSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3 18 L16 7" stroke="${MOVEMENT_COLOR}" stroke-width="2" stroke-dasharray="3 3"/><path d="M12 6 L18 5 L17 11" stroke="${MOVEMENT_COLOR}" stroke-width="2" fill="none"/></svg>`;
|
|
14
|
+
const markerSwatch = (fill, letter) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="3" fill="${fill}" stroke="#1f2328" stroke-width="1.2"/><text x="12" y="16" text-anchor="middle" font-size="11" font-family="sans-serif" fill="#1f2328">${letter}</text></svg>`;
|
|
15
15
|
const SPECS = [
|
|
16
16
|
{
|
|
17
17
|
id: 'addChart',
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
|
|
|
3
3
|
export { coreDomainTranslationEntries } from './translations.js';
|
|
4
4
|
export { CORE_DOMAIN_BACKGROUND, CORE_DOMAIN_LEGEND_TONES, } from './core-domain/background.js';
|
|
5
5
|
export { coreDomain } from './core-domain/element-renderer.js';
|
|
6
|
+
export { CORE_DOMAIN_MORPH_FAMILIES, CORE_DOMAIN_MORPH_SPEC, coreDomainArtefactOf, coreDomainMorphClears, coreDomainMorphedCaption, coreDomainMorphProps, type CdMorphKind, } from './morph.js';
|
|
6
7
|
export { CORE_DOMAIN_NUDGES } from './nudges.js';
|
|
7
8
|
export { CORE_DOMAIN_PROFILES } from './profiles.js';
|
|
8
9
|
export { CORE_DOMAIN_ROLE, CORE_DOMAIN_ROLES, type CoreDomainRole, type CoreDomainRoleId, subdomainRole, } from './roles.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,10 @@ export { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
|
|
|
3
3
|
export { coreDomainTranslationEntries } from './translations.js';
|
|
4
4
|
export { CORE_DOMAIN_BACKGROUND, CORE_DOMAIN_LEGEND_TONES, } from './core-domain/background.js';
|
|
5
5
|
export { coreDomain } from './core-domain/element-renderer.js';
|
|
6
|
+
// The morph: the families the dropdown is parameterized by, the resolution from
|
|
7
|
+
// a selected group to the shape the role lives on, the patch a kind is worth,
|
|
8
|
+
// and the caption rule — all of them what the unit and integration suites drive.
|
|
9
|
+
export { CORE_DOMAIN_MORPH_FAMILIES, CORE_DOMAIN_MORPH_SPEC, coreDomainArtefactOf, coreDomainMorphClears, coreDomainMorphedCaption, coreDomainMorphProps, } from './morph.js';
|
|
6
10
|
export { CORE_DOMAIN_NUDGES } from './nudges.js';
|
|
7
11
|
export { CORE_DOMAIN_PROFILES } from './profiles.js';
|
|
8
12
|
export { CORE_DOMAIN_ROLE, CORE_DOMAIN_ROLES, subdomainRole, } from './roles.js';
|
package/dist/morph.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { ShapeElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
import { type CdMarkerKind, type CdSubdomainKind } from './roles.js';
|
|
5
|
+
/**
|
|
6
|
+
* What a Core Domain artefact may BECOME — the declaration behind the "Change
|
|
7
|
+
* type" dropdown on a selected dot's or marker's contextual toolbar.
|
|
8
|
+
*
|
|
9
|
+
* ## Two families, and nothing between them
|
|
10
|
+
*
|
|
11
|
+
* A big bet and a platform sub-domain are the same artefact said differently:
|
|
12
|
+
* both are a dot plotted at a (differentiation, complexity) position, and
|
|
13
|
+
* realising halfway through a chart that this one is really a platform is
|
|
14
|
+
* modelling rather than a mistake. The two bounded contexts are in that family
|
|
15
|
+
* too — `roles.ts` already parents all five on `core-domain:subdomain`, because
|
|
16
|
+
* a bounded context is placed and read exactly like the other three and what
|
|
17
|
+
* makes it a `bc-current` is only that a movement may start from it.
|
|
18
|
+
*
|
|
19
|
+
* The three Team Topologies markers are the second family and the two never
|
|
20
|
+
* meet. That is the same disjunction `roles.ts` draws: a marker is an
|
|
21
|
+
* ANNOTATION about how work flows between teams, a sub-domain is a thing
|
|
22
|
+
* plotted on the chart, and no rule written on one may ever fall on the other.
|
|
23
|
+
* Turning a dot into a square would move an artefact from the picture to the
|
|
24
|
+
* commentary about it, which is not "the same thing, said more precisely". The
|
|
25
|
+
* generic module refuses a mixed selection on its own (the two kinds are in no
|
|
26
|
+
* common family), so this stays a statement of data rather than a check.
|
|
27
|
+
*
|
|
28
|
+
* The chart background and the movement edge are in NO family: the chart is the
|
|
29
|
+
* frame of reference the others are measured against, and the movement is an
|
|
30
|
+
* edge whose morph would be a different gesture entirely (`docs/adr/0010`).
|
|
31
|
+
*/
|
|
32
|
+
export declare const CORE_DOMAIN_MORPH_FAMILIES: readonly (readonly CdMorphKind[])[];
|
|
33
|
+
/** Everything that morphs in this framework: the five dots and the three markers. */
|
|
34
|
+
export type CdMorphKind = CdSubdomainKind | CdMarkerKind;
|
|
35
|
+
/**
|
|
36
|
+
* What a kind is worth to a shape that ALREADY EXISTS — the creation preset
|
|
37
|
+
* minus the three things a morph has no business touching.
|
|
38
|
+
*
|
|
39
|
+
* ## Why the whole preset, and not `{role, fillColor}`
|
|
40
|
+
*
|
|
41
|
+
* Because it is insurance, and it is free. Every member of a family shares its
|
|
42
|
+
* stroke, its stroke width, its silhouette and — inside the markers — its corner
|
|
43
|
+
* radius today, so all but `role` and `fillColor` are inert on this table. That
|
|
44
|
+
* is exactly the state BPMN's families were in before `subProcess` /
|
|
45
|
+
* `callActivity` gained a border weight that mattered: a family is DATA and
|
|
46
|
+
* grows by declaration, with no code change to prompt anyone to check the
|
|
47
|
+
* presets. Derived from the creation builder, the palette and the morph cannot
|
|
48
|
+
* drift apart whatever is added, and a morphed dot is byte-for-byte a dot
|
|
49
|
+
* freshly placed from the sub-menu.
|
|
50
|
+
*/
|
|
51
|
+
export declare function coreDomainMorphProps(kind: CdMorphKind): Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
54
|
+
* writes and this one does not.
|
|
55
|
+
*
|
|
56
|
+
* EMPTY for every kind today: `dddShapeProps` writes one fixed key set whatever
|
|
57
|
+
* the options say, and the label branch it does spread conditionally is never
|
|
58
|
+
* taken here (a dot's caption is a sibling text element, not the shape's own
|
|
59
|
+
* words). Derived rather than hard-coded to `[]` for the reason C4's equivalent
|
|
60
|
+
* is: a patch cannot express absence, and the day one preset stops writing a key
|
|
61
|
+
* the previous kind's value would otherwise stay in the Y.Map, silently in
|
|
62
|
+
* force.
|
|
63
|
+
*/
|
|
64
|
+
export declare function coreDomainMorphClears(kind: CdMorphKind): readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* The dot or the marker square a selected GROUP is built round — `undefined`
|
|
67
|
+
* when the group is not one of ours.
|
|
68
|
+
*
|
|
69
|
+
* Both artefacts are composites: a dot is `group[ellipse, caption]` and a marker
|
|
70
|
+
* is `group[square, letter, caption]`, so what a click selects is the group and
|
|
71
|
+
* what carries the role is the shape inside it. This is the whole of the
|
|
72
|
+
* indirection and it is also the gate: a plain group somebody lassoed round
|
|
73
|
+
* three rectangles, a C4 component, a legend box and a group of two dots all
|
|
74
|
+
* answer `undefined`, so none of them is ever offered the menu.
|
|
75
|
+
*
|
|
76
|
+
* TWO artefacts is a refusal rather than a first-wins pick, for the reason
|
|
77
|
+
* `c4NodeOfComponent` gives: morphing "it" would mean choosing one of them by
|
|
78
|
+
* document order, and the honest answer to an ambiguous selection is nothing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function coreDomainArtefactOf(model: GfxPrimitiveElementModel): ShapeElementModel | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The caption an artefact should carry once it has morphed — or `null` when the
|
|
83
|
+
* words are the AUTHOR's and must not be touched.
|
|
84
|
+
*
|
|
85
|
+
* The C4 title rule, applied to the one caption these artefacts have: exactly
|
|
86
|
+
* one case rewrites, the caption being the source kind's own creation prompt
|
|
87
|
+
* letter for letter, which is what a dot nobody has named still says. Everything
|
|
88
|
+
* else — a name typed over it, the prompt with a word added, another kind's
|
|
89
|
+
* prompt, a cleared caption — is content, and content survives a morph
|
|
90
|
+
* untouched.
|
|
91
|
+
*
|
|
92
|
+
* It is the tier a user sees go wrong: every one of the eight kinds prompts with
|
|
93
|
+
* a different word, so a fresh "Big-bet sub-domain" morphed to a platform would
|
|
94
|
+
* otherwise be a blue dot captioned "Big-bet sub-domain" — a picture flatly
|
|
95
|
+
* contradicting its own name.
|
|
96
|
+
*
|
|
97
|
+
* Pure and total over every string, like `c4MorphedTitle`: the input is a canvas
|
|
98
|
+
* text element somebody may have typed anything into.
|
|
99
|
+
*/
|
|
100
|
+
export declare function coreDomainMorphedCaption(from: CdMorphKind, to: CdMorphKind, rawText: string | null | undefined): string | null;
|
|
101
|
+
/**
|
|
102
|
+
* The Core Domain morph declaration, handed to the generic `morphToolbarConfig`.
|
|
103
|
+
*
|
|
104
|
+
* `modelType` is the native `GroupElementModel` and not `ShapeElementModel`,
|
|
105
|
+
* because both artefacts are composites and the group is what a click on one
|
|
106
|
+
* selects. Everything that makes that safe is in {@link coreDomainArtefactOf}:
|
|
107
|
+
* the toolbar's own homogeneity test only proves that every selected element is
|
|
108
|
+
* A group, and the resolution proves that every one of them is one of ours.
|
|
109
|
+
*
|
|
110
|
+
* Every shipped creation path builds the group — `commands.ts` always passes a
|
|
111
|
+
* caption, so `addDot`'s bare-ellipse branch is unreachable from the palette,
|
|
112
|
+
* the templates and the agent alike. A dot that somehow arrives ungrouped is
|
|
113
|
+
* simply not offered the menu, which is the same refusal a role-less dot gets
|
|
114
|
+
* and for the same reason: nothing is inferred, nothing is repaired behind the
|
|
115
|
+
* user's back.
|
|
116
|
+
*/
|
|
117
|
+
export declare const CORE_DOMAIN_MORPH_SPEC: MorphSpec<CdMorphKind>;
|
package/dist/morph.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { CD_SUBDOMAINS, dddShapeProps, dotShapeOpts, markerShapeOpts, TEAM_TOPOLOGIES, } from '@formicoidea/labre-ddd-shared';
|
|
3
|
+
import { GroupElementModel, ShapeElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
|
|
4
|
+
import { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
|
|
5
|
+
import { CORE_DOMAIN_ROLE, markerRole, subdomainRole, } from './roles.js';
|
|
6
|
+
/**
|
|
7
|
+
* What a Core Domain artefact may BECOME — the declaration behind the "Change
|
|
8
|
+
* type" dropdown on a selected dot's or marker's contextual toolbar.
|
|
9
|
+
*
|
|
10
|
+
* ## Two families, and nothing between them
|
|
11
|
+
*
|
|
12
|
+
* A big bet and a platform sub-domain are the same artefact said differently:
|
|
13
|
+
* both are a dot plotted at a (differentiation, complexity) position, and
|
|
14
|
+
* realising halfway through a chart that this one is really a platform is
|
|
15
|
+
* modelling rather than a mistake. The two bounded contexts are in that family
|
|
16
|
+
* too — `roles.ts` already parents all five on `core-domain:subdomain`, because
|
|
17
|
+
* a bounded context is placed and read exactly like the other three and what
|
|
18
|
+
* makes it a `bc-current` is only that a movement may start from it.
|
|
19
|
+
*
|
|
20
|
+
* The three Team Topologies markers are the second family and the two never
|
|
21
|
+
* meet. That is the same disjunction `roles.ts` draws: a marker is an
|
|
22
|
+
* ANNOTATION about how work flows between teams, a sub-domain is a thing
|
|
23
|
+
* plotted on the chart, and no rule written on one may ever fall on the other.
|
|
24
|
+
* Turning a dot into a square would move an artefact from the picture to the
|
|
25
|
+
* commentary about it, which is not "the same thing, said more precisely". The
|
|
26
|
+
* generic module refuses a mixed selection on its own (the two kinds are in no
|
|
27
|
+
* common family), so this stays a statement of data rather than a check.
|
|
28
|
+
*
|
|
29
|
+
* The chart background and the movement edge are in NO family: the chart is the
|
|
30
|
+
* frame of reference the others are measured against, and the movement is an
|
|
31
|
+
* edge whose morph would be a different gesture entirely (`docs/adr/0010`).
|
|
32
|
+
*/
|
|
33
|
+
export const CORE_DOMAIN_MORPH_FAMILIES = [
|
|
34
|
+
// Derived from the shared preset tables rather than restated, so declaration
|
|
35
|
+
// order is MENU order and the menu reads in the order the senior sub-menu
|
|
36
|
+
// offers them — a sixth dot preset arrives in the dropdown with no edit here.
|
|
37
|
+
CD_SUBDOMAINS.map(preset => preset.kind),
|
|
38
|
+
TEAM_TOPOLOGIES.map(preset => preset.kind),
|
|
39
|
+
];
|
|
40
|
+
/** A kind's own role — the id the patch writes and the telemetry reports. */
|
|
41
|
+
function roleOf(kind) {
|
|
42
|
+
return CORE_DOMAIN_ROLE[kind];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Role → kind, the inverse of {@link CORE_DOMAIN_ROLE} restricted to the eight
|
|
46
|
+
* morphable kinds.
|
|
47
|
+
*
|
|
48
|
+
* The kind of a Core Domain artefact has nowhere else to live: a dot is a
|
|
49
|
+
* native `shape` and a marker is a native `shape`, so unlike a `bpmnNode` or a
|
|
50
|
+
* `c4Node` there is no `kind` field to read — the role IS the kind, which is the
|
|
51
|
+
* arrangement `commands.ts` has stamped since the roles landed.
|
|
52
|
+
*
|
|
53
|
+
* An element carrying no role is therefore an element with no kind, and it is
|
|
54
|
+
* REFUSED rather than guessed at. A chart drawn before the vocabulary existed
|
|
55
|
+
* carries role-less dots, and the promise `roles.ts` makes about them is that
|
|
56
|
+
* nothing infers anything from their colour: no backfill, ever (#71). Such a dot
|
|
57
|
+
* opens, paints and round-trips exactly as it always did; it just is not offered
|
|
58
|
+
* a menu that would have to invent what it is first.
|
|
59
|
+
*
|
|
60
|
+
* Null prototype, like the vocabulary itself: this is a lookup keyed by strings
|
|
61
|
+
* read off a document, so `KIND_OF_ROLE['toString']` must not resolve.
|
|
62
|
+
*/
|
|
63
|
+
const KIND_OF_ROLE = Object.assign(Object.create(null), Object.fromEntries(CORE_DOMAIN_MORPH_FAMILIES.flat().map(kind => [roleOf(kind), kind])));
|
|
64
|
+
/** Whether a kind is one of the three markers — the family with a glyph. */
|
|
65
|
+
function isMarkerKind(kind) {
|
|
66
|
+
return kind in MARKER_LETTER;
|
|
67
|
+
}
|
|
68
|
+
/* ── The patch one kind is worth ───────────────────────────────────────── */
|
|
69
|
+
/**
|
|
70
|
+
* The box {@link coreDomainMorphProps} hands the shared builder and then throws
|
|
71
|
+
* away. Never written to a document: a morph keeps the geometry the element
|
|
72
|
+
* already has, and this exists only because the one builder takes a box.
|
|
73
|
+
*/
|
|
74
|
+
const DISCARDED_BOX = [0, 0, 0, 0];
|
|
75
|
+
/** What a morph must never rewrite: identity, geometry, and the user's words. */
|
|
76
|
+
const NOT_A_MORPH = ['type', 'xywh', 'text'];
|
|
77
|
+
/**
|
|
78
|
+
* What each kind IS, as shape options — the very objects `addDot` and
|
|
79
|
+
* `addMarker` build their artefact from ({@link dotShapeOpts},
|
|
80
|
+
* {@link markerShapeOpts}).
|
|
81
|
+
*/
|
|
82
|
+
const SHAPE_OPTS = Object.assign(Object.create(null), Object.fromEntries([
|
|
83
|
+
...CD_SUBDOMAINS.map(preset => [
|
|
84
|
+
preset.kind,
|
|
85
|
+
dotShapeOpts(preset.fill, subdomainRole(preset.kind)),
|
|
86
|
+
]),
|
|
87
|
+
...TEAM_TOPOLOGIES.map(preset => [
|
|
88
|
+
preset.kind,
|
|
89
|
+
markerShapeOpts(preset.fill, markerRole(preset.kind)),
|
|
90
|
+
]),
|
|
91
|
+
]));
|
|
92
|
+
/**
|
|
93
|
+
* What a kind is worth to a shape that ALREADY EXISTS — the creation preset
|
|
94
|
+
* minus the three things a morph has no business touching.
|
|
95
|
+
*
|
|
96
|
+
* ## Why the whole preset, and not `{role, fillColor}`
|
|
97
|
+
*
|
|
98
|
+
* Because it is insurance, and it is free. Every member of a family shares its
|
|
99
|
+
* stroke, its stroke width, its silhouette and — inside the markers — its corner
|
|
100
|
+
* radius today, so all but `role` and `fillColor` are inert on this table. That
|
|
101
|
+
* is exactly the state BPMN's families were in before `subProcess` /
|
|
102
|
+
* `callActivity` gained a border weight that mattered: a family is DATA and
|
|
103
|
+
* grows by declaration, with no code change to prompt anyone to check the
|
|
104
|
+
* presets. Derived from the creation builder, the palette and the morph cannot
|
|
105
|
+
* drift apart whatever is added, and a morphed dot is byte-for-byte a dot
|
|
106
|
+
* freshly placed from the sub-menu.
|
|
107
|
+
*/
|
|
108
|
+
export function coreDomainMorphProps(kind) {
|
|
109
|
+
const props = dddShapeProps(...DISCARDED_BOX, SHAPE_OPTS[kind]);
|
|
110
|
+
for (const key of NOT_A_MORPH)
|
|
111
|
+
delete props[key];
|
|
112
|
+
return props;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Every key ANY kind's props may carry — the union over both families.
|
|
116
|
+
*
|
|
117
|
+
* Computed rather than listed, so a preset that starts spreading a key
|
|
118
|
+
* conditionally is covered on the day it is added rather than on the day
|
|
119
|
+
* somebody notices.
|
|
120
|
+
*/
|
|
121
|
+
const EVERY_MORPH_KEY = new Set(CORE_DOMAIN_MORPH_FAMILIES.flat().flatMap(kind => Object.keys(coreDomainMorphProps(kind))));
|
|
122
|
+
/**
|
|
123
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
124
|
+
* writes and this one does not.
|
|
125
|
+
*
|
|
126
|
+
* EMPTY for every kind today: `dddShapeProps` writes one fixed key set whatever
|
|
127
|
+
* the options say, and the label branch it does spread conditionally is never
|
|
128
|
+
* taken here (a dot's caption is a sibling text element, not the shape's own
|
|
129
|
+
* words). Derived rather than hard-coded to `[]` for the reason C4's equivalent
|
|
130
|
+
* is: a patch cannot express absence, and the day one preset stops writing a key
|
|
131
|
+
* the previous kind's value would otherwise stay in the Y.Map, silently in
|
|
132
|
+
* force.
|
|
133
|
+
*/
|
|
134
|
+
export function coreDomainMorphClears(kind) {
|
|
135
|
+
const present = new Set(Object.keys(coreDomainMorphProps(kind)));
|
|
136
|
+
return [...EVERY_MORPH_KEY].filter(key => !present.has(key));
|
|
137
|
+
}
|
|
138
|
+
/* ── Resolving the composite ───────────────────────────────────────────── */
|
|
139
|
+
/**
|
|
140
|
+
* The dot or the marker square a selected GROUP is built round — `undefined`
|
|
141
|
+
* when the group is not one of ours.
|
|
142
|
+
*
|
|
143
|
+
* Both artefacts are composites: a dot is `group[ellipse, caption]` and a marker
|
|
144
|
+
* is `group[square, letter, caption]`, so what a click selects is the group and
|
|
145
|
+
* what carries the role is the shape inside it. This is the whole of the
|
|
146
|
+
* indirection and it is also the gate: a plain group somebody lassoed round
|
|
147
|
+
* three rectangles, a C4 component, a legend box and a group of two dots all
|
|
148
|
+
* answer `undefined`, so none of them is ever offered the menu.
|
|
149
|
+
*
|
|
150
|
+
* TWO artefacts is a refusal rather than a first-wins pick, for the reason
|
|
151
|
+
* `c4NodeOfComponent` gives: morphing "it" would mean choosing one of them by
|
|
152
|
+
* document order, and the honest answer to an ambiguous selection is nothing.
|
|
153
|
+
*/
|
|
154
|
+
export function coreDomainArtefactOf(model) {
|
|
155
|
+
if (!(model instanceof GroupElementModel))
|
|
156
|
+
return undefined;
|
|
157
|
+
let found;
|
|
158
|
+
for (const child of model.childElements) {
|
|
159
|
+
if (!(child instanceof ShapeElementModel))
|
|
160
|
+
continue;
|
|
161
|
+
if (kindOfArtefact(child) === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
if (found)
|
|
164
|
+
return undefined;
|
|
165
|
+
found = child;
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
/** The kind a shape carries, read off its role. */
|
|
170
|
+
function kindOfArtefact(model) {
|
|
171
|
+
if (!(model instanceof ShapeElementModel))
|
|
172
|
+
return undefined;
|
|
173
|
+
return model.role ? KIND_OF_ROLE[model.role] : undefined;
|
|
174
|
+
}
|
|
175
|
+
/* ── The words the composite owes the change ───────────────────────────── */
|
|
176
|
+
/** A kind's creation caption, both families in one table. */
|
|
177
|
+
const KIND_LABEL = Object.fromEntries([
|
|
178
|
+
...CD_SUBDOMAINS.map(preset => [preset.kind, preset.label]),
|
|
179
|
+
...TEAM_TOPOLOGIES.map(preset => [preset.kind, preset.label]),
|
|
180
|
+
]);
|
|
181
|
+
/** The glyph each marker wears — C, X, F. */
|
|
182
|
+
const MARKER_LETTER = Object.fromEntries(TEAM_TOPOLOGIES.map(preset => [preset.kind, preset.letter]));
|
|
183
|
+
/**
|
|
184
|
+
* The caption an artefact should carry once it has morphed — or `null` when the
|
|
185
|
+
* words are the AUTHOR's and must not be touched.
|
|
186
|
+
*
|
|
187
|
+
* The C4 title rule, applied to the one caption these artefacts have: exactly
|
|
188
|
+
* one case rewrites, the caption being the source kind's own creation prompt
|
|
189
|
+
* letter for letter, which is what a dot nobody has named still says. Everything
|
|
190
|
+
* else — a name typed over it, the prompt with a word added, another kind's
|
|
191
|
+
* prompt, a cleared caption — is content, and content survives a morph
|
|
192
|
+
* untouched.
|
|
193
|
+
*
|
|
194
|
+
* It is the tier a user sees go wrong: every one of the eight kinds prompts with
|
|
195
|
+
* a different word, so a fresh "Big-bet sub-domain" morphed to a platform would
|
|
196
|
+
* otherwise be a blue dot captioned "Big-bet sub-domain" — a picture flatly
|
|
197
|
+
* contradicting its own name.
|
|
198
|
+
*
|
|
199
|
+
* Pure and total over every string, like `c4MorphedTitle`: the input is a canvas
|
|
200
|
+
* text element somebody may have typed anything into.
|
|
201
|
+
*/
|
|
202
|
+
export function coreDomainMorphedCaption(from, to, rawText) {
|
|
203
|
+
return (rawText ?? '').trim() === KIND_LABEL[from] ? KIND_LABEL[to] : null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The two texts of a composite, told apart by WHERE they sit.
|
|
207
|
+
*
|
|
208
|
+
* The letter is centred ON the square and the caption stands beside the
|
|
209
|
+
* artefact, which is the notation's own arrangement rather than a detail of the
|
|
210
|
+
* builder: geometry survives a restyle, a re-word and a font change, where
|
|
211
|
+
* `textAlign`, a font size or "the text is one character long" would each stop
|
|
212
|
+
* being true the moment a user edits the thing they describe. A dot has no glyph
|
|
213
|
+
* at all and simply answers `undefined` for it.
|
|
214
|
+
*
|
|
215
|
+
* Either half is taken only when it is the ONE candidate. A group somebody has
|
|
216
|
+
* added a second caption to is a group where "the caption" is a guess, and this
|
|
217
|
+
* would rather leave both alone than rewrite the wrong one.
|
|
218
|
+
*/
|
|
219
|
+
function wordsOf(group, artefact) {
|
|
220
|
+
const texts = group.childElements.filter((child) => child instanceof TextElementModel);
|
|
221
|
+
const on = (text) => {
|
|
222
|
+
const cx = text.x + text.w / 2;
|
|
223
|
+
const cy = text.y + text.h / 2;
|
|
224
|
+
return (cx >= artefact.x &&
|
|
225
|
+
cx <= artefact.x + artefact.w &&
|
|
226
|
+
cy >= artefact.y &&
|
|
227
|
+
cy <= artefact.y + artefact.h);
|
|
228
|
+
};
|
|
229
|
+
const only = (found) => found.length === 1 ? found[0] : undefined;
|
|
230
|
+
return {
|
|
231
|
+
glyph: only(texts.filter(on)),
|
|
232
|
+
caption: only(texts.filter(text => !on(text))),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Write one text, in place, if the decision says to.
|
|
237
|
+
*
|
|
238
|
+
* `rewriteTier`'s contract, and its reasons: the comparison is against the
|
|
239
|
+
* TRIMMED text — the same string `decide` was given — so padding is never
|
|
240
|
+
* rewritten away for nothing, and the `Y.Text` instance is mutated rather than
|
|
241
|
+
* replaced because it is what any bound editor holds. Inside the caller's
|
|
242
|
+
* `captureSync`, so it is part of the same single ctrl+z as the role that made
|
|
243
|
+
* it necessary.
|
|
244
|
+
*/
|
|
245
|
+
function rewriteText(text, decide) {
|
|
246
|
+
if (!text || text.isLocked())
|
|
247
|
+
return;
|
|
248
|
+
const current = text.text.toString().trim();
|
|
249
|
+
const next = decide(current);
|
|
250
|
+
if (next === null || next === current)
|
|
251
|
+
return;
|
|
252
|
+
text.surface.store.transact(() => {
|
|
253
|
+
text.text.delete(0, text.text.length);
|
|
254
|
+
text.text.insert(0, next);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Keep the artefact's own words saying what it now is — the `afterMorph` half of
|
|
259
|
+
* the spec.
|
|
260
|
+
*
|
|
261
|
+
* Two texts and two DIFFERENT rules, because they are two different kinds of
|
|
262
|
+
* thing:
|
|
263
|
+
*
|
|
264
|
+
* - **The caption** is content, or a prompt standing in for content. Timid:
|
|
265
|
+
* placeholder→placeholder and nothing else ({@link coreDomainMorphedCaption}).
|
|
266
|
+
* A dot somebody called "Billing" is called that whatever it becomes.
|
|
267
|
+
* - **The letter** is NOTATION. C, X and F are not words anybody wrote, they are
|
|
268
|
+
* Team Topologies' own glyphs for the three interaction modes, and a square
|
|
269
|
+
* that has become an X-as-a-Service while still showing a C is not "a label
|
|
270
|
+
* the author owns" — it is the picture lying about itself. So it is rewritten
|
|
271
|
+
* ALWAYS, whatever it currently says, which is the only rule that also
|
|
272
|
+
* repairs a glyph a previous edit left wrong.
|
|
273
|
+
*/
|
|
274
|
+
function rewriteWords(model, from, to) {
|
|
275
|
+
if (!(model instanceof GroupElementModel))
|
|
276
|
+
return;
|
|
277
|
+
const artefact = coreDomainArtefactOf(model);
|
|
278
|
+
if (!artefact)
|
|
279
|
+
return;
|
|
280
|
+
const { glyph, caption } = wordsOf(model, artefact);
|
|
281
|
+
rewriteText(caption, text => coreDomainMorphedCaption(from, to, text));
|
|
282
|
+
if (isMarkerKind(to))
|
|
283
|
+
rewriteText(glyph, () => MARKER_LETTER[to]);
|
|
284
|
+
}
|
|
285
|
+
/* ── The menu's own wording ────────────────────────────────────────────── */
|
|
286
|
+
/**
|
|
287
|
+
* The creation command that places each kind, keyed BY that kind.
|
|
288
|
+
*
|
|
289
|
+
* Derived from `telemetry.element`, which is where the kind is already written
|
|
290
|
+
* down (`subdomain:platform`, `team-topology:xaas`) and is documented as a
|
|
291
|
+
* historical value that must not be renamed. Deriving rather than restating is
|
|
292
|
+
* what stops a second table of labels and icons drifting from the one the
|
|
293
|
+
* sub-menu and the catalogue read.
|
|
294
|
+
*/
|
|
295
|
+
const ARTEFACT_PREFIXES = ['subdomain:', 'team-topology:'];
|
|
296
|
+
const ARTEFACT_COMMANDS = new Map(coreDomainCommands.flatMap(command => {
|
|
297
|
+
const element = command.telemetry?.element ?? '';
|
|
298
|
+
const prefix = ARTEFACT_PREFIXES.find(candidate => element.startsWith(candidate));
|
|
299
|
+
return prefix ? [[element.slice(prefix.length), command]] : [];
|
|
300
|
+
}));
|
|
301
|
+
/** A kind's wording: the creation command's own key and English. */
|
|
302
|
+
function labelOf(kind) {
|
|
303
|
+
const command = ARTEFACT_COMMANDS.get(kind);
|
|
304
|
+
return {
|
|
305
|
+
key: command?.labelKey,
|
|
306
|
+
fallback: command?.labelFallback ?? KIND_LABEL[kind],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** A kind's icon: the creation command's own swatch, reused rather than redrawn. */
|
|
310
|
+
function iconOf(kind) {
|
|
311
|
+
const iconKey = ARTEFACT_COMMANDS.get(kind)?.iconKey;
|
|
312
|
+
return ((iconKey && coreDomainCommandIcons[iconKey]) ||
|
|
313
|
+
coreDomainCommandIcons['ddd-core-domain.chart']);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The Core Domain morph declaration, handed to the generic `morphToolbarConfig`.
|
|
317
|
+
*
|
|
318
|
+
* `modelType` is the native `GroupElementModel` and not `ShapeElementModel`,
|
|
319
|
+
* because both artefacts are composites and the group is what a click on one
|
|
320
|
+
* selects. Everything that makes that safe is in {@link coreDomainArtefactOf}:
|
|
321
|
+
* the toolbar's own homogeneity test only proves that every selected element is
|
|
322
|
+
* A group, and the resolution proves that every one of them is one of ours.
|
|
323
|
+
*
|
|
324
|
+
* Every shipped creation path builds the group — `commands.ts` always passes a
|
|
325
|
+
* caption, so `addDot`'s bare-ellipse branch is unreachable from the palette,
|
|
326
|
+
* the templates and the agent alike. A dot that somehow arrives ungrouped is
|
|
327
|
+
* simply not offered the menu, which is the same refusal a role-less dot gets
|
|
328
|
+
* and for the same reason: nothing is inferred, nothing is repaired behind the
|
|
329
|
+
* user's back.
|
|
330
|
+
*/
|
|
331
|
+
export const CORE_DOMAIN_MORPH_SPEC = {
|
|
332
|
+
framework: 'ddd-core-domain',
|
|
333
|
+
families: CORE_DOMAIN_MORPH_FAMILIES,
|
|
334
|
+
modelType: GroupElementModel,
|
|
335
|
+
resolveTarget: coreDomainArtefactOf,
|
|
336
|
+
kindOf: kindOfArtefact,
|
|
337
|
+
roleOf,
|
|
338
|
+
propsOf: coreDomainMorphProps,
|
|
339
|
+
clearOf: coreDomainMorphClears,
|
|
340
|
+
afterMorph: rewriteWords,
|
|
341
|
+
labelOf,
|
|
342
|
+
iconOf,
|
|
343
|
+
label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
|
|
344
|
+
};
|
package/dist/view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
|
|
@@ -8,6 +8,7 @@ import { CoreDomainRendererExtension } from './core-domain/element-renderer.js';
|
|
|
8
8
|
import { CoreDomainInteraction, CoreDomainView, } from './core-domain/element-view.js';
|
|
9
9
|
import { coreDomainToolbarExtension } from './core-domain/toolbar-config.js';
|
|
10
10
|
import { coreDomainEffects } from './effects.js';
|
|
11
|
+
import { CORE_DOMAIN_MORPH_SPEC } from './morph.js';
|
|
11
12
|
import { CORE_DOMAIN_NUDGES } from './nudges.js';
|
|
12
13
|
import { CORE_DOMAIN_PROFILES } from './profiles.js';
|
|
13
14
|
import { CORE_DOMAIN_ROLES } from './roles.js';
|
|
@@ -79,6 +80,33 @@ export class DddCoreDomainViewExtension extends ViewExtensionProvider {
|
|
|
79
80
|
}));
|
|
80
81
|
context.register(coreDomainSeniorTool);
|
|
81
82
|
context.register(CommandExtension(coreDomainCommands, coreDomainCommandIcons));
|
|
83
|
+
// The "Change type" dropdown on a selected dot's or marker's contextual
|
|
84
|
+
// toolbar — the generic module, parameterized by this framework's two
|
|
85
|
+
// families.
|
|
86
|
+
//
|
|
87
|
+
// ## Why the key carries an owner
|
|
88
|
+
//
|
|
89
|
+
// Both artefacts are native `group`s, so the row the toolbar draws for one
|
|
90
|
+
// is the GROUP's row, merged from `affine:surface:group`,
|
|
91
|
+
// `custom:affine:surface:group` and the surface wildcards. Both group keys
|
|
92
|
+
// are long since claimed — the native group operations on one, Wardley's
|
|
93
|
+
// qualification dropdown on the other, with C4's morph already registered
|
|
94
|
+
// under a suffixed variant — and a second module on either would throw
|
|
95
|
+
// `DuplicateServiceDefinitionError` before the editor finished setting up.
|
|
96
|
+
// `toolbarModuleKey` is what lifts that ceiling: the module is registered
|
|
97
|
+
// under `custom:affine:surface:group#ddd-core-domain-morph` and the
|
|
98
|
+
// registry hands it to the same row.
|
|
99
|
+
//
|
|
100
|
+
// ## Why here
|
|
101
|
+
//
|
|
102
|
+
// In the flag-gated half, because a morph is TOOLING: a dot placed while
|
|
103
|
+
// the flag was on keeps its role, its colour, its caption and its place in
|
|
104
|
+
// every rule when the flag goes off — it just stops being something the
|
|
105
|
+
// toolbar offers to say differently (`docs/adr/0009`).
|
|
106
|
+
context.register(ToolbarModuleExtension({
|
|
107
|
+
id: BlockFlavourIdentifier(toolbarModuleKey('custom:affine:surface:group', 'ddd-core-domain-morph')),
|
|
108
|
+
config: morphToolbarConfig(CORE_DOMAIN_MORPH_SPEC),
|
|
109
|
+
}));
|
|
82
110
|
}
|
|
83
111
|
}
|
|
84
112
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formicoidea/labre-framework-ddd-core-domain",
|
|
3
3
|
"description": "Labre ddd-core-domain framework for @formicoidea/labre-core.",
|
|
4
|
-
"version": "0.
|
|
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.
|
|
35
|
-
"@formicoidea/labre-ddd-shared": "0.
|
|
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
|
}
|