@cfbender/cesium 0.4.0 → 0.5.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 (45) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +2 -5
  3. package/package.json +3 -2
  4. package/src/cli/commands/serve.ts +18 -2
  5. package/src/index.ts +4 -1
  6. package/src/prompt/field-reference.ts +94 -0
  7. package/src/prompt/system-fragment.md +56 -65
  8. package/src/render/blocks/catalog.ts +39 -0
  9. package/src/render/blocks/escape.ts +27 -0
  10. package/src/render/blocks/highlight.ts +188 -0
  11. package/src/render/blocks/index.ts +6 -0
  12. package/src/render/blocks/markdown.ts +217 -0
  13. package/src/render/blocks/render.ts +104 -0
  14. package/src/render/blocks/renderers/callout.ts +38 -0
  15. package/src/render/blocks/renderers/code.ts +46 -0
  16. package/src/render/blocks/renderers/compare-table.ts +56 -0
  17. package/src/render/blocks/renderers/diagram.ts +48 -0
  18. package/src/render/blocks/renderers/divider.ts +31 -0
  19. package/src/render/blocks/renderers/hero.ts +66 -0
  20. package/src/render/blocks/renderers/kv.ts +45 -0
  21. package/src/render/blocks/renderers/list.ts +51 -0
  22. package/src/render/blocks/renderers/pill-row.ts +45 -0
  23. package/src/render/blocks/renderers/prose.ts +29 -0
  24. package/src/render/blocks/renderers/raw-html.ts +32 -0
  25. package/src/render/blocks/renderers/risk-table.ts +76 -0
  26. package/src/render/blocks/renderers/section.ts +97 -0
  27. package/src/render/blocks/renderers/timeline.ts +58 -0
  28. package/src/render/blocks/renderers/tldr.ts +30 -0
  29. package/src/render/blocks/themes/claret-dark.ts +206 -0
  30. package/src/render/blocks/themes/claret-light.ts +227 -0
  31. package/src/render/blocks/types.ts +127 -0
  32. package/src/render/blocks/validate-block.ts +202 -0
  33. package/src/render/critique.ts +410 -10
  34. package/src/render/fallback.ts +18 -0
  35. package/src/render/theme.ts +154 -0
  36. package/src/render/validate.ts +282 -17
  37. package/src/render/wrap.ts +7 -7
  38. package/src/server/lifecycle.ts +190 -3
  39. package/src/storage/assets.ts +66 -0
  40. package/src/storage/index-cache.ts +1 -0
  41. package/src/storage/index-gen.ts +13 -14
  42. package/src/tools/ask.ts +7 -5
  43. package/src/tools/critique.ts +41 -6
  44. package/src/tools/publish.ts +43 -14
  45. package/src/tools/styleguide.ts +118 -9
@@ -0,0 +1,48 @@
1
+ // Diagram block renderer — escape-hatch for inline SVG or HTML diagrams.
2
+ // src/render/blocks/renderers/diagram.ts
3
+
4
+ import type { DiagramBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+ import { scrub } from "../../scrub.ts";
9
+
10
+ export function renderDiagram(block: DiagramBlock, _ctx: RenderCtx): string {
11
+ const payload = block.svg ?? block.html ?? "";
12
+ const scrubResult = scrub(payload);
13
+ const scrubbed = scrubResult.html;
14
+
15
+ const parts: string[] = [];
16
+ parts.push(scrubbed);
17
+
18
+ if (block.caption !== undefined && block.caption !== "") {
19
+ parts.push(`<figcaption>${escapeHtml(block.caption)}</figcaption>`);
20
+ }
21
+
22
+ return `<figure class="diagram">\n${parts.join("\n")}\n</figure>`;
23
+ }
24
+
25
+ export const meta: BlockMeta = {
26
+ type: "diagram",
27
+ description:
28
+ "Escape-hatch for inline SVG or bespoke HTML diagrams. Exactly one of svg or html required. Payload is scrubbed. For SVG, prefer fill=\"currentColor\" and stroke=\"currentColor\" so the diagram inherits the theme's text color. Use explicit colors only for emphasis (accents, warnings).",
29
+ schema: {
30
+ type: "object",
31
+ properties: {
32
+ type: { const: "diagram" },
33
+ caption: { type: "string" },
34
+ svg: { type: "string" },
35
+ html: { type: "string" },
36
+ },
37
+ required: ["type"],
38
+ oneOf: [
39
+ { required: ["svg"] },
40
+ { required: ["html"] },
41
+ ],
42
+ },
43
+ example: {
44
+ type: "diagram",
45
+ caption: "System architecture overview",
46
+ svg: '<svg viewBox="0 0 100 50" xmlns="http://www.w3.org/2000/svg"><rect x="10" y="10" width="80" height="30" rx="4" fill="none" stroke="#888"/><text x="50" y="30" text-anchor="middle" font-size="12">cesium</text></svg>',
47
+ },
48
+ };
@@ -0,0 +1,31 @@
1
+ // Divider block renderer.
2
+ // src/render/blocks/renderers/divider.ts
3
+
4
+ import type { DividerBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeAttr } from "../escape.ts";
8
+
9
+ export function renderDivider(block: DividerBlock, _ctx: RenderCtx): string {
10
+ if (block.label !== undefined && block.label !== "") {
11
+ return `<hr data-label="${escapeAttr(block.label)}">`;
12
+ }
13
+ return `<hr>`;
14
+ }
15
+
16
+ export const meta: BlockMeta = {
17
+ type: "divider",
18
+ description: "Horizontal rule separator, with an optional text label.",
19
+ schema: {
20
+ type: "object",
21
+ properties: {
22
+ type: { const: "divider" },
23
+ label: { type: "string" },
24
+ },
25
+ required: ["type"],
26
+ },
27
+ example: {
28
+ type: "divider",
29
+ label: "End of Phase 1",
30
+ },
31
+ };
@@ -0,0 +1,66 @@
1
+ // Hero block renderer.
2
+ // src/render/blocks/renderers/hero.ts
3
+
4
+ import type { HeroBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+
9
+ export function renderHero(block: HeroBlock, _ctx: RenderCtx): string {
10
+ const parts: string[] = [];
11
+
12
+ if (block.eyebrow !== undefined && block.eyebrow !== "") {
13
+ parts.push(` <div class="eyebrow">${escapeHtml(block.eyebrow)}</div>`);
14
+ }
15
+
16
+ parts.push(` <h1 class="h-display">${escapeHtml(block.title)}</h1>`);
17
+
18
+ if (block.subtitle !== undefined && block.subtitle !== "") {
19
+ parts.push(` <p class="lede">${escapeHtml(block.subtitle)}</p>`);
20
+ }
21
+
22
+ if (block.meta !== undefined && block.meta.length > 0) {
23
+ const rows = block.meta
24
+ .map((row) => ` <dt>${escapeHtml(row.k)}</dt><dd>${escapeHtml(row.v)}</dd>`)
25
+ .join("\n");
26
+ parts.push(` <dl class="kv">\n${rows}\n </dl>`);
27
+ }
28
+
29
+ return `<header>\n${parts.join("\n")}\n</header>`;
30
+ }
31
+
32
+ export const meta: BlockMeta = {
33
+ type: "hero",
34
+ description: "Page title header with optional eyebrow, subtitle, and key-value metadata pairs.",
35
+ schema: {
36
+ type: "object",
37
+ properties: {
38
+ type: { const: "hero" },
39
+ eyebrow: { type: "string" },
40
+ title: { type: "string" },
41
+ subtitle: { type: "string" },
42
+ meta: {
43
+ type: "array",
44
+ items: {
45
+ type: "object",
46
+ properties: {
47
+ k: { type: "string" },
48
+ v: { type: "string" },
49
+ },
50
+ required: ["k", "v"],
51
+ },
52
+ },
53
+ },
54
+ required: ["type", "title"],
55
+ },
56
+ example: {
57
+ type: "hero",
58
+ eyebrow: "Phase 2",
59
+ title: "Block Mode Design",
60
+ subtitle: "Structured input for cesium_publish",
61
+ meta: [
62
+ { k: "Status", v: "Draft" },
63
+ { k: "Author", v: "AI" },
64
+ ],
65
+ },
66
+ };
@@ -0,0 +1,45 @@
1
+ // KV block renderer.
2
+ // src/render/blocks/renderers/kv.ts
3
+
4
+ import type { KvBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+
9
+ export function renderKv(block: KvBlock, _ctx: RenderCtx): string {
10
+ const rows = block.rows
11
+ .map((row) => ` <dt>${escapeHtml(row.k)}</dt><dd>${escapeHtml(row.v)}</dd>`)
12
+ .join("\n");
13
+ return `<dl class="kv">\n${rows}\n</dl>`;
14
+ }
15
+
16
+ export const meta: BlockMeta = {
17
+ type: "kv",
18
+ description: "Key-value metadata list. Renders as a definition list.",
19
+ schema: {
20
+ type: "object",
21
+ properties: {
22
+ type: { const: "kv" },
23
+ rows: {
24
+ type: "array",
25
+ items: {
26
+ type: "object",
27
+ properties: {
28
+ k: { type: "string" },
29
+ v: { type: "string" },
30
+ },
31
+ required: ["k", "v"],
32
+ },
33
+ },
34
+ },
35
+ required: ["type", "rows"],
36
+ },
37
+ example: {
38
+ type: "kv",
39
+ rows: [
40
+ { k: "Author", v: "AI Agent" },
41
+ { k: "Status", v: "Draft" },
42
+ { k: "Version", v: "2.0.0" },
43
+ ],
44
+ },
45
+ };
@@ -0,0 +1,51 @@
1
+ // List block renderer.
2
+ // src/render/blocks/renderers/list.ts
3
+
4
+ import type { ListBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { renderMarkdown } from "../markdown.ts";
8
+
9
+ export function renderList(block: ListBlock, _ctx: RenderCtx): string {
10
+ const style = block.style ?? "bullet";
11
+
12
+ const items = block.items
13
+ .map((item) => {
14
+ const content = renderMarkdown(item).replace(/^<p>|<\/p>$/g, "");
15
+ return ` <li>${content}</li>`;
16
+ })
17
+ .join("\n");
18
+
19
+ if (style === "number") {
20
+ return `<ol>\n${items}\n</ol>`;
21
+ } else if (style === "check") {
22
+ const checkItems = block.items
23
+ .map((item) => {
24
+ const content = renderMarkdown(item).replace(/^<p>|<\/p>$/g, "");
25
+ return ` <li class="check">${content}</li>`;
26
+ })
27
+ .join("\n");
28
+ return `<ul class="check-list">\n${checkItems}\n</ul>`;
29
+ } else {
30
+ return `<ul>\n${items}\n</ul>`;
31
+ }
32
+ }
33
+
34
+ export const meta: BlockMeta = {
35
+ type: "list",
36
+ description: "Bullet, numbered, or checklist. Items are markdown strings.",
37
+ schema: {
38
+ type: "object",
39
+ properties: {
40
+ type: { const: "list" },
41
+ style: { type: "string", enum: ["bullet", "number", "check"] },
42
+ items: { type: "array", items: { type: "string" } },
43
+ },
44
+ required: ["type", "items"],
45
+ },
46
+ example: {
47
+ type: "list",
48
+ style: "bullet",
49
+ items: ["First item with **bold**", "Second item", "Third item"],
50
+ },
51
+ };
@@ -0,0 +1,45 @@
1
+ // PillRow block renderer.
2
+ // src/render/blocks/renderers/pill-row.ts
3
+
4
+ import type { PillRowBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+
9
+ export function renderPillRow(block: PillRowBlock, _ctx: RenderCtx): string {
10
+ const pills = block.items
11
+ .map((item) => ` <span class="${item.kind}">${escapeHtml(item.text)}</span>`)
12
+ .join("\n");
13
+ return `<div class="pill-row">\n${pills}\n</div>`;
14
+ }
15
+
16
+ export const meta: BlockMeta = {
17
+ type: "pill_row",
18
+ description: "Horizontal row of pill or tag chips. Each item has a kind (pill or tag) and text.",
19
+ schema: {
20
+ type: "object",
21
+ properties: {
22
+ type: { const: "pill_row" },
23
+ items: {
24
+ type: "array",
25
+ items: {
26
+ type: "object",
27
+ properties: {
28
+ kind: { type: "string", enum: ["pill", "tag"] },
29
+ text: { type: "string" },
30
+ },
31
+ required: ["kind", "text"],
32
+ },
33
+ },
34
+ },
35
+ required: ["type", "items"],
36
+ },
37
+ example: {
38
+ type: "pill_row",
39
+ items: [
40
+ { kind: "pill", text: "TypeScript" },
41
+ { kind: "pill", text: "Bun" },
42
+ { kind: "tag", text: "phase-2" },
43
+ ],
44
+ },
45
+ };
@@ -0,0 +1,29 @@
1
+ // Prose block renderer.
2
+ // src/render/blocks/renderers/prose.ts
3
+
4
+ import type { ProseBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { renderMarkdown } from "../markdown.ts";
8
+
9
+ export function renderProse(block: ProseBlock, _ctx: RenderCtx): string {
10
+ return renderMarkdown(block.markdown);
11
+ }
12
+
13
+ export const meta: BlockMeta = {
14
+ type: "prose",
15
+ description: "Free-form markdown text block. Renders paragraphs, lists, emphasis, links.",
16
+ schema: {
17
+ type: "object",
18
+ properties: {
19
+ type: { const: "prose" },
20
+ markdown: { type: "string" },
21
+ },
22
+ required: ["type", "markdown"],
23
+ },
24
+ example: {
25
+ type: "prose",
26
+ markdown:
27
+ "This is a paragraph with **bold** and *italic* text.\n\n- Item one\n- Item two",
28
+ },
29
+ };
@@ -0,0 +1,32 @@
1
+ // RawHtml block renderer — escape-hatch for fully custom HTML payloads.
2
+ // src/render/blocks/renderers/raw-html.ts
3
+
4
+ import type { RawHtmlBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { scrub } from "../../scrub.ts";
8
+
9
+ export function renderRawHtml(block: RawHtmlBlock, _ctx: RenderCtx): string {
10
+ const scrubResult = scrub(block.html);
11
+ return scrubResult.html;
12
+ }
13
+
14
+ export const meta: BlockMeta = {
15
+ type: "raw_html",
16
+ description:
17
+ "Fully custom HTML payload. Scrubbed of external resources. Use when no structured block fits. Include a purpose string for audit trail.",
18
+ schema: {
19
+ type: "object",
20
+ properties: {
21
+ type: { const: "raw_html" },
22
+ html: { type: "string" },
23
+ purpose: { type: "string" },
24
+ },
25
+ required: ["type", "html"],
26
+ },
27
+ example: {
28
+ type: "raw_html",
29
+ html: '<div class="card" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;"><div><h3>Option A</h3><p>Fast but brittle.</p></div><div><h3>Option B</h3><p>Slower but robust.</p></div></div>',
30
+ purpose: "Two-column card layout not expressible as compare_table",
31
+ },
32
+ };
@@ -0,0 +1,76 @@
1
+ // RiskTable block renderer.
2
+ // src/render/blocks/renderers/risk-table.ts
3
+
4
+ import type { RiskTableBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+
9
+ export function renderRiskTable(block: RiskTableBlock, _ctx: RenderCtx): string {
10
+ const headerRow =
11
+ " <thead>\n <tr>\n" +
12
+ " <th>Risk</th>\n <th>Likelihood</th>\n <th>Impact</th>\n <th>Mitigation</th>\n" +
13
+ " </tr>\n </thead>";
14
+
15
+ const bodyRows = block.rows
16
+ .map((row) => {
17
+ return (
18
+ ` <tr>\n` +
19
+ ` <td>${escapeHtml(row.risk)}</td>\n` +
20
+ ` <td class="risk-${escapeHtml(row.likelihood)}">${escapeHtml(row.likelihood)}</td>\n` +
21
+ ` <td class="risk-${escapeHtml(row.impact)}">${escapeHtml(row.impact)}</td>\n` +
22
+ ` <td>${escapeHtml(row.mitigation)}</td>\n` +
23
+ ` </tr>`
24
+ );
25
+ })
26
+ .join("\n");
27
+
28
+ return (
29
+ `<table class="risk-table">\n` +
30
+ `${headerRow}\n` +
31
+ ` <tbody>\n${bodyRows}\n </tbody>\n` +
32
+ `</table>`
33
+ );
34
+ }
35
+
36
+ export const meta: BlockMeta = {
37
+ type: "risk_table",
38
+ description: "Risk register grid with likelihood/impact/mitigation columns.",
39
+ schema: {
40
+ type: "object",
41
+ properties: {
42
+ type: { const: "risk_table" },
43
+ rows: {
44
+ type: "array",
45
+ items: {
46
+ type: "object",
47
+ properties: {
48
+ risk: { type: "string" },
49
+ likelihood: { type: "string", enum: ["low", "medium", "high"] },
50
+ impact: { type: "string", enum: ["low", "medium", "high"] },
51
+ mitigation: { type: "string" },
52
+ },
53
+ required: ["risk", "likelihood", "impact", "mitigation"],
54
+ },
55
+ },
56
+ },
57
+ required: ["type", "rows"],
58
+ },
59
+ example: {
60
+ type: "risk_table",
61
+ rows: [
62
+ {
63
+ risk: "Agent ignores blocks mode",
64
+ likelihood: "high",
65
+ impact: "high",
66
+ mitigation: "Prompt steering + critique bonus + styleguide.",
67
+ },
68
+ {
69
+ risk: "Markdown subset too narrow",
70
+ likelihood: "low",
71
+ impact: "medium",
72
+ mitigation: "Audit first 50 artifacts; expand if needed.",
73
+ },
74
+ ],
75
+ },
76
+ };
@@ -0,0 +1,97 @@
1
+ // Section block renderer.
2
+ // src/render/blocks/renderers/section.ts
3
+
4
+ import type { SectionBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { renderBlock } from "../render.ts";
8
+ import { escapeHtml } from "../escape.ts";
9
+
10
+ export async function renderSection(block: SectionBlock, ctx: RenderCtx): Promise<string> {
11
+ // Determine section number: explicit or auto-increment
12
+ let num: string;
13
+ if (block.num !== undefined && block.num !== "") {
14
+ num = block.num;
15
+ } else {
16
+ num = String(ctx.sectionCounter.value).padStart(2, "0");
17
+ ctx.sectionCounter.value += 1;
18
+ }
19
+
20
+ const parts: string[] = [];
21
+
22
+ if (block.eyebrow !== undefined && block.eyebrow !== "") {
23
+ parts.push(` <div class="eyebrow">${escapeHtml(block.eyebrow)}</div>`);
24
+ }
25
+
26
+ parts.push(
27
+ ` <h2 class="h-section"><span class="section-num">${escapeHtml(num)}</span> ${escapeHtml(block.title)}</h2>`,
28
+ );
29
+
30
+ // Render children with incremented depth.
31
+ // Non-section children are grouped into <div class="card"> wrappers for visual polish.
32
+ // Nested section children are emitted at top level (they carry their own card structure).
33
+ const childCtx: RenderCtx = {
34
+ sectionCounter: ctx.sectionCounter,
35
+ depth: ctx.depth + 1,
36
+ path: `${ctx.path}.children`,
37
+ highlightTheme: ctx.highlightTheme,
38
+ };
39
+
40
+ let buffer: string[] = [];
41
+
42
+ for (let i = 0; i < block.children.length; i++) {
43
+ const child = block.children[i];
44
+ if (child === undefined) continue;
45
+ const childBlockCtx: RenderCtx = {
46
+ ...childCtx,
47
+ path: `${ctx.path}.children[${i}]`,
48
+ };
49
+ // eslint-disable-next-line no-await-in-loop -- sequential render required; card buffer tracks contiguous non-section children
50
+ const rendered = await renderBlock(child, childBlockCtx);
51
+ if (child.type === "section") {
52
+ // Flush buffered non-section children into a card first
53
+ if (buffer.length > 0) {
54
+ parts.push(` <div class="card">\n${buffer.join("\n")}\n </div>`);
55
+ buffer = [];
56
+ }
57
+ parts.push(` ${rendered}`);
58
+ } else {
59
+ buffer.push(` ${rendered}`);
60
+ }
61
+ }
62
+
63
+ // Flush any remaining non-section children
64
+ if (buffer.length > 0) {
65
+ parts.push(` <div class="card">\n${buffer.join("\n")}\n </div>`);
66
+ }
67
+
68
+ return `<section>\n${parts.join("\n")}\n</section>`;
69
+ }
70
+
71
+ export const meta: BlockMeta = {
72
+ type: "section",
73
+ description:
74
+ "Numbered section with title and child blocks. Only block type with children. Nesting depth ≤ 3.",
75
+ schema: {
76
+ type: "object",
77
+ properties: {
78
+ type: { const: "section" },
79
+ title: { type: "string" },
80
+ num: { type: "string" },
81
+ eyebrow: { type: "string" },
82
+ children: { type: "array", items: { type: "object" } },
83
+ },
84
+ required: ["type", "title", "children"],
85
+ },
86
+ example: {
87
+ type: "section",
88
+ title: "Goals",
89
+ eyebrow: "Why we're here",
90
+ children: [
91
+ {
92
+ type: "prose",
93
+ markdown: "Reduce output tokens by moving structural HTML into the server.",
94
+ },
95
+ ],
96
+ },
97
+ };
@@ -0,0 +1,58 @@
1
+ // Timeline block renderer.
2
+ // src/render/blocks/renderers/timeline.ts
3
+
4
+ import type { TimelineBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { escapeHtml } from "../escape.ts";
8
+
9
+ export function renderTimeline(block: TimelineBlock, _ctx: RenderCtx): string {
10
+ const items = block.items
11
+ .map((item) => {
12
+ const dateHtml =
13
+ item.date !== undefined && item.date !== ""
14
+ ? ` <span class="timeline-date">${escapeHtml(item.date)}</span>`
15
+ : "";
16
+ return (
17
+ ` <li class="timeline-item">\n` +
18
+ ` <span class="timeline-label">${escapeHtml(item.label)}${dateHtml}</span>\n` +
19
+ ` <span class="timeline-text">${escapeHtml(item.text)}</span>\n` +
20
+ ` </li>`
21
+ );
22
+ })
23
+ .join("\n");
24
+
25
+ return `<ul class="timeline">\n${items}\n</ul>`;
26
+ }
27
+
28
+ export const meta: BlockMeta = {
29
+ type: "timeline",
30
+ description: "Milestone list with dot connectors. Each item has a label, text, and optional date.",
31
+ schema: {
32
+ type: "object",
33
+ properties: {
34
+ type: { const: "timeline" },
35
+ items: {
36
+ type: "array",
37
+ items: {
38
+ type: "object",
39
+ properties: {
40
+ label: { type: "string" },
41
+ text: { type: "string" },
42
+ date: { type: "string" },
43
+ },
44
+ required: ["label", "text"],
45
+ },
46
+ },
47
+ },
48
+ required: ["type", "items"],
49
+ },
50
+ example: {
51
+ type: "timeline",
52
+ items: [
53
+ { label: "Phase 1", text: "CSS extraction and theme serving", date: "2026-05-10" },
54
+ { label: "Phase 2", text: "Block plumbing and renderers", date: "2026-05-12" },
55
+ { label: "Phase 3", text: "Tooling flip — prompt and styleguide update" },
56
+ ],
57
+ },
58
+ };
@@ -0,0 +1,30 @@
1
+ // Tldr block renderer.
2
+ // src/render/blocks/renderers/tldr.ts
3
+
4
+ import type { TldrBlock } from "../types.ts";
5
+ import type { BlockMeta } from "../types.ts";
6
+ import type { RenderCtx } from "../render.ts";
7
+ import { renderMarkdown } from "../markdown.ts";
8
+
9
+ export function renderTldr(block: TldrBlock, _ctx: RenderCtx): string {
10
+ return `<aside class="tldr">\n${renderMarkdown(block.markdown)}\n</aside>`;
11
+ }
12
+
13
+ export const meta: BlockMeta = {
14
+ type: "tldr",
15
+ description:
16
+ "Clay-bordered summary box. Use at most one per document, near the top. Content is markdown.",
17
+ schema: {
18
+ type: "object",
19
+ properties: {
20
+ type: { const: "tldr" },
21
+ markdown: { type: "string" },
22
+ },
23
+ required: ["type", "markdown"],
24
+ },
25
+ example: {
26
+ type: "tldr",
27
+ markdown:
28
+ "**Summary:** This document covers the block-mode refactor for `cesium_publish`. Three phases: plumbing, tooling flip, and cleanup.",
29
+ },
30
+ };