pi-design-deck 0.2.0 → 0.3.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 +51 -13
- package/deck-schema.ts +33 -3
- package/deck-server.ts +64 -10
- package/export-html.ts +329 -0
- package/form/css/controls.css +152 -16
- package/form/css/layout.css +7 -0
- package/form/deck.html +16 -0
- package/form/js/deck-core.js +118 -0
- package/form/js/deck-interact.js +30 -12
- package/form/js/deck-render.js +2 -0
- package/form/js/deck-session.js +31 -12
- package/generate-prompts.ts +8 -10
- package/index.ts +318 -42
- package/package.json +2 -1
- package/prompts/deck-discover.md +3 -1
- package/prompts/deck-plan.md +3 -1
- package/prompts/deck.md +3 -1
- package/skills/design-deck/SKILL.md +45 -9
- package/skills/design-deck/references/component-gallery/INDEX.md +88 -0
- package/skills/design-deck/references/component-gallery/LOOKUP.md +592 -0
- package/skills/design-deck/references/component-gallery/components/INDEX.md +106 -0
- package/skills/design-deck/references/component-gallery/components/actions.md +354 -0
- package/skills/design-deck/references/component-gallery/components/data-display.md +812 -0
- package/skills/design-deck/references/component-gallery/components/feedback.md +513 -0
- package/skills/design-deck/references/component-gallery/components/inputs.md +921 -0
- package/skills/design-deck/references/component-gallery/components/layout.md +167 -0
- package/skills/design-deck/references/component-gallery/components/navigation.md +350 -0
- package/skills/design-deck/references/component-gallery/components/overlays.md +208 -0
- package/skills/design-deck/references/component-gallery/components/utilities.md +29 -0
- 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, "&")
|
|
155
|
+
.replace(/</g, "<")
|
|
156
|
+
.replace(/>/g, ">")
|
|
157
|
+
.replace(/"/g, """);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function optionCountClass(count: number, columns?: 1 | 2 | 3 | 4): 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">✓</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
|
+
}
|
package/form/css/controls.css
CHANGED
|
@@ -154,10 +154,17 @@
|
|
|
154
154
|
z-index: 10;
|
|
155
155
|
animation: regen-fade-in 0.3s ease-out;
|
|
156
156
|
}
|
|
157
|
+
.regen-overlay.cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
157
158
|
.regen-overlay.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
158
159
|
.regen-overlay.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
159
160
|
.regen-overlay.cols-1 { grid-template-columns: 1fr; }
|
|
160
161
|
|
|
162
|
+
/* Regen overlay respects layout override */
|
|
163
|
+
.deck[data-layout="1"] .regen-overlay { grid-template-columns: 1fr; }
|
|
164
|
+
.deck[data-layout="2"] .regen-overlay { grid-template-columns: repeat(2, 1fr); }
|
|
165
|
+
.deck[data-layout="3"] .regen-overlay { grid-template-columns: repeat(3, 1fr); }
|
|
166
|
+
.deck[data-layout="4"] .regen-overlay { grid-template-columns: repeat(4, 1fr); }
|
|
167
|
+
|
|
161
168
|
@keyframes regen-fade-in {
|
|
162
169
|
from { opacity: 0; }
|
|
163
170
|
to { opacity: 1; }
|
|
@@ -197,26 +204,60 @@
|
|
|
197
204
|
.regen-skeleton:nth-child(4)::before,
|
|
198
205
|
.regen-skeleton:nth-child(4)::after { animation-delay: 0.4s; }
|
|
199
206
|
|
|
200
|
-
/*
|
|
201
|
-
|
|
207
|
+
/* ─────────────────────────────────────────────────────────────
|
|
208
|
+
LOADING SPINNER
|
|
209
|
+
───────────────────────────────────────────────────────────── */
|
|
210
|
+
|
|
211
|
+
.spinner {
|
|
212
|
+
width: 28px;
|
|
213
|
+
height: 28px;
|
|
214
|
+
border: 2.5px solid rgba(138,190,183,0.12);
|
|
215
|
+
border-top-color: var(--dk-accent);
|
|
216
|
+
border-radius: 50%;
|
|
217
|
+
animation: spin 0.7s linear infinite;
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Spinner inside skeleton cards */
|
|
222
|
+
.option-skeleton .spinner {
|
|
202
223
|
position: absolute;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
top: 50%;
|
|
225
|
+
left: 50%;
|
|
226
|
+
transform: translate(-50%, -50%);
|
|
227
|
+
z-index: 2;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Centered loading overlay for regenerate-all */
|
|
231
|
+
.regen-center {
|
|
232
|
+
position: absolute;
|
|
233
|
+
inset: 0;
|
|
209
234
|
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
210
236
|
align-items: center;
|
|
211
|
-
|
|
237
|
+
justify-content: center;
|
|
238
|
+
gap: 14px;
|
|
239
|
+
z-index: 5;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
background: rgba(24,24,30,0.5);
|
|
242
|
+
backdrop-filter: blur(2px);
|
|
243
|
+
border-radius: 12px;
|
|
212
244
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
245
|
+
|
|
246
|
+
[data-theme="light"] .regen-center {
|
|
247
|
+
background: rgba(248,248,248,0.6);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.regen-center .spinner {
|
|
251
|
+
width: 36px;
|
|
252
|
+
height: 36px;
|
|
253
|
+
border-width: 3px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.regen-center-text {
|
|
257
|
+
font-family: var(--dk-font-mono);
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
color: var(--dk-accent-text);
|
|
260
|
+
letter-spacing: 0.3px;
|
|
220
261
|
}
|
|
221
262
|
|
|
222
263
|
/* Options grid regenerating state */
|
|
@@ -240,6 +281,49 @@
|
|
|
240
281
|
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
/* ─────────────────────────────────────────────────────────────
|
|
285
|
+
LAYOUT TOGGLE
|
|
286
|
+
───────────────────────────────────────────────────────────── */
|
|
287
|
+
|
|
288
|
+
.layout-toggle {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 1px;
|
|
292
|
+
background: rgba(var(--dk-ink),0.06);
|
|
293
|
+
border-radius: 6px;
|
|
294
|
+
padding: 2px;
|
|
295
|
+
margin-right: 8px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.layout-btn {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
width: 26px;
|
|
303
|
+
height: 22px;
|
|
304
|
+
border: none;
|
|
305
|
+
background: transparent;
|
|
306
|
+
border-radius: 4px;
|
|
307
|
+
color: var(--dk-text-hint);
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
transition: background 0.15s, color 0.15s;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.layout-btn:hover {
|
|
313
|
+
background: rgba(var(--dk-ink),0.08);
|
|
314
|
+
color: var(--dk-text);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.layout-btn.active {
|
|
318
|
+
background: rgba(138,190,183,0.15);
|
|
319
|
+
color: var(--dk-accent-text);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.layout-btn svg {
|
|
323
|
+
width: 12px;
|
|
324
|
+
height: 12px;
|
|
325
|
+
}
|
|
326
|
+
|
|
243
327
|
/* ─────────────────────────────────────────────────────────────
|
|
244
328
|
MODEL BAR
|
|
245
329
|
───────────────────────────────────────────────────────────── */
|
|
@@ -423,11 +507,62 @@
|
|
|
423
507
|
.btn-nav:focus-visible,
|
|
424
508
|
.btn-gen-more:focus-visible,
|
|
425
509
|
.btn-generate:focus-visible,
|
|
510
|
+
.deck-save-btn:focus-visible,
|
|
426
511
|
.confirm-bar-btn:focus-visible {
|
|
427
512
|
outline: 2px solid var(--dk-accent);
|
|
428
513
|
outline-offset: 2px;
|
|
429
514
|
}
|
|
430
515
|
|
|
516
|
+
/* ─────────────────────────────────────────────────────────────
|
|
517
|
+
SAVE CONTROLS
|
|
518
|
+
───────────────────────────────────────────────────────────── */
|
|
519
|
+
|
|
520
|
+
.deck-save-btn {
|
|
521
|
+
display: inline-flex;
|
|
522
|
+
align-items: center;
|
|
523
|
+
justify-content: center;
|
|
524
|
+
padding: 6px 12px;
|
|
525
|
+
border-radius: 999px;
|
|
526
|
+
border: 1px solid rgba(var(--dk-ink),0.16);
|
|
527
|
+
background: rgba(var(--dk-ink),0.06);
|
|
528
|
+
color: var(--dk-text-secondary);
|
|
529
|
+
font: 11px var(--dk-font-mono);
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.deck-save-btn:hover {
|
|
535
|
+
border-color: rgba(138,190,183,0.28);
|
|
536
|
+
color: var(--dk-accent-text);
|
|
537
|
+
background: rgba(138,190,183,0.08);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.deck-save-btn.dirty {
|
|
541
|
+
border-color: rgba(251,191,36,0.28);
|
|
542
|
+
color: var(--dk-status-warn);
|
|
543
|
+
background: rgba(251,191,36,0.08);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.deck-save-btn:disabled {
|
|
547
|
+
opacity: 0.45;
|
|
548
|
+
cursor: not-allowed;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.deck-save-status {
|
|
552
|
+
font: 11px var(--dk-font-mono);
|
|
553
|
+
color: var(--dk-text-hint);
|
|
554
|
+
white-space: nowrap;
|
|
555
|
+
padding: 6px 12px;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.deck-save-status.saved {
|
|
559
|
+
color: var(--dk-status-success);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.deck-save-status.dirty {
|
|
563
|
+
color: var(--dk-status-warn);
|
|
564
|
+
}
|
|
565
|
+
|
|
431
566
|
.deck {
|
|
432
567
|
touch-action: manipulation;
|
|
433
568
|
overscroll-behavior: contain;
|
|
@@ -451,6 +586,7 @@
|
|
|
451
586
|
.slide { padding: 20px 16px 16px; }
|
|
452
587
|
.summary-grid { grid-template-columns: 1fr; }
|
|
453
588
|
.pv-layout-sidebar { grid-template-columns: 120px 1fr; }
|
|
589
|
+
.deck-save-status { display: none; }
|
|
454
590
|
}
|
|
455
591
|
|
|
456
592
|
/* ─────────────────────────────────────────────────────────────
|
package/form/css/layout.css
CHANGED
|
@@ -148,10 +148,17 @@ body {
|
|
|
148
148
|
───────────────────────────────────────────────────────────── */
|
|
149
149
|
|
|
150
150
|
.options { display: grid; column-gap: 20px; row-gap: 0; counter-reset: opt; }
|
|
151
|
+
.options.cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
151
152
|
.options.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
152
153
|
.options.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
153
154
|
.options.cols-1 { grid-template-columns: 1fr; }
|
|
154
155
|
|
|
156
|
+
/* Layout override - global toggle overrides per-slide classes */
|
|
157
|
+
.deck[data-layout="1"] .options { grid-template-columns: 1fr; }
|
|
158
|
+
.deck[data-layout="2"] .options { grid-template-columns: repeat(2, 1fr); }
|
|
159
|
+
.deck[data-layout="3"] .options { grid-template-columns: repeat(3, 1fr); }
|
|
160
|
+
.deck[data-layout="4"] .options { grid-template-columns: repeat(4, 1fr); }
|
|
161
|
+
|
|
155
162
|
.option {
|
|
156
163
|
border: 2px solid rgba(var(--dk-ink),0.12);
|
|
157
164
|
border-radius: 12px; overflow: hidden;
|
package/form/deck.html
CHANGED
|
@@ -44,7 +44,23 @@
|
|
|
44
44
|
<span class="deck-key"><kbd>←</kbd><kbd>→</kbd> Navigate</span>
|
|
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
|
+
<div class="layout-toggle" id="layout-toggle" role="toolbar" aria-label="Grid columns">
|
|
48
|
+
<button class="layout-btn" data-cols="1" type="button" title="1 column" aria-label="1 column" aria-pressed="false">
|
|
49
|
+
<svg viewBox="0 0 12 12"><rect x="3" y="1" width="6" height="10" rx="1" fill="currentColor"/></svg>
|
|
50
|
+
</button>
|
|
51
|
+
<button class="layout-btn" data-cols="2" type="button" title="2 columns" aria-label="2 columns" aria-pressed="false">
|
|
52
|
+
<svg viewBox="0 0 12 12"><rect x="1" y="1" width="4" height="10" rx="0.75" fill="currentColor"/><rect x="7" y="1" width="4" height="10" rx="0.75" fill="currentColor"/></svg>
|
|
53
|
+
</button>
|
|
54
|
+
<button class="layout-btn" data-cols="3" type="button" title="3 columns" aria-label="3 columns" aria-pressed="false">
|
|
55
|
+
<svg viewBox="0 0 12 12"><rect x="0.5" y="1" width="3" height="10" rx="0.5" fill="currentColor"/><rect x="4.5" y="1" width="3" height="10" rx="0.5" fill="currentColor"/><rect x="8.5" y="1" width="3" height="10" rx="0.5" fill="currentColor"/></svg>
|
|
56
|
+
</button>
|
|
57
|
+
<button class="layout-btn" data-cols="4" type="button" title="4 columns" aria-label="4 columns" aria-pressed="false">
|
|
58
|
+
<svg viewBox="0 0 12 12"><rect x="0" y="1" width="2.4" height="10" rx="0.5" fill="currentColor"/><rect x="3.2" y="1" width="2.4" height="10" rx="0.5" fill="currentColor"/><rect x="6.4" y="1" width="2.4" height="10" rx="0.5" fill="currentColor"/><rect x="9.6" y="1" width="2.4" height="10" rx="0.5" fill="currentColor"/></svg>
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
47
61
|
<span class="deck-key"><kbd class="mod-key">⌘</kbd><kbd>S</kbd> Save</span>
|
|
62
|
+
<button class="deck-save-btn" id="btn-save" type="button">Save</button>
|
|
63
|
+
<span class="deck-save-status" id="save-status" role="status" aria-live="polite">No unsaved changes</span>
|
|
48
64
|
<span class="deck-key hidden" id="theme-shortcut"></span>
|
|
49
65
|
</div>
|
|
50
66
|
<button class="btn-nav primary" id="btn-next" type="button">Next →</button>
|