@classic-homes/slides 0.2.0 → 0.2.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/README.md CHANGED
@@ -91,10 +91,12 @@ flags errors and crowded slides.
91
91
  ## With an AI agent (MCP)
92
92
 
93
93
  The package ships an MCP server that edits deck files slide by slide and
94
- validates before every save, so an agent cannot leave a deck broken.
94
+ validates before every save, so an agent cannot leave a deck broken. It needs
95
+ `@modelcontextprotocol/sdk` (an optional peer, so apps that only render decks
96
+ don't install it).
95
97
 
96
98
  ```bash
97
- claude mcp add classic-slides -- npx -y @classic-homes/slides mcp --root ./decks
99
+ claude mcp add classic-slides -- npx -y -p @classic-homes/slides -p @modelcontextprotocol/sdk classic-slides mcp --root ./decks
98
100
  ```
99
101
 
100
102
  Then ask: _"Build a 10-slide deck in closing-day.yaml for first-time buyers
@@ -3,7 +3,7 @@ import {
3
3
  LAYOUTS,
4
4
  baseSlide,
5
5
  getLayout
6
- } from "./chunk-TM3TNVHB.js";
6
+ } from "./chunk-7FROOJCY.js";
7
7
 
8
8
  // src/catalog.ts
9
9
  import { z } from "zod";
@@ -66,4 +66,4 @@ export {
66
66
  deckJsonSchema,
67
67
  AUTHORING_GUIDE
68
68
  };
69
- //# sourceMappingURL=chunk-TXNQKBWJ.js.map
69
+ //# sourceMappingURL=chunk-33EPZMT6.js.map
@@ -881,102 +881,158 @@ function getLayout(name) {
881
881
  return byName.get(name);
882
882
  }
883
883
 
884
- // src/schema/deck.ts
884
+ // src/editor.ts
885
885
  import { z as z4 } from "zod";
886
- var slideSchemas = LAYOUTS.map(
887
- (layout) => z4.object({ layout: z4.literal(layout.name), ...baseSlide, ...layout.shape }).strict().describe(`${layout.description} Use for: ${layout.whenToUse}`)
888
- );
889
- var SlideSchema = z4.discriminatedUnion("layout", slideSchemas);
890
- var DeckSchema = z4.object({
891
- title: z4.string().min(1).describe("Deck title (browser tab, file name, gallery)."),
892
- description: z4.string().optional().describe("One sentence: who the deck is for and what it covers."),
893
- author: z4.string().optional(),
894
- date: z4.string().optional(),
895
- lang: z4.string().default("en"),
896
- journey: z4.object({
897
- steps: z4.array(Text).min(2).max(10).describe("Short names of each step, in order."),
898
- label: z4.string().default("STEP").describe("Word on the vertical step rail.")
899
- }).optional().describe(
900
- 'Optional numbered journey (agenda). Enables the journey layout, step rails and "you are here" strips.'
901
- ),
902
- slides: z4.array(SlideSchema).min(1)
903
- }).strict();
904
-
905
- // src/validate.ts
906
- function fromZod(issue) {
907
- let message = issue.message;
908
- if (issue.code === "invalid_union_discriminator") {
909
- message = `Unknown layout. Use one of: ${issue.options.join(", ")}.`;
910
- } else if (issue.code === "unrecognized_keys") {
911
- message = `Unknown field(s) ${issue.keys.map((k) => `"${k}"`).join(", ")} for this layout.`;
886
+ var MULTILINE = /* @__PURE__ */ new Set(["lead", "body", "text", "quote", "footnote", "sub", "notes", "caption"]);
887
+ var LABELS = {
888
+ eyebrowSub: "Eyebrow subtitle",
889
+ showJourney: "Show journey strip",
890
+ stepDone: "Step completed",
891
+ mediaSide: "Photo side",
892
+ imageAspect: "Photo shape",
893
+ numberFrom: "First badge number",
894
+ url: "URL",
895
+ id: "Slide id"
896
+ };
897
+ function labelFor(key) {
898
+ if (LABELS[key]) return LABELS[key];
899
+ const words = key.replace(/([A-Z])/g, " $1").toLowerCase();
900
+ return words.charAt(0).toUpperCase() + words.slice(1);
901
+ }
902
+ function unwrap(schema) {
903
+ let s = schema;
904
+ let required = true;
905
+ let dflt;
906
+ let description = schema.description;
907
+ for (; ; ) {
908
+ description ??= s.description;
909
+ if (s instanceof z4.ZodOptional || s instanceof z4.ZodNullable) {
910
+ required = false;
911
+ s = s.unwrap();
912
+ } else if (s instanceof z4.ZodDefault) {
913
+ required = false;
914
+ dflt = s._def.defaultValue();
915
+ s = s._def.innerType;
916
+ } else break;
912
917
  }
913
- return { level: "error", path: issue.path, message };
918
+ return { inner: s, required, default: dflt, description };
914
919
  }
915
- function validateDeck(input) {
916
- const parsed = DeckSchema.safeParse(input);
917
- if (!parsed.success) {
918
- return { ok: false, issues: parsed.error.issues.map(fromZod) };
920
+ function isText(s) {
921
+ return s === Text || s instanceof z4.ZodString && s.description === Text.description;
922
+ }
923
+ function describeField(key, schema) {
924
+ const { inner, required, default: dflt, description } = unwrap(schema);
925
+ const base = {
926
+ key,
927
+ label: labelFor(key),
928
+ required,
929
+ ...description ? { description } : {},
930
+ ...dflt !== void 0 ? { default: dflt } : {},
931
+ ...MULTILINE.has(key) ? { multiline: true } : {}
932
+ };
933
+ if (inner === Image) return { ...base, kind: "image" };
934
+ if (inner === Item) return { ...base, kind: "item" };
935
+ if (isText(inner)) return { ...base, kind: "inline" };
936
+ if (inner instanceof z4.ZodString) return { ...base, kind: "string" };
937
+ if (inner instanceof z4.ZodBoolean) return { ...base, kind: "boolean" };
938
+ if (inner instanceof z4.ZodEnum) return { ...base, kind: "enum", options: [...inner.options] };
939
+ if (inner instanceof z4.ZodLiteral) return { ...base, kind: "enum", options: [inner.value] };
940
+ if (inner instanceof z4.ZodNumber) {
941
+ return {
942
+ ...base,
943
+ kind: "number",
944
+ integer: inner.isInt,
945
+ ...inner.minValue !== null ? { min: inner.minValue } : {},
946
+ ...inner.maxValue !== null ? { max: inner.maxValue } : {}
947
+ };
919
948
  }
920
- const deck = parsed.data;
921
- const issues = [];
922
- const steps = deck.journey?.steps.length ?? 0;
923
- const seen = /* @__PURE__ */ new Set();
924
- deck.slides.forEach((slide, i) => {
925
- const layout = getLayout(slide.layout);
926
- for (const issue of layout.lint?.(slide) ?? []) {
927
- issues.push({ ...issue, path: ["slides", i, ...issue.path] });
928
- }
929
- if (slide.id) {
930
- if (seen.has(slide.id))
931
- issues.push({
932
- level: "error",
933
- path: ["slides", i, "id"],
934
- message: `Duplicate slide id "${slide.id}".`
935
- });
936
- seen.add(slide.id);
937
- }
938
- if ((slide.step || slide.showJourney || slide.layout === "journey") && !steps) {
939
- issues.push({
940
- level: "error",
941
- path: ["slides", i, slide.step ? "step" : "layout"],
942
- message: "This slide uses the journey, but the deck has no journey.steps defined."
943
- });
944
- }
945
- if (slide.step && steps && slide.step > steps) {
946
- issues.push({
947
- level: "error",
948
- path: ["slides", i, "step"],
949
- message: `Step ${slide.step} is past the last journey step (${steps}).`
950
- });
949
+ if (inner instanceof z4.ZodUnion) {
950
+ const opts = inner.options;
951
+ if (opts.every((o) => o instanceof z4.ZodLiteral)) {
952
+ return {
953
+ ...base,
954
+ kind: "enum",
955
+ options: opts.map((o) => o.value)
956
+ };
951
957
  }
952
- });
953
- if (deck.slides[0]?.layout !== "title") {
954
- issues.push({
955
- level: "warning",
956
- path: ["slides", 0, "layout"],
957
- message: 'Decks usually open with a "title" slide.'
958
- });
958
+ return { ...base, kind: "string" };
959
959
  }
960
- return { ok: !issues.some((i) => i.level === "error"), deck, issues };
960
+ if (inner instanceof z4.ZodArray) {
961
+ const el = inner.element;
962
+ return {
963
+ ...base,
964
+ kind: "list",
965
+ of: { ...describeField("", el), label: labelFor(key.replace(/s$/, "")) },
966
+ ...inner._def.minLength ? { min: inner._def.minLength.value } : {},
967
+ ...inner._def.maxLength ? { max: inner._def.maxLength.value } : {}
968
+ };
969
+ }
970
+ if (inner instanceof z4.ZodObject) {
971
+ return {
972
+ ...base,
973
+ kind: "group",
974
+ fields: Object.entries(inner.shape).map(
975
+ ([k, v]) => describeField(k, v)
976
+ )
977
+ };
978
+ }
979
+ return { ...base, kind: "string" };
961
980
  }
962
- function formatPath(path) {
963
- if (!path.length) return "deck";
964
- return path.map(
965
- (p, i) => typeof p === "number" ? path[i - 1] === "slides" ? `slide ${p + 1}` : String(p + 1) : p
966
- ).filter((p) => p !== "slides").join(" \u203A ");
981
+ function commonSlideFields() {
982
+ return Object.entries(baseSlide).map(([k, v]) => describeField(k, v));
967
983
  }
968
- function formatIssues(issues) {
969
- return issues.map((i) => `${i.level === "error" ? "\u2716" : "\u26A0"} ${formatPath(i.path)}: ${i.message}`).join("\n");
984
+ function layoutEditorInfo(name) {
985
+ const layout = getLayout(name);
986
+ if (!layout) return void 0;
987
+ return {
988
+ name: layout.name,
989
+ description: layout.description,
990
+ whenToUse: layout.whenToUse,
991
+ defaults: layout.defaults ?? {},
992
+ fields: Object.entries(layout.shape).map(
993
+ ([k, v]) => describeField(k, v)
994
+ ),
995
+ example: layout.example
996
+ };
970
997
  }
971
- var DeckError = class extends Error {
972
- constructor(issues) {
973
- super(`Deck is not valid:
974
- ${formatIssues(issues.filter((i) => i.level === "error"))}`);
975
- this.issues = issues;
976
- this.name = "DeckError";
977
- }
978
- issues;
979
- };
998
+ function allLayoutEditorInfo() {
999
+ return LAYOUTS.map((l) => layoutEditorInfo(l.name));
1000
+ }
1001
+ var PLACEHOLDER_IMAGE = "data:image/svg+xml;base64," + btoa(
1002
+ '<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000"><rect width="1600" height="1000" fill="#ecebe6"/><rect x="24" y="24" width="1552" height="952" fill="none" stroke="#b9bcb4" stroke-width="6" stroke-dasharray="28 20"/><text x="800" y="520" text-anchor="middle" font-family="Figtree,Arial,sans-serif" font-size="64" fill="#7a7a7a">Add a photo</text></svg>'
1003
+ );
1004
+ function isPlaceholderImage(src) {
1005
+ return src === PLACEHOLDER_IMAGE;
1006
+ }
1007
+ function blankSlide(name) {
1008
+ const layout = getLayout(name);
1009
+ if (!layout) return void 0;
1010
+ return JSON.parse(
1011
+ JSON.stringify(layout.example),
1012
+ (key, value) => key === "src" && typeof value === "string" && !/^(https?:|data:)/.test(value) ? PLACEHOLDER_IMAGE : value
1013
+ );
1014
+ }
1015
+
1016
+ // src/schema/deck.ts
1017
+ import { z as z5 } from "zod";
1018
+ var slideSchemas = LAYOUTS.map(
1019
+ (layout) => z5.object({ layout: z5.literal(layout.name), ...baseSlide, ...layout.shape }).strict().describe(`${layout.description} Use for: ${layout.whenToUse}`)
1020
+ );
1021
+ var SlideSchema = z5.discriminatedUnion("layout", slideSchemas);
1022
+ var DeckSchema = z5.object({
1023
+ title: z5.string().min(1).describe("Deck title (browser tab, file name, gallery)."),
1024
+ description: z5.string().optional().describe("One sentence: who the deck is for and what it covers."),
1025
+ author: z5.string().optional(),
1026
+ date: z5.string().optional(),
1027
+ lang: z5.string().default("en"),
1028
+ journey: z5.object({
1029
+ steps: z5.array(Text).min(2).max(10).describe("Short names of each step, in order."),
1030
+ label: z5.string().default("STEP").describe("Word on the vertical step rail.")
1031
+ }).optional().describe(
1032
+ 'Optional numbered journey (agenda). Enables the journey layout, step rails and "you are here" strips.'
1033
+ ),
1034
+ slides: z5.array(SlideSchema).min(1)
1035
+ }).strict();
980
1036
 
981
1037
  // src/render.ts
982
1038
  import QRCode from "qrcode";
@@ -1015,7 +1071,11 @@ var RUNTIME_JS = String.raw`
1015
1071
  notesBody.innerHTML = n.html || '<p><i>No notes for this slide.</i></p>';
1016
1072
  notesTime.textContent = n.time || '';
1017
1073
  }
1018
- if (location.hash !== '#slide-' + (i + 1)) history.replaceState(null, '', '#slide-' + (i + 1));
1074
+ // Sandboxed or srcdoc frames may refuse history access; the deep link is optional.
1075
+ try {
1076
+ if (location.hash !== '#slide-' + (i + 1)) history.replaceState(null, '', '#slide-' + (i + 1));
1077
+ } catch (_) {}
1078
+ if (window.parent !== window) window.parent.postMessage({ type: 'classic-slides:slide', index: i, count: slides.length }, '*');
1019
1079
  }
1020
1080
 
1021
1081
  // Current slide comes from the scroll position, the single source of truth.
@@ -1038,7 +1098,20 @@ var RUNTIME_JS = String.raw`
1038
1098
  });
1039
1099
  }, { passive: true });
1040
1100
 
1041
- function go(n) { const t = clamp(n); slides[t].scrollIntoView(); setActive(t); }
1101
+ // Scroll the deck element itself; scrolling the slide into view would also move an embedding page.
1102
+ function go(n, instant) {
1103
+ const t = clamp(n);
1104
+ deck.scrollTo({ top: t * deck.clientHeight, behavior: instant ? 'auto' : 'smooth' });
1105
+ setActive(t);
1106
+ }
1107
+
1108
+ // Embedders (editors, previews) can drive the deck: postMessage({ type: 'classic-slides:go', index }).
1109
+ window.addEventListener('message', (e) => {
1110
+ const d = e.data;
1111
+ if (!d || typeof d !== 'object') return;
1112
+ if (d.type === 'classic-slides:go' && Number.isInteger(d.index)) go(d.index, true);
1113
+ else if (d.type === 'classic-slides:notes') { if (notes.classList.contains('open') !== !!d.open) toggleNotes(); }
1114
+ });
1042
1115
 
1043
1116
  document.addEventListener('keydown', (e) => {
1044
1117
  if (e.metaKey || e.ctrlKey || e.altKey) return;
@@ -1058,7 +1131,8 @@ var RUNTIME_JS = String.raw`
1058
1131
  }
1059
1132
 
1060
1133
  const m = /^#slide-(\d+)$/.exec(location.hash);
1061
- if (m) requestAnimationFrame(() => go(Number(m[1]) - 1));
1134
+ const initial = m ? Number(m[1]) - 1 : Number(document.body.dataset.initialSlide || 0);
1135
+ if (initial > 0) requestAnimationFrame(() => go(initial, true));
1062
1136
  else setActive(0);
1063
1137
  })();
1064
1138
  `;
@@ -1374,6 +1448,7 @@ img.natural,.media img.natural,.photo-grid img.natural{width:auto;max-width:min(
1374
1448
  `;
1375
1449
 
1376
1450
  // src/render.ts
1451
+ var PREVIEW_SIZE = { width: 1280, height: 720 };
1377
1452
  var passThrough = (src) => {
1378
1453
  if (/^(data:|https?:)/.test(src)) return src;
1379
1454
  throw new Error(
@@ -1422,12 +1497,14 @@ async function renderDeck(input, options = {}) {
1422
1497
  };
1423
1498
  const sections = [];
1424
1499
  const notes = [];
1500
+ const slideMap = [];
1425
1501
  deck.slides.forEach((slide, i) => {
1426
1502
  const layout = getLayout(slide.layout);
1427
1503
  const rendered = layout.render(slide, ctx);
1428
1504
  const list = Array.isArray(rendered) ? rendered : [rendered];
1429
1505
  const track = slide.track ?? layout.defaults?.track ?? "value";
1430
1506
  const bg = slide.background ?? layout.defaults?.background ?? "paper";
1507
+ slideMap.push(list.map((_, j) => sections.length + j));
1431
1508
  list.forEach((r, j) => {
1432
1509
  const classes = ["slide", `layout-${layout.name}`, ...r.classes ?? []];
1433
1510
  if (bg !== "paper") classes.push("solid", `bg-${bg}`);
@@ -1459,7 +1536,7 @@ ${deck.description ? `<meta name="description" content="${escapeAttr(deck.descri
1459
1536
  <style id="brand-fonts">${fontsCss()}</style>
1460
1537
  <style>${tokensCss()}${DECK_CSS}</style>
1461
1538
  </head>
1462
- <body>
1539
+ <body${options.initialSlide ? ` data-initial-slide="${Math.max(0, Math.floor(options.initialSlide))}"` : ""}>
1463
1540
  <div class="rail" id="rail"></div>
1464
1541
  <main class="deck" id="deck">
1465
1542
  ${sections.join("\n")}
@@ -1473,13 +1550,96 @@ ${source}<script>${RUNTIME_JS}</script>
1473
1550
  </body>
1474
1551
  </html>
1475
1552
  `;
1476
- return { html, deck, slideCount: sections.length, warnings: result.issues };
1553
+ return { html, deck, slideCount: sections.length, slideMap, warnings: result.issues };
1477
1554
  }
1478
1555
  function extractDeckSource(html) {
1479
1556
  const m = /<script type="application\/json" id="deck-source">([\s\S]*?)<\/script>/.exec(html);
1480
1557
  return m ? JSON.parse(m[1]) : void 0;
1481
1558
  }
1482
1559
 
1560
+ // src/validate.ts
1561
+ function fromZod(issue) {
1562
+ let message = issue.message;
1563
+ if (issue.code === "invalid_union_discriminator") {
1564
+ message = `Unknown layout. Use one of: ${issue.options.join(", ")}.`;
1565
+ } else if (issue.code === "unrecognized_keys") {
1566
+ message = `Unknown field(s) ${issue.keys.map((k) => `"${k}"`).join(", ")} for this layout.`;
1567
+ }
1568
+ return { level: "error", path: issue.path, message };
1569
+ }
1570
+ function validateDeck(input) {
1571
+ const parsed = DeckSchema.safeParse(input);
1572
+ if (!parsed.success) {
1573
+ return { ok: false, issues: parsed.error.issues.map(fromZod) };
1574
+ }
1575
+ const deck = parsed.data;
1576
+ const issues = [];
1577
+ const steps = deck.journey?.steps.length ?? 0;
1578
+ const seen = /* @__PURE__ */ new Set();
1579
+ deck.slides.forEach((slide, i) => {
1580
+ const layout = getLayout(slide.layout);
1581
+ for (const issue of layout.lint?.(slide) ?? []) {
1582
+ issues.push({ ...issue, path: ["slides", i, ...issue.path] });
1583
+ }
1584
+ if (collectImageSources(slide).has(PLACEHOLDER_IMAGE)) {
1585
+ issues.push({
1586
+ level: "warning",
1587
+ path: ["slides", i],
1588
+ message: "Replace the placeholder photo before presenting."
1589
+ });
1590
+ }
1591
+ if (slide.id) {
1592
+ if (seen.has(slide.id))
1593
+ issues.push({
1594
+ level: "error",
1595
+ path: ["slides", i, "id"],
1596
+ message: `Duplicate slide id "${slide.id}".`
1597
+ });
1598
+ seen.add(slide.id);
1599
+ }
1600
+ if ((slide.step || slide.showJourney || slide.layout === "journey") && !steps) {
1601
+ issues.push({
1602
+ level: "error",
1603
+ path: ["slides", i, slide.step ? "step" : "layout"],
1604
+ message: "This slide uses the journey, but the deck has no journey.steps defined."
1605
+ });
1606
+ }
1607
+ if (slide.step && steps && slide.step > steps) {
1608
+ issues.push({
1609
+ level: "error",
1610
+ path: ["slides", i, "step"],
1611
+ message: `Step ${slide.step} is past the last journey step (${steps}).`
1612
+ });
1613
+ }
1614
+ });
1615
+ if (deck.slides[0]?.layout !== "title") {
1616
+ issues.push({
1617
+ level: "warning",
1618
+ path: ["slides", 0, "layout"],
1619
+ message: 'Decks usually open with a "title" slide.'
1620
+ });
1621
+ }
1622
+ return { ok: !issues.some((i) => i.level === "error"), deck, issues };
1623
+ }
1624
+ function formatPath(path) {
1625
+ if (!path.length) return "deck";
1626
+ return path.map(
1627
+ (p, i) => typeof p === "number" ? path[i - 1] === "slides" ? `slide ${p + 1}` : String(p + 1) : p
1628
+ ).filter((p) => p !== "slides").join(" \u203A ");
1629
+ }
1630
+ function formatIssues(issues) {
1631
+ return issues.map((i) => `${i.level === "error" ? "\u2716" : "\u26A0"} ${formatPath(i.path)}: ${i.message}`).join("\n");
1632
+ }
1633
+ var DeckError = class extends Error {
1634
+ constructor(issues) {
1635
+ super(`Deck is not valid:
1636
+ ${formatIssues(issues.filter((i) => i.level === "error"))}`);
1637
+ this.issues = issues;
1638
+ this.name = "DeckError";
1639
+ }
1640
+ issues;
1641
+ };
1642
+
1483
1643
  // src/parse.ts
1484
1644
  import YAML from "yaml";
1485
1645
  function parseDeckSource(text) {
@@ -1497,24 +1657,28 @@ export {
1497
1657
  escapeHtml,
1498
1658
  inline,
1499
1659
  prose,
1500
- Text,
1501
- Image,
1502
- Item,
1503
1660
  baseSlide,
1504
1661
  PALETTE,
1505
1662
  LOGO_SVG,
1506
1663
  LAYOUTS,
1507
1664
  getLayout,
1665
+ commonSlideFields,
1666
+ layoutEditorInfo,
1667
+ allLayoutEditorInfo,
1668
+ PLACEHOLDER_IMAGE,
1669
+ isPlaceholderImage,
1670
+ blankSlide,
1508
1671
  SlideSchema,
1509
1672
  DeckSchema,
1510
1673
  validateDeck,
1511
1674
  formatPath,
1512
1675
  formatIssues,
1513
1676
  DeckError,
1677
+ PREVIEW_SIZE,
1514
1678
  collectImageSources,
1515
1679
  renderDeck,
1516
1680
  extractDeckSource,
1517
1681
  parseDeckSource,
1518
1682
  stringifyDeck
1519
1683
  };
1520
- //# sourceMappingURL=chunk-TM3TNVHB.js.map
1684
+ //# sourceMappingURL=chunk-7FROOJCY.js.map