@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.
Files changed (34) hide show
  1. package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.d.ts +18 -2
  2. package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.js +141 -8
  3. package/dist/_pkgs/affine-block-surface/extensions/map-quality-widget.js +16 -22
  4. package/dist/_pkgs/affine-block-surface/extensions/reading-widget.d.ts +18 -0
  5. package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +35 -12
  6. package/dist/_pkgs/affine-block-surface/extensions/reading.d.ts +8 -0
  7. package/dist/_pkgs/affine-block-surface/extensions/reading.js +17 -6
  8. package/dist/_pkgs/affine-block-surface/extensions/tags-toolbar.js +10 -2
  9. package/dist/_pkgs/affine-block-surface/extensions/validation-toolbar.js +9 -2
  10. package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +14 -11
  11. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +14 -7
  12. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  13. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  14. package/dist/_pkgs/affine-components/icons/text.js +426 -426
  15. package/dist/_pkgs/affine-components/toolbar/menu-button.js +27 -0
  16. package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.d.ts +2 -2
  17. package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.js +1 -1
  18. package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.d.ts +1 -1
  19. package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.js +10 -1
  20. package/dist/_pkgs/affine-model/elements/c4/board.d.ts +49 -0
  21. package/dist/_pkgs/affine-model/elements/c4/board.js +68 -0
  22. package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/slide-menu.js +1 -1
  23. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  24. package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
  25. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  26. package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
  27. package/dist/_pkgs/std/gfx/index.d.ts +1 -1
  28. package/dist/_pkgs/std/gfx/index.js +1 -1
  29. package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.d.ts +57 -0
  30. package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.js +80 -25
  31. package/dist/_pkgs/std/gfx/model/surface/pivot.d.ts +55 -1
  32. package/dist/_pkgs/std/gfx/model/surface/pivot.js +87 -0
  33. package/dist/_pkgs/std/gfx/model/surface/role.d.ts +12 -1
  34. 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 last(this._hoveredElementsStack)?.dispatch(eventName, evt);
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
- _handlePointerMove(_evt) {
48
- const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
49
- const hoveredElmViews = this.gfx.grid
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
- // The VIEW's hit test, not the model's.
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 type { GfxPrimitiveElementModel } from './element-model.js';
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 — `'depends on'`. Resolved by the host. */
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>`. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-core",
3
3
  "description": "Labre editor — the full BlockSuite-derived editor as one package.",
4
- "version": "0.34.1",
4
+ "version": "0.35.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",