@deckspec/renderer 0.1.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.
@@ -0,0 +1,418 @@
1
+ export const dashboardCSS = /* css */ `
2
+ /* ================================================================ */
3
+ /* Reset */
4
+ /* ================================================================ */
5
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6
+
7
+ /* ================================================================ */
8
+ /* Body */
9
+ /* ================================================================ */
10
+ body {
11
+ background: #fff;
12
+ color: #1a1a1a;
13
+ font-family: system-ui, -apple-system, sans-serif;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
16
+ min-height: 100vh;
17
+ }
18
+
19
+ /* ================================================================ */
20
+ /* Layout */
21
+ /* ================================================================ */
22
+ .dashboard {
23
+ max-width: 800px;
24
+ margin: 0 auto;
25
+ padding: 2.5rem 1.5rem;
26
+ }
27
+
28
+ /* ================================================================ */
29
+ /* Navigation */
30
+ /* ================================================================ */
31
+ .page-nav {
32
+ margin-bottom: 1rem;
33
+ }
34
+
35
+ .page-nav a {
36
+ color: #737373;
37
+ text-decoration: none;
38
+ font-size: 0.85rem;
39
+ }
40
+
41
+ .page-nav a:hover {
42
+ color: #1a1a1a;
43
+ }
44
+
45
+ /* ================================================================ */
46
+ /* Page Title */
47
+ /* ================================================================ */
48
+ .page-title {
49
+ font-size: 1.5rem;
50
+ font-weight: 700;
51
+ color: #1a1a1a;
52
+ margin-bottom: 2rem;
53
+ }
54
+
55
+ /* ================================================================ */
56
+ /* Sections */
57
+ /* ================================================================ */
58
+ .section {
59
+ margin-bottom: 2.5rem;
60
+ }
61
+
62
+ .section-title {
63
+ font-size: 0.85rem;
64
+ font-weight: 600;
65
+ color: #737373;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.05em;
68
+ margin-bottom: 0.75rem;
69
+ }
70
+
71
+ /* ================================================================ */
72
+ /* List Group (shared by themes & decks on Home) */
73
+ /* ================================================================ */
74
+ .list-group {
75
+ border: 1px solid #e5e5e5;
76
+ border-radius: 0.5rem;
77
+ overflow: hidden;
78
+ }
79
+
80
+ .list-row {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ padding: 0.875rem 1rem;
85
+ border-bottom: 1px solid #f0f0f0;
86
+ }
87
+
88
+ .list-row:last-child {
89
+ border-bottom: none;
90
+ }
91
+
92
+ .list-row-link {
93
+ text-decoration: none;
94
+ color: inherit;
95
+ transition: background 0.1s;
96
+ }
97
+
98
+ .list-row-link:hover {
99
+ background: #fafafa;
100
+ }
101
+
102
+ .list-row-left {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.75rem;
106
+ min-width: 0;
107
+ }
108
+
109
+ .list-row-right {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 0.5rem;
113
+ flex-shrink: 0;
114
+ }
115
+
116
+ .list-row-text {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.125rem;
120
+ min-width: 0;
121
+ }
122
+
123
+ .list-row-name {
124
+ font-size: 0.95rem;
125
+ font-weight: 600;
126
+ color: #1a1a1a;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ }
131
+
132
+ .list-row-meta {
133
+ font-size: 0.75rem;
134
+ color: #a3a3a3;
135
+ }
136
+
137
+ .list-row-arrow {
138
+ color: #d4d4d4;
139
+ font-size: 1.2rem;
140
+ flex-shrink: 0;
141
+ }
142
+
143
+ /* ================================================================ */
144
+ /* Color Dots (Theme row on Home) */
145
+ /* ================================================================ */
146
+ .color-dots {
147
+ display: flex;
148
+ gap: 0;
149
+ }
150
+
151
+ .color-dot {
152
+ width: 24px;
153
+ height: 24px;
154
+ border-radius: 50%;
155
+ border: 2px solid #fff;
156
+ box-shadow: 0 0 0 1px #e5e5e5;
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ .color-dot + .color-dot {
161
+ margin-left: -6px;
162
+ }
163
+
164
+ /* ================================================================ */
165
+ /* Deck Thumbnail (Deck row on Home) */
166
+ /* ================================================================ */
167
+ .deck-thumb {
168
+ width: 64px;
169
+ height: 36px;
170
+ border-radius: 0.25rem;
171
+ overflow: hidden;
172
+ border: 1px solid #e5e5e5;
173
+ flex-shrink: 0;
174
+ background: var(--color-background, #f4f4f4);
175
+ }
176
+
177
+ .deck-thumb-inner {
178
+ transform-origin: top left;
179
+ transform: scale(calc(64 / 1200));
180
+ width: 1200px;
181
+ height: 675px;
182
+ pointer-events: none;
183
+ }
184
+
185
+ .deck-thumb-inner .slide {
186
+ width: 1200px;
187
+ height: 675px;
188
+ }
189
+
190
+ .deck-thumb-empty {
191
+ background: #f5f5f5;
192
+ }
193
+
194
+ /* ================================================================ */
195
+ /* Approval Chip (Deck row) */
196
+ /* ================================================================ */
197
+ .approval-chip {
198
+ font-size: 0.7rem;
199
+ font-weight: 600;
200
+ color: #737373;
201
+ background: #f5f5f5;
202
+ padding: 0.15rem 0.5rem;
203
+ border-radius: 1rem;
204
+ white-space: nowrap;
205
+ }
206
+
207
+ .approval-chip.approval-done {
208
+ background: #dcfce7;
209
+ color: #166534;
210
+ }
211
+
212
+ /* ================================================================ */
213
+ /* Theme Description (Theme Detail page) */
214
+ /* ================================================================ */
215
+ .theme-description {
216
+ font-size: 0.9rem;
217
+ color: #525252;
218
+ line-height: 1.7;
219
+ margin-bottom: 2rem;
220
+ white-space: pre-line;
221
+ }
222
+
223
+ /* ================================================================ */
224
+ /* Color Grid (Theme Detail) */
225
+ /* ================================================================ */
226
+ .color-grid {
227
+ display: flex;
228
+ flex-wrap: wrap;
229
+ gap: 1rem;
230
+ }
231
+
232
+ .color-swatch {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 0.625rem;
236
+ }
237
+
238
+ .color-swatch-circle {
239
+ width: 40px;
240
+ height: 40px;
241
+ border-radius: 50%;
242
+ border: 1px solid #e5e5e5;
243
+ flex-shrink: 0;
244
+ }
245
+
246
+ .color-swatch-info {
247
+ display: flex;
248
+ flex-direction: column;
249
+ }
250
+
251
+ .color-swatch-name {
252
+ font-size: 0.8rem;
253
+ font-weight: 600;
254
+ color: #1a1a1a;
255
+ }
256
+
257
+ .color-swatch-hex {
258
+ font-size: 0.7rem;
259
+ color: #a3a3a3;
260
+ font-family: ui-monospace, monospace;
261
+ }
262
+
263
+ /* ================================================================ */
264
+ /* Typography Samples (Theme Detail) */
265
+ /* ================================================================ */
266
+ .typo-list {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: 0.75rem;
270
+ }
271
+
272
+ .typo-row {
273
+ display: flex;
274
+ align-items: baseline;
275
+ gap: 1rem;
276
+ }
277
+
278
+ .typo-label {
279
+ font-size: 0.7rem;
280
+ font-weight: 600;
281
+ color: #737373;
282
+ width: 2.5rem;
283
+ flex-shrink: 0;
284
+ font-family: ui-monospace, monospace;
285
+ }
286
+
287
+ .typo-meta {
288
+ font-size: 0.7rem;
289
+ color: #a3a3a3;
290
+ width: 7rem;
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ .typo-sample {
295
+ color: #1a1a1a;
296
+ white-space: nowrap;
297
+ overflow: hidden;
298
+ text-overflow: ellipsis;
299
+ }
300
+
301
+ /* ================================================================ */
302
+ /* Spacing Bars (Theme Detail) */
303
+ /* ================================================================ */
304
+ .spacing-list {
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: 0.5rem;
308
+ }
309
+
310
+ .spacing-item {
311
+ display: flex;
312
+ align-items: center;
313
+ gap: 0.75rem;
314
+ }
315
+
316
+ .spacing-bar {
317
+ height: 8px;
318
+ background: #e5e5e5;
319
+ border-radius: 2px;
320
+ }
321
+
322
+ .spacing-label {
323
+ font-size: 0.7rem;
324
+ color: #a3a3a3;
325
+ font-family: ui-monospace, monospace;
326
+ }
327
+
328
+ /* ================================================================ */
329
+ /* Pattern Grid (Theme Detail) */
330
+ /* ================================================================ */
331
+ .pattern-grid {
332
+ display: grid;
333
+ grid-template-columns: repeat(4, 1fr);
334
+ gap: 0.75rem;
335
+ }
336
+
337
+ .pattern-card {
338
+ border: 1px solid #e5e5e5;
339
+ border-radius: 0.375rem;
340
+ overflow: hidden;
341
+ background: #fff;
342
+ }
343
+
344
+ .pattern-thumb {
345
+ aspect-ratio: 16 / 9;
346
+ overflow: hidden;
347
+ background: var(--color-background, #f4f4f4);
348
+ position: relative;
349
+ }
350
+
351
+ .pattern-thumb-inner {
352
+ transform-origin: top left;
353
+ --thumb-scale: 0.155;
354
+ transform: scale(var(--thumb-scale));
355
+ width: 1200px;
356
+ height: 675px;
357
+ pointer-events: none;
358
+ }
359
+
360
+ .pattern-thumb-inner .slide,
361
+ .pattern-thumb-inner .slide-pad,
362
+ .pattern-thumb-inner .slide-stack,
363
+ .pattern-thumb-inner .slide-center,
364
+ .pattern-thumb-inner .slide-white {
365
+ width: 1200px;
366
+ height: 675px;
367
+ }
368
+
369
+ .pattern-thumb-empty {
370
+ background: #f5f5f5;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ }
375
+
376
+ .pattern-thumb-empty::after {
377
+ content: "";
378
+ width: 24px;
379
+ height: 24px;
380
+ border: 2px dashed #d4d4d4;
381
+ border-radius: 0.25rem;
382
+ }
383
+
384
+ .pattern-card-name {
385
+ display: block;
386
+ padding: 0.375rem 0.5rem;
387
+ font-size: 0.7rem;
388
+ color: #525252;
389
+ white-space: nowrap;
390
+ overflow: hidden;
391
+ text-overflow: ellipsis;
392
+ }
393
+
394
+ /* ================================================================ */
395
+ /* Count Badge */
396
+ /* ================================================================ */
397
+ .count-badge {
398
+ font-size: 0.7rem;
399
+ font-weight: 600;
400
+ color: #a3a3a3;
401
+ background: #f5f5f5;
402
+ padding: 0.1rem 0.4rem;
403
+ border-radius: 1rem;
404
+ margin-left: 0.375rem;
405
+ text-transform: none;
406
+ letter-spacing: 0;
407
+ }
408
+
409
+ /* ================================================================ */
410
+ /* Empty State */
411
+ /* ================================================================ */
412
+ .empty-state {
413
+ text-align: center;
414
+ padding: 2rem 1rem;
415
+ color: #a3a3a3;
416
+ font-size: 0.85rem;
417
+ }
418
+ `;
@@ -0,0 +1,14 @@
1
+ export const dashboardJS = /* js */ `
2
+ (function() {
3
+ /* -- SSE live reload --------------------------------------------- */
4
+ if (typeof EventSource !== 'undefined') {
5
+ var es = new EventSource('/events');
6
+ es.addEventListener('reload', function() {
7
+ location.reload();
8
+ });
9
+ es.onerror = function() {
10
+ // Reconnect handled automatically by EventSource
11
+ };
12
+ }
13
+ })();
14
+ `;
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ export {
2
+ loadThemeTokens,
3
+ loadThemeCSS,
4
+ resolveThemePatternsDir,
5
+ resolveThemePatternsSrcDir,
6
+ extractThemeName,
7
+ type ThemeTokens,
8
+ } from "./theme.js";
9
+ export { renderSlide, type RenderSlideContext } from "./render-slide.js";
10
+ export {
11
+ resolveAssets,
12
+ sanitizeSvg,
13
+ type AssetFieldSpec,
14
+ } from "./resolve-assets.js";
15
+ export { renderDeck } from "./render-deck.js";
16
+ export {
17
+ renderDashboard,
18
+ type DeckWithPreviews,
19
+ type SlidePreview,
20
+ type ThemeSummary,
21
+ } from "./render-dashboard.js";
22
+ export { renderThemeDetail } from "./render-theme-detail.js";
23
+ export {
24
+ compileTsx,
25
+ compileTsxCached,
26
+ clearCompileCache,
27
+ } from "./compile-tsx.js";
@@ -0,0 +1,144 @@
1
+ import type { DeckSummary } from "@deckspec/dsl";
2
+ import { dashboardCSS } from "./dashboard-css.js";
3
+ import { dashboardJS } from "./dashboard-js.js";
4
+
5
+ export interface SlidePreview {
6
+ index: number;
7
+ state: string;
8
+ html: string;
9
+ file?: string;
10
+ }
11
+
12
+ export interface DeckWithPreviews {
13
+ summary: DeckSummary;
14
+ slidePreviews: SlidePreview[];
15
+ /** Last modified date of deck.yaml (ISO string or undefined) */
16
+ mtime?: string;
17
+ }
18
+
19
+ export interface ThemeSummary {
20
+ name: string;
21
+ displayName: string;
22
+ patternCount: number;
23
+ /** Key colors for visual identification (3-4 swatches) */
24
+ colors: { name: string; hex: string }[];
25
+ }
26
+
27
+ interface DashboardOptions {
28
+ /** "interactive" enables approve/reject buttons (dev server mode) */
29
+ mode: "static" | "interactive";
30
+ /** Theme CSS to embed for slide previews */
31
+ themeCSS?: string;
32
+ }
33
+
34
+ function escapeHtml(text: string): string {
35
+ return text
36
+ .replace(/&/g, "&")
37
+ .replace(/</g, "&lt;")
38
+ .replace(/>/g, "&gt;")
39
+ .replace(/"/g, "&quot;");
40
+ }
41
+
42
+ function renderThemeRow(theme: ThemeSummary): string {
43
+ const colorDots = theme.colors
44
+ .map(
45
+ (c) =>
46
+ `<span class="color-dot" style="background:${escapeHtml(c.hex)}" title="${escapeHtml(c.name)}: ${escapeHtml(c.hex)}"></span>`,
47
+ )
48
+ .join("");
49
+
50
+ return `<a class="list-row list-row-link" href="/theme/${escapeHtml(theme.name)}">
51
+ <div class="list-row-left">
52
+ <div class="color-dots">${colorDots}</div>
53
+ <div class="list-row-text">
54
+ <span class="list-row-name">${escapeHtml(theme.displayName)}</span>
55
+ <span class="list-row-meta">${theme.patternCount} patterns</span>
56
+ </div>
57
+ </div>
58
+ <span class="list-row-arrow">›</span>
59
+ </a>`;
60
+ }
61
+
62
+ function renderDeckRow(deck: DeckWithPreviews): string {
63
+ const { summary } = deck;
64
+ const deckName = summary.relativePath
65
+ .replace(/^decks\//, "")
66
+ .replace(/\/deck\.yaml$/, "");
67
+ const themeName = escapeHtml(summary.meta.theme);
68
+ const approved = summary.approvedCount;
69
+ const total = summary.slideCount;
70
+
71
+ // First slide thumbnail
72
+ const firstSlide = deck.slidePreviews[0];
73
+ const thumbHtml = firstSlide
74
+ ? `<div class="deck-thumb"><div class="deck-thumb-inner">${firstSlide.html}</div></div>`
75
+ : `<div class="deck-thumb deck-thumb-empty"></div>`;
76
+
77
+ // Format mtime
78
+ const dateStr = deck.mtime
79
+ ? ` · ${new Date(deck.mtime).toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit" })}`
80
+ : "";
81
+
82
+ return `<a class="list-row list-row-link" href="/deck/${escapeHtml(deckName)}">
83
+ <div class="list-row-left">
84
+ ${thumbHtml}
85
+ <div class="list-row-text">
86
+ <span class="list-row-name">${escapeHtml(summary.meta.title)}</span>
87
+ <span class="list-row-meta">${themeName} · ${total} slides${dateStr}</span>
88
+ </div>
89
+ </div>
90
+ <div class="list-row-right">
91
+ <span class="list-row-arrow">›</span>
92
+ </div>
93
+ </a>`;
94
+ }
95
+
96
+ /**
97
+ * Renders the dashboard as a standalone HTML page.
98
+ */
99
+ export function renderDashboard(
100
+ decks: DeckWithPreviews[],
101
+ options: DashboardOptions = { mode: "static" },
102
+ themes: ThemeSummary[] = [],
103
+ ): string {
104
+ const themesHtml =
105
+ themes.length > 0
106
+ ? themes.map((t) => renderThemeRow(t)).join("\n")
107
+ : `<div class="empty-state">No themes found.</div>`;
108
+
109
+ const decksHtml =
110
+ decks.length > 0
111
+ ? decks.map((d) => renderDeckRow(d)).join("\n")
112
+ : `<div class="empty-state">No decks found. Create a deck.yaml to get started.</div>`;
113
+
114
+ return `<!DOCTYPE html>
115
+ <html lang="ja">
116
+ <head>
117
+ <meta charset="utf-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1">
119
+ <title>DeckSpec</title>
120
+ <style>
121
+ ${options.themeCSS ?? ""}
122
+ ${dashboardCSS}
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div class="dashboard">
127
+ <h1 class="page-title">DeckSpec</h1>
128
+
129
+ <section class="section">
130
+ <h2 class="section-title">Themes</h2>
131
+ <div class="list-group">${themesHtml}</div>
132
+ </section>
133
+
134
+ <section class="section">
135
+ <h2 class="section-title">Decks</h2>
136
+ <div class="list-group">${decksHtml}</div>
137
+ </section>
138
+ </div>
139
+ <script>
140
+ ${dashboardJS}
141
+ </script>
142
+ </body>
143
+ </html>`;
144
+ }