@aivorynet/slaide 1.0.3 → 1.0.4

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.
@@ -161,7 +161,10 @@ body{
161
161
  background:rgba(0,0,0,.3);color:#fff;opacity:.26;cursor:pointer;font:16px/1 var(--font-sans,system-ui);
162
162
  display:grid;place-items:center;backdrop-filter:blur(6px);transition:opacity .2s;}
163
163
  .sl-present-toggle:hover{opacity:.85;}
164
- body.sl-presenting .sl-present-toggle,body.sl-presenting .sl-counter{opacity:0;pointer-events:none;}
164
+ body.sl-presenting .sl-present-toggle,body.slv-presenting .sl-present-toggle,
165
+ body.sl-presenting .sl-counter,body.slv-presenting .sl-counter,
166
+ body.sl-presenting .sl-progress,body.slv-presenting .sl-progress{opacity:0;pointer-events:none;}
167
+ body.sl-presenting .sl-stage,body.slv-presenting .sl-stage{box-shadow:none;}
165
168
  @media print{.sl-present-toggle{display:none;}}
166
169
 
167
170
  /* ---- builds + transitions live in anim.ts (appended to BASE_CSS below) ---- */
@@ -170,7 +173,7 @@ body.sl-presenting .sl-present-toggle,body.sl-presenting .sl-counter{opacity:0;p
170
173
  .sl-progress{position:fixed;left:0;bottom:0;height:3px;background:var(--color-accent,#6cf);width:0;z-index:50;transition:width .3s ease;}
171
174
  .sl-counter{position:fixed;right:14px;bottom:12px;font:600 13px/1 var(--font-sans,system-ui);color:#fff;opacity:.35;z-index:50;background:rgba(0,0,0,.3);padding:5px 9px;border-radius:20px;backdrop-filter:blur(6px);}
172
175
  .sl-counter:hover{opacity:.8;}
173
- .sl-notes{position:fixed;left:0;right:0;bottom:0;max-height:38vh;overflow:auto;background:rgba(10,10,16,.94);color:#ddd;padding:16px 22px;font:15px/1.6 var(--font-sans,system-ui);z-index:60;border-top:2px solid var(--color-accent,#6cf);display:none;}
176
+ .sl-notes{position:fixed;left:var(--sl-dock-left,0px);right:var(--sl-dock-right,0px);bottom:var(--sl-dock-bottom,0px);height:180px;overflow:auto;background:rgba(10,10,16,.94);color:#ddd;padding:16px 22px;font:15px/1.6 var(--font-sans,system-ui);z-index:60;border-top:2px solid var(--color-accent,#6cf);display:none;box-sizing:border-box;}
174
177
  .sl-notes.sl-open{display:block;}
175
178
  .sl-help{position:fixed;inset:0;display:none;place-items:center;background:rgba(5,5,10,.82);z-index:70;}
176
179
  .sl-help.sl-open{display:grid;}
@@ -1 +1 @@
1
- {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/render/css.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,UAAU,QAAQ,CAAC,EAAU;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzE,OAAO,WAAW,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,MAAM,CAAC,KAAK,qBAAqB,EAAE,CAAC,MAAM,CAAC,MAAM,0BAA0B,EAAE,CAAC,WAAW,CAAC,QAAQ,QAAQ,CAAC;AACpK,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6KlB,CAAC;AAEF,qFAAqF;AACrF,gFAAgF;AAChF,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,GAAG,IAAI,GAAG,YAAY,EAAE,GAAG,IAAI,GAAG,WAAW,EAAE,GAAG,IAAI,GAAG,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/render/css.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,UAAU,QAAQ,CAAC,EAAU;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzE,OAAO,WAAW,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,MAAM,CAAC,KAAK,qBAAqB,EAAE,CAAC,MAAM,CAAC,MAAM,0BAA0B,EAAE,CAAC,WAAW,CAAC,QAAQ,QAAQ,CAAC;AACpK,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgLlB,CAAC;AAEF,qFAAqF;AACrF,gFAAgF;AAChF,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,GAAG,IAAI,GAAG,YAAY,EAAE,GAAG,IAAI,GAAG,WAAW,EAAE,GAAG,IAAI,GAAG,kBAAkB,EAAE,CAAC"}
@@ -40,20 +40,20 @@ export const RUNTIME_JS = String.raw `
40
40
  panX = Math.max(-ox, Math.min(ox, panX));
41
41
  panY = Math.max(-oy, Math.min(oy, panY));
42
42
  }
43
- // Docked panels (a left slide navigator, an optional right side panel) reserve gutters
44
- // via the --sl-dock-left / --sl-dock-right CSS vars; the stage centers in the *remaining*
45
- // width and shifts right by the left gutter. 0 (unset) means full-bleed, as before.
46
- function dockVar(name){
47
- var v = getComputedStyle(document.documentElement).getPropertyValue(name);
48
- var n = parseFloat(v); return isFinite(n) ? n : 0;
49
- }
43
+ function cssVar(cs, name){ var n = parseFloat(cs.getPropertyValue(name)); return isFinite(n) ? n : 0; }
50
44
  function scale(){
51
- var dl = dockVar('--sl-dock-left'), dr = dockVar('--sl-dock-right');
52
- var vw = window.innerWidth - dl - dr, vh = window.innerHeight;
45
+ var pres = isPresenting();
46
+ var cs = getComputedStyle(document.documentElement);
47
+ var dl = pres ? 0 : cssVar(cs,'--sl-dock-left'), dr = pres ? 0 : cssVar(cs,'--sl-dock-right');
48
+ var dt = pres ? 0 : cssVar(cs,'--sl-dock-top');
49
+ // Bottom dock = chrome dock (bstrip/filmstrip, absolute) + notes reservation (independent), so
50
+ // the two writers never clobber each other. .sl-notes sits at --sl-dock-bottom (above the chrome).
51
+ var db = pres ? 0 : cssVar(cs,'--sl-dock-bottom') + cssVar(cs,'--sl-dock-notes');
52
+ var vw = window.innerWidth - dl - dr, vh = window.innerHeight - dt - db;
53
53
  var fit = Math.min(vw/CW, vh/CH);
54
54
  var s = fit * zoomFactor;
55
55
  if(zoomFactor<=1){ panX = panY = 0; } else { clampPan(s, vw, vh); }
56
- var tx = dl + (vw - CW*s)/2 + panX, ty = (vh - CH*s)/2 + panY;
56
+ var tx = dl + (vw - CW*s)/2 + panX, ty = dt + (vh - CH*s)/2 + panY;
57
57
  stage.style.transform = 'translate('+tx+'px,'+ty+'px) scale('+s+')';
58
58
  }
59
59
 
@@ -142,6 +142,27 @@ export const RUNTIME_JS = String.raw `
142
142
  }
143
143
 
144
144
  function toggle(sel){ var e=document.querySelector(sel); if(e) e.classList.toggle('sl-open'); updateChrome(); }
145
+ var NOTES_H = 180;
146
+ // Dock the notes panel BENEATH the slide (not floating over it). The panel is a fixed sibling;
147
+ // it reserves its own space via the INDEPENDENT --sl-dock-notes var (summed into the stage fit by
148
+ // scale()), and anchors at --sl-dock-bottom so it sits flush above the chrome dock (the editor
149
+ // filmstrip, or nothing). Keeping notes' reservation separate from --sl-dock-bottom means the
150
+ // bstrip's absolute writes and the notes' toggle never clobber each other (no overlap on toggle).
151
+ function setNotesOpen(open){
152
+ var np = document.querySelector('.sl-notes');
153
+ if(!np) return;
154
+ if(open === np.classList.contains('sl-open')) return; // already in the requested state
155
+ var ds = document.documentElement.style;
156
+ if(open){ np.classList.add('sl-open'); ds.setProperty('--sl-dock-notes', NOTES_H + 'px'); }
157
+ else { np.classList.remove('sl-open'); ds.setProperty('--sl-dock-notes', '0px'); }
158
+ // innerHTML is populated by the updateChrome() call below (it fills .sl-notes when open).
159
+ scale();
160
+ updateChrome();
161
+ }
162
+ function toggleNotes(){
163
+ var np = document.querySelector('.sl-notes');
164
+ if(np) setNotesOpen(!np.classList.contains('sl-open'));
165
+ }
145
166
 
146
167
  // ---- present extras: screen blank (b/w), presenter view, cross-window sync ----------------
147
168
  var blanked = '';
@@ -292,13 +313,18 @@ export const RUNTIME_JS = String.raw `
292
313
  case 'ArrowLeft': case 'ArrowDown': case 'PageUp': backward(); e.preventDefault(); break;
293
314
  case 'Home': goTo(0,1); break;
294
315
  case 'End': goTo(slides.length-1,1); break;
295
- case 'n': case 'N': toggle('.sl-notes'); break;
316
+ case 'n': case 'N': toggleNotes(); break;
296
317
  case 'f': case 'F': setPresenting(!isPresenting(), true); break;
297
318
  case 'p': case 'P': presenter(); break;
298
319
  case 'b': case 'B': blank(blanked==='b'?'':'b'); e.preventDefault(); break;
299
320
  case 'w': case 'W': blank(blanked==='w'?'':'w'); e.preventDefault(); break;
300
321
  case '?': case 'h': toggle('.sl-help'); break;
301
- case 'Escape': document.querySelectorAll('.sl-open').forEach(function(e){e.classList.remove('sl-open');}); break;
322
+ case 'Escape':
323
+ // Close any open overlay (help, etc.) directly; notes go through setNotesOpen so the
324
+ // reserved bottom dock unwinds too. (Loop param is el; the outer e is the KeyboardEvent.)
325
+ document.querySelectorAll('.sl-open').forEach(function(el){ if(!el.classList.contains('sl-notes')) el.classList.remove('sl-open'); });
326
+ setNotesOpen(false);
327
+ break;
302
328
  }
303
329
  });
304
330
  // ---- drag-to-pan (only when zoomed in past fit) -------------------------
@@ -347,6 +373,7 @@ export const RUNTIME_JS = String.raw `
347
373
  }
348
374
  document.addEventListener('fullscreenchange', function(){
349
375
  document.body.classList.toggle('sl-presenting', !!document.fullscreenElement);
376
+ scale();
350
377
  });
351
378
  var presentBtn = document.querySelector('.sl-present-toggle');
352
379
  if(presentBtn) presentBtn.addEventListener('click', function(){ setPresenting(!isPresenting(), true); });
@@ -359,7 +386,7 @@ export const RUNTIME_JS = String.raw `
359
386
  relayout: function(){ scale(); }, // recompute the stage fit (e.g. after the dock opens/closes)
360
387
  show: function(i){ if(i<0||i>=slides.length) return; activate(i, true); cur=i; updateChrome(); },
361
388
  setInteractive: function(on){ navEnabled = on!==false; },
362
- toggleNotes: function(){ toggle('.sl-notes'); },
389
+ toggleNotes: function(){ toggleNotes(); },
363
390
  toggleHelp: function(){ toggle('.sl-help'); },
364
391
  zoom: function(f){
365
392
  zoomFactor = (f==='fit'||!f) ? 1 : Math.max(0.25, Math.min(8, f));
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/render/runtime.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,sCAAsC;AACtC,8EAA8E;AAC9E,uFAAuF;AACvF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmYnC,CAAC"}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/render/runtime.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,sCAAsC;AACtC,8EAA8E;AAC9E,uFAAuF;AACvF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8ZnC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { DEFAULT_ENTRANCE } from './render/anim.js';
2
+ /** Named slide transitions (frontmatter `transition:`). Canonical: render/anim.ts. */
3
+ export declare const TRANSITIONS: readonly string[];
4
+ /** Element/build entrances (`>>> <entrance>`), default `fade-up`. Canonical: render/anim.ts. */
5
+ export declare const ENTRANCES: readonly string[];
6
+ export { DEFAULT_ENTRANCE };
7
+ /** Per-slide frontmatter keys the parser recognizes (metadata keys included — they also
8
+ * become `{{placeholders}}`). Canonical: parser/parse.ts `KNOWN_SLIDE_KEYS`. */
9
+ export declare const FRONTMATTER_KEYS: readonly string[];
10
+ /** The config (non-metadata) subset — the keys that change how a slide renders. Used by
11
+ * the docs lint to check the frontmatter-key table; metadata keys (title/author/…) are
12
+ * documented as placeholders/headmatter instead. */
13
+ export declare const FRONTMATTER_CONFIG_KEYS: readonly string[];
14
+ /** Deck headmatter keys with defined meaning (plus any custom scalar → placeholder). */
15
+ export declare const HEADMATTER_KEYS: readonly string[];
16
+ /** Inline span size classes `[x]{.xs}`. Canonical: compiler/markdown.ts `SIZE_CLASS`. */
17
+ export declare const SPAN_SIZE_CLASSES: readonly string[];
18
+ /** Inline span utility classes (besides `.grad`/`.grad-<name>` and colour names). */
19
+ export declare const SPAN_UTIL_CLASSES: readonly string[];
20
+ /** Image `{...}` utility classes `![x](y){.round}`. Canonical: render/css.ts `.sl-img.*`. */
21
+ export declare const IMAGE_UTIL_CLASSES: readonly string[];
22
+ /** Renderable code-fence info-strings (rendered, not shown as code). Canonical: compiler/markdown.ts. */
23
+ export declare const RENDERABLE_FENCES: readonly string[];
24
+ /** Placeholder built-ins `{{page}}` (plus any scalar headmatter/frontmatter key). Canonical: compiler/chrome.ts `placeholderCtx`. */
25
+ export declare const PLACEHOLDER_BUILTINS: readonly string[];
26
+ /** Master layout slot `style:` keys. Canonical: compiler/compile.ts `STYLE_MAP` + the two
27
+ * keys handled before the map (`anchor`, `box`). */
28
+ export declare const SLOT_STYLE_KEYS: readonly string[];
29
+ /** Master layout slot `type:`s that get first-class styling. Canonical: render/css.ts `.sl-slot-*`. */
30
+ export declare const SLOT_TYPES: readonly string[];
31
+ /** Every diagnostic (`validate`) code the engine can emit. Canonical: emitted as `code:`
32
+ * literals across the parser/compiler; the lint scrapes src to keep this exhaustive. */
33
+ export declare const DIAGNOSTIC_CODES: readonly string[];
34
+ /** Diagnostics that are hard errors (`ok:false`); every other code is a warning.
35
+ * Canonical here so `index.ts` and the docs share one definition. */
36
+ export declare const ERROR_SEVERITY_CODES: ReadonlySet<string>;
package/dist/vocab.js ADDED
@@ -0,0 +1,77 @@
1
+ // Copyright 2026 AIVory, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // THE canonical registry of the slaide language's author-facing vocabulary.
5
+ //
6
+ // Why this file exists: the token sets (transitions, frontmatter keys, span/slot
7
+ // classes, placeholders, fence types, diagnostic codes) used to live scattered and
8
+ // mostly un-exported across parser/compiler/render modules, and the human docs
9
+ // (docs/spec.md, docs/grammar.md, docs/themes.md) were hand-kept in parallel — so they
10
+ // drifted (e.g. docs listed ~5 transitions while the engine shipped 13). Everything the
11
+ // docs must stay complete about is now imported/derived here from the real code, and
12
+ // `test/docs-sync.test.ts` fails CI if a token is missing from the docs or if the code
13
+ // grows a token this registry doesn't know about. Add a token to the engine → add it
14
+ // here → the lint tells you which doc to update. One source, no drift.
15
+ //
16
+ // This module is a LEAF: it only pulls the already-exported catalogs it can reuse
17
+ // verbatim; anything without a single code-level const is listed here as the canonical
18
+ // definition and cross-checked against source text by the lint.
19
+ import { SLIDE_TRANSITION_NAMES, ENTRANCE_NAMES, DEFAULT_ENTRANCE } from './render/anim.js';
20
+ import { KNOWN_SLIDE_KEYS } from './parser/parse.js';
21
+ import { SIZE_CLASS } from './compiler/markdown.js';
22
+ import { STYLE_MAP } from './compiler/compile.js';
23
+ /** Named slide transitions (frontmatter `transition:`). Canonical: render/anim.ts. */
24
+ export const TRANSITIONS = SLIDE_TRANSITION_NAMES;
25
+ /** Element/build entrances (`>>> <entrance>`), default `fade-up`. Canonical: render/anim.ts. */
26
+ export const ENTRANCES = ENTRANCE_NAMES;
27
+ export { DEFAULT_ENTRANCE };
28
+ /** Per-slide frontmatter keys the parser recognizes (metadata keys included — they also
29
+ * become `{{placeholders}}`). Canonical: parser/parse.ts `KNOWN_SLIDE_KEYS`. */
30
+ export const FRONTMATTER_KEYS = [...KNOWN_SLIDE_KEYS];
31
+ /** The config (non-metadata) subset — the keys that change how a slide renders. Used by
32
+ * the docs lint to check the frontmatter-key table; metadata keys (title/author/…) are
33
+ * documented as placeholders/headmatter instead. */
34
+ export const FRONTMATTER_CONFIG_KEYS = [
35
+ 'layout', 'transition', 'transition-ms', 'transition-ease',
36
+ 'background', 'variant', 'morph', 'chrome', 'logo', 'footer', 'notes',
37
+ ];
38
+ /** Deck headmatter keys with defined meaning (plus any custom scalar → placeholder). */
39
+ export const HEADMATTER_KEYS = ['master', 'title', 'author', 'date', 'company', 'subtitle', 'progress'];
40
+ /** Inline span size classes `[x]{.xs}`. Canonical: compiler/markdown.ts `SIZE_CLASS`. */
41
+ export const SPAN_SIZE_CLASSES = Object.keys(SIZE_CLASS);
42
+ /** Inline span utility classes (besides `.grad`/`.grad-<name>` and colour names). */
43
+ export const SPAN_UTIL_CLASSES = ['grad', 'bold', 'muted'];
44
+ /** Image `{...}` utility classes `![x](y){.round}`. Canonical: render/css.ts `.sl-img.*`. */
45
+ export const IMAGE_UTIL_CLASSES = ['round', 'cover', 'shadow'];
46
+ /** Renderable code-fence info-strings (rendered, not shown as code). Canonical: compiler/markdown.ts. */
47
+ export const RENDERABLE_FENCES = ['svg', 'embed', 'widget', 'mermaid', 'echart'];
48
+ /** Placeholder built-ins `{{page}}` (plus any scalar headmatter/frontmatter key). Canonical: compiler/chrome.ts `placeholderCtx`. */
49
+ export const PLACEHOLDER_BUILTINS = [
50
+ 'page', 'total', 'pagePadded', 'totalPadded', 'date', 'title', 'author', 'slideTitle', 'footer',
51
+ ];
52
+ /** Master layout slot `style:` keys. Canonical: compiler/compile.ts `STYLE_MAP` + the two
53
+ * keys handled before the map (`anchor`, `box`). */
54
+ export const SLOT_STYLE_KEYS = [...Object.keys(STYLE_MAP), 'anchor', 'box'];
55
+ /** Master layout slot `type:`s that get first-class styling. Canonical: render/css.ts `.sl-slot-*`. */
56
+ export const SLOT_TYPES = ['title', 'subtitle', 'body', 'image', 'media', 'quote', 'caption'];
57
+ /** Every diagnostic (`validate`) code the engine can emit. Canonical: emitted as `code:`
58
+ * literals across the parser/compiler; the lint scrapes src to keep this exhaustive. */
59
+ export const DIAGNOSTIC_CODES = [
60
+ // parser/parse.ts
61
+ 'bad-config', 'no-headmatter', 'ambiguous-frontmatter', 'empty-deck',
62
+ // compiler/compile.ts
63
+ 'unknown-color', 'unknown-gradient', 'bad-animation', 'unknown-layout',
64
+ 'unknown-transition', 'unknown-background', 'unknown-slot', 'low-contrast',
65
+ // compiler/markdown.ts
66
+ 'unknown-class', 'unknown-entrance', 'stray-build', 'bad-chart',
67
+ // compiler/tokens.ts
68
+ 'unknown-token', 'token-cycle', 'non-embeddable-font', 'unknown-variant',
69
+ // compiler/chrome.ts
70
+ 'unknown-placeholder',
71
+ // index.ts (validation entry)
72
+ 'no-master', 'parse-error',
73
+ ];
74
+ /** Diagnostics that are hard errors (`ok:false`); every other code is a warning.
75
+ * Canonical here so `index.ts` and the docs share one definition. */
76
+ export const ERROR_SEVERITY_CODES = new Set(['empty-deck', 'no-master', 'unknown-layout']);
77
+ //# sourceMappingURL=vocab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vocab.js","sourceRoot":"","sources":["../src/vocab.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aivorynet/slaide",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "An AI-authorable slide language with beautiful web + PDF output",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -16,7 +16,7 @@ Run as `npx @aivorynet/slaide <cmd>` (or `npm run slaide -- <cmd>` where a scrip
16
16
 
17
17
  ## Workflow
18
18
  1. **Brand first — check, then ask.** Scan the project/context for brand assets: colours, fonts, a logo, an existing `*.slaide.yaml` master, a brand doc. Found a master → use it. Found brand → build the master around it. Found nothing → **ask** the user: brand colours? logo? an existing theme, or author one from scratch? Prefer authoring a **new master** over copying an example.
19
- 2. **Read** `reference.md` (language) and `themes.md` (the master format + footguns) before authoring a master. Worked example decks: https://github.com/aivorynet/slaide/tree/main/examples
19
+ 2. **Read** `reference.md` (language) and `themes.md` (the master format + footguns) before authoring a master; `grammar.md` is the precise formal reference (frontmatter detection, attribute braces, transitions/entrances, master value forms) when you need it. Worked example decks: https://github.com/aivorynet/slaide/tree/main/examples
20
20
  3. **`slaide slots <deck>`** — prints the master's real layouts, slots, colours, gradients, sizes, transitions. Reference these **by name**. (Run on the deck, not the master.)
21
21
  4. **Write** the `.slaide` — and the master, if authoring one.
22
22
  5. **`slaide validate <deck> --strict`** — a **gate**: must be clean. Every `unknown-*` / `low-contrast` / `non-embeddable-font` means something renders wrong even when it "looks valid".
@@ -0,0 +1,183 @@
1
+ # slaide grammar (v1)
2
+
3
+ A formal grammar for the `.slaide` document layer. Notation: EBNF — `=` defines, `|` alternation, `{ }` zero-or-more, `[ ]` optional, `( )` grouping, `"x"` literal, `…` charset prose. The Markdown *inside* slot content is CommonMark and is not re-specified here; this grammar defines slaide' structural envelope, sigils, and the inline/attribute extensions, plus the master value forms.
4
+
5
+ This file is the **precise structural reference**. For what each construct *does* — usage, examples, the full transition/entrance catalogs, chart/image/media options, and the `validate` diagnostic codes — see [spec.md](spec.md); for master semantics (colours, type scale, layouts, slot styles) see [themes.md](themes.md). The token sets are generated from the engine (`src/vocab.ts`) and kept in sync by `test/docs-sync.test.ts`.
6
+
7
+ ## 1. Document
8
+
9
+ ```ebnf
10
+ deck = [ headmatter ] , slide , { separator , slide } , [ NL ] ;
11
+ headmatter = fence , config-block , fence ;
12
+ slide = [ frontmatter ] , body ;
13
+ frontmatter = config-block , fence ; (* see §2 detection rule *)
14
+ separator = NL , fence ; (* a line that is exactly "---" *)
15
+ fence = "---" , EOL ;
16
+ NL = newline ;
17
+ ```
18
+
19
+ A deck is an optional headmatter block, then one or more slides separated by a bare `---` line. `headmatter` is the first fenced config block. Each slide is an optional fenced frontmatter block followed by a body.
20
+
21
+ ## 2. Frontmatter-vs-body detection (the load-bearing rule)
22
+
23
+ A block immediately after a separator (or the leading fence) is **frontmatter** iff **both**:
24
+ 1. every non-blank, non-`#comment` line matches `config-line` (a `key:`/`~key:`/list-continuation), **and**
25
+ 2. it is terminated by a `fence` before the body begins.
26
+
27
+ Otherwise the block is `body`. Consequences the parser guarantees:
28
+ - The **first** config-like fenced block in the file is `headmatter`.
29
+ - A body whose first line merely *contains* a colon (e.g. `We offer: Tooling`) is **not** frontmatter (it is not `key:`-only and has no trailing fence).
30
+ - A config-shaped block whose keys are **none of** the known slide keys (`layout`, `background`, `transition`, `variant`, `chrome`, `footer`, `logo`, `morph`, …) is still read as frontmatter, **but** the compiler emits an `ambiguous-frontmatter` warning — so a spec-sheet body (`Name: …` / `Founded: …`) eaten as config is no longer silent.
31
+ - To force a body that would otherwise look config-like, escape its first line with a leading backslash (`\Name: Acme`) or precede the slide with an empty frontmatter (`---` `---`).
32
+ - Malformed YAML in a config block emits a `bad-config` warning (with the parser message) instead of silently rendering with defaults.
33
+
34
+ ```ebnf
35
+ config-block = { config-line | comment | blank } ;
36
+ config-line = [ "~" ] , key , ":" , [ value ] , EOL ;
37
+ key = ident ;
38
+ comment = "#" , … , EOL ;
39
+ ident = letter , { letter | digit | "-" | "_" } ;
40
+ ```
41
+
42
+ `value` and nested structures follow YAML. A leading `~` on a key marks a **cascading** default (applies to this slide and all following until overridden); a bare key is **scoped** to its slide.
43
+
44
+ ## 3. Fences vs code
45
+
46
+ `---` is a separator **only outside** a fenced code region. Inside ```` ``` ```` / `~~~` fences it is literal text.
47
+
48
+ ```ebnf
49
+ code-fence = ("```" | "~~~") , [ info-string ] , EOL , { any-line } , ("```" | "~~~") , EOL ;
50
+ ```
51
+
52
+ A code fence whose `info-string` is a renderable type is rendered, not listed as code: `svg` (inline vector), `embed` (its body is a URL → sandboxed `<iframe>`), `widget` (its body is HTML/JS → `sandbox="allow-scripts"` srcdoc iframe, theme tokens injected), `mermaid` (diagram DSL → inline SVG), `echart` (ECharts `option` as JSON/YAML → inline SVG). Other info-strings render as code. CommonMark GFM pipe tables are supported as ordinary body content.
53
+
54
+ Media uses the image syntax with a media extension: `![alt](clip.mp4)` / `![alt](track.mp3)` → `<video>`/`<audio>` (see §6).
55
+
56
+ ## 4. Body
57
+
58
+ ```ebnf
59
+ body = { region-marker | note | content-line } ;
60
+ region-marker = "::" , WS , slot-name , WS , "::" , EOL ;
61
+ slot-name = ident ; (* valid names are per-layout *)
62
+ note = "???" , [ WS ] , inline-text , EOL , { non-blank-line } ;
63
+ content-line = markdown-line ; (* CommonMark, with §5–§7 extensions *)
64
+ ```
65
+
66
+ - `region-marker` routes following content into the named slot until the next marker or separator. Content before any marker → the layout's main slot.
67
+ - `note` (a `???` line and its continuation until a blank line) is a speaker note: shown in the presenter overlay, omitted from audience view and PDF.
68
+
69
+ ### 4.1 Build sigil
70
+
71
+ ```ebnf
72
+ build = content , WS , ">>>" , EOL ;
73
+ ```
74
+
75
+ A list item or block ending with `>>>` becomes an incremental build step. Steps auto-number in document order (a shared per-slide counter); identical effective step → simultaneous. In PDF all builds are settled (shown).
76
+
77
+ ### 4.2 Escaping sigils
78
+
79
+ A leading backslash turns a slaide sigil into literal content (the backslash is removed):
80
+
81
+ | Write | Renders as | Instead of |
82
+ |---|---|---|
83
+ | `\:: word ::` | literal `:: word ::` | a region marker |
84
+ | `\??? text` | literal `??? text` body | a speaker note |
85
+ | `… \>>>` | literal trailing `>>>` | a build step |
86
+ | `\[text]{.cls}` | literal `[text]{.cls}` | a styled span (and skips class validation) |
87
+ | `\Name: Acme` (first body line) | body line | frontmatter (see §2) |
88
+
89
+ ## 5. Inline styled spans
90
+
91
+ ```ebnf
92
+ span = "[" , inline-text , "]" , "{" , class , { class } , "}" ;
93
+ class = "." , class-name ;
94
+ class-name = ident ; (* resolved against the master *)
95
+ ```
96
+
97
+ Classes chain (`[40-80%]{.grad-purple .huge}`). Resolution (see themes.md → *Marks / utility classes*):
98
+ - `.grad` → brand gradient text; `.grad-<name>` → named gradient text.
99
+ - `.<color>` → text color, where `<color>` is a `palette` key, `roles` name, or a literal CSS colour.
100
+ - `.xs .sm .md .lg .xl .xxl .huge` → font-size (type-scale steps `small`…`stat`).
101
+ - `.bold`, `.muted`, and image utilities `.round`, `.cover`, `.shadow`.
102
+
103
+ A class that is **none** of the above (e.g. a typo like `.xxlarge` or `.grad-teel`) emits an `unknown-class` / `unknown-gradient` warning rather than silently degrading to inert/invisible CSS. Run `slaide slots <deck>` to print the legal slot, colour, gradient and size names for a deck's master; `slaide validate <deck> [--strict]` surfaces all diagnostics (`--strict` makes warnings fail).
104
+
105
+ ## 6. Image & attribute brace
106
+
107
+ ```ebnf
108
+ image = "![" , alt , "](" , src , [ WS , quoted ] , ")" , [ attr-brace ] ;
109
+ attr-brace = "{" , WS , { attr , WS } , "}" ;
110
+ attr = id-attr | class | kv-attr | anchor-attr ;
111
+ id-attr = "#" , ident ;
112
+ kv-attr = key , "=" , value-token ; (* e.g. width=170px — unquoted, units ok *)
113
+ anchor-attr = "anchor" , ":" , WS , '"' , pct , WS , pct , WS , pct , WS , pct , '"' ;
114
+ value-token = { non-space-non-brace } ;
115
+ pct = number , "%" ;
116
+ ```
117
+
118
+ One brace may mix `#id`, `.class`, `key=value`, and `anchor:"…"` in any order. `#id` enables shared-element **morph** to a same-id element on the next slide (`transition: morph`).
119
+
120
+ ## 7. Placeholders
121
+
122
+ ```ebnf
123
+ placeholder = "{{" , WS , ph-name , WS , "}}" ;
124
+ ph-name = ident ;
125
+ ```
126
+
127
+ Substituted at compile time inside chrome bands and master text. Names: `page`, `total`, `pagePadded`, `totalPadded`, `date`, `title`, `author`, `slideTitle`, `footer`, plus any scalar headmatter/frontmatter key. Unknown names resolve to empty (with a warning).
128
+
129
+ ## 8. Master (`*.slaide.yaml`) value forms
130
+
131
+ The master is YAML; this section pins the slaide-specific value vocabulary (see themes.md for full semantics).
132
+
133
+ ```ebnf
134
+ master = mapping of:
135
+ "schema" , "name" , [ "description" ] ,
136
+ [ "canvas" ] , [ "fonts" ] , [ "typeScale" ] , [ "colors" ] ,
137
+ [ "gradients" ] , [ "tokens" ] , [ "backgrounds" ] ,
138
+ [ "variants" ] , [ "transitions" ] , [ "chrome" ] , [ "ui" ] , [ "layouts" ] ;
139
+ ui = { [ "progress": boolean ] } ; (* web position indicator; default true *)
140
+
141
+ typeScale = { "base": dim , "ratio": number , "steps": { step-name : (integer | dim) } } ;
142
+ dim = number , ("px"|"em"|"rem"|"%"|"vw"|"vh") ;
143
+ color-ref = "{palette." , ident , "}" | "{" , ident , "}" | css-color ;
144
+ gradient-ref = "{gradients." , ident , "}" | gradient-name ;
145
+
146
+ layout = { "areas": [ area-row , { area-row } ] ,
147
+ [ "rows" ] , [ "cols" ] , [ "gap" ] , [ "padding" ] ,
148
+ [ "align": ("start"|"center"|"end") ] ,
149
+ [ "background": bg-name ] ,
150
+ [ "chrome": ("both"|"header"|"footer"|"false") ] ,
151
+ [ "logo": false ] ,
152
+ "slots": { slot-name : slot } } ;
153
+ area-row = string ; (* space-separated slot names; rectangular *)
154
+ slot = { "type": slot-type , [ "style": style-map ] } ;
155
+ slot-type = "title"|"subtitle"|"body"|"image"|"media"|"quote"|"caption"| ident ;
156
+ style-map = { style-key : style-val } ;
157
+ style-key = "font"|"size"|"color"|"fill"|"align"|"valign"|"justify"
158
+ | "weight"|"leading"|"transform"|"italic"|"maxw"|"box" ;
159
+
160
+ chrome = { [ "header": band ] , [ "footer": band ] ,
161
+ [ "logo": svg-string ] , [ "logoPos": corner ] } ;
162
+ band = { [ "left": tmpl ] , [ "center": tmpl ] , [ "right": tmpl ] } ;
163
+ tmpl = string ; (* Markdown-inline + placeholders *)
164
+ corner = "top-left"|"top-right"|"bottom-left"|"bottom-right" ;
165
+ ```
166
+
167
+ ## 9. Reserved tokens (summary)
168
+
169
+ | Token | Context | Meaning |
170
+ |---|---|---|
171
+ | `---` | line | slide separator / config fence (outside code fences) |
172
+ | `:: name ::` | line | region/slot marker |
173
+ | `>>>` | end of line/item | build step |
174
+ | `???` | line start | speaker note |
175
+ | `~key:` | frontmatter | cascading default |
176
+ | `[t]{.c}` | inline | styled span |
177
+ | `{#id .c k=v anchor:"…"}` | after image | attributes |
178
+ | `{{name}}` | chrome/master text | placeholder |
179
+ | `![alt](x.mp4\|.mp3)` | inline | video / audio |
180
+ | ```` ```svg ```` | fence | inline vector embed |
181
+ | ```` ```embed ```` / ```` ```widget ```` | fence | sandboxed iframe (URL / inline JS) |
182
+ | ```` ```mermaid ```` / ```` ```echart ```` | fence | chart → inline SVG (diagram / data viz) |
183
+ | `\` (leading) | before a sigil | escape to literal (`\::`, `\???`, `\>>>`, `\[t]{.c}`) |
@@ -40,22 +40,37 @@ My Talk
40
40
  | Key | Values | Meaning |
41
41
  |---|---|---|
42
42
  | `layout` | a master layout name | Which grid to use. |
43
- | `transition` | `none`,`fade`,`slide-left/right/up/down`,`zoom`,`morph` | Transition **into** this slide. |
43
+ | `transition` | a transition name (§3.1) | Transition **into** this slide. |
44
+ | `transition-ms` | milliseconds | Duration override for this slide's transition. |
45
+ | `transition-ease` | a CSS easing | Easing override (`ease`, `cubic-bezier(…)`). |
44
46
  | `background` | a master background name | Override the layout's background. |
45
47
  | `variant` | a master variant name | Scoped token overrides (e.g. a light section). |
46
48
  | `morph` | an id | Participate in a shared-element morph. |
47
49
  | `footer` | inline Markdown | Per-slide footer; also `{{footer}}`. |
48
50
  | `chrome` | `both`,`header`,`footer`,`none`,`false` | Header/footer visibility this slide. |
49
51
  | `logo` | `false` | Hide the corner logo this slide. |
52
+ | `notes` | inline Markdown | Speaker note for this slide (frontmatter form of a `???` line). |
50
53
  | *any other scalar* | — | Available as a `{{placeholder}}`. |
51
54
 
52
55
  **Cascade vs scope:** a bare key (`transition: zoom`) is this-slide-only; a `~`-prefixed key cascades to this slide and every later one until overridden (also valid in headmatter).
53
56
 
57
+ ### 3.1 Transition names
58
+
59
+ `transition:` takes one of these built-ins; per-slide `transition-ms` / `transition-ease` override timing, and the master's `transitions: { default, duration }` sets the deck-wide default.
60
+
61
+ | Group | Names |
62
+ |---|---|
63
+ | Fades | `none`, `fade`, `dissolve`, `fade-through-black` (alias `fade-black`) |
64
+ | Slides | `slide-left` (alias `slide`), `slide-right`, `slide-up`, `slide-down` |
65
+ | Push/cover | `push`, `cover`, `reveal` |
66
+ | Scale/3-D | `zoom`, `zoom-out`, `flip` |
67
+ | Shared-element | `morph` — pairs a `{#id}` image (or a `morph:` id) with the same id on the next slide |
68
+
54
69
  ## 4. Slide body
55
70
 
56
71
  - **Regions:** `:: name ::` on its own line routes following Markdown into slot `name`. Text before any marker → the main slot (`body`, else the first slot).
57
72
  - **Markdown:** standard CommonMark — headings, lists, **bold**, *italic*, `code`, fences, > quotes, links, tables, images. (A single newline is a space; blank line = new paragraph.)
58
- - **Builds `>>>`:** end an item/block with `>>>` to reveal it; steps auto-number in order, same step = simultaneous. PDF shows all.
73
+ - **Builds `>>>`:** end an item/block with `>>>` to reveal it; steps auto-number in order, same step = simultaneous. PDF shows all. Add an entrance and timing after the sigil (§4.7).
59
74
  - **Notes `??? …`:** a `???` line (until a blank line) is a speaker note — presenter overlay only, hidden from audience and PDF.
60
75
 
61
76
  ### 4.1 Inline styled spans — `[text]{.class …}`
@@ -114,6 +129,17 @@ Both render to **theme-coloured inline SVG** (web/PDF/PNG/PPTX); `slaide build`
114
129
 
115
130
  GFM pipe tables, styled by the master; use `[cell]{.class}` spans for emphasis.
116
131
 
132
+ ### 4.7 Build entrances — `>>> <entrance> [opts]`
133
+
134
+ A bare `>>>` reveals with the default entrance (`fade-up`). Name an entrance right after the sigil, optionally with `delay=`, `dur=` and `ease=` (bare numbers are milliseconds): `- Big reveal >>> zoom-in delay=200 dur=600`. An unknown name warns (`unknown-entrance`) and falls back to the default. A master `animations:` block can define custom entrances too.
135
+
136
+ | Group | Names |
137
+ |---|---|
138
+ | Fades | `fade`, `fade-up`, `fade-down`, `fade-left`, `fade-right`, `blur-in` |
139
+ | Slides | `slide-in-left`, `slide-in-right`, `slide-in-up`, `slide-in-down`, `rise` |
140
+ | Scale | `zoom-in`, `zoom-out`, `pop` |
141
+ | Instant | `none` |
142
+
117
143
  ## 5. Layers
118
144
 
119
145
  Each slide composites **background → content → chrome** by paint order. Backgrounds come from the master (per layout or per slide); content flows through the layout's named slots; chrome sits on top.
@@ -175,3 +201,33 @@ footer: A first deck
175
201
  ```
176
202
 
177
203
  See [themes.md](themes.md) to author a master.
204
+
205
+ ## 10. Diagnostics — what `validate` reports
206
+
207
+ `slaide validate <deck>` prints line-numbered diagnostics; `--strict` makes every warning fail. **Errors** fail regardless of `--strict`; everything else is a **warning** (a real render defect even when the deck "looks valid"). Every code:
208
+
209
+ | Code | Severity | Meaning |
210
+ |---|---|---|
211
+ | `parse-error` | error | The source could not be parsed at all. |
212
+ | `empty-deck` | error | No slides were found. |
213
+ | `no-master` | error | The deck's `master:` could not be resolved. |
214
+ | `unknown-layout` | error | A slide's `layout:` names no layout in the master. |
215
+ | `no-headmatter` | warning | No leading `---` deck headmatter block (e.g. `master:`). |
216
+ | `bad-config` | warning | A headmatter/frontmatter block is not valid YAML (rendered with defaults). |
217
+ | `ambiguous-frontmatter` | warning | A config-shaped **body** was eaten as frontmatter — escape the first line with `\` or add an explicit `---`. |
218
+ | `unknown-transition` | warning | A `transition:` names no built-in transition (§3.1). |
219
+ | `unknown-background` | warning | A `background:` names no master background. |
220
+ | `unknown-variant` | warning | A `variant:` names no master variant. |
221
+ | `unknown-slot` | warning | Content routed to a slot the chosen layout doesn't define (dropped silently). |
222
+ | `unknown-class` | warning | An inline `[x]{.cls}` is not a size / `.bold` / `.muted` / `.grad` / master colour / CSS colour. |
223
+ | `unknown-gradient` | warning | A `.grad-<name>` or slot `fill:` names no master gradient (text gets no fill — often invisible). |
224
+ | `unknown-color` | warning | A slot `color:`/`box:` names no master role/palette or CSS colour (falls back to a literal, often invisible). |
225
+ | `unknown-entrance` | warning | A `>>> <name>` build entrance isn't a known effect (§4.7). |
226
+ | `stray-build` | warning | A `>>>` on a non-list line (headings, bold labels, paragraphs). |
227
+ | `low-contrast` | warning | Resolved text ≈ its background (dark-on-dark / light-on-light) — bind a `variant:` or set an explicit `color:`. |
228
+ | `bad-chart` | warning | An ````echart` option didn't parse — rendered as a plain code block instead. |
229
+ | `bad-animation` | warning | A master `animations:` entry is missing its required keyframes/`hidden` state. |
230
+ | `unknown-token` | warning | A master token reference names nothing. |
231
+ | `token-cycle` | warning | A master token references itself in a cycle. |
232
+ | `non-embeddable-font` | warning | A font won't embed in `.pptx` — PowerPoint will substitute it. |
233
+ | `unknown-placeholder` | warning | A `{{name}}` resolves to nothing (renders empty). |
@@ -47,12 +47,14 @@ colors:
47
47
  accent: "{palette.brand}"
48
48
  layouts:
49
49
  cover:
50
- align: center
51
- areas: ["title", "subtitle"]
50
+ areas: ["title visual", "subtitle visual"]
52
51
  rows: "auto auto"
52
+ cols: "1.1fr 0.9fr"
53
+ gap: "0.5em 3em"
53
54
  slots:
54
55
  title: { type: title, style: { font: display, size: h1, weight: "900" } }
55
56
  subtitle: { type: subtitle, style: { size: h3, color: accent } }
57
+ visual: { type: image, style: { valign: center } }
56
58
  title-content:
57
59
  areas: ["title", "body"]
58
60
  rows: "auto 1fr"
@@ -69,7 +71,7 @@ layouts:
69
71
 
70
72
  ## fonts / typeScale
71
73
 
72
- **Use real Google Fonts only** (`provider: google`) — pick families that actually exist on Google Fonts and embed (avoid **JetBrains Mono**, which doesn't). A `display` (headings) + a `sans` (body) is plenty; add a `mono` role only if the deck shows code. A `provider: system`/`local` font that isn't a common system font (Arial, Calibri, Georgia…) **warns** (`non-embeddable-font`): it won't embed in the `.pptx`, so PowerPoint substitutes it off your machine.
74
+ **Use real Google Fonts only** (`provider: google`) — pick families that actually exist on Google Fonts. A `display` + `sans` is plenty; add `mono` only if the deck shows code. A `provider: system`/`local` font that isn't a common system font (Arial, Calibri, Georgia…) **warns** (`non-embeddable-font`): it won't embed in `.pptx`.
73
75
 
74
76
  ```yaml
75
77
  fonts:
@@ -195,6 +197,7 @@ layouts:
195
197
  | `box` | surface panel (bg + padding + radius) | `true`, a colour role/palette name (preferred), a **named master gradient** (`box: brand` → padded, rounded, gradient hero/closing panel), or a raw hex / CSS colour / gradient |
196
198
  | `bg` | background of the slot region | any CSS colour / gradient (literal) |
197
199
  | `anchor` | absolutely position the slot | `"x% y% w% h%"` of the canvas |
200
+ | `pad` / `opacity` / `radius` / `border` / `rotate` | fine control — padding / opacity / border-radius / border / rotation (also emitted by the importer) | CSS values (`rotate` accepts a bare deg or a full `transform`) |
198
201
 
199
202
  `color:`/`box:`/`fill:` are validated against the master — an unresolved name warns, so `validate` can't call an invisible-text slide valid. Run `slaide slots` for legal names.
200
203
 
@@ -208,8 +211,19 @@ Inline `[text]{.class}` resolves against this master: **colour** = any `palette`
208
211
 
209
212
  ## Tips for AI authors
210
213
 
211
- - Reference **roles** and **scale steps**, not raw hex/px, so a reskin is a palette swap. Add a layout by adding an `areas` map + `slots` — no code.
212
- - **Cards (`box:`) — kill hollow tops.** A `box:` is bg + padding + radius only (solid colour — **no gradient/shadow/border**). With no `valign` a card **stretches to its grid row** and pins text to the top hollow in a tall `1fr` row. Two fixes: **`valign: center`** shrinks the card to its content and centres it; or, to keep a row of cards equal height, put them in an **`auto`** row and centre that block with spacer rows (`rows: "auto 1fr auto 1fr"`). On a light ground, tint the card surface off-white vs the page so it reads.
213
- - **Signature, not just tidy:** give the deck one repeated motif (a faint logo-derived shape or a recurring accent panel) and vary composition slide to slide. Make the **cover and closing command** don't leave half the canvas empty: pair a strong text column with a full-height brand/gradient panel or full-bleed motif.
214
- - **Inline `svg` needs a size:** give the `<svg>` explicit `width`/`height`, or a centered zero-size SVG collapses and vanishes.
215
- - **A deck about slaide itself?** Use the shipped brand in the sibling `slaide-hugo` repo (`themes/slaide/assets/css/slaide.css` `:root`; `static/images/slaide-*.svg`): slate-azure gradient `#243B6B→#3E6FB0→#6FA8DC` on cream `#FBF7F0` / ink `#15120E`; IBM Plex Sans + JetBrains Mono + Fraunces.
214
+ - Use **roles** and **scale steps**, not raw hex/px reskin = palette swap.
215
+ - **Cards (`box:`):** bg + padding + radius only (no gradient/shadow/border). Without `valign` a card stretches full-height and pins text top (hollow). Fix: `valign: center` to shrink-wrap, or put cards in an `auto` row with `1fr` spacers. Tint card surface off-white vs the page.
216
+ - **Cover = dramatic hero.** Visual slot FILLS bold brand SVG, product mock, or illustrated motif at half the slide, not a small icon. Think Apple keynote: few bold shapes at large scale. Centred text on a gradient is a section divider, not a cover. Never `.grad` text on a gradient bg (vanishes). **Closing** = bold CTA or contact slide (name/role/email/URL for pitch/agency decks). Both must feel intentional.
217
+ - **Brand ground dominates.** Most slides use the brand's background role (warm cream, tinted neutral not generic white). Dark/gradient = accent moments only (cover, one stat, closing).
218
+ - **No text-only slides no exceptions.** Every content slide needs a visual: ` ```echart ` for data, inline ` ```svg ` for diagrams/mockups (preferred precise, brand-styled), `box:` cards, or a stat callout. A comparison/pro-con slide is NOT exempt: use cards, a table, or SVG icons. Mix visual types across the deck — all-SVG or all-cards = monotone.
219
+ - **Vary composition.** Never repeat the same layout archetype — two text+visual slides need structurally different layouts (not image-left twice). Mix archetypes, alternate dark/light grounds. One slide should break the pattern — oversized `.grad` stat, serif quote owning the canvas, or full-bleed visual.
220
+ - **One idea per slide.** Each slide delivers one clear message (4 bullets max). The visual and text reinforce the SAME idea — if the visual still communicates without the text, that's right.
221
+ - **Size contrast.** Pair `hero`/`stat` scale with body text. At least one slide MUST feature a single big number or statement using `.huge` or `.stat` — the audience remembers ONE number from every deck. No decorative accent lines under titles (AI tell).
222
+ - **Use every font role** defined in the master (serif, display, mono).
223
+ - **Whitespace.** `--slide-padding` ≥ 96px, `gap` ≥ 2em. `auto` rows for content, `1fr` spacers to centre. Cards need `pad`.
224
+ - **Avoid `<a>` links in slides.** They aren't clickable during presentations and their `link` role color can override slot `color:`. Write URLs as plain text; style with `[text]{.class}` spans.
225
+ - **Muted text must read at projection scale.** Keep well above 2.5:1 contrast. Never use light-ground `muted` inside a dark `box:`/`bg:` card — set explicit light `color:` instead.
226
+ - **Inline SVG:** explicit `width`/`height` required (collapses without them). Images fill their container (the layout controls sizing via grid areas + padding).
227
+ - **Academic / teaching decks** (symposia, lectures, workshops): clarity over flash. Favour ` ```svg ` / ` ```mermaid ` diagrams for processes and systems, ` ```echart ` for data/results. Use `>>>` heavily for progressive build. Larger body text (≥ h3). Section dividers between topics. End with summary/takeaways/questions, not a sales CTA. High-contrast, clean backgrounds.
228
+ - **Financial / board reports**: data credibility first. Heavy ` ```echart ` (bar, line, waterfall, pie). Stats use `.huge` with exact numbers. Comparison tables via `box:` card grids (Plan vs Actual). Conservative palette — dark text on light ground, accent on KPI highlights only. Section dividers per business unit or period. Cover = clean title + period, not a hero. End with outlook/risks.
229
+ - **A deck about slaide?** Brand from `slaide-hugo` repo: azure `#243B6B→#3E6FB0→#6FA8DC`, cream `#FBF7F0`, ink `#15120E`; IBM Plex Sans + JetBrains Mono + Fraunces.