powerpointproto slides.waow.tech
slides

add resizable presenter panel and present shortcut

- add drag handle to resize presenter notes panel (60-400px)
- slide canvas adjusts dynamically as panel resizes
- show notes area even when no notes (displays placeholder)
- add present button (play icon) to deck list on homepage
- clicking present loads deck and starts presentation immediately

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+166 -24
+103 -18
src/lib/components/PresenterView.svelte
··· 9 10 let elapsed = $state("00:00"); 11 let interval: ReturnType<typeof setInterval>; 12 13 $effect(() => { 14 if (editorState.isPresenting && editorState.presentationStartTime) { ··· 40 break; 41 } 42 }; 43 </script> 44 45 - <svelte:window onkeydown={handleKeydown} /> 46 47 {#if editorState.isPresenting} 48 - <div class="presenter-overlay"> 49 <div class="controls"> 50 <button onclick={prevSlide}>←</button> 51 <span class="slide-counter"> ··· 56 <button onclick={stopPresenting}>exit</button> 57 </div> 58 59 - {#if editorState.showNotes && getCurrentSlide()?.notes} 60 <div class="notes"> 61 - <strong>notes:</strong> 62 - <p>{getCurrentSlide()?.notes}</p> 63 </div> 64 {/if} 65 ··· 78 bottom: 0; 79 left: 0; 80 right: 0; 81 - background: rgba(0, 0, 0, 0.9); 82 - padding: 12px 20px; 83 z-index: 1000; 84 } 85 86 .controls { ··· 88 align-items: center; 89 justify-content: center; 90 gap: 16px; 91 } 92 93 .controls button { ··· 118 } 119 120 .notes { 121 - margin-top: 12px; 122 padding: 12px; 123 - background: #222; 124 border-radius: 4px; 125 color: #ccc; 126 - max-height: 100px; 127 overflow-y: auto; 128 - } 129 - 130 - .notes strong { 131 - color: #888; 132 - font-size: 12px; 133 - text-transform: uppercase; 134 } 135 136 .notes p { 137 - margin: 8px 0 0; 138 white-space: pre-wrap; 139 } 140 141 .notes-toggle { 142 position: absolute; 143 right: 20px; 144 - bottom: 12px; 145 padding: 6px 12px; 146 background: transparent; 147 border: 1px solid #555; ··· 149 cursor: pointer; 150 font-size: 12px; 151 border-radius: 4px; 152 } 153 </style>
··· 9 10 let elapsed = $state("00:00"); 11 let interval: ReturnType<typeof setInterval>; 12 + let isDragging = $state(false); 13 + let dragStartY = 0; 14 + let dragStartHeight = 0; 15 + 16 + const MIN_HEIGHT = 60; 17 + const MAX_HEIGHT = 400; 18 19 $effect(() => { 20 if (editorState.isPresenting && editorState.presentationStartTime) { ··· 46 break; 47 } 48 }; 49 + 50 + const handleDragStart = (e: MouseEvent | TouchEvent) => { 51 + isDragging = true; 52 + dragStartY = "touches" in e ? e.touches[0].clientY : e.clientY; 53 + dragStartHeight = editorState.presenterPanelHeight; 54 + e.preventDefault(); 55 + }; 56 + 57 + const handleDragMove = (e: MouseEvent | TouchEvent) => { 58 + if (!isDragging) return; 59 + const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; 60 + const delta = dragStartY - clientY; 61 + const newHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, dragStartHeight + delta)); 62 + editorState.presenterPanelHeight = newHeight; 63 + }; 64 + 65 + const handleDragEnd = () => { 66 + isDragging = false; 67 + }; 68 </script> 69 70 + <svelte:window 71 + onkeydown={handleKeydown} 72 + onmousemove={handleDragMove} 73 + onmouseup={handleDragEnd} 74 + ontouchmove={handleDragMove} 75 + ontouchend={handleDragEnd} 76 + /> 77 78 {#if editorState.isPresenting} 79 + <div 80 + class="presenter-overlay" 81 + class:dragging={isDragging} 82 + style="height: {editorState.presenterPanelHeight}px;" 83 + > 84 + <!-- svelte-ignore a11y_no_static_element_interactions --> 85 + <div 86 + class="drag-handle" 87 + onmousedown={handleDragStart} 88 + ontouchstart={handleDragStart} 89 + > 90 + <div class="drag-indicator"></div> 91 + </div> 92 + 93 <div class="controls"> 94 <button onclick={prevSlide}>←</button> 95 <span class="slide-counter"> ··· 100 <button onclick={stopPresenting}>exit</button> 101 </div> 102 103 + {#if editorState.showNotes} 104 <div class="notes"> 105 + {#if getCurrentSlide()?.notes} 106 + <p>{getCurrentSlide()?.notes}</p> 107 + {:else} 108 + <p class="no-notes">no notes for this slide</p> 109 + {/if} 110 </div> 111 {/if} 112 ··· 125 bottom: 0; 126 left: 0; 127 right: 0; 128 + background: rgba(0, 0, 0, 0.95); 129 z-index: 1000; 130 + display: flex; 131 + flex-direction: column; 132 + min-height: 60px; 133 + } 134 + 135 + .presenter-overlay.dragging { 136 + user-select: none; 137 + } 138 + 139 + .drag-handle { 140 + position: absolute; 141 + top: 0; 142 + left: 0; 143 + right: 0; 144 + height: 12px; 145 + cursor: ns-resize; 146 + display: flex; 147 + align-items: center; 148 + justify-content: center; 149 + } 150 + 151 + .drag-handle:hover .drag-indicator, 152 + .dragging .drag-indicator { 153 + background: #666; 154 + } 155 + 156 + .drag-indicator { 157 + width: 40px; 158 + height: 4px; 159 + background: #444; 160 + border-radius: 2px; 161 + transition: background 0.15s ease; 162 } 163 164 .controls { ··· 166 align-items: center; 167 justify-content: center; 168 gap: 16px; 169 + padding: 16px 20px 8px; 170 } 171 172 .controls button { ··· 197 } 198 199 .notes { 200 + flex: 1; 201 + margin: 8px 20px 12px; 202 padding: 12px; 203 + background: #1a1a1a; 204 border-radius: 4px; 205 color: #ccc; 206 overflow-y: auto; 207 + min-height: 0; 208 } 209 210 .notes p { 211 + margin: 0; 212 white-space: pre-wrap; 213 + line-height: 1.5; 214 + } 215 + 216 + .notes .no-notes { 217 + color: #666; 218 + font-style: italic; 219 } 220 221 .notes-toggle { 222 position: absolute; 223 right: 20px; 224 + top: 16px; 225 padding: 6px 12px; 226 background: transparent; 227 border: 1px solid #555; ··· 229 cursor: pointer; 230 font-size: 12px; 231 border-radius: 4px; 232 + } 233 + 234 + .notes-toggle:hover { 235 + border-color: #777; 236 + color: #ccc; 237 } 238 </style>
+1
src/lib/state.svelte.ts
··· 9 isPresenting: false, 10 showNotes: false, 11 presentationStartTime: null as number | null, 12 }); 13 14 export const getCurrentSlide = (): Slide | null => {
··· 9 isPresenting: false, 10 showNotes: false, 11 presentationStartTime: null as number | null, 12 + presenterPanelHeight: 80, // default height in pixels 13 }); 14 15 export const getCurrentSlide = (): Slide | null => {
+62 -6
src/routes/+page.svelte
··· 1 <script lang="ts"> 2 import { onMount } from "svelte"; 3 import { goto } from "$app/navigation"; 4 - import { editorState, newDeck } from "$lib/state.svelte"; 5 import { auth, initAuth, doDeleteDeck } from "$lib/auth.svelte"; 6 - import type { Deck } from "$lib/api"; 7 import Toolbar from "$lib/components/Toolbar.svelte"; 8 import SlideList from "$lib/components/SlideList.svelte"; 9 import SlideCanvas from "$lib/components/SlideCanvas.svelte"; ··· 56 e.stopPropagation(); 57 confirmDelete = null; 58 }; 59 </script> 60 61 <svelte:head> ··· 70 </svg> 71 </div> 72 {:else if editorState.isPresenting} 73 - <div class="presenter-canvas"> 74 <SlideCanvas /> 75 <PresenterView /> 76 </div> ··· 118 </div> 119 {:else} 120 <button 121 class="share-btn" 122 class:copied={copied === deck.rkey} 123 onclick={(e) => handleShare(e, deck)} ··· 329 font-size: 13px; 330 } 331 332 - .share-btn, .delete-btn { 333 padding: 8px; 334 background: transparent; 335 border: none; ··· 340 341 .delete-btn { 342 margin-right: 8px; 343 } 344 345 .share-btn:hover { ··· 440 display: flex; 441 align-items: center; 442 justify-content: center; 443 - padding-bottom: 80px; 444 } 445 446 .presenter-canvas :global(.canvas) { 447 width: 100vw; 448 - height: calc(100vh - 80px); 449 max-width: 100%; 450 max-height: 100%; 451 }
··· 1 <script lang="ts"> 2 import { onMount } from "svelte"; 3 import { goto } from "$app/navigation"; 4 + import { editorState, newDeck, loadDeck, startPresenting } from "$lib/state.svelte"; 5 import { auth, initAuth, doDeleteDeck } from "$lib/auth.svelte"; 6 + import { getDeck, type Deck } from "$lib/api"; 7 import Toolbar from "$lib/components/Toolbar.svelte"; 8 import SlideList from "$lib/components/SlideList.svelte"; 9 import SlideCanvas from "$lib/components/SlideCanvas.svelte"; ··· 56 e.stopPropagation(); 57 confirmDelete = null; 58 }; 59 + 60 + let presenting = $state<string | null>(null); 61 + 62 + const handlePresent = async (e: MouseEvent, deck: Deck) => { 63 + e.stopPropagation(); 64 + if (!deck.rkey || !deck.repo) return; 65 + 66 + presenting = deck.rkey; 67 + try { 68 + const fullDeck = await getDeck(deck.repo, deck.rkey); 69 + if (fullDeck) { 70 + loadDeck(fullDeck); 71 + startPresenting(); 72 + } 73 + } finally { 74 + presenting = null; 75 + } 76 + }; 77 </script> 78 79 <svelte:head> ··· 88 </svg> 89 </div> 90 {:else if editorState.isPresenting} 91 + <div 92 + class="presenter-canvas" 93 + style="--panel-height: {editorState.presenterPanelHeight}px;" 94 + > 95 <SlideCanvas /> 96 <PresenterView /> 97 </div> ··· 139 </div> 140 {:else} 141 <button 142 + class="present-btn" 143 + onclick={(e) => handlePresent(e, deck)} 144 + disabled={presenting === deck.rkey} 145 + title="Present" 146 + > 147 + {#if presenting === deck.rkey} 148 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spinning"> 149 + <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/> 150 + </svg> 151 + {:else} 152 + <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 153 + <path d="M8 5v14l11-7z"/> 154 + </svg> 155 + {/if} 156 + </button> 157 + <button 158 class="share-btn" 159 class:copied={copied === deck.rkey} 160 onclick={(e) => handleShare(e, deck)} ··· 366 font-size: 13px; 367 } 368 369 + .present-btn, .share-btn, .delete-btn { 370 padding: 8px; 371 background: transparent; 372 border: none; ··· 377 378 .delete-btn { 379 margin-right: 8px; 380 + } 381 + 382 + .present-btn:hover { 383 + color: #10b981; 384 + background: rgba(16, 185, 129, 0.1); 385 + } 386 + 387 + .present-btn:disabled { 388 + opacity: 0.5; 389 + cursor: wait; 390 + } 391 + 392 + .present-btn .spinning { 393 + animation: spin 1s linear infinite; 394 + } 395 + 396 + @keyframes spin { 397 + from { transform: rotate(0deg); } 398 + to { transform: rotate(360deg); } 399 } 400 401 .share-btn:hover { ··· 496 display: flex; 497 align-items: center; 498 justify-content: center; 499 + padding-bottom: var(--panel-height, 80px); 500 } 501 502 .presenter-canvas :global(.canvas) { 503 width: 100vw; 504 + height: calc(100vh - var(--panel-height, 80px)); 505 max-width: 100%; 506 max-height: 100%; 507 }