@formicoidea/labre-framework-edgy 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +39 -0
- package/dist/actions.js +193 -0
- package/dist/board-renderer.d.ts +12 -0
- package/dist/board-renderer.js +28 -0
- package/dist/board-view.d.ts +11 -0
- package/dist/board-view.js +20 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +103 -0
- package/dist/consts.d.ts +35 -0
- package/dist/consts.js +46 -0
- package/dist/descriptor.d.ts +8 -3
- package/dist/descriptor.js +6 -3
- package/dist/element-renderer.js +24 -13
- package/dist/element-view.js +9 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/label-layout.js +8 -2
- package/dist/legend.d.ts +2 -0
- package/dist/legend.js +109 -0
- package/dist/metamodel.d.ts +96 -0
- package/dist/metamodel.js +128 -0
- package/dist/nudges.d.ts +26 -0
- package/dist/nudges.js +54 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +66 -0
- package/dist/relation-resolver.d.ts +32 -0
- package/dist/relation-resolver.js +101 -0
- package/dist/relation.d.ts +142 -0
- package/dist/relation.js +188 -0
- package/dist/roles.d.ts +108 -0
- package/dist/roles.js +182 -0
- package/dist/rules.d.ts +43 -0
- package/dist/rules.js +153 -0
- package/dist/templates/index.d.ts +22 -1
- package/dist/templates/index.js +303 -52
- package/dist/toolbar/config.d.ts +33 -0
- package/dist/toolbar/config.js +82 -15
- package/dist/toolbar/edgy-menu.d.ts +7 -25
- package/dist/toolbar/edgy-menu.js +7 -186
- package/dist/toolbar/edgy-senior-button.js +8 -2
- package/dist/toolbar/icons.d.ts +12 -0
- package/dist/toolbar/icons.js +31 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/translations.d.ts +24 -0
- package/dist/translations.js +29 -0
- package/dist/view.d.ts +18 -0
- package/dist/view.js +95 -8
- package/package.json +6 -3
package/dist/legend.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { roleLabel, } from '@formicoidea/labre-ddd-shared';
|
|
2
|
+
import { EDGY_DYNAMIC_NODES, EDGY_ZONE_FILL, EDGY_ZONES, edgyElementLabel, } from './metamodel.js';
|
|
3
|
+
import { NODE_FILL, NODE_STROKE } from './node/consts.js';
|
|
4
|
+
import { EDGY_ROLE, EDGY_ROLES } from './roles.js';
|
|
5
|
+
/**
|
|
6
|
+
* What an EDGY background's automatic legend can say — a TABLE, and nothing
|
|
7
|
+
* else: the scan, the placement and the box are `createAutoLegend`'s job, the
|
|
8
|
+
* same generic the three DDD backgrounds were given
|
|
9
|
+
* (`@labre/affine-gfx-ddd-shared`, `shared/legend-auto.ts`). EDGY takes a
|
|
10
|
+
* dependency on that package for the generic and the box prefab alone; moving
|
|
11
|
+
* them to a neutral home would be the tidier answer and is not worth the churn
|
|
12
|
+
* of this fix.
|
|
13
|
+
*
|
|
14
|
+
* Every row is DERIVED — from the metamodel (`./metamodel.ts`: which elements
|
|
15
|
+
* exist, which zone each belongs to, what colour that zone is drawn in) and
|
|
16
|
+
* from the role vocabulary's own labels (`./roles.ts`). Nothing here is
|
|
17
|
+
* restated: a thirteenth element added to the metamodel gets its legend row the
|
|
18
|
+
* same way it gets its role, with no edit to this file.
|
|
19
|
+
*
|
|
20
|
+
* ## The order
|
|
21
|
+
*
|
|
22
|
+
* By ZONE, and the order of the table carries it: the three facets first
|
|
23
|
+
* (Identity, Architecture, Experience), then the three intersections in one
|
|
24
|
+
* section, then the four base elements, then the relations — which is the
|
|
25
|
+
* reading order of {@link EDGY_ZONES} and, before it, of the Venn itself.
|
|
26
|
+
*/
|
|
27
|
+
/** One row per official element of `zone`, in the metamodel's own order. */
|
|
28
|
+
function zoneEntries(zone) {
|
|
29
|
+
return Object.entries(EDGY_DYNAMIC_NODES)
|
|
30
|
+
.filter(([, node]) => node.zone === zone)
|
|
31
|
+
.map(([name]) => ({
|
|
32
|
+
role: EDGY_ROLE[name],
|
|
33
|
+
row: {
|
|
34
|
+
swatch: 'square',
|
|
35
|
+
// The zone's fill, which IS what the diagram paints the element with.
|
|
36
|
+
color: EDGY_ZONE_FILL[zone],
|
|
37
|
+
label: roleLabel(EDGY_ROLES, EDGY_ROLE[name]),
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const FACET_SECTIONS = EDGY_ZONES.filter(zone => zone.group === 'facet').map(zone => ({
|
|
42
|
+
title: edgyElementLabel(zone.id),
|
|
43
|
+
entries: zoneEntries(zone.id),
|
|
44
|
+
}));
|
|
45
|
+
/**
|
|
46
|
+
* The three intersection elements in ONE section: Organisation, Product and
|
|
47
|
+
* Brand are not three facets, they are the three lenses between them, and the
|
|
48
|
+
* diagram says so by drawing them in the overlaps. Each keeps its own colour.
|
|
49
|
+
*/
|
|
50
|
+
const INTERSECTIONS_SECTION = {
|
|
51
|
+
title: 'Intersections',
|
|
52
|
+
entries: EDGY_ZONES.filter(zone => zone.group === 'intersection').flatMap(zone => zoneEntries(zone.id)),
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The four base elements, listed only when one is on the board BARE — an
|
|
56
|
+
* element the user dropped from the toolbox and left as a People, an Outcome,
|
|
57
|
+
* an Object or an Activity, without saying which of the twelve it is. White,
|
|
58
|
+
* because that is the fill the palette gives them (`node/consts.ts`).
|
|
59
|
+
*
|
|
60
|
+
* `exact` is what makes "bare" true. Detection is otherwise an ancestor walk,
|
|
61
|
+
* and the twelve official elements specialise these four — Content is an
|
|
62
|
+
* object, Story an activity — so an "Object" row would appear on a board
|
|
63
|
+
* carrying nothing but Contents, keyed to a white swatch that is drawn nowhere
|
|
64
|
+
* on it. The relation entry below keeps the walk, because there the parent row
|
|
65
|
+
* IS the fair summary: a board carrying `edgy:expresses` carries a relation.
|
|
66
|
+
*/
|
|
67
|
+
const BASE_SECTION = {
|
|
68
|
+
title: 'Base elements',
|
|
69
|
+
entries: ['people', 'outcome', 'object', 'activity'].map(kind => ({
|
|
70
|
+
role: EDGY_ROLE[kind],
|
|
71
|
+
exact: true,
|
|
72
|
+
row: {
|
|
73
|
+
swatch: 'square',
|
|
74
|
+
color: NODE_FILL,
|
|
75
|
+
label: roleLabel(EDGY_ROLES, EDGY_ROLE[kind]),
|
|
76
|
+
},
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* ONE row for the whole relation family, keyed on the PARENT role: a board
|
|
81
|
+
* carrying "content expresses purpose" carries `edgy:expresses`, which is a
|
|
82
|
+
* relation, and that is all the legend has to say. Twenty-two rows naming the
|
|
83
|
+
* verbs would restate the metamodel rather than document the drawing — and the
|
|
84
|
+
* verb travels on the link itself, visible, where it belongs.
|
|
85
|
+
*/
|
|
86
|
+
const RELATIONS_SECTION = {
|
|
87
|
+
title: 'Relations',
|
|
88
|
+
entries: [
|
|
89
|
+
{
|
|
90
|
+
role: EDGY_ROLE.relation,
|
|
91
|
+
row: {
|
|
92
|
+
swatch: 'line',
|
|
93
|
+
// The stroke `activateEdgyRelation` arms the connector tool with.
|
|
94
|
+
color: NODE_STROKE,
|
|
95
|
+
label: roleLabel(EDGY_ROLES, EDGY_ROLE.relation),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
export const EDGY_AUTO_LEGEND = {
|
|
101
|
+
title: 'Legend',
|
|
102
|
+
roles: EDGY_ROLES,
|
|
103
|
+
sections: [
|
|
104
|
+
...FACET_SECTIONS,
|
|
105
|
+
INTERSECTIONS_SECTION,
|
|
106
|
+
BASE_SECTION,
|
|
107
|
+
RELATIONS_SECTION,
|
|
108
|
+
],
|
|
109
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The EDGY metamodel, as DATA — the 12 official elements and the 24 canonical
|
|
3
|
+
* relations that link them.
|
|
4
|
+
*
|
|
5
|
+
* It used to live in `templates/index.ts`, where it was drawn. It moved here
|
|
6
|
+
* the day the roles started being DERIVED from it (`./roles.ts`): the template
|
|
7
|
+
* needs the vocabulary to stamp its elements, and the vocabulary needs the
|
|
8
|
+
* metamodel to know which verbs exist, so a single module holding both would be
|
|
9
|
+
* a cycle. Nothing was renamed — `templates/index.ts` re-exports both tables
|
|
10
|
+
* under the names they always had.
|
|
11
|
+
*
|
|
12
|
+
* This file is the ONE place the metamodel is written down. `roles.ts` derives
|
|
13
|
+
* an edge role per verb from it, `rules.ts` derives the sanctioned sentences of
|
|
14
|
+
* `edgy.non-canonical-link` from it, and the template draws it. Restating the
|
|
15
|
+
* matrix anywhere else would be inviting the three to disagree.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Official pastel fills per zone (the `pictograms/Shape-*.svg` colors) — the
|
|
19
|
+
* colour an element of that zone is DRAWN with, on the diagram and therefore in
|
|
20
|
+
* the legend that documents it.
|
|
21
|
+
*
|
|
22
|
+
* Exported because the legend needs the same swatch the board uses: a swatch
|
|
23
|
+
* restated as a second literal would be a colour free to drift away from the
|
|
24
|
+
* one the eye sees.
|
|
25
|
+
*/
|
|
26
|
+
export declare const EDGY_ZONE_FILL: {
|
|
27
|
+
readonly identity: "#80ffb7";
|
|
28
|
+
readonly architecture: "#a6c0ff";
|
|
29
|
+
readonly experience: "#ff99bd";
|
|
30
|
+
readonly organisation: "#80eaff";
|
|
31
|
+
readonly brand: "#ffd580";
|
|
32
|
+
readonly product: "#e599ff";
|
|
33
|
+
};
|
|
34
|
+
/** One of the six regions of the Venn: three facets, three intersections. */
|
|
35
|
+
export type EdgyZone = keyof typeof EDGY_ZONE_FILL;
|
|
36
|
+
/**
|
|
37
|
+
* The six zones, in reading order — the three FACETS first, then the three
|
|
38
|
+
* INTERSECTIONS, each named by the pair of circles it is the lens of (the very
|
|
39
|
+
* pairs `element-renderer.ts` clips: A∩B, B∩C, A∩C).
|
|
40
|
+
*
|
|
41
|
+
* Written down as data because the split is otherwise readable only inside the
|
|
42
|
+
* renderer's paint calls, and the legend has to group the twelve elements by
|
|
43
|
+
* it. `group` is the only thing here a reader could not already derive from
|
|
44
|
+
* {@link EDGY_DYNAMIC_NODES}.
|
|
45
|
+
*/
|
|
46
|
+
export declare const EDGY_ZONES: readonly {
|
|
47
|
+
id: EdgyZone;
|
|
48
|
+
group: 'facet' | 'intersection';
|
|
49
|
+
}[];
|
|
50
|
+
/**
|
|
51
|
+
* The 24 canonical EDGY relations (source, target, verb, label position along
|
|
52
|
+
* the link) — exported for the unit tests. 7 per facet + 3 between the
|
|
53
|
+
* intersections. The optional 4th member mirrors the reference diagram's
|
|
54
|
+
* placements: verbs of intersection-outgoing links sit near the far element
|
|
55
|
+
* (`labelOffset.distance` ≈ .75), short peer links keep the middle.
|
|
56
|
+
*
|
|
57
|
+
* Read the way `docs/adr/0010` reads every typed edge: the SOURCE is the
|
|
58
|
+
* subject of the verb and the TARGET its object, so each row is one sentence a
|
|
59
|
+
* practitioner would say out loud — "content expresses purpose". That is what
|
|
60
|
+
* makes the row usable as an {@link EndpointTriplet} without a single
|
|
61
|
+
* coordinate taking part.
|
|
62
|
+
*/
|
|
63
|
+
export declare const EDGY_DYNAMIC_RELATIONS: [string, string, string, number?][];
|
|
64
|
+
/**
|
|
65
|
+
* The 12 elements, centred coordinates in REFERENCE coords (the fixed space
|
|
66
|
+
* of consts.ts — the same space as `VENN`), laid out like the reference
|
|
67
|
+
* "elements & relations" diagram: aligned top row, Story/Capability flanks,
|
|
68
|
+
* Brand/Product astride the white centre, Task/Journey/Channel triangle.
|
|
69
|
+
* Exported (with `dynToModel`) for the containment test.
|
|
70
|
+
*
|
|
71
|
+
* `kind` is the OFFICIAL kind of each element — the base shape EDGY draws it
|
|
72
|
+
* with — and `roles.ts` reads it to give each of the twelve leaf roles its
|
|
73
|
+
* parent. Written once, here, rather than restated as a second table nobody
|
|
74
|
+
* would think to keep in step.
|
|
75
|
+
*
|
|
76
|
+
* `zone` is the region of the Venn the element belongs to, and it REPLACES the
|
|
77
|
+
* fill this table used to carry: the fill is `EDGY_ZONE_FILL[zone]` and nothing
|
|
78
|
+
* else, so an element cannot be filed under one facet and painted another's
|
|
79
|
+
* colour. The template reads it to draw, the legend reads it to group and to
|
|
80
|
+
* swatch.
|
|
81
|
+
*/
|
|
82
|
+
export type EdgyElementName = 'content' | 'purpose' | 'organisation' | 'process' | 'asset' | 'story' | 'capability' | 'brand' | 'product' | 'task' | 'journey' | 'channel';
|
|
83
|
+
/**
|
|
84
|
+
* The metamodel's own wording for one of its lowercase names — one of the
|
|
85
|
+
* twelve elements, or one of the six zones. It is the name written on the
|
|
86
|
+
* diagram, so it is also what the vocabulary falls back to and what the legend
|
|
87
|
+
* prints as a row label or a section title. One expression, one wording.
|
|
88
|
+
*/
|
|
89
|
+
export declare const edgyElementLabel: (name: EdgyElementName | EdgyZone) => string;
|
|
90
|
+
export declare const EDGY_DYNAMIC_NODES: Record<EdgyElementName, {
|
|
91
|
+
kind: 'outcome' | 'object' | 'activity';
|
|
92
|
+
cx: number;
|
|
93
|
+
cy: number;
|
|
94
|
+
w?: number;
|
|
95
|
+
zone: EdgyZone;
|
|
96
|
+
}>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The EDGY metamodel, as DATA — the 12 official elements and the 24 canonical
|
|
3
|
+
* relations that link them.
|
|
4
|
+
*
|
|
5
|
+
* It used to live in `templates/index.ts`, where it was drawn. It moved here
|
|
6
|
+
* the day the roles started being DERIVED from it (`./roles.ts`): the template
|
|
7
|
+
* needs the vocabulary to stamp its elements, and the vocabulary needs the
|
|
8
|
+
* metamodel to know which verbs exist, so a single module holding both would be
|
|
9
|
+
* a cycle. Nothing was renamed — `templates/index.ts` re-exports both tables
|
|
10
|
+
* under the names they always had.
|
|
11
|
+
*
|
|
12
|
+
* This file is the ONE place the metamodel is written down. `roles.ts` derives
|
|
13
|
+
* an edge role per verb from it, `rules.ts` derives the sanctioned sentences of
|
|
14
|
+
* `edgy.non-canonical-link` from it, and the template draws it. Restating the
|
|
15
|
+
* matrix anywhere else would be inviting the three to disagree.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Official pastel fills per zone (the `pictograms/Shape-*.svg` colors) — the
|
|
19
|
+
* colour an element of that zone is DRAWN with, on the diagram and therefore in
|
|
20
|
+
* the legend that documents it.
|
|
21
|
+
*
|
|
22
|
+
* Exported because the legend needs the same swatch the board uses: a swatch
|
|
23
|
+
* restated as a second literal would be a colour free to drift away from the
|
|
24
|
+
* one the eye sees.
|
|
25
|
+
*/
|
|
26
|
+
export const EDGY_ZONE_FILL = {
|
|
27
|
+
identity: '#80ffb7',
|
|
28
|
+
architecture: '#a6c0ff',
|
|
29
|
+
experience: '#ff99bd',
|
|
30
|
+
organisation: '#80eaff',
|
|
31
|
+
brand: '#ffd580',
|
|
32
|
+
product: '#e599ff',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The six zones, in reading order — the three FACETS first, then the three
|
|
36
|
+
* INTERSECTIONS, each named by the pair of circles it is the lens of (the very
|
|
37
|
+
* pairs `element-renderer.ts` clips: A∩B, B∩C, A∩C).
|
|
38
|
+
*
|
|
39
|
+
* Written down as data because the split is otherwise readable only inside the
|
|
40
|
+
* renderer's paint calls, and the legend has to group the twelve elements by
|
|
41
|
+
* it. `group` is the only thing here a reader could not already derive from
|
|
42
|
+
* {@link EDGY_DYNAMIC_NODES}.
|
|
43
|
+
*/
|
|
44
|
+
export const EDGY_ZONES = [
|
|
45
|
+
{ id: 'identity', group: 'facet' },
|
|
46
|
+
{ id: 'architecture', group: 'facet' },
|
|
47
|
+
{ id: 'experience', group: 'facet' },
|
|
48
|
+
// Identity ∩ Architecture, Architecture ∩ Experience, Experience ∩ Identity.
|
|
49
|
+
{ id: 'organisation', group: 'intersection' },
|
|
50
|
+
{ id: 'product', group: 'intersection' },
|
|
51
|
+
{ id: 'brand', group: 'intersection' },
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* The 24 canonical EDGY relations (source, target, verb, label position along
|
|
55
|
+
* the link) — exported for the unit tests. 7 per facet + 3 between the
|
|
56
|
+
* intersections. The optional 4th member mirrors the reference diagram's
|
|
57
|
+
* placements: verbs of intersection-outgoing links sit near the far element
|
|
58
|
+
* (`labelOffset.distance` ≈ .75), short peer links keep the middle.
|
|
59
|
+
*
|
|
60
|
+
* Read the way `docs/adr/0010` reads every typed edge: the SOURCE is the
|
|
61
|
+
* subject of the verb and the TARGET its object, so each row is one sentence a
|
|
62
|
+
* practitioner would say out loud — "content expresses purpose". That is what
|
|
63
|
+
* makes the row usable as an {@link EndpointTriplet} without a single
|
|
64
|
+
* coordinate taking part.
|
|
65
|
+
*/
|
|
66
|
+
export const EDGY_DYNAMIC_RELATIONS = [
|
|
67
|
+
// Identity
|
|
68
|
+
['content', 'purpose', 'expresses'],
|
|
69
|
+
['content', 'story', 'conveys', 0.75],
|
|
70
|
+
['story', 'purpose', 'contextualises', 0.8],
|
|
71
|
+
['organisation', 'purpose', 'pursues', 0.8],
|
|
72
|
+
['organisation', 'story', 'authors', 0.6],
|
|
73
|
+
['brand', 'purpose', 'represents', 0.8],
|
|
74
|
+
['brand', 'story', 'evokes', 0.65],
|
|
75
|
+
// Architecture
|
|
76
|
+
['organisation', 'process', 'performs', 0.8],
|
|
77
|
+
['process', 'capability', 'realises', 0.75],
|
|
78
|
+
['process', 'asset', 'requires'],
|
|
79
|
+
['capability', 'asset', 'requires', 0.75],
|
|
80
|
+
['organisation', 'capability', 'has', 0.6],
|
|
81
|
+
['product', 'capability', 'requires', 0.75],
|
|
82
|
+
['process', 'product', 'creates', 0.65],
|
|
83
|
+
// Experience
|
|
84
|
+
['task', 'journey', 'is part of', 0.6],
|
|
85
|
+
['task', 'channel', 'uses', 0.6],
|
|
86
|
+
['journey', 'channel', 'traverses', 0.6],
|
|
87
|
+
['product', 'task', 'serves', 0.9],
|
|
88
|
+
['product', 'journey', 'features in', 0.8],
|
|
89
|
+
['brand', 'task', 'supports', 0.8],
|
|
90
|
+
['brand', 'journey', 'appears in', 0.9],
|
|
91
|
+
// Intersections
|
|
92
|
+
['organisation', 'brand', 'builds', 0.85],
|
|
93
|
+
['organisation', 'product', 'makes', 0.65],
|
|
94
|
+
['product', 'brand', 'embodies', 0.8],
|
|
95
|
+
];
|
|
96
|
+
/**
|
|
97
|
+
* The metamodel's own wording for one of its lowercase names — one of the
|
|
98
|
+
* twelve elements, or one of the six zones. It is the name written on the
|
|
99
|
+
* diagram, so it is also what the vocabulary falls back to and what the legend
|
|
100
|
+
* prints as a row label or a section title. One expression, one wording.
|
|
101
|
+
*/
|
|
102
|
+
export const edgyElementLabel = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
103
|
+
export const EDGY_DYNAMIC_NODES = {
|
|
104
|
+
content: { kind: 'object', cx: 237.5, cy: 100, zone: 'identity' },
|
|
105
|
+
purpose: { kind: 'outcome', cx: 282.5, cy: 100, zone: 'identity' },
|
|
106
|
+
organisation: {
|
|
107
|
+
kind: 'object',
|
|
108
|
+
cx: 340,
|
|
109
|
+
cy: 100,
|
|
110
|
+
w: 175,
|
|
111
|
+
zone: 'organisation',
|
|
112
|
+
},
|
|
113
|
+
process: { kind: 'activity', cx: 397.5, cy: 100, zone: 'architecture' },
|
|
114
|
+
asset: { kind: 'object', cx: 442.5, cy: 100, zone: 'architecture' },
|
|
115
|
+
story: { kind: 'activity', cx: 255, cy: 152.5, zone: 'identity' },
|
|
116
|
+
capability: {
|
|
117
|
+
kind: 'outcome',
|
|
118
|
+
cx: 425,
|
|
119
|
+
cy: 152.5,
|
|
120
|
+
w: 150,
|
|
121
|
+
zone: 'architecture',
|
|
122
|
+
},
|
|
123
|
+
brand: { kind: 'object', cx: 280, cy: 195, zone: 'brand' },
|
|
124
|
+
product: { kind: 'object', cx: 400, cy: 195, zone: 'product' },
|
|
125
|
+
task: { kind: 'outcome', cx: 310, cy: 257.5, zone: 'experience' },
|
|
126
|
+
journey: { kind: 'activity', cx: 370, cy: 257.5, zone: 'experience' },
|
|
127
|
+
channel: { kind: 'object', cx: 340, cy: 297.5, zone: 'experience' },
|
|
128
|
+
};
|
package/dist/nudges.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { QualityNudge } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
/**
|
|
3
|
+
* EDGY **work quality** — the checklist (WS1).
|
|
4
|
+
*
|
|
5
|
+
* Level 1 is `./rules.ts`: deterministic, decidable, real time. This file is
|
|
6
|
+
* level 2, and the split is the whole point of the taxonomy — a rule only
|
|
7
|
+
* enters the deterministic engine if an algorithm can decide it on persisted
|
|
8
|
+
* data inside the ~16 ms budget. Everything else is a NUDGE: an expectation
|
|
9
|
+
* offered as a checklist, never evaluated, never blocking anything. Ticking is
|
|
10
|
+
* assuming.
|
|
11
|
+
*
|
|
12
|
+
* ## Why q1 and q2 are here and not in `rules.ts` (PO arbitration, 26/08/2026)
|
|
13
|
+
*
|
|
14
|
+
* They read like rules, and they are not. "Each intersection element is linked
|
|
15
|
+
* to both of its parent facets" would need the tool to know which circle an
|
|
16
|
+
* element belongs to — and nothing says so except where somebody dropped it,
|
|
17
|
+
* which is the layout answering a question about meaning. "Each element wears
|
|
18
|
+
* its facet's colour" would indict every board whose author uses their own
|
|
19
|
+
* palette, which is most of them after the first workshop. Both are judgements
|
|
20
|
+
* a modeller makes; the tool can put them on screen and cannot answer them.
|
|
21
|
+
*
|
|
22
|
+
* Registered from the flag-gated `EdgyViewExtension`, beside the rules and the
|
|
23
|
+
* profiles: a checklist is tooling. Switching the flag off takes it away and
|
|
24
|
+
* leaves the ticks written on the board, unread, until it comes back (PF7.10).
|
|
25
|
+
*/
|
|
26
|
+
export declare const EDGY_NUDGES: readonly QualityNudge[];
|
package/dist/nudges.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDGY **work quality** — the checklist (WS1).
|
|
3
|
+
*
|
|
4
|
+
* Level 1 is `./rules.ts`: deterministic, decidable, real time. This file is
|
|
5
|
+
* level 2, and the split is the whole point of the taxonomy — a rule only
|
|
6
|
+
* enters the deterministic engine if an algorithm can decide it on persisted
|
|
7
|
+
* data inside the ~16 ms budget. Everything else is a NUDGE: an expectation
|
|
8
|
+
* offered as a checklist, never evaluated, never blocking anything. Ticking is
|
|
9
|
+
* assuming.
|
|
10
|
+
*
|
|
11
|
+
* ## Why q1 and q2 are here and not in `rules.ts` (PO arbitration, 26/08/2026)
|
|
12
|
+
*
|
|
13
|
+
* They read like rules, and they are not. "Each intersection element is linked
|
|
14
|
+
* to both of its parent facets" would need the tool to know which circle an
|
|
15
|
+
* element belongs to — and nothing says so except where somebody dropped it,
|
|
16
|
+
* which is the layout answering a question about meaning. "Each element wears
|
|
17
|
+
* its facet's colour" would indict every board whose author uses their own
|
|
18
|
+
* palette, which is most of them after the first workshop. Both are judgements
|
|
19
|
+
* a modeller makes; the tool can put them on screen and cannot answer them.
|
|
20
|
+
*
|
|
21
|
+
* Registered from the flag-gated `EdgyViewExtension`, beside the rules and the
|
|
22
|
+
* profiles: a checklist is tooling. Switching the flag off takes it away and
|
|
23
|
+
* leaves the ticks written on the board, unread, until it comes back (PF7.10).
|
|
24
|
+
*/
|
|
25
|
+
export const EDGY_NUDGES = [
|
|
26
|
+
{
|
|
27
|
+
id: 'edgy.q1-intersection-links',
|
|
28
|
+
framework: 'edgy',
|
|
29
|
+
labelKey: 'com.labre.edgy.quality.intersection-links',
|
|
30
|
+
fallback: 'Each intersection element is linked to both of its parent facets.',
|
|
31
|
+
order: 1,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'edgy.q2-facet-colour',
|
|
35
|
+
framework: 'edgy',
|
|
36
|
+
labelKey: 'com.labre.edgy.quality.facet-colour',
|
|
37
|
+
fallback: "Each element wears its facet's colour.",
|
|
38
|
+
order: 2,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'edgy.q3-readable-relations',
|
|
42
|
+
framework: 'edgy',
|
|
43
|
+
labelKey: 'com.labre.edgy.quality.readable-relations',
|
|
44
|
+
fallback: 'Relations read correctly (the source is the subject of the verb).',
|
|
45
|
+
order: 3,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'edgy.q4-three-facets',
|
|
49
|
+
framework: 'edgy',
|
|
50
|
+
labelKey: 'com.labre.edgy.quality.three-facets',
|
|
51
|
+
fallback: 'All three facets have been explored.',
|
|
52
|
+
order: 4,
|
|
53
|
+
},
|
|
54
|
+
];
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDGY validation profiles (WS1).
|
|
3
|
+
*
|
|
4
|
+
* DATA owned by the framework, like its rules and its roles: the engine knows
|
|
5
|
+
* how to APPLY a profile, never which ones EDGY has. Adding a level of
|
|
6
|
+
* requirement is adding an entry to this array.
|
|
7
|
+
*
|
|
8
|
+
* A profile is chosen per BACKGROUND, not per document (PF9.1) — a workshop
|
|
9
|
+
* scribble and a deliverable model coexist on the same canvas with different
|
|
10
|
+
* requirements, and the choice rides on the facets diagram or board itself.
|
|
11
|
+
*
|
|
12
|
+
* Registered from the flag-gated `EdgyViewExtension`, beside the rules:
|
|
13
|
+
* switching the EDGY flag off takes the choice away with the rest of the
|
|
14
|
+
* tooling, and a board already set to `strict` simply stops being checked until
|
|
15
|
+
* it comes back — the id stays written, untouched.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Sketch: every rule demoted to `audit`, so findings still reach `violations$`
|
|
19
|
+
* — a host panel and a conformance report see them — and the canvas says
|
|
20
|
+
* nothing at all.
|
|
21
|
+
*
|
|
22
|
+
* The DEFAULT, and deliberately so (PRD principle 3). EDGY is a WORKSHOP
|
|
23
|
+
* notation: elements land on the diagram in the order people say them, half the
|
|
24
|
+
* relations point the wrong way for the ten minutes it takes to argue about
|
|
25
|
+
* them, and boxes pile up in a corner before anybody sorts them into facets. A
|
|
26
|
+
* tool warning about all of that in real time would be arguing with the
|
|
27
|
+
* workshop.
|
|
28
|
+
*
|
|
29
|
+
* Being the default also means it is the one profile that WRITES NOTHING: a
|
|
30
|
+
* board on `sketch` carries no profile key, so every EDGY diagram ever drawn is
|
|
31
|
+
* on it, with no migration and no backfill.
|
|
32
|
+
*/
|
|
33
|
+
const sketch = {
|
|
34
|
+
id: 'edgy.sketch',
|
|
35
|
+
framework: 'edgy',
|
|
36
|
+
labelKey: 'com.labre.edgy.profile.sketch',
|
|
37
|
+
fallback: 'Sketch',
|
|
38
|
+
isDefault: true,
|
|
39
|
+
rules: {
|
|
40
|
+
'edgy.non-canonical-link': 'audit',
|
|
41
|
+
'edgy.overlapping-artefacts': 'audit',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Strict: the deliverable level. Both rules bite, and both severities are
|
|
46
|
+
* spelled out here rather than left absent — PF9.4 wants the answer readable in
|
|
47
|
+
* one place, and a profile that says "warning" out loud cannot be misread as
|
|
48
|
+
* "whatever the rule happens to declare this quarter".
|
|
49
|
+
*
|
|
50
|
+
* `warning` and not `blocking-overridable` for E1, though a relation outside the
|
|
51
|
+
* metamodel is exactly the statement a strict EDGY model must not contain:
|
|
52
|
+
* nothing in this library implements a blocking level, so the value would claim
|
|
53
|
+
* an effect that does not exist. When the gesture refusal lands, this is a
|
|
54
|
+
* two-word change and this file is the only one that moves.
|
|
55
|
+
*/
|
|
56
|
+
const strict = {
|
|
57
|
+
id: 'edgy.strict',
|
|
58
|
+
framework: 'edgy',
|
|
59
|
+
labelKey: 'com.labre.edgy.profile.strict',
|
|
60
|
+
fallback: 'Strict',
|
|
61
|
+
rules: {
|
|
62
|
+
'edgy.non-canonical-link': 'warning',
|
|
63
|
+
'edgy.overlapping-artefacts': 'warning',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export const EDGY_PROFILES = [sketch, strict];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
+
/**
|
|
3
|
+
* Turns "a connector was drawn, or one of its ends just moved" into a naming.
|
|
4
|
+
*
|
|
5
|
+
* Registered from the FLAG-GATED `EdgyViewExtension`, not from the always-on
|
|
6
|
+
* render one, and the distinction is `docs/adr/0009`'s: this is a creation
|
|
7
|
+
* tool. It authors content — it writes a role and a label into the document —
|
|
8
|
+
* and it only ever fires on an edge the flag-gated toolbox itself stamped
|
|
9
|
+
* `edgy:relation`. A board drawn while the flag was on keeps every verb it was
|
|
10
|
+
* given, painted by the always-on renderer and read by the always-on
|
|
11
|
+
* vocabulary; with the flag off nothing new is named because nothing new is
|
|
12
|
+
* being armed. Registering it always-on would mean the library kept authoring
|
|
13
|
+
* EDGY content for a host that switched EDGY off.
|
|
14
|
+
*
|
|
15
|
+
* The subscription mechanics are `EstuarineGhostManager`'s, including the part
|
|
16
|
+
* that matters: the surface is a SIGNAL, null at mount and replaced when the
|
|
17
|
+
* surface block is.
|
|
18
|
+
*/
|
|
19
|
+
export declare class EdgyRelationResolver extends InteractivityExtension {
|
|
20
|
+
static key: string;
|
|
21
|
+
private _subscriptions;
|
|
22
|
+
private _disposeSurfaceEffect;
|
|
23
|
+
private _pending;
|
|
24
|
+
private _candidates;
|
|
25
|
+
mounted(): void;
|
|
26
|
+
unmounted(): void;
|
|
27
|
+
private _forget;
|
|
28
|
+
private _unsubscribe;
|
|
29
|
+
private _resubscribe;
|
|
30
|
+
private _schedule;
|
|
31
|
+
private _flush;
|
|
32
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
+
import { effect } from '@preact/signals-core';
|
|
3
|
+
import { EDGY_RELATION_DELAY_MS, resolveEdgyRelations } from './relation.js';
|
|
4
|
+
/**
|
|
5
|
+
* Turns "a connector was drawn, or one of its ends just moved" into a naming.
|
|
6
|
+
*
|
|
7
|
+
* Registered from the FLAG-GATED `EdgyViewExtension`, not from the always-on
|
|
8
|
+
* render one, and the distinction is `docs/adr/0009`'s: this is a creation
|
|
9
|
+
* tool. It authors content — it writes a role and a label into the document —
|
|
10
|
+
* and it only ever fires on an edge the flag-gated toolbox itself stamped
|
|
11
|
+
* `edgy:relation`. A board drawn while the flag was on keeps every verb it was
|
|
12
|
+
* given, painted by the always-on renderer and read by the always-on
|
|
13
|
+
* vocabulary; with the flag off nothing new is named because nothing new is
|
|
14
|
+
* being armed. Registering it always-on would mean the library kept authoring
|
|
15
|
+
* EDGY content for a host that switched EDGY off.
|
|
16
|
+
*
|
|
17
|
+
* The subscription mechanics are `EstuarineGhostManager`'s, including the part
|
|
18
|
+
* that matters: the surface is a SIGNAL, null at mount and replaced when the
|
|
19
|
+
* surface block is.
|
|
20
|
+
*/
|
|
21
|
+
export class EdgyRelationResolver extends InteractivityExtension {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this._subscriptions = [];
|
|
25
|
+
this._disposeSurfaceEffect = null;
|
|
26
|
+
this._pending = null;
|
|
27
|
+
this._candidates = new Set();
|
|
28
|
+
}
|
|
29
|
+
static { this.key = 'edgy-relation-resolver'; }
|
|
30
|
+
mounted() {
|
|
31
|
+
this._disposeSurfaceEffect = effect(() => {
|
|
32
|
+
this._resubscribe(this.gfx.surface$.value);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
unmounted() {
|
|
36
|
+
this._disposeSurfaceEffect?.();
|
|
37
|
+
this._disposeSurfaceEffect = null;
|
|
38
|
+
this._unsubscribe();
|
|
39
|
+
this._forget();
|
|
40
|
+
super.unmounted();
|
|
41
|
+
}
|
|
42
|
+
_forget() {
|
|
43
|
+
if (this._pending)
|
|
44
|
+
clearTimeout(this._pending);
|
|
45
|
+
this._pending = null;
|
|
46
|
+
this._candidates.clear();
|
|
47
|
+
}
|
|
48
|
+
_unsubscribe() {
|
|
49
|
+
for (const subscription of this._subscriptions)
|
|
50
|
+
subscription.unsubscribe();
|
|
51
|
+
this._subscriptions = [];
|
|
52
|
+
}
|
|
53
|
+
_resubscribe(surface) {
|
|
54
|
+
this._unsubscribe();
|
|
55
|
+
// Candidates queued against the previous surface are about elements this
|
|
56
|
+
// one has never heard of.
|
|
57
|
+
this._forget();
|
|
58
|
+
if (!surface)
|
|
59
|
+
return;
|
|
60
|
+
this._subscriptions.push(surface.elementAdded.subscribe(({ id, local }) => {
|
|
61
|
+
if (!local)
|
|
62
|
+
return;
|
|
63
|
+
this._schedule(id);
|
|
64
|
+
}));
|
|
65
|
+
this._subscriptions.push(surface.elementUpdated.subscribe(({ id, props, local }) => {
|
|
66
|
+
// LOCAL only, the filter `reading.ts` documents at length: `local`
|
|
67
|
+
// partitions the fleet into one writer and N−1 silent observers with
|
|
68
|
+
// no leader election. Without it, everyone on a shared board would
|
|
69
|
+
// name the same relation the moment it synced — the same role and the
|
|
70
|
+
// same word each time, so nothing would corrupt, but each peer would
|
|
71
|
+
// get an undo entry for a gesture they did not make.
|
|
72
|
+
if (!local)
|
|
73
|
+
return;
|
|
74
|
+
// Only the two ends can turn an unnamed relation into a sentence.
|
|
75
|
+
// Every other prop — a drag, a restyle, a label edit — must not even
|
|
76
|
+
// rearm the timer.
|
|
77
|
+
if (!props || (!('source' in props) && !('target' in props)))
|
|
78
|
+
return;
|
|
79
|
+
this._schedule(id);
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
_schedule(id) {
|
|
83
|
+
this._candidates.add(id);
|
|
84
|
+
if (this._pending)
|
|
85
|
+
clearTimeout(this._pending);
|
|
86
|
+
this._pending = setTimeout(() => {
|
|
87
|
+
this._pending = null;
|
|
88
|
+
this._flush();
|
|
89
|
+
}, EDGY_RELATION_DELAY_MS);
|
|
90
|
+
}
|
|
91
|
+
_flush() {
|
|
92
|
+
const surface = this.gfx.surface;
|
|
93
|
+
const candidates = this._candidates;
|
|
94
|
+
// Whatever happens below, what accumulated is accounted for: a set left
|
|
95
|
+
// behind would be replayed against a later, unrelated gesture.
|
|
96
|
+
this._candidates = new Set();
|
|
97
|
+
if (!surface)
|
|
98
|
+
return;
|
|
99
|
+
resolveEdgyRelations(surface, candidates);
|
|
100
|
+
}
|
|
101
|
+
}
|