@mulmocast/deck 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.
- package/README.md +289 -1
- package/lib/blocks.js +46 -9
- package/lib/index.d.ts +1 -1
- package/lib/layouts/comparison.js +18 -2
- package/lib/layouts/grid.js +4 -1
- package/lib/layouts/title.js +12 -1
- package/lib/render.js +15 -2
- package/lib/schema.d.ts +2454 -0
- package/lib/schema.js +47 -0
- package/lib/utils.d.ts +13 -1
- package/lib/utils.js +29 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,16 +45,304 @@ const html = generateSlideHTML(theme, slide);
|
|
|
45
45
|
|
|
46
46
|
## Available layouts
|
|
47
47
|
|
|
48
|
-
`title` · `bigQuote` · `columns` · `comparison` · `stats` · `table` · `timeline` · `matrix` · `grid` · `split` · `funnel` · `waterfall`
|
|
48
|
+
`title` · `bigQuote` · `columns` · `comparison` · `stats` · `table` · `timeline` · `matrix` · `grid` · `split` · `funnel` · `waterfall` · `manifesto`
|
|
49
49
|
|
|
50
50
|
Each layout has its own Zod schema under `slideLayoutSchema` (a discriminated union). See `src/schema.ts` for the full shape.
|
|
51
51
|
|
|
52
|
+
## Content blocks
|
|
53
|
+
|
|
54
|
+
`text` · `bullets` · `code` · `callout` · `metric` · `divider` · `image` · `imageRef` · `chart` · `mermaid` · `section` · `table` · `tag`
|
|
55
|
+
|
|
56
|
+
## Enhancements (0.2 – 0.5)
|
|
57
|
+
|
|
58
|
+
All fields below are **optional and additive**. Existing decks render byte-identically when they're absent.
|
|
59
|
+
|
|
60
|
+
### Theme — backgrounds, gradient titles, accent font (0.2.0)
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const theme: SlideTheme = {
|
|
64
|
+
colors: { /* ... */ },
|
|
65
|
+
fonts: {
|
|
66
|
+
title: "'Noto Sans JP', system-ui, sans-serif",
|
|
67
|
+
body: "'Noto Sans JP', system-ui, sans-serif",
|
|
68
|
+
mono: "Consolas",
|
|
69
|
+
accent: "Outfit", // optional
|
|
70
|
+
},
|
|
71
|
+
bgGradient: `
|
|
72
|
+
radial-gradient(1200px 700px at 12% -10%, rgba(56,189,248,.16), transparent 60%),
|
|
73
|
+
linear-gradient(160deg, #0A0F24, #16224D)
|
|
74
|
+
`, // optional
|
|
75
|
+
titleGradient: "linear-gradient(100deg, #FFF, #38BDF8 60%, #818CF8)", // optional
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `bgGradient` — any CSS background string. Each slide also accepts `style.bgGradient` to override per-slide.
|
|
80
|
+
- `titleGradient` — applied as `background-clip: text` to slide `<h1>` / `<h2>` for gradient-filled titles.
|
|
81
|
+
- `fonts.accent` — registered as a Tailwind `font-accent` class. Used for tracking-heavy uppercase labels (eyebrow, numLabel, chip stats, etc.).
|
|
82
|
+
- `isSafeCssBackground()` is exported — bad values are silently dropped so you can't break out of the CSS context.
|
|
83
|
+
|
|
84
|
+
### Eyebrow — small uppercase category pill (0.3.0)
|
|
85
|
+
|
|
86
|
+
Available on every layout that uses the standard slide header (`stats`, `columns`, `comparison`, `grid`, `timeline`, `matrix`, `funnel`, `waterfall`, `manifesto`) plus `title` and `bigQuote`.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
{
|
|
90
|
+
layout: "stats",
|
|
91
|
+
eyebrow: { label: "Highlights" }, // primary by default
|
|
92
|
+
// or with explicit color
|
|
93
|
+
// eyebrow: { label: "重要", color: "warning" },
|
|
94
|
+
title: "Quarterly Snapshot",
|
|
95
|
+
stats: [/* ... */],
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Chips row (title layout) — pill badges (0.3.0)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
{
|
|
103
|
+
layout: "title",
|
|
104
|
+
title: "第4回 BootCamp\nキックオフ",
|
|
105
|
+
chips: ["🚀 deploy or die", "🔁 ドッグフーディング", "⚡ 週1アウトプット"],
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### numLabel — accent-colored prefix (0.3.0)
|
|
110
|
+
|
|
111
|
+
Available on `stats[]` items and `columns[]` cards. Renders as a small accent-colored typographic prefix above (stats) or before (columns) the title — useful for "01 / 02 / 03 …" numbered lists.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
{
|
|
115
|
+
layout: "stats",
|
|
116
|
+
title: "Quarterly Snapshot",
|
|
117
|
+
stats: [
|
|
118
|
+
{ numLabel: "01", value: "+42%", label: "Revenue YoY", color: "success" },
|
|
119
|
+
{ numLabel: "02", value: "1.8M", label: "Active Users", color: "primary" },
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
{
|
|
126
|
+
layout: "columns",
|
|
127
|
+
title: "Agenda",
|
|
128
|
+
columns: [
|
|
129
|
+
{ numLabel: "01", title: "Origin", content: [/* ... */] },
|
|
130
|
+
{ numLabel: "02", title: "Plan", content: [/* ... */] },
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Icon bullets — status glyphs (0.4.0)
|
|
136
|
+
|
|
137
|
+
Bullet items now accept `{ icon: "ok" | "no" | "warn" }` to render ✓ / ✕ / ⚠ in the success / danger / warning theme color, replacing the default block marker.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
{
|
|
141
|
+
type: "bullets",
|
|
142
|
+
items: [
|
|
143
|
+
{ text: "all green", icon: "ok" }, // ✓ in success color
|
|
144
|
+
{ text: "broken", icon: "no" }, // ✕ in danger color
|
|
145
|
+
{ text: "watch out", icon: "warn" }, // ⚠ in warning color
|
|
146
|
+
"plain string still works (uses block-level marker)",
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Hot timeline — "you are here" emphasis (0.4.0)
|
|
152
|
+
|
|
153
|
+
Add `hot: true` to a `timeline` item to ring its dot. Defaults the color to `warning` when no `color` is set; otherwise it preserves the item's color.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
{
|
|
157
|
+
layout: "timeline",
|
|
158
|
+
title: "Roadmap",
|
|
159
|
+
items: [
|
|
160
|
+
{ date: "Q1", title: "Kickoff", done: true, color: "success" },
|
|
161
|
+
{ date: "Q2", title: "MVP", done: true, color: "success" },
|
|
162
|
+
{ date: "Q3", title: "Dogfooding", hot: true, color: "warning" }, // ← ring
|
|
163
|
+
{ date: "Q4", title: "Launch" },
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Manifesto layout — principles grid (0.4.0)
|
|
169
|
+
|
|
170
|
+
Grid of small left-bordered cards. Useful for creeds / "what we believe" lists / commitments. Configurable column count (1-4); each line has its own `accentColor` for the left bar.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
{
|
|
174
|
+
layout: "manifesto",
|
|
175
|
+
eyebrow: { label: "Culture" },
|
|
176
|
+
title: "SS の行動哲学",
|
|
177
|
+
columns: 2, // optional, default 2 (1-4)
|
|
178
|
+
items: [
|
|
179
|
+
{ title: "行動する、それがすべて。",
|
|
180
|
+
description: "考えているだけでは、存在しないのと同じ。",
|
|
181
|
+
accentColor: "primary" },
|
|
182
|
+
{ title: "deploy or die.",
|
|
183
|
+
description: "社会に実装していくことが、すべて。",
|
|
184
|
+
accentColor: "warning" },
|
|
185
|
+
{ title: "締切のないタスクは、やらなくていいタスク。",
|
|
186
|
+
accentColor: "success" },
|
|
187
|
+
{ title: "失敗していない=挑戦していない。",
|
|
188
|
+
accentColor: "danger" },
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Text size variants — lead / big / sub (0.5.0)
|
|
194
|
+
|
|
195
|
+
Theme-aware size variants for `text`, `bullets`, and `callout` blocks. Use these instead of hand-picking pixel sizes so the (font + color) tuple stays consistent.
|
|
196
|
+
|
|
197
|
+
| `size` | px | color | role |
|
|
198
|
+
|--|--|--|--|
|
|
199
|
+
| `default` (omitted) | 15 | text-muted | body |
|
|
200
|
+
| `lead` | 17 | text-muted | intro paragraph |
|
|
201
|
+
| `big` | 19 | text-full | emphasized body |
|
|
202
|
+
| `sub` | 13 | text-dim | card footnote |
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// Block-level (applies to every item in the list)
|
|
206
|
+
{ type: "bullets", size: "lead", items: ["…", "…"] }
|
|
207
|
+
|
|
208
|
+
// Per-item override (mixes sizes inside one block)
|
|
209
|
+
{ type: "bullets", size: "lead", items: [
|
|
210
|
+
"intro size lead",
|
|
211
|
+
{ text: "footnote size sub", size: "sub" },
|
|
212
|
+
]}
|
|
213
|
+
|
|
214
|
+
// callout with smaller body text
|
|
215
|
+
{ type: "callout", label: "Note", text: "…", size: "sub" }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Inline `*emphasis*` (0.5.0)
|
|
219
|
+
|
|
220
|
+
In addition to `**bold**` and `{color:text}`, single-asterisk emphasis renders as warning-colored bold (mimics reveal.js amber `<em>`):
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
**bold** → strong, full text color
|
|
224
|
+
*emphasis* → bold, warning color (amber)
|
|
225
|
+
{primary:x} → primary-colored span
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`*` is treated as emphasis only at word boundaries — mid-word `a*b*c` (and `* spaced *`) are left as literal asterisks, so existing prose isn't accidentally parsed.
|
|
229
|
+
|
|
230
|
+
### Slide density — compact (0.5.0)
|
|
231
|
+
|
|
232
|
+
`density: "compact"` shrinks body / list text and tightens padding for slides with a lot of content. Approximates reveal.js' autofit, no JS required.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
{
|
|
236
|
+
layout: "comparison",
|
|
237
|
+
density: "compact",
|
|
238
|
+
// …content-heavy comparison content here…
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
CSS is scoped to `.density-compact` on the slide wrapper so it can't leak.
|
|
243
|
+
|
|
244
|
+
### Comparison panel `ratio` and `cardless` (0.5.0)
|
|
245
|
+
|
|
246
|
+
`comparison.left.ratio` / `comparison.right.ratio` (numeric) give asymmetric left/right panels. `cardless: true` drops the card chrome and renders content directly on the slide — useful for the reveal.js `.two` pattern (bare list left, boxed callout right).
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
{
|
|
250
|
+
layout: "comparison",
|
|
251
|
+
left: {
|
|
252
|
+
title: "共有する",
|
|
253
|
+
cardless: true,
|
|
254
|
+
ratio: 1.2,
|
|
255
|
+
content: [{ type: "bullets", size: "lead", items: [...] }],
|
|
256
|
+
},
|
|
257
|
+
right: {
|
|
258
|
+
title: "まず動く最小(MVP)",
|
|
259
|
+
content: [
|
|
260
|
+
{ type: "tag", text: "MVP", color: "warning" },
|
|
261
|
+
{ type: "text", value: "…", size: "sub" },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Grid item `span` (0.5.0)
|
|
268
|
+
|
|
269
|
+
Asymmetric grids — one wide item spanning multiple columns.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
{
|
|
273
|
+
layout: "grid",
|
|
274
|
+
gridColumns: 3,
|
|
275
|
+
items: [
|
|
276
|
+
{ title: "wide hero card", span: 2 }, // takes two columns
|
|
277
|
+
{ title: "narrow card" },
|
|
278
|
+
{ title: "another", span: 3 }, // spans full row
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Title size override — small / default / large / hero (0.5.0)
|
|
284
|
+
|
|
285
|
+
Per-slide override for the slide title. Applies to layouts that use `slideHeader` / `centeredSlideHeader` (most layouts), and to the `title` layout's h1.
|
|
286
|
+
|
|
287
|
+
| `titleSize` | h2 (px) | title-layout h1 (px) |
|
|
288
|
+
|--|--|--|
|
|
289
|
+
| `small` | 34 | 48 |
|
|
290
|
+
| `default` (omitted) | 42 | 60 |
|
|
291
|
+
| `large` | 52 | 68 |
|
|
292
|
+
| `hero` | 64 | 76 |
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
{ layout: "title", titleSize: "hero", title: "OPENING" }
|
|
296
|
+
{ layout: "comparison", titleSize: "small", density: "compact", title: "ルールと注意点", … }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Subtitle size — default / lead / big (0.5.0)
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
{
|
|
303
|
+
layout: "stats",
|
|
304
|
+
title: "Q1 Snapshot",
|
|
305
|
+
subtitle: "売上は前年同期比 +42%",
|
|
306
|
+
subtitleSize: "big", // 22px — matches reveal.js .big.muted
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Glass card style (0.5.0)
|
|
311
|
+
|
|
312
|
+
`theme.cardStyle: "glass"` swaps the default opaque `bg-d-card` for a subtle white-gradient + 1px translucent border + 16px radius. Off by default — when set, every card on every slide gets the treatment.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const theme: SlideTheme = {
|
|
316
|
+
colors: { … },
|
|
317
|
+
fonts: { … },
|
|
318
|
+
cardStyle: "glass",
|
|
319
|
+
};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `tag` content block (0.5.0)
|
|
323
|
+
|
|
324
|
+
Small uppercase accent label intended for use INSIDE cards. Distinct from the slide-level `eyebrow` (which sits at the top of the whole slide).
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
{
|
|
328
|
+
layout: "comparison",
|
|
329
|
+
right: {
|
|
330
|
+
title: "価値が伝わる最小のものを",
|
|
331
|
+
content: [
|
|
332
|
+
{ type: "tag", text: "まず動く最小 (MVP)", color: "warning" },
|
|
333
|
+
{ type: "text", value: "CLI / HTML1枚 / LP / モック でいい。", size: "sub" },
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
52
339
|
## Design
|
|
53
340
|
|
|
54
341
|
- **Data → HTML, no side effects.** Pure functions, easy to test and use anywhere.
|
|
55
342
|
- **Tailwind via CDN.** Themes resolve to CSS variables; no compile step.
|
|
56
343
|
- **Schema-first.** All shapes are validated with [Zod](https://zod.dev), so types are derived (not duplicated).
|
|
57
344
|
- **Browser-safe.** No Node-only APIs. Just import in a Vite/Vue/React app and render into an iframe.
|
|
345
|
+
- **Additive evolution.** New optional fields never break existing decks — guaranteed by the test suite.
|
|
58
346
|
|
|
59
347
|
## Consumers
|
|
60
348
|
|
package/lib/blocks.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { escapeHtml, c, generateSlideId, renderInlineMarkup, blockTitle, resolveChangeColor, resolveAccent } from "./utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Map a TextSize variant to its Tailwind classes.
|
|
4
|
+
* default — body / muted (the original 0.1.x behavior; emitted only when neither size nor numeric fontSize is set).
|
|
5
|
+
* lead — slightly larger muted intro paragraph.
|
|
6
|
+
* big — larger, full text color.
|
|
7
|
+
* sub — smaller, dimmer card footnote.
|
|
8
|
+
*/
|
|
9
|
+
const TEXT_SIZE_STYLES = {
|
|
10
|
+
default: { fontCls: "text-[15px]", colorCls: "text-d-muted" },
|
|
11
|
+
lead: { fontCls: "text-[17px] leading-relaxed", colorCls: "text-d-muted" },
|
|
12
|
+
big: { fontCls: "text-[19px] leading-snug", colorCls: "text-d-text" },
|
|
13
|
+
sub: { fontCls: "text-[13px] leading-snug", colorCls: "text-d-dim" },
|
|
14
|
+
};
|
|
2
15
|
// ─── Table cell rendering (shared with layouts/table.ts) ───
|
|
3
16
|
export const resolveCellColor = (cellObj, isRowHeader) => {
|
|
4
17
|
if (cellObj.color)
|
|
@@ -76,10 +89,17 @@ export const renderContentBlock = (block) => {
|
|
|
76
89
|
return renderSection(block);
|
|
77
90
|
case "table":
|
|
78
91
|
return renderTableBlock(block);
|
|
92
|
+
case "tag":
|
|
93
|
+
return renderTag(block);
|
|
79
94
|
default:
|
|
80
95
|
return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
|
|
81
96
|
}
|
|
82
97
|
};
|
|
98
|
+
/** Render a card-internal accent tag (small uppercase label, sits above an h3). Matches reveal.js .tag. */
|
|
99
|
+
const renderTag = (block) => {
|
|
100
|
+
const color = c(block.color || "primary");
|
|
101
|
+
return `<span class="text-xs font-bold uppercase tracking-[0.12em] text-${color} font-accent">${renderInlineMarkup(block.text)}</span>`;
|
|
102
|
+
};
|
|
83
103
|
/** Render an array of content blocks to HTML */
|
|
84
104
|
export const renderContentBlocks = (blocks) => {
|
|
85
105
|
return blocks.map(renderContentBlock).join("\n");
|
|
@@ -95,12 +115,13 @@ export const renderCardContentBlocks = (blocks) => {
|
|
|
95
115
|
})
|
|
96
116
|
.join("\n");
|
|
97
117
|
};
|
|
118
|
+
/** When the author explicitly sets color/dim, honor it; otherwise inherit from the size variant's default. */
|
|
98
119
|
const resolveTextColor = (block) => {
|
|
99
120
|
if (block.color)
|
|
100
121
|
return `text-${c(block.color)}`;
|
|
101
122
|
if (block.dim)
|
|
102
123
|
return "text-d-dim";
|
|
103
|
-
return
|
|
124
|
+
return undefined;
|
|
104
125
|
};
|
|
105
126
|
const resolveAlign = (align) => {
|
|
106
127
|
if (align === "center")
|
|
@@ -110,11 +131,15 @@ const resolveAlign = (align) => {
|
|
|
110
131
|
return "";
|
|
111
132
|
};
|
|
112
133
|
const renderText = (block) => {
|
|
113
|
-
|
|
134
|
+
// Resolution order for size: explicit numeric `fontSize` (legacy) wins over new `size` variant.
|
|
135
|
+
const legacyXl = block.fontSize !== undefined && block.fontSize >= 18;
|
|
136
|
+
const style = TEXT_SIZE_STYLES[block.size ?? "default"];
|
|
137
|
+
const sizeCls = legacyXl ? "text-xl" : style.fontCls;
|
|
138
|
+
const explicitColor = resolveTextColor(block);
|
|
139
|
+
const colorCls = explicitColor ?? style.colorCls ?? "text-d-muted";
|
|
114
140
|
const bold = block.bold ? "font-bold" : "";
|
|
115
|
-
const size = block.fontSize !== undefined && block.fontSize >= 18 ? "text-xl" : "text-[15px]";
|
|
116
141
|
const alignCls = resolveAlign(block.align);
|
|
117
|
-
return `<p class="${
|
|
142
|
+
return `<p class="${sizeCls} ${colorCls} ${bold} ${alignCls} font-body leading-relaxed">${renderInlineMarkup(block.value)}</p>`;
|
|
118
143
|
};
|
|
119
144
|
/** Extract text from a bullet item (string or object) */
|
|
120
145
|
const bulletItemText = (item) => {
|
|
@@ -137,8 +162,15 @@ const STATUS_ICON_GLYPHS = {
|
|
|
137
162
|
no: { glyph: "\u2715", color: "danger" }, // \u2715
|
|
138
163
|
warn: { glyph: "\u26a0", color: "warning" }, // \u26a0
|
|
139
164
|
};
|
|
165
|
+
/** Resolve the size variant for a bullet item \u2014 per-item size wins over block-level size. */
|
|
166
|
+
const resolveBulletSize = (block, item) => {
|
|
167
|
+
if (typeof item === "object" && item.size)
|
|
168
|
+
return item.size;
|
|
169
|
+
return block.size ?? "default";
|
|
170
|
+
};
|
|
140
171
|
const renderBullets = (block) => {
|
|
141
172
|
const tag = block.ordered ? "ol" : "ul";
|
|
173
|
+
const blockStyle = TEXT_SIZE_STYLES[block.size ?? "default"];
|
|
142
174
|
const items = block.items
|
|
143
175
|
.map((item, i) => {
|
|
144
176
|
// Per-item status icon overrides the block-level marker / numbered prefix.
|
|
@@ -148,10 +180,13 @@ const renderBullets = (block) => {
|
|
|
148
180
|
: `<span class="text-d-dim shrink-0">${block.ordered ? `${i + 1}.` : escapeHtml(block.icon || "\u2022")}</span>`;
|
|
149
181
|
const text = bulletItemText(item);
|
|
150
182
|
const subHtml = renderSubBullets(item);
|
|
151
|
-
|
|
183
|
+
const itemStyle = TEXT_SIZE_STYLES[resolveBulletSize(block, item)];
|
|
184
|
+
// Emit per-item classes only when they differ from the block-level style so output stays compact in the common case.
|
|
185
|
+
const itemCls = itemStyle.fontCls === blockStyle.fontCls && itemStyle.colorCls === blockStyle.colorCls ? "" : ` ${itemStyle.fontCls} ${itemStyle.colorCls}`;
|
|
186
|
+
return ` <li class="flex flex-col gap-1${itemCls}"><div class="flex gap-2">${markerHtml}<span>${renderInlineMarkup(text)}</span></div>${subHtml}</li>`;
|
|
152
187
|
})
|
|
153
188
|
.join("\n");
|
|
154
|
-
return `<${tag} class="space-y-2
|
|
189
|
+
return `<${tag} class="space-y-2 ${blockStyle.fontCls} ${blockStyle.colorCls} font-body">\n${items}\n</${tag}>`;
|
|
155
190
|
};
|
|
156
191
|
const renderCode = (block) => {
|
|
157
192
|
return `<pre class="bg-[#0D1117] p-4 rounded text-sm font-mono text-d-dim leading-relaxed whitespace-pre-wrap">${escapeHtml(block.code)}</pre>`;
|
|
@@ -167,11 +202,13 @@ const renderCallout = (block) => {
|
|
|
167
202
|
};
|
|
168
203
|
const borderCls = resolveBorderCls(block.style);
|
|
169
204
|
const bg = isQuote ? "bg-d-alt" : "bg-d-card";
|
|
170
|
-
|
|
205
|
+
// Pre-Phase-4 default was `text-sm` (~14px). Map to the size variants only when explicitly requested.
|
|
206
|
+
const sizeStyle = block.size ? TEXT_SIZE_STYLES[block.size] : { fontCls: "text-sm", colorCls: "text-d-muted" };
|
|
207
|
+
const textCls = isQuote ? `italic ${sizeStyle.colorCls}` : sizeStyle.colorCls;
|
|
171
208
|
const content = block.label
|
|
172
|
-
? `<span class="font-bold text-${c(block.color || "warning")}">${renderInlineMarkup(block.label)}:</span> <span class="
|
|
209
|
+
? `<span class="font-bold text-${c(block.color || "warning")}">${renderInlineMarkup(block.label)}:</span> <span class="${textCls}">${renderInlineMarkup(block.text)}</span>`
|
|
173
210
|
: `<span class="${textCls}">${renderInlineMarkup(block.text)}</span>`;
|
|
174
|
-
return `<div class="${bg} ${borderCls} p-3 rounded
|
|
211
|
+
return `<div class="${bg} ${borderCls} p-3 rounded ${sizeStyle.fontCls} font-body">${content}</div>`;
|
|
175
212
|
};
|
|
176
213
|
const renderMetric = (block) => {
|
|
177
214
|
const lines = [];
|
package/lib/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { renderSlideContent } from "./layouts/index.js";
|
|
|
4
4
|
export { renderContentBlock, renderContentBlocks } from "./blocks.js";
|
|
5
5
|
export { escapeHtml, resetSlideIdCounter, renderInlineMarkup } from "./utils.js";
|
|
6
6
|
export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, slideBrandingLogoSchema, slideBrandingSchema, } from "./schema.js";
|
|
7
|
-
export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, ImageRefBlock, ChartBlock, MermaidBlock,
|
|
7
|
+
export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, AccentColorKey, ContentBlock, TextBlock, BulletsBlock, BulletItem, CodeBlock, CalloutBlock, MetricBlock, DividerBlock, ImageBlock, ImageRefBlock, ChartBlock, MermaidBlock, SectionBlock, TableBlock, TagBlock, TableCellValue, TitleSlide, ColumnsSlide, ComparisonSlide, ComparisonPanel, GridSlide, GridItem, BigQuoteSlide, StatsSlide, StatItem, TimelineSlide, TimelineItem, SplitSlide, SplitPanel, MatrixSlide, MatrixCell, TableSlide, FunnelSlide, FunnelStage, WaterfallSlide, WaterfallItem, ManifestoSlide, ManifestoLine, Card, CalloutBar, SlideStyle, SlideBrandingLogo, SlideBranding, BulletIcon, TextSize, SlideDensity, SlideTitleSize, SlideSubtitleSize, SlideCardStyle, } from "./schema.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c,
|
|
1
|
+
import { renderInlineMarkup, c, accentBar, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
|
|
2
2
|
import { renderContentBlocks } from "../blocks.js";
|
|
3
3
|
const buildPanel = (panel) => {
|
|
4
4
|
const accent = resolveAccent(panel.accentColor);
|
|
@@ -12,7 +12,23 @@ const buildPanel = (panel) => {
|
|
|
12
12
|
if (panel.footer) {
|
|
13
13
|
inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(panel.footer)}</p>`);
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// Tailwind's arbitrary `flex-[1.5]` works at runtime but stops short of clean class sanitization;
|
|
16
|
+
// emitting `flex-grow` inline keeps the output predictable and avoids depending on JIT mode.
|
|
17
|
+
const flexStyle = panel.ratio !== undefined ? ` style="flex-grow:${panel.ratio};flex-shrink:1;flex-basis:0"` : "";
|
|
18
|
+
const flexCls = panel.ratio !== undefined ? "" : " flex-1";
|
|
19
|
+
// Cardless: render content directly without card chrome. Useful for the bullet-list / card mixed layout
|
|
20
|
+
// common in slide decks (one bare list, one boxed callout).
|
|
21
|
+
if (panel.cardless) {
|
|
22
|
+
return `<div class="flex flex-col min-h-0${flexCls} py-1"${flexStyle}>
|
|
23
|
+
${inner.join("\n")}
|
|
24
|
+
</div>`;
|
|
25
|
+
}
|
|
26
|
+
return `<div class="bg-d-card rounded-lg shadow-lg overflow-hidden flex flex-col min-h-0${flexCls}"${flexStyle}>
|
|
27
|
+
${accentBar(accent)}
|
|
28
|
+
<div class="p-5 flex flex-col flex-1 min-h-0 overflow-hidden">
|
|
29
|
+
${inner.join("\n")}
|
|
30
|
+
</div>
|
|
31
|
+
</div>`;
|
|
16
32
|
};
|
|
17
33
|
export const layoutComparison = (data) => {
|
|
18
34
|
const parts = [slideHeader(data)];
|
package/lib/layouts/grid.js
CHANGED
|
@@ -28,7 +28,10 @@ export const layoutGrid = (data) => {
|
|
|
28
28
|
if (item.content) {
|
|
29
29
|
inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(item.content)}</div>`);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// Asymmetric grids: items can span multiple columns. Class names are mapped explicitly so the JIT compiler keeps them.
|
|
32
|
+
const SPAN_CLS = { 1: "", 2: "col-span-2", 3: "col-span-3", 4: "col-span-4" };
|
|
33
|
+
const spanCls = item.span && item.span > 1 ? SPAN_CLS[item.span] || "" : "";
|
|
34
|
+
parts.push(cardWrap(itemAccent, inner.join("\n"), spanCls));
|
|
32
35
|
});
|
|
33
36
|
parts.push(`</div>`);
|
|
34
37
|
if (data.footer) {
|
package/lib/layouts/title.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { renderInlineMarkup, accentBar, renderEyebrow, renderChipRow, resolveAccent } from "../utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* h1 font-size for the title layout, keyed by titleSize variant.
|
|
4
|
+
* Tuned to match reveal.js scale (base 30px × multiplier): default ≈ 2em, hero ≈ 2.5em.
|
|
5
|
+
*/
|
|
6
|
+
const TITLE_H1_CLS = {
|
|
7
|
+
small: "text-[48px]",
|
|
8
|
+
default: "text-[60px]",
|
|
9
|
+
large: "text-[68px]",
|
|
10
|
+
hero: "text-[76px]",
|
|
11
|
+
};
|
|
2
12
|
export const layoutTitle = (data) => {
|
|
3
13
|
const accent = resolveAccent(data.accentColor);
|
|
4
14
|
const eyebrowHtml = renderEyebrow(data.eyebrow, accent);
|
|
5
15
|
const chipsHtml = renderChipRow(data.chips);
|
|
16
|
+
const titleCls = TITLE_H1_CLS[data.titleSize ?? "default"];
|
|
6
17
|
return [
|
|
7
18
|
accentBar("primary"),
|
|
8
19
|
`<div class="absolute -top-20 -right-8 w-[360px] h-[360px] rounded-full bg-d-primary opacity-10"></div>`,
|
|
9
20
|
`<div class="absolute -bottom-12 -left-16 w-[280px] h-[280px] rounded-full bg-d-accent opacity-10"></div>`,
|
|
10
21
|
`<div class="flex flex-col justify-center h-full px-16 relative z-10">`,
|
|
11
22
|
eyebrowHtml ? ` <div class="mb-4">${eyebrowHtml}</div>` : "",
|
|
12
|
-
` <h1 class="
|
|
23
|
+
` <h1 class="${titleCls} leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h1>`,
|
|
13
24
|
data.subtitle ? ` <p class="text-2xl text-d-muted mt-6 font-body">${renderInlineMarkup(data.subtitle)}</p>` : "",
|
|
14
25
|
data.author ? ` <p class="text-lg text-d-dim mt-10 font-body">${renderInlineMarkup(data.author)}</p>` : "",
|
|
15
26
|
data.note
|
package/lib/render.js
CHANGED
|
@@ -88,6 +88,19 @@ export const generateSlideHTML = (theme, slide, reference, branding) => {
|
|
|
88
88
|
const titleGradientCss = theme.titleGradient && isSafeCssBackground(theme.titleGradient)
|
|
89
89
|
? `\n<style>h1.font-title.font-bold{background:${theme.titleGradient};-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;}</style>`
|
|
90
90
|
: "";
|
|
91
|
+
// Density: when slide.density === "compact", shrink body text and pad spacing — approximates reveal.js' autofit.
|
|
92
|
+
// Scoped to .density-compact so the override never leaks to other slides; the rules are intentionally !important
|
|
93
|
+
// because they have to win over per-utility Tailwind rules injected by the CDN at runtime.
|
|
94
|
+
const densityCss = slide.density === "compact"
|
|
95
|
+
? `\n<style>.density-compact p,.density-compact li{font-size:14px!important;line-height:1.5}.density-compact h2{font-size:32px!important}.density-compact h3{font-size:17px!important}.density-compact .px-12{padding-left:28px!important;padding-right:28px!important}.density-compact .px-16{padding-left:36px!important;padding-right:36px!important}.density-compact .pt-5{padding-top:10px!important}.density-compact .mt-10{margin-top:16px!important}.density-compact .mt-5{margin-top:10px!important}.density-compact .gap-4{gap:10px!important}.density-compact .gap-6{gap:14px!important}.density-compact .space-y-2>*+*{margin-top:4px!important}.density-compact .space-y-4>*+*{margin-top:8px!important}.density-compact .p-5{padding:14px!important}.density-compact .p-10{padding:20px!important}</style>`
|
|
96
|
+
: "";
|
|
97
|
+
const densityCls = slide.density === "compact" ? " density-compact" : "";
|
|
98
|
+
// Glass card style: swap the default solid bg-d-card cards for a subtle gradient + border (reveal.js-style "glass" cards).
|
|
99
|
+
// Scoped to .card-glass on the slide wrapper so it doesn't leak to other slides on the same page.
|
|
100
|
+
const cardGlassCss = theme.cardStyle === "glass"
|
|
101
|
+
? `\n<style>.card-glass .bg-d-card{background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,.02))!important;border:1px solid rgba(120,150,220,.22)!important;box-shadow:none!important}.card-glass .rounded-lg{border-radius:16px!important}</style>`
|
|
102
|
+
: "";
|
|
103
|
+
const cardStyleCls = theme.cardStyle === "glass" ? " card-glass" : "";
|
|
91
104
|
const footer = slideStyle?.footer ? `<p class="absolute bottom-2 right-4 text-xs text-d-dim font-body">${escapeHtml(slideStyle.footer)}</p>` : "";
|
|
92
105
|
const referenceHtml = reference
|
|
93
106
|
? `<div class="mt-auto px-4 pb-2"><p class="text-sm text-d-muted font-body opacity-80">${escapeHtml(reference)}</p></div>`
|
|
@@ -105,10 +118,10 @@ export const generateSlideHTML = (theme, slide, reference, branding) => {
|
|
|
105
118
|
${cdnScripts}
|
|
106
119
|
<style>
|
|
107
120
|
html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
|
108
|
-
</style>${titleGradientCss}
|
|
121
|
+
</style>${titleGradientCss}${densityCss}${cardGlassCss}
|
|
109
122
|
</head>
|
|
110
123
|
<body class="h-full">
|
|
111
|
-
<div class="relative overflow-hidden ${bgCls} w-full h-full flex flex-col"${inlineStyle}>
|
|
124
|
+
<div class="relative overflow-hidden ${bgCls}${densityCls}${cardStyleCls} w-full h-full flex flex-col"${inlineStyle}>
|
|
112
125
|
${brandingBg}
|
|
113
126
|
<div class="relative z-[1] flex flex-col flex-1">
|
|
114
127
|
${content}
|