slotdeck 0.1.1 → 0.2.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 (109) hide show
  1. package/README.md +94 -8
  2. package/dist/deck/alternatives.d.ts +34 -0
  3. package/dist/deck/alternatives.js +370 -0
  4. package/dist/deck/alternatives.js.map +1 -0
  5. package/dist/deck/design.d.ts +40 -0
  6. package/dist/deck/design.js +112 -0
  7. package/dist/deck/design.js.map +1 -0
  8. package/dist/deck/validate.d.ts +18 -0
  9. package/dist/deck/validate.js +44 -6
  10. package/dist/deck/validate.js.map +1 -1
  11. package/dist/index.d.ts +23 -1
  12. package/dist/index.js +14 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/llm/gemini.d.ts +4 -0
  15. package/dist/llm/gemini.js +43 -7
  16. package/dist/llm/gemini.js.map +1 -1
  17. package/dist/llm/provider.d.ts +12 -0
  18. package/dist/node.d.ts +7 -0
  19. package/dist/node.js +29 -0
  20. package/dist/node.js.map +1 -1
  21. package/dist/plan/catalog.d.ts +11 -1
  22. package/dist/plan/catalog.js +23 -8
  23. package/dist/plan/catalog.js.map +1 -1
  24. package/dist/plan/edit.js +14 -6
  25. package/dist/plan/edit.js.map +1 -1
  26. package/dist/plan/plan.d.ts +26 -2
  27. package/dist/plan/plan.js +58 -17
  28. package/dist/plan/plan.js.map +1 -1
  29. package/dist/plan/review.d.ts +43 -0
  30. package/dist/plan/review.js +141 -0
  31. package/dist/plan/review.js.map +1 -0
  32. package/dist/plan/write.d.ts +13 -1
  33. package/dist/plan/write.js +40 -7
  34. package/dist/plan/write.js.map +1 -1
  35. package/dist/preview/pagus.d.ts +30 -0
  36. package/dist/preview/pagus.js +45 -0
  37. package/dist/preview/pagus.js.map +1 -0
  38. package/dist/preview/patterns.d.ts +27 -0
  39. package/dist/preview/patterns.js +35 -0
  40. package/dist/preview/patterns.js.map +1 -0
  41. package/dist/preview/provider.d.ts +30 -0
  42. package/dist/preview/provider.js +11 -0
  43. package/dist/preview/provider.js.map +1 -0
  44. package/dist/render/render.d.ts +18 -1
  45. package/dist/render/render.js +32 -12
  46. package/dist/render/render.js.map +1 -1
  47. package/dist/render/repeat.d.ts +17 -2
  48. package/dist/render/repeat.js +150 -4
  49. package/dist/render/repeat.js.map +1 -1
  50. package/dist/render/text.d.ts +6 -0
  51. package/dist/render/text.js +24 -0
  52. package/dist/render/text.js.map +1 -1
  53. package/dist/template/budgets.d.ts +33 -0
  54. package/dist/template/budgets.js +38 -0
  55. package/dist/template/budgets.js.map +1 -0
  56. package/dist/template/change.d.ts +19 -1
  57. package/dist/template/change.js +45 -3
  58. package/dist/template/change.js.map +1 -1
  59. package/dist/template/check.d.ts +3 -0
  60. package/dist/template/check.js +53 -3
  61. package/dist/template/check.js.map +1 -1
  62. package/dist/template/derive.d.ts +54 -0
  63. package/dist/template/derive.js +402 -0
  64. package/dist/template/derive.js.map +1 -0
  65. package/dist/template/draft.d.ts +7 -0
  66. package/dist/template/draft.js +17 -1
  67. package/dist/template/draft.js.map +1 -1
  68. package/dist/template/instruct.js +15 -7
  69. package/dist/template/instruct.js.map +1 -1
  70. package/dist/template/manifest.d.ts +38 -0
  71. package/dist/template/manifest.js +43 -1
  72. package/dist/template/manifest.js.map +1 -1
  73. package/dist/template/sample.d.ts +17 -0
  74. package/dist/template/sample.js +31 -5
  75. package/dist/template/sample.js.map +1 -1
  76. package/dist/text/fontkit.d.ts +18 -0
  77. package/dist/text/fontkit.js +91 -0
  78. package/dist/text/fontkit.js.map +1 -0
  79. package/dist/text/fonts.d.ts +45 -0
  80. package/dist/text/fonts.js +59 -0
  81. package/dist/text/fonts.js.map +1 -0
  82. package/dist/text/layout.d.ts +12 -0
  83. package/dist/text/layout.js +104 -0
  84. package/dist/text/layout.js.map +1 -0
  85. package/dist/text/measure.d.ts +85 -0
  86. package/dist/text/measure.js +369 -0
  87. package/dist/text/measure.js.map +1 -0
  88. package/dist/text/styles.d.ts +123 -0
  89. package/dist/text/styles.js +417 -0
  90. package/dist/text/styles.js.map +1 -0
  91. package/dist/tools.d.ts +12 -0
  92. package/dist/tools.js +38 -6
  93. package/dist/tools.js.map +1 -1
  94. package/docs/README.md +16 -5
  95. package/docs/api-reference.md +291 -9
  96. package/docs/changelog.md +242 -0
  97. package/docs/chat-tools.md +26 -5
  98. package/docs/deck-format.md +6 -0
  99. package/docs/design-checks.md +216 -0
  100. package/docs/generating.md +43 -4
  101. package/docs/layout-freedom.md +278 -3
  102. package/docs/manifest-format.md +20 -3
  103. package/docs/preview.md +217 -0
  104. package/docs/providers.md +49 -4
  105. package/docs/rendering.md +8 -0
  106. package/docs/template-studio.md +121 -12
  107. package/docs/text-fit.md +456 -0
  108. package/docs/troubleshooting.md +21 -2
  109. package/package.json +7 -1
package/docs/providers.md CHANGED
@@ -20,6 +20,8 @@ interface JsonRequest {
20
20
  system: string; // the system prompt: the role and the rules
21
21
  prompt: string; // the user message: the brief, the manifest's patterns, what to answer
22
22
  schema: JsonSchema; // the JSON Schema the answer must follow
23
+ maxOutputTokens?: number; // a ceiling on the answer's size in tokens, reasoning not counted; see "Runaway answers"
24
+ effort?: 'low'; // set on short structural requests: use light reasoning if your model has a setting for it
23
25
  images?: Array<{ mimeType: string; data: Uint8Array }>; // pictures to look at; only draftManifest sends them
24
26
  signal?: AbortSignal; // pass it to your HTTP call so work can be cancelled
25
27
  }
@@ -30,8 +32,8 @@ The contract:
30
32
  - Resolve with the **parsed** value (an object), not a string of JSON.
31
33
  - The value should follow `schema`. slotdeck checks what it gets back against the template's manifest and asks
32
34
  again when content breaks a limit, but it expects the overall shape the schema describes.
33
- - Reject (throw) when the call fails. slotdeck does not retry failed calls; add retries in your provider if you
34
- want them.
35
+ - Reject (throw) when the call fails. The planner does not retry failed calls; retries belong in the provider,
36
+ which knows which failures pass by themselves (see "Runaway answers" and the Gemini adapter below).
35
37
  - One call is one request and one answer. There is no conversation state: every request carries everything it
36
38
  needs.
37
39
 
@@ -106,7 +108,7 @@ Every top-level answer slotdeck asks for is a JSON object, so looking for the ou
106
108
 
107
109
  ### Choosing a model
108
110
 
109
- - The requests are small. Measured on the demo template (15 patterns): the outline request is a system prompt
111
+ - The requests are small. Measured on the demo template when it had 15 patterns: the outline request is a system prompt
110
112
  of about 600 characters, a prompt of about 3,600 characters and a schema of about 700 characters. The writing
111
113
  request for a 15-slide deck is a prompt of about 1,500 characters and a schema of about 9,800 characters. Any
112
114
  current general-purpose model handles this.
@@ -117,6 +119,47 @@ Every top-level answer slotdeck asks for is a JSON object, so looking for the ou
117
119
  - `draftManifest` can send a picture of each slide (`images`). A provider for a model that cannot see may ignore
118
120
  them; the draft then relies on measurements alone.
119
121
 
122
+ ### Runaway answers: always set an output limit
123
+
124
+ Now and then a model runs away inside one text field of its answer: it keeps adding sentence fragments to a
125
+ single string until something stops it. This was seen with a planning request whose proper answer is about 200
126
+ tokens: the model wrote more than 27,000 tokens into one field. Without a limit such a call takes minutes, costs
127
+ tens of thousands of output tokens, and ends in a timeout. It is a matter of chance: the same request usually
128
+ works the next time.
129
+
130
+ It was tied to reasoning. With the model's default reasoning, a request for a text-only plan ran away in 6 of
131
+ 24 calls. With light reasoning it did not run away in 18 calls, and each took 4 seconds instead of about 35.
132
+ That is why planning requests carry `effort: 'low'`.
133
+
134
+ So a provider should do three things:
135
+
136
+ 1. **Use light reasoning when a request says `effort: 'low'`**, if your model has such a setting.
137
+ 2. **Limit the output of every call.** The planner passes `maxOutputTokens` with its requests: a generous
138
+ ceiling for that answer (for an outline of six slides, 1,360 tokens). Pass it to your model as its output
139
+ limit. If your model counts reasoning tokens against the same limit, add room for them. When a request
140
+ carries no estimate, use a default of your own rather than no limit.
141
+ 3. **Ask once more when an answer is cut off at the limit.** A cut-off answer is not valid JSON. Retry the same
142
+ request once, and throw if it is cut off again.
143
+
144
+ ```ts
145
+ async generateJson({ system, prompt, schema, maxOutputTokens, effort, signal }) {
146
+ const limit = (maxOutputTokens ?? 16_000) + 8_000; // room for reasoning, if your model counts it here
147
+ const reasoning = effort === 'low' ? 'low' : undefined; // whatever your model calls its light setting
148
+ for (let attempt = 1; attempt <= 2; attempt++) {
149
+ const answer = await callModel({ system, prompt, schema, limit, reasoning, signal });
150
+ if (!answer.cutOffAtLimit) return JSON.parse(answer.text);
151
+ }
152
+ throw new Error(`The model's answer was cut off at ${limit} tokens twice`);
153
+ }
154
+ ```
155
+
156
+ The included Gemini adapter does all three. Its limit is the request's estimate plus 8,192 tokens for reasoning
157
+ (24,576 when there is no estimate, or its own `maxOutputTokens` option when set). It sends a cut-off request
158
+ once more. It uses thinking level `low` for requests with `effort: 'low'`, unless `thinkingLevel` is set on the
159
+ adapter, which then applies to every request. It also sends a request once more after a server error (500 to
160
+ 504) or when the service's recitation filter blocks an ordinary answer, which happens at random; a bad key, a
161
+ quota or a bad request is not repeated.
162
+
120
163
  ### Timeouts and cancellation
121
164
 
122
165
  slotdeck passes `signal` through when the caller gave one. Your provider should pass it to its HTTP call, and
@@ -203,7 +246,9 @@ Options of both:
203
246
  | `fetch` | A custom `fetch`, for proxies and tests. |
204
247
  | `timeoutMs` | Gives up on a request after this long. Default 180000 (3 minutes). `0` waits for ever. |
205
248
 
206
- Only for `gemini`: `thinkingLevel` (`minimal`, `low`, `medium`, `high`) and `maxOutputTokens`.
249
+ Only for `gemini`: `thinkingLevel` (`minimal`, `low`, `medium`, `high`) and `maxOutputTokens`, the output limit
250
+ of a request with reasoning included. By default the limit is the request's own estimate plus 8,192 tokens, or
251
+ 24,576 when the request has none.
207
252
 
208
253
  Only for `geminiImages`: `imageSize` (`512`, `1K`, `2K`, `4K`; default `1K`) and `style`, a sentence appended to
209
254
  every prompt, such as a house photo style.
package/docs/rendering.md CHANGED
@@ -20,6 +20,12 @@ is fast: a 15-slide deck from the demo template takes about 100 ms.
20
20
  It does not verify the manifest. Call `checkManifest(template)` when a template or manifest is loaded if you
21
21
  want to catch a manifest that no longer matches its file (see [template-studio.md](template-studio.md)).
22
22
 
23
+ To get a picture of each slide of the result without PowerPoint, see [preview.md](preview.md).
24
+
25
+ A third argument, `{ fonts }`, gives the template the metrics of its fonts. Text is then measured as it will
26
+ be set: warnings say how many lines a text runs over, and text can be shrunk to fit. See
27
+ [text-fit.md](text-fit.md).
28
+
23
29
  ## `renderDeck(template, deck, options?)`
24
30
 
25
31
  ```ts
@@ -33,6 +39,7 @@ interface RenderOptions {
33
39
  interface RenderResult {
34
40
  pptx: Uint8Array;
35
41
  warnings: Issue[]; // non-blocking findings, such as text over a slot's budget
42
+ shrunk: Array<{ slide: number; slot: string; size: number; scale: number }>; // see text-fit.md; empty unless the manifest has shrinkText
36
43
  }
37
44
  ```
38
45
 
@@ -97,6 +104,7 @@ function validateDeck(manifest: TemplateManifest, deck: DeckSpec, options?: Vali
97
104
  interface ValidateOptions {
98
105
  layout?: 'strict' | 'adaptive';
99
106
  assets?: Iterable<string>; // ids that will be available at render time; when given, image references are checked
107
+ measure?: TextMeasurer; // measures text with the template's fonts: see text-fit.md
100
108
  }
101
109
 
102
110
  interface Issue {
@@ -5,10 +5,10 @@ manifest with a model, checking it, seeing whether its budgets hold, and changin
5
5
 
6
6
  ```ts
7
7
  import {
8
- draftManifest, checkManifest, sampleDeck, changeManifest, editManifest,
8
+ draftManifest, checkManifest, sampleDeck, changeManifest, editManifest, patternVariants, VARIANTS,
9
9
  studioView, describeManifest, describeSlot, inspectTemplate, ROLES, PptxPackage, loadTemplate, renderDeck,
10
10
  } from 'slotdeck';
11
- import type { ManifestChange, SlotPatch, ChangeResult, ManifestIssue, Role, StudioView } from 'slotdeck';
11
+ import type { ManifestChange, SlotPatch, ChangeResult, ManifestIssue, PatternVariant, Role, StudioView, VariantOption } from 'slotdeck';
12
12
  ```
13
13
 
14
14
  ## The workflow
@@ -21,8 +21,8 @@ import type { ManifestChange, SlotPatch, ChangeResult, ManifestIssue, Role, Stud
21
21
  5. Repeat 2 to 4 until the sample looks right. Store the manifest as JSON next to the pptx.
22
22
 
23
23
  A draft is a starting point for review, not a finished manifest. What a draft does not contain, and a person
24
- adds: budgets tuned by eye, derived patterns (a text-only variant, a native chart), wider rows, picture areas,
25
- chart colours, and alignment choices for decks that read the other way.
24
+ adds: budgets tuned by eye, derived patterns (a mirrored or text-only variant, a native chart), wider rows,
25
+ picture areas, chart colours, and alignment choices for decks that read the other way.
26
26
 
27
27
  ## `draftManifest(pptx, options)`
28
28
 
@@ -32,6 +32,7 @@ const { manifest, skipped, issues } = await draftManifest(pptxBytes, {
32
32
  name: 'marketing', // optional name for the manifest; default 'template'
33
33
  slideImage: (slide) => thumbnails.get(slide), // optional: { mimeType, data } of a picture of slide N (1-based); may be async
34
34
  slides: [1, 2, 5], // optional: only these slides; default all
35
+ variants: ['mirror', 'text-only'], // optional: also add these variants of every pattern that allows them; default none
35
36
  concurrency: 4, // optional: slides sent to the model at once; default 4
36
37
  onProgress: ({ done, total, slide }) => {}, // optional
37
38
  signal, // optional
@@ -71,6 +72,18 @@ Any PNG or JPEG of the slide works; 480 pixels wide is enough.
71
72
  To redo one slide after changing the pptx, call `draftManifest` with `slides: [n]` and add the pattern it
72
73
  returns to your manifest.
73
74
 
75
+ ### Variants in a draft
76
+
77
+ With `variants`, the draft also gets the mirror image and the text-only version of every drafted pattern whose
78
+ slide allows one (see `derivePattern` below). They are worked out by code after the model has labelled the
79
+ slides, so they cost no model call. A pattern that allows no variant simply gets none.
80
+
81
+ This is off by default, for three reasons. Every variant is one more layout to review in the sample deck.
82
+ Whether a mirrored slide suits a design is for a person to say. And a variant is only as good as the labels it
83
+ is derived from, which a draft has not had reviewed yet. The usual order is therefore: draft, review, then add
84
+ the variants you want with `derivePattern`. Use `variants` when a studio should show everything a template can
85
+ do in one pass. The `without` variant is never added by a draft, because it needs a slot to be named.
86
+
74
87
  ### Roles
75
88
 
76
89
  `ROLES` lists them. A role says what a shape is for and decides the kind of slot it becomes.
@@ -106,6 +119,12 @@ kind of shape, names are unique, rows are consistent. The rules are listed in
106
119
  [manifest-format.md](manifest-format.md). Run it when a template is loaded, so a template that someone changed
107
120
  shows up there and not in the middle of a render.
108
121
 
122
+ For a template loaded with fonts it also tries every text budget out and warns about one that lets text run
123
+ over its box. A row with `widen` gives its texts a larger budget when a slide has fewer items; that budget is
124
+ tried in the wider box, for each number of items that widens the row, and a warning then says which
125
+ (`on a slide with 2 of its 4 items`). See "The studio: checking and setting budgets" in
126
+ [text-fit.md](text-fit.md).
127
+
109
128
  ## `sampleDeck(template, options?)`
110
129
 
111
130
  ```ts
@@ -120,6 +139,10 @@ filler words and plain coloured rectangles for pictures.
120
139
  | --- | --- | --- |
121
140
  | `fill` | `0.95` | How full to make each text slot, as a share of its budget. |
122
141
  | `items` | `'drawn'` | For patterns with a row or grid: `'drawn'` uses the template's number, `'max'` the most allowed, `'min'` the fewest. |
142
+ | `fit` | `'budget'` | What "full" means for a text slot: `'budget'` is its character budget; `'box'` is what its box holds as measured with the template's fonts (see [text-fit.md](text-fit.md)). |
143
+
144
+ To look at the sample deck without opening PowerPoint, draw it with a preview provider:
145
+ `previewDeck(template, deck, preview, { assets, layout: 'adaptive' })`. See [preview.md](preview.md).
123
146
 
124
147
  ## `changeManifest(template, changes)`
125
148
 
@@ -145,9 +168,12 @@ Every change is an object with `op`. `pattern` is a pattern id, `slot` a slot na
145
168
  #### `setManifest`
146
169
 
147
170
  ```ts
148
- { op: 'setManifest', name?: string, language?: string, direction?: 'ltr' | 'rtl', alignOnDirectionChange?: 'flip' | 'keep' }
171
+ { op: 'setManifest', name?: string, language?: string, direction?: 'ltr' | 'rtl', alignOnDirectionChange?: 'flip' | 'keep', textScale?: number | null }
149
172
  ```
150
173
 
174
+ `textScale` enlarges the body text of every deck made from the template (`1.3` for 30% larger); `null` or `1`
175
+ returns to the designed size.
176
+
151
177
  #### `addPattern`
152
178
 
153
179
  ```ts
@@ -164,7 +190,38 @@ instead to get a complete pattern.
164
190
  { op: 'copyPattern', pattern: string, id: string, description?: string }
165
191
  ```
166
192
 
167
- A second pattern from the same slide, to turn into a variant with further operations.
193
+ A second pattern from the same slide, to turn into a variant by hand with further operations. For the mirror
194
+ image, a text-only version or a version without an optional line, use `derivePattern`, which works out the
195
+ positions.
196
+
197
+ #### `derivePattern`
198
+
199
+ ```ts
200
+ { op: 'derivePattern', pattern: string, variant: 'mirror' | 'text-only' | 'without', slot?: string, id?: string, description?: string }
201
+ ```
202
+
203
+ A second pattern from the same slide, laid out by the code. You name the variant; every position and budget is
204
+ worked out from the shapes of the slide. `VARIANTS` lists the variant names.
205
+
206
+ | Variant | What it makes | Default id |
207
+ | --- | --- | --- |
208
+ | `mirror` | The picture and what sits beside it change sides. | `<pattern>-mirrored` |
209
+ | `text-only` | The picture is left out, and the text beside it widens over the room it leaves. | `<pattern>-text-only` |
210
+ | `without` | The optional text named in `slot` is left out, and what sits under it moves up. | `<pattern>-no-<slot>` |
211
+
212
+ - `slot` is needed for `without` only, and must name an optional text slot (`"empty": "remove"`).
213
+ - `id` and `description` replace the defaults. The default description is the source's with a sentence added,
214
+ such as `Mirrored: here the picture is on the left and the text on the right. Alternate it with "photo-stat" for variety.`
215
+ Write your own when the source's description names a side ("a photo on the right"), so that the planner
216
+ reads one clear statement.
217
+ - The new pattern is placed right after its source. The source is not changed.
218
+ - A variant can be derived from a variant: the mirror image, then that without its subtitle.
219
+ - A variant the slide does not allow is refused with the reason, such as
220
+ `"cover" has no photo, graphic or chart of its own, so there is nothing to change sides with`.
221
+
222
+ How each variant is worked out, what it leaves alone and when it is refused is described under "Variants the
223
+ code works out" in [layout-freedom.md](layout-freedom.md). `patternVariants` (below) says in advance which
224
+ variants a pattern allows.
168
225
 
169
226
  #### `removePattern`
170
227
 
@@ -231,13 +288,18 @@ A property that does not apply to the slot's kind is refused (`"title" is a text
231
288
  #### `setRepeat`
232
289
 
233
290
  ```ts
234
- { op: 'setRepeat', pattern: string, min?: number, max?: number, item?: string, span?: { left: number, right: number } | null }
291
+ { op: 'setRepeat', pattern: string, min?: number, max?: number, item?: string, span?: { left: number, right: number } | null, widen?: number | null }
235
292
  ```
236
293
 
237
294
  Sets how many items a row or grid takes. When the pattern has no row yet, it is found from numbered slot names:
238
295
  name the slots of each item alike with a number (`card1Heading`, `card1Text`, `card2Heading`, `card2Text`) and
239
296
  they become a row.
240
297
 
298
+ `widen` lets the items grow wider when a line holds fewer of them than the template draws, so that two or
299
+ three cards fill a row drawn for four. It is the most an item may grow, as a factor on its drawn width,
300
+ between 1 and 4; `null` or `1` turns it off. See "Fewer items that fill the row" in
301
+ [layout-freedom.md](layout-freedom.md). Render `sampleDeck(template, { items: 'min' })` to look at the result.
302
+
241
303
  #### `removeRepeat`
242
304
 
243
305
  ```ts
@@ -256,6 +318,8 @@ The items stop being a row whose number can change.
256
318
  with the most that do: `5 cards do not fit side by side: each is 21% of the slide wide and the row has 93%. The most is 4`.
257
319
  - **Row items stay alike.** A `setSlot` on one item's slot (`card1Icon`) is made to that slot of every item.
258
320
  Slots of a row cannot be renamed, moved or removed one by one; remove the row first.
321
+ - **Variants are laid out.** `derivePattern` moves and resizes the shapes of a variant by itself, measures
322
+ the budgets of every text that changed size, and writes a description the planner can choose the variant by.
259
323
  - **Nothing is left broken.** After each change the manifest is checked against the pptx. A change that
260
324
  introduces a new error is refused.
261
325
 
@@ -280,22 +344,47 @@ A native chart in place of a sample chart picture, as a second pattern:
280
344
  ]
281
345
  ```
282
346
 
283
- A text-only slide from a slide with a photo:
347
+ The mirror image of a slide with a photo, for variety in a deck with several such slides:
348
+
349
+ ```ts
350
+ { op: 'derivePattern', pattern: 'photo-stat', variant: 'mirror', description: 'One key figure with a title and a short explanation on the left, and a photo on the right.' }
351
+ ```
352
+
353
+ A text-only slide from a slide with a photo, then larger text with room for three paragraphs (the budget
354
+ follows the new size):
284
355
 
285
356
  ```ts
286
357
  [
287
- { op: 'copyPattern', pattern: 'text-and-photo', id: 'text-only' },
288
- { op: 'addSlot', pattern: 'text-only', shape: '2', role: 'decoration' }, // the photo
289
- { op: 'setSlot', pattern: 'text-only', slot: 'body', box: { x: 0.04, y: 0.49, w: 0.72, h: 0.39 }, fontSize: 20, maxParagraphs: 3 },
358
+ { op: 'derivePattern', pattern: 'text-and-photo', variant: 'text-only', id: 'text-only' },
359
+ { op: 'setSlot', pattern: 'text-only', slot: 'body', fontSize: 20, maxParagraphs: 3, paragraphGap: 12 },
290
360
  ]
291
361
  ```
292
362
 
363
+ The same slide for decks that have no subtitle, with the body moved up into its place:
364
+
365
+ ```ts
366
+ { op: 'derivePattern', pattern: 'text-and-photo', variant: 'without', slot: 'subtitle' }
367
+ ```
368
+
369
+ Two or three cards that fill a row drawn for four, each holding more text:
370
+
371
+ ```ts
372
+ { op: 'setRepeat', pattern: 'icon-cards', widen: 2.2 }
373
+ ```
374
+
293
375
  Smaller table text and fewer columns (the cell budget is measured for you):
294
376
 
295
377
  ```ts
296
378
  { op: 'setSlot', pattern: 'table', slot: 'table', maxColumns: 5, bodyText: { size: 9 }, headerText: { size: 10, bold: true } }
297
379
  ```
298
380
 
381
+ Larger body text in every deck, for a template whose own body text is small (render the sample deck afterwards
382
+ to see that everything still fits):
383
+
384
+ ```ts
385
+ { op: 'setManifest', textScale: 1.3 }
386
+ ```
387
+
299
388
  Mirror alignment for right-to-left decks, but keep the line beside the logo where it is:
300
389
 
301
390
  ```ts
@@ -319,7 +408,10 @@ by hand. One model call, and a second when some operations were refused, so the
319
408
 
320
409
  - Instructions can be in any language.
321
410
  - A person may refer to a slide by number or to a pattern by id.
322
- - The model cannot set positions: `box` and `span` exist only for changes made by hand.
411
+ - The model cannot set positions: `box` and `span` exist only for changes made by hand. It can ask for a
412
+ variant by name ("add a mirrored version of the photo slides", "a version of slide 2 without the picture"),
413
+ which becomes a `derivePattern` operation: the model names the variant, and the code works out where
414
+ everything goes.
323
415
  - An operation the model got wrong shows up in `rejected` with the reason; nothing of it is applied.
324
416
 
325
417
  `describeManifest(template)` returns the text the model reads: every pattern with its slots and limits, and the
@@ -352,6 +444,23 @@ that are slots, and on a click offers the shape's `roles` (sending `addSlot`) or
352
444
 
353
445
  `describeSlot(slot)` gives a slot in a few words, such as `text, up to 40 characters, required`.
354
446
 
447
+ ## `patternVariants(template, pattern)`
448
+
449
+ ```ts
450
+ const options = patternVariants(template, 'text-and-photo');
451
+ // [
452
+ // { variant: 'mirror', available: true, note: 'the mirror image of "text-and-photo", with the picture on the left and what sat beside it on the right. ...' },
453
+ // { variant: 'text-only', available: true, note: '"text-and-photo" without its picture "photo". Widened over the freed area, with budgets to match: ...' },
454
+ // { variant: 'without', slot: 'subtitle', available: true, note: '"text-and-photo" without "subtitle": body moves up and has more room (431 characters).' },
455
+ // ]
456
+ ```
457
+
458
+ No model, and nothing is changed. It lists the variants `derivePattern` could make from a pattern: the mirror
459
+ image, the text-only version, and one `without` for each optional text slot. An entry with `available: false`
460
+ carries the reason in `note`; an available one says there what the variant would do. A studio screen can show
461
+ the available ones as buttons that send `{ op: 'derivePattern', pattern, variant, slot }`. It throws when the
462
+ template has no pattern of that id.
463
+
355
464
  ## `inspectTemplate(pkg)`
356
465
 
357
466
  ```ts