slidemind 0.3.19 → 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 (186) 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/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
@@ -6,12 +6,15 @@
6
6
  // slidemind view-draft <deck> (the CLI entry point; it builds, then captures)
7
7
  // node src/builders/build.mjs --deck <deck> [--open] [--fix]
8
8
  //
9
- // Convention:
10
- // <deck>/
9
+ // Convention (the deck's WORKSPACE — `<deck>/.slidemind/`, or the deck dir
10
+ // itself for a deck that predates the layout; see src/lib/deck-paths.mjs):
11
+ // <workspace>/
11
12
  // slides/
12
13
  // 01-cover.jsx
13
14
  // 02-agenda.jsx
14
15
  // ...
16
+ // preview/presentation.html what this module writes
17
+ // preview/fonts/ the bundle's brand faces, next to the page
15
18
  //
16
19
  // Each slide is a thin JSX component that composes a layout from
17
20
  // skills/create-slides/layouts/. Slide files do NOT import layouts; the
@@ -21,9 +24,11 @@
21
24
  // The build:
22
25
  // 1. Inlines every layout .jsx as a <script type="text/jsx-template"> block.
23
26
  // 2. Inlines every slide .jsx the same way, in filename order.
24
- // 3. Copies corporate-design.css, layouts.css, and the assets/ folder into
25
- // the deck directory so the output is fully self-contained.
26
- // 4. Emits presentation.html with React + Babel-standalone from CDN and a
27
+ // 3. Inlines corporate-design.css + layouts.css FROM THE BUNDLE CACHE and
28
+ // points every brand asset (chrome-*, master-*, logos) at the bundle's
29
+ // assets dir by absolute file: URL — the design is referenced, never
30
+ // copied into the deck (see the deck-workspace change).
31
+ // 4. Emits preview/presentation.html with React + Babel-standalone from CDN and a
27
32
  // bootstrap that transpiles on load and renders a keyboard-navigable
28
33
  // deck.
29
34
 
@@ -31,6 +36,7 @@ import { spawn } from "node:child_process";
31
36
  import fs from "node:fs";
32
37
  import os from "node:os";
33
38
  import path from "node:path";
39
+ import crypto from "node:crypto";
34
40
  import { fileURLToPath, pathToFileURL } from "node:url";
35
41
 
36
42
  import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
@@ -41,6 +47,8 @@ import { lintConvertedOrThrow } from "../lib/lint-converted.mjs";
41
47
  import { resolveSkillForDeck } from "./resolve-skill.mjs";
42
48
  import { convertedHostLayouts, loadConversions } from "../lib/conversions.mjs";
43
49
  import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
50
+ import { selectSlideFiles } from "../lib/slide-selection.mjs";
51
+ import { bundleOwnedFiles, deckSlug, previewPath } from "../lib/deck-paths.mjs";
44
52
 
45
53
  const __filename = fileURLToPath(import.meta.url);
46
54
  const __dirname = path.dirname(__filename);
@@ -68,19 +76,59 @@ export function stripImports(src) {
68
76
  );
69
77
  }
70
78
 
71
- function rewriteAssetsPath(src) {
79
+ // Brand assets (logos, decor, chrome-*/master-* layout backgrounds) live in the
80
+ // materialized bundle cache and are NOT copied into the deck. Every reference
81
+ // to them in a layout, a slide or the CSS is rewritten to an absolute file: URL
82
+ // into that cache, so the page works opened straight off disk. The review
83
+ // server rewrites this same prefix to a route it serves (see review.mjs) —
84
+ // a browser on an http page may not load file: subresources.
85
+ export function brandAssetsUrl(skill) {
86
+ const dir = skill?.brand?.assetsDir;
87
+ return dir ? pathToFileURL(dir).href.replace(/\/$/, "") : "./assets";
88
+ }
89
+
90
+ function rewriteAssetsPath(src, brandUrl) {
72
91
  // The layout defaults point at "../assets" (correct for in-repo authoring
73
- // under skills/create-slides/layouts/). In a built deck the copied
74
- // assets sit next to the HTML, so the default should be "./assets".
92
+ // under skills/create-slides/layouts/) — that layer IS the brand's, so it
93
+ // resolves to the bundle.
94
+ return src.replace(/assetsPath = "\.\.\/assets"/g, `assetsPath = "${brandUrl}"`);
95
+ }
96
+
97
+ // A slide that names a layout background by hand ("./assets/chrome-Black.png")
98
+ // meant the brand's copy; deck-owned media ("./assets/photo.jpg") stays local.
99
+ // `deckOwned` holds the files the DECK owns under those names — a user's own
100
+ // `assets/master-data.png` is its own image, not the bundle's, and keeps
101
+ // resolving locally. Layout sources are the brand's, so they pass none.
102
+ function rewriteBrandAssetRefs(src, brandUrl, deckOwned = null) {
75
103
  return src.replace(
76
- /assetsPath = "\.\.\/assets"/g,
77
- 'assetsPath = "./assets"',
104
+ /(["'`(])\.?\/?assets\/((?:chrome|master)-[^"'`)]*)/g,
105
+ (m, open, name) => (deckOwned?.has(`assets/${name}`) ? m : `${open}${brandUrl}/${name}`),
78
106
  );
79
107
  }
80
108
 
81
- function inlineLayout(file) {
109
+ // Files in the deck's assets/ that are NOT verified copies of the bundle's own —
110
+ // the deck owns them whatever they are called.
111
+ function deckOwnedAssets(deckDir) {
112
+ const bundleOwned = bundleOwnedFiles(deckDir, resolveBundleDirForDeck(deckDir));
113
+ const owned = new Set();
114
+ let names = [];
115
+ try {
116
+ names = fs.readdirSync(path.join(deckDir, "assets"));
117
+ } catch {
118
+ return owned;
119
+ }
120
+ for (const name of names) {
121
+ if (!bundleOwned.has(`assets/${name}`)) owned.add(`assets/${name}`);
122
+ }
123
+ return owned;
124
+ }
125
+
126
+ function inlineLayout(file, brandUrl) {
82
127
  const stem = path.basename(file, ".jsx");
83
- const raw = rewriteAssetsPath(fs.readFileSync(file, "utf8"));
128
+ const raw = rewriteBrandAssetRefs(
129
+ rewriteAssetsPath(fs.readFileSync(file, "utf8"), brandUrl),
130
+ brandUrl,
131
+ );
84
132
  const binding = raw.match(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)\s*\(/)?.[1] || layoutBindingNameForStem(stem);
85
133
  const src = safeInline(raw);
86
134
  return (
@@ -105,59 +153,42 @@ function layoutBindingNameForStem(stem) {
105
153
  )).join("");
106
154
  }
107
155
 
108
- function inlineSlide(file, index) {
156
+ function inlineSlide(file, index, brandUrl, deckOwned) {
109
157
  const stem = path.basename(file, ".jsx");
110
- const src = safeInline(stripImports(fs.readFileSync(file, "utf8")));
158
+ const src = safeInline(
159
+ rewriteBrandAssetRefs(stripImports(fs.readFileSync(file, "utf8")), brandUrl, deckOwned),
160
+ );
111
161
  return (
112
162
  `<script type="text/jsx-template" data-kind="slide" ` +
113
163
  `data-index="${index}" data-name="${stem}">\n${src}\n</script>`
114
164
  );
115
165
  }
116
166
 
117
- function copyDesignSystem(deckDir, skill) {
118
- // Copy CSS + assets into the deck directory so the HTML is portable.
119
- // Layering: brand assets first (logos, shared decor from the design
120
- // bundle), then the active skill's own assets on top — so a skill
121
- // can override a brand file by shipping its own copy with the same name.
122
- // For create-slides itself, skill.assetsDir === skill.brand.assetsDir, so
123
- // this is a single copy pass.
124
- const outAssets = path.join(deckDir, "assets");
125
- fs.mkdirSync(outAssets, { recursive: true });
126
- const assetSources =
127
- skill.brand.assetsDir === skill.assetsDir
128
- ? [skill.assetsDir]
129
- : [skill.brand.assetsDir, skill.assetsDir];
130
- for (const src of assetSources) {
131
- if (!fs.existsSync(src)) continue;
132
- for (const entry of fs.readdirSync(src)) {
133
- const p = path.join(src, entry);
134
- if (fs.statSync(p).isFile()) {
135
- fs.copyFileSync(p, path.join(outAssets, entry));
136
- }
137
- }
138
- }
139
- fs.copyFileSync(skill.cssFile, path.join(deckDir, "corporate-design.css"));
140
-
141
- // layouts.css references url("../assets/...") relative to the in-repo
142
- // layouts dir. The build copies it flat into the deck next to assets/,
143
- // so rewrite the URLs to "./assets/...".
144
- //
145
- // Layering: brand layouts.css first (lower in the cascade), then the
146
- // active skill's layouts.css. Skill rules sharing a selector win.
147
- const cssSources =
148
- skill.brand.layoutsCssFile === path.join(skill.layoutsDir, "layouts.css")
167
+ // The design system, INLINED from the bundle cache instead of copied into the
168
+ // deck: two stylesheets (corporate-design.css, then layouts.css so its rules
169
+ // win a selector tie) with every url("../assets/…") pointed at the bundle.
170
+ //
171
+ // Layering is unchanged — brand layouts.css first, the active skill's on top —
172
+ // only the destination is: nothing lands in the deck, so a deck no longer
173
+ // carries (or uploads, or hashes into an approval) a copy of the design.
174
+ function designSystemCss(skill, brandUrl) {
175
+ const chunks = [];
176
+ const rewrite = (css) =>
177
+ css
178
+ .replace(/url\("\.\.\/assets\//g, `url("${brandUrl}/`)
179
+ .replace(/url\('\.\.\/assets\//g, `url('${brandUrl}/`)
180
+ .replace(/url\(\.\.\/assets\//g, `url(${brandUrl}/`);
181
+ const cssSources = [
182
+ skill.cssFile,
183
+ ...(skill.brand.layoutsCssFile === path.join(skill.layoutsDir, "layouts.css")
149
184
  ? [path.join(skill.layoutsDir, "layouts.css")]
150
- : [skill.brand.layoutsCssFile, path.join(skill.layoutsDir, "layouts.css")];
151
- const cssChunks = [];
185
+ : [skill.brand.layoutsCssFile, path.join(skill.layoutsDir, "layouts.css")]),
186
+ ];
152
187
  for (const cssPath of cssSources) {
153
- if (!fs.existsSync(cssPath)) continue;
154
- let css = fs.readFileSync(cssPath, "utf8");
155
- css = css.replace(/url\("\.\.\/assets\//g, 'url("./assets/');
156
- css = css.replace(/url\('\.\.\/assets\//g, "url('./assets/");
157
- css = css.replace(/url\(\.\.\/assets\//g, "url(./assets/");
158
- cssChunks.push(css);
188
+ if (!cssPath || !fs.existsSync(cssPath)) continue;
189
+ chunks.push(rewrite(fs.readFileSync(cssPath, "utf8")));
159
190
  }
160
- fs.writeFileSync(path.join(deckDir, "layouts.css"), cssChunks.join("\n\n"));
191
+ return chunks.join("\n\n");
161
192
  }
162
193
 
163
194
  // First concrete (non-generic) family of a CSS var's font stack, or null.
@@ -204,7 +235,7 @@ function brandFontFaces(deckDir, skill) {
204
235
 
205
236
  let css = "";
206
237
  try {
207
- css = fs.readFileSync(path.join(deckDir, "corporate-design.css"), "utf8");
238
+ css = fs.readFileSync(skill.cssFile, "utf8");
208
239
  } catch {
209
240
  /* alias resolution degrades to canonical families only */
210
241
  }
@@ -213,7 +244,7 @@ function brandFontFaces(deckDir, skill) {
213
244
  mono: firstConcreteFamily(css, "font-mono"),
214
245
  };
215
246
 
216
- const outFonts = path.join(deckDir, "fonts");
247
+ const outFonts = previewPath(deckDir, "fonts");
217
248
  const rules = [];
218
249
  for (const font of fonts) {
219
250
  if (!font?.family || !font?.filename) continue;
@@ -239,9 +270,17 @@ function brandFontFaces(deckDir, skill) {
239
270
  const format = filename.toLowerCase().endsWith(".otf")
240
271
  ? "opentype"
241
272
  : "truetype";
273
+ // Two sources, because this one page is opened from four places and no single
274
+ // relative URL resolves in all of them: page-relative (`./fonts/`) is right for
275
+ // a file:// open and for the workspace's own /decks/:id/preview/ route, while
276
+ // `./preview/fonts/` is right for the review server (page at /, statics from the
277
+ // workspace root) and the embedded /decks/:id/review gate. A `src:` list is
278
+ // exactly the fallback CSS provides for this — the browser takes the first that
279
+ // loads, so brand type no longer silently degrades to a system font.
242
280
  const rule = (family) =>
243
281
  `@font-face { font-family: ${JSON.stringify(family)}; ` +
244
- `src: url(${JSON.stringify(`./fonts/${filename}`)}) format("${format}"); ` +
282
+ `src: url(${JSON.stringify(`./fonts/${filename}`)}) format("${format}"), ` +
283
+ `url(${JSON.stringify(`./preview/fonts/${filename}`)}) format("${format}"); ` +
245
284
  `font-weight: ${weight}; font-style: ${style}; font-display: block; }`;
246
285
  rules.push(rule(font.family));
247
286
  const alias = aliasByRole[font.role];
@@ -267,34 +306,36 @@ function loadOverrides(deckDir) {
267
306
  return data;
268
307
  }
269
308
 
270
- // Read the deck's SVG assets (after copyDesignSystem has populated assets/) so
271
- // the preview can inline them. An <img src="x.svg"> can't inherit `color`, so a
309
+ // SVG assets the preview can inline — the deck's own downloads plus the
310
+ // bundle's brand glyphs (the deck no longer carries a copy of those). An <img src="x.svg"> can't inherit `color`, so a
272
311
  // CSS-tinted icon looks wrong in the HTML preview yet right in the .pptx render
273
312
  // (which substitutes currentColor) — the source-mismatch that sends agents off
274
313
  // to inspect SVGs. Inlining the SVG at runtime makes the preview honour `color`
275
314
  // exactly as the render does. Keyed three ways so any of the src spellings an
276
315
  // author uses ("./assets/x.svg", "assets/x.svg", "x.svg") resolves.
277
- function collectSvgAssets(deckDir) {
278
- const assetsDir = path.join(deckDir, "assets");
316
+ function collectSvgAssets(deckDir, skill, brandUrl) {
279
317
  const map = {};
280
- let entries;
281
- try {
282
- entries = fs.readdirSync(assetsDir);
283
- } catch {
284
- return map;
285
- }
286
- for (const name of entries) {
287
- if (!name.toLowerCase().endsWith(".svg")) continue;
288
- let markup;
318
+ const read = (dir, keys) => {
319
+ let entries;
289
320
  try {
290
- markup = fs.readFileSync(path.join(assetsDir, name), "utf8");
321
+ entries = fs.readdirSync(dir);
291
322
  } catch {
292
- continue;
323
+ return;
293
324
  }
294
- map[`./assets/${name}`] = markup;
295
- map[`assets/${name}`] = markup;
296
- map[name] = markup;
297
- }
325
+ for (const name of entries) {
326
+ if (!name.toLowerCase().endsWith(".svg")) continue;
327
+ let markup;
328
+ try {
329
+ markup = fs.readFileSync(path.join(dir, name), "utf8");
330
+ } catch {
331
+ continue;
332
+ }
333
+ for (const key of keys(name)) map[key] = markup;
334
+ }
335
+ };
336
+ // Brand first, deck second: a deck file of the same name overrides it.
337
+ read(skill?.brand?.assetsDir, (n) => [`${brandUrl}/${n}`, n]);
338
+ read(path.join(deckDir, "assets"), (n) => [`./assets/${n}`, `assets/${n}`, n]);
298
339
  return map;
299
340
  }
300
341
 
@@ -306,6 +347,23 @@ const HTML_TEMPLATE = fs
306
347
  .readFileSync(path.join(__dirname, "preview.html"), "utf8")
307
348
  .replace("__FAVICON_LINK__", () => FAVICON_LINK);
308
349
 
350
+ /** Next to presentation.html: the content hash it was built from. */
351
+ export const PREVIEW_SOURCE_FILE = "presentation.source.json";
352
+
353
+ /**
354
+ * The content hash presentation.html shows, when its stamp is the one written
355
+ * with it (a rebuild without a stamp, or a hand-copied preview, answers null).
356
+ */
357
+ export function previewBuiltFrom(deckDir) {
358
+ try {
359
+ const stamp = JSON.parse(fs.readFileSync(previewPath(deckDir, PREVIEW_SOURCE_FILE), "utf8"));
360
+ const htmlAt = fs.statSync(previewPath(deckDir, "presentation.html")).mtimeMs;
361
+ return typeof stamp?.hash === "string" && Date.parse(stamp.builtAt) >= htmlAt - 1000 ? stamp.hash : null;
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+
309
367
  export async function buildDeck(deckDir, opts = {}) {
310
368
  const skill = resolveSkillForDeck(deckDir);
311
369
  const slidesDir = path.join(deckDir, "slides");
@@ -317,14 +375,14 @@ export async function buildDeck(deckDir, opts = {}) {
317
375
  // inserts internally, but `slidemind build` should not create a lossy
318
376
  // presentation.html artifact for them.
319
377
  throw new Error(
320
- `Deck ${path.basename(deckDir)} uses skill '${skill.skillName}' (mode ` +
378
+ `Deck ${deckSlug(deckDir)} uses skill '${skill.skillName}' (mode ` +
321
379
  `fill-template); it has no HTML preview. Run 'slidemind build-pptx ` +
322
- `${path.basename(deckDir)}' to produce the editable .pptx.`,
380
+ `${deckSlug(deckDir)}' to produce the editable .pptx.`,
323
381
  );
324
382
  }
325
- const slideNames = sortSlideFilenames(
383
+ const slideNames = selectSlideFiles(sortSlideFilenames(
326
384
  fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx")),
327
- );
385
+ ), opts.only);
328
386
  const slideFiles = slideNames.map((f) => path.join(slidesDir, f));
329
387
  if (slideFiles.length === 0) {
330
388
  throw new Error(`No .jsx files in ${slidesDir}`);
@@ -394,19 +452,21 @@ export async function buildDeck(deckDir, opts = {}) {
394
452
  const chartPreview = await loadChartPreview(resolveBundleDirForDeck(deckDir));
395
453
  const connectorPreview = await loadConnectorPreview(resolveBundleDirForDeck(deckDir));
396
454
 
397
- copyDesignSystem(deckDir, skill);
455
+ const brandUrl = brandAssetsUrl(skill);
398
456
 
399
457
  const layoutsBlock = layoutFiles
400
- .map(inlineLayout)
458
+ .map((file) => inlineLayout(file, brandUrl))
401
459
  .concat(convertedHostStubs(deckDir, conversions))
402
460
  .join("\n");
461
+ const deckOwned = deckOwnedAssets(deckDir);
403
462
  const slidesBlock = slideFiles
404
- .map((file, i) => inlineSlide(file, i))
463
+ .map((file, i) => inlineSlide(file, i, brandUrl, deckOwned))
405
464
  .join("\n");
406
465
 
407
- const svgAssets = collectSvgAssets(deckDir);
466
+ const svgAssets = collectSvgAssets(deckDir, skill, brandUrl);
408
467
 
409
- const html = HTML_TEMPLATE.replace("__TITLE__", path.basename(deckDir))
468
+ const html = HTML_TEMPLATE.replace("__TITLE__", deckSlug(deckDir))
469
+ .replace("__DESIGN_CSS__", () => designSystemCss(skill, brandUrl))
410
470
  .replace("__BRAND_FONT_FACES__", brandFontFaces(deckDir, skill))
411
471
  .replace("__LAYOUTS__", layoutsBlock)
412
472
  .replace("__SLIDES__", slidesBlock)
@@ -427,8 +487,22 @@ export async function buildDeck(deckDir, opts = {}) {
427
487
  connectorPreview ? ` <script>\n${safeInline(connectorPreview.source)}\n </script>` : "",
428
488
  );
429
489
 
430
- const out = path.join(deckDir, "presentation.html");
490
+ // Parallel lanes must not replace the full-deck review page, or each other's
491
+ // partial preview. Keep the same directory so relative asset URLs still work.
492
+ const suffix = opts.only == null ? "" : `-${crypto.createHash("sha256").update(JSON.stringify(slideNames)).digest("hex").slice(0, 16)}`;
493
+ const out = previewPath(deckDir, `presentation${suffix}.html`);
494
+ fs.mkdirSync(path.dirname(out), { recursive: true });
431
495
  fs.writeFileSync(out, html);
496
+ // What this preview was built FROM: the review gate binds an approval to it, so
497
+ // an approval never covers slides changed, renamed or deleted after the build.
498
+ if (!suffix) {
499
+ try {
500
+ const { hashDeckContent } = await import("./review.mjs");
501
+ fs.writeFileSync(previewPath(deckDir, PREVIEW_SOURCE_FILE), JSON.stringify({ hash: `sha256:${hashDeckContent(deckDir)}`, builtAt: new Date().toISOString() }) + "\n");
502
+ } catch {
503
+ // no stamp: freshness falls back to file times
504
+ }
505
+ }
432
506
  return { out, slideCount: slideFiles.length };
433
507
  }
434
508
 
@@ -588,6 +662,16 @@ async function main() {
588
662
  `Autofixed ${fixed.fixes} lint issue(s) in ${fixed.files} slide file(s) — re-read any file before patching it.\n`,
589
663
  );
590
664
  }
665
+ // Source someone else owns is withheld, never rewritten — and a withheld
666
+ // mutation is always SAID. `slidemind view-draft --fix` prints these from
667
+ // src/cli/deck.mjs; this dev script was the one path where the same guard
668
+ // ran silently. Duplicated on purpose: sharing the printer would mean
669
+ // reshaping a seam four production commands now depend on.
670
+ for (const w of fixed.withheld ?? []) {
671
+ process.stderr.write(
672
+ `[fix:withheld] ${w.file} — autofix (${w.fixes.join(", ")}) NOT applied: ${w.reason}.\n`,
673
+ );
674
+ }
591
675
  }
592
676
  const { out, slideCount } = await buildDeck(deckDir);
593
677
  const rel = path.relative(process.cwd(), out) || out;