@classic-homes/slides 0.5.5 → 0.5.7

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.
@@ -1,14 +1,40 @@
1
1
  import {
2
2
  DeckSchema,
3
+ Image,
4
+ Item,
3
5
  LAYOUTS,
4
6
  LAYOUT_CATEGORIES,
5
7
  baseSlide,
6
8
  getLayout
7
- } from "./chunk-ULP6ACVZ.js";
9
+ } from "./chunk-ILE46F7Y.js";
8
10
 
9
11
  // src/catalog.ts
10
12
  import { z } from "zod";
11
- import { zodToJsonSchema } from "zod-to-json-schema";
13
+ import { ignoreOverride, zodToJsonSchema } from "zod-to-json-schema";
14
+ var SHORTHAND = /* @__PURE__ */ new Map([
15
+ [Item._def.schema, "Shorthand for { text }."],
16
+ [Image._def.schema, "Shorthand for { src }."]
17
+ ]);
18
+ function toJsonSchema(schema, name) {
19
+ return zodToJsonSchema(schema, {
20
+ ...name ? { name } : {},
21
+ $refStrategy: "none",
22
+ override: (def) => {
23
+ const inner = def.schema;
24
+ const isEffects = def.typeName === "ZodEffects";
25
+ const hint = isEffects ? SHORTHAND.get(inner) : void 0;
26
+ if (!hint) return ignoreOverride;
27
+ const object = zodToJsonSchema(inner, {
28
+ $refStrategy: "none"
29
+ });
30
+ delete object.$schema;
31
+ return {
32
+ anyOf: [{ type: "string", description: hint }, object],
33
+ ...def.description ? { description: def.description } : {}
34
+ };
35
+ }
36
+ });
37
+ }
12
38
  function listLayouts() {
13
39
  return LAYOUTS.map((l) => ({
14
40
  name: l.name,
@@ -26,12 +52,12 @@ function describeLayout(name) {
26
52
  description: layout.description,
27
53
  whenToUse: layout.whenToUse,
28
54
  defaults: layout.defaults ?? {},
29
- schema: zodToJsonSchema(schema, { $refStrategy: "none" }),
55
+ schema: toJsonSchema(schema),
30
56
  example: layout.example
31
57
  };
32
58
  }
33
59
  function deckJsonSchema() {
34
- return zodToJsonSchema(DeckSchema, { name: "ClassicDeck", $refStrategy: "none" });
60
+ return toJsonSchema(DeckSchema, "ClassicDeck");
35
61
  }
36
62
  var AUTHORING_GUIDE = `# Writing a Classic Homes deck
37
63
 
@@ -104,4 +130,4 @@ export {
104
130
  deckJsonSchema,
105
131
  AUTHORING_GUIDE
106
132
  };
107
- //# sourceMappingURL=chunk-GNTIZN4Y.js.map
133
+ //# sourceMappingURL=chunk-AJFBVFYM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["import { z } from 'zod';\nimport { ignoreOverride, zodToJsonSchema } from 'zod-to-json-schema';\nimport {\n LAYOUT_CATEGORIES,\n LAYOUTS,\n getLayout,\n type LayoutCategory,\n type LayoutName,\n} from './layouts/index.js';\nimport { baseSlide, Image, Item } from './schema/common.js';\nimport { DeckSchema } from './schema/deck.js';\n\n/**\n * Item and Image accept a bare string as shorthand (\"Q4 best\" → { text }, \"a.jpg\" → { src }).\n * zod-to-json-schema only sees the object behind the preprocess, so advertise both forms\n * or agents reading the schema think the shorthand in our own examples is invalid.\n */\nconst SHORTHAND = new Map<unknown, string>([\n [(Item._def as { schema: unknown }).schema, 'Shorthand for { text }.'],\n [(Image._def as { schema: unknown }).schema, 'Shorthand for { src }.'],\n]);\n\nfunction toJsonSchema(schema: z.ZodTypeAny, name?: string) {\n return zodToJsonSchema(schema, {\n ...(name ? { name } : {}),\n $refStrategy: 'none',\n override: (def) => {\n const inner = (def as { schema?: unknown }).schema;\n const isEffects = (def as { typeName?: string }).typeName === 'ZodEffects';\n const hint = isEffects ? SHORTHAND.get(inner) : undefined;\n if (!hint) return ignoreOverride;\n const object = zodToJsonSchema(inner as z.ZodTypeAny, {\n $refStrategy: 'none',\n }) as Record<string, unknown>;\n delete object.$schema;\n return {\n anyOf: [{ type: 'string', description: hint }, object],\n ...((def as { description?: string }).description\n ? { description: (def as { description?: string }).description }\n : {}),\n };\n },\n });\n}\n\nexport interface LayoutSummary {\n name: string;\n category: LayoutCategory;\n description: string;\n whenToUse: string;\n}\n\nexport function listLayouts(): LayoutSummary[] {\n return LAYOUTS.map((l) => ({\n name: l.name,\n category: LAYOUT_CATEGORIES[l.name as LayoutName],\n description: l.description,\n whenToUse: l.whenToUse,\n }));\n}\n\n/** Full description of one layout: fields as JSON Schema plus a working example. */\nexport function describeLayout(name: string) {\n const layout = getLayout(name);\n if (!layout) return undefined;\n const schema = z.object({ layout: z.literal(layout.name), ...layout.shape, ...baseSlide });\n return {\n name: layout.name,\n description: layout.description,\n whenToUse: layout.whenToUse,\n defaults: layout.defaults ?? {},\n schema: toJsonSchema(schema),\n example: layout.example,\n };\n}\n\n/** JSON Schema for a whole deck (editor tooling, `$schema` in deck files). */\nexport function deckJsonSchema() {\n return toJsonSchema(DeckSchema, 'ClassicDeck');\n}\n\n/** Authoring guidance shared by the CLI, MCP server and docs. */\nexport const AUTHORING_GUIDE = `# Writing a Classic Homes deck\n\nA deck is a YAML (or JSON) file: deck-level fields plus a list of slides. Each slide\npicks a **layout** and fills its fields. Layouts own all styling — there is no way to\nset colours, fonts or sizes directly, which is what keeps every deck on-brand.\n\n## Principles\n- **One idea per slide.** The headline states the point. Detail goes in \\`notes\\`.\n- **Short copy.** Titles under ~70 characters, bullets a few words, at most 6 bullets.\n \\`validate\\` warns when a slide gets crowded — split it rather than shrinking text.\n- **Speaker notes on every content slide.** Presenters press N (or P for presenter view).\n- **Real photos over decoration.** Every image needs \\`alt\\` text.\n- **Tracks** colour the accent: \\`value\\` (red, default) for the homeowner/value story,\n \\`build\\` (teal) for construction/process, \\`neutral\\` (navy) for framing slides.\n- **Backgrounds**: \\`paper\\` (default) for content; solid \\`navy\\`/\\`red\\`/\\`teal\\`/\\`plum\\`/\\`grey\\`/\\`stone\\`\n for dividers. \\`backgroundImage\\` + \\`overlay\\` puts a photo behind title/section/statement slides.\n\n## Picking a layout\n- **Numbers:** \\`stats\\` (2–6 headline figures, optional trend + sparkline), \\`chart\\` (a real chart —\n put the takeaway in the title), \\`table\\` (exact figures people compare).\n- **Processes & structure:** \\`flow\\` (ordered steps with arrows), \\`timeline\\` (dated milestones, phases),\n \\`cycle\\` (things that repeat), \\`hierarchy\\` (org chart / tree), \\`matrix\\` (2×2), \\`tiers\\`\n (funnel or pyramid), \\`walkthrough\\` (photo per stage), \\`journey\\` (deck agenda).\n- **Media:** \\`split\\` (text + photos), \\`photos\\`, \\`image\\` (with numbered \\`hotspots\\` for floor plans\n and site maps), \\`compare\\` (before/after slider), \\`video\\` (YouTube/Vimeo), \\`embed\\` (any https page:\n maps, dashboards, 3D tours, forms).\n- **Words:** \\`statement\\`, \\`bullets\\`, \\`cards\\`, \\`comparison\\`, \\`quote\\`.\n- **Wrap-up:** \\`qr\\` (send people somewhere), \\`contacts\\`, closing \\`statement\\`.\n\n## Charts\n\\`chart.labels\\` are the categories; each series has one value per label. Pie, donut, funnel and\ngauge use the first series; gauge needs \\`max\\`; scatter uses two series as x and y. Use\n\\`highlight\\` to emphasise one category and \\`format\\`/\\`unit\\` instead of typing symbols into numbers.\n\n## Video & embeds\nThey need internet while presenting. Offline, in print and in PDF they show a poster (or\n\\`fallback\\` screenshot) with a QR code. Some sites refuse to be framed — the \"Open ↗\" link\nalways works; add a \\`fallback\\` image for anything critical.\n\n## Brand\n\\`brand.profile\\` picks who the deck speaks for (default \\`classic-homes\\`). You may override\n\\`name\\`, \\`tagline\\`, \\`logo\\`, \\`logoOnDark\\`, \\`favicon\\` and \\`footer\\`; colours come from the profile.\n\\`settings\\` controls slide numbers, logo placement, a confidential label and kiosk auto-advance.\n\n## Presenting\nArrow keys / Space advance (and step through \\`build: true\\` reveals). N notes · P presenter window\n(current + next slide, notes, timer) · O overview · F fullscreen · \\`?kiosk=15\\` on the URL loops\nunattended. \\`hidden: true\\` skips a slide without deleting it.\n\n## Inline text\n\\`**bold**\\`, \\`*highlight*\\` (accent colour), \\`\\\\\\`code\\\\\\`\\`, \\`[label](https://…)\\`. No HTML.\n\n## Journey decks\nSet \\`journey.steps\\` at deck level to get a numbered agenda. Then:\n- a \\`journey\\` slide shows the full map,\n- \\`step: 3\\` on any slide draws the vertical STEP rail,\n- \\`showJourney: true\\` adds the compact \"you are here\" strip (great on \\`section\\` slides).\n\n## Typical shape\n1. \\`title\\`\n2. \\`statement\\` or \\`journey\\` (why we're here / the agenda)\n3. per part: \\`section\\` → 2–5 content slides\n4. \\`qr\\` (where to go next) → \\`contacts\\` → closing \\`statement\\`\n`;\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,SAAS;AAClB,SAAS,gBAAgB,uBAAuB;AAgBhD,IAAM,YAAY,oBAAI,IAAqB;AAAA,EACzC,CAAE,KAAK,KAA6B,QAAQ,yBAAyB;AAAA,EACrE,CAAE,MAAM,KAA6B,QAAQ,wBAAwB;AACvE,CAAC;AAED,SAAS,aAAa,QAAsB,MAAe;AACzD,SAAO,gBAAgB,QAAQ;AAAA,IAC7B,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,cAAc;AAAA,IACd,UAAU,CAAC,QAAQ;AACjB,YAAM,QAAS,IAA6B;AAC5C,YAAM,YAAa,IAA8B,aAAa;AAC9D,YAAM,OAAO,YAAY,UAAU,IAAI,KAAK,IAAI;AAChD,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,SAAS,gBAAgB,OAAuB;AAAA,QACpD,cAAc;AAAA,MAChB,CAAC;AACD,aAAO,OAAO;AACd,aAAO;AAAA,QACL,OAAO,CAAC,EAAE,MAAM,UAAU,aAAa,KAAK,GAAG,MAAM;AAAA,QACrD,GAAK,IAAiC,cAClC,EAAE,aAAc,IAAiC,YAAY,IAC7D,CAAC;AAAA,MACP;AAAA,IACF;AAAA,EACF,CAAC;AACH;AASO,SAAS,cAA+B;AAC7C,SAAO,QAAQ,IAAI,CAAC,OAAO;AAAA,IACzB,MAAM,EAAE;AAAA,IACR,UAAU,kBAAkB,EAAE,IAAkB;AAAA,IAChD,aAAa,EAAE;AAAA,IACf,WAAW,EAAE;AAAA,EACf,EAAE;AACJ;AAGO,SAAS,eAAe,MAAc;AAC3C,QAAM,SAAS,UAAU,IAAI;AAC7B,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,OAAO,IAAI,GAAG,GAAG,OAAO,OAAO,GAAG,UAAU,CAAC;AACzF,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO,YAAY,CAAC;AAAA,IAC9B,QAAQ,aAAa,MAAM;AAAA,IAC3B,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,iBAAiB;AAC/B,SAAO,aAAa,YAAY,aAAa;AAC/C;AAGO,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}
@@ -195,7 +195,9 @@ function tokensCss(palette) {
195
195
  --font-display:"Spectral",Georgia,"Times New Roman",serif;
196
196
  --font-body:"Figtree",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
197
197
  --font-mono:ui-monospace,"SF Mono","Cascadia Code","Roboto Mono",Menlo,Consolas,monospace;
198
- --maxw:1120px}`;
198
+ --slide-pad-x:clamp(24px,4vw,96px);--slide-pad-y:clamp(28px,4vh,56px);
199
+ /* Grow with the screen, reserving height for titles and footers on wide, short windows. */
200
+ --maxw:calc(200vh - 176px)}`;
199
201
  }
200
202
  function fontsCss() {
201
203
  const face = (family, style, weight, key) => `@font-face{font-family:'${family}';font-style:${style};font-weight:${weight};font-display:swap;src:url(data:font/woff2;base64,${FONTS[key]}) format('woff2')}`;
@@ -2846,7 +2848,7 @@ img{display:block;max-width:100%}
2846
2848
  .deck{height:100vh;overflow-y:scroll;scroll-snap-type:y mandatory;scroll-behavior:smooth}
2847
2849
  @media (prefers-reduced-motion:reduce){.deck{scroll-behavior:auto}}
2848
2850
  .slide{height:100vh;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;flex-direction:column;justify-content:center;
2849
- padding:clamp(24px,5.5vh,76px) clamp(28px,8vw,116px);overflow:hidden;
2851
+ --slide-pad-left:var(--slide-pad-x);padding:var(--slide-pad-y) var(--slide-pad-x) var(--slide-pad-y) var(--slide-pad-left);overflow:hidden;
2850
2852
  background:linear-gradient(var(--grid) 1px,transparent 1px) 0 0/100% 46px,linear-gradient(90deg,var(--grid) 1px,transparent 1px) 0 0/46px 100%,var(--paper)}
2851
2853
  .slide__inner{width:100%;max-width:var(--maxw);margin:0 auto;position:relative}
2852
2854
  .slide::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;background:var(--red)}
@@ -3094,7 +3096,7 @@ img.natural,.media img.natural,.photo-grid img.natural{width:auto;max-width:min(
3094
3096
  .slide.solid .roadmap.mini .rn{background:transparent;border-color:rgba(255,255,255,.55);color:rgba(255,255,255,.85)}
3095
3097
  .slide.solid .roadmap.mini .rstep::before{background:rgba(255,255,255,.3)}
3096
3098
  .slide.solid .roadmap.mini .rstep.active .rn{background:#fff;border-color:#fff;color:var(--red)}
3097
- .slide.has-step{padding-left:clamp(92px,10vw,168px)}
3099
+ .slide.has-step{--slide-pad-left:calc(clamp(60px,6.5vw,100px) + var(--slide-pad-x))}
3098
3100
  .slide.has-step::before{display:none}
3099
3101
  .steprail{position:absolute;left:0;top:0;bottom:0;width:clamp(60px,6.5vw,100px);background:var(--red);display:flex;flex-direction:column;align-items:center;z-index:3}
3100
3102
  .slide[data-track="build"] .steprail{background:var(--teal-fill)}
@@ -3159,8 +3161,7 @@ var DECK_CSS_EXTRA = String.raw`
3159
3161
  .slide.has-bg.solid .lead{color:#fff}
3160
3162
 
3161
3163
  /* ---------- footer, hidden ---------- */
3162
- .slide-foot{position:absolute;left:clamp(28px,8vw,116px);right:clamp(28px,8vw,116px);bottom:14px;display:flex;align-items:center;justify-content:space-between;gap:16px;font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;color:var(--ink-faint);pointer-events:none}
3163
- .slide.has-step .slide-foot{left:clamp(92px,10vw,168px)}
3164
+ .slide-foot{position:absolute;left:var(--slide-pad-left);right:var(--slide-pad-x);max-width:var(--maxw);margin:0 auto;bottom:14px;display:flex;align-items:center;justify-content:space-between;gap:16px;font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;color:var(--ink-faint);pointer-events:none}
3164
3165
  .slide-foot .foot-l{display:flex;align-items:center;gap:16px}
3165
3166
  .slide-foot .foot-l>span+span::before{content:"·";margin-right:16px}
3166
3167
  .slide-foot .foot-logo .logo{font-size:12px}
@@ -3718,39 +3719,52 @@ function fromZod(issue) {
3718
3719
  }
3719
3720
  return { level: "error", path: issue.path, message };
3720
3721
  }
3722
+ function slideIssues(slide, i) {
3723
+ const issues = [];
3724
+ const layout = getLayout(slide.layout);
3725
+ for (const url of layout.prepare?.(slide)?.qr ?? []) {
3726
+ try {
3727
+ if (url.length > 1e4) throw new Error("URL too long");
3728
+ QRCode2.create(url, { errorCorrectionLevel: "M" });
3729
+ } catch {
3730
+ issues.push({
3731
+ level: "error",
3732
+ path: ["slides", i, "url"],
3733
+ message: "This URL is too long for a QR code. Use a shorter URL."
3734
+ });
3735
+ }
3736
+ }
3737
+ for (const issue of layout.lint?.(slide) ?? []) {
3738
+ issues.push({ ...issue, path: ["slides", i, ...issue.path] });
3739
+ }
3740
+ if (collectImageSources(slide).has(PLACEHOLDER_IMAGE)) {
3741
+ issues.push({
3742
+ level: "warning",
3743
+ path: ["slides", i],
3744
+ message: "Replace the placeholder photo before presenting."
3745
+ });
3746
+ }
3747
+ return issues;
3748
+ }
3721
3749
  function validateDeck(input, options = {}) {
3722
3750
  const parsed = DeckSchema.safeParse(input);
3723
3751
  if (!parsed.success) {
3724
- return { ok: false, issues: parsed.error.issues.map(fromZod) };
3752
+ const issues2 = parsed.error.issues.map(fromZod);
3753
+ const raw = input?.slides;
3754
+ if (Array.isArray(raw)) {
3755
+ raw.forEach((candidate, i) => {
3756
+ const slide = SlideSchema.safeParse(candidate);
3757
+ if (slide.success) issues2.push(...slideIssues(slide.data, i));
3758
+ });
3759
+ }
3760
+ return { ok: false, issues: issues2 };
3725
3761
  }
3726
3762
  const deck = parsed.data;
3727
3763
  const issues = [];
3728
3764
  const steps = deck.journey?.steps.length ?? 0;
3729
3765
  const seen = /* @__PURE__ */ new Set();
3730
3766
  deck.slides.forEach((slide, i) => {
3731
- const layout = getLayout(slide.layout);
3732
- for (const url of layout.prepare?.(slide)?.qr ?? []) {
3733
- try {
3734
- if (url.length > 1e4) throw new Error("URL too long");
3735
- QRCode2.create(url, { errorCorrectionLevel: "M" });
3736
- } catch {
3737
- issues.push({
3738
- level: "error",
3739
- path: ["slides", i, "url"],
3740
- message: "This URL is too long for a QR code. Use a shorter URL."
3741
- });
3742
- }
3743
- }
3744
- for (const issue of layout.lint?.(slide) ?? []) {
3745
- issues.push({ ...issue, path: ["slides", i, ...issue.path] });
3746
- }
3747
- if (collectImageSources(slide).has(PLACEHOLDER_IMAGE)) {
3748
- issues.push({
3749
- level: "warning",
3750
- path: ["slides", i],
3751
- message: "Replace the placeholder photo before presenting."
3752
- });
3753
- }
3767
+ issues.push(...slideIssues(slide, i));
3754
3768
  if (slide.id) {
3755
3769
  if (seen.has(slide.id))
3756
3770
  issues.push({
@@ -3900,6 +3914,8 @@ function externalizeDeckImages(input, upload, options = {}) {
3900
3914
  }
3901
3915
 
3902
3916
  export {
3917
+ Image,
3918
+ Item,
3903
3919
  baseSlide,
3904
3920
  BrandPaletteSchema,
3905
3921
  BrandProfileSchema,
@@ -3962,4 +3978,4 @@ export {
3962
3978
  inlineDeckImages,
3963
3979
  externalizeDeckImages
3964
3980
  };
3965
- //# sourceMappingURL=chunk-ULP6ACVZ.js.map
3981
+ //# sourceMappingURL=chunk-ILE46F7Y.js.map