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.
- package/README.md +54 -13
- package/deck-schema.ts +30 -0
- package/deck-server.ts +76 -19
- package/export-html.ts +329 -0
- package/form/css/controls.css +171 -0
- package/form/css/layout.css +56 -0
- package/form/css/preview.css +60 -6
- package/form/deck.html +2 -0
- package/form/js/deck-core.js +60 -2
- package/form/js/deck-interact.js +63 -19
- package/form/js/deck-render.js +95 -6
- package/form/js/deck-session.js +140 -27
- package/generate-prompts.ts +18 -12
- package/index.ts +364 -66
- 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 +44 -8
- 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/form/js/deck-core.js
CHANGED
|
@@ -21,8 +21,13 @@ let events = null;
|
|
|
21
21
|
let heartbeatTimer = null;
|
|
22
22
|
let isClosed = false;
|
|
23
23
|
let isSubmitting = false;
|
|
24
|
+
let isDirty = false;
|
|
25
|
+
let lastSavedLabel = "";
|
|
26
|
+
let isRestoringSelections = false;
|
|
24
27
|
|
|
25
28
|
const selections = {};
|
|
29
|
+
const optionNotes = {};
|
|
30
|
+
let finalNotes = "";
|
|
26
31
|
const pendingGenerate = new Map();
|
|
27
32
|
let selectedModel = "";
|
|
28
33
|
let selectedThinking = "off";
|
|
@@ -33,6 +38,8 @@ const progressFill = document.getElementById("progress-fill");
|
|
|
33
38
|
const slidesWrap = document.getElementById("slides-wrap");
|
|
34
39
|
const btnBack = document.getElementById("btn-back");
|
|
35
40
|
const btnNext = document.getElementById("btn-next");
|
|
41
|
+
const btnSave = document.getElementById("btn-save");
|
|
42
|
+
const saveStatus = document.getElementById("save-status");
|
|
36
43
|
|
|
37
44
|
// ─── UTILITIES ───────────────────────────────────────────────
|
|
38
45
|
|
|
@@ -72,6 +79,47 @@ function setMetaLabel() {
|
|
|
72
79
|
deckTitle.textContent = title;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
function formatSavedTimestamp(value) {
|
|
83
|
+
try {
|
|
84
|
+
return new Date(value).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
85
|
+
} catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateSaveStatus() {
|
|
91
|
+
if (saveStatus) {
|
|
92
|
+
if (isDirty) {
|
|
93
|
+
saveStatus.textContent = "Unsaved changes";
|
|
94
|
+
saveStatus.classList.add("dirty");
|
|
95
|
+
saveStatus.classList.remove("saved");
|
|
96
|
+
} else if (lastSavedLabel) {
|
|
97
|
+
saveStatus.textContent = `Saved ${lastSavedLabel}`;
|
|
98
|
+
saveStatus.classList.add("saved");
|
|
99
|
+
saveStatus.classList.remove("dirty");
|
|
100
|
+
} else {
|
|
101
|
+
saveStatus.textContent = "No unsaved changes";
|
|
102
|
+
saveStatus.classList.remove("dirty", "saved");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (btnSave) {
|
|
106
|
+
btnSave.disabled = isClosed;
|
|
107
|
+
btnSave.classList.toggle("dirty", isDirty);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function markDirty() {
|
|
112
|
+
if (isClosed) return;
|
|
113
|
+
isDirty = true;
|
|
114
|
+
updateSaveStatus();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function markSaved(savedAt) {
|
|
118
|
+
isDirty = false;
|
|
119
|
+
lastSavedLabel = formatSavedTimestamp(savedAt || new Date().toISOString());
|
|
120
|
+
updateSaveStatus();
|
|
121
|
+
}
|
|
122
|
+
|
|
75
123
|
// ─── THEME SYSTEM ────────────────────────────────────────────
|
|
76
124
|
|
|
77
125
|
const themeConfig = deckData && typeof deckData === "object" && deckData.theme && typeof deckData.theme === "object" ? deckData.theme : {};
|
|
@@ -184,13 +232,23 @@ function initTheme() {
|
|
|
184
232
|
const SELECTIONS_KEY = `pi-deck-${typeof deckData.sessionId === "string" ? deckData.sessionId : "unknown"}`;
|
|
185
233
|
|
|
186
234
|
function saveSelectionsToStorage() {
|
|
187
|
-
try {
|
|
235
|
+
try {
|
|
236
|
+
const data = { selections, optionNotes };
|
|
237
|
+
if (finalNotes) data.finalNotes = finalNotes;
|
|
238
|
+
localStorage.setItem(SELECTIONS_KEY, JSON.stringify(data));
|
|
239
|
+
} catch {}
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
function loadSelectionsFromStorage() {
|
|
191
243
|
try {
|
|
192
244
|
const saved = localStorage.getItem(SELECTIONS_KEY);
|
|
193
|
-
|
|
245
|
+
if (!saved) return null;
|
|
246
|
+
const parsed = JSON.parse(saved);
|
|
247
|
+
// Handle both old format (just selections) and new format ({ selections, optionNotes })
|
|
248
|
+
if (parsed && typeof parsed === "object" && !parsed.selections) {
|
|
249
|
+
return { selections: parsed, optionNotes: {} };
|
|
250
|
+
}
|
|
251
|
+
return parsed;
|
|
194
252
|
} catch { return null; }
|
|
195
253
|
}
|
|
196
254
|
|
package/form/js/deck-interact.js
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
// ─── SELECTION ───────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
function applySavedSelections(
|
|
4
|
-
if (!
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
function applySavedSelections(savedSelections, savedNotes) {
|
|
4
|
+
if (!savedSelections || typeof savedSelections !== "object") return;
|
|
5
|
+
isRestoringSelections = true;
|
|
6
|
+
try {
|
|
7
|
+
for (const [slideId, label] of Object.entries(savedSelections)) {
|
|
8
|
+
if (typeof label !== "string") continue;
|
|
9
|
+
const slideEl = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
|
|
10
|
+
if (!slideEl) continue;
|
|
11
|
+
for (const card of slideEl.querySelectorAll(".option")) {
|
|
12
|
+
if (card.dataset.value === label) {
|
|
13
|
+
selectOption(card);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} finally {
|
|
19
|
+
isRestoringSelections = false;
|
|
20
|
+
}
|
|
21
|
+
// Restore notes
|
|
22
|
+
if (savedNotes && typeof savedNotes === "object") {
|
|
23
|
+
for (const [slideId, noteData] of Object.entries(savedNotes)) {
|
|
24
|
+
if (noteData && typeof noteData === "object" && noteData.label && noteData.notes) {
|
|
25
|
+
optionNotes[slideId] = noteData;
|
|
26
|
+
// Find and populate the textarea
|
|
27
|
+
const input = document.querySelector(`.option-notes-input[data-slide-id="${CSS.escape(slideId)}"][data-option-label="${CSS.escape(noteData.label)}"]`);
|
|
28
|
+
if (input) input.value = noteData.notes;
|
|
13
29
|
}
|
|
14
30
|
}
|
|
15
31
|
}
|
|
@@ -17,12 +33,28 @@ function applySavedSelections(saved) {
|
|
|
17
33
|
|
|
18
34
|
function restoreSelections() {
|
|
19
35
|
const serverSaved = deckData.savedSelections;
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
const hasServerSavedSelections = serverSaved && typeof serverSaved === "object" && Object.keys(serverSaved).length > 0;
|
|
37
|
+
const hasServerSavedNotes = deckData.savedNotes && typeof deckData.savedNotes === "object" && Object.keys(deckData.savedNotes).length > 0;
|
|
38
|
+
const hasServerFinalNotes = typeof deckData.savedFinalNotes === "string" && deckData.savedFinalNotes.trim() !== "";
|
|
39
|
+
if (hasServerSavedSelections || hasServerSavedNotes || hasServerFinalNotes) {
|
|
40
|
+
applySavedSelections(hasServerSavedSelections ? serverSaved : null, deckData.savedNotes || null);
|
|
41
|
+
if (deckData.savedFinalNotes) {
|
|
42
|
+
finalNotes = deckData.savedFinalNotes;
|
|
43
|
+
const input = document.getElementById("final-notes-input");
|
|
44
|
+
if (input) input.value = deckData.savedFinalNotes;
|
|
45
|
+
}
|
|
22
46
|
return;
|
|
23
47
|
}
|
|
24
48
|
const stored = loadSelectionsFromStorage();
|
|
25
|
-
if (stored)
|
|
49
|
+
if (stored) {
|
|
50
|
+
applySavedSelections(stored.selections || stored, stored.optionNotes);
|
|
51
|
+
// Restore final notes if present
|
|
52
|
+
if (stored.finalNotes) {
|
|
53
|
+
finalNotes = stored.finalNotes;
|
|
54
|
+
const input = document.getElementById("final-notes-input");
|
|
55
|
+
if (input) input.value = stored.finalNotes;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
26
58
|
}
|
|
27
59
|
|
|
28
60
|
function applySelectionClasses(slideElement, selectedElement) {
|
|
@@ -40,10 +72,15 @@ function selectOption(optionElement) {
|
|
|
40
72
|
if (!slideElement) return;
|
|
41
73
|
const slideId = slideElement.dataset.id;
|
|
42
74
|
if (!slideId || slideId === "summary") return;
|
|
75
|
+
const nextValue = optionElement.dataset.value || "";
|
|
76
|
+
if (selections[slideId] === nextValue) return;
|
|
43
77
|
|
|
44
78
|
applySelectionClasses(slideElement, optionElement);
|
|
45
|
-
selections[slideId] =
|
|
46
|
-
|
|
79
|
+
selections[slideId] = nextValue;
|
|
80
|
+
if (!isRestoringSelections) {
|
|
81
|
+
saveSelectionsToStorage();
|
|
82
|
+
markDirty();
|
|
83
|
+
}
|
|
47
84
|
}
|
|
48
85
|
|
|
49
86
|
// ─── NAVIGATION ──────────────────────────────────────────────
|
|
@@ -337,7 +374,8 @@ function initModelBar(modelsData) {
|
|
|
337
374
|
}
|
|
338
375
|
|
|
339
376
|
function syncDefaultCheck() {
|
|
340
|
-
|
|
377
|
+
const effectiveModel = selectedModel || modelsData.current;
|
|
378
|
+
defaultCheck.checked = modelsData.defaultModel != null && effectiveModel === modelsData.defaultModel;
|
|
341
379
|
}
|
|
342
380
|
|
|
343
381
|
function activateProvider(provider) {
|
|
@@ -369,12 +407,18 @@ function initModelBar(modelsData) {
|
|
|
369
407
|
});
|
|
370
408
|
|
|
371
409
|
defaultCheck.addEventListener("change", async () => {
|
|
372
|
-
|
|
373
|
-
const
|
|
410
|
+
// Use selectedModel, or fall back to current model when on "Current" pill
|
|
411
|
+
const modelToSave = selectedModel || modelsData.current;
|
|
412
|
+
if (defaultCheck.checked && !modelToSave) { defaultCheck.checked = false; return; }
|
|
413
|
+
const model = defaultCheck.checked ? modelToSave : null;
|
|
374
414
|
try {
|
|
375
415
|
await postJson("/save-model-default", { token: sessionToken, model });
|
|
376
416
|
modelsData.defaultModel = model;
|
|
377
|
-
} catch {
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error("Failed to save default model:", err);
|
|
419
|
+
// Revert checkbox to match actual state
|
|
420
|
+
syncDefaultCheck();
|
|
421
|
+
}
|
|
378
422
|
});
|
|
379
423
|
|
|
380
424
|
if (modelsData.defaultModel) {
|
package/form/js/deck-render.js
CHANGED
|
@@ -114,6 +114,10 @@ function applyPreviewHtml(preview, previewHtml) {
|
|
|
114
114
|
if (dataset.fonts) {
|
|
115
115
|
preview.dataset.fonts = dataset.fonts;
|
|
116
116
|
}
|
|
117
|
+
// Copy inline styles from source preview div
|
|
118
|
+
if (first.style.cssText) {
|
|
119
|
+
preview.style.cssText = first.style.cssText;
|
|
120
|
+
}
|
|
117
121
|
preview.innerHTML = first.innerHTML;
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
@@ -186,11 +190,48 @@ function createOptionCard(option, slideId, generatedBy) {
|
|
|
186
190
|
el.tabIndex = -1;
|
|
187
191
|
});
|
|
188
192
|
|
|
193
|
+
// Footer contains aside (optional) + notes input
|
|
194
|
+
const footer = createElement("div", "option-footer");
|
|
195
|
+
|
|
189
196
|
if (option.aside) {
|
|
190
197
|
const aside = createElement("div", "option-aside");
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
// Handle both actual newlines and literal \n sequences
|
|
199
|
+
aside.innerHTML = escapeHtml(option.aside).replace(/\\n/g, "<br>").replace(/\n/g, "<br>");
|
|
200
|
+
footer.appendChild(aside);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Notes input for user instructions
|
|
204
|
+
const notesContainer = createElement("div", "option-notes");
|
|
205
|
+
const notesLabel = createElement("label", "option-notes-label", "Your notes (optional)");
|
|
206
|
+
const notesInput = createElement("textarea", "option-notes-input");
|
|
207
|
+
notesInput.placeholder = "Add notes...";
|
|
208
|
+
notesInput.rows = 1;
|
|
209
|
+
notesInput.dataset.slideId = slideId;
|
|
210
|
+
notesInput.dataset.optionLabel = option.label;
|
|
211
|
+
|
|
212
|
+
// Restore saved notes if this option was previously selected with notes
|
|
213
|
+
if (optionNotes[slideId]?.label === option.label && optionNotes[slideId]?.notes) {
|
|
214
|
+
notesInput.value = optionNotes[slideId].notes;
|
|
193
215
|
}
|
|
216
|
+
|
|
217
|
+
notesInput.addEventListener("input", (e) => {
|
|
218
|
+
const value = e.target.value.trim();
|
|
219
|
+
if (value) {
|
|
220
|
+
optionNotes[slideId] = { label: option.label, notes: value };
|
|
221
|
+
} else if (optionNotes[slideId]?.label === option.label) {
|
|
222
|
+
delete optionNotes[slideId];
|
|
223
|
+
}
|
|
224
|
+
saveSelectionsToStorage();
|
|
225
|
+
markDirty();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Prevent click from bubbling to card (which would select it)
|
|
229
|
+
notesInput.addEventListener("click", (e) => e.stopPropagation());
|
|
230
|
+
|
|
231
|
+
notesContainer.appendChild(notesLabel);
|
|
232
|
+
notesContainer.appendChild(notesInput);
|
|
233
|
+
footer.appendChild(notesContainer);
|
|
234
|
+
card.appendChild(footer);
|
|
194
235
|
|
|
195
236
|
if (option.description) {
|
|
196
237
|
card.setAttribute("title", option.description);
|
|
@@ -207,10 +248,33 @@ function createGenerateBar(slideId) {
|
|
|
207
248
|
const bar = createElement("div", "gen-bar");
|
|
208
249
|
|
|
209
250
|
const row = createElement("div", "gen-row");
|
|
251
|
+
|
|
252
|
+
// Generate button with separate count selector
|
|
253
|
+
const genGroup = createElement("div", "gen-group");
|
|
210
254
|
const button = createElement("button", "btn-gen-more");
|
|
211
255
|
button.type = "button";
|
|
212
256
|
button.appendChild(createElement("span", "btn-gen-plus", "+"));
|
|
213
|
-
button.appendChild(document.createTextNode("
|
|
257
|
+
button.appendChild(document.createTextNode("Generate"));
|
|
258
|
+
|
|
259
|
+
const countSelect = document.createElement("select");
|
|
260
|
+
countSelect.className = "gen-count";
|
|
261
|
+
countSelect.setAttribute("aria-label", "Number of options to generate");
|
|
262
|
+
[1, 2, 3].forEach((n) => {
|
|
263
|
+
const opt = document.createElement("option");
|
|
264
|
+
opt.value = n;
|
|
265
|
+
opt.textContent = n;
|
|
266
|
+
countSelect.appendChild(opt);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const countLabel = createElement("span", "gen-count-label", "option");
|
|
270
|
+
countSelect.addEventListener("change", () => {
|
|
271
|
+
const count = parseInt(countSelect.value, 10);
|
|
272
|
+
countLabel.textContent = count === 1 ? "option" : "options";
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
genGroup.appendChild(button);
|
|
276
|
+
genGroup.appendChild(countSelect);
|
|
277
|
+
genGroup.appendChild(countLabel);
|
|
214
278
|
|
|
215
279
|
const regenButton = createElement("button", "btn-regen");
|
|
216
280
|
regenButton.type = "button";
|
|
@@ -231,8 +295,8 @@ function createGenerateBar(slideId) {
|
|
|
231
295
|
e.stopPropagation();
|
|
232
296
|
});
|
|
233
297
|
|
|
234
|
-
button.addEventListener("click", () => generateMore(button, slideId, input));
|
|
235
|
-
row.appendChild(
|
|
298
|
+
button.addEventListener("click", () => generateMore(button, slideId, input, countSelect));
|
|
299
|
+
row.appendChild(genGroup);
|
|
236
300
|
row.appendChild(regenButton);
|
|
237
301
|
row.appendChild(input);
|
|
238
302
|
bar.appendChild(row);
|
|
@@ -301,6 +365,23 @@ function createSummarySlide(index) {
|
|
|
301
365
|
summaryGrid.id = "summary-grid";
|
|
302
366
|
section.appendChild(summaryGrid);
|
|
303
367
|
|
|
368
|
+
// Final instructions textarea
|
|
369
|
+
const finalNotesContainer = createElement("div", "final-notes");
|
|
370
|
+
const finalNotesLabel = createElement("label", "final-notes-label", "Additional instructions (optional)");
|
|
371
|
+
finalNotesLabel.setAttribute("for", "final-notes-input");
|
|
372
|
+
const finalNotesInput = createElement("textarea", "final-notes-input");
|
|
373
|
+
finalNotesInput.id = "final-notes-input";
|
|
374
|
+
finalNotesInput.placeholder = "Add any final notes or instructions for implementation...";
|
|
375
|
+
finalNotesInput.rows = 2;
|
|
376
|
+
finalNotesInput.addEventListener("input", (e) => {
|
|
377
|
+
finalNotes = e.target.value.trim();
|
|
378
|
+
saveSelectionsToStorage();
|
|
379
|
+
markDirty();
|
|
380
|
+
});
|
|
381
|
+
finalNotesContainer.appendChild(finalNotesLabel);
|
|
382
|
+
finalNotesContainer.appendChild(finalNotesInput);
|
|
383
|
+
section.appendChild(finalNotesContainer);
|
|
384
|
+
|
|
304
385
|
const submitButton = createElement("button", "btn-generate", "Submit Selections");
|
|
305
386
|
submitButton.type = "button";
|
|
306
387
|
submitButton.id = "btn-generate";
|
|
@@ -382,10 +463,18 @@ function createSummaryCard(slide) {
|
|
|
382
463
|
const text = selectedOption.aside.length > 120
|
|
383
464
|
? selectedOption.aside.slice(0, 120).trimEnd() + "..."
|
|
384
465
|
: selectedOption.aside;
|
|
385
|
-
aside.
|
|
466
|
+
aside.innerHTML = escapeHtml(text).replace(/\\n/g, "<br>").replace(/\n/g, "<br>");
|
|
386
467
|
card.appendChild(aside);
|
|
387
468
|
}
|
|
388
469
|
}
|
|
470
|
+
|
|
471
|
+
// Show user notes if present
|
|
472
|
+
const noteData = optionNotes[slide.id];
|
|
473
|
+
if (noteData && noteData.label === selectedLabel && noteData.notes) {
|
|
474
|
+
const notesDisplay = createElement("div", "summary-notes");
|
|
475
|
+
notesDisplay.innerHTML = `<span class="summary-notes-label">Your notes:</span> ${escapeHtml(noteData.notes)}`;
|
|
476
|
+
card.appendChild(notesDisplay);
|
|
477
|
+
}
|
|
389
478
|
}
|
|
390
479
|
|
|
391
480
|
return card;
|