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 9 10 10 let elapsed = $state("00:00"); 11 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; 12 18 13 19 $effect(() => { 14 20 if (editorState.isPresenting && editorState.presentationStartTime) { ··· 40 46 break; 41 47 } 42 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 + }; 43 68 </script> 44 69 45 - <svelte:window onkeydown={handleKeydown} /> 70 + <svelte:window 71 + onkeydown={handleKeydown} 72 + onmousemove={handleDragMove} 73 + onmouseup={handleDragEnd} 74 + ontouchmove={handleDragMove} 75 + ontouchend={handleDragEnd} 76 + /> 46 77 47 78 {#if editorState.isPresenting} 48 - <div class="presenter-overlay"> 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 + 49 93 <div class="controls"> 50 94 <button onclick={prevSlide}>←</button> 51 95 <span class="slide-counter"> ··· 56 100 <button onclick={stopPresenting}>exit</button> 57 101 </div> 58 102 59 - {#if editorState.showNotes && getCurrentSlide()?.notes} 103 + {#if editorState.showNotes} 60 104 <div class="notes"> 61 - <strong>notes:</strong> 62 - <p>{getCurrentSlide()?.notes}</p> 105 + {#if getCurrentSlide()?.notes} 106 + <p>{getCurrentSlide()?.notes}</p> 107 + {:else} 108 + <p class="no-notes">no notes for this slide</p> 109 + {/if} 63 110 </div> 64 111 {/if} 65 112 ··· 78 125 bottom: 0; 79 126 left: 0; 80 127 right: 0; 81 - background: rgba(0, 0, 0, 0.9); 82 - padding: 12px 20px; 128 + background: rgba(0, 0, 0, 0.95); 83 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; 84 162 } 85 163 86 164 .controls { ··· 88 166 align-items: center; 89 167 justify-content: center; 90 168 gap: 16px; 169 + padding: 16px 20px 8px; 91 170 } 92 171 93 172 .controls button { ··· 118 197 } 119 198 120 199 .notes { 121 - margin-top: 12px; 200 + flex: 1; 201 + margin: 8px 20px 12px; 122 202 padding: 12px; 123 - background: #222; 203 + background: #1a1a1a; 124 204 border-radius: 4px; 125 205 color: #ccc; 126 - max-height: 100px; 127 206 overflow-y: auto; 128 - } 129 - 130 - .notes strong { 131 - color: #888; 132 - font-size: 12px; 133 - text-transform: uppercase; 207 + min-height: 0; 134 208 } 135 209 136 210 .notes p { 137 - margin: 8px 0 0; 211 + margin: 0; 138 212 white-space: pre-wrap; 213 + line-height: 1.5; 214 + } 215 + 216 + .notes .no-notes { 217 + color: #666; 218 + font-style: italic; 139 219 } 140 220 141 221 .notes-toggle { 142 222 position: absolute; 143 223 right: 20px; 144 - bottom: 12px; 224 + top: 16px; 145 225 padding: 6px 12px; 146 226 background: transparent; 147 227 border: 1px solid #555; ··· 149 229 cursor: pointer; 150 230 font-size: 12px; 151 231 border-radius: 4px; 232 + } 233 + 234 + .notes-toggle:hover { 235 + border-color: #777; 236 + color: #ccc; 152 237 } 153 238 </style>
+1
src/lib/state.svelte.ts
··· 9 9 isPresenting: false, 10 10 showNotes: false, 11 11 presentationStartTime: null as number | null, 12 + presenterPanelHeight: 80, // default height in pixels 12 13 }); 13 14 14 15 export const getCurrentSlide = (): Slide | null => {
+62 -6
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from "svelte"; 3 3 import { goto } from "$app/navigation"; 4 - import { editorState, newDeck } from "$lib/state.svelte"; 4 + import { editorState, newDeck, loadDeck, startPresenting } from "$lib/state.svelte"; 5 5 import { auth, initAuth, doDeleteDeck } from "$lib/auth.svelte"; 6 - import type { Deck } from "$lib/api"; 6 + import { getDeck, type Deck } from "$lib/api"; 7 7 import Toolbar from "$lib/components/Toolbar.svelte"; 8 8 import SlideList from "$lib/components/SlideList.svelte"; 9 9 import SlideCanvas from "$lib/components/SlideCanvas.svelte"; ··· 56 56 e.stopPropagation(); 57 57 confirmDelete = null; 58 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 + }; 59 77 </script> 60 78 61 79 <svelte:head> ··· 70 88 </svg> 71 89 </div> 72 90 {:else if editorState.isPresenting} 73 - <div class="presenter-canvas"> 91 + <div 92 + class="presenter-canvas" 93 + style="--panel-height: {editorState.presenterPanelHeight}px;" 94 + > 74 95 <SlideCanvas /> 75 96 <PresenterView /> 76 97 </div> ··· 118 139 </div> 119 140 {:else} 120 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 121 158 class="share-btn" 122 159 class:copied={copied === deck.rkey} 123 160 onclick={(e) => handleShare(e, deck)} ··· 329 366 font-size: 13px; 330 367 } 331 368 332 - .share-btn, .delete-btn { 369 + .present-btn, .share-btn, .delete-btn { 333 370 padding: 8px; 334 371 background: transparent; 335 372 border: none; ··· 340 377 341 378 .delete-btn { 342 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); } 343 399 } 344 400 345 401 .share-btn:hover { ··· 440 496 display: flex; 441 497 align-items: center; 442 498 justify-content: center; 443 - padding-bottom: 80px; 499 + padding-bottom: var(--panel-height, 80px); 444 500 } 445 501 446 502 .presenter-canvas :global(.canvas) { 447 503 width: 100vw; 448 - height: calc(100vh - 80px); 504 + height: calc(100vh - var(--panel-height, 80px)); 449 505 max-width: 100%; 450 506 max-height: 100%; 451 507 }