Skip to content

Commit 087bd33

Browse files
feat: ::cell primitive, grid spans, extended cols 6/12 and rows 4 (#4)
1 parent 594524a commit 087bd33

7 files changed

Lines changed: 144 additions & 8 deletions

File tree

src/blocks/BlockRenderer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export function BlockRenderer({ block }: { block: Block }) {
6464
const Comp = resolveBlockComponent('grid', styleId);
6565
return <Comp block={block} />;
6666
}
67+
case 'cell': {
68+
const Comp = resolveBlockComponent('cell', styleId);
69+
return <Comp block={block} />;
70+
}
6771
default: {
6872
const _exhaustive: never = block;
6973
void _exhaustive;

src/blocks/default/Cell.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Cell as CellBlock } from '@/ir/schema';
2+
3+
import { BlockRenderer } from '../BlockRenderer';
4+
5+
export function Cell({ block }: { block: CellBlock }) {
6+
const style: React.CSSProperties = {};
7+
if (block.span) style.gridColumn = `span ${block.span}`;
8+
if (block.rowSpan) style.gridRow = `span ${block.rowSpan}`;
9+
return (
10+
<div
11+
className="block block-cell"
12+
data-span={block.span ?? undefined}
13+
data-row-span={block.rowSpan ?? undefined}
14+
style={style}
15+
>
16+
{block.children.map((child, idx) => (
17+
<BlockRenderer key={idx} block={child} />
18+
))}
19+
</div>
20+
);
21+
}

src/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ComponentType } from 'react';
33
import type { Block } from '@/ir/schema';
44

55
import { Box } from './default/Box';
6+
import { Cell } from './default/Cell';
67
import { Chart } from './default/Chart';
78
import { Code } from './default/Code';
89
import { Columns } from './default/Columns';
@@ -70,6 +71,7 @@ const blockRegistry: Registry = {
7071
box: { default: Box },
7172
columns: { default: Columns },
7273
grid: { default: Grid },
74+
cell: { default: Cell },
7375
};
7476

7577
export function resolveBlockComponent<T extends BlockType>(

src/ir/parse.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
type Block,
77
type Box,
88
type Brand,
9+
type Cell,
910
type Chart,
1011
type ChartDatum,
1112
type ChartKind,
1213
type Code,
1314
type Deck,
15+
type Grid,
1416
type Heading,
1517
type LayoutId,
1618
type List,
@@ -308,15 +310,27 @@ function expandBlockDirective(
308310

309311
if (name === 'grid') {
310312
const cols = (
311-
options.cols && [2, 3, 4].includes(Number(options.cols)) ? Number(options.cols) : 2
312-
) as 2 | 3 | 4;
313+
options.cols && [2, 3, 4, 6, 12].includes(Number(options.cols)) ? Number(options.cols) : 2
314+
) as Grid['cols'];
313315
const rows = (
314-
options.rows && [1, 2, 3].includes(Number(options.rows)) ? Number(options.rows) : 2
315-
) as 1 | 2 | 3;
316+
options.rows && [1, 2, 3, 4].includes(Number(options.rows)) ? Number(options.rows) : 2
317+
) as Grid['rows'];
316318
const children = parseChildren(tokens, cursor, true);
317319
return [{ type: 'grid', cols, rows, children }];
318320
}
319321

322+
if (name === 'cell') {
323+
const children = parseChildren(tokens, cursor, true);
324+
const spanRaw = Number(options.span);
325+
const rowRaw = Number(options.row ?? options.rowSpan);
326+
const span = spanRaw && spanRaw >= 1 && spanRaw <= 12 ? (spanRaw as Cell['span']) : undefined;
327+
const rowSpan = rowRaw && rowRaw >= 1 && rowRaw <= 4 ? (rowRaw as Cell['rowSpan']) : undefined;
328+
const cell: Cell = { type: 'cell', children };
329+
if (span !== undefined) cell.span = span;
330+
if (rowSpan !== undefined) cell.rowSpan = rowSpan;
331+
return [cell];
332+
}
333+
320334
if (name === 'lead' || name === 'caption') {
321335
const children = parseChildren(tokens, cursor, true);
322336
return children.map((b) =>

src/ir/schema.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,15 @@ export type Columns = {
8686

8787
export type Grid = {
8888
type: 'grid';
89-
cols: 2 | 3 | 4;
90-
rows: 1 | 2 | 3;
89+
cols: 2 | 3 | 4 | 6 | 12;
90+
rows: 1 | 2 | 3 | 4;
91+
children: Block[];
92+
};
93+
94+
export type Cell = {
95+
type: 'cell';
96+
span?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
97+
rowSpan?: 1 | 2 | 3 | 4;
9198
children: Block[];
9299
};
93100

@@ -125,6 +132,7 @@ export type Block =
125132
| Box
126133
| Columns
127134
| Grid
135+
| Cell
128136
| Chart
129137
| Table;
130138

@@ -209,6 +217,7 @@ const BlockSchema: z.ZodType<Block> = z.lazy(() =>
209217
BoxSchema,
210218
ColumnsSchema,
211219
GridSchema,
220+
CellSchema,
212221
]),
213222
);
214223

@@ -231,12 +240,36 @@ const ColumnsSchema: z.ZodType<Columns> = z.lazy(() =>
231240
const GridSchema: z.ZodType<Grid> = z.lazy(() =>
232241
z.object({
233242
type: z.literal('grid'),
234-
cols: z.union([z.literal(2), z.literal(3), z.literal(4)]),
235-
rows: z.union([z.literal(1), z.literal(2), z.literal(3)]),
243+
cols: z.union([z.literal(2), z.literal(3), z.literal(4), z.literal(6), z.literal(12)]),
244+
rows: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
236245
children: z.array(BlockSchema).min(1),
237246
}),
238247
);
239248

249+
const CellSchema: z.ZodType<Cell> = z.lazy(() =>
250+
z.object({
251+
type: z.literal('cell'),
252+
span: z
253+
.union([
254+
z.literal(1),
255+
z.literal(2),
256+
z.literal(3),
257+
z.literal(4),
258+
z.literal(5),
259+
z.literal(6),
260+
z.literal(7),
261+
z.literal(8),
262+
z.literal(9),
263+
z.literal(10),
264+
z.literal(11),
265+
z.literal(12),
266+
])
267+
.optional(),
268+
rowSpan: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional(),
269+
children: z.array(BlockSchema),
270+
}),
271+
);
272+
240273
const LayoutIdSchema = z.enum(LAYOUT_IDS);
241274

242275
export type Slide = {

src/styles/blocks.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@
379379
grid-template-columns: repeat(4, minmax(0, 1fr));
380380
}
381381

382+
.block-grid[data-cols='6'] {
383+
grid-template-columns: repeat(6, minmax(0, 1fr));
384+
}
385+
386+
.block-grid[data-cols='12'] {
387+
grid-template-columns: repeat(12, minmax(0, 1fr));
388+
}
389+
382390
.block-grid[data-rows='1'] {
383391
grid-template-rows: minmax(0, 1fr);
384392
}
@@ -391,6 +399,20 @@
391399
grid-template-rows: repeat(3, minmax(0, 1fr));
392400
}
393401

402+
.block-grid[data-rows='4'] {
403+
grid-template-rows: repeat(4, minmax(0, 1fr));
404+
}
405+
406+
/* ─── Cell ─────────────────────────────────────────────────────────────── */
407+
408+
.block-cell {
409+
display: flex;
410+
flex-direction: column;
411+
gap: var(--space-md);
412+
min-width: 0;
413+
min-height: 0;
414+
}
415+
394416
/* ============================================================
395417
Brutalist-specific block treatments
396418
============================================================ */

tests/ir/parse.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,4 +446,44 @@ B
446446
};
447447
deck.slides.forEach((s) => s.blocks.forEach(walk));
448448
});
449+
450+
it('parses ::grid with extended cols=12 and rows=4', () => {
451+
const md = '::grid{cols=12 rows=4}\nHello\n::';
452+
const deck = parseDeck(md);
453+
const grid = deck.slides[0].blocks[0] as Grid;
454+
expect(grid.type).toBe('grid');
455+
expect(grid.cols).toBe(12);
456+
expect(grid.rows).toBe(4);
457+
});
458+
459+
it('parses ::cell with span inside a grid', () => {
460+
const md = `::grid{cols=12}
461+
::cell{span=8}
462+
Main content.
463+
::
464+
::cell{span=4}
465+
Side note.
466+
::
467+
::`;
468+
const deck = parseDeck(md);
469+
const grid = deck.slides[0].blocks[0] as Grid;
470+
expect(grid.children).toHaveLength(2);
471+
const c1 = grid.children[0] as import('@/ir/schema').Cell;
472+
expect(c1.type).toBe('cell');
473+
expect(c1.span).toBe(8);
474+
expect((grid.children[1] as import('@/ir/schema').Cell).span).toBe(4);
475+
});
476+
477+
it('parses ::cell rowSpan via row option', () => {
478+
const md = `::grid{cols=4 rows=2}
479+
::cell{span=2 row=2}
480+
Tall.
481+
::
482+
::`;
483+
const deck = parseDeck(md);
484+
const grid = deck.slides[0].blocks[0] as Grid;
485+
const cell = grid.children[0] as import('@/ir/schema').Cell;
486+
expect(cell.rowSpan).toBe(2);
487+
expect(cell.span).toBe(2);
488+
});
449489
});

0 commit comments

Comments
 (0)