pi-design-deck 0.3.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 CHANGED
@@ -96,7 +96,7 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
96
96
  - **Generate-more loop**: Users click "Generate another option" and the agent pushes a new option into the live deck via SSE. No page reload.
97
97
  - **Model selector**: Dropdown to pick which model generates new options. Save as default, or override per-request.
98
98
  - **Thinking level**: Adjust reasoning effort for option generation when the selected model supports it.
99
- - **Slide columns**: `columns` property (1, 2, or 3) per slide. Auto-detected from option count if omitted.
99
+ - **Slide columns**: `columns` property (1, 2, 3, or 4) per slide. Auto-detected from option count if omitted.
100
100
  - **Smart rebalancing**: Grid layout recalculates after generate-more adds options to minimize orphans.
101
101
  - **Option aside**: Explanatory text rendered below the preview. Supports `\n` for line breaks.
102
102
  - **Save/load snapshots**: `Cmd+S` saves the deck to disk. Use `action: "list"` to enumerate saved decks, `action: "open"` to reopen one by deck ID, or pass a file path to `slides`.
@@ -156,7 +156,7 @@ Image blocks reference absolute file paths. The server copies each file into a t
156
156
 
157
157
  ### Columns
158
158
 
159
- Each slide supports `columns: 1 | 2 | 3` to control the grid layout. Omit it and the deck auto-detects based on option count. Use `columns: 1` for wide architecture diagrams, `columns: 2` for side-by-side comparisons.
159
+ Each slide supports `columns: 1 | 2 | 3 | 4` to control the grid layout. Omit it and the deck auto-detects based on option count. Use `columns: 1` for wide architecture diagrams, `columns: 2` for side-by-side comparisons, `columns: 4` for many small items.
160
160
 
161
161
  ### Aside
162
162
 
package/deck-schema.ts CHANGED
@@ -17,7 +17,7 @@ export interface DeckSlide {
17
17
  id: string;
18
18
  title: string;
19
19
  context?: string;
20
- columns?: 1 | 2 | 3;
20
+ columns?: 1 | 2 | 3 | 4;
21
21
  options: DeckOption[];
22
22
  }
23
23
 
@@ -155,8 +155,8 @@ function validateDeckSlide(slide: unknown, index: number): DeckSlide {
155
155
  }
156
156
 
157
157
  if (obj.columns !== undefined) {
158
- if (obj.columns !== 1 && obj.columns !== 2 && obj.columns !== 3) {
159
- throw new Error(`Slide "${obj.id}": columns must be 1, 2, or 3`);
158
+ if (obj.columns !== 1 && obj.columns !== 2 && obj.columns !== 3 && obj.columns !== 4) {
159
+ throw new Error(`Slide "${obj.id}": columns must be 1, 2, 3, or 4`);
160
160
  }
161
161
  }
162
162
 
package/export-html.ts CHANGED
@@ -157,7 +157,7 @@ function escapeHtml(value: string): string {
157
157
  .replace(/"/g, """);
158
158
  }
159
159
 
160
- function optionCountClass(count: number, columns?: 1 | 2 | 3): string {
160
+ function optionCountClass(count: number, columns?: 1 | 2 | 3 | 4): string {
161
161
  if (columns === 1) return "cols-1";
162
162
  if (columns && count >= columns && count % columns !== 1) {
163
163
  return `cols-${columns}`;
@@ -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
- /* Regeneration status text */
201
- .regen-status {
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
- 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;
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
- gap: 8px;
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
- .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;
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
  ───────────────────────────────────────────────────────────── */
@@ -468,6 +552,7 @@
468
552
  font: 11px var(--dk-font-mono);
469
553
  color: var(--dk-text-hint);
470
554
  white-space: nowrap;
555
+ padding: 6px 12px;
471
556
  }
472
557
 
473
558
  .deck-save-status.saved {
@@ -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,6 +44,20 @@
44
44
  <span class="deck-key"><kbd>&larr;</kbd><kbd>&rarr;</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>
48
62
  <button class="deck-save-btn" id="btn-save" type="button">Save</button>
49
63
  <span class="deck-save-status" id="save-status" role="status" aria-live="polite">No unsaved changes</span>
@@ -227,6 +227,78 @@ function initTheme() {
227
227
  }
228
228
  }
229
229
 
230
+ // ─── LAYOUT TOGGLE ───────────────────────────────────────────
231
+
232
+ const LAYOUT_KEY = "pi-deck-layout";
233
+
234
+ function getStoredLayout() {
235
+ try {
236
+ const value = localStorage.getItem(LAYOUT_KEY);
237
+ return value === "1" || value === "2" || value === "3" || value === "4" ? value : null;
238
+ } catch { return null; }
239
+ }
240
+
241
+ function setStoredLayout(value) {
242
+ try {
243
+ if (!value) {
244
+ localStorage.removeItem(LAYOUT_KEY);
245
+ } else {
246
+ localStorage.setItem(LAYOUT_KEY, value);
247
+ }
248
+ } catch {}
249
+ }
250
+
251
+ function applyLayout(cols) {
252
+ const deck = document.querySelector(".deck");
253
+ if (!deck) return;
254
+ if (cols) {
255
+ deck.dataset.layout = cols;
256
+ } else {
257
+ delete deck.dataset.layout;
258
+ }
259
+ }
260
+
261
+ function updateLayoutButtons(activeCols) {
262
+ const toggle = document.getElementById("layout-toggle");
263
+ if (!toggle) return;
264
+ toggle.querySelectorAll(".layout-btn").forEach((btn) => {
265
+ const isActive = btn.dataset.cols === activeCols;
266
+ btn.classList.toggle("active", isActive);
267
+ btn.setAttribute("aria-pressed", isActive ? "true" : "false");
268
+ // Update title to show "Auto" hint when clicking would reset
269
+ const cols = btn.dataset.cols;
270
+ btn.title = isActive ? `${cols} column${cols === "1" ? "" : "s"} (click for auto)` : `${cols} column${cols === "1" ? "" : "s"}`;
271
+ });
272
+ }
273
+
274
+ function initLayoutToggle() {
275
+ const stored = getStoredLayout();
276
+ if (stored) {
277
+ applyLayout(stored);
278
+ updateLayoutButtons(stored);
279
+ }
280
+
281
+ const toggle = document.getElementById("layout-toggle");
282
+ if (!toggle) return;
283
+
284
+ toggle.addEventListener("click", (event) => {
285
+ const btn = event.target.closest(".layout-btn");
286
+ if (!btn) return;
287
+ const cols = btn.dataset.cols;
288
+ const current = getStoredLayout();
289
+ if (cols === current) {
290
+ // Clicking active button toggles back to auto
291
+ setStoredLayout(null);
292
+ applyLayout(null);
293
+ updateLayoutButtons(null);
294
+ } else {
295
+ setStoredLayout(cols);
296
+ applyLayout(cols);
297
+ updateLayoutButtons(cols);
298
+ }
299
+ });
300
+ }
301
+
230
302
  // ─── SELECTION PERSISTENCE ────────────────────────────────────
231
303
 
232
304
  const SELECTIONS_KEY = `pi-deck-${typeof deckData.sessionId === "string" ? deckData.sessionId : "unknown"}`;
@@ -507,6 +507,7 @@ async function generateMore(button, slideId, input, countSelect) {
507
507
  const skeletons = [];
508
508
  for (let i = 0; i < count; i++) {
509
509
  const skeleton = createElement("div", "option-skeleton");
510
+ skeleton.innerHTML = '<div class="spinner"></div>';
510
511
  optionsGrid.appendChild(skeleton);
511
512
  skeletons.push(skeleton);
512
513
  }
@@ -563,7 +564,7 @@ async function regenerateSlide(button, slideId) {
563
564
  const prompt = input ? input.value.trim() : "";
564
565
  if (input) input.value = "";
565
566
 
566
- // Create skeleton overlay
567
+ // Create skeleton overlay with centered spinner
567
568
  const optionCount = slide.options?.length || 2;
568
569
  const colClass = optionsGrid.classList.contains("cols-3") ? "cols-3" :
569
570
  optionsGrid.classList.contains("cols-1") ? "cols-1" : "cols-2";
@@ -571,8 +572,9 @@ async function regenerateSlide(button, slideId) {
571
572
  for (let i = 0; i < optionCount; i++) {
572
573
  overlay.appendChild(createElement("div", "regen-skeleton"));
573
574
  }
574
- const status = createElement("div", "regen-status", "Regenerating options...");
575
- overlay.appendChild(status);
575
+ const center = createElement("div", "regen-center");
576
+ center.innerHTML = '<div class="spinner"></div><div class="regen-center-text">Regenerating options...</div>';
577
+ overlay.appendChild(center);
576
578
 
577
579
  // Position overlay relative to options grid
578
580
  optionsGrid.style.position = "relative";
@@ -670,6 +672,7 @@ function hideLoadingOverlay() {
670
672
 
671
673
  function init() {
672
674
  initTheme();
675
+ initLayoutToggle();
673
676
  setMetaLabel();
674
677
  renderSlides();
675
678
  restoreSelections();
package/index.ts CHANGED
@@ -88,7 +88,7 @@ const DeckParams = Type.Object(
88
88
  slides: Type.Optional(
89
89
  Type.String({
90
90
  description:
91
- "JSON string of deck config. Each slide has id, title, context?, columns? (1|2|3, omit for auto-layout), and options[]. " +
91
+ "JSON string of deck config. Each slide has id, title, context?, columns? (1|2|3|4, omit for auto-layout), and options[]. " +
92
92
  "Each option has label, description?, aside?, recommended?, and either previewHtml (raw HTML string) or " +
93
93
  "previewBlocks (array of typed blocks: {type:'html',content}, {type:'mermaid',content,theme?}, " +
94
94
  "{type:'code',code,lang}, {type:'image',src,alt,caption?}). Exactly one of previewHtml or previewBlocks required per option.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-design-deck",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Visual design deck for presenting multi-slide options with high-fidelity previews",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@ Build 1 decision per slide.
44
44
 
45
45
  Provide 2-4 options per slide that are genuinely distinct in direction, not tiny variants.
46
46
 
47
- Each slide supports an optional `columns` property (1, 2, or 3) to override the auto-detected grid. **Omit `columns` by default** — the auto-layout picks 2 or 3 columns based on option count, which is correct for most content. Only override to `columns: 1` when options contain wide architecture diagrams or detailed code that genuinely needs full viewport width. Never use `columns: 1` for text comparisons or simple previews.
47
+ Each slide supports an optional `columns` property (1, 2, 3, or 4) to override the auto-detected grid. **Omit `columns` by default** — the auto-layout picks 2 or 3 columns based on option count, which is correct for most content. Only override to `columns: 1` when options contain wide architecture diagrams or detailed code that genuinely needs full viewport width. Use `columns: 4` when presenting many small, comparable items (e.g., icon sets, color swatches).
48
48
 
49
49
  Each slide supports an optional `context` property — a string displayed below the title that frames the decision for the user.
50
50