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
@@ -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 { localStorage.setItem(SELECTIONS_KEY, JSON.stringify(selections)); } catch {}
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
- return saved ? JSON.parse(saved) : null;
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
 
@@ -1,15 +1,31 @@
1
1
  // ─── SELECTION ───────────────────────────────────────────────
2
2
 
3
- function applySavedSelections(saved) {
4
- if (!saved || typeof saved !== "object") return;
5
- for (const [slideId, label] of Object.entries(saved)) {
6
- if (typeof label !== "string") continue;
7
- const slideEl = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
8
- if (!slideEl) continue;
9
- for (const card of slideEl.querySelectorAll(".option")) {
10
- if (card.dataset.value === label) {
11
- selectOption(card);
12
- break;
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
- if (serverSaved && typeof serverSaved === "object" && Object.keys(serverSaved).length > 0) {
21
- applySavedSelections(serverSaved);
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) applySavedSelections(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] = optionElement.dataset.value || "";
46
- saveSelectionsToStorage();
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
- defaultCheck.checked = modelsData.defaultModel != null && selectedModel === modelsData.defaultModel;
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
- if (defaultCheck.checked && !selectedModel) { defaultCheck.checked = false; return; }
373
- const model = defaultCheck.checked ? selectedModel : null;
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) {
@@ -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
- aside.innerHTML = escapeHtml(option.aside).replace(/\n/g, "<br>");
192
- card.appendChild(aside);
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(" Generate another option"));
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(button);
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.textContent = text;
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;