@texuli/deck 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -2
  3. package/dist/authoring/builder.d.ts +351 -0
  4. package/dist/authoring/builder.js +64 -0
  5. package/dist/authoring/index.d.ts +6 -0
  6. package/dist/authoring/index.js +6 -0
  7. package/dist/authoring/lint.d.ts +36 -0
  8. package/dist/authoring/lint.js +325 -0
  9. package/dist/authoring/outlines.d.ts +34 -0
  10. package/dist/authoring/outlines.js +255 -0
  11. package/dist/authoring/patch.d.ts +23 -0
  12. package/dist/authoring/patch.js +166 -0
  13. package/dist/authoring/select-layouts.d.ts +19 -0
  14. package/dist/authoring/select-layouts.js +90 -0
  15. package/dist/engine/auto-slide.d.ts +32 -0
  16. package/dist/engine/auto-slide.js +67 -0
  17. package/dist/engine/canvas.d.ts +56 -0
  18. package/dist/engine/canvas.js +48 -0
  19. package/dist/engine/controller.d.ts +126 -0
  20. package/dist/engine/controller.js +248 -0
  21. package/dist/engine/controls.d.ts +128 -0
  22. package/dist/engine/controls.js +460 -0
  23. package/dist/engine/hash.d.ts +42 -0
  24. package/dist/engine/hash.js +86 -0
  25. package/dist/engine/index.d.ts +88 -0
  26. package/dist/engine/index.js +88 -0
  27. package/dist/engine/messages.d.ts +138 -0
  28. package/dist/engine/messages.js +140 -0
  29. package/dist/engine/motion.d.ts +64 -0
  30. package/dist/engine/motion.js +112 -0
  31. package/dist/engine/navigation.d.ts +87 -0
  32. package/dist/engine/navigation.js +156 -0
  33. package/dist/engine/presenter.d.ts +58 -0
  34. package/dist/engine/presenter.js +157 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +9 -0
  37. package/dist/markdown/attributes.d.ts +24 -0
  38. package/dist/markdown/attributes.js +94 -0
  39. package/dist/markdown/attrs-closed.d.ts +52 -0
  40. package/dist/markdown/attrs-closed.js +520 -0
  41. package/dist/markdown/blocks.d.ts +112 -0
  42. package/dist/markdown/blocks.js +766 -0
  43. package/dist/markdown/frontmatter.d.ts +30 -0
  44. package/dist/markdown/frontmatter.js +165 -0
  45. package/dist/markdown/index.d.ts +29 -0
  46. package/dist/markdown/index.js +29 -0
  47. package/dist/markdown/issues.d.ts +68 -0
  48. package/dist/markdown/issues.js +83 -0
  49. package/dist/markdown/json.d.ts +21 -0
  50. package/dist/markdown/json.js +236 -0
  51. package/dist/markdown/limits.d.ts +32 -0
  52. package/dist/markdown/limits.js +32 -0
  53. package/dist/markdown/parse.d.ts +31 -0
  54. package/dist/markdown/parse.js +283 -0
  55. package/dist/markdown/scan.d.ts +26 -0
  56. package/dist/markdown/scan.js +138 -0
  57. package/dist/markdown/slides.d.ts +32 -0
  58. package/dist/markdown/slides.js +854 -0
  59. package/dist/markdown/text.d.ts +76 -0
  60. package/dist/markdown/text.js +447 -0
  61. package/dist/markdown/twin.d.ts +41 -0
  62. package/dist/markdown/twin.js +374 -0
  63. package/dist/markdown/write.d.ts +17 -0
  64. package/dist/markdown/write.js +688 -0
  65. package/dist/math/index.d.ts +14 -0
  66. package/dist/math/index.js +14 -0
  67. package/dist/math/parse.d.ts +9 -0
  68. package/dist/math/parse.js +648 -0
  69. package/dist/math/symbols.d.ts +44 -0
  70. package/dist/math/symbols.js +333 -0
  71. package/dist/math/types.d.ts +59 -0
  72. package/dist/math/types.js +69 -0
  73. package/dist/model/backgrounds/contrast.d.ts +77 -0
  74. package/dist/model/backgrounds/contrast.js +186 -0
  75. package/dist/model/backgrounds/index.d.ts +7 -0
  76. package/dist/model/backgrounds/index.js +7 -0
  77. package/dist/model/backgrounds/resolve.d.ts +86 -0
  78. package/dist/model/backgrounds/resolve.js +215 -0
  79. package/dist/model/backgrounds/schema.d.ts +178 -0
  80. package/dist/model/backgrounds/schema.js +203 -0
  81. package/dist/model/codes.d.ts +73 -0
  82. package/dist/model/codes.js +67 -0
  83. package/dist/model/deck.d.ts +392 -0
  84. package/dist/model/deck.js +338 -0
  85. package/dist/model/index.d.ts +19 -0
  86. package/dist/model/index.js +19 -0
  87. package/dist/model/layouts/catalog.d.ts +433 -0
  88. package/dist/model/layouts/catalog.js +495 -0
  89. package/dist/model/layouts/index.d.ts +5 -0
  90. package/dist/model/layouts/index.js +5 -0
  91. package/dist/model/layouts/props.d.ts +302 -0
  92. package/dist/model/layouts/props.js +272 -0
  93. package/dist/model/loader.d.ts +37 -0
  94. package/dist/model/loader.js +317 -0
  95. package/dist/model/migrate.d.ts +34 -0
  96. package/dist/model/migrate.js +453 -0
  97. package/dist/model/motion/index.d.ts +6 -0
  98. package/dist/model/motion/index.js +6 -0
  99. package/dist/model/motion/transition.d.ts +96 -0
  100. package/dist/model/motion/transition.js +127 -0
  101. package/dist/model/parse.d.ts +57 -0
  102. package/dist/model/parse.js +307 -0
  103. package/dist/model/plain.d.ts +40 -0
  104. package/dist/model/plain.js +275 -0
  105. package/dist/model/shared.d.ts +63 -0
  106. package/dist/model/shared.js +86 -0
  107. package/dist/model/steps/index.d.ts +7 -0
  108. package/dist/model/steps/index.js +7 -0
  109. package/dist/model/steps/phase.d.ts +34 -0
  110. package/dist/model/steps/phase.js +50 -0
  111. package/dist/model/steps/slide-steps.d.ts +129 -0
  112. package/dist/model/steps/slide-steps.js +256 -0
  113. package/dist/model/style/index.d.ts +8 -0
  114. package/dist/model/style/index.js +8 -0
  115. package/dist/model/style/resolve.d.ts +87 -0
  116. package/dist/model/style/resolve.js +174 -0
  117. package/dist/model/style/schema.d.ts +85 -0
  118. package/dist/model/style/schema.js +53 -0
  119. package/dist/model/style/theme.d.ts +69 -0
  120. package/dist/model/style/theme.js +84 -0
  121. package/dist/schema/builders.d.ts +146 -0
  122. package/dist/schema/builders.js +285 -0
  123. package/dist/schema/diagnostics.d.ts +78 -0
  124. package/dist/schema/diagnostics.js +81 -0
  125. package/dist/schema/index.d.ts +15 -0
  126. package/dist/schema/index.js +14 -0
  127. package/dist/schema/internal.d.ts +40 -0
  128. package/dist/schema/internal.js +209 -0
  129. package/dist/schema/json-schema.d.ts +12 -0
  130. package/dist/schema/json-schema.js +213 -0
  131. package/dist/schema/types.d.ts +166 -0
  132. package/dist/schema/types.js +1 -0
  133. package/dist/schema/validate.d.ts +51 -0
  134. package/dist/schema/validate.js +765 -0
  135. package/dist/templates/index.d.ts +2 -0
  136. package/dist/templates/index.js +2 -0
  137. package/dist/templates/templates.d.ts +11 -0
  138. package/dist/templates/templates.js +47 -0
  139. package/package.json +60 -4
@@ -0,0 +1,495 @@
1
+ /**
2
+ * The deck layout catalog (model-v2 §3.3): 20 layouts with strict props plus
3
+ * `free`, the slide kind whose content is a list of blocks. Data only: ids,
4
+ * copy for authors and agents, pacing fit, limits, the props schema, a valid
5
+ * sample and the paths where a step marker reveals an element.
6
+ */
7
+ import { createDiagnostic, t, toJsonSchema, validate, } from "../../schema/index.js";
8
+ import { steppablePattern } from "../steps/slide-steps.js";
9
+ import { bigStatProps, chartProps, closingCtaProps, codeWithCalloutProps, comparisonTableProps, imageCenteredProps, imageFullBleedProps, imageSideProps, processStepsProps, quoteProps, sectionDividerProps, teamAvatarsProps, threeColumnCardsProps, timelineProps, titleHeroProps, twoColumnSplitProps, videoCenteredProps, videoSideProps, } from "./props.js";
10
+ /**
11
+ * Pacing fit: `punch` lands in a few seconds, `idea` carries one thought,
12
+ * `diagram` explains a structure, `story` moves the narrative.
13
+ */
14
+ export const LAYOUT_PACING = ['punch', 'idea', 'diagram', 'story'];
15
+ /** The 20 layouts with strict props, in catalog order. */
16
+ export const DECK_CATALOG_LAYOUT_IDS = Object.freeze([
17
+ 'title-hero',
18
+ 'section-divider',
19
+ 'two-column-split',
20
+ 'three-column-cards',
21
+ 'image-left',
22
+ 'image-right',
23
+ 'image-centered',
24
+ 'image-full-bleed',
25
+ 'quote',
26
+ 'code-with-callout',
27
+ 'big-stat',
28
+ 'comparison-table',
29
+ 'process-steps',
30
+ 'timeline',
31
+ 'team-avatars',
32
+ 'closing-cta',
33
+ 'video-centered',
34
+ 'video-left',
35
+ 'video-right',
36
+ 'chart',
37
+ ]);
38
+ /** Every catalog id: the 20 props layouts and `free` (21). */
39
+ export const DECK_LAYOUT_IDS = Object.freeze([...DECK_CATALOG_LAYOUT_IDS, 'free']);
40
+ /** The props schema of each catalog layout. */
41
+ const PROPS_SCHEMAS = {
42
+ 'title-hero': titleHeroProps,
43
+ 'section-divider': sectionDividerProps,
44
+ 'two-column-split': twoColumnSplitProps,
45
+ 'three-column-cards': threeColumnCardsProps,
46
+ 'image-left': imageSideProps,
47
+ 'image-right': imageSideProps,
48
+ 'image-centered': imageCenteredProps,
49
+ 'image-full-bleed': imageFullBleedProps,
50
+ quote: quoteProps,
51
+ 'code-with-callout': codeWithCalloutProps,
52
+ 'big-stat': bigStatProps,
53
+ 'comparison-table': comparisonTableProps,
54
+ 'process-steps': processStepsProps,
55
+ timeline: timelineProps,
56
+ 'team-avatars': teamAvatarsProps,
57
+ 'closing-cta': closingCtaProps,
58
+ 'video-centered': videoCenteredProps,
59
+ 'video-left': videoSideProps,
60
+ 'video-right': videoSideProps,
61
+ chart: chartProps,
62
+ };
63
+ const IMAGE = {
64
+ src: 'https://example.com/media/diagram.webp',
65
+ alt: 'A placeholder diagram with three connected boxes',
66
+ width: 640,
67
+ height: 480,
68
+ };
69
+ const VIDEO = {
70
+ src: 'https://example.com/media/demo.mp4',
71
+ label: 'Product demo clip',
72
+ width: 960,
73
+ height: 540,
74
+ muted: true,
75
+ };
76
+ function deepFreeze(value) {
77
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
78
+ for (const child of Object.values(value))
79
+ deepFreeze(child);
80
+ Object.freeze(value);
81
+ }
82
+ return value;
83
+ }
84
+ function entry(id, copy, sample) {
85
+ return {
86
+ id,
87
+ kind: 'props',
88
+ label: copy.label,
89
+ description: copy.description,
90
+ useWhen: copy.useWhen,
91
+ dontUseWhen: copy.dontUseWhen,
92
+ pacing: copy.pacing,
93
+ limits: { summary: copy.limits },
94
+ steppable: copy.steppable,
95
+ props: PROPS_SCHEMAS[id],
96
+ sample,
97
+ };
98
+ }
99
+ /** The layout catalog, keyed by id in catalog order (deeply frozen). */
100
+ export const DECK_LAYOUTS = deepFreeze({
101
+ 'title-hero': entry('title-hero', {
102
+ label: 'Title hero',
103
+ description: 'The cover: a title, a subtitle and a meta line.',
104
+ useWhen: ['The first slide of a deck: title, subtitle, speaker and event.'],
105
+ dontUseWhen: ['Any slide other than the cover.'],
106
+ pacing: ['punch', 'story'],
107
+ limits: ['title 1–100 characters', 'optional subtitle and meta, 160 characters each'],
108
+ steppable: [''],
109
+ }, {
110
+ title: 'Shipping calmer releases',
111
+ subtitle: 'What a small team learned in one year',
112
+ meta: 'Ada Example · Example Conf · 2026',
113
+ }),
114
+ 'section-divider': entry('section-divider', {
115
+ label: 'Section divider',
116
+ description: 'A chapter break with an optional eyebrow.',
117
+ useWhen: ['Opening a new chapter or section of the deck.'],
118
+ dontUseWhen: ['Carrying content: a divider pauses the audience, it does not inform.'],
119
+ pacing: ['punch'],
120
+ limits: ['title 1–120 characters', 'optional eyebrow, 80 characters'],
121
+ steppable: [''],
122
+ }, { eyebrow: 'Part 2', title: 'How the pipeline works' }),
123
+ 'two-column-split': entry('two-column-split', {
124
+ label: 'Two-column split',
125
+ description: 'Two parallel text columns.',
126
+ useWhen: ['Two ideas side by side: before and after, problem and solution.'],
127
+ dontUseWhen: ['There are three or more items (use three-column-cards or a list).'],
128
+ pacing: ['idea', 'diagram'],
129
+ limits: ['exactly two columns', 'body 1–600 characters each', 'optional heading, 80'],
130
+ steppable: ['', 'left', 'right'],
131
+ }, {
132
+ title: 'Before and after',
133
+ left: { heading: 'Before', body: 'Releases waited for a weekly window.' },
134
+ right: { heading: 'After', body: 'Every merged change ships the same day.' },
135
+ }),
136
+ 'three-column-cards': entry('three-column-cards', {
137
+ label: 'Three-column cards',
138
+ description: 'Three parallel cards, each with an icon, a heading and a body.',
139
+ useWhen: ['Three parallel concepts: pillars, features or principles.'],
140
+ dontUseWhen: ['There are more or fewer than three items.'],
141
+ pacing: ['idea', 'diagram'],
142
+ limits: ['exactly 3 cards', 'heading 1–80, body 1–280 characters', 'optional icon name'],
143
+ steppable: ['', 'cards.*'],
144
+ }, {
145
+ title: 'Three principles',
146
+ cards: [
147
+ { icon: 'zap', heading: 'Small', body: 'Ship the smallest useful change first.' },
148
+ { icon: 'compass', heading: 'Clear', body: 'One sentence explains every change.' },
149
+ { icon: 'users', heading: 'Shared', body: 'Anyone on the team can release.' },
150
+ ],
151
+ }),
152
+ 'image-left': entry('image-left', {
153
+ label: 'Image left',
154
+ description: 'An image with explanatory text beside it, image first.',
155
+ useWhen: ['Walking through a screenshot, photo or diagram with some text.'],
156
+ dontUseWhen: ['The image is the whole message (use image-full-bleed).'],
157
+ pacing: ['idea', 'diagram'],
158
+ limits: ['one image with alt text', 'body 1–600 characters'],
159
+ steppable: ['', 'image'],
160
+ }, {
161
+ title: 'The release board',
162
+ image: IMAGE,
163
+ heading: 'What you are looking at',
164
+ body: 'Each box is a stage; a change moves right when its checks pass.',
165
+ }),
166
+ 'image-right': entry('image-right', {
167
+ label: 'Image right',
168
+ description: 'Explanatory text with an image beside it, text first.',
169
+ useWhen: ['The idea comes first and the image anchors it.'],
170
+ dontUseWhen: ['The image needs to be read before the text (use image-left).'],
171
+ pacing: ['idea', 'story'],
172
+ limits: ['one image with alt text', 'body 1–600 characters'],
173
+ steppable: ['', 'image'],
174
+ }, {
175
+ title: 'Lead with the idea',
176
+ image: IMAGE,
177
+ heading: 'Checks run in parallel',
178
+ body: 'Say it first, then show where it happens.',
179
+ }),
180
+ 'image-centered': entry('image-centered', {
181
+ label: 'Image centered',
182
+ description: 'One image is the focus, with a title and a caption.',
183
+ useWhen: ['A single diagram, screenshot or chart image is the point of the slide.'],
184
+ dontUseWhen: [
185
+ 'The image needs text beside it (use image-left or image-right).',
186
+ 'The image should fill the slide (use image-full-bleed).',
187
+ ],
188
+ pacing: ['diagram', 'punch'],
189
+ limits: ['one image with alt text', 'optional caption, 200 characters'],
190
+ steppable: ['', 'image'],
191
+ }, { title: 'The whole pipeline', image: IMAGE, caption: 'Three stages, one direction.' }),
192
+ 'image-full-bleed': entry('image-full-bleed', {
193
+ label: 'Image full bleed',
194
+ description: 'An image fills the slide, with an optional caption and credit.',
195
+ useWhen: ['The image is the message: a hero photo or a quote on an image.'],
196
+ dontUseWhen: ['There is substantial text to say (use image-left or image-right).'],
197
+ pacing: ['punch', 'story'],
198
+ limits: ['one image with alt text', 'optional caption (200) and credit (160)'],
199
+ steppable: ['', 'image'],
200
+ }, {
201
+ image: IMAGE,
202
+ caption: 'Where it started',
203
+ credit: 'Photo: Example Studio',
204
+ overlay: 'dark',
205
+ }),
206
+ quote: entry('quote', {
207
+ label: 'Quote',
208
+ description: 'One short pull quote with its source.',
209
+ useWhen: ['A short quote that earns a slide of its own.'],
210
+ dontUseWhen: ['The quote cannot be attributed, or runs past two lines.'],
211
+ pacing: ['punch', 'story'],
212
+ limits: ['quote 1–280 characters', 'optional cite, 120 characters'],
213
+ steppable: [''],
214
+ }, { quote: 'Make the next step obvious, then make it small.', cite: 'Marco Example' }),
215
+ 'code-with-callout': entry('code-with-callout', {
216
+ label: 'Code with callout',
217
+ description: 'A code block beside a short explanation.',
218
+ useWhen: ['Explaining what a short piece of code does.'],
219
+ dontUseWhen: ['The code is too long for a slide: trim it and step through highlights.'],
220
+ pacing: ['diagram', 'idea'],
221
+ limits: [
222
+ 'source 1–8,000 characters, kept verbatim',
223
+ '1–6 bullets of up to 200 characters',
224
+ 'up to 16 highlight ranges, each revealed as a step',
225
+ ],
226
+ steppable: ['', 'code'],
227
+ }, {
228
+ title: 'One function, one job',
229
+ code: {
230
+ lang: 'ts',
231
+ source: 'export function total(items: number[]) {\n return items.reduce((a, b) => a + b, 0);\n}',
232
+ highlights: ['2'],
233
+ },
234
+ heading: 'What is happening',
235
+ bullets: ['Sum the list in one pass', 'Start from zero for an empty list'],
236
+ }),
237
+ 'big-stat': entry('big-stat', {
238
+ label: 'Big stat',
239
+ description: 'One number that lands a point.',
240
+ useWhen: ['A single number makes the case: adoption, scale, time saved.'],
241
+ dontUseWhen: ['The trend matters more than the number (use chart).'],
242
+ pacing: ['punch'],
243
+ limits: ['number 1–32 characters', 'label 1–200 characters', 'optional context and source'],
244
+ steppable: [''],
245
+ }, {
246
+ number: '87%',
247
+ label: 'of releases shipped on the day they merged',
248
+ context: 'A fictional team, over twelve months',
249
+ source: 'n = 412 releases',
250
+ }),
251
+ 'comparison-table': entry('comparison-table', {
252
+ label: 'Comparison table',
253
+ description: 'Two to four options compared across three to five attributes.',
254
+ useWhen: ['Comparing 2–4 options across 3–5 attributes.'],
255
+ dontUseWhen: ['There are more than four options or five attributes.'],
256
+ pacing: ['diagram', 'idea'],
257
+ limits: ['2–4 columns', '3–5 rows', 'every row has one value per column'],
258
+ steppable: ['', 'rows.*'],
259
+ }, {
260
+ title: 'Two ways to release',
261
+ columns: ['Weekly window', 'Continuous'],
262
+ rows: [
263
+ { label: 'Lead time', values: ['5 days', 'Same day'] },
264
+ { label: 'Rollback', values: [false, true] },
265
+ { label: 'Best for', values: ['Large batches', 'Small changes'] },
266
+ ],
267
+ }),
268
+ 'process-steps': entry('process-steps', {
269
+ label: 'Process steps',
270
+ description: 'A linear process of three to five steps.',
271
+ useWhen: ['A 3–5 step process or workflow.'],
272
+ dontUseWhen: ['There are more than five steps (use timeline) or the order does not matter.'],
273
+ pacing: ['idea', 'diagram'],
274
+ limits: ['3–5 steps', 'title 1–80, optional body 200 characters'],
275
+ steppable: ['', 'steps.*'],
276
+ }, {
277
+ title: 'How a change ships',
278
+ steps: [
279
+ { title: 'Propose', body: 'Open a small pull request.' },
280
+ { title: 'Check', body: 'Tests and reviews run in parallel.' },
281
+ { title: 'Merge', body: 'The branch lands on main.' },
282
+ { title: 'Release', body: 'The pipeline publishes it.' },
283
+ ],
284
+ }),
285
+ timeline: entry('timeline', {
286
+ label: 'Timeline',
287
+ description: 'Four to eight dated events that tell a story.',
288
+ useWhen: ['4–8 dated events that form a story arc.'],
289
+ dontUseWhen: ['The dates do not matter, or there are more than eight events.'],
290
+ pacing: ['story', 'diagram'],
291
+ limits: ['4–8 events', 'time 1–40, text 1–200 characters'],
292
+ steppable: ['', 'events.*'],
293
+ }, {
294
+ title: 'One year of releases',
295
+ events: [
296
+ { time: 'January', text: 'First automated release' },
297
+ { time: 'April', text: 'Checks run in parallel' },
298
+ { time: 'August', text: 'Same-day releases by default' },
299
+ { time: 'December', text: 'Rollback in one click' },
300
+ ],
301
+ }),
302
+ 'team-avatars': entry('team-avatars', {
303
+ label: 'Team avatars',
304
+ description: 'Up to eight people with roles and optional avatars.',
305
+ useWhen: ['Introducing the team or the co-presenters.'],
306
+ dontUseWhen: ['There are more than eight people (group them instead).'],
307
+ pacing: ['idea', 'story'],
308
+ limits: ['1–8 people', 'name 1–80, optional role 80', 'optional avatar id or media source'],
309
+ steppable: ['', 'people.*'],
310
+ }, {
311
+ title: 'The team',
312
+ people: [
313
+ { name: 'Ada Example', role: 'Design', avatar: 'av-001' },
314
+ { name: 'Marco Example', role: 'Engineering', avatar: 'av-002' },
315
+ ],
316
+ }),
317
+ 'closing-cta': entry('closing-cta', {
318
+ label: 'Closing call to action',
319
+ description: 'The last slide, with one primary action.',
320
+ useWhen: ['The final slide: what to do next and where to go.'],
321
+ dontUseWhen: ['In the middle of the deck (use section-divider for a break).'],
322
+ pacing: ['punch', 'story'],
323
+ limits: [
324
+ 'title 1–120 characters',
325
+ 'one link: label 1–60, href up to 512',
326
+ 'optional footnote',
327
+ ],
328
+ steppable: ['', 'cta'],
329
+ }, {
330
+ title: 'Thank you',
331
+ text: 'Questions are welcome after the talk.',
332
+ cta: { label: 'Read the guide', href: 'https://example.com/guide' },
333
+ footnote: 'All names and numbers are fictional.',
334
+ }),
335
+ 'video-centered': entry('video-centered', {
336
+ label: 'Video centered',
337
+ description: 'One video is the focus, with a title and a caption.',
338
+ useWhen: ['A single demo, trailer or explainer clip is the point of the slide.'],
339
+ dontUseWhen: ['The video needs text beside it (use video-left or video-right).'],
340
+ pacing: ['diagram', 'story'],
341
+ limits: ['one video', 'optional caption, 200 characters'],
342
+ steppable: ['', 'video'],
343
+ }, { title: 'The demo', video: VIDEO, caption: 'Recorded on a test project.' }),
344
+ 'video-left': entry('video-left', {
345
+ label: 'Video left',
346
+ description: 'A video with explanatory text beside it, video first.',
347
+ useWhen: ['Walking through a demo or clip with some text.'],
348
+ dontUseWhen: ['The video is the only focus (use video-centered).'],
349
+ pacing: ['idea', 'diagram'],
350
+ limits: ['one video', 'body 1–600 characters'],
351
+ steppable: ['', 'video'],
352
+ }, {
353
+ title: 'A release in one minute',
354
+ video: VIDEO,
355
+ heading: 'What you are watching',
356
+ body: 'A change goes from merge to production while the checks report.',
357
+ }),
358
+ 'video-right': entry('video-right', {
359
+ label: 'Video right',
360
+ description: 'Explanatory text with a video beside it, text first.',
361
+ useWhen: ['The idea comes first and the clip shows it.'],
362
+ dontUseWhen: ['The video needs to be watched before the text (use video-left).'],
363
+ pacing: ['idea', 'story'],
364
+ limits: ['one video', 'body 1–600 characters'],
365
+ steppable: ['', 'video'],
366
+ }, {
367
+ title: 'Lead with the idea',
368
+ video: VIDEO,
369
+ heading: 'Rollback is one click',
370
+ body: 'Say it first, then let the clip prove it.',
371
+ }),
372
+ chart: entry('chart', {
373
+ label: 'Chart',
374
+ description: 'A chart block (any Texuli chart kind) with an optional data table.',
375
+ useWhen: ['A trend or a distribution is clearer as a chart than as a number or a table.'],
376
+ dontUseWhen: ['One number makes the point (use big-stat).'],
377
+ pacing: ['diagram', 'idea'],
378
+ limits: [
379
+ 'spec: a chart block without `type`, checked by the stage chart block parser',
380
+ 'spec JSON: depth 6, 100 keys per object, 400 items per array',
381
+ 'optional height (sm, md, lg) and data table',
382
+ ],
383
+ steppable: ['', 'spec'],
384
+ }, {
385
+ title: 'Releases per month',
386
+ spec: {
387
+ kind: 'bar',
388
+ title: 'Releases per month',
389
+ description: 'Releases grew from 12 in January to 31 in March.',
390
+ series: [
391
+ {
392
+ id: 'releases',
393
+ label: 'Releases',
394
+ points: [
395
+ { x: 'Jan', y: 12 },
396
+ { x: 'Feb', y: 22 },
397
+ { x: 'Mar', y: 31 },
398
+ ],
399
+ },
400
+ ],
401
+ },
402
+ height: 'md',
403
+ showTable: true,
404
+ }),
405
+ free: {
406
+ id: 'free',
407
+ kind: 'blocks',
408
+ label: 'Free blocks',
409
+ description: 'A list of validated content blocks, for when no layout fits.',
410
+ useWhen: ['No catalog layout fits the idea; compose it from content blocks.'],
411
+ dontUseWhen: ['A catalog layout already fits: prefer it for lint, pacing and steps.'],
412
+ pacing: ['punch', 'idea', 'diagram', 'story'],
413
+ limits: {
414
+ summary: [
415
+ '1–2,000 top-level blocks; lint warns past 48',
416
+ 'each block follows the blocks schema and may carry step fields',
417
+ ],
418
+ },
419
+ steppable: [],
420
+ props: null,
421
+ sample: null,
422
+ },
423
+ });
424
+ /** Codes this module adds to the schema kit's own. */
425
+ export const LAYOUT_CODES = Object.freeze({
426
+ unknownLayout: 'unknown-layout',
427
+ layoutProps: 'layout-props',
428
+ });
429
+ /** True for the 21 catalog ids (own keys only, so `constructor` is never one). */
430
+ export function isLayoutId(value) {
431
+ return typeof value === 'string' && DECK_LAYOUT_IDS.includes(value);
432
+ }
433
+ /** True for the 20 layouts that take strict props (every id but `free`). */
434
+ export function isCatalogLayoutId(value) {
435
+ return (typeof value === 'string' && DECK_CATALOG_LAYOUT_IDS.includes(value));
436
+ }
437
+ /** The catalog entry for `id`, or `undefined`. */
438
+ export function getDeckLayout(id) {
439
+ return isLayoutId(id) ? DECK_LAYOUTS[id] : undefined;
440
+ }
441
+ export function layoutPropsSchema(id) {
442
+ return isCatalogLayoutId(id) ? PROPS_SCHEMAS[id] : undefined;
443
+ }
444
+ /** The steppable path patterns of `id` (`''`, `image`, `cards.*`); empty when unknown. */
445
+ export function layoutSteppablePaths(id) {
446
+ return isLayoutId(id) ? DECK_LAYOUTS[id].steppable : [];
447
+ }
448
+ /** The pattern of a path inside `props` (one definition, shared with the steps pass). */
449
+ export { steppablePattern };
450
+ /** Whether `layout` reveals a step marker found at `path` (relative to `props`). */
451
+ export function isSteppablePath(layout, path) {
452
+ return layoutSteppablePaths(layout).includes(steppablePattern(path));
453
+ }
454
+ function failure(issue) {
455
+ return { ok: false, issues: [issue], normalized: [], truncated: false };
456
+ }
457
+ export function validateLayoutProps(id, props, options = {}) {
458
+ const path = options.path ?? [];
459
+ if (isCatalogLayoutId(id)) {
460
+ const { layoutPath: _ignored, ...rest } = options;
461
+ return validate(PROPS_SCHEMAS[id], props, rest);
462
+ }
463
+ if (id === 'free') {
464
+ return failure(createDiagnostic(LAYOUT_CODES.layoutProps, 'A free slide has blocks, not props.', path, {
465
+ fix: 'Move the content into the slide `blocks` list, or pick a catalog layout.',
466
+ }));
467
+ }
468
+ const layoutPath = options.layoutPath ??
469
+ (path.length > 0 && path[path.length - 1] === 'props'
470
+ ? [...path.slice(0, -1), 'layout']
471
+ : path);
472
+ const shown = typeof id === 'string' ? JSON.stringify(id.slice(0, 64)) : `of type ${typeof id}`;
473
+ return failure(createDiagnostic(LAYOUT_CODES.unknownLayout, `Unknown layout ${shown}.`, layoutPath, {
474
+ fix: `Use one of: ${DECK_LAYOUT_IDS.join(', ')}.`,
475
+ }));
476
+ }
477
+ /**
478
+ * One JSON Schema 2020-12 document for every catalog layout's props: a
479
+ * `$defs` entry named by layout id (`#/$defs/title-hero`), and a root that
480
+ * accepts any of them. `free` has no entry (its blocks belong to the deck
481
+ * schema). Refinements are described in `description`.
482
+ */
483
+ export function layoutsJsonSchema() {
484
+ const refs = DECK_CATALOG_LAYOUT_IDS.map((id) => t.lazy(() => PROPS_SCHEMAS[id], {
485
+ name: id,
486
+ description: `Props of the ${DECK_LAYOUTS[id].label.toLowerCase()} layout.`,
487
+ }));
488
+ const [first, ...more] = refs;
489
+ if (first === undefined)
490
+ return toJsonSchema(t.object({}));
491
+ return toJsonSchema(t.union([first, ...more]), {
492
+ title: 'Texuli deck layout props',
493
+ description: 'The props of one deck layout; pick the definition named by the slide layout id.',
494
+ });
495
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The deck layout catalog: 20 layouts with strict props and `free` (model-v2 §3.3).
3
+ */
4
+ export { DECK_CATALOG_LAYOUT_IDS, DECK_LAYOUT_IDS, DECK_LAYOUTS, type DeckCatalogLayoutId, type DeckFreeLayoutEntry, type DeckLayoutCatalog, type DeckLayoutCatalogEntry, type DeckLayoutEntry, type DeckLayoutId, getDeckLayout, isCatalogLayoutId, isLayoutId, isSteppablePath, LAYOUT_CODES, LAYOUT_PACING, type LayoutLimits, type LayoutPacing, type LayoutProps, type LayoutValidateOptions, layoutPropsSchema, layoutSteppablePaths, layoutsJsonSchema, steppablePattern, validateLayoutProps, } from './catalog.js';
5
+ export { bigStatProps, CHART_SPEC_LIMITS, chartProps, chartSpec, closingCtaProps, codeWithCalloutProps, comparisonTableProps, IMAGE_OVERLAYS, imageCenteredProps, imageFullBleedProps, imageSideProps, processStepsProps, quoteProps, sectionDividerProps, teamAvatarsProps, threeColumnCardsProps, timelineProps, titleHeroProps, twoColumnSplitProps, videoCenteredProps, videoSideProps, } from './props.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The deck layout catalog: 20 layouts with strict props and `free` (model-v2 §3.3).
3
+ */
4
+ export { DECK_CATALOG_LAYOUT_IDS, DECK_LAYOUT_IDS, DECK_LAYOUTS, getDeckLayout, isCatalogLayoutId, isLayoutId, isSteppablePath, LAYOUT_CODES, LAYOUT_PACING, layoutPropsSchema, layoutSteppablePaths, layoutsJsonSchema, steppablePattern, validateLayoutProps, } from "./catalog.js";
5
+ export { bigStatProps, CHART_SPEC_LIMITS, chartProps, chartSpec, closingCtaProps, codeWithCalloutProps, comparisonTableProps, IMAGE_OVERLAYS, imageCenteredProps, imageFullBleedProps, imageSideProps, processStepsProps, quoteProps, sectionDividerProps, teamAvatarsProps, threeColumnCardsProps, timelineProps, titleHeroProps, twoColumnSplitProps, videoCenteredProps, videoSideProps, } from "./props.js";