@formicoidea/labre-core 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/_pkgs/affine-block-surface/extensions/editor-anchored-panel.d.ts +18 -2
- package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.js +141 -8
- package/dist/_pkgs/affine-block-surface/extensions/map-quality-widget.js +16 -22
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +35 -12
- package/dist/_pkgs/affine-block-surface/extensions/reading.d.ts +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading.js +17 -6
- package/dist/_pkgs/affine-block-surface/extensions/tags-toolbar.js +10 -2
- package/dist/_pkgs/affine-block-surface/extensions/validation-toolbar.js +9 -2
- package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +14 -11
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +14 -7
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
- package/dist/_pkgs/affine-components/icons/text.js +426 -426
- package/dist/_pkgs/affine-components/toolbar/menu-button.js +27 -0
- package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.d.ts +2 -2
- package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.js +1 -1
- package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.d.ts +1 -1
- package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.js +10 -1
- package/dist/_pkgs/affine-model/elements/c4/board.d.ts +49 -0
- package/dist/_pkgs/affine-model/elements/c4/board.js +68 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/slide-menu.js +1 -1
- package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
- package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
- package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
- package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
- package/dist/_pkgs/std/gfx/index.d.ts +1 -1
- package/dist/_pkgs/std/gfx/index.js +1 -1
- package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.d.ts +57 -0
- package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.js +80 -25
- package/dist/_pkgs/std/gfx/model/surface/pivot.d.ts +55 -1
- package/dist/_pkgs/std/gfx/model/surface/pivot.js +87 -0
- package/dist/_pkgs/std/gfx/model/surface/role.d.ts +12 -1
- package/package.json +1 -1
|
@@ -8,6 +8,63 @@ export declare class GfxViewEventManager {
|
|
|
8
8
|
private _callInReverseOrder;
|
|
9
9
|
constructor(gfx: GfxController);
|
|
10
10
|
dispatch(eventName: SupportedEvent, evt: PointerEventState): boolean | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Which view a POSITIONED event (click, dblclick, pointerdown/up) belongs to.
|
|
13
|
+
*
|
|
14
|
+
* The hovered stack answers whenever it is still true — the pointer has not
|
|
15
|
+
* moved off the view it was last seen on, which is the ordinary case and the
|
|
16
|
+
* one every framework gesture already relies on. What this adds is the
|
|
17
|
+
* fallback: when the top of the stack is NOT under the event's own
|
|
18
|
+
* coordinates, the target is resolved from those coordinates instead of the
|
|
19
|
+
* event being dropped.
|
|
20
|
+
*
|
|
21
|
+
* ## The bug this repairs
|
|
22
|
+
*
|
|
23
|
+
* The stack is only ever rebuilt by {@link _handlePointerMove}, so a pointer
|
|
24
|
+
* that arrives somewhere without a `pointermove` reaching this manager leaves
|
|
25
|
+
* it EMPTY or STALE — and every click and double-click on a canvas element is
|
|
26
|
+
* then delivered to nobody, or to whatever the pointer was last over. It
|
|
27
|
+
* happens for real: a `pointermove` emitted while the event dispatcher is not
|
|
28
|
+
* yet active is dropped before it gets here (the first gesture in a freshly
|
|
29
|
+
* mounted editor), an element created under a stationary pointer is never
|
|
30
|
+
* moved onto, and a viewport change slides the canvas under a pointer that
|
|
31
|
+
* never moves.
|
|
32
|
+
*
|
|
33
|
+
* The symptom is a very specific one, and it is what sent us here: SELECTING
|
|
34
|
+
* the element works — `handleElementSelection` re-picks by point on every
|
|
35
|
+
* click — while the element's own gestures do not, because they came through
|
|
36
|
+
* this stack. Two answers to "what is under the pointer", one of them stale.
|
|
37
|
+
* Reported on the C4 board's title band, where a click selected the sheet and
|
|
38
|
+
* a double-click renamed nothing.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT a rebuild of the stack: hover enter/leave is a `pointermove`
|
|
41
|
+
* story, and firing those from a click would announce arrivals and departures
|
|
42
|
+
* that never happened.
|
|
43
|
+
*/
|
|
44
|
+
private _targetOf;
|
|
11
45
|
private _handleDrag;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a view answers for a MODEL point — the VIEW's hit test, not the
|
|
48
|
+
* model's.
|
|
49
|
+
*
|
|
50
|
+
* `GfxElementModelView.includesPoint` delegates to the model by default, so
|
|
51
|
+
* for every view that does not override it this is the same answer as
|
|
52
|
+
* before. It is the seam a framework needs when the area that SELECTS an
|
|
53
|
+
* element and the area that its own gestures answer on are not the same
|
|
54
|
+
* rectangle: a framework background is picked by its border alone (issue
|
|
55
|
+
* #194), yet its editable labels must keep receiving the double-click that
|
|
56
|
+
* renames them, and a BPMN pool's lane separators must keep receiving the
|
|
57
|
+
* drag that moves them. Those zones are drawn by the framework's view, so
|
|
58
|
+
* that is where they are declared — the model layer stays free of geometry it
|
|
59
|
+
* cannot see.
|
|
60
|
+
*/
|
|
61
|
+
private _answersAt;
|
|
62
|
+
/**
|
|
63
|
+
* The views under a MODEL point, in PAINT ORDER — the topmost last.
|
|
64
|
+
*
|
|
65
|
+
* One walk, used by the hover bookkeeping and by {@link _targetOf}, so "what
|
|
66
|
+
* is under the pointer" has exactly one answer however it is asked.
|
|
67
|
+
*/
|
|
68
|
+
private _viewsAt;
|
|
12
69
|
private _handlePointerMove;
|
|
13
70
|
}
|
|
@@ -22,9 +22,49 @@ export class GfxViewEventManager {
|
|
|
22
22
|
return this._handleDrag(eventName, evt);
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
|
-
return
|
|
25
|
+
return this._targetOf(evt)?.dispatch(eventName, evt);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Which view a POSITIONED event (click, dblclick, pointerdown/up) belongs to.
|
|
30
|
+
*
|
|
31
|
+
* The hovered stack answers whenever it is still true — the pointer has not
|
|
32
|
+
* moved off the view it was last seen on, which is the ordinary case and the
|
|
33
|
+
* one every framework gesture already relies on. What this adds is the
|
|
34
|
+
* fallback: when the top of the stack is NOT under the event's own
|
|
35
|
+
* coordinates, the target is resolved from those coordinates instead of the
|
|
36
|
+
* event being dropped.
|
|
37
|
+
*
|
|
38
|
+
* ## The bug this repairs
|
|
39
|
+
*
|
|
40
|
+
* The stack is only ever rebuilt by {@link _handlePointerMove}, so a pointer
|
|
41
|
+
* that arrives somewhere without a `pointermove` reaching this manager leaves
|
|
42
|
+
* it EMPTY or STALE — and every click and double-click on a canvas element is
|
|
43
|
+
* then delivered to nobody, or to whatever the pointer was last over. It
|
|
44
|
+
* happens for real: a `pointermove` emitted while the event dispatcher is not
|
|
45
|
+
* yet active is dropped before it gets here (the first gesture in a freshly
|
|
46
|
+
* mounted editor), an element created under a stationary pointer is never
|
|
47
|
+
* moved onto, and a viewport change slides the canvas under a pointer that
|
|
48
|
+
* never moves.
|
|
49
|
+
*
|
|
50
|
+
* The symptom is a very specific one, and it is what sent us here: SELECTING
|
|
51
|
+
* the element works — `handleElementSelection` re-picks by point on every
|
|
52
|
+
* click — while the element's own gestures do not, because they came through
|
|
53
|
+
* this stack. Two answers to "what is under the pointer", one of them stale.
|
|
54
|
+
* Reported on the C4 board's title band, where a click selected the sheet and
|
|
55
|
+
* a double-click renamed nothing.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately NOT a rebuild of the stack: hover enter/leave is a `pointermove`
|
|
58
|
+
* story, and firing those from a click would announce arrivals and departures
|
|
59
|
+
* that never happened.
|
|
60
|
+
*/
|
|
61
|
+
_targetOf(evt) {
|
|
62
|
+
const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
|
|
63
|
+
const hovered = last(this._hoveredElementsStack) ?? null;
|
|
64
|
+
if (hovered && this._answersAt(hovered, x, y))
|
|
65
|
+
return hovered;
|
|
66
|
+
return last(this._viewsAt(x, y)) ?? null;
|
|
67
|
+
}
|
|
28
68
|
_handleDrag(evtName, _evt) {
|
|
29
69
|
switch (evtName) {
|
|
30
70
|
case 'dragstart': {
|
|
@@ -44,9 +84,39 @@ export class GfxViewEventManager {
|
|
|
44
84
|
}
|
|
45
85
|
}
|
|
46
86
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Whether a view answers for a MODEL point — the VIEW's hit test, not the
|
|
89
|
+
* model's.
|
|
90
|
+
*
|
|
91
|
+
* `GfxElementModelView.includesPoint` delegates to the model by default, so
|
|
92
|
+
* for every view that does not override it this is the same answer as
|
|
93
|
+
* before. It is the seam a framework needs when the area that SELECTS an
|
|
94
|
+
* element and the area that its own gestures answer on are not the same
|
|
95
|
+
* rectangle: a framework background is picked by its border alone (issue
|
|
96
|
+
* #194), yet its editable labels must keep receiving the double-click that
|
|
97
|
+
* renames them, and a BPMN pool's lane separators must keep receiving the
|
|
98
|
+
* drag that moves them. Those zones are drawn by the framework's view, so
|
|
99
|
+
* that is where they are declared — the model layer stays free of geometry it
|
|
100
|
+
* cannot see.
|
|
101
|
+
*/
|
|
102
|
+
_answersAt(view, x, y) {
|
|
103
|
+
const model = view.model;
|
|
104
|
+
return (view.includesPoint(x, y, {
|
|
105
|
+
hitThreshold: 10,
|
|
106
|
+
responsePadding: [5, 5],
|
|
107
|
+
}, this.gfx.std.host) ||
|
|
108
|
+
('externalBound' in model
|
|
109
|
+
? (model.externalBound?.isPointInBound([x, y]) ?? false)
|
|
110
|
+
: false));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The views under a MODEL point, in PAINT ORDER — the topmost last.
|
|
114
|
+
*
|
|
115
|
+
* One walk, used by the hover bookkeeping and by {@link _targetOf}, so "what
|
|
116
|
+
* is under the pointer" has exactly one answer however it is asked.
|
|
117
|
+
*/
|
|
118
|
+
_viewsAt(x, y) {
|
|
119
|
+
return (this.gfx.grid
|
|
50
120
|
.search(new Bound(x - 5, y - 5, 10, 10), {
|
|
51
121
|
filter: ['canvas', 'local'],
|
|
52
122
|
})
|
|
@@ -54,33 +124,18 @@ export class GfxViewEventManager {
|
|
|
54
124
|
const view = this.gfx.view.get(model);
|
|
55
125
|
if (!view)
|
|
56
126
|
return pre;
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
// `GfxElementModelView.includesPoint` delegates to the model by
|
|
60
|
-
// default, so for every view that does not override it this is the
|
|
61
|
-
// same answer as before. It is the seam a framework needs when the
|
|
62
|
-
// area that SELECTS an element and the area that its own gestures
|
|
63
|
-
// answer on are not the same rectangle: a framework background is
|
|
64
|
-
// picked by its border alone (issue #194), yet its editable labels
|
|
65
|
-
// must keep receiving the double-click that renames them, and a BPMN
|
|
66
|
-
// pool's lane separators must keep receiving the drag that moves them.
|
|
67
|
-
// Those zones are drawn by the framework's view, so that is where they
|
|
68
|
-
// are declared — the model layer stays free of geometry it cannot see.
|
|
69
|
-
if (view.includesPoint(x, y, {
|
|
70
|
-
hitThreshold: 10,
|
|
71
|
-
responsePadding: [5, 5],
|
|
72
|
-
}, this.gfx.std.host) ||
|
|
73
|
-
('externalBound' in model
|
|
74
|
-
? model.externalBound?.isPointInBound([x, y])
|
|
75
|
-
: false)) {
|
|
127
|
+
if (this._answersAt(view, x, y))
|
|
76
128
|
pre.push(view);
|
|
77
|
-
}
|
|
78
129
|
return pre;
|
|
79
130
|
}, [])
|
|
80
131
|
// Sort by paint order (same convention as `getElementByPoint`) so the
|
|
81
132
|
// stack's last entry — the click/dblclick target — is the TOPMOST view,
|
|
82
133
|
// not whichever the grid returned last (e.g. a background under a node).
|
|
83
|
-
.sort((a, b) => compare(a.model, b.model));
|
|
134
|
+
.sort((a, b) => compare(a.model, b.model)));
|
|
135
|
+
}
|
|
136
|
+
_handlePointerMove(_evt) {
|
|
137
|
+
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
|
|
138
|
+
const hoveredElmViews = this._viewsAt(x, y);
|
|
84
139
|
const currentStackedViews = new Set(this._hoveredElementsStack);
|
|
85
140
|
const visited = new Set();
|
|
86
141
|
this._callInReverseOrder(view => {
|
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
* the host's job, built from per-document calls to
|
|
13
13
|
* {@link collectPivotOccurrences}. The repository has no backlink
|
|
14
14
|
* infrastructure and this module deliberately does not introduce one.
|
|
15
|
+
*
|
|
16
|
+
* Beside the collector sit the two READ predicates over the same field —
|
|
17
|
+
* {@link isPivotBound}, the strict one, and {@link resolvePivotBinding}, the
|
|
18
|
+
* one that tolerates a binding carried by a composite. Both are pure functions
|
|
19
|
+
* of the document, and neither writes.
|
|
15
20
|
*/
|
|
16
|
-
import
|
|
21
|
+
import { GfxPrimitiveElementModel } from './element-model.js';
|
|
17
22
|
import type { SurfaceBlockModel } from './surface-model.js';
|
|
18
23
|
/** One element that is an occurrence of one pivot record. */
|
|
19
24
|
export type PivotOccurrence = {
|
|
@@ -37,6 +42,55 @@ export type PivotBoundElement = GfxPrimitiveElementModel & {
|
|
|
37
42
|
* the provider looking for a document that cannot exist.
|
|
38
43
|
*/
|
|
39
44
|
export declare function isPivotBound(el: GfxPrimitiveElementModel): el is PivotBoundElement;
|
|
45
|
+
/**
|
|
46
|
+
* The binding that governs an element — **a READING tolerance, and the only
|
|
47
|
+
* one** (PO arbitration of 02/09/2026).
|
|
48
|
+
*
|
|
49
|
+
* Returns the element itself when it carries a binding, otherwise the first
|
|
50
|
+
* bound element in its chain of ancestor groups, otherwise `undefined`.
|
|
51
|
+
*
|
|
52
|
+
* ## Why a chain at all
|
|
53
|
+
*
|
|
54
|
+
* ADR 0005/0006 put the binding ON THE ELEMENT, and that has not changed. What
|
|
55
|
+
* the format never said is WHICH element of a COMPOSITE carries it — and every
|
|
56
|
+
* business framework draws its artefacts as composites: a Wardley component is
|
|
57
|
+
* a `group` holding the `wardleyNode` circle and a free `text` label, a C4
|
|
58
|
+
* component is a shape plus three texts. Two different elements can therefore
|
|
59
|
+
* legitimately be "the component":
|
|
60
|
+
*
|
|
61
|
+
* - the one carrying the ROLE (`wardley:component`), which is what the reading
|
|
62
|
+
* engine and the toolbar resolve to, through the group;
|
|
63
|
+
* - the one a plain click SELECTS, which is the GROUP — and therefore what a
|
|
64
|
+
* host's own "link this to a record" gesture naturally stamps.
|
|
65
|
+
*
|
|
66
|
+
* A reader that only looked at the role-carrying element reported "Not linked
|
|
67
|
+
* to a record" on a component the host had bound by the group, with the link
|
|
68
|
+
* plainly there in the document. That is the bug this function exists for. It
|
|
69
|
+
* is a filter on the way IN, never a rule about where a binding belongs: the
|
|
70
|
+
* WRITE contract is untouched — `pivot.bind` and the panel's "Link to a record"
|
|
71
|
+
* still stamp the role-carrying element.
|
|
72
|
+
*
|
|
73
|
+
* ## Why the child wins
|
|
74
|
+
*
|
|
75
|
+
* The most SPECIFIC binding wins, so an element bound in its own right is never
|
|
76
|
+
* shadowed by the group it happens to sit in. Deliberately asymmetric with the
|
|
77
|
+
* ancestor walk: a group that gathers two bound components is a container, not
|
|
78
|
+
* an occurrence of either, and each child keeps answering for itself.
|
|
79
|
+
*
|
|
80
|
+
* ## Scope
|
|
81
|
+
*
|
|
82
|
+
* Reading only. `collectPivotOccurrences` and the host-facing seam
|
|
83
|
+
* (materiality patches, backlinks) stay strictly element-by-element: they are
|
|
84
|
+
* the ADR 0006 contract the host builds its own derived state on, and widening
|
|
85
|
+
* them would change what "an occurrence" means without a decision.
|
|
86
|
+
*
|
|
87
|
+
* Cheap and allocation-light: the chain is at most a handful of groups deep and
|
|
88
|
+
* the visited set is only built when the element has an ancestor at all. The
|
|
89
|
+
* guard is not decoration — a corrupted document can hold a cycle in the group
|
|
90
|
+
* relation (`SurfaceBlockModel.getGroups` carries the same guard), and a reader
|
|
91
|
+
* on the render path must degrade to `undefined`, never hang.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolvePivotBinding(element: GfxPrimitiveElementModel): PivotBoundElement | undefined;
|
|
40
94
|
/**
|
|
41
95
|
* Every occurrence on one surface, in element order. O(n) over the surface's
|
|
42
96
|
* elements; allocates nothing persistent.
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlinks over `pivotDocId` — **computed, never persisted** (MF1, ADR 0005 § 5).
|
|
3
|
+
*
|
|
4
|
+
* The library ships one pure, synchronous collector and nothing else: no index,
|
|
5
|
+
* no reverse map, no cache, and nothing written back into the document. That is
|
|
6
|
+
* not an optimisation left for later, it is the invariant: "which boards
|
|
7
|
+
* mention this component" is a recomputation, always, so it can never drift
|
|
8
|
+
* from the document and can never be corrupted by a bad write — because there
|
|
9
|
+
* is no write.
|
|
10
|
+
*
|
|
11
|
+
* Cross-document aggregation ("every board where this component appears") is
|
|
12
|
+
* the host's job, built from per-document calls to
|
|
13
|
+
* {@link collectPivotOccurrences}. The repository has no backlink
|
|
14
|
+
* infrastructure and this module deliberately does not introduce one.
|
|
15
|
+
*
|
|
16
|
+
* Beside the collector sit the two READ predicates over the same field —
|
|
17
|
+
* {@link isPivotBound}, the strict one, and {@link resolvePivotBinding}, the
|
|
18
|
+
* one that tolerates a binding carried by a composite. Both are pure functions
|
|
19
|
+
* of the document, and neither writes.
|
|
20
|
+
*/
|
|
21
|
+
import { GfxPrimitiveElementModel } from './element-model.js';
|
|
1
22
|
/**
|
|
2
23
|
* Whether the element carries a binding. Narrowing type guard, so a caller that
|
|
3
24
|
* has checked does not have to re-assert the string.
|
|
@@ -9,6 +30,72 @@
|
|
|
9
30
|
export function isPivotBound(el) {
|
|
10
31
|
return typeof el.pivotDocId === 'string' && el.pivotDocId.length > 0;
|
|
11
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The binding that governs an element — **a READING tolerance, and the only
|
|
35
|
+
* one** (PO arbitration of 02/09/2026).
|
|
36
|
+
*
|
|
37
|
+
* Returns the element itself when it carries a binding, otherwise the first
|
|
38
|
+
* bound element in its chain of ancestor groups, otherwise `undefined`.
|
|
39
|
+
*
|
|
40
|
+
* ## Why a chain at all
|
|
41
|
+
*
|
|
42
|
+
* ADR 0005/0006 put the binding ON THE ELEMENT, and that has not changed. What
|
|
43
|
+
* the format never said is WHICH element of a COMPOSITE carries it — and every
|
|
44
|
+
* business framework draws its artefacts as composites: a Wardley component is
|
|
45
|
+
* a `group` holding the `wardleyNode` circle and a free `text` label, a C4
|
|
46
|
+
* component is a shape plus three texts. Two different elements can therefore
|
|
47
|
+
* legitimately be "the component":
|
|
48
|
+
*
|
|
49
|
+
* - the one carrying the ROLE (`wardley:component`), which is what the reading
|
|
50
|
+
* engine and the toolbar resolve to, through the group;
|
|
51
|
+
* - the one a plain click SELECTS, which is the GROUP — and therefore what a
|
|
52
|
+
* host's own "link this to a record" gesture naturally stamps.
|
|
53
|
+
*
|
|
54
|
+
* A reader that only looked at the role-carrying element reported "Not linked
|
|
55
|
+
* to a record" on a component the host had bound by the group, with the link
|
|
56
|
+
* plainly there in the document. That is the bug this function exists for. It
|
|
57
|
+
* is a filter on the way IN, never a rule about where a binding belongs: the
|
|
58
|
+
* WRITE contract is untouched — `pivot.bind` and the panel's "Link to a record"
|
|
59
|
+
* still stamp the role-carrying element.
|
|
60
|
+
*
|
|
61
|
+
* ## Why the child wins
|
|
62
|
+
*
|
|
63
|
+
* The most SPECIFIC binding wins, so an element bound in its own right is never
|
|
64
|
+
* shadowed by the group it happens to sit in. Deliberately asymmetric with the
|
|
65
|
+
* ancestor walk: a group that gathers two bound components is a container, not
|
|
66
|
+
* an occurrence of either, and each child keeps answering for itself.
|
|
67
|
+
*
|
|
68
|
+
* ## Scope
|
|
69
|
+
*
|
|
70
|
+
* Reading only. `collectPivotOccurrences` and the host-facing seam
|
|
71
|
+
* (materiality patches, backlinks) stay strictly element-by-element: they are
|
|
72
|
+
* the ADR 0006 contract the host builds its own derived state on, and widening
|
|
73
|
+
* them would change what "an occurrence" means without a decision.
|
|
74
|
+
*
|
|
75
|
+
* Cheap and allocation-light: the chain is at most a handful of groups deep and
|
|
76
|
+
* the visited set is only built when the element has an ancestor at all. The
|
|
77
|
+
* guard is not decoration — a corrupted document can hold a cycle in the group
|
|
78
|
+
* relation (`SurfaceBlockModel.getGroups` carries the same guard), and a reader
|
|
79
|
+
* on the render path must degrade to `undefined`, never hang.
|
|
80
|
+
*/
|
|
81
|
+
export function resolvePivotBinding(element) {
|
|
82
|
+
if (isPivotBound(element))
|
|
83
|
+
return element;
|
|
84
|
+
const visited = new Set([element.id]);
|
|
85
|
+
let ancestor = element.group;
|
|
86
|
+
while (ancestor && !visited.has(ancestor.id)) {
|
|
87
|
+
visited.add(ancestor.id);
|
|
88
|
+
// A group may be a BLOCK (a frame): only a canvas element declares
|
|
89
|
+
// `pivotDocId`, so anything else can never be bound — the walk steps over
|
|
90
|
+
// it rather than stopping, because what sits above it still can be.
|
|
91
|
+
if (ancestor instanceof GfxPrimitiveElementModel &&
|
|
92
|
+
isPivotBound(ancestor)) {
|
|
93
|
+
return ancestor;
|
|
94
|
+
}
|
|
95
|
+
ancestor = ancestor.group;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
12
99
|
/**
|
|
13
100
|
* Every occurrence on one surface, in element order. O(n) over the surface's
|
|
14
101
|
* elements; allocates nothing persistent.
|
|
@@ -62,7 +62,7 @@ export type RoleKind = 'node' | 'edge' | 'text';
|
|
|
62
62
|
* catalogue always wins, and a catalogue-less playground still reads.
|
|
63
63
|
*/
|
|
64
64
|
export interface EdgeDirectionDef {
|
|
65
|
-
/** i18n key of the verb — `'
|
|
65
|
+
/** i18n key of the verb — `'needs'`. Resolved by the host. */
|
|
66
66
|
verbKey: string;
|
|
67
67
|
/** The framework's own wording for {@link verbKey}. */
|
|
68
68
|
verbFallback?: string;
|
|
@@ -75,6 +75,17 @@ export interface EdgeDirectionDef {
|
|
|
75
75
|
gestureHintKey?: string;
|
|
76
76
|
/** The framework's own wording for {@link gestureHintKey}. */
|
|
77
77
|
gestureHintFallback?: string;
|
|
78
|
+
/**
|
|
79
|
+
* CSS colour of the chip the reveal draws this verb in (M2). Absent — which
|
|
80
|
+
* is every role but one — keeps the house affordance blue the mechanism has
|
|
81
|
+
* always used, so declaring nothing repaints nothing.
|
|
82
|
+
*
|
|
83
|
+
* A colour belongs to the ROLE rather than to the widget because the chip is
|
|
84
|
+
* per-relation: the PO may want one framework's central relation to stand out
|
|
85
|
+
* without every other framework's edges changing with it. The chip's text is
|
|
86
|
+
* white, so a colour declared here must be dark enough to carry it.
|
|
87
|
+
*/
|
|
88
|
+
chipColor?: string;
|
|
78
89
|
}
|
|
79
90
|
export interface RoleDef {
|
|
80
91
|
/** Namespaced id, `<framework>:<role>`. */
|