slidemind 0.3.20 → 0.4.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// A STACK the reader declares (`spec.stacks`, concept-stacks.ts): rows of one
|
|
2
|
+
// structure repeated down a container — an icon disc, its text and the divider
|
|
3
|
+
// under them, four times; steps along a timeline — each row listed as the ids
|
|
4
|
+
// of its parts, with how a row's parts sit on the row's axis (`align`) and how
|
|
5
|
+
// the rows are spaced (`spacing`: even gaps, or the gaps as drawn). Code builds
|
|
6
|
+
// exactly that and nothing more: a row is laid out from its parts' CURRENT
|
|
7
|
+
// sizes (a text's measured height), the rows are placed in the band the stack
|
|
8
|
+
// owns inside its parent (what lies above and below the whole stack bounds it,
|
|
9
|
+
// recorded once from the painting), and a divider listed with a row moves
|
|
10
|
+
// with that row. Nothing here decides that a structure is a stack or that a
|
|
11
|
+
// part is centred; a stack the reader did not declare changes nothing. This
|
|
12
|
+
// replaced seating each row "middle" against the whole card (x101: four rows
|
|
13
|
+
// collapsed onto the card's centre, their dividers through the text).
|
|
14
|
+
import { translateElement } from "./concept-geometry.mjs";
|
|
15
|
+
import { linePoints, linesWithin, moveLine, fixCopies, translateFigure } from "./repeat-groups.mjs";
|
|
16
|
+
|
|
17
|
+
const half = (v) => Math.round(v * 2) / 2;
|
|
18
|
+
const isText = (e) => (e.kind === "text" || e.kind === "number") && e.text != null;
|
|
19
|
+
|
|
20
|
+
const horizontal = (c) => {
|
|
21
|
+
const pts = linePoints(c);
|
|
22
|
+
if (pts.length < 2) return null;
|
|
23
|
+
const ys = pts.map((p) => p.y);
|
|
24
|
+
if (Math.max(...ys) - Math.min(...ys) > 2) return null;
|
|
25
|
+
return { y: (Math.min(...ys) + Math.max(...ys)) / 2, x0: Math.min(...pts.map((p) => p.x)), x1: Math.max(...pts.map((p) => p.x)) };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Point the stack's rows at what survived normalization (a text block's lines
|
|
30
|
+
* folded into one element, a label into its shape, a rule into its line); a
|
|
31
|
+
* stack whose row lost every part is dropped with a note. `before`: the
|
|
32
|
+
* elements by id before normalization.
|
|
33
|
+
*/
|
|
34
|
+
export function remapStacks(spec, before) {
|
|
35
|
+
if (!spec.stacks?.length) return [];
|
|
36
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
37
|
+
const lines = new Set((spec.connectors ?? []).map((c) => c.id).filter(Boolean));
|
|
38
|
+
const into = new Map();
|
|
39
|
+
for (const e of spec.elements ?? []) for (const p of e.paragraphs ?? []) if (p.id && p.id !== e.id) into.set(p.id, e.id);
|
|
40
|
+
const resolve = (id) => {
|
|
41
|
+
if (byId.has(id) || lines.has(id)) return id;
|
|
42
|
+
if (into.has(id)) return into.get(id);
|
|
43
|
+
const old = before.get(id);
|
|
44
|
+
const words = String(old?.text ?? "").trim();
|
|
45
|
+
const p = old?.parent ? byId.get(old.parent) : null;
|
|
46
|
+
return words && p && String(p.text ?? "").includes(words) ? p.id : null;
|
|
47
|
+
};
|
|
48
|
+
const notes = [];
|
|
49
|
+
spec.stacks = spec.stacks.flatMap((g) => {
|
|
50
|
+
const rows = g.rows.map((row) => [...new Set(row.map(resolve).filter(Boolean))]);
|
|
51
|
+
if (rows.some((r) => !r.length)) {
|
|
52
|
+
notes.push(`${g.id}: a row did not survive normalization — kept as drawn`);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
// A part in two rows (a text block folded across them) is no row's part.
|
|
56
|
+
if (new Set(rows.flat()).size < rows.flat().length) {
|
|
57
|
+
notes.push(`${g.id}: a part belongs to two of its rows after normalization — kept as drawn`);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return [{ ...g, rows, parent: g.parent && byId.has(g.parent) ? g.parent : null }];
|
|
61
|
+
});
|
|
62
|
+
return notes;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The parts of a row, as elements and as lines.
|
|
66
|
+
function partsOf(spec, row) {
|
|
67
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
68
|
+
const byLine = new Map((spec.connectors ?? []).filter((c) => c.id).map((c) => [c.id, c]));
|
|
69
|
+
return { elements: row.map((id) => byId.get(id)).filter(Boolean), lines: row.map((id) => byLine.get(id)).filter(Boolean) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A row's current extent along y, elements by `at`, lines by their points.
|
|
73
|
+
export function rowExtent(spec, row, at) {
|
|
74
|
+
const { elements, lines } = partsOf(spec, row);
|
|
75
|
+
const ys = [
|
|
76
|
+
...elements.flatMap((e) => { const r = { ...e, ...at(e) }; return [r.y, r.y + r.h]; }),
|
|
77
|
+
...lines.flatMap((c) => linePoints(c).map((p) => p.y)),
|
|
78
|
+
];
|
|
79
|
+
return ys.length ? { top: Math.min(...ys), bottom: Math.max(...ys) } : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A block may not span the rows of a declared stack: the row is the finer
|
|
84
|
+
* structure, and a text block folding a title with the bodies of three rows
|
|
85
|
+
* would make one element of three rows (x105 QBR 4: the reader declared both).
|
|
86
|
+
* The texts of such a block keep their block only within one row; a text in
|
|
87
|
+
* no row (the title over the rows) keeps nothing to fold with. Runs before
|
|
88
|
+
* the folds. Returns the ids unblocked.
|
|
89
|
+
*/
|
|
90
|
+
export function unblockStackRows(spec) {
|
|
91
|
+
const rowOf = new Map();
|
|
92
|
+
for (const g of spec.stacks ?? []) g.rows.forEach((row, i) => { for (const id of row) rowOf.set(id, `${g.id}:${i}`); });
|
|
93
|
+
if (!rowOf.size) return [];
|
|
94
|
+
const blocks = new Map();
|
|
95
|
+
for (const e of spec.elements ?? []) if (e.block) blocks.set(e.block, [...(blocks.get(e.block) ?? []), e]);
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const members of blocks.values()) {
|
|
98
|
+
const rows = new Set(members.map((e) => rowOf.get(e.id)).filter(Boolean));
|
|
99
|
+
if (rows.size < 2 && !(rows.size === 1 && members.some((e) => !rowOf.has(e.id)))) continue;
|
|
100
|
+
for (const e of members) { delete e.block; out.push(e.id); }
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What bounds the stack in its parent, read once from the painting: the
|
|
107
|
+
* siblings (not parts of the stack) that lie wholly above or below it in its
|
|
108
|
+
* columns, and the dividers inside the parent that are no row's part. Stored
|
|
109
|
+
* on the stack as `band`; measured from where those are NOW at every pass.
|
|
110
|
+
*/
|
|
111
|
+
export function recordStackBand(spec, g) {
|
|
112
|
+
const els = spec.elements ?? [];
|
|
113
|
+
const byId = new Map(els.map((e) => [e.id, e]));
|
|
114
|
+
const parts = new Set(g.rows.flat());
|
|
115
|
+
const p = g.parent ? byId.get(g.parent) : null;
|
|
116
|
+
const rows = g.rows.map((r) => rowExtent(spec, r, (o) => o)).filter(Boolean);
|
|
117
|
+
if (!rows.length) return null;
|
|
118
|
+
const top = Math.min(...rows.map((r) => r.top));
|
|
119
|
+
const bottom = Math.max(...rows.map((r) => r.bottom));
|
|
120
|
+
const members = g.rows.flat().map((id) => byId.get(id)).filter(Boolean);
|
|
121
|
+
const x0 = Math.min(...members.map((e) => e.x));
|
|
122
|
+
const x1 = Math.max(...members.map((e) => e.x + e.w));
|
|
123
|
+
const spans = (o) => o.x < x1 && x0 < o.x + o.w;
|
|
124
|
+
const sibs = els.filter((o) => !parts.has(o.id) && o.w > 0 && o.h > 0 && o.kind !== "rule" && (p ? o.parent === p.id : !o.parent) && o !== p);
|
|
125
|
+
// A rule between two paragraphs of one block is that block's (block-dividers.mjs), never a gap's.
|
|
126
|
+
const blockDividers = new Set(els.flatMap((e) => (e.dividers ?? []).map((d) => d.id)));
|
|
127
|
+
const seps = (p ? linesWithin(spec, p) : spec.connectors ?? []).filter((c) => c.id && !parts.has(c.id) && !blockDividers.has(c.id)).map((c) => ({ c, l: horizontal(c) })).filter((x) => x.l && x.l.x0 < x1 && x0 < x.l.x1);
|
|
128
|
+
const band = {
|
|
129
|
+
before: sibs.filter((o) => o.y + o.h <= top + 0.5 && spans(o)).map((o) => o.id),
|
|
130
|
+
after: sibs.filter((o) => o.y >= bottom - 0.5 && spans(o)).map((o) => o.id),
|
|
131
|
+
sepBefore: seps.filter((x) => x.l.y <= top + 0.5).sort((a, b) => b.l.y - a.l.y)[0]?.c.id ?? null,
|
|
132
|
+
sepAfter: seps.filter((x) => x.l.y >= bottom - 0.5).sort((a, b) => a.l.y - b.l.y)[0]?.c.id ?? null,
|
|
133
|
+
// The drawn gaps between rows (spacing "drawn") …
|
|
134
|
+
gaps: rows.slice(1).map((r, i) => r.top - rows[i].bottom),
|
|
135
|
+
lead: 0,
|
|
136
|
+
// … and each listed divider's drawn distance under its row's content, kept
|
|
137
|
+
// at every pass (a text that grew over its divider pushes it down).
|
|
138
|
+
lines: Object.fromEntries(g.rows.flatMap((row) => {
|
|
139
|
+
const { elements, lines } = partsOf(spec, row);
|
|
140
|
+
if (!elements.length) return [];
|
|
141
|
+
const bottom = Math.max(...elements.map((e) => e.y + e.h));
|
|
142
|
+
return lines.map((c) => { const ys = linePoints(c).map((p) => p.y); return [c.id, { lo: Math.min(...ys) - bottom, hi: Math.max(...ys) - bottom }]; });
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
// … and the first row's drawn offset from the band's top.
|
|
146
|
+
band.lead = top - stackBand(spec, { ...g, band }).top;
|
|
147
|
+
// What is drawn BETWEEN two rows (a divider, a down-arrow) and listed with
|
|
148
|
+
// no row belongs to that gap: the rows the reader declared define the gap,
|
|
149
|
+
// and an element of the same parent lying inside it, across the stack's
|
|
150
|
+
// columns, is in it by containment — the same fact as `parent`, no symmetry
|
|
151
|
+
// read into it. It rides with its gap, at its drawn fraction of it (a
|
|
152
|
+
// divider in the middle of the gap stays in the middle).
|
|
153
|
+
const inGap = (y0, y1) => rows.findIndex((r, i) => i < rows.length - 1 && y0 >= r.bottom - 0.5 && y1 <= rows[i + 1].top + 0.5);
|
|
154
|
+
const frac = (i, y0, y1) => { const a = rows[i].bottom, b = rows[i + 1].top; return b > a ? ((y0 + y1) / 2 - a) / (b - a) : 0.5; };
|
|
155
|
+
band.between = [
|
|
156
|
+
...sibs.filter((o) => spans(o)).map((o) => ({ o, i: inGap(o.y, o.y + o.h) })).filter((x) => x.i >= 0).map((x) => ({ id: x.o.id, line: false, gap: x.i, frac: frac(x.i, x.o.y, x.o.y + x.o.h) })),
|
|
157
|
+
...(p ? linesWithin(spec, p) : spec.connectors ?? []).filter((c) => c.id && !parts.has(c.id) && !blockDividers.has(c.id)).map((c) => {
|
|
158
|
+
const pts = linePoints(c);
|
|
159
|
+
const xs = pts.map((q) => q.x), ys = pts.map((q) => q.y);
|
|
160
|
+
if (!pts.length || !(Math.min(...xs) < x1 && x0 < Math.max(...xs))) return null;
|
|
161
|
+
const i = inGap(Math.min(...ys), Math.max(...ys));
|
|
162
|
+
return i >= 0 ? { id: c.id, line: true, gap: i, frac: frac(i, Math.min(...ys), Math.max(...ys)) } : null;
|
|
163
|
+
}).filter(Boolean),
|
|
164
|
+
];
|
|
165
|
+
return band;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The band the stack owns now: its parent's box cut by what lies beyond the stack. */
|
|
169
|
+
export function stackBand(spec, g, at = (o) => o) {
|
|
170
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
171
|
+
const p = g.parent ? byId.get(g.parent) : null;
|
|
172
|
+
const slot = spec.slot;
|
|
173
|
+
let lo = p ? p.y : 0;
|
|
174
|
+
let hi = p ? p.y + p.h : slot.h;
|
|
175
|
+
const b = g.band;
|
|
176
|
+
if (!b) return { top: lo, bottom: hi };
|
|
177
|
+
const rect = (o) => ({ y: o.y, h: o.h, ...at(o) });
|
|
178
|
+
for (const id of b.before ?? []) { const o = byId.get(id); if (o) lo = Math.max(lo, rect(o).y + rect(o).h); }
|
|
179
|
+
for (const id of b.after ?? []) { const o = byId.get(id); if (o) hi = Math.min(hi, rect(o).y); }
|
|
180
|
+
const line = (id) => (id == null ? null : (spec.connectors ?? []).find((c) => c.id === id) ?? null);
|
|
181
|
+
const la = line(b.sepBefore) ? horizontal(line(b.sepBefore)) : null;
|
|
182
|
+
const lb = line(b.sepAfter) ? horizontal(line(b.sepAfter)) : null;
|
|
183
|
+
if (la) lo = Math.max(lo, la.y);
|
|
184
|
+
if (lb) hi = Math.min(hi, lb.y);
|
|
185
|
+
return hi - lo > 1 ? { top: lo, bottom: hi } : { top: p ? p.y : 0, bottom: p ? p.y + p.h : slot.h };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build every declared stack: rows placed in the stack's band — equal gaps and
|
|
190
|
+
* margins (`even`), or the drawn lead and gaps (`drawn`) — each row's parts set
|
|
191
|
+
* on the row's axis by `align` (a line listed with a row moves with it, never
|
|
192
|
+
* re-aligned). `at(o)` gives a text's measured rect; `move(o, dy)` moves one
|
|
193
|
+
* element (the caller's, so a settle pass records the shift). Returns one
|
|
194
|
+
* line per row moved.
|
|
195
|
+
*/
|
|
196
|
+
export function applyStacks(spec, { at = (o) => o, move = null } = {}) {
|
|
197
|
+
const els = spec.elements ?? [];
|
|
198
|
+
const byId = new Map(els.map((e) => [e.id, e]));
|
|
199
|
+
const figures = new Set((spec.sketch?.figures ?? []).map((f) => f.id));
|
|
200
|
+
const deltas = new Map();
|
|
201
|
+
const movedCircles = new Set();
|
|
202
|
+
const kids = (id) => els.filter((o) => o.parent === id);
|
|
203
|
+
let moved = 0;
|
|
204
|
+
const shift = (e, dy) => {
|
|
205
|
+
if (!dy) return;
|
|
206
|
+
moved++;
|
|
207
|
+
if (move) move(e, dy);
|
|
208
|
+
else translateElement(e, 0, dy);
|
|
209
|
+
if (figures.has(e.id)) translateFigure(spec.sketch, e.id, 0, dy, deltas, movedCircles);
|
|
210
|
+
for (const k of kids(e.id)) shift(k, dy);
|
|
211
|
+
};
|
|
212
|
+
const changes = [];
|
|
213
|
+
// A declared grid: its member stacks' rows line up, row by row, in one
|
|
214
|
+
// shared layout (row i as tall as the tallest row i across the members, the
|
|
215
|
+
// first member's band and spacing); a divider drawn across the columns in a
|
|
216
|
+
// shared gap is placed once, at its drawn fraction of that gap.
|
|
217
|
+
const shared = new Map(); // stack id → [{ top, h }] from its grid
|
|
218
|
+
for (const grid of spec.grids ?? []) {
|
|
219
|
+
const members = gridStacks(spec, grid);
|
|
220
|
+
if (members.length < 2) continue;
|
|
221
|
+
const plans = members.map((g) => ({ g, rows: rowPlans(spec, g, at) })).filter((p) => p.rows && p.rows.length === grid.rows);
|
|
222
|
+
if (plans.length !== members.length) { changes.push(`${grid.id}: its columns do not all hold ${grid.rows} rows; laid out column by column`); continue; }
|
|
223
|
+
for (const p of plans) p.g.band ??= recordStackBand(spec, p.g);
|
|
224
|
+
// A member's own dividers lie in the GRID's gaps: their fraction is of the
|
|
225
|
+
// shared gap (the rows' union across the columns), so the same divider in
|
|
226
|
+
// every column lands on the same boundary. Recorded once, like the band.
|
|
227
|
+
const union = Array.from({ length: grid.rows }, (_, i) => ({ top: Math.min(...plans.map((p) => p.rows[i].ext.top)), bottom: Math.max(...plans.map((p) => p.rows[i].ext.bottom)) }));
|
|
228
|
+
for (const p of plans) {
|
|
229
|
+
if (p.g.band.gridFracs) continue;
|
|
230
|
+
for (const it of p.g.band.between ?? []) {
|
|
231
|
+
const c = it.line ? (spec.connectors ?? []).find((x) => x.id === it.id) : null;
|
|
232
|
+
const e = it.line ? null : byId.get(it.id);
|
|
233
|
+
const y = c ? (() => { const ys = linePoints(c).map((q) => q.y); return (Math.min(...ys) + Math.max(...ys)) / 2; })() : e ? e.y + e.h / 2 : null;
|
|
234
|
+
const a = union[it.gap]?.bottom, b = union[it.gap + 1]?.top;
|
|
235
|
+
if (y != null && a != null && b != null && b > a) it.frac = Math.min(1, Math.max(0, (y - a) / (b - a)));
|
|
236
|
+
}
|
|
237
|
+
p.g.band.gridFracs = true;
|
|
238
|
+
}
|
|
239
|
+
const lead = plans[0].g;
|
|
240
|
+
const band = stackBand(spec, lead, at);
|
|
241
|
+
const heights = Array.from({ length: grid.rows }, (_, i) => Math.max(...plans.map((p) => p.rows[i].ext.bottom - p.rows[i].ext.top)));
|
|
242
|
+
const total = heights.reduce((a, b) => a + b, 0);
|
|
243
|
+
const len = band.bottom - band.top;
|
|
244
|
+
const even = lead.spacing !== "drawn";
|
|
245
|
+
const gaps = even ? heights.map(() => (len - total) / (heights.length + 1)) : [lead.band.lead, ...lead.band.gaps];
|
|
246
|
+
if (even && gaps[0] < 0) { changes.push(`${grid.id}: its rows need ${Math.ceil(total - len)}px more than its columns hold; laid out column by column`); continue; }
|
|
247
|
+
const tops = [];
|
|
248
|
+
let top = band.top;
|
|
249
|
+
heights.forEach((h, i) => { top += gaps[i] ?? 0; tops.push({ top: half(top), h }); top += h; });
|
|
250
|
+
for (const p of plans) shared.set(p.g.id, tops);
|
|
251
|
+
grid.band ??= recordGridBand(spec, grid, members);
|
|
252
|
+
grid.placed = tops.map((t) => [t.top, t.top + t.h]);
|
|
253
|
+
}
|
|
254
|
+
for (const g of spec.stacks ?? []) {
|
|
255
|
+
if (new Set(g.rows.flat()).size < g.rows.flat().length) continue;
|
|
256
|
+
g.band ??= recordStackBand(spec, g);
|
|
257
|
+
if (!g.band) continue;
|
|
258
|
+
const band = stackBand(spec, g, at);
|
|
259
|
+
// A row: its elements (the content set on the row's axis) and its lines (a
|
|
260
|
+
// divider drawn under the content, which keeps that distance to the
|
|
261
|
+
// content's bottom — so it follows a text that grew).
|
|
262
|
+
const rows = g.rows.map((row) => {
|
|
263
|
+
const parts = partsOf(spec, row);
|
|
264
|
+
const rects = parts.elements.map((e) => ({ ...e, ...at(e) }));
|
|
265
|
+
if (!rects.length && !parts.lines.length) return null;
|
|
266
|
+
const content = rects.length
|
|
267
|
+
? { top: Math.min(...rects.map((r) => r.y)), bottom: Math.max(...rects.map((r) => r.y + r.h)) }
|
|
268
|
+
: { top: Math.min(...parts.lines.flatMap((c) => linePoints(c).map((p) => p.y))), bottom: Math.max(...parts.lines.flatMap((c) => linePoints(c).map((p) => p.y))) };
|
|
269
|
+
const lineOff = parts.lines.map((c) => { const ys = linePoints(c).map((p) => p.y); const drawn = g.band.lines?.[c.id]; return { c, lo: drawn?.lo ?? Math.min(...ys) - content.bottom, hi: drawn?.hi ?? Math.max(...ys) - content.bottom }; });
|
|
270
|
+
const top = Math.min(content.top, ...lineOff.map((l) => content.bottom + l.lo));
|
|
271
|
+
const bottom = Math.max(content.bottom, ...lineOff.map((l) => content.bottom + l.hi));
|
|
272
|
+
return { ...parts, rects, content, lineOff, ext: { top, bottom }, contentOff: content.top - top };
|
|
273
|
+
}).filter(Boolean);
|
|
274
|
+
if (rows.length < 2) continue;
|
|
275
|
+
const heights = rows.map((r) => r.ext.bottom - r.ext.top);
|
|
276
|
+
const total = heights.reduce((a, b) => a + b, 0);
|
|
277
|
+
const len = band.bottom - band.top;
|
|
278
|
+
const even = g.spacing !== "drawn";
|
|
279
|
+
const gaps = even ? rows.map(() => (len - total) / (rows.length + 1)) : [g.band.lead, ...g.band.gaps];
|
|
280
|
+
if (even && gaps[0] < 0) { changes.push(`${g.id}: its rows need ${Math.ceil(total - len)}px more than ${g.parent ?? "the slide"} holds; left as drawn`); continue; }
|
|
281
|
+
let top = band.top;
|
|
282
|
+
const placed = []; // each row's [top, bottom] after this pass
|
|
283
|
+
const fixed = shared.get(g.id) ?? null;
|
|
284
|
+
rows.forEach((r, i) => {
|
|
285
|
+
top += gaps[i] ?? 0;
|
|
286
|
+
const rowTop = fixed ? fixed[i].top : half(top);
|
|
287
|
+
const rowH = fixed ? fixed[i].h : heights[i];
|
|
288
|
+
// In a grid row taller than this member's own content, the content sits
|
|
289
|
+
// by the stack's `align` within the shared row.
|
|
290
|
+
const slack = rowH - heights[i];
|
|
291
|
+
const contentTop = rowTop + r.contentOff + (g.align === "bottom" ? slack : g.align === "top" ? 0 : slack / 2);
|
|
292
|
+
const contentH = r.content.bottom - r.content.top;
|
|
293
|
+
const before = moved;
|
|
294
|
+
// A row's parts sit in COLUMNS (the icon; the title over its body): parts
|
|
295
|
+
// overlapping in x form one column, stacked as drawn; the column block is
|
|
296
|
+
// what `align` sets on the row's axis, never each part alone (x102 GTM 3:
|
|
297
|
+
// a title and its body both "middle" would land on one line).
|
|
298
|
+
// A part inside another part of the row (the icon on its disc) rides with
|
|
299
|
+
// it: moving both would move it twice.
|
|
300
|
+
const partIds = new Set(r.elements.map((e) => e.id));
|
|
301
|
+
const own = r.elements.filter((e) => !(e.parent && partIds.has(e.parent)));
|
|
302
|
+
const columns = [];
|
|
303
|
+
for (const e of [...own].sort((a, b) => a.y - b.y)) {
|
|
304
|
+
const hit = columns.find((col) => col.some((o) => o.x < e.x + e.w && e.x < o.x + o.w));
|
|
305
|
+
if (hit) hit.push(e); else columns.push([e]);
|
|
306
|
+
}
|
|
307
|
+
for (const col of columns) {
|
|
308
|
+
const rects = col.map((e) => ({ e, ...at(e) }));
|
|
309
|
+
const top0 = Math.min(...rects.map((q) => q.y));
|
|
310
|
+
const gapsIn = rects.slice(1).map((q, k) => q.y - (rects[k].y + rects[k].h));
|
|
311
|
+
const blockH = rects.reduce((n, q) => n + q.h, 0) + gapsIn.reduce((n, v) => n + v, 0);
|
|
312
|
+
const want = g.align === "top" ? contentTop : g.align === "bottom" ? contentTop + contentH - blockH : contentTop + (contentH - blockH) / 2;
|
|
313
|
+
let y = want;
|
|
314
|
+
rects.forEach((q, k) => {
|
|
315
|
+
shift(q.e, half(y - q.y));
|
|
316
|
+
y += q.h + (gapsIn[k] ?? 0);
|
|
317
|
+
});
|
|
318
|
+
void top0;
|
|
319
|
+
}
|
|
320
|
+
for (const { c, lo } of r.lineOff) {
|
|
321
|
+
const was = Math.min(...linePoints(c).map((p) => p.y));
|
|
322
|
+
const dy = half(contentTop + contentH + lo - was);
|
|
323
|
+
if (dy) { moveLine(c, 0, dy); moved++; }
|
|
324
|
+
}
|
|
325
|
+
const dyRow = half(rowTop - r.ext.top);
|
|
326
|
+
if (moved > before) changes.push(`${g.id}: row ${i + 1} ${dyRow >= 0 ? "+" : ""}${dyRow}px, its parts ${g.align === "top" ? "on its top" : g.align === "bottom" ? "on its bottom" : "on its axis"}`);
|
|
327
|
+
placed.push([rowTop, rowTop + rowH]);
|
|
328
|
+
top += rowH;
|
|
329
|
+
});
|
|
330
|
+
// What is drawn in a gap rides with it, at its drawn fraction of the gap.
|
|
331
|
+
placeBetween(g, placed);
|
|
332
|
+
}
|
|
333
|
+
// A grid's own dividers — drawn across its columns in a shared gap — once.
|
|
334
|
+
for (const grid of spec.grids ?? []) if (grid.placed && grid.band) placeBetween(grid, grid.placed);
|
|
335
|
+
fixCopies(spec.sketch, deltas);
|
|
336
|
+
return changes;
|
|
337
|
+
|
|
338
|
+
function placeBetween(owner, placed) {
|
|
339
|
+
for (const it of owner.band.between ?? []) {
|
|
340
|
+
const a = placed[it.gap]?.[1], b = placed[it.gap + 1]?.[0];
|
|
341
|
+
if (a == null || b == null) continue;
|
|
342
|
+
const want = a + (b - a) * it.frac;
|
|
343
|
+
if (it.line) {
|
|
344
|
+
const c = (spec.connectors ?? []).find((x) => x.id === it.id);
|
|
345
|
+
if (!c) continue;
|
|
346
|
+
const ys = linePoints(c).map((q) => q.y);
|
|
347
|
+
const dy = half(want - (Math.min(...ys) + Math.max(...ys)) / 2);
|
|
348
|
+
if (dy) { moveLine(c, 0, dy); moved++; changes.push(`${owner.id}: ${it.id} ${dy >= 0 ? "+" : ""}${dy}px with its gap`); }
|
|
349
|
+
} else {
|
|
350
|
+
const e = byId.get(it.id);
|
|
351
|
+
if (!e) continue;
|
|
352
|
+
const cur = { ...e, ...at(e) };
|
|
353
|
+
const dy = half(want - (cur.y + cur.h / 2));
|
|
354
|
+
if (dy) { shift(e, dy); changes.push(`${owner.id}: ${it.id} ${dy >= 0 ? "+" : ""}${dy}px with its gap`); }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// The rows of a stack as applyStacks reads them (extents on the current sizes), or null.
|
|
361
|
+
function rowPlans(spec, g, at) {
|
|
362
|
+
const rows = g.rows.map((row) => rowExtent(spec, row, at) && { ext: rowExtent(spec, row, at) }).filter(Boolean);
|
|
363
|
+
return rows.length >= 2 ? rows : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The member stacks of a grid, in column order: by stack id (a repaired slide's marks) or by the column's card. */
|
|
367
|
+
export function gridStacks(spec, grid) {
|
|
368
|
+
const stacks = spec.stacks ?? [];
|
|
369
|
+
if (grid.stacks?.length) return grid.stacks.map((id) => stacks.find((g) => g.id === id)).filter(Boolean);
|
|
370
|
+
return (grid.columns ?? []).map((col) => stacks.find((g) => g.parent === col) ?? stacks.find((g) => g.rows.flat().includes(col))).filter(Boolean);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* What is drawn across a grid's columns between two of its rows (a divider
|
|
375
|
+
* running through every column), recorded once from the painting: lines
|
|
376
|
+
* spanning two or more member columns whose y lies between the rows' shared
|
|
377
|
+
* extents, with their drawn fraction of that gap.
|
|
378
|
+
*/
|
|
379
|
+
export function recordGridBand(spec, grid, members) {
|
|
380
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
381
|
+
const parts = new Set(members.flatMap((g) => g.rows.flat()));
|
|
382
|
+
const own = new Set(members.flatMap((g) => (g.band?.between ?? []).map((it) => it.id)));
|
|
383
|
+
const cols = members.map((g) => { const es = g.rows.flat().map((id) => byId.get(id)).filter(Boolean); return { x0: Math.min(...es.map((e) => e.x)), x1: Math.max(...es.map((e) => e.x + e.w)) }; });
|
|
384
|
+
const rows = Array.from({ length: grid.rows }, (_, i) => {
|
|
385
|
+
const exts = members.map((g) => rowExtent(spec, g.rows[i], (o) => o)).filter(Boolean);
|
|
386
|
+
return { top: Math.min(...exts.map((r) => r.top)), bottom: Math.max(...exts.map((r) => r.bottom)) };
|
|
387
|
+
});
|
|
388
|
+
const between = [];
|
|
389
|
+
for (const c of spec.connectors ?? []) {
|
|
390
|
+
if (!c.id || parts.has(c.id) || own.has(c.id)) continue;
|
|
391
|
+
const h = horizontal(c);
|
|
392
|
+
if (!h || cols.filter((col) => h.x0 < col.x1 && col.x0 < h.x1).length < 2) continue;
|
|
393
|
+
const i = rows.findIndex((r, k) => k < rows.length - 1 && h.y >= r.bottom - 0.5 && h.y <= rows[k + 1].top + 0.5);
|
|
394
|
+
if (i < 0) continue;
|
|
395
|
+
const a = rows[i].bottom, b = rows[i + 1].top;
|
|
396
|
+
between.push({ id: c.id, line: true, gap: i, frac: b > a ? (h.y - a) / (b - a) : 0.5 });
|
|
397
|
+
}
|
|
398
|
+
return { between };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Point a grid's columns at what survived normalization; a grid short of two columns is dropped with a note. */
|
|
402
|
+
export function remapGrids(spec, before) {
|
|
403
|
+
if (!spec.grids?.length) return [];
|
|
404
|
+
const notes = [];
|
|
405
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
406
|
+
spec.grids = spec.grids.flatMap((grid) => {
|
|
407
|
+
const columns = (grid.columns ?? []).filter((id) => byId.has(id));
|
|
408
|
+
if (!grid.stacks?.length && columns.length < 2) { notes.push(`${grid.id}: fewer than two of its columns survived normalization — laid out column by column`); return []; }
|
|
409
|
+
return [{ ...grid, columns }];
|
|
410
|
+
});
|
|
411
|
+
void before;
|
|
412
|
+
return notes;
|
|
413
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Storyboard-markdown: segment a plan.md into alternating markdown / storyboard
|
|
2
2
|
// runs, splice an edited storyboard fence back in, and pull the red-thread beats
|
|
3
|
-
// out of the `## Red thread` section.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// shared IIFE with storyboard-schema and the
|
|
7
|
-
// helpers use an `sm` prefix to stay
|
|
8
|
-
//
|
|
3
|
+
// out of the `## Red thread` section. Canonical here: the add-in's plan review
|
|
4
|
+
// and runtime import a byte-identical mirror (scripts/sync-transcription-runtime.mjs),
|
|
5
|
+
// typed by src/ui/src/pages/plan-review/storyboard-markdown.ts. Ships into the
|
|
6
|
+
// plan gate's browser bundle in one shared IIFE with storyboard-schema and the
|
|
7
|
+
// other plan modules, so module-scope helpers use an `sm` prefix to stay
|
|
8
|
+
// collision-free. The import below is a single line on purpose — the browser
|
|
9
9
|
// bundler strips lines matching `import … from "…";`.
|
|
10
10
|
import { parseStoryboardCanvas, serializeStoryboardCanvas, STORYBOARD_CANVAS_LANG } from "./storyboard-schema.mjs";
|
|
11
11
|
|
|
@@ -30,8 +30,30 @@ function smCleanRedThreadLine(line) {
|
|
|
30
30
|
.trim();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The authoring contract ends the red thread with one `Frame: cover · closing`
|
|
35
|
+
* (or `Frame: none`) line naming the frame slides. It is a declaration ABOUT the
|
|
36
|
+
* deck, not a move in it, and reading it as one did real damage: it minted a
|
|
37
|
+
* phantom "Frame: none" rail card, and it pushed the move count past the
|
|
38
|
+
* `### Slide N` count, which is the exact condition that turns the whole
|
|
39
|
+
* backbone read-only — no insert, no reorder, no rename.
|
|
40
|
+
*/
|
|
41
|
+
const SM_FRAME_LINE_RE = /^Frame\s*:/i;
|
|
42
|
+
|
|
43
|
+
/** The plan's `Frame:` declaration, verbatim, or null when it carries none. */
|
|
44
|
+
export function extractFrameLine(markdown) {
|
|
45
|
+
const section = findRedThreadSection(markdown);
|
|
46
|
+
if (!section) return null;
|
|
47
|
+
for (const raw of markdown.slice(section.bodyStart, section.bodyEnd).split("\n")) {
|
|
48
|
+
const line = smCleanRedThreadLine(raw);
|
|
49
|
+
if (line && SM_FRAME_LINE_RE.test(line)) return line;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
function smSplitRedThreadBody(body) {
|
|
34
|
-
const lines = body.split("\n").map(smCleanRedThreadLine).filter(Boolean)
|
|
55
|
+
const lines = body.split("\n").map(smCleanRedThreadLine).filter(Boolean)
|
|
56
|
+
.filter((line) => !SM_FRAME_LINE_RE.test(line));
|
|
35
57
|
if (lines.length !== 1) return lines;
|
|
36
58
|
const [line] = lines;
|
|
37
59
|
if (!line) return [];
|
|
@@ -87,6 +109,11 @@ export function splitPlanMarkdownIntoSegments(markdown) {
|
|
|
87
109
|
data: parsed.data,
|
|
88
110
|
error: parsed.error,
|
|
89
111
|
warnings: parsed.warnings,
|
|
112
|
+
// The typed issues behind an `error`, when the fence failed on its image
|
|
113
|
+
// uses — `{code, assetId?, useId?, slide?, message, action}` from the
|
|
114
|
+
// image-intent contract. Absent on every other parse failure, so a
|
|
115
|
+
// consumer that ignores it reads exactly as before.
|
|
116
|
+
...(parsed.issues ? { issues: parsed.issues } : {}),
|
|
90
117
|
start: matchStart,
|
|
91
118
|
end: matchEnd,
|
|
92
119
|
lineOffset,
|
|
@@ -117,9 +144,11 @@ export function replaceStoryboardSegment(markdown, segment, nextData) {
|
|
|
117
144
|
)}`;
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
// Deck-level layout knob: binds EVERY valid storyboard fence to `layout`.
|
|
121
|
-
//
|
|
122
|
-
//
|
|
147
|
+
// Deck-level layout knob: binds EVERY valid body storyboard fence to `layout`.
|
|
148
|
+
// The user picked this in the review, so agent-side hints (`layoutSelection`,
|
|
149
|
+
// canvas geometry) never veto it — only frames stay put. Fences are spliced
|
|
150
|
+
// last-to-first so earlier offsets stay valid; malformed fences are left
|
|
151
|
+
// untouched. Mirrors the add-in's setStoryboardLayoutForAll (b939ea372).
|
|
123
152
|
export function setStoryboardLayoutForAll(markdown, layout, options = []) {
|
|
124
153
|
let next = markdown;
|
|
125
154
|
const target = options.find((option) => option.componentName === layout);
|
|
@@ -129,29 +158,43 @@ export function setStoryboardLayoutForAll(markdown, layout, options = []) {
|
|
|
129
158
|
for (const segment of boards.reverse()) {
|
|
130
159
|
if (segment.type !== "storyboard" || !segment.data) continue;
|
|
131
160
|
const data = segment.data;
|
|
132
|
-
|
|
133
|
-
if (data.role === "frame" || data.layoutSelection === "explicit") continue;
|
|
134
|
-
if (!current || (!current.canvasBounds && !current.contentAreas?.length)) continue;
|
|
135
|
-
// Specialized geometries stay put. Bulk defaults change equivalent canvases only.
|
|
136
|
-
if (JSON.stringify(current.contentAreas ?? current.canvasBounds) !== JSON.stringify(target.contentAreas ?? target.canvasBounds)) continue;
|
|
137
|
-
if (data.layout === layout) continue;
|
|
161
|
+
if (data.role === "frame" || data.layout === layout) continue;
|
|
138
162
|
next = replaceStoryboardSegment(next, segment, { ...data, role: "body", layout,
|
|
139
|
-
layoutKey: target.layoutKey, layoutSelection: "
|
|
163
|
+
layoutKey: target.layoutKey, layoutSelection: "explicit", layoutReason: undefined });
|
|
140
164
|
}
|
|
141
165
|
return next;
|
|
142
166
|
}
|
|
143
167
|
|
|
144
|
-
|
|
145
|
-
|
|
168
|
+
const SM_FRAME_JOBS = ["cover", "agenda", "divider", "closing", "quote"];
|
|
169
|
+
const smHasCanvas = (option) => Boolean(option?.canvasBounds || option?.contentAreas?.length);
|
|
170
|
+
|
|
171
|
+
// CLI-only (the plan gate's design picker): move every storyboard whose layout
|
|
172
|
+
// the design in `options` lacks onto that design's nearest equivalent. A frame
|
|
173
|
+
// slide keeps its job (cover → the design's first cover layout); a body slide
|
|
174
|
+
// takes the first content layout with as many content areas as its old one
|
|
175
|
+
// (`previous` is the old design's option list), else the first content layout.
|
|
176
|
+
// Options arrive ranked, so "first" is the design's own preference. Boards
|
|
177
|
+
// whose layout the design has, or for which it offers nothing, stay as they are.
|
|
178
|
+
export function remapStoryboardLayouts(markdown, options = [], previous = []) {
|
|
179
|
+
const names = new Set(options.map((option) => option.componentName));
|
|
180
|
+
const content = options.filter((option) => smHasCanvas(option) && (option.job ?? "content") === "content");
|
|
146
181
|
let next = markdown;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
const boards = splitPlanMarkdownIntoSegments(markdown).filter((segment) => segment.type === "storyboard" && segment.data);
|
|
183
|
+
for (const segment of boards.reverse()) {
|
|
184
|
+
const data = segment.data;
|
|
185
|
+
if (!data.layout || names.has(data.layout)) continue;
|
|
186
|
+
const old = previous.find((option) => option.componentName === data.layout) ?? null;
|
|
187
|
+
const job = data.role === "frame" ? data.frameKind : old?.job;
|
|
188
|
+
let target = null;
|
|
189
|
+
if (data.role === "frame" || SM_FRAME_JOBS.includes(job)) {
|
|
190
|
+
target = options.find((option) => option.job === job) ?? null;
|
|
191
|
+
} else {
|
|
192
|
+
const areas = old?.contentAreas?.length ?? 0;
|
|
193
|
+
target = (areas && content.find((option) => (option.contentAreas?.length ?? 0) === areas)) || content[0] || null;
|
|
194
|
+
}
|
|
195
|
+
if (!target) continue;
|
|
196
|
+
next = replaceStoryboardSegment(next, segment, { ...data, layout: target.componentName,
|
|
197
|
+
layoutKey: target.layoutKey, layoutSelection: "automatic", layoutReason: undefined });
|
|
155
198
|
}
|
|
156
199
|
return next;
|
|
157
200
|
}
|