pi-design-deck 0.2.0 → 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 +49 -11
- package/deck-schema.ts +30 -0
- package/deck-server.ts +64 -10
- package/export-html.ts +329 -0
- package/form/css/controls.css +51 -0
- package/form/deck.html +2 -0
- package/form/js/deck-core.js +46 -0
- package/form/js/deck-interact.js +30 -12
- package/form/js/deck-render.js +2 -0
- package/form/js/deck-session.js +25 -9
- package/generate-prompts.ts +8 -10
- package/index.ts +317 -41
- 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/css/controls.css
CHANGED
|
@@ -423,11 +423,61 @@
|
|
|
423
423
|
.btn-nav:focus-visible,
|
|
424
424
|
.btn-gen-more:focus-visible,
|
|
425
425
|
.btn-generate:focus-visible,
|
|
426
|
+
.deck-save-btn:focus-visible,
|
|
426
427
|
.confirm-bar-btn:focus-visible {
|
|
427
428
|
outline: 2px solid var(--dk-accent);
|
|
428
429
|
outline-offset: 2px;
|
|
429
430
|
}
|
|
430
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
|
+
|
|
431
481
|
.deck {
|
|
432
482
|
touch-action: manipulation;
|
|
433
483
|
overscroll-behavior: contain;
|
|
@@ -451,6 +501,7 @@
|
|
|
451
501
|
.slide { padding: 20px 16px 16px; }
|
|
452
502
|
.summary-grid { grid-template-columns: 1fr; }
|
|
453
503
|
.pv-layout-sidebar { grid-template-columns: 120px 1fr; }
|
|
504
|
+
.deck-save-status { display: none; }
|
|
454
505
|
}
|
|
455
506
|
|
|
456
507
|
/* ─────────────────────────────────────────────────────────────
|
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 →</button>
|
package/form/js/deck-core.js
CHANGED
|
@@ -21,6 +21,9 @@ 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 = {};
|
|
26
29
|
const optionNotes = {};
|
|
@@ -35,6 +38,8 @@ const progressFill = document.getElementById("progress-fill");
|
|
|
35
38
|
const slidesWrap = document.getElementById("slides-wrap");
|
|
36
39
|
const btnBack = document.getElementById("btn-back");
|
|
37
40
|
const btnNext = document.getElementById("btn-next");
|
|
41
|
+
const btnSave = document.getElementById("btn-save");
|
|
42
|
+
const saveStatus = document.getElementById("save-status");
|
|
38
43
|
|
|
39
44
|
// ─── UTILITIES ───────────────────────────────────────────────
|
|
40
45
|
|
|
@@ -74,6 +79,47 @@ function setMetaLabel() {
|
|
|
74
79
|
deckTitle.textContent = title;
|
|
75
80
|
}
|
|
76
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
|
+
|
|
77
123
|
// ─── THEME SYSTEM ────────────────────────────────────────────
|
|
78
124
|
|
|
79
125
|
const themeConfig = deckData && typeof deckData === "object" && deckData.theme && typeof deckData.theme === "object" ? deckData.theme : {};
|
package/form/js/deck-interact.js
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
function applySavedSelections(savedSelections, savedNotes) {
|
|
4
4
|
if (!savedSelections || typeof savedSelections !== "object") return;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
16
|
}
|
|
14
17
|
}
|
|
18
|
+
} finally {
|
|
19
|
+
isRestoringSelections = false;
|
|
15
20
|
}
|
|
16
21
|
// Restore notes
|
|
17
22
|
if (savedNotes && typeof savedNotes === "object") {
|
|
@@ -28,8 +33,16 @@ function applySavedSelections(savedSelections, savedNotes) {
|
|
|
28
33
|
|
|
29
34
|
function restoreSelections() {
|
|
30
35
|
const serverSaved = deckData.savedSelections;
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
46
|
return;
|
|
34
47
|
}
|
|
35
48
|
const stored = loadSelectionsFromStorage();
|
|
@@ -59,10 +72,15 @@ function selectOption(optionElement) {
|
|
|
59
72
|
if (!slideElement) return;
|
|
60
73
|
const slideId = slideElement.dataset.id;
|
|
61
74
|
if (!slideId || slideId === "summary") return;
|
|
75
|
+
const nextValue = optionElement.dataset.value || "";
|
|
76
|
+
if (selections[slideId] === nextValue) return;
|
|
62
77
|
|
|
63
78
|
applySelectionClasses(slideElement, optionElement);
|
|
64
|
-
selections[slideId] =
|
|
65
|
-
|
|
79
|
+
selections[slideId] = nextValue;
|
|
80
|
+
if (!isRestoringSelections) {
|
|
81
|
+
saveSelectionsToStorage();
|
|
82
|
+
markDirty();
|
|
83
|
+
}
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
// ─── NAVIGATION ──────────────────────────────────────────────
|
package/form/js/deck-render.js
CHANGED
|
@@ -222,6 +222,7 @@ function createOptionCard(option, slideId, generatedBy) {
|
|
|
222
222
|
delete optionNotes[slideId];
|
|
223
223
|
}
|
|
224
224
|
saveSelectionsToStorage();
|
|
225
|
+
markDirty();
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
// Prevent click from bubbling to card (which would select it)
|
|
@@ -375,6 +376,7 @@ function createSummarySlide(index) {
|
|
|
375
376
|
finalNotesInput.addEventListener("input", (e) => {
|
|
376
377
|
finalNotes = e.target.value.trim();
|
|
377
378
|
saveSelectionsToStorage();
|
|
379
|
+
markDirty();
|
|
378
380
|
});
|
|
379
381
|
finalNotesContainer.appendChild(finalNotesLabel);
|
|
380
382
|
finalNotesContainer.appendChild(finalNotesInput);
|
package/form/js/deck-session.js
CHANGED
|
@@ -41,11 +41,26 @@ function showSaveToast(message, isError) {
|
|
|
41
41
|
saveToastTimer = setTimeout(() => { toast.classList.add("hidden"); saveToastTimer = null; }, 3000);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function buildSelectedNotes() {
|
|
45
|
+
const notes = {};
|
|
46
|
+
for (const [slideId, noteData] of Object.entries(optionNotes)) {
|
|
47
|
+
if (noteData && selections[slideId] === noteData.label && noteData.notes) {
|
|
48
|
+
notes[slideId] = noteData.notes;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return notes;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
async function saveDeck() {
|
|
45
55
|
if (isClosed) return;
|
|
46
56
|
try {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
57
|
+
const payload = { token: sessionToken, selections, notes: buildSelectedNotes() };
|
|
58
|
+
if (finalNotes) payload.finalNotes = finalNotes;
|
|
59
|
+
const result = await postJson("/save", payload);
|
|
60
|
+
if (result.ok) {
|
|
61
|
+
markSaved(new Date().toISOString());
|
|
62
|
+
showSaveToast(`Saved to ${result.relativePath}`);
|
|
63
|
+
}
|
|
49
64
|
else showSaveToast(result.error || "Save failed", true);
|
|
50
65
|
} catch {
|
|
51
66
|
showSaveToast("Save failed", true);
|
|
@@ -71,6 +86,7 @@ function stopHeartbeat() {
|
|
|
71
86
|
function disableDeckInteractions() {
|
|
72
87
|
if (btnBack) btnBack.disabled = true;
|
|
73
88
|
if (btnNext) btnNext.disabled = true;
|
|
89
|
+
if (btnSave) btnSave.disabled = true;
|
|
74
90
|
document.querySelectorAll(".btn-gen-more, .btn-regen").forEach((button) => {
|
|
75
91
|
button.disabled = true;
|
|
76
92
|
});
|
|
@@ -98,13 +114,7 @@ async function submitDeck() {
|
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
try {
|
|
101
|
-
|
|
102
|
-
const notes = {};
|
|
103
|
-
for (const [slideId, noteData] of Object.entries(optionNotes)) {
|
|
104
|
-
if (noteData && selections[slideId] === noteData.label && noteData.notes) {
|
|
105
|
-
notes[slideId] = noteData.notes;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
117
|
+
const notes = buildSelectedNotes();
|
|
108
118
|
const payload = { token: sessionToken, selections, notes };
|
|
109
119
|
if (finalNotes) payload.finalNotes = finalNotes;
|
|
110
120
|
await postJson("/submit", payload);
|
|
@@ -307,6 +317,7 @@ function insertGeneratedOption(slideId, option, model) {
|
|
|
307
317
|
if (current === totalSlides - 1) {
|
|
308
318
|
updateSummary();
|
|
309
319
|
}
|
|
320
|
+
markDirty();
|
|
310
321
|
}
|
|
311
322
|
|
|
312
323
|
function replaceSlideOptions(slideId, newOptions) {
|
|
@@ -355,6 +366,7 @@ function replaceSlideOptions(slideId, newOptions) {
|
|
|
355
366
|
if (current === totalSlides - 1) {
|
|
356
367
|
updateSummary();
|
|
357
368
|
}
|
|
369
|
+
markDirty();
|
|
358
370
|
}
|
|
359
371
|
|
|
360
372
|
function connectEvents() {
|
|
@@ -636,6 +648,9 @@ function initSaveShortcut() {
|
|
|
636
648
|
document.querySelectorAll(".mod-key").forEach((el) => {
|
|
637
649
|
el.textContent = isMac ? "⌘" : "Ctrl";
|
|
638
650
|
});
|
|
651
|
+
if (btnSave) {
|
|
652
|
+
btnSave.addEventListener("click", () => saveDeck());
|
|
653
|
+
}
|
|
639
654
|
document.addEventListener("keydown", (e) => {
|
|
640
655
|
const mod = isMac ? e.metaKey : e.ctrlKey;
|
|
641
656
|
if (mod && e.key === "s") {
|
|
@@ -667,6 +682,7 @@ function init() {
|
|
|
667
682
|
fetchModels().then((data) => {
|
|
668
683
|
if (data) initModelBar(data);
|
|
669
684
|
});
|
|
685
|
+
updateSaveStatus();
|
|
670
686
|
|
|
671
687
|
document.addEventListener("keydown", handleKeydown);
|
|
672
688
|
window.addEventListener("beforeunload", sendCancelBeacon);
|
package/generate-prompts.ts
CHANGED
|
@@ -28,8 +28,8 @@ function optionTemplate(hasBlocks: boolean): string {
|
|
|
28
28
|
|
|
29
29
|
function modelHints(generateModel?: string, thinking?: string, action?: string): string {
|
|
30
30
|
if (!generateModel) return "";
|
|
31
|
-
const verb = action === "replace-options" ? "replace-options" : "add-
|
|
32
|
-
let hint = `\nGenerate
|
|
31
|
+
const verb = action === "replace-options" ? "replace-options" : "add-options";
|
|
32
|
+
let hint = `\nGenerate options using deck_generate({ model: "${generateModel}", task: "..." }), then push with ${verb}.`;
|
|
33
33
|
if (thinking && thinking !== "off") {
|
|
34
34
|
hint += `\nUse thinking level: "${thinking}".`;
|
|
35
35
|
}
|
|
@@ -56,20 +56,18 @@ export function buildGenerateMoreResult(slideId: string, slide: DeckSlide | unde
|
|
|
56
56
|
const userInstructions = prompt ? `\nUser instructions: "${prompt}"` : "";
|
|
57
57
|
|
|
58
58
|
const optionWord = count === 1 ? "option" : "options";
|
|
59
|
-
const callInstructions = count === 1
|
|
60
|
-
? `YOU MUST generate one distinctive additional option and call design_deck with add-option.`
|
|
61
|
-
: `YOU MUST generate ${count} distinctive additional options. Call design_deck with add-option ${count} times (once per option).`;
|
|
62
59
|
|
|
63
60
|
return (
|
|
64
61
|
"The design deck is still open and waiting for your response.\n\n" +
|
|
65
62
|
`User clicked "Generate ${count} ${optionWord}" for slide \"${title}\".${context}${userInstructions}\n\n` +
|
|
66
63
|
`Existing options:\n${existingText}\n\n` +
|
|
67
|
-
|
|
68
|
-
`Do not skip this step or decide the user has enough options — they explicitly requested ${count === 1 ? "another one" : `${count} more`}.${modelHints(generateModel, thinking, "add-
|
|
69
|
-
`design_deck({\"action\":\"add-
|
|
70
|
-
|
|
64
|
+
`YOU MUST generate ${count} distinctive additional ${optionWord} and call design_deck with add-options (one call with all options in an array). ` +
|
|
65
|
+
`Do not skip this step or decide the user has enough options — they explicitly requested ${count === 1 ? "another one" : `${count} more`}.${modelHints(generateModel, thinking, "add-options")}\n\n` +
|
|
66
|
+
`design_deck({\"action\":\"add-options\",\"slideId\":\"${slideId}\",\"options\":\"[${template}${count > 1 ? ", ..." : ""}]\"})` +
|
|
67
|
+
`\n\nThe options field must be a JSON string containing an array of ${count} option object${count > 1 ? "s" : ""}.\n` +
|
|
68
|
+
`Each option needs: label, optional description, optional aside (explanatory notes below preview), optional recommended, and either previewHtml or previewBlocks.\n` +
|
|
71
69
|
`${formatHint}` +
|
|
72
|
-
(count > 1 ? `\n\
|
|
70
|
+
(count > 1 ? `\n\nMake each option distinctive — they should represent genuinely different approaches.` : "")
|
|
73
71
|
);
|
|
74
72
|
}
|
|
75
73
|
|