@blockslides/ai-context 0.2.0 → 0.3.1

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 (49) hide show
  1. package/dist/index.cjs +985 -996
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +817 -300
  4. package/dist/index.d.ts +817 -300
  5. package/dist/index.js +985 -994
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/bundles/v1/all.ts +0 -1
  9. package/src/bundles/v1/allContexts.ts +1 -1
  10. package/src/bundles/v1/minimalCreate.ts +2 -2
  11. package/src/contexts/v1/blockquote.ts +10 -5
  12. package/src/contexts/v1/bulletList.ts +10 -5
  13. package/src/contexts/v1/codeBlock.ts +11 -3
  14. package/src/contexts/v1/column.ts +34 -14
  15. package/src/contexts/v1/columnGroup.ts +44 -0
  16. package/src/contexts/v1/core.ts +24 -4
  17. package/src/contexts/v1/editingRules.ts +5 -5
  18. package/src/contexts/v1/heading.ts +11 -4
  19. package/src/contexts/v1/horizontalRule.ts +9 -4
  20. package/src/contexts/v1/imageBlock.ts +31 -22
  21. package/src/contexts/v1/index.ts +1 -1
  22. package/src/contexts/v1/paragraph.ts +11 -5
  23. package/src/contexts/v1/slide.ts +5 -1
  24. package/src/contexts/v1/youtube.ts +14 -7
  25. package/src/index.ts +0 -3
  26. package/src/schemas/v1/blockquote.schema.json +13 -2
  27. package/src/schemas/v1/bulletList.schema.json +13 -2
  28. package/src/schemas/v1/codeBlock.schema.json +12 -3
  29. package/src/schemas/v1/column.schema.json +18 -14
  30. package/src/schemas/v1/columnGroup.schema.json +45 -0
  31. package/src/schemas/v1/heading.schema.json +12 -7
  32. package/src/schemas/v1/horizontalRule.schema.json +7 -2
  33. package/src/schemas/v1/imageBlock.schema.json +25 -15
  34. package/src/schemas/v1/index.ts +1 -1
  35. package/src/schemas/v1/paragraph.schema.json +13 -2
  36. package/src/schemas/v1/slide.schema.json +6 -0
  37. package/src/schemas/v1/youtube.schema.json +9 -6
  38. package/src/templates/v1/presetTemplateBuilder.ts +401 -443
  39. package/src/templates/v1/schemaBuilder.ts +195 -263
  40. package/src/types/v1.ts +40 -25
  41. package/src/contexts/v1/row.ts +0 -25
  42. package/src/examples/v1/flyers.ts +0 -30
  43. package/src/examples/v1/index.ts +0 -4
  44. package/src/examples/v1/slides.ts +0 -31
  45. package/src/recipes/v1/addTwoColumns.ts +0 -13
  46. package/src/recipes/v1/createSlide.ts +0 -29
  47. package/src/recipes/v1/editImageToCover.ts +0 -13
  48. package/src/recipes/v1/index.ts +0 -5
  49. package/src/schemas/v1/row.schema.json +0 -29
@@ -1,5 +1,4 @@
1
1
  // TODO: Add additional block helpers (tables, marks) to cover all extensions.
2
- // TODO: Add more slide presets (hero, imageCover, agenda) once shapes are finalized.
3
2
  // TODO: Add server-side validation helper that checks slides against schemasV1.
4
3
  // TODO: Add tests/examples that demonstrate agent/tool usage.
5
4
 
@@ -7,13 +6,13 @@ import type { JSONContent } from "@blockslides/core";
7
6
  import type {
8
7
  SizeKey,
9
8
  SlideAttrs,
10
- RowAttrs,
11
9
  ColumnAttrs,
12
10
  ImageBlockAttrs,
11
+ BaseBlockAttrs,
13
12
  } from "../../types/v1";
14
13
 
15
14
  type Block = JSONContent;
16
- type SlideNode = JSONContent; // slide -> row -> column -> blocks
15
+ type SlideNode = JSONContent; // slide -> block+
17
16
  type TextMark = { type: string; attrs?: Record<string, any> };
18
17
 
19
18
  const defaults = {
@@ -22,16 +21,8 @@ const defaults = {
22
21
  size: attrs?.size ?? ("16x9" as SizeKey),
23
22
  className: attrs?.className ?? "",
24
23
  }),
25
- row: (attrs?: Partial<RowAttrs>): RowAttrs => ({
26
- layout: attrs?.layout ?? "1",
27
- className: attrs?.className ?? "",
28
- }),
29
24
  column: (attrs?: Partial<ColumnAttrs>): ColumnAttrs => ({
30
- className: attrs?.className ?? "",
31
- contentMode: attrs?.contentMode ?? "default",
32
- verticalAlign: attrs?.verticalAlign,
33
- horizontalAlign: attrs?.horizontalAlign,
34
- padding: attrs?.padding,
25
+ ...attrs,
35
26
  }),
36
27
  };
37
28
 
@@ -116,87 +107,115 @@ export const blocks = {
116
107
  type: "youtube",
117
108
  attrs,
118
109
  }),
110
+
111
+ column: (content: Block[], attrs?: Partial<ColumnAttrs>) => ({
112
+ type: "column" as const,
113
+ attrs: attrs ?? {},
114
+ content,
115
+ }),
116
+
117
+ columnGroup: (columns: Block[], attrs?: { layout?: string; fill?: boolean; className?: string }) => ({
118
+ type: "columnGroup" as const,
119
+ attrs: attrs ?? {},
120
+ content: columns,
121
+ }),
119
122
  };
120
123
 
121
124
  type SingleColOpts = {
122
125
  slideAttrs?: Partial<SlideAttrs>;
123
- rowAttrs?: Partial<RowAttrs>;
124
126
  columnAttrs?: Partial<ColumnAttrs>;
125
127
  content?: Block[];
126
128
  };
127
129
 
130
+ type ColumnNode = {
131
+ type: "column";
132
+ attrs?: Partial<ColumnAttrs>;
133
+ content: Block[];
134
+ };
135
+
128
136
  type TwoColOpts = {
129
137
  slideAttrs?: Partial<SlideAttrs>;
130
- rowAttrs?: Partial<RowAttrs>;
131
- leftColumnAttrs?: Partial<ColumnAttrs>;
132
- rightColumnAttrs?: Partial<ColumnAttrs>;
133
- left?: Block[];
134
- right?: Block[];
138
+ columns: [ColumnNode, ColumnNode];
135
139
  };
136
140
 
141
+ /**
142
+ * Slide builder with presets for common layouts.
143
+ *
144
+ * New schema: slides contain blocks directly (doc → slide+ → block+).
145
+ * Adjacent columns automatically form horizontal layouts via CSS.
146
+ */
137
147
  export const slide = Object.assign(
148
+ /**
149
+ * Create a slide with direct block content (no column wrapper).
150
+ */
138
151
  (opts: {
139
152
  slideAttrs?: Partial<SlideAttrs>;
140
- rowAttrs?: Partial<RowAttrs>;
141
- columnAttrs?: Partial<ColumnAttrs>;
153
+ content?: Block[];
142
154
  } = {}): SlideNode => ({
143
155
  type: "slide",
144
156
  attrs: defaults.slide(opts.slideAttrs),
145
- content: [
146
- {
147
- type: "row",
148
- attrs: defaults.row(opts.rowAttrs),
149
- content: [
150
- {
151
- type: "column",
152
- attrs: defaults.column(opts.columnAttrs),
153
- content: [],
154
- },
155
- ],
156
- },
157
- ],
157
+ content: opts.content ?? [],
158
158
  }),
159
159
  {
160
+ /**
161
+ * Single column layout - wraps content in one column block.
162
+ */
160
163
  singleCol: (opts: SingleColOpts = {}): SlideNode => ({
161
164
  type: "slide",
162
165
  attrs: defaults.slide(opts.slideAttrs),
163
166
  content: [
164
167
  {
165
- type: "row",
166
- attrs: defaults.row(opts.rowAttrs),
167
- content: [
168
- {
169
- type: "column",
170
- attrs: defaults.column(opts.columnAttrs),
171
- content: opts.content ?? [],
172
- },
173
- ],
168
+ type: "column",
169
+ attrs: defaults.column(opts.columnAttrs),
170
+ content: opts.content ?? [],
174
171
  },
175
172
  ],
176
173
  }),
177
174
 
178
- twoCol: (opts: TwoColOpts = {}): SlideNode => ({
175
+ /**
176
+ * Two column layout - columns grouped side-by-side via columnGroup.
177
+ * Pass two column objects created with blocks.column().
178
+ */
179
+ twoCol: (col1: ColumnNode, col2: ColumnNode, slideAttrs?: Partial<SlideAttrs>): SlideNode => ({
179
180
  type: "slide",
180
- attrs: defaults.slide(opts.slideAttrs),
181
+ attrs: defaults.slide(slideAttrs),
181
182
  content: [
182
183
  {
183
- type: "row",
184
- attrs: defaults.row({
185
- layout: opts.rowAttrs?.layout ?? "1-1",
186
- ...opts.rowAttrs,
187
- }),
188
- content: [
189
- {
190
- type: "column",
191
- attrs: defaults.column(opts.leftColumnAttrs),
192
- content: opts.left ?? [],
193
- },
194
- {
195
- type: "column",
196
- attrs: defaults.column(opts.rightColumnAttrs),
197
- content: opts.right ?? [],
198
- },
199
- ],
184
+ type: "columnGroup",
185
+ attrs: { fill: true },
186
+ content: [col1, col2],
187
+ },
188
+ ],
189
+ }),
190
+
191
+ /**
192
+ * Three column layout - columns grouped side-by-side via columnGroup.
193
+ * Pass three column objects created with blocks.column().
194
+ */
195
+ threeCol: (col1: ColumnNode, col2: ColumnNode, col3: ColumnNode, slideAttrs?: Partial<SlideAttrs>): SlideNode => ({
196
+ type: "slide",
197
+ attrs: defaults.slide(slideAttrs),
198
+ content: [
199
+ {
200
+ type: "columnGroup",
201
+ attrs: { fill: true },
202
+ content: [col1, col2, col3],
203
+ },
204
+ ],
205
+ }),
206
+
207
+ /**
208
+ * Four column layout - columns grouped side-by-side via columnGroup.
209
+ * Pass four column objects created with blocks.column().
210
+ */
211
+ fourCol: (col1: ColumnNode, col2: ColumnNode, col3: ColumnNode, col4: ColumnNode, slideAttrs?: Partial<SlideAttrs>): SlideNode => ({
212
+ type: "slide",
213
+ attrs: defaults.slide(slideAttrs),
214
+ content: [
215
+ {
216
+ type: "columnGroup",
217
+ attrs: { fill: true },
218
+ content: [col1, col2, col3, col4],
200
219
  },
201
220
  ],
202
221
  }),
@@ -206,7 +225,6 @@ export const slide = Object.assign(
206
225
  // Slide layout presets beyond the base helpers
207
226
  type HeroOpts = {
208
227
  slideAttrs?: Partial<SlideAttrs>;
209
- rowAttrs?: Partial<RowAttrs>;
210
228
  columnAttrs?: Partial<ColumnAttrs>;
211
229
  content?: Block[];
212
230
  };
@@ -216,26 +234,22 @@ type ImageCoverOpts = {
216
234
  image?: ImageBlockAttrs;
217
235
  overlay?: Block[];
218
236
  columnAttrs?: Partial<ColumnAttrs>;
219
- rowAttrs?: Partial<RowAttrs>;
220
237
  };
221
238
 
222
239
  type QuoteOpts = {
223
240
  slideAttrs?: Partial<SlideAttrs>;
224
- rowAttrs?: Partial<RowAttrs>;
225
241
  columnAttrs?: Partial<ColumnAttrs>;
226
242
  quote?: Block[];
227
243
  };
228
244
 
229
245
  type AgendaOpts = {
230
246
  slideAttrs?: Partial<SlideAttrs>;
231
- rowAttrs?: Partial<RowAttrs>;
232
247
  columnAttrs?: Partial<ColumnAttrs>;
233
248
  items?: (string | Block)[];
234
249
  };
235
250
 
236
251
  type MultiColOpts = {
237
252
  slideAttrs?: Partial<SlideAttrs>;
238
- rowAttrs?: Partial<RowAttrs>;
239
253
  columns: {
240
254
  content?: Block[];
241
255
  attrs?: Partial<ColumnAttrs>;
@@ -244,7 +258,6 @@ type MultiColOpts = {
244
258
 
245
259
  type MediaTextOpts = {
246
260
  slideAttrs?: Partial<SlideAttrs>;
247
- rowAttrs?: Partial<RowAttrs>;
248
261
  media?: Block[];
249
262
  text?: Block[];
250
263
  mediaColumnAttrs?: Partial<ColumnAttrs>;
@@ -253,16 +266,17 @@ type MediaTextOpts = {
253
266
 
254
267
  type Stack2Opts = {
255
268
  slideAttrs?: Partial<SlideAttrs>;
256
- topRow?: {
257
- rowAttrs?: Partial<RowAttrs>;
258
- columns: { content?: Block[]; attrs?: Partial<ColumnAttrs> }[];
259
- };
260
- bottomRow?: {
261
- rowAttrs?: Partial<RowAttrs>;
262
- columns: { content?: Block[]; attrs?: Partial<ColumnAttrs> }[];
263
- };
269
+ topColumns?: { content?: Block[]; attrs?: Partial<ColumnAttrs> }[];
270
+ bottomColumns?: { content?: Block[]; attrs?: Partial<ColumnAttrs> }[];
264
271
  };
265
272
 
273
+ /** Helper to create a column block with attrs and content */
274
+ const column = (attrs: Partial<ColumnAttrs> | undefined, content: Block[] = []): Block => ({
275
+ type: "column",
276
+ attrs: defaults.column(attrs),
277
+ content,
278
+ });
279
+
266
280
  type TemplatePreset =
267
281
  | "slide.empty"
268
282
  | "slide.singleCol"
@@ -283,7 +297,6 @@ type TemplatePreset =
283
297
  type CreateTemplateInput = {
284
298
  preset: TemplatePreset;
285
299
  slideAttrs?: Partial<SlideAttrs>;
286
- rowAttrs?: Partial<RowAttrs>;
287
300
  columnAttrs?: Partial<ColumnAttrs>;
288
301
  leftColumnAttrs?: Partial<ColumnAttrs>;
289
302
  rightColumnAttrs?: Partial<ColumnAttrs>;
@@ -299,61 +312,38 @@ type CreateTemplateInput = {
299
312
  stack2Opts?: Stack2Opts;
300
313
  };
301
314
 
302
- const column = (attrs: Partial<ColumnAttrs> | undefined, content: Block[] = []): Block => ({
303
- type: "column",
304
- attrs: defaults.column(attrs),
305
- content,
306
- });
307
-
308
- const row = (layout: RowAttrs["layout"], content: Block[], attrs?: Partial<RowAttrs>): Block => ({
309
- type: "row",
310
- attrs: defaults.row({ layout, ...attrs }),
311
- content,
312
- });
313
-
314
315
  export const createTemplate = (input: CreateTemplateInput): SlideNode => {
315
316
  switch (input.preset) {
316
317
  case "slide.empty":
317
318
  return slide({
318
319
  slideAttrs: input.slideAttrs,
319
- rowAttrs: input.rowAttrs,
320
- columnAttrs: input.columnAttrs,
320
+ content: [],
321
321
  });
322
322
  case "slide.singleCol":
323
323
  return slide.singleCol({
324
324
  slideAttrs: input.slideAttrs,
325
- rowAttrs: input.rowAttrs,
326
325
  columnAttrs: input.columnAttrs,
327
326
  content: input.content ?? [],
328
327
  });
329
328
  case "slide.twoCol":
330
- return slide.twoCol({
331
- slideAttrs: input.slideAttrs,
332
- rowAttrs: input.rowAttrs,
333
- leftColumnAttrs: input.leftColumnAttrs,
334
- rightColumnAttrs: input.rightColumnAttrs,
335
- left: input.left ?? [],
336
- right: input.right ?? [],
337
- });
329
+ return slide.twoCol(
330
+ blocks.column(input.left ?? [], input.leftColumnAttrs),
331
+ blocks.column(input.right ?? [], input.rightColumnAttrs),
332
+ input.slideAttrs
333
+ );
338
334
  case "slide.hero": {
339
335
  const opts = input.heroOpts ?? {};
340
336
  return {
341
337
  type: "slide",
342
- attrs: defaults.slide({ className: "bg-slate-950 text-white", ...opts.slideAttrs }),
338
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#020617" }),
343
339
  content: [
344
- row(
345
- opts.rowAttrs?.layout ?? "1",
346
- [
347
- column(
348
- opts.columnAttrs ?? { className: "max-w-4xl gap-6 p-12" },
349
- opts.content ?? [
350
- blocks.heading("Your headline", 1),
351
- blocks.paragraph("Subhead goes here."),
352
- blocks.paragraph("Add supporting details here."),
353
- ]
354
- ),
355
- ],
356
- { className: "min-h-[720px] items-center justify-center p-12", ...opts.rowAttrs }
340
+ column(
341
+ { justify: "center", align: "center", padding: "lg", fill: true, ...opts.columnAttrs },
342
+ opts.content ?? [
343
+ blocks.heading("Your headline", 1),
344
+ blocks.paragraph("Subhead goes here."),
345
+ blocks.paragraph("Add supporting details here."),
346
+ ]
357
347
  ),
358
348
  ],
359
349
  };
@@ -362,34 +352,19 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
362
352
  const opts = input.imageCoverOpts ?? {};
363
353
  const image = opts.image ?? {
364
354
  src: "https://placehold.co/1600x900/png",
365
- layout: "cover",
366
- fullBleed: true,
367
- align: "center",
355
+ size: "fill",
368
356
  };
369
357
  const overlay = opts.overlay ?? [blocks.heading("Overlay title", 1)];
370
358
  return {
371
359
  type: "slide",
372
- attrs: defaults.slide({ className: "bg-black text-white", ...opts.slideAttrs }),
360
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#000000" }),
373
361
  content: [
374
- row(
375
- opts.rowAttrs?.layout ?? "1",
362
+ column(
363
+ { fill: true, padding: "none", ...opts.columnAttrs },
376
364
  [
377
- {
378
- type: "column",
379
- attrs: defaults.column({ className: "relative w-full h-full", ...opts.columnAttrs }),
380
- content: [
381
- blocks.imageBlock(image),
382
- ...overlay.map((node) => ({
383
- ...node,
384
- attrs: {
385
- ...(node as any).attrs,
386
- className: `${((node as any).attrs?.className ?? "")} absolute bottom-12 left-12 drop-shadow-lg`.trim(),
387
- },
388
- })),
389
- ],
390
- },
391
- ],
392
- { className: "min-h-[720px]", ...opts.rowAttrs }
365
+ blocks.imageBlock(image),
366
+ ...overlay,
367
+ ]
393
368
  ),
394
369
  ],
395
370
  };
@@ -398,24 +373,18 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
398
373
  const opts = input.quoteOpts ?? {};
399
374
  return {
400
375
  type: "slide",
401
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
376
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
402
377
  content: [
403
- row(
404
- opts.rowAttrs?.layout ?? "1",
378
+ column(
379
+ { justify: "center", align: "center", padding: "lg", gap: "md", fill: true, ...opts.columnAttrs },
405
380
  [
406
- column(
407
- opts.columnAttrs ?? { className: "max-w-3xl mx-auto gap-4 p-12" },
408
- [
409
- blocks.blockquote(
410
- opts.quote ?? [
411
- blocks.paragraph("“Add your quote here.”"),
412
- blocks.paragraph("— Author"),
413
- ]
414
- ),
381
+ blocks.blockquote(
382
+ opts.quote ?? [
383
+ blocks.paragraph("Add your quote here."),
384
+ blocks.paragraph("— Author"),
415
385
  ]
416
386
  ),
417
- ],
418
- { className: "min-h-[640px] items-center justify-center", ...opts.rowAttrs }
387
+ ]
419
388
  ),
420
389
  ],
421
390
  };
@@ -424,20 +393,14 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
424
393
  const opts = input.agendaOpts ?? {};
425
394
  return {
426
395
  type: "slide",
427
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
396
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
428
397
  content: [
429
- row(
430
- opts.rowAttrs?.layout ?? "1",
398
+ column(
399
+ { padding: "lg", gap: "lg", ...opts.columnAttrs },
431
400
  [
432
- column(
433
- opts.columnAttrs ?? { className: "p-12 gap-6" },
434
- [
435
- blocks.heading("Agenda", 1),
436
- blocks.bulletList(opts.items ?? ["Topic 1", "Topic 2", "Topic 3"]),
437
- ]
438
- ),
439
- ],
440
- { className: "min-h-[640px]", ...opts.rowAttrs }
401
+ blocks.heading("Agenda", 1),
402
+ blocks.bulletList(opts.items ?? ["Topic 1", "Topic 2", "Topic 3"]),
403
+ ]
441
404
  ),
442
405
  ],
443
406
  };
@@ -454,14 +417,8 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
454
417
  ];
455
418
  return {
456
419
  type: "slide",
457
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
458
- content: [
459
- row(
460
- opts.rowAttrs?.layout ?? "1-1-1",
461
- cols.map((c) => column(c.attrs, c.content ?? [])),
462
- { className: "p-8 gap-4", ...opts.rowAttrs }
463
- ),
464
- ],
420
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
421
+ content: cols.map((c) => column({ padding: "md", gap: "sm", fill: true, ...c.attrs }, c.content ?? [])),
465
422
  };
466
423
  }
467
424
  case "slide.grid4": {
@@ -477,30 +434,18 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
477
434
  ];
478
435
  return {
479
436
  type: "slide",
480
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
481
- content: [
482
- row(
483
- opts.rowAttrs?.layout ?? "1-1-1-1",
484
- cols.map((c) => column(c.attrs, c.content ?? [])),
485
- { className: "p-8 gap-4", ...opts.rowAttrs }
486
- ),
487
- ],
437
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
438
+ content: cols.map((c) => column({ padding: "md", gap: "sm", fill: true, ...c.attrs }, c.content ?? [])),
488
439
  };
489
440
  }
490
441
  case "slide.oneTwo": {
491
442
  const opts = input.mediaTextOpts ?? {};
492
443
  return {
493
444
  type: "slide",
494
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
445
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
495
446
  content: [
496
- row(
497
- opts.rowAttrs?.layout ?? "1-2",
498
- [
499
- column(opts.textColumnAttrs ?? { className: "p-8 gap-4" }, opts.text ?? []),
500
- column(opts.mediaColumnAttrs ?? { className: "p-8 gap-4" }, opts.media ?? []),
501
- ],
502
- { className: "min-h-[640px]", ...opts.rowAttrs }
503
- ),
447
+ column({ padding: "md", gap: "sm", width: "33%", ...opts.textColumnAttrs }, opts.text ?? []),
448
+ column({ padding: "md", gap: "sm", fill: true, ...opts.mediaColumnAttrs }, opts.media ?? []),
504
449
  ],
505
450
  };
506
451
  }
@@ -508,16 +453,10 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
508
453
  const opts = input.mediaTextOpts ?? {};
509
454
  return {
510
455
  type: "slide",
511
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
456
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
512
457
  content: [
513
- row(
514
- opts.rowAttrs?.layout ?? "2-1",
515
- [
516
- column(opts.mediaColumnAttrs ?? { className: "p-8 gap-4" }, opts.media ?? []),
517
- column(opts.textColumnAttrs ?? { className: "p-8 gap-4" }, opts.text ?? []),
518
- ],
519
- { className: "min-h-[640px]", ...opts.rowAttrs }
520
- ),
458
+ column({ padding: "md", gap: "sm", fill: true, ...opts.mediaColumnAttrs }, opts.media ?? []),
459
+ column({ padding: "md", gap: "sm", width: "33%", ...opts.textColumnAttrs }, opts.text ?? []),
521
460
  ],
522
461
  };
523
462
  }
@@ -533,30 +472,18 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
533
472
  ];
534
473
  return {
535
474
  type: "slide",
536
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
537
- content: [
538
- row(
539
- opts.rowAttrs?.layout ?? "1-2-1",
540
- cols.map((c) => column(c.attrs, c.content ?? [])),
541
- { className: "p-8 gap-4", ...opts.rowAttrs }
542
- ),
543
- ],
475
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
476
+ content: cols.map((c, i) => column({ padding: "md", gap: "sm", width: i === 1 ? "50%" : "25%", ...c.attrs }, c.content ?? [])),
544
477
  };
545
478
  }
546
479
  case "slide.textMedia": {
547
480
  const opts = input.mediaTextOpts ?? {};
548
481
  return {
549
482
  type: "slide",
550
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
483
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
551
484
  content: [
552
- row(
553
- opts.rowAttrs?.layout ?? "1-1",
554
- [
555
- column(opts.textColumnAttrs ?? { className: "p-10 gap-4" }, opts.text ?? []),
556
- column(opts.mediaColumnAttrs ?? { className: "p-10 gap-4 bg-slate-50" }, opts.media ?? []),
557
- ],
558
- { className: "min-h-[640px]", ...opts.rowAttrs }
559
- ),
485
+ column({ padding: "lg", gap: "sm", fill: true, ...opts.textColumnAttrs }, opts.text ?? []),
486
+ column({ padding: "lg", gap: "sm", fill: true, backgroundColor: "#f8fafc", ...opts.mediaColumnAttrs }, opts.media ?? []),
560
487
  ],
561
488
  };
562
489
  }
@@ -564,46 +491,26 @@ export const createTemplate = (input: CreateTemplateInput): SlideNode => {
564
491
  const opts = input.mediaTextOpts ?? {};
565
492
  return {
566
493
  type: "slide",
567
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
494
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
568
495
  content: [
569
- row(
570
- opts.rowAttrs?.layout ?? "1-1",
571
- [
572
- column(opts.mediaColumnAttrs ?? { className: "p-10 gap-4 bg-slate-50" }, opts.media ?? []),
573
- column(opts.textColumnAttrs ?? { className: "p-10 gap-4" }, opts.text ?? []),
574
- ],
575
- { className: "min-h-[640px]", ...opts.rowAttrs }
576
- ),
496
+ column({ padding: "lg", gap: "sm", fill: true, backgroundColor: "#f8fafc", ...opts.mediaColumnAttrs }, opts.media ?? []),
497
+ column({ padding: "lg", gap: "sm", fill: true, ...opts.textColumnAttrs }, opts.text ?? []),
577
498
  ],
578
499
  };
579
500
  }
580
501
  case "slide.stack2": {
581
502
  const opts = input.stack2Opts ?? {};
582
- const top = opts.topRow ?? {
583
- rowAttrs: { layout: "1" },
584
- columns: [{ content: [blocks.heading("Title", 1), blocks.paragraph("Subhead")] }],
585
- };
586
- const bottom = opts.bottomRow ?? {
587
- rowAttrs: { layout: "1-1" },
588
- columns: [
589
- { content: [blocks.paragraph("Left detail")] },
590
- { content: [blocks.paragraph("Right detail")] },
591
- ],
592
- };
503
+ const top = opts.topColumns ?? [{ content: [blocks.heading("Title", 1), blocks.paragraph("Subhead")] }];
504
+ const bottom = opts.bottomColumns ?? [
505
+ { content: [blocks.paragraph("Left detail")] },
506
+ { content: [blocks.paragraph("Right detail")] },
507
+ ];
593
508
  return {
594
509
  type: "slide",
595
- attrs: defaults.slide({ className: "bg-white text-slate-900", ...opts.slideAttrs }),
510
+ attrs: defaults.slide({ ...opts.slideAttrs, backgroundColor: opts.slideAttrs?.backgroundColor ?? "#ffffff" }),
596
511
  content: [
597
- row(
598
- top.rowAttrs?.layout ?? "1",
599
- top.columns.map((c) => column(c.attrs, c.content ?? [])),
600
- { className: "p-8 gap-4", ...top.rowAttrs }
601
- ),
602
- row(
603
- bottom.rowAttrs?.layout ?? "1-1",
604
- bottom.columns.map((c) => column(c.attrs, c.content ?? [])),
605
- { className: "p-8 gap-4", ...bottom.rowAttrs }
606
- ),
512
+ ...top.map((c) => column({ padding: "md", gap: "sm", ...c.attrs }, c.content ?? [])),
513
+ ...bottom.map((c) => column({ padding: "md", gap: "sm", ...c.attrs }, c.content ?? [])),
607
514
  ],
608
515
  };
609
516
  }
@@ -633,40 +540,65 @@ export const listTemplates = (): TemplatePreset[] => [
633
540
  export const templatesV1Context = `
634
541
  BlockSlides templates API (v1)
635
542
 
543
+ Document Hierarchy:
544
+ doc → slide+ → block+
545
+ Slides contain blocks directly - no mandatory row wrapper.
546
+ Adjacent columns automatically form horizontal layouts via CSS.
547
+
636
548
  Presets:
637
- - slide.empty(): slide with one empty column.
638
- - slide.singleCol({ content?, slideAttrs?, rowAttrs?, columnAttrs? })
639
- - slide.twoCol({ left?, right?, slideAttrs?, rowAttrs?, leftColumnAttrs?, rightColumnAttrs? })
640
- - slide.hero({ content?, slideAttrs?, rowAttrs?, columnAttrs? })
641
- - slide.imageCover({ image?, overlay?, slideAttrs?, rowAttrs?, columnAttrs? })
642
- - slide.quote({ quote?, slideAttrs?, rowAttrs?, columnAttrs? })
643
- - slide.agenda({ items?, slideAttrs?, rowAttrs?, columnAttrs? })
644
- - slide.grid3/grid4({ columns?, slideAttrs?, rowAttrs? })
645
- - slide.oneTwo/twoOne/oneTwoOne({ columns?, slideAttrs?, rowAttrs? })
646
- - slide.textMedia/mediaText({ text?, media?, slideAttrs?, rowAttrs? })
647
- - slide.stack2({ topRow?, bottomRow?, slideAttrs? })
648
-
649
- Blocks:
549
+ - slide({ content?, slideAttrs? }): slide with direct block content
550
+ - slide.singleCol({ content?, slideAttrs?, columnAttrs? }): single column layout
551
+ - slide.twoCol(column1, column2, slideAttrs?): two columns side by side
552
+ - slide.threeCol(col1, col2, col3, slideAttrs?): three columns side by side
553
+ - slide.fourCol(col1, col2, col3, col4, slideAttrs?): four columns side by side
554
+ - slide.hero({ heroOpts }): centered content on dark background
555
+ - slide.imageCover({ imageCoverOpts }): full-bleed image with overlay
556
+ - slide.quote({ quoteOpts }): centered blockquote
557
+ - slide.agenda({ agendaOpts }): title with bullet list
558
+ - slide.grid3/grid4({ multiColOpts }): equal-width column grids
559
+ - slide.oneTwo/twoOne({ mediaTextOpts }): asymmetric layouts
560
+ - slide.textMedia/mediaText({ mediaTextOpts }): text and media columns
561
+ - slide.stack2({ stack2Opts }): stacked column sections
562
+
563
+ Base Block Attributes (available on ALL blocks):
564
+ - align: "left" | "center" | "right" | "stretch" - horizontal alignment
565
+ - justify: "start" | "center" | "end" | "space-between" - vertical distribution
566
+ - padding: "none" | "sm" | "md" | "lg" - internal spacing (8px/16px/32px)
567
+ - margin: "none" | "sm" | "md" | "lg" - external spacing
568
+ - gap: "none" | "sm" | "md" | "lg" - space between children
569
+ - backgroundColor: CSS color
570
+ - backgroundImage: URL
571
+ - borderRadius: "none" | "sm" | "md" | "lg" (4px/8px/16px)
572
+ - border: CSS border
573
+ - fill: boolean - fill available space
574
+ - width: CSS width
575
+ - height: CSS height
576
+
577
+ Block Helpers:
650
578
  - blocks.text(text, marks?)
651
- - blocks.heading(text, level?)
579
+ - blocks.heading(text, level?) - level 1-6
652
580
  - blocks.paragraph(text?)
653
581
  - blocks.bulletList([string | Block][])
654
582
  - blocks.codeBlock(code, language?)
655
583
  - blocks.horizontalRule()
656
584
  - blocks.hardBreak()
657
- - blocks.imageBlock({ src, alt?, layout?, fullBleed?, align?, ...ImageBlockAttrs })
585
+ - blocks.imageBlock({ src, size?, crop?, alt?, caption?, credit? })
586
+ - size: "fill" | "fit" | "natural" - how image fills container
587
+ - crop: "center" | "top" | "bottom" | "left" | "right" | corner positions
658
588
  - blocks.blockquote(content?)
659
589
  - blocks.listItem(content?)
660
- - blocks.image({ src, alt?, title?, width?, height? })
661
590
  - blocks.youtube({ src?, start?, width?, height? })
662
591
 
663
592
  Agent/tool usage:
664
- - Call createTemplate({ preset: "slide.twoCol", left: [blocks.paragraph("Left")], right: [blocks.imageBlock({ src })] })
665
- - Wrap returned slides in { type: "doc", content: [/* slides here */] } before sending to the editor.
593
+ - Use blocks.column(content, attrs) to create columns
594
+ - Use blocks.columnGroup(columns) to group columns horizontally
595
+ - Call createTemplate({ preset: "slide.twoCol", left: [...], right: [...] })
596
+ - Wrap returned slides in { type: "doc", content: [/* slides here */] }
666
597
 
667
598
  Notes:
668
- - Size defaults to 16x9; override via slideAttrs.size.
669
- - Layout defaults: singleCol uses "1"; twoCol uses "1-1" unless rowAttrs.layout overrides.
599
+ - Size defaults to 16x9; override via slideAttrs.size
600
+ - Use fill: true on columns to distribute space evenly
601
+ - Use semantic spacing tokens (sm/md/lg) instead of raw pixel values
670
602
  `.trim();
671
603
 
672
604
  export type { Block, SlideNode, TemplatePreset, CreateTemplateInput };