@tycoworks/tycoslide 0.10.1 → 0.11.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.
@@ -21,9 +21,12 @@ import { strict } from "./strict.js";
21
21
  * `z.record`, open by design); strictness lands on their leaf entries.
22
22
  */
23
23
  // Reuse the const-object enums as runtime values — no third copy of the literals.
24
- const acceptTypeSchema = z.enum(Object.values(AcceptType));
25
24
  const assetTypeSchema = z.enum(Object.values(AssetType));
26
25
  const variantSchema = z.enum(Object.values(Variant));
26
+ // A table specimen's repeatable row range: `[start, end]`, 0-based inclusive,
27
+ // non-negative integers. The range is validated against the specimen's actual row
28
+ // count at fill time, where the row count is known.
29
+ const bodyRowsSchema = z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]);
27
30
  // Re-declared here (not imported from the engine) so the schema layer never
28
31
  // depends on the engine — mirrors engine `Frame`, guarded by `_drift`.
29
32
  const FrameSchema = strict({
@@ -32,16 +35,10 @@ const FrameSchema = strict({
32
35
  cx: z.number(),
33
36
  cy: z.number(),
34
37
  });
35
- const LimitSchema = strict({
36
- maxChars: z.number().optional(),
37
- maxLines: z.number().optional(),
38
- maxItems: z.number().optional(),
39
- });
40
38
  const AssetEntrySchema = strict({
41
39
  path: z.string(),
42
40
  type: assetTypeSchema,
43
41
  description: z.string(),
44
- whenToUse: z.string().optional(),
45
42
  });
46
43
  // AssetCatalog: `{ category: { name: AssetEntry } }`. The two record levels are
47
44
  // OPEN (names are user-defined); only the leaf entry is strict.
@@ -68,7 +65,6 @@ const ThemeFontSchema = strict({
68
65
  });
69
66
  const TemplateParamSchema = strict({
70
67
  shapeName: z.string(),
71
- limit: LimitSchema.optional(),
72
68
  required: z.boolean().optional(),
73
69
  type: z.literal(ParameterType.Template),
74
70
  template: z.string(),
@@ -80,25 +76,38 @@ const ImageParamSchema = strict({
80
76
  type: z.literal(ParameterType.Image),
81
77
  });
82
78
  const ParameterSchema = z.discriminatedUnion("type", [TemplateParamSchema, ImageParamSchema]);
83
- const BlockSchema = strict({
84
- type: acceptTypeSchema,
79
+ // One arm per accept type, discriminated by `type`, mirroring `CompilerBlock`:
80
+ // `startAt` lives only on the text arm, and `bodyRows` only on the table arm (a
81
+ // table block MUST declare its repeatable-row range; text/image arms have no
82
+ // `bodyRows` field, so a stray one is an unknown key the `strict` arm rejects).
83
+ const TextBlockSchema = strict({
84
+ type: z.literal(AcceptType.Text),
85
85
  sourceSlide: z.number(),
86
86
  shapeName: z.string(),
87
87
  startAt: z.number().optional(),
88
88
  });
89
+ const TableBlockSchema = strict({
90
+ type: z.literal(AcceptType.Table),
91
+ sourceSlide: z.number(),
92
+ shapeName: z.string(),
93
+ bodyRows: bodyRowsSchema,
94
+ });
95
+ const ImageBlockSchema = strict({
96
+ type: z.literal(AcceptType.Image),
97
+ sourceSlide: z.number(),
98
+ shapeName: z.string(),
99
+ });
100
+ const BlockSchema = z.discriminatedUnion("type", [TextBlockSchema, TableBlockSchema, ImageBlockSchema]);
89
101
  const SlotSchema = strict({
90
102
  key: z.string(),
91
103
  accepts: z.array(BlockSchema),
92
104
  frame: FrameSchema.optional(),
93
- limit: LimitSchema.optional(),
94
105
  required: z.boolean().optional(),
95
106
  });
96
107
  const LayoutSchema = strict({
97
108
  name: z.string(),
98
109
  slideNumber: z.number(),
99
110
  description: z.string().optional(),
100
- whenToUse: z.string().optional(),
101
- whenNotToUse: z.string().optional(),
102
111
  variant: variantSchema.optional(),
103
112
  parameters: z.array(ParameterSchema),
104
113
  slots: z.array(SlotSchema),
@@ -107,7 +116,6 @@ export const ThemeConfigSchema = strict({
107
116
  layouts: z.array(LayoutSchema),
108
117
  assets: AssetCatalogSchema,
109
118
  template: z.string(),
110
- outputDir: z.string().optional(),
111
119
  mermaid: MermaidConfigSchema.optional(),
112
120
  fonts: z.array(ThemeFontSchema).optional(),
113
121
  codeTheme: z.union([z.string(), strict({ light: z.string().min(1), dark: z.string().min(1) })]).optional(),
@@ -1,7 +1,6 @@
1
1
  export interface RawSlide {
2
2
  index: number;
3
3
  frontmatter: Record<string, unknown>;
4
- body: string;
5
4
  slots: Record<string, string>;
6
5
  }
7
6
  export interface ParsedDocument {
@@ -123,8 +123,11 @@ function splitIntoSlides(text) {
123
123
  // ============================================
124
124
  function buildSlide(index, fmString, rawContent) {
125
125
  const frontmatter = parseFrontmatter(fmString, index);
126
- const { defaultSlot, slots } = extractSlots(rawContent);
127
- return { index, frontmatter, body: defaultSlot, slots };
126
+ const { leading, slots } = extractSlots(rawContent);
127
+ if (leading.trim()) {
128
+ throw new Error(`Slide ${index + 1}: text found outside a ::slot:: marker`);
129
+ }
130
+ return { index, frontmatter, slots };
128
131
  }
129
132
  export class FrontmatterParseError extends Error {
130
133
  constructor(slideIndex, yamlSource, cause) {
@@ -185,12 +188,12 @@ function extractSlots(content) {
185
188
  slotLines.get(currentSlot).push(line);
186
189
  }
187
190
  }
188
- const defaultSlot = slotLines.get(null).join("\n").trim();
191
+ const leading = slotLines.get(null).join("\n").trim();
189
192
  const slots = {};
190
193
  for (const [name, sLines] of slotLines) {
191
194
  if (name !== null) {
192
195
  slots[name] = sLines.join("\n").trim();
193
196
  }
194
197
  }
195
- return { defaultSlot, slots };
198
+ return { leading, slots };
196
199
  }
@@ -1,6 +1,7 @@
1
1
  import type { RootContent } from "mdast";
2
- import { type Frame, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
2
+ import { type BodyRows, type Frame, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
3
3
  import type { MermaidConfig } from "./blocks/mermaidTheme.js";
4
+ export type { BodyRows };
4
5
  /**
5
6
  * An asset's scaling/cropping tolerance, declared in the theme catalog. The
6
7
  * compiler maps it to the engine's object-fit `fit`. `icon`: never enlarge,
@@ -23,7 +24,6 @@ export type AssetEntry = {
23
24
  /** Required — a missing type is a fail-fast error. */
24
25
  type: AssetType;
25
26
  description: string;
26
- whenToUse?: string;
27
27
  };
28
28
  /** Two-level catalog: `{ category: { name: AssetEntry } }`. */
29
29
  export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
@@ -83,8 +83,8 @@ export type BlockFill = TextFill | TableFill | ImageFill;
83
83
  * asset resolver, the diagnostic context to name the offending layout/slide/slot
84
84
  * when a region's markdown shape is illegal (a stray standalone block mixed into
85
85
  * prose), and the theme-level `config` — from which code/mermaid compile read
86
- * their one-per-theme style (`codeTheme`, `mermaid`, `mermaidVariant`,
87
- * `outputDir`), not the slot.
86
+ * their one-per-theme style (`codeTheme`, `mermaid`, `mermaidVariant`), not the
87
+ * slot.
88
88
  *
89
89
  * Lives in types.ts (not in `blocks/registry.ts`, which re-exports it) so a
90
90
  * per-kind block file can import it without importing the registry — the registry
@@ -111,7 +111,7 @@ export type BlockContext = {
111
111
  * build (`compile`) live together — a new content kind is one file, one registry
112
112
  * row. Non-generic like the engine's `Filler`: the array can't correlate a
113
113
  * per-element type guard with `compile`'s param, so each handler narrows the node
114
- * internally with a single-hop cast. Mirrors the old sdk's `SyntaxHandler`.
114
+ * internally with a single-hop cast.
115
115
  */
116
116
  export type BlockHandler = {
117
117
  match(node: RootContent): boolean;
@@ -138,20 +138,10 @@ export type CompilerDeckStep = {
138
138
  */
139
139
  export type CompilerDeck = {
140
140
  theme: string;
141
+ /** Output path, set by the caller (the CLI, or a programmatic `buildDeck`), not from frontmatter. */
141
142
  output?: string;
142
143
  steps: CompilerDeckStep[];
143
144
  };
144
- /**
145
- * Markdown-flavored measurement hints on a slot or template parameter: caps on
146
- * expanded run text, line count, or list items. All optional — a missing cap is
147
- * "no limit." Advisory metadata surfaced in the manifest; the fill never enforces
148
- * it.
149
- */
150
- export type Limit = {
151
- maxChars?: number;
152
- maxLines?: number;
153
- maxItems?: number;
154
- };
155
145
  /**
156
146
  * Template parameter: a styled text shape filled by expanding a `template` into the
157
147
  * shape's paragraphs via fillTemplate. The template is one string with `{key}`
@@ -162,11 +152,11 @@ export type Limit = {
162
152
  */
163
153
  export type CompilerTemplateParameter = {
164
154
  shapeName: string;
165
- limit?: Limit;
166
155
  /**
167
156
  * Whether the parameter may be omitted from a slide. Optional (defaults to
168
157
  * false): a required one with no value causes the compiler to throw with
169
- * layout + key names.
158
+ * layout + key names; an optional one left unfilled has its shape removed
159
+ * from the slide.
170
160
  */
171
161
  required?: boolean;
172
162
  type: typeof ParameterType.Template;
@@ -174,14 +164,14 @@ export type CompilerTemplateParameter = {
174
164
  template: string;
175
165
  };
176
166
  /** Image parameter: one frontmatter path filled by fillImage. Sizing/crop
177
- * behaviour comes from the resolved asset's `type`, not the slot. Carries no
178
- * `limit` — measuring an image path against char/line caps is meaningless. */
167
+ * behaviour comes from the resolved asset's `type`, not the slot. */
179
168
  export type CompilerImageParameter = {
180
169
  shapeName: string;
181
170
  /**
182
171
  * Whether the parameter may be omitted from a slide. Optional (defaults to
183
172
  * false): a required one with no value causes the compiler to throw with
184
- * layout + key names.
173
+ * layout + key names; an optional one left unfilled has its shape removed
174
+ * from the slide.
185
175
  */
186
176
  required?: boolean;
187
177
  key: string;
@@ -193,63 +183,72 @@ export type CompilerImageParameter = {
193
183
  */
194
184
  export type CompilerParameter = CompilerTemplateParameter | CompilerImageParameter;
195
185
  /**
196
- * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
197
- * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
186
+ * Reserved keys in a deck's frontmatter — global (theme) and per-slide
187
+ * (layout). Exported so callers (e.g. cli.ts) reference the constants
198
188
  * instead of literal strings.
199
189
  */
200
190
  export declare const RESERVED_KEY: {
201
191
  readonly LAYOUT: "layout";
202
- readonly BODY: "body";
203
- readonly OUTPUT: "output";
204
192
  readonly THEME: "theme";
205
193
  readonly NOTES: "notes";
206
194
  };
207
195
  /**
208
196
  * A kind of content a slot accepts, and the real template shape that realizes
209
- * it — the compiler mirror of the engine's `Block`. `type` is an engine content
210
- * type (`text` | `table` | `image`); `shapeName` names the shape on
211
- * `sourceSlide` carrying the specimen styling. When `sourceSlide` equals the
197
+ * it — the compiler mirror of the engine's `Block`, a union discriminated by
198
+ * `type` (an engine content type: `text` | `table` | `image`) so each variant
199
+ * carries only its own specimen options. `shapeName` names the shape on
200
+ * `sourceSlide` carrying the specimen styling; when `sourceSlide` equals the
212
201
  * layout's `slideNumber` the shape is already on the cloned slide (fill in
213
- * place); otherwise it is transplanted into the slot's `frame`. `startAt` is a
214
- * text-specimen concern (leave the first N specimen paragraphs untouched), only
215
- * meaningful on a text block. Named `CompilerBlock` to stay distinct from the
216
- * engine's `Block` and the compiler's `BlockHandler`.
202
+ * place), otherwise it is transplanted into the slot's `frame`. A text block may
203
+ * pin `startAt` (leave the first N specimen paragraphs untouched); a table block
204
+ * MUST declare its `bodyRows` range (the repeatable `<a:tbl>` specimen rows); an
205
+ * image block carries neither. There is no `Template` variant `AcceptType` excludes
206
+ * it (a parameter, not a body block). Named `CompilerBlock` to stay distinct from
207
+ * the engine's `Block` and the compiler's `BlockHandler`.
217
208
  */
218
- export type CompilerBlock = {
219
- type: AcceptType;
209
+ export type CompilerTextBlock = {
210
+ type: typeof AcceptType.Text;
220
211
  sourceSlide: number;
221
212
  shapeName: string;
222
213
  startAt?: number;
223
214
  };
215
+ export type CompilerTableBlock = {
216
+ type: typeof AcceptType.Table;
217
+ sourceSlide: number;
218
+ shapeName: string;
219
+ bodyRows: BodyRows;
220
+ };
221
+ export type CompilerImageBlock = {
222
+ type: typeof AcceptType.Image;
223
+ sourceSlide: number;
224
+ shapeName: string;
225
+ };
226
+ export type CompilerBlock = CompilerTextBlock | CompilerTableBlock | CompilerImageBlock;
224
227
  /**
225
- * Compiler-facing slot. A layout's body region (the default region, or a
226
- * `::name::` region), no longer welded to one shape+type: it `accepts` a set of
227
- * `CompilerBlock`s and owns a `frame` (real observed EMU coordinates, never
228
+ * Compiler-facing slot. A layout's body region (a `::name::` region): it
229
+ * `accepts` a set of `CompilerBlock`s and owns a `frame` (real observed EMU coordinates, never
228
230
  * computed). The author's markdown shape selects which accepted block a region
229
231
  * routes to; a type the slot does not accept fails fast. `frame` is required
230
232
  * only when a slot has a transplant block (a block whose `sourceSlide` differs
231
233
  * from the layout's `slideNumber`); a base-only slot fills in place and needs
232
- * none. Adds markdown-flavored `limit` hints on top of the engine's minimal
233
- * Slot; per-slot `codeTheme` / `mermaidVariant` moved to the theme level.
234
+ * none.
234
235
  */
235
236
  export type CompilerSlot = {
236
237
  key: string;
237
238
  accepts: CompilerBlock[];
238
239
  frame?: Frame;
239
- limit?: Limit;
240
240
  /**
241
241
  * Whether the slot may be omitted from a slide. Optional (defaults to false):
242
- * a required slot with no content throws with layout + key names.
242
+ * a required slot with no content throws with layout + key names; an optional
243
+ * slot left unfilled has its shape removed from the slide.
243
244
  */
244
245
  required?: boolean;
245
246
  };
246
247
  export type CompilerLayout = {
247
248
  name: string;
248
249
  slideNumber: number;
249
- /** Optional prose — a layout is a shape, not a purpose (agent-guidance descoped). */
250
+ /** Optional prose — a neutral description of the arrangement (a layout is a shape, not a purpose). */
250
251
  description?: string;
251
- whenToUse?: string;
252
- whenNotToUse?: string;
253
252
  /**
254
253
  * The layout's tonal surface. Selects the arm of a `{ light, dark }` `codeTheme`
255
254
  * pair for code fences on this layout. Required when `codeTheme` is a pair.
@@ -257,7 +256,7 @@ export type CompilerLayout = {
257
256
  variant?: Variant;
258
257
  /** Frontmatter inputs (template, image) — one value per `key: value` line. */
259
258
  parameters: CompilerParameter[];
260
- /** Body regions — the default body or `::name::` regions; each `accepts` blocks. */
259
+ /** Body regions — `::name::` regions; each `accepts` blocks. */
261
260
  slots: CompilerSlot[];
262
261
  };
263
262
  /**
@@ -286,7 +285,6 @@ export type CompilerThemeConfig = {
286
285
  layouts: CompilerLayout[];
287
286
  assets: AssetCatalog;
288
287
  template: string;
289
- outputDir?: string;
290
288
  mermaid?: MermaidConfig;
291
289
  /**
292
290
  * Brand fonts injected as `@font-face` when rendering mermaid, so diagram text
@@ -49,14 +49,12 @@ export const Variant = {
49
49
  Dark: "dark",
50
50
  };
51
51
  /**
52
- * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
53
- * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
52
+ * Reserved keys in a deck's frontmatter — global (theme) and per-slide
53
+ * (layout). Exported so callers (e.g. cli.ts) reference the constants
54
54
  * instead of literal strings.
55
55
  */
56
56
  export const RESERVED_KEY = {
57
57
  LAYOUT: "layout",
58
- BODY: "body",
59
- OUTPUT: "output",
60
58
  THEME: "theme",
61
59
  NOTES: "notes",
62
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Create editable, on-brand PowerPoint slides from markdown.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/syntax.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This document covers the detailed syntax for writing slide content in tycoslide deck files. For an overview of how to create slides, see [SKILL.md](SKILL.md).
4
4
 
5
+ > **Every layout, parameter, and slot name in the examples below is a placeholder.** Your theme's real names live in `manifest.json` — read it first, and never assume a name shown here (`Body`, `TwoColumn`, `hero`, `::left::`, etc.) exists in your theme.
6
+
5
7
  ---
6
8
 
7
9
  ## Global frontmatter
@@ -11,25 +13,27 @@ Every deck file starts with a global frontmatter block declaring the theme:
11
13
  ```markdown
12
14
  ---
13
15
  theme: ./theme.json
14
- output: my-deck.pptx
15
16
  ---
16
17
  ```
17
18
 
18
19
  - **`theme`** (required) -- path to the theme config file, relative to the deck file.
19
- - **`output`** -- output filename. Defaults to the deck filename with a `.pptx` extension.
20
+
21
+ The output `.pptx` is written next to the deck file, named after it (`deck.md` → `deck.pptx`).
20
22
 
21
23
  ---
22
24
 
23
- ## Body content (body slots)
25
+ ## Body content
24
26
 
25
- Everything after a slide's closing `---` and before the next slide separator is body content. It maps to the `body` slot as a string array.
27
+ A slide's body is split into named regions with `::name::` markers. Each region fills the slot whose key matches the marker name.
26
28
 
27
29
  ```markdown
28
30
  ---
29
- layout: Body
31
+ layout: Body # ← placeholder; use a real layout from your manifest.json
30
32
  title: Key Achievements
31
33
  ---
32
34
 
35
+ ::body::
36
+
33
37
  We exceeded targets across all metrics.
34
38
 
35
39
  - Revenue up 23% quarter-over-quarter
@@ -37,9 +41,9 @@ We exceeded targets across all metrics.
37
41
  - Three major product launches completed
38
42
  ```
39
43
 
40
- Write body content as paragraphs and bullets:
44
+ Write each region as paragraphs and bullets:
41
45
  - `- ` starts a **bullet**; indent **2 spaces per level** to nest (` - ` = level 1).
42
- - A line with no marker is a **paragraph** (a lead-in / prose line).
46
+ - A line without a `- ` bullet is a **paragraph** (a lead-in / prose line).
43
47
  - Blank lines are ignored (they're visual separators, not content).
44
48
  - Do NOT put headings in body content -- the heading is the slide's `title` slot, and a subheading is the `subtitle` slot. Body is paragraphs + bullets only.
45
49
 
@@ -77,13 +81,13 @@ title: Before & After
77
81
  - Zero-downtime deploys
78
82
  ```
79
83
 
80
- Content before the first `::name::` marker goes to the `body` slot. Content after a marker goes to the slot matching that name. The marker names must match the layout's slot keys.
84
+ Content after a marker goes to the slot matching that name. The marker names must match the layout's slot keys.
81
85
 
82
86
  ---
83
87
 
84
88
  ## Parameters and slots (in `manifest.json`)
85
89
 
86
- A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (the default region, or a `::name::` region). In `manifest.json` each layout carries two lists, `parameters` and `slots`:
90
+ A layout advertises two kinds of author-facing input, split by one rule: **a parameter is one value on a frontmatter line; a slot is a multi-line region in the body** (a `::name::` region). In `manifest.json` each layout carries two lists, `parameters` and `slots`:
87
91
 
88
92
  ```jsonc
89
93
  {
@@ -94,8 +98,8 @@ A layout advertises two kinds of author-facing input, split by one rule: **a par
94
98
  { "key": "logo", "type": "image", "required": true }
95
99
  ],
96
100
  "slots": [
97
- { "key": "body", "type": "text" },
98
- { "key": "code", "type": "code", "codeTheme": "github-dark" }
101
+ { "key": "body", "accepts": ["text"] },
102
+ { "key": "diagram", "accepts": ["image"] }
99
103
  ]
100
104
  }
101
105
  ```
@@ -119,13 +123,11 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
119
123
  hero: assets/diagrams/architecture.png
120
124
  ```
121
125
 
122
- ### Slot types (body regions)
126
+ ### Body content shapes
123
127
 
124
- Fill a slot by writing a region in the body: the default (unmarked) region maps to the `body` slot; a `::name::` marker maps to the slot of that name.
128
+ Fill a slot by writing a `::name::` region in the body; the marker maps to the slot of that name. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
125
129
 
126
- - **`text`** -- a body block, written as markdown paragraphs and bullets. Paragraphs are rebuilt from the template's specimen paragraph styles. Set it as body content after the closing `---`, or as a named slot with `::name::` markers.
127
- - **`table`** -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
128
- - **`code`** -- a syntax-highlighted code block. Write a fenced code block with a language tag in the slot region:
130
+ - **text** (slots that accept `text`) -- markdown paragraphs and bullets, rebuilt from the template's specimen paragraph styles. A fenced code block also routes here:
129
131
  ````markdown
130
132
  ::code::
131
133
 
@@ -135,16 +137,15 @@ Fill a slot by writing a region in the body: the default (unmarked) region maps
135
137
  WHERE created_at > now() - INTERVAL '5 minutes';
136
138
  ```
137
139
  ````
138
- The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting. Colors are applied as native text runs in the output, not images.
139
- - **`mermaid`** -- a mermaid diagram rendered as a themed PNG (see below). Written as a fenced `mermaid` region; the resulting PNG is shown contained (in its entirety).
140
+ The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting, using the theme's `codeTheme` (set once in `theme.json`, not per slot). Colors are applied as native text runs in the output, not images.
141
+ - **table** (slots that accept `table`) -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
142
+ - **image** (slots that accept `image`) -- a picture. A fenced `mermaid` block renders to a themed PNG and fills it (see below).
140
143
 
141
144
  ---
142
145
 
143
146
  ## Mermaid diagrams
144
147
 
145
- Mermaid diagrams are rendered as themed PNGs and delivered to any slot declared with `type: mermaid`. Write a fenced code block with the `mermaid` language tag in a named slot whose layout declares that slot as `type: mermaid` (with a `mermaidVariant` naming the theme's color variant). The resulting PNG behaves like any other image in the slot -- always shown in its entirety.
146
-
147
- To let the same physical slide accept either an image or a diagram, the theme author declares two layouts with the same `slideNumber` -- one exposing the fill as an `image` parameter (frontmatter path), one as a `mermaid` slot (a fenced region). Authors pick between them by naming the layout in frontmatter; the compiler routes content based on the layout's declaration, so there is no ambiguity.
148
+ Write a fenced code block with the `mermaid` language tag in a named slot that accepts `image`. It renders to a themed PNG and behaves like any other image in the slot -- always shown in its entirety. The color variant comes from the theme's `mermaidVariant` (set once in `theme.json`, not per slot).
148
149
 
149
150
  ````markdown
150
151
  ---
@@ -212,13 +213,13 @@ hero: assets/diagrams/architecture.png
212
213
  ```
213
214
 
214
215
  Each parameter or slot in the layout definition may declare:
215
- - **`type`** (required) -- parameters: `template`, `image`; slots: `text`, `table`, `code`, `mermaid`.
216
+ - **`type`** (parameters, required) -- `template` or `image`.
217
+ - **`accepts`** (slots, required) -- an array of `text`, `table`, `image`.
216
218
  - **`required: true`** -- the slide has no usable default and the build fails if the parameter/slot has no value (e.g. team-member photos, icon-grid icons, the quote logo). If you don't have a suitable image, ask the user for one.
219
+ - **optional (the default)** -- a parameter or slot you leave unfilled is dropped from the slide (its shape is removed), so a layout with numbered slots (e.g. up to six sections, up to four stats) renders only the ones you fill.
217
220
  - **image sizing** -- each catalog asset declares a `type` (`icon` | `image` | `background`) that determines how it is scaled and cropped: `icon` never enlarges past native and never crops; `image` fits the whole picture (no crop, may scale); `background` fills and center-crops. Mermaid renders as `image` (contained).
218
- - **`codeTheme`** -- code slots only (required): the Shiki theme id used to syntax-highlight fenced code that lands in this slot (e.g. `"github-dark"`).
219
- - **`mermaidVariant`** -- mermaid slots only (required): names the color variant from `theme.mermaid` (e.g. `"dark"`). See [Mermaid diagrams](#mermaid-diagrams) above.
220
221
 
221
- Each layout also declares a `slideNumber` pointing at the physical slide in the theme's template. **`slideNumber` may repeat across layouts**: two (or more) manifest entries with the same `slideNumber` back a single physical slide, distinguished only by which parameter/slot types they declare -- e.g. an "image" variant and a "mermaid" variant on the same full-bleed slide. The compiler enforces that shared-`slideNumber` layouts agree on keys and shape names; the only allowed cross-type variation is `{image, mermaid}` on the same key.
222
+ Each layout also declares a `slideNumber` pointing at the physical slide in the theme's template -- unique per layout (one layout maps to one physical slide).
222
223
 
223
224
  ---
224
225
 
@@ -274,6 +275,8 @@ layout: Body
274
275
  title: Your First Week
275
276
  ---
276
277
 
278
+ ::body::
279
+
277
280
  Here is what to expect in your first week.
278
281
 
279
282
  - Day 1: Laptop setup and HR orientation