slidemind 0.3.20 → 0.4.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 (185) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/render.mjs +27 -1
  16. package/src/bridge/client/transport.mjs +499 -23
  17. package/src/bridge/client.mjs +10 -0
  18. package/src/bridge/config.mjs +67 -6
  19. package/src/bridge/engine-download.mjs +0 -10
  20. package/src/bridge/graph-auth.mjs +55 -3
  21. package/src/bridge/image-intent-envelope.mjs +239 -0
  22. package/src/bridge/package-preview.mjs +67 -0
  23. package/src/bridge/sharepoint.mjs +914 -0
  24. package/src/builders/asset-fetch.mjs +203 -0
  25. package/src/builders/block-dividers.mjs +83 -0
  26. package/src/builders/build.mjs +171 -87
  27. package/src/builders/card-height.mjs +294 -0
  28. package/src/builders/chart-hole.mjs +183 -0
  29. package/src/builders/chart-jsx.mjs +410 -0
  30. package/src/builders/chart-labels.mjs +326 -0
  31. package/src/builders/concept-geometry.mjs +370 -0
  32. package/src/builders/concept-intent.mjs +200 -0
  33. package/src/builders/concept-layout.mjs +79 -0
  34. package/src/builders/concept-lines.mjs +509 -0
  35. package/src/builders/concept-lists.mjs +270 -0
  36. package/src/builders/concept-marks.mjs +84 -0
  37. package/src/builders/concept-relationships.mjs +12 -0
  38. package/src/builders/concept-spec.mjs +645 -0
  39. package/src/builders/concept-text.mjs +140 -0
  40. package/src/builders/concept-waves.mjs +57 -0
  41. package/src/builders/copy-fit.mjs +139 -0
  42. package/src/builders/design-tokens.mjs +347 -0
  43. package/src/builders/draft-check.mjs +552 -340
  44. package/src/builders/draft-lines.mjs +369 -0
  45. package/src/builders/edit-route.mjs +29 -0
  46. package/src/builders/figure-sketch.mjs +375 -0
  47. package/src/builders/fold-sets.mjs +639 -0
  48. package/src/builders/gate-server.mjs +9 -4
  49. package/src/builders/gate-session.js +273 -12
  50. package/src/builders/gate-shell.js +1494 -0
  51. package/src/builders/image-intent-check.mjs +308 -0
  52. package/src/builders/image-use-reconcile.mjs +107 -0
  53. package/src/builders/kit-helpers.mjs +121 -0
  54. package/src/builders/layout-names.mjs +32 -0
  55. package/src/builders/legacy-reference-uses.mjs +179 -0
  56. package/src/builders/line-fit.mjs +224 -0
  57. package/src/builders/line-native.mjs +895 -0
  58. package/src/builders/line-relations.mjs +230 -0
  59. package/src/builders/lint-fix.mjs +117 -9
  60. package/src/builders/lint.mjs +25 -20
  61. package/src/builders/list-jsx.mjs +88 -0
  62. package/src/builders/placement.mjs +133 -0
  63. package/src/builders/plan-checks.mjs +135 -0
  64. package/src/builders/plan-comments.mjs +19 -0
  65. package/src/builders/plan-handlers.mjs +95 -0
  66. package/src/builders/plan-markdown-edit.mjs +3 -3
  67. package/src/builders/plan-overlay.js +1530 -1608
  68. package/src/builders/plan.mjs +689 -186
  69. package/src/builders/preset-definitions.json +1 -0
  70. package/src/builders/preset-eval.mjs +244 -0
  71. package/src/builders/preview.html +51 -120
  72. package/src/builders/red-thread-backbone.mjs +135 -36
  73. package/src/builders/repair.mjs +205 -0
  74. package/src/builders/repeat-groups.mjs +540 -0
  75. package/src/builders/resolve-skill.mjs +5 -4
  76. package/src/builders/review-overlay.js +296 -293
  77. package/src/builders/review.mjs +305 -84
  78. package/src/builders/rule-shift.mjs +96 -0
  79. package/src/builders/seat.mjs +354 -0
  80. package/src/builders/settle-driver.mjs +236 -0
  81. package/src/builders/settle.mjs +1448 -0
  82. package/src/builders/slide-prep.mjs +67 -0
  83. package/src/builders/spec-assets.mjs +236 -0
  84. package/src/builders/spec-match.mjs +56 -0
  85. package/src/builders/stack.mjs +413 -0
  86. package/src/builders/storyboard-markdown.mjs +70 -27
  87. package/src/builders/storyboard-schema.mjs +54 -114
  88. package/src/builders/text-blocks.mjs +212 -0
  89. package/src/builders/text-card-fit.mjs +552 -0
  90. package/src/builders/text-stack.mjs +276 -0
  91. package/src/builders/transcribe.mjs +1069 -0
  92. package/src/cli/assets.mjs +164 -0
  93. package/src/cli/common.mjs +173 -12
  94. package/src/cli/concept-spec-support.mjs +76 -0
  95. package/src/cli/concepts.mjs +1002 -103
  96. package/src/cli/deck.mjs +401 -65
  97. package/src/cli/diagnose.mjs +4 -2
  98. package/src/cli/engine-helpers.mjs +17 -10
  99. package/src/cli/engine-results.mjs +76 -3
  100. package/src/cli/help.mjs +232 -28
  101. package/src/cli/install.mjs +9 -0
  102. package/src/cli/maintenance.mjs +104 -0
  103. package/src/cli/read.mjs +2 -1
  104. package/src/cli/ready.mjs +6 -3
  105. package/src/cli/record.mjs +522 -0
  106. package/src/cli/remote.mjs +818 -0
  107. package/src/cli/revision.mjs +686 -0
  108. package/src/cli/search.mjs +30 -11
  109. package/src/cli/session.mjs +56 -13
  110. package/src/cli/setup.mjs +46 -2
  111. package/src/cli/status.mjs +142 -33
  112. package/src/cli/tasks.mjs +246 -0
  113. package/src/cli/transcribe.mjs +854 -0
  114. package/src/cli/workspace-ui.mjs +260 -0
  115. package/src/cli/workspace.mjs +212 -34
  116. package/src/install-channel.mjs +64 -7
  117. package/src/lib/adopt.mjs +346 -25
  118. package/src/lib/concept-spec-binding.mjs +96 -0
  119. package/src/lib/conversions.mjs +70 -6
  120. package/src/lib/convert-folded.mjs +107 -0
  121. package/src/lib/deck-paths.mjs +418 -0
  122. package/src/lib/durable-ids.mjs +47 -0
  123. package/src/lib/edit-operations.mjs +316 -0
  124. package/src/lib/edit-state.mjs +1244 -0
  125. package/src/lib/edit-transitions.mjs +120 -0
  126. package/src/lib/extensions.mjs +501 -0
  127. package/src/lib/image-assets.mjs +465 -0
  128. package/src/lib/image-derivatives.mjs +234 -0
  129. package/src/lib/image-intent-fingerprint.mjs +70 -0
  130. package/src/lib/image-intent-tags.mjs +176 -0
  131. package/src/lib/image-intent.mjs +802 -0
  132. package/src/lib/image-use-overrides.mjs +279 -0
  133. package/src/lib/inline-components.mjs +613 -0
  134. package/src/lib/lint-converted.mjs +68 -4
  135. package/src/lib/merge-json.mjs +163 -0
  136. package/src/lib/overwrite-guard.mjs +3 -3
  137. package/src/lib/presentation-context.mjs +531 -0
  138. package/src/lib/preservation-tags.mjs +157 -0
  139. package/src/lib/preservation-verdict.mjs +1342 -0
  140. package/src/lib/publish.mjs +1557 -0
  141. package/src/lib/rebase.mjs +945 -0
  142. package/src/lib/reconcile-ack.mjs +4 -6
  143. package/src/lib/reconcile.mjs +62 -24
  144. package/src/lib/record-client.mjs +175 -0
  145. package/src/lib/record-state.mjs +151 -0
  146. package/src/lib/remote.mjs +661 -0
  147. package/src/lib/revision.mjs +1210 -0
  148. package/src/lib/slide-guids.mjs +4 -6
  149. package/src/lib/slide-preservation.mjs +423 -0
  150. package/src/lib/slide-selection.mjs +9 -0
  151. package/src/lib/workspace-transaction.mjs +117 -0
  152. package/src/pptx/content-digest.mjs +334 -0
  153. package/src/pptx/deck-assemble.mjs +1278 -0
  154. package/src/pptx/fill/engine.mjs +2 -3
  155. package/src/pptx/fill/freeform.mjs +6 -3
  156. package/src/pptx/ooxml.mjs +20 -0
  157. package/src/pptx/package-evidence.mjs +383 -0
  158. package/src/pptx/tags.mjs +163 -11
  159. package/src/update-check.mjs +22 -4
  160. package/src/workspace/adapters.mjs +222 -0
  161. package/src/workspace/auth.mjs +256 -0
  162. package/src/workspace/autostart.mjs +204 -0
  163. package/src/workspace/daemon.mjs +360 -0
  164. package/src/workspace/discovery.mjs +180 -0
  165. package/src/workspace/edit.mjs +167 -0
  166. package/src/workspace/events.mjs +52 -0
  167. package/src/workspace/gate-client.mjs +290 -0
  168. package/src/workspace/handback.mjs +73 -0
  169. package/src/workspace/lifecycle.mjs +635 -0
  170. package/src/workspace/operations.mjs +200 -0
  171. package/src/workspace/pick-folder.mjs +152 -0
  172. package/src/workspace/registry.mjs +359 -0
  173. package/src/workspace/remote-links.mjs +397 -0
  174. package/src/workspace/remote-status.mjs +359 -0
  175. package/src/workspace/routes.mjs +1331 -0
  176. package/src/workspace/server.mjs +159 -0
  177. package/src/workspace/sync.mjs +1119 -0
  178. package/src/workspace/task-actions.mjs +215 -0
  179. package/src/workspace/tasks.mjs +507 -0
  180. package/src/workspace/ui/app.js +1886 -0
  181. package/src/workspace/ui/index.html +141 -0
  182. package/src/workspace/ui/plan-bridge.js +120 -0
  183. package/src/workspace/ui/sharepoint.svg +59 -0
  184. package/src/workspace/ui/styles.css +423 -0
  185. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,276 @@
1
+ // A text set at the design's ramp often takes more lines, or taller ones, than
2
+ // the concept drew, and grows into what is stacked beneath it in its column:
3
+ // a title into its body by a few px, a body into the next row, a step number
4
+ // set at the number rung into its label (parity decks: 71 overlap warnings on
5
+ // 25 slides, most of them title-into-body). The stack beneath moves down by
6
+ // what the text took, as one: inside a card everything below it in its column
7
+ // (with the rows it shares and the lines lying on them), on no card the chain
8
+ // of texts stacked on each other. It moves only into free room — the next
9
+ // obstacle, the card's bottom, the slot's — and a card grows only through
10
+ // text-card-fit's own height lever. A small marker (a step number, 1–3
11
+ // characters) lifts clear of its label instead, where it has the room above
12
+ // it (live 6bb93436: "2" over "Plan steps"). The type never changes, a text never
13
+ // leaves its card, nothing moves into a line it did not cross before.
14
+ // Pure helpers over text-card-fit's view of the slide (`env`): which stack a
15
+ // text runs into and how far that stack may go. The levers stay there.
16
+ import { fixedAspect } from "./concept-layout.mjs";
17
+ import { gluedIds, holdsDrawing, linePoints, lineRows, linesWithin, repeatGroups } from "./repeat-groups.mjs";
18
+
19
+ const GAP = 4; // px between ink and what is stacked under it, at least
20
+ const STACK_GAP = 12; // px: texts closer than this are one stack (a title on its body)
21
+ const INSET = 8; // px kept inside a card's edge (text-card-fit)
22
+ const MARKER_CHARS = 3; // a step number, a badge digit: "1", "12", "120"
23
+ const LOOKALIKE_W = 0.1; // an undeclared lookalike differs by at most 10% in width…
24
+ const LOOKALIKE_H = 0.15; // …and 15% in height
25
+
26
+ const span = (a0, a1, b0, b1) => a0 < b1 && b0 < a1;
27
+
28
+ /** A marker: a number of at most three visible characters (a step number over its label). */
29
+ export const isMarker = (e) => (e.kind === "number" || e.role === "number") && String(e.text ?? "").replace(/\s+/g, "").length <= MARKER_CHARS;
30
+
31
+ /**
32
+ * `env`: { s, byId, measured, slot, at(t) → rendered box {y,h} with this
33
+ * call's moves, img(o) → the rect the call started from, isText, filled,
34
+ * subtree(o), inTree(el, root) }.
35
+ */
36
+ /** Where a text's glyphs are, with this call's moves: its measured ink, else its box. */
37
+ export const inkOf = (env, t) => {
38
+ const m = env.measured.get(t.id);
39
+ const box = env.at(t);
40
+ if (!(m?.inkHeight > 0) || !Number.isFinite(m.inkY)) return { top: box.y, bottom: box.y + box.h };
41
+ const top = m.inkY + (box.y - (m.y ?? box.y));
42
+ return { top, bottom: top + m.inkHeight };
43
+ };
44
+ const topOf = (env, o) => (env.isText(o) ? inkOf(env, o).top : o.y);
45
+ const boxOf = (env, o) => (env.isText(o) ? { x: o.x, y: env.at(o).y, w: o.w, h: env.at(o).h } : { x: o.x, y: o.y, w: o.w, h: o.h });
46
+
47
+ const filledOf = (e) => Boolean(e?.fill && e.fill !== "none");
48
+ // A text box; words ON a painted shape (a number badge) belong to that shape.
49
+ const isTextBox = (e) => (e.kind === "text" || e.kind === "number") && e.text != null && !e.textRotation && !filledOf(e);
50
+ export const isCard = (e) => filledOf(e) && !fixedAspect(e) && !isTextBox(e);
51
+ export const isRound = (e) => filledOf(e) && fixedAspect(e) && !e.geometry && !isTextBox(e);
52
+
53
+ /** The declared sets (repeat-groups.mjs) that move as one: built from one template, or holding no drawing. */
54
+ export const declaredSets = (s) => repeatGroups(s).filter((g) => g.built || !g.members.some((m) => holdsDrawing(s, m)));
55
+ const sig = (s, el) => [el.kind, el.fill, el.shape ?? "", Boolean(el.shadow), s.elements.filter((k) => k.parent === el.id).map((k) => k.kind).sort().join(",")].join("|");
56
+ /**
57
+ * Symmetry nobody declared is still symmetry: `o` looks like `card` (the same
58
+ * kind of shape and content on the same parent, within 10% of its width and
59
+ * 15% of its height), so neither changes on its own.
60
+ */
61
+ export const looksLike = (s, o, card) => o !== card && (isCard(card) ? isCard(o) : isRound(o)) && o.parent === card.parent && sig(s, o) === sig(s, card)
62
+ && Math.abs(o.w - card.w) <= card.w * LOOKALIKE_W && Math.abs(o.h - card.h) <= card.h * LOOKALIKE_H;
63
+
64
+ /**
65
+ * Whether text-card-fit's stack lever (1a) moves what lies under `t` on its
66
+ * own: t on no card, or on a card of no declared set and with no undeclared
67
+ * lookalike — a set moves alike or not at all, a lookalike never alone.
68
+ */
69
+ export function stacksAlone(s, t) {
70
+ const byId = new Map(s.elements.map((e) => [e.id, e]));
71
+ const parent = t.parent ? byId.get(t.parent) : null;
72
+ if (parent && filledOf(parent)) {
73
+ return isCard(parent) && !declaredSets(s).some((g) => g.members.includes(parent)) && !s.elements.some((o) => looksLike(s, o, parent));
74
+ }
75
+ for (let p = parent, g = 0; p && g < 32; p = p.parent ? byId.get(p.parent) : null, g++) if (filledOf(p)) return false;
76
+ return true;
77
+ }
78
+
79
+ /** The slide as a render measured it, nothing moved yet: `env` for a caller outside text-card-fit. */
80
+ export function stackEnv(s, measured, slot = s.slot) {
81
+ const byId = new Map(s.elements.map((e) => [e.id, e]));
82
+ const kids = (el) => s.elements.filter((o) => o.parent === el.id);
83
+ const subtree = (el) => [el, ...kids(el).flatMap(subtree)];
84
+ const inTree = (el, root) => {
85
+ for (let p = el, guard = 0; p && guard < 32; p = p.parent ? byId.get(p.parent) : null, guard++) if (p === root) return true;
86
+ return false;
87
+ };
88
+ const isText = isTextBox;
89
+ const at = (t) => {
90
+ const m = measured.get(t.id);
91
+ return m?.h > 0 ? { y: m.y ?? t.y, h: m.h } : { y: t.y, h: t.h };
92
+ };
93
+ return { s, byId, measured, slot, at, img: (o) => o, isText, filled: filledOf, subtree, inTree };
94
+ }
95
+
96
+ /** The columns a text's words take: its box, and its ink where that runs past it. */
97
+ export function columnOf(env, t) {
98
+ const m = env.measured.get(t.id);
99
+ const ink = m?.inkWidth > t.w ? m.inkWidth : t.w;
100
+ const x0 = t.align === "center" ? t.x + t.w / 2 - ink / 2 : t.align === "right" ? t.x + t.w - ink : t.x;
101
+ return [x0, x0 + ink];
102
+ }
103
+
104
+ /**
105
+ * The stack `t` runs into, or null. `need` is how far that stack moves for t's
106
+ * ink to end the image's gap above it (GAP at least, STACK_GAP at most),
107
+ * `least` how far it must move for the two only to stop touching; `force`
108
+ * returns the stack even where t does not reach it (a set member moving with
109
+ * its set, a line about to wrap), `want` the px it should make room for.
110
+ * `members` move (subtrees included), `lines` move with them, `room` is how
111
+ * far they can go down without growing the card.
112
+ */
113
+ export function stackUnder(env, t, { force = false, want = 0 } = {}) {
114
+ const { s, byId, img, isText, filled } = env;
115
+ const parent = t.parent ? byId.get(t.parent) : null;
116
+ // A disc's content is text-card-fit's round lever's (0).
117
+ if (parent && fixedAspect(parent)) return null;
118
+ const card = parent && filled(parent) ? parent : null;
119
+ const it = img(t);
120
+ const [x0, x1] = columnOf(env, t);
121
+ // On no card only texts stack: a disc or a panel under a free text is an
122
+ // organism of its own, never pushed for it.
123
+ const pool = s.elements.filter((o) => o !== t && (o.parent ?? null) === (t.parent ?? null) && o.w > 0 && o.h > 0 && !(isText(o) && o.textRotation) && (card || isText(o)));
124
+ const below = (y) => pool.filter((o) => img(o).y >= y && span(img(o).x, img(o).x + img(o).w, x0 + 1, x1 - 1)).sort((a, b) => img(a).y - img(b).y)[0];
125
+ const first = below(it.y + it.h - 2);
126
+ if (!first) return null;
127
+ const tInk = inkOf(env, t);
128
+ // Only ink that grew past the text's box and now lies on what is under it:
129
+ // the concept's own spacing stands wherever the two do not touch.
130
+ if (!force && (tInk.bottom <= it.y + it.h + 1 || tInk.bottom <= topOf(env, first) + 0.5)) return null;
131
+ // A rule between the text and what is under it is text-card-fit's divider
132
+ // lever (1b): moving the stack past it would leave the text on the rule.
133
+ const firstTop = topOf(env, first);
134
+ const ruled = (s.connectors ?? []).some((c) => !gluedIds(c).length && linePoints(c).some((p, i, pts) => i > 0
135
+ && span(Math.min(p.x, pts[i - 1].x), Math.max(p.x, pts[i - 1].x) + 1, x0, x1)
136
+ && Math.min(p.y, pts[i - 1].y) > it.y + it.h / 2 && Math.max(p.y, pts[i - 1].y) < firstTop));
137
+ if (ruled) return null;
138
+ const gap = Math.min(STACK_GAP, Math.max(GAP, img(first).y - (it.y + it.h)));
139
+ const need = Math.ceil(tInk.bottom + gap - topOf(env, first));
140
+ const least = Math.ceil(tInk.bottom + 1 - topOf(env, first));
141
+ if (!force && least < 1) return null;
142
+ // What moves: the chain stacked on `first` (within STACK_GAP of each other
143
+ // in the image; on a card with what shares their rows), then — while that has
144
+ // less room than it needs — on a card the next chain under it, a rule
145
+ // between them included; on no card the next text under it. Only as much
146
+ // of the column as has to move.
147
+ const from = img(first).y - 1;
148
+ const stack = [];
149
+ const chain = (seed) => {
150
+ stack.push(seed);
151
+ for (let grew = true; grew;) {
152
+ grew = false;
153
+ for (const o of pool) {
154
+ if (stack.includes(o) || img(o).y < from) continue;
155
+ const r = img(o);
156
+ // A disc, an icon or a rule beside a moving text rides with it; a
157
+ // text beside it heads a column of its own.
158
+ const onRow = card && !isText(o) && stack.some((k) => span(img(k).y, img(k).y + img(k).h, r.y, r.y + r.h));
159
+ const chained = stack.some((k) => span(img(k).x, img(k).x + img(k).w, r.x, r.x + r.w) && r.y >= img(k).y && r.y - (img(k).y + img(k).h) <= STACK_GAP);
160
+ if (onRow || chained) {
161
+ stack.push(o);
162
+ grew = true;
163
+ }
164
+ }
165
+ }
166
+ };
167
+ chain(first);
168
+ let at = reach(env, t, stack, card);
169
+ const passed = []; // [stack length before it, line]
170
+ while (at.room < Math.max(need, want) && at.blocker) {
171
+ const b = at.blocker;
172
+ const next = b.line ? (card ? below(b.y) : null) : pool.includes(b) && (card || isText(b)) ? b : null;
173
+ if (!next || stack.includes(next)) break;
174
+ if (b.line) passed.push([stack.length, b.line]);
175
+ chain(next);
176
+ at = reach(env, t, stack, card);
177
+ }
178
+ // Past a line only where the line moves with the stack (a rule between two
179
+ // of its rows); one that runs on past it (a card-wide divider, a timeline)
180
+ // stays, and the stack above it stops there.
181
+ for (let bad; (bad = passed.find(([n, line]) => n < stack.length && !at.lines.includes(line)));) {
182
+ stack.length = bad[0];
183
+ at = reach(env, t, stack, card);
184
+ }
185
+ return { t, first, need, least, gap, card, ...at };
186
+ }
187
+
188
+ const bottomOf = (env, o) => (env.isText(o) ? inkOf(env, o).bottom : o.y + o.h);
189
+
190
+ // What a stack moves (subtrees included), the lines that go with it, and how
191
+ // far down it may go, glyphs measured as glyphs: its card's inner bottom (a
192
+ // margin under INSET is kept, never taken), what lies under it in its columns
193
+ // — an element, or a line that does not move with it, member by member (a
194
+ // divider between two of its rows holds the one above it); the nearest is the
195
+ // `blocker` — a painted panel it lies on, and the slot, box for box.
196
+ function reach(env, t, stack, card) {
197
+ const { s, isText, filled, subtree, inTree, slot } = env;
198
+ const members = [...new Set(stack.flatMap(subtree))];
199
+ const ids = new Set(members.map((o) => o.id));
200
+ const rects = stack.map((o) => boxOf(env, o));
201
+ const sx0 = Math.min(...rects.map((r) => r.x));
202
+ const sx1 = Math.max(...rects.map((r) => r.x + r.w));
203
+ const sTop = Math.min(...members.map((o) => (isText(o) ? env.at(o).y : o.y)));
204
+ const boxBottom = Math.max(...members.map((o) => (isText(o) ? env.at(o).y + env.at(o).h : o.y + o.h)));
205
+ const inkBottom = Math.max(...members.map((o) => bottomOf(env, o)));
206
+ // A line moves with the stack when it is glued only to what moves, or lies
207
+ // free inside the stack's rows and columns (a rule between two of its rows)
208
+ // or, on a card, inside the card (a row's divider wider than the stack).
209
+ const cardLines = new Set(card ? linesWithin(s, card) : []);
210
+ const lines = (s.connectors ?? []).filter((c) => {
211
+ const glued = gluedIds(c);
212
+ if (glued.length) return glued.every((id) => ids.has(id));
213
+ const pts = linePoints(c);
214
+ return pts.length >= 2 && pts.every((p) => p.y >= sTop - 2 && p.y <= boxBottom + 2)
215
+ && (cardLines.has(c) || pts.every((p) => p.x >= sx0 - 2 && p.x <= sx1 + 2));
216
+ });
217
+ let room = slot.h - 1 - boxBottom;
218
+ let blocker = null;
219
+ const limit = (px, by = null) => {
220
+ if (px < room) [room, blocker] = [px, by];
221
+ };
222
+ if (card) limit(card.y + card.h - Math.min(INSET, Math.max(0, card.y + card.h - inkBottom)) - inkBottom);
223
+ const free = lines.filter((c) => !gluedIds(c).length).map((c) => {
224
+ const pts = linePoints(c);
225
+ return { x0: Math.min(...pts.map((p) => p.x)), x1: Math.max(...pts.map((p) => p.x)), y: Math.max(...pts.map((p) => p.y)) };
226
+ });
227
+ for (const o of s.elements) {
228
+ if (ids.has(o.id) || o === t || !(o.w > 0 && o.h > 0) || inTree(t, o) || members.some((k) => inTree(o, k))) continue;
229
+ const r = boxOf(env, o);
230
+ // A line moving with the stack stops short of what lies under it in its
231
+ // own columns (the next column's text under a card-wide divider).
232
+ for (const l of free) if (span(r.x, r.x + r.w, l.x0, l.x1) && topOf(env, o) > l.y + 1) limit(topOf(env, o) - GAP - l.y, o);
233
+ if (!span(r.x, r.x + r.w, sx0, sx1)) continue;
234
+ // Beneath is anything starting below the stack's top, already under its
235
+ // glyphs or not (a callout band a body has run onto blocks it too).
236
+ const top = topOf(env, o);
237
+ if (top > sTop + 1) limit(top - (isText(o) ? 1 : GAP) - inkBottom, o);
238
+ else if (!card && filled(o) && r.y < sTop && r.x <= sx0 + 1 && r.x + r.w >= sx1 - 1 && r.y + r.h >= inkBottom) {
239
+ limit(r.y + r.h - Math.min(INSET, Math.max(0, r.y + r.h - inkBottom)) - inkBottom);
240
+ }
241
+ }
242
+ const moving = new Set(lines);
243
+ for (const o of members) {
244
+ if (!(o.w > 0 && o.h > 0)) continue;
245
+ const bottom = bottomOf(env, o);
246
+ for (const l of lineRows(s, ...(isText(o) ? columnOf(env, o) : [o.x, o.x + o.w]), ids)) {
247
+ if (!moving.has(l.line) && l.y >= bottom - 1) limit(l.y - 1 - bottom, { line: l.line, y: l.y });
248
+ }
249
+ }
250
+ return { members, lines, blocker, room: Math.floor(room) };
251
+ }
252
+
253
+ /**
254
+ * How far a marker can lift clear of its label: `up` px free above its ink in
255
+ * its columns (its card's inset, whatever sits above it, a line drawn over it).
256
+ */
257
+ export function markerRoom(env, t) {
258
+ const { s, byId, isText, filled, inTree } = env;
259
+ const parent = t.parent ? byId.get(t.parent) : null;
260
+ const card = parent && filled(parent) ? parent : null;
261
+ const ink = inkOf(env, t);
262
+ const [x0, x1] = columnOf(env, t);
263
+ let ceil = card ? card.y + Math.min(INSET, Math.max(0, ink.top - card.y)) : 0;
264
+ for (const o of s.elements) {
265
+ if (o === t || !(o.w > 0 && o.h > 0) || inTree(t, o) || inTree(o, t)) continue;
266
+ const r = boxOf(env, o);
267
+ if (!span(r.x, r.x + r.w, x0, x1)) continue;
268
+ const bottom = isText(o) ? inkOf(env, o).bottom : r.y + r.h;
269
+ if (bottom <= ink.top + 1) ceil = Math.max(ceil, bottom + GAP);
270
+ }
271
+ for (const l of lineRows(s, x0, x1, new Set(env.subtree(t).map((o) => o.id)))) {
272
+ if (l.y + l.h <= ink.top + 1) ceil = Math.max(ceil, l.y + l.h + GAP);
273
+ }
274
+ // Its box, not only its ink, stays inside the slot (slot-fit).
275
+ return { up: Math.floor(Math.min(ink.top - ceil, env.at(t).y)) };
276
+ }