@formicoidea/labre-core 0.37.0 → 0.38.2

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 (54) hide show
  1. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +31 -0
  2. package/dist/_pkgs/affine-block-frame/frame-manager.js +62 -2
  3. package/dist/_pkgs/affine-block-frame/frame-tool.js +6 -2
  4. package/dist/_pkgs/affine-block-surface/extensions/audit.js +12 -34
  5. package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
  6. package/dist/_pkgs/affine-block-surface/extensions/reading.js +10 -28
  7. package/dist/_pkgs/affine-block-surface/extensions/validation.d.ts +321 -16
  8. package/dist/_pkgs/affine-block-surface/extensions/validation.js +945 -62
  9. package/dist/_pkgs/affine-block-surface/framework-background/facts.d.ts +24 -0
  10. package/dist/_pkgs/affine-block-surface/framework-background/facts.js +31 -0
  11. package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +1 -0
  12. package/dist/_pkgs/affine-block-surface/framework-background/index.js +1 -0
  13. package/dist/_pkgs/affine-block-surface/framework-background/stacking.d.ts +131 -0
  14. package/dist/_pkgs/affine-block-surface/framework-background/stacking.js +184 -0
  15. package/dist/_pkgs/affine-block-surface/utils/sort.d.ts +15 -1
  16. package/dist/_pkgs/affine-block-surface/utils/sort.js +30 -31
  17. package/dist/_pkgs/affine-block-surface/view.js +5 -0
  18. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  19. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  20. package/dist/_pkgs/affine-components/icons/text.js +426 -426
  21. package/dist/_pkgs/affine-gfx-group/element-renderer/utils.js +3 -1
  22. package/dist/_pkgs/affine-gfx-template/index.d.ts +1 -0
  23. package/dist/_pkgs/affine-gfx-template/index.js +1 -0
  24. package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +16 -2
  25. package/dist/_pkgs/affine-gfx-template/make-snapshot.js +43 -4
  26. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +1 -0
  27. package/dist/_pkgs/affine-gfx-template/services/template.d.ts +6 -0
  28. package/dist/_pkgs/affine-gfx-template/services/template.js +6 -0
  29. package/dist/_pkgs/affine-gfx-template/snapshot-from-action.d.ts +20 -0
  30. package/dist/_pkgs/affine-gfx-template/snapshot-from-action.js +205 -0
  31. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +4 -0
  32. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +13 -0
  33. package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.d.ts +11 -17
  34. package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.js +55 -14
  35. package/dist/_pkgs/affine-model/elements/cynefin/cynefin.d.ts +9 -20
  36. package/dist/_pkgs/affine-model/elements/cynefin/cynefin.js +7 -28
  37. package/dist/_pkgs/affine-model/elements/edgy/board.d.ts +9 -21
  38. package/dist/_pkgs/affine-model/elements/edgy/board.js +8 -29
  39. package/dist/_pkgs/affine-model/elements/edgy/facets.d.ts +14 -28
  40. package/dist/_pkgs/affine-model/elements/edgy/facets.js +12 -36
  41. package/dist/_pkgs/affine-model/elements/estuarine/estuarine.d.ts +9 -19
  42. package/dist/_pkgs/affine-model/elements/estuarine/estuarine.js +7 -27
  43. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.d.ts +28 -0
  44. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.js +20 -3
  45. package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +124 -124
  46. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
  47. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  48. package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
  49. package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
  50. package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
  51. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  52. package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
  53. package/dist/_pkgs/std/gfx/model/surface/surface-model.js +12 -2
  54. package/package.json +1 -1
@@ -61,6 +61,30 @@ export interface BackgroundAxisFact {
61
61
  */
62
62
  transitionsVisibleProp?: string;
63
63
  }
64
+ /**
65
+ * The frame that WHOLLY CONTAINS `bound`, or `null` — the one membership test
66
+ * every reader of a framework board uses (PF2.4).
67
+ *
68
+ * An element belongs to a board (a Wardley map, a BPMN pool, a C4 board) only
69
+ * when it is ENTIRELY inside the board's element bound. Not its centre: a node
70
+ * hanging over the edge of a map is a node the author has not finished putting
71
+ * on it, and the validation engine already judges it that way — reading or
72
+ * auditing it as a member would give two answers about one node.
73
+ *
74
+ * The asymmetry with the INNER regions is deliberate: a zone, an evolution
75
+ * stage, a BPMN lane keeps the centre test, because a band is meant to be
76
+ * straddled and a board is not.
77
+ *
78
+ * `Bound.contains` is inclusive on all four edges, so an element whose bound
79
+ * EQUALS the board's is a member — the same answer `containingBackground` gives
80
+ * inside the engine, which is the whole point of having one function.
81
+ *
82
+ * Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in: two
83
+ * overlapping maps must attribute an artefact the same way on every reload.
84
+ */
85
+ export declare function containingFrame<T extends {
86
+ id: string;
87
+ }>(bound: Bound, candidates: Iterable<T>, boundOf: (candidate: T) => Bound): T | null;
64
88
  /** The declared axes, as facts. Empty for a background that declares none. */
65
89
  export declare function backgroundAxisFacts(def: FrameworkBackgroundDef): readonly BackgroundAxisFact[];
66
90
  /** The declared axis with this id, as a fact. */
@@ -29,6 +29,37 @@ const FORWARD = {
29
29
  Object.freeze(FORWARD.horizontal);
30
30
  Object.freeze(FORWARD.vertical);
31
31
  Object.freeze(FORWARD);
32
+ /**
33
+ * The frame that WHOLLY CONTAINS `bound`, or `null` — the one membership test
34
+ * every reader of a framework board uses (PF2.4).
35
+ *
36
+ * An element belongs to a board (a Wardley map, a BPMN pool, a C4 board) only
37
+ * when it is ENTIRELY inside the board's element bound. Not its centre: a node
38
+ * hanging over the edge of a map is a node the author has not finished putting
39
+ * on it, and the validation engine already judges it that way — reading or
40
+ * auditing it as a member would give two answers about one node.
41
+ *
42
+ * The asymmetry with the INNER regions is deliberate: a zone, an evolution
43
+ * stage, a BPMN lane keeps the centre test, because a band is meant to be
44
+ * straddled and a board is not.
45
+ *
46
+ * `Bound.contains` is inclusive on all four edges, so an element whose bound
47
+ * EQUALS the board's is a member — the same answer `containingBackground` gives
48
+ * inside the engine, which is the whole point of having one function.
49
+ *
50
+ * Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in: two
51
+ * overlapping maps must attribute an artefact the same way on every reload.
52
+ */
53
+ export function containingFrame(bound, candidates, boundOf) {
54
+ let found = null;
55
+ for (const candidate of candidates) {
56
+ if (!boundOf(candidate).contains(bound))
57
+ continue;
58
+ if (found === null || candidate.id < found.id)
59
+ found = candidate;
60
+ }
61
+ return found;
62
+ }
32
63
  /** The declared axes, as facts. Empty for a background that declares none. */
33
64
  export function backgroundAxisFacts(def) {
34
65
  return (def.axes ?? []).map(axis => ({
@@ -3,3 +3,4 @@ export * from './facts.js';
3
3
  export * from './interaction.js';
4
4
  export * from './labels.js';
5
5
  export * from './renderer.js';
6
+ export * from './stacking.js';
@@ -3,3 +3,4 @@ export * from './facts.js';
3
3
  export * from './interaction.js';
4
4
  export * from './labels.js';
5
5
  export * from './renderer.js';
6
+ export * from './stacking.js';
@@ -0,0 +1,131 @@
1
+ import { Bound } from '../../global/gfx/index.js';
2
+ import { type GfxController, GfxExtension, type GfxModel } from '../../std/gfx/index.js';
3
+ /**
4
+ * One already-placed element, as {@link indexOverBackgrounds} needs to read it.
5
+ *
6
+ * Three fields and no model, so the placement can be reasoned about — and
7
+ * tested — without a document: what it is, where it is, and how deep. Blocks
8
+ * and canvas elements both reduce to this, which is what lets a frame (a
9
+ * block) and a Wardley zone (a canvas element) share one answer.
10
+ */
11
+ export interface StackedElement {
12
+ /** Fractional index. Sorts lexicographically, which IS the paint order. */
13
+ index: string;
14
+ /** Serialized box. */
15
+ xywh: string;
16
+ /** Whether this is a framework BACKGROUND — a map, a pool, a C4 board… */
17
+ isBackground: boolean;
18
+ }
19
+ /**
20
+ * How deep something drawn ON a framework board goes: just ABOVE the
21
+ * backgrounds it covers, and below everything else. `null` means the back of
22
+ * the surface.
23
+ *
24
+ * "The back" alone was the first answer and it was wrong, as the recette of
25
+ * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so an
26
+ * element sent behind the whole surface went behind the map and vanished. What
27
+ * it must be under is the artefacts it groups; what it must be over is the
28
+ * canvas they are drawn on. Those are two different depths, and only the first
29
+ * one is "the back".
30
+ *
31
+ * So: find the topmost background the box actually OVERLAPS — the map it was
32
+ * drawn on, and not some other framework's board parked elsewhere on the same
33
+ * canvas — and mint a key between it and whatever sits directly above it. With
34
+ * no background under it at all there is nothing to clear, and the back of the
35
+ * surface is right again.
36
+ *
37
+ * Two callers want exactly this depth, for the same reason. A Wardley zone is
38
+ * a wash over the components it names, so it is lowered — but not past the map
39
+ * (#213). A frame is a container that renders behind everything it owns, so it
40
+ * too is lowered — and it too vanished under the map. Same problem, same
41
+ * answer, one function.
42
+ *
43
+ * `siblings` need not be sorted: fractional indexes sort lexicographically, so
44
+ * this sorts them itself and reads the paint order straight off the strings.
45
+ */
46
+ export declare function indexOverBackgrounds(siblings: readonly StackedElement[], box: Bound): string | null;
47
+ /**
48
+ * Where a top-level element belongs in the stack, once the rule "a framework
49
+ * background is a FLOOR, never a lid" is applied. `null` means "leave it
50
+ * alone".
51
+ *
52
+ * ## The rule
53
+ *
54
+ * A background is the canvas the user draws ON. It must therefore stay under
55
+ * everything that overlaps it, and boards must still be able to overlap each
56
+ * other — a board dropped on a board is a cross-reading, not a mistake. Two
57
+ * cases, and they are the same sentence read from either side:
58
+ *
59
+ * 1. `element` is NOT a background — a shape, a node, a foreign framework's
60
+ * artefact. If a background it overlaps sits ABOVE it, it has been buried:
61
+ * raise it just above that background ({@link indexOverBackgrounds}), which
62
+ * is still below every artefact already drawn there. Otherwise nothing.
63
+ * 2. `element` IS a background. If another background it overlaps sits above
64
+ * it, the same raise applies — superposed boards stack in the order they
65
+ * were placed, each still under its own artefacts. Otherwise, if it covers
66
+ * an artefact it overlaps, it is acting as a lid: lower it to just above
67
+ * the floor it lies on, or to the back of the surface when it lies on bare
68
+ * canvas. Otherwise nothing.
69
+ *
70
+ * `siblings` are the OTHER top-level elements of the surface — canvas elements
71
+ * and blocks alike, since the surface paints both by index — with `element`
72
+ * itself removed. Nested elements are left out by the caller: a group's child
73
+ * is ordered by its ancestor's index, so moving it would change nothing.
74
+ *
75
+ * ## Idempotence, which is what makes undo safe
76
+ *
77
+ * The function never returns the index the element already has, so a second
78
+ * call about the same element on an unchanged board answers `null`: the rule
79
+ * has a fixed point and reaches it in one step. It answers about the element
80
+ * the user just placed or moved, and about that one only — which is what makes
81
+ * two superposed boards settle in the order the gestures happened rather than
82
+ * in some order the surface would have to arbitrate.
83
+ *
84
+ * That is the whole undo/redo argument, and it restates the frame manager's
85
+ * (`_watchFrameMoved`, #223): an index restored by undo is one the rule had
86
+ * already accepted when it was written, so replaying the rule over a restored
87
+ * state writes nothing — no corrupted depth, and no extra entry pushed onto
88
+ * the history stack.
89
+ */
90
+ export declare function stackingIndexFor(element: StackedElement, siblings: readonly StackedElement[]): string | null;
91
+ /**
92
+ * The top-level elements of a surface as {@link StackedElement}s — blocks as
93
+ * well as canvas elements, since the surface paints both by index — minus the
94
+ * one being placed.
95
+ *
96
+ * Read off `gfx.layer.layers` because that is the list the canvas draws from,
97
+ * and shared because both callers of the depth rules need exactly this list:
98
+ * the frame manager (#223) and {@link BackgroundStackingExtension}.
99
+ */
100
+ export declare function stackedElementsOf(gfx: GfxController, exclude?: GfxModel): StackedElement[];
101
+ /**
102
+ * Keeps {@link stackingIndexFor}'s rule true on a live surface: a framework
103
+ * background is a floor, never a lid — whatever framework it belongs to, and
104
+ * whichever of the two elements arrived first.
105
+ *
106
+ * ENGINE behaviour, not tooling, so it is registered unconditionally: a board
107
+ * whose framework flag is off still has to sit under what the user drops on it
108
+ * (`docs/adr/0009`).
109
+ *
110
+ * Two gestures produce a wrong depth, and they are the two this listens for:
111
+ * an element CREATED (every creation site mints the top of the stack, so a
112
+ * board drawn last covers what it was drawn around) and an element MOVED (onto
113
+ * a board that is above it, or over free elements it then hides). Anything
114
+ * else — a resize, a colour, a rename — cannot change who covers whom.
115
+ *
116
+ * Frames are BLOCKS: their own two gestures are already handled, at creation
117
+ * by `frameIndexAt` and on a move by `_watchFrameMoved` (#223), so this never
118
+ * writes a frame's index. They are read as siblings all the same — a board
119
+ * lowered under a frame must go under the frame.
120
+ *
121
+ * A remote change is ignored: the client that made it ran this same rule and
122
+ * synced the index it decided on. Applying it again here would be a second
123
+ * writer for one gesture.
124
+ */
125
+ export declare class BackgroundStackingExtension extends GfxExtension {
126
+ static key: string;
127
+ private readonly _disposable;
128
+ constructor(gfx: GfxController);
129
+ private _restack;
130
+ unmounted(): void;
131
+ }
@@ -0,0 +1,184 @@
1
+ import { FrameworkBackgroundElementModel } from '../../affine-model/index.js';
2
+ import { DisposableGroup } from '../../global/disposable/index.js';
3
+ import { Bound } from '../../global/gfx/index.js';
4
+ import { generateKeyBetween, GfxExtension, } from '../../std/gfx/index.js';
5
+ /**
6
+ * How deep something drawn ON a framework board goes: just ABOVE the
7
+ * backgrounds it covers, and below everything else. `null` means the back of
8
+ * the surface.
9
+ *
10
+ * "The back" alone was the first answer and it was wrong, as the recette of
11
+ * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so an
12
+ * element sent behind the whole surface went behind the map and vanished. What
13
+ * it must be under is the artefacts it groups; what it must be over is the
14
+ * canvas they are drawn on. Those are two different depths, and only the first
15
+ * one is "the back".
16
+ *
17
+ * So: find the topmost background the box actually OVERLAPS — the map it was
18
+ * drawn on, and not some other framework's board parked elsewhere on the same
19
+ * canvas — and mint a key between it and whatever sits directly above it. With
20
+ * no background under it at all there is nothing to clear, and the back of the
21
+ * surface is right again.
22
+ *
23
+ * Two callers want exactly this depth, for the same reason. A Wardley zone is
24
+ * a wash over the components it names, so it is lowered — but not past the map
25
+ * (#213). A frame is a container that renders behind everything it owns, so it
26
+ * too is lowered — and it too vanished under the map. Same problem, same
27
+ * answer, one function.
28
+ *
29
+ * `siblings` need not be sorted: fractional indexes sort lexicographically, so
30
+ * this sorts them itself and reads the paint order straight off the strings.
31
+ */
32
+ export function indexOverBackgrounds(siblings, box) {
33
+ const stack = [...siblings].sort((a, b) => a.index < b.index ? -1 : a.index > b.index ? 1 : 0);
34
+ // The LAST match, which is the topmost: a board with two maps on it gets the
35
+ // element above the one it is actually drawn over, whichever was drawn first.
36
+ const under = stack.reduce((found, element, at) => element.isBackground &&
37
+ Bound.deserialize(element.xywh).isOverlapWithBound(box)
38
+ ? at
39
+ : found, -1);
40
+ if (under < 0)
41
+ return null;
42
+ // `null` for the upper bound when the background is the topmost element
43
+ // there is — `generateKeyBetween` reads that as "append after".
44
+ return generateKeyBetween(stack[under].index, stack[under + 1]?.index ?? null);
45
+ }
46
+ /**
47
+ * Where a top-level element belongs in the stack, once the rule "a framework
48
+ * background is a FLOOR, never a lid" is applied. `null` means "leave it
49
+ * alone".
50
+ *
51
+ * ## The rule
52
+ *
53
+ * A background is the canvas the user draws ON. It must therefore stay under
54
+ * everything that overlaps it, and boards must still be able to overlap each
55
+ * other — a board dropped on a board is a cross-reading, not a mistake. Two
56
+ * cases, and they are the same sentence read from either side:
57
+ *
58
+ * 1. `element` is NOT a background — a shape, a node, a foreign framework's
59
+ * artefact. If a background it overlaps sits ABOVE it, it has been buried:
60
+ * raise it just above that background ({@link indexOverBackgrounds}), which
61
+ * is still below every artefact already drawn there. Otherwise nothing.
62
+ * 2. `element` IS a background. If another background it overlaps sits above
63
+ * it, the same raise applies — superposed boards stack in the order they
64
+ * were placed, each still under its own artefacts. Otherwise, if it covers
65
+ * an artefact it overlaps, it is acting as a lid: lower it to just above
66
+ * the floor it lies on, or to the back of the surface when it lies on bare
67
+ * canvas. Otherwise nothing.
68
+ *
69
+ * `siblings` are the OTHER top-level elements of the surface — canvas elements
70
+ * and blocks alike, since the surface paints both by index — with `element`
71
+ * itself removed. Nested elements are left out by the caller: a group's child
72
+ * is ordered by its ancestor's index, so moving it would change nothing.
73
+ *
74
+ * ## Idempotence, which is what makes undo safe
75
+ *
76
+ * The function never returns the index the element already has, so a second
77
+ * call about the same element on an unchanged board answers `null`: the rule
78
+ * has a fixed point and reaches it in one step. It answers about the element
79
+ * the user just placed or moved, and about that one only — which is what makes
80
+ * two superposed boards settle in the order the gestures happened rather than
81
+ * in some order the surface would have to arbitrate.
82
+ *
83
+ * That is the whole undo/redo argument, and it restates the frame manager's
84
+ * (`_watchFrameMoved`, #223): an index restored by undo is one the rule had
85
+ * already accepted when it was written, so replaying the rule over a restored
86
+ * state writes nothing — no corrupted depth, and no extra entry pushed onto
87
+ * the history stack.
88
+ */
89
+ export function stackingIndexFor(element, siblings) {
90
+ const box = Bound.deserialize(element.xywh);
91
+ const overlapping = siblings.filter(sibling => Bound.deserialize(sibling.xywh).isOverlapWithBound(box));
92
+ const buried = overlapping.some(sibling => sibling.isBackground && sibling.index > element.index);
93
+ const lidding = element.isBackground &&
94
+ overlapping.some(sibling => !sibling.isBackground && sibling.index < element.index);
95
+ const next = buried
96
+ ? indexOverBackgrounds(siblings, box)
97
+ : lidding
98
+ ? // Down to the floor it lies on, or to the back of the surface when
99
+ // there is none — the depth `createWardleyArea` mints by hand.
100
+ (indexOverBackgrounds(siblings, box) ?? backOf(siblings))
101
+ : null;
102
+ return next === element.index ? null : next;
103
+ }
104
+ /** A key below every sibling: the back of the surface. */
105
+ function backOf(siblings) {
106
+ const lowest = siblings.reduce((min, sibling) => min === null || sibling.index < min ? sibling.index : min, null);
107
+ return lowest === null ? null : generateKeyBetween(null, lowest);
108
+ }
109
+ /**
110
+ * The top-level elements of a surface as {@link StackedElement}s — blocks as
111
+ * well as canvas elements, since the surface paints both by index — minus the
112
+ * one being placed.
113
+ *
114
+ * Read off `gfx.layer.layers` because that is the list the canvas draws from,
115
+ * and shared because both callers of the depth rules need exactly this list:
116
+ * the frame manager (#223) and {@link BackgroundStackingExtension}.
117
+ */
118
+ export function stackedElementsOf(gfx, exclude) {
119
+ return gfx.layer.layers.reduce((all, layer) => all.concat(layer.elements
120
+ .filter(element => element.group === null && element !== exclude)
121
+ .map(element => ({
122
+ index: element.index,
123
+ xywh: element.xywh,
124
+ isBackground: element instanceof FrameworkBackgroundElementModel,
125
+ }))), []);
126
+ }
127
+ /**
128
+ * Keeps {@link stackingIndexFor}'s rule true on a live surface: a framework
129
+ * background is a floor, never a lid — whatever framework it belongs to, and
130
+ * whichever of the two elements arrived first.
131
+ *
132
+ * ENGINE behaviour, not tooling, so it is registered unconditionally: a board
133
+ * whose framework flag is off still has to sit under what the user drops on it
134
+ * (`docs/adr/0009`).
135
+ *
136
+ * Two gestures produce a wrong depth, and they are the two this listens for:
137
+ * an element CREATED (every creation site mints the top of the stack, so a
138
+ * board drawn last covers what it was drawn around) and an element MOVED (onto
139
+ * a board that is above it, or over free elements it then hides). Anything
140
+ * else — a resize, a colour, a rename — cannot change who covers whom.
141
+ *
142
+ * Frames are BLOCKS: their own two gestures are already handled, at creation
143
+ * by `frameIndexAt` and on a move by `_watchFrameMoved` (#223), so this never
144
+ * writes a frame's index. They are read as siblings all the same — a board
145
+ * lowered under a frame must go under the frame.
146
+ *
147
+ * A remote change is ignored: the client that made it ran this same rule and
148
+ * synced the index it decided on. Applying it again here would be a second
149
+ * writer for one gesture.
150
+ */
151
+ export class BackgroundStackingExtension extends GfxExtension {
152
+ static { this.key = 'background-stacking'; }
153
+ constructor(gfx) {
154
+ super(gfx);
155
+ this._disposable = new DisposableGroup();
156
+ const surface = gfx.surface;
157
+ if (!surface)
158
+ return;
159
+ this._disposable.add(surface.elementAdded.subscribe(({ id, local }) => {
160
+ if (local)
161
+ this._restack(surface.getElementById(id));
162
+ }));
163
+ this._disposable.add(surface.elementUpdated.subscribe(({ id, props, local }) => {
164
+ if (local && props['xywh'])
165
+ this._restack(surface.getElementById(id));
166
+ }));
167
+ }
168
+ _restack(model) {
169
+ // A nested element is ordered by its ancestor's index — `compare` reads
170
+ // that and ignores the child's own, so restacking it would change nothing.
171
+ if (!model || model.group !== null)
172
+ return;
173
+ const index = stackingIndexFor({
174
+ index: model.index,
175
+ xywh: model.xywh,
176
+ isBackground: model instanceof FrameworkBackgroundElementModel,
177
+ }, stackedElementsOf(this.gfx, model));
178
+ if (index !== null)
179
+ this.gfx.updateElement(model, { index });
180
+ }
181
+ unmounted() {
182
+ this._disposable.dispose();
183
+ }
184
+ }
@@ -2,6 +2,20 @@ export declare function loadingSort<T extends {
2
2
  id: string;
3
3
  deps: string[];
4
4
  }>(elements: T[]): T[];
5
+ /**
6
+ * Paint order of two serialized elements, group-aware to ANY depth.
7
+ *
8
+ * `groupIndexMap` maps a child id to its parent group's `{ id, index }`. An
9
+ * element's position is the chain of indexes from its outermost group down to
10
+ * itself, and two chains compare lexicographically — the same order the live
11
+ * layer's `compare` derives from the group tree. A group is a strict prefix of
12
+ * its children's chains, so it sorts before them.
13
+ *
14
+ * It used to look ONE level up only, which made the comparator cyclic on a
15
+ * group inside a group (a leaf of the outer group, a leaf of the inner one and
16
+ * the inner group itself had no consistent order) — and `Array.sort` on a
17
+ * cyclic comparator returns whatever it likes.
18
+ */
5
19
  export declare function sortIndex(a: {
6
20
  id: string;
7
21
  index: string;
@@ -11,4 +25,4 @@ export declare function sortIndex(a: {
11
25
  }, groupIndexMap: Map<string, {
12
26
  id: string;
13
27
  index: string;
14
- }>): 0 | 1 | -1;
28
+ }>): number;
@@ -38,37 +38,36 @@ export function loadingSort(elements) {
38
38
  }
39
39
  return sortedOrder.map(id => map.get(id));
40
40
  }
41
+ /**
42
+ * Paint order of two serialized elements, group-aware to ANY depth.
43
+ *
44
+ * `groupIndexMap` maps a child id to its parent group's `{ id, index }`. An
45
+ * element's position is the chain of indexes from its outermost group down to
46
+ * itself, and two chains compare lexicographically — the same order the live
47
+ * layer's `compare` derives from the group tree. A group is a strict prefix of
48
+ * its children's chains, so it sorts before them.
49
+ *
50
+ * It used to look ONE level up only, which made the comparator cyclic on a
51
+ * group inside a group (a leaf of the outer group, a leaf of the inner one and
52
+ * the inner group itself had no consistent order) — and `Array.sort` on a
53
+ * cyclic comparator returns whatever it likes.
54
+ */
41
55
  export function sortIndex(a, b, groupIndexMap) {
42
- const aGroupIndex = groupIndexMap.get(a.id);
43
- const bGroupIndex = groupIndexMap.get(b.id);
44
- if (aGroupIndex && bGroupIndex) {
45
- return aGroupIndex.id === bGroupIndex.id
46
- ? a.index === b.index
47
- ? 0
48
- : a.index > b.index
49
- ? 1
50
- : -1
51
- : aGroupIndex.index > bGroupIndex.index
52
- ? 1
53
- : -1;
54
- }
55
- if (aGroupIndex) {
56
- return aGroupIndex.id === b.id
57
- ? 1
58
- : aGroupIndex.index === b.index
59
- ? 0
60
- : aGroupIndex.index > b.index
61
- ? 1
62
- : -1;
63
- }
64
- if (bGroupIndex) {
65
- return a.id === bGroupIndex.id
66
- ? -1
67
- : a.index === bGroupIndex.index
68
- ? 0
69
- : a.index > bGroupIndex.index
70
- ? 1
71
- : -1;
56
+ const chainOf = (el) => {
57
+ const chain = [el.index];
58
+ const seen = new Set([el.id]);
59
+ for (let group = groupIndexMap.get(el.id); group && !seen.has(group.id); group = groupIndexMap.get(group.id)) {
60
+ seen.add(group.id);
61
+ chain.unshift(group.index);
62
+ }
63
+ return chain;
64
+ };
65
+ const ca = chainOf(a);
66
+ const cb = chainOf(b);
67
+ const depth = Math.min(ca.length, cb.length);
68
+ for (let i = 0; i < depth; i++) {
69
+ if (ca[i] !== cb[i])
70
+ return ca[i] > cb[i] ? 1 : -1;
72
71
  }
73
- return a.index === b.index ? 0 : a.index > b.index ? 1 : -1;
72
+ return ca.length - cb.length;
74
73
  }
@@ -4,6 +4,7 @@ import { literal } from 'lit/static-html.js';
4
4
  import { effects } from './effects.js';
5
5
  import { auditCommands, EdgelessCRUDExtension, EdgelessLegacySlotExtension, EditPropsMiddlewareBuilder, mapQualityCommands, mapQualityWidget, ReadingManager, readingProposalWidget, readingToolbarExtension, SpotlightManager, ValidationManager, validationExceptionToolbarExtension, ValidationOverlay, violationDetailWidget, } from './extensions/index.js';
6
6
  import { ExportManagerExtension } from './extensions/export-manager/export-manager.js';
7
+ import { BackgroundStackingExtension } from './framework-background/stacking.js';
7
8
  import { ElementHighlightOverlay } from './renderer/element-highlight-overlay.js';
8
9
  import { DefaultTool } from './tool/default-tool.js';
9
10
  export class SurfaceViewExtension extends ViewExtensionProvider {
@@ -31,6 +32,10 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
31
32
  // too — embedded read-only windows are the API's primary consumer — and
32
33
  // paints nothing until a host asks for a highlight.
33
34
  context.register(ElementHighlightOverlay);
35
+ // "A framework background is a floor, never a lid" — engine behaviour,
36
+ // so it is registered unconditionally, like the frame manager it mirrors
37
+ // (`docs/adr/0009`: a flag gates tooling, never what a board does).
38
+ context.register(BackgroundStackingExtension);
34
39
  // No-op until a framework registers a SpotlightHostExtension.
35
40
  context.register(SpotlightManager);
36
41
  // No-op until a framework registers a ValidationRuleExtension — which
@@ -1,14 +1,14 @@
1
1
  import { HighLightDuotoneIcon } from '@blocksuite/icons/lit';
2
2
  import { css, LitElement } from 'lit';
3
3
  export class HighlightDuotoneIcon extends LitElement {
4
- static { this.styles = css `
5
- svg {
6
- display: flex;
7
- font-size: 20px;
8
- }
9
- svg > path:nth-child(1) {
10
- fill: var(--color, unset);
11
- }
4
+ static { this.styles = css `
5
+ svg {
6
+ display: flex;
7
+ font-size: 20px;
8
+ }
9
+ svg > path:nth-child(1) {
10
+ fill: var(--color, unset);
11
+ }
12
12
  `; }
13
13
  render() {
14
14
  return HighLightDuotoneIcon();
@@ -1,17 +1,17 @@
1
1
  import { TextBackgroundDuotoneIcon } from '@blocksuite/icons/lit';
2
2
  import { css, LitElement } from 'lit';
3
3
  export class TextDuotoneIcon extends LitElement {
4
- static { this.styles = css `
5
- svg {
6
- display: flex;
7
- font-size: 20px;
8
- }
9
- svg > path:nth-child(1) {
10
- fill: var(--background, unset);
11
- }
12
- svg > path:nth-child(3) {
13
- fill: var(--color, unset);
14
- }
4
+ static { this.styles = css `
5
+ svg {
6
+ display: flex;
7
+ font-size: 20px;
8
+ }
9
+ svg > path:nth-child(1) {
10
+ fill: var(--background, unset);
11
+ }
12
+ svg > path:nth-child(3) {
13
+ fill: var(--color, unset);
14
+ }
15
15
  `; }
16
16
  render() {
17
17
  return TextBackgroundDuotoneIcon();