pi-design-deck 0.1.1 → 0.3.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.
Files changed (31) hide show
  1. package/README.md +54 -13
  2. package/deck-schema.ts +30 -0
  3. package/deck-server.ts +76 -19
  4. package/export-html.ts +329 -0
  5. package/form/css/controls.css +171 -0
  6. package/form/css/layout.css +56 -0
  7. package/form/css/preview.css +60 -6
  8. package/form/deck.html +2 -0
  9. package/form/js/deck-core.js +60 -2
  10. package/form/js/deck-interact.js +63 -19
  11. package/form/js/deck-render.js +95 -6
  12. package/form/js/deck-session.js +140 -27
  13. package/generate-prompts.ts +18 -12
  14. package/index.ts +364 -66
  15. package/package.json +2 -1
  16. package/prompts/deck-discover.md +3 -1
  17. package/prompts/deck-plan.md +3 -1
  18. package/prompts/deck.md +3 -1
  19. package/skills/design-deck/SKILL.md +44 -8
  20. package/skills/design-deck/references/component-gallery/INDEX.md +88 -0
  21. package/skills/design-deck/references/component-gallery/LOOKUP.md +592 -0
  22. package/skills/design-deck/references/component-gallery/components/INDEX.md +106 -0
  23. package/skills/design-deck/references/component-gallery/components/actions.md +354 -0
  24. package/skills/design-deck/references/component-gallery/components/data-display.md +812 -0
  25. package/skills/design-deck/references/component-gallery/components/feedback.md +513 -0
  26. package/skills/design-deck/references/component-gallery/components/inputs.md +921 -0
  27. package/skills/design-deck/references/component-gallery/components/layout.md +167 -0
  28. package/skills/design-deck/references/component-gallery/components/navigation.md +350 -0
  29. package/skills/design-deck/references/component-gallery/components/overlays.md +208 -0
  30. package/skills/design-deck/references/component-gallery/components/utilities.md +29 -0
  31. package/skills/design-deck/references/component-gallery/components.md +1383 -0
package/export-html.ts ADDED
@@ -0,0 +1,329 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, extname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { deriveDeckStatusFromFolderName, type DeckOption, type DeckSlide, type PreviewBlock, type SavedDeckData } from "./deck-schema.js";
5
+
6
+ const FORM_DIR = join(dirname(fileURLToPath(import.meta.url)), "form");
7
+ const CSS_FILES = ["variables", "layout", "preview", "controls"];
8
+ const EMBEDDED_CSS = CSS_FILES
9
+ .map((name) => readFileSync(join(FORM_DIR, "css", `${name}.css`), "utf-8"))
10
+ .join("\n");
11
+
12
+ const GOOGLE_FONTS_LINK = "https://fonts.googleapis.com/css2?family=Albert+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Fira+Code:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&family=Space+Mono:wght@400;500;600&display=swap";
13
+
14
+ const IMAGE_MIME_TYPES: Record<string, string> = {
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".svg": "image/svg+xml",
21
+ ".avif": "image/avif",
22
+ };
23
+
24
+ const EXPORT_CSS = `
25
+ body {
26
+ margin: 0;
27
+ background: var(--dk-bg);
28
+ color: var(--dk-text);
29
+ }
30
+
31
+ .deck {
32
+ min-height: 100vh;
33
+ }
34
+
35
+ .deck-header {
36
+ position: static;
37
+ }
38
+
39
+ .progress,
40
+ .deck-footer,
41
+ .deck-loading,
42
+ .confirm-bar,
43
+ .deck-close-overlay,
44
+ .save-toast,
45
+ .model-bar,
46
+ .gen-bar {
47
+ display: none !important;
48
+ }
49
+
50
+ .slides-wrap {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 24px;
54
+ padding: 24px;
55
+ }
56
+
57
+ .slide {
58
+ display: block !important;
59
+ opacity: 1 !important;
60
+ transform: none !important;
61
+ max-width: 1400px;
62
+ margin: 0 auto;
63
+ }
64
+
65
+ .option {
66
+ cursor: default;
67
+ }
68
+
69
+ .option:hover {
70
+ transform: none;
71
+ }
72
+
73
+ .export-meta {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ gap: 10px;
77
+ margin-top: 12px;
78
+ }
79
+
80
+ .export-chip {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 6px;
84
+ padding: 6px 10px;
85
+ border-radius: 999px;
86
+ border: 1px solid rgba(var(--dk-ink),0.12);
87
+ background: rgba(var(--dk-ink),0.06);
88
+ font: 11px var(--dk-font-mono);
89
+ color: var(--dk-text-secondary);
90
+ }
91
+
92
+ .export-chip-label {
93
+ color: var(--dk-text-hint);
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.08em;
96
+ }
97
+
98
+ .export-selected-badge {
99
+ margin-left: auto;
100
+ padding: 2px 8px;
101
+ border-radius: 999px;
102
+ background: rgba(52,211,153,0.12);
103
+ color: var(--dk-status-success);
104
+ font: 10px var(--dk-font-mono);
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.08em;
107
+ }
108
+
109
+ .export-final-notes {
110
+ max-width: 1400px;
111
+ margin: 0 auto 24px;
112
+ }
113
+
114
+ .export-final-notes-body {
115
+ margin-top: 12px;
116
+ padding: 16px 18px;
117
+ border-radius: 12px;
118
+ background: rgba(var(--dk-ink),0.06);
119
+ border: 1px solid rgba(var(--dk-ink),0.1);
120
+ white-space: pre-wrap;
121
+ line-height: 1.6;
122
+ }
123
+
124
+ .summary-notes {
125
+ margin-top: 12px;
126
+ }
127
+
128
+ .summary-notes-label {
129
+ font-weight: 600;
130
+ }
131
+
132
+ .preview-block-mermaid {
133
+ overflow: hidden;
134
+ }
135
+
136
+ .preview-block-mermaid svg {
137
+ max-width: 100%;
138
+ height: auto;
139
+ }
140
+
141
+ @media (max-width: 900px) {
142
+ .slides-wrap {
143
+ padding: 16px;
144
+ }
145
+
146
+ .export-meta {
147
+ gap: 8px;
148
+ }
149
+ }
150
+ `;
151
+
152
+ function escapeHtml(value: string): string {
153
+ return value
154
+ .replace(/&/g, "&amp;")
155
+ .replace(/</g, "&lt;")
156
+ .replace(/>/g, "&gt;")
157
+ .replace(/"/g, "&quot;");
158
+ }
159
+
160
+ function optionCountClass(count: number, columns?: 1 | 2 | 3): string {
161
+ if (columns === 1) return "cols-1";
162
+ if (columns && count >= columns && count % columns !== 1) {
163
+ return `cols-${columns}`;
164
+ }
165
+ if (count <= 1) return "cols-1";
166
+ if (count === 2 || count === 4) return "cols-2";
167
+ return "cols-3";
168
+ }
169
+
170
+ function formatTimestamp(value?: string): string {
171
+ if (!value) return "";
172
+ try {
173
+ return new Date(value).toLocaleString();
174
+ } catch {
175
+ return value;
176
+ }
177
+ }
178
+
179
+ function inlineImageSrc(src: string, baseDir: string): string {
180
+ if (/^(data:|https?:|file:|blob:)/i.test(src)) {
181
+ return src;
182
+ }
183
+
184
+ const absolutePath = resolve(baseDir, src);
185
+ if (!existsSync(absolutePath)) {
186
+ return src;
187
+ }
188
+
189
+ const mimeType = IMAGE_MIME_TYPES[extname(absolutePath).toLowerCase()] || "application/octet-stream";
190
+ const data = readFileSync(absolutePath).toString("base64");
191
+ return `data:${mimeType};base64,${data}`;
192
+ }
193
+
194
+ function renderPreviewBlocks(blocks: PreviewBlock[], baseDir: string): string {
195
+ return blocks.map((block) => {
196
+ if (block.type === "html") {
197
+ return `<div class="preview-block preview-block-html">${block.content}</div>`;
198
+ }
199
+ if (block.type === "mermaid") {
200
+ return `<div class="preview-block preview-block-mermaid"><div class="mermaid">${escapeHtml(block.content)}</div></div>`;
201
+ }
202
+ if (block.type === "code") {
203
+ return `<div class="preview-block preview-block-code"><pre><code class="language-${escapeHtml(block.lang)}">${escapeHtml(block.code)}</code></pre></div>`;
204
+ }
205
+ const imageSrc = inlineImageSrc(block.src, baseDir);
206
+ return `<div class="preview-block preview-block-image"><img src="${escapeHtml(imageSrc)}" alt="${escapeHtml(block.alt)}" loading="lazy">${block.caption ? `<div class="preview-block-caption">${escapeHtml(block.caption)}</div>` : ""}</div>`;
207
+ }).join("");
208
+ }
209
+
210
+ function renderOption(option: DeckOption, slideId: string, selectedLabel: string | undefined, note: string | undefined, baseDir: string): string {
211
+ const isSelected = selectedLabel === option.label;
212
+ const previewContent = Array.isArray(option.previewBlocks) && option.previewBlocks.length > 0
213
+ ? renderPreviewBlocks(option.previewBlocks, baseDir)
214
+ : option.previewHtml || "";
215
+
216
+ return `
217
+ <article class="option${isSelected ? " selected" : ""}" role="presentation" data-slide-id="${escapeHtml(slideId)}">
218
+ <div class="option-check">&#10003;</div>
219
+ <div class="option-header">
220
+ <span class="option-radio"></span>
221
+ <span class="option-label">${escapeHtml(option.label)}</span>
222
+ ${isSelected ? `<span class="export-selected-badge">Selected</span>` : option.recommended ? `<span class="rec-badge">Recommended</span>` : ""}
223
+ </div>
224
+ <div class="preview${Array.isArray(option.previewBlocks) && option.previewBlocks.length > 0 ? " preview-blocks" : ""}">${previewContent}</div>
225
+ <div class="option-footer">
226
+ ${option.aside ? `<div class="option-aside">${escapeHtml(option.aside).replace(/\\n/g, "<br>").replace(/\n/g, "<br>")}</div>` : ""}
227
+ ${isSelected && note ? `<div class="summary-notes"><span class="summary-notes-label">Your notes:</span> ${escapeHtml(note)}</div>` : ""}
228
+ </div>
229
+ </article>
230
+ `;
231
+ }
232
+
233
+ function renderSlide(slide: DeckSlide, savedDeck: SavedDeckData, slideIndex: number, baseDir: string): string {
234
+ const selectedLabel = savedDeck.selections[slide.id];
235
+ const note = savedDeck.notes?.[slide.id];
236
+ return `
237
+ <section class="slide active" data-id="${escapeHtml(slide.id)}" data-slide="${slideIndex}">
238
+ <span class="slide-step">${slideIndex + 1} / ${savedDeck.config.slides.length}</span>
239
+ <h2>${escapeHtml(slide.title)}</h2>
240
+ ${slide.context ? `<p class="slide-context">${escapeHtml(slide.context)}</p>` : ""}
241
+ <div class="options ${optionCountClass(slide.options.length, slide.columns)}">
242
+ ${slide.options.map((option) => renderOption(option, slide.id, selectedLabel, note, baseDir)).join("")}
243
+ </div>
244
+ </section>
245
+ `;
246
+ }
247
+
248
+ function renderMetaChip(label: string, value: string): string {
249
+ return `<div class="export-chip"><span class="export-chip-label">${escapeHtml(label)}</span><span>${escapeHtml(value)}</span></div>`;
250
+ }
251
+
252
+ export function buildStandaloneDeckHtml(deckPath: string, savedDeck: SavedDeckData): string {
253
+ const baseDir = dirname(deckPath);
254
+ const deckId = savedDeck.id || basename(baseDir) || "deck";
255
+ const status = savedDeck.status || deriveDeckStatusFromFolderName(deckId);
256
+ const hasMermaid = savedDeck.config.slides.some((slide) =>
257
+ slide.options.some((option) =>
258
+ Array.isArray(option.previewBlocks) && option.previewBlocks.some((block) => block.type === "mermaid")
259
+ )
260
+ );
261
+ const title = savedDeck.config.title || "Design Deck";
262
+ const metaChips = [
263
+ renderMetaChip("deck", deckId),
264
+ renderMetaChip("status", status),
265
+ renderMetaChip("saved", formatTimestamp(savedDeck.savedAt)),
266
+ renderMetaChip("modified", formatTimestamp(savedDeck.modifiedAt || savedDeck.savedAt)),
267
+ ];
268
+ if (savedDeck.savedFrom?.cwd) {
269
+ metaChips.push(renderMetaChip("cwd", savedDeck.savedFrom.cwd));
270
+ }
271
+ if (savedDeck.savedFrom?.branch) {
272
+ metaChips.push(renderMetaChip("branch", savedDeck.savedFrom.branch));
273
+ }
274
+
275
+ const mermaidScript = hasMermaid
276
+ ? `
277
+ <script type="module">
278
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
279
+ mermaid.initialize({
280
+ startOnLoad: false,
281
+ theme: 'base',
282
+ themeVariables: {
283
+ background: '#1a1a22',
284
+ primaryColor: '#2a3f3d',
285
+ primaryTextColor: '#e0e0e0',
286
+ primaryBorderColor: '#8abeb7',
287
+ lineColor: '#555555',
288
+ secondaryColor: '#1e2a2e',
289
+ tertiaryColor: '#1a1a22',
290
+ noteBkgColor: '#222230',
291
+ noteTextColor: '#b0b0b0',
292
+ fontSize: '13px',
293
+ fontFamily: "'Space Mono', monospace",
294
+ }
295
+ });
296
+ mermaid.run({ querySelector: '.mermaid' });
297
+ </script>`
298
+ : "";
299
+
300
+ return `<!DOCTYPE html>
301
+ <html lang="en" data-theme="dark">
302
+ <head>
303
+ <meta charset="UTF-8">
304
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
305
+ <meta name="theme-color" content="#18181e">
306
+ <title>${escapeHtml(title)} - Export</title>
307
+ <link rel="preconnect" href="https://fonts.googleapis.com">
308
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
309
+ <link href="${GOOGLE_FONTS_LINK}" rel="stylesheet">
310
+ <style>${EMBEDDED_CSS}
311
+ ${EXPORT_CSS}</style>
312
+ </head>
313
+ <body>
314
+ <div class="deck">
315
+ <header class="deck-header">
316
+ <div class="deck-meta">
317
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
318
+ <div class="export-meta">${metaChips.join("")}</div>
319
+ </div>
320
+ </header>
321
+ <div class="slides-wrap">
322
+ ${savedDeck.config.slides.map((slide, index) => renderSlide(slide, savedDeck, index, baseDir)).join("")}
323
+ ${savedDeck.finalNotes ? `<section class="slide active export-final-notes"><span class="slide-step">Notes</span><h2>Additional Instructions</h2><div class="export-final-notes-body">${escapeHtml(savedDeck.finalNotes)}</div></section>` : ""}
324
+ </div>
325
+ </div>
326
+ ${mermaidScript}
327
+ </body>
328
+ </html>`;
329
+ }
@@ -27,6 +27,30 @@
27
27
  }
28
28
  .btn-gen-more:disabled { opacity: 0.3; cursor: not-allowed; }
29
29
 
30
+ .gen-group { display: flex; align-items: center; gap: 6px; }
31
+
32
+ .gen-count {
33
+ appearance: none;
34
+ -webkit-appearance: none;
35
+ background: rgba(var(--dk-ink),0.12);
36
+ border: 1px solid rgba(var(--dk-ink),0.15);
37
+ border-radius: 4px;
38
+ padding: 4px 8px;
39
+ font-size: 12px;
40
+ font-weight: 600;
41
+ color: var(--dk-accent-text);
42
+ cursor: pointer;
43
+ text-align: center;
44
+ min-width: 32px;
45
+ }
46
+ .gen-count:focus { outline: none; border-color: rgba(138,190,183,0.4); background: rgba(138,190,183,0.1); }
47
+ .gen-count:disabled { opacity: 0.4; cursor: not-allowed; }
48
+
49
+ .gen-count-label {
50
+ font-size: 12px;
51
+ color: var(--dk-text-dim);
52
+ }
53
+
30
54
  .btn-regen {
31
55
  display: inline-flex; align-items: center; gap: 6px;
32
56
  padding: 8px 14px; border-radius: 8px;
@@ -120,6 +144,102 @@
120
144
  }
121
145
  @keyframes skel-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
122
146
 
147
+ /* Regeneration overlay - covers options grid during regenerate-all */
148
+ .regen-overlay {
149
+ position: absolute;
150
+ inset: 0;
151
+ display: grid;
152
+ gap: 20px;
153
+ padding: 0;
154
+ z-index: 10;
155
+ animation: regen-fade-in 0.3s ease-out;
156
+ }
157
+ .regen-overlay.cols-3 { grid-template-columns: repeat(3, 1fr); }
158
+ .regen-overlay.cols-2 { grid-template-columns: repeat(2, 1fr); }
159
+ .regen-overlay.cols-1 { grid-template-columns: 1fr; }
160
+
161
+ @keyframes regen-fade-in {
162
+ from { opacity: 0; }
163
+ to { opacity: 1; }
164
+ }
165
+
166
+ .regen-skeleton {
167
+ border: 2px dashed rgba(138,190,183,0.15);
168
+ border-radius: 12px;
169
+ background: var(--dk-surface);
170
+ min-height: 280px;
171
+ position: relative;
172
+ overflow: hidden;
173
+ }
174
+ .regen-skeleton::before {
175
+ content: '';
176
+ position: absolute;
177
+ top: 0; left: 0; right: 0;
178
+ height: 44px;
179
+ background: linear-gradient(110deg, rgba(var(--dk-ink),0.03) 30%, rgba(var(--dk-ink),0.07) 50%, rgba(var(--dk-ink),0.03) 70%);
180
+ background-size: 200% 100%;
181
+ animation: skel-shimmer 1.5s ease-in-out infinite;
182
+ border-bottom: 1px solid rgba(var(--dk-ink),0.06);
183
+ }
184
+ .regen-skeleton::after {
185
+ content: '';
186
+ position: absolute;
187
+ top: 45px; left: 0; right: 0; bottom: 0;
188
+ background: linear-gradient(110deg, var(--dk-surface) 30%, var(--dk-surface-shimmer) 50%, var(--dk-surface) 70%);
189
+ background-size: 200% 100%;
190
+ animation: skel-shimmer 1.5s ease-in-out infinite;
191
+ animation-delay: 0.1s;
192
+ }
193
+ .regen-skeleton:nth-child(2)::before,
194
+ .regen-skeleton:nth-child(2)::after { animation-delay: 0.2s; }
195
+ .regen-skeleton:nth-child(3)::before,
196
+ .regen-skeleton:nth-child(3)::after { animation-delay: 0.3s; }
197
+ .regen-skeleton:nth-child(4)::before,
198
+ .regen-skeleton:nth-child(4)::after { animation-delay: 0.4s; }
199
+
200
+ /* Regeneration status text */
201
+ .regen-status {
202
+ position: absolute;
203
+ bottom: 16px; left: 50%;
204
+ transform: translateX(-50%);
205
+ font-family: var(--dk-font-mono);
206
+ font-size: 11px;
207
+ color: var(--dk-accent-text);
208
+ letter-spacing: 0.3px;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 8px;
212
+ }
213
+ .regen-status::before {
214
+ content: '';
215
+ width: 14px; height: 14px;
216
+ border: 2px solid rgba(138,190,183,0.2);
217
+ border-top-color: var(--dk-accent);
218
+ border-radius: 50%;
219
+ animation: spin 0.8s linear infinite;
220
+ }
221
+
222
+ /* Options grid regenerating state */
223
+ .options.regenerating {
224
+ opacity: 0;
225
+ pointer-events: none;
226
+ transition: opacity 0.25s ease-out;
227
+ }
228
+
229
+ /* Regenerated options entry animation */
230
+ .option-regenerated {
231
+ animation: opt-regen-appear 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
232
+ opacity: 0;
233
+ }
234
+ .option-regenerated:nth-child(1) { animation-delay: 0.05s; }
235
+ .option-regenerated:nth-child(2) { animation-delay: 0.1s; }
236
+ .option-regenerated:nth-child(3) { animation-delay: 0.15s; }
237
+ .option-regenerated:nth-child(4) { animation-delay: 0.2s; }
238
+ @keyframes opt-regen-appear {
239
+ 0% { opacity: 0; transform: translateY(12px) scale(0.96); }
240
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
241
+ }
242
+
123
243
  /* ─────────────────────────────────────────────────────────────
124
244
  MODEL BAR
125
245
  ───────────────────────────────────────────────────────────── */
@@ -303,11 +423,61 @@
303
423
  .btn-nav:focus-visible,
304
424
  .btn-gen-more:focus-visible,
305
425
  .btn-generate:focus-visible,
426
+ .deck-save-btn:focus-visible,
306
427
  .confirm-bar-btn:focus-visible {
307
428
  outline: 2px solid var(--dk-accent);
308
429
  outline-offset: 2px;
309
430
  }
310
431
 
432
+ /* ─────────────────────────────────────────────────────────────
433
+ SAVE CONTROLS
434
+ ───────────────────────────────────────────────────────────── */
435
+
436
+ .deck-save-btn {
437
+ display: inline-flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ padding: 6px 12px;
441
+ border-radius: 999px;
442
+ border: 1px solid rgba(var(--dk-ink),0.16);
443
+ background: rgba(var(--dk-ink),0.06);
444
+ color: var(--dk-text-secondary);
445
+ font: 11px var(--dk-font-mono);
446
+ cursor: pointer;
447
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
448
+ }
449
+
450
+ .deck-save-btn:hover {
451
+ border-color: rgba(138,190,183,0.28);
452
+ color: var(--dk-accent-text);
453
+ background: rgba(138,190,183,0.08);
454
+ }
455
+
456
+ .deck-save-btn.dirty {
457
+ border-color: rgba(251,191,36,0.28);
458
+ color: var(--dk-status-warn);
459
+ background: rgba(251,191,36,0.08);
460
+ }
461
+
462
+ .deck-save-btn:disabled {
463
+ opacity: 0.45;
464
+ cursor: not-allowed;
465
+ }
466
+
467
+ .deck-save-status {
468
+ font: 11px var(--dk-font-mono);
469
+ color: var(--dk-text-hint);
470
+ white-space: nowrap;
471
+ }
472
+
473
+ .deck-save-status.saved {
474
+ color: var(--dk-status-success);
475
+ }
476
+
477
+ .deck-save-status.dirty {
478
+ color: var(--dk-status-warn);
479
+ }
480
+
311
481
  .deck {
312
482
  touch-action: manipulation;
313
483
  overscroll-behavior: contain;
@@ -331,6 +501,7 @@
331
501
  .slide { padding: 20px 16px 16px; }
332
502
  .summary-grid { grid-template-columns: 1fr; }
333
503
  .pv-layout-sidebar { grid-template-columns: 120px 1fr; }
504
+ .deck-save-status { display: none; }
334
505
  }
335
506
 
336
507
  /* ─────────────────────────────────────────────────────────────
@@ -268,6 +268,21 @@ body {
268
268
  color: var(--dk-text-secondary); font-family: var(--dk-font-mono);
269
269
  font-style: italic;
270
270
  }
271
+ .summary-notes {
272
+ margin-top: 10px; padding: 8px 10px;
273
+ font-size: 11px; line-height: 1.5;
274
+ color: var(--dk-accent-text);
275
+ background: rgba(138,190,183,0.08);
276
+ border-radius: 4px;
277
+ border-left: 2px solid rgba(138,190,183,0.3);
278
+ }
279
+ .summary-notes-label {
280
+ font-weight: 600;
281
+ font-size: 9px;
282
+ text-transform: uppercase;
283
+ letter-spacing: 0.3px;
284
+ opacity: 0.8;
285
+ }
271
286
  .summary-preview {
272
287
  margin-top: 12px; border-radius: 6px; overflow: hidden;
273
288
  border: 1px solid rgba(var(--dk-ink),0.12); max-height: 120px;
@@ -288,6 +303,47 @@ body {
288
303
  }
289
304
  .summary-mermaid svg { max-width: 100%; max-height: 100%; }
290
305
 
306
+ /* Final notes input on summary slide */
307
+ .final-notes {
308
+ width: 100%;
309
+ max-width: 560px;
310
+ margin-top: 24px;
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 8px;
314
+ }
315
+ .final-notes-label {
316
+ font-size: 11px;
317
+ font-weight: 500;
318
+ color: var(--dk-text-dim);
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.5px;
321
+ }
322
+ .final-notes-input {
323
+ width: 100%;
324
+ padding: 12px 14px;
325
+ border-radius: 8px;
326
+ border: 1px solid rgba(var(--dk-ink),0.12);
327
+ background: rgba(var(--dk-ink),0.04);
328
+ font-size: 13px;
329
+ font-family: inherit;
330
+ color: var(--dk-text);
331
+ resize: vertical;
332
+ min-height: 60px;
333
+ max-height: 120px;
334
+ transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
335
+ }
336
+ .final-notes-input::placeholder {
337
+ color: var(--dk-text-placeholder);
338
+ font-style: italic;
339
+ }
340
+ .final-notes-input:focus {
341
+ outline: none;
342
+ border-color: rgba(138,190,183,0.4);
343
+ background: rgba(var(--dk-ink),0.06);
344
+ box-shadow: 0 0 0 3px rgba(138,190,183,0.1);
345
+ }
346
+
291
347
  .btn-generate {
292
348
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
293
349
  padding: 14px 48px; border-radius: 10px; font-size: 14px; font-weight: 600;
@@ -59,17 +59,71 @@ code[class*="language-"], pre[class*="language-"] {
59
59
  OPTION ASIDE
60
60
  ───────────────────────────────────────────────────────────── */
61
61
 
62
- .option-aside {
63
- margin: 0 14px 14px;
62
+ /* Option footer - contains aside + notes */
63
+ .option-footer {
64
64
  padding: 12px 14px;
65
- border-left: 2px solid rgba(138,190,183,0.25);
66
- border-radius: 0 6px 6px 0;
67
- background: rgba(var(--dk-ink),0.04);
68
- font-size: 12px; line-height: 1.7;
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 8px;
68
+ }
69
+
70
+ .option-aside {
71
+ padding: 8px 10px;
72
+ border-left: 2px solid rgba(138,190,183,0.2);
73
+ background: rgba(var(--dk-ink),0.02);
74
+ font-size: 11px; line-height: 1.55;
69
75
  color: var(--dk-text-aside);
70
76
  font-family: var(--dk-font-mono);
71
77
  font-style: italic;
72
78
  letter-spacing: -0.01em;
79
+ border-radius: 0 4px 4px 0;
80
+ }
81
+
82
+ /* Option notes input */
83
+ .option-notes {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 8px;
87
+ }
88
+ .option-notes-label {
89
+ font-size: 9px;
90
+ font-weight: 500;
91
+ color: var(--dk-text-hint);
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.3px;
94
+ white-space: nowrap;
95
+ opacity: 0.6;
96
+ }
97
+ .option-notes-input {
98
+ flex: 1;
99
+ padding: 5px 8px;
100
+ border-radius: 4px;
101
+ border: 1px dashed rgba(var(--dk-ink),0.12);
102
+ background: transparent;
103
+ font-size: 11px;
104
+ font-family: var(--dk-font-mono);
105
+ color: var(--dk-text);
106
+ resize: none;
107
+ min-height: 24px;
108
+ transition: border-color 0.2s, background 0.2s;
109
+ }
110
+ .option-notes-input::placeholder {
111
+ color: var(--dk-text-placeholder);
112
+ opacity: 0.4;
113
+ font-size: 10px;
114
+ }
115
+ .option-notes-input:focus {
116
+ outline: none;
117
+ border-style: solid;
118
+ border-color: rgba(138,190,183,0.3);
119
+ background: rgba(var(--dk-ink),0.02);
120
+ }
121
+ .option.selected .option-notes-input {
122
+ border-color: rgba(138,190,183,0.2);
123
+ }
124
+ .option-notes-input:disabled {
125
+ opacity: 0.4;
126
+ cursor: not-allowed;
73
127
  }
74
128
 
75
129
  /* ─────────────────────────────────────────────────────────────
package/form/deck.html CHANGED
@@ -45,6 +45,8 @@
45
45
  <span class="deck-key"><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd> Select</span>
46
46
  <span class="deck-key"><kbd>Enter</kbd> Confirm</span>
47
47
  <span class="deck-key"><kbd class="mod-key">⌘</kbd><kbd>S</kbd> Save</span>
48
+ <button class="deck-save-btn" id="btn-save" type="button">Save</button>
49
+ <span class="deck-save-status" id="save-status" role="status" aria-live="polite">No unsaved changes</span>
48
50
  <span class="deck-key hidden" id="theme-shortcut"></span>
49
51
  </div>
50
52
  <button class="btn-nav primary" id="btn-next" type="button">Next &rarr;</button>