powerpointproto slides.waow.tech
slides

add notes editor and clickable links in presentation mode

- linkify URLs in presentation mode (opens in new tab)
- notes editor panel below canvas for speaker notes
- notes visible during presentation via toggle

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

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

+102 -2
+70
src/lib/components/NotesEditor.svelte
··· 1 + <script lang="ts"> 2 + import { getCurrentSlide } from "$lib/state.svelte"; 3 + 4 + let notes = $state(""); 5 + 6 + // Sync with current slide 7 + $effect(() => { 8 + const slide = getCurrentSlide(); 9 + notes = slide?.notes || ""; 10 + }); 11 + 12 + const handleInput = (e: Event) => { 13 + const slide = getCurrentSlide(); 14 + if (slide) { 15 + slide.notes = (e.target as HTMLTextAreaElement).value; 16 + } 17 + }; 18 + </script> 19 + 20 + <div class="notes-editor"> 21 + <label for="notes">notes</label> 22 + <textarea 23 + id="notes" 24 + bind:value={notes} 25 + oninput={handleInput} 26 + placeholder="speaker notes for this slide..." 27 + ></textarea> 28 + </div> 29 + 30 + <style> 31 + .notes-editor { 32 + display: flex; 33 + flex-direction: column; 34 + gap: 6px; 35 + padding: 12px; 36 + background: #111; 37 + border-top: 1px solid #222; 38 + min-height: 100px; 39 + max-height: 180px; 40 + } 41 + 42 + label { 43 + font-size: 11px; 44 + color: #666; 45 + text-transform: uppercase; 46 + letter-spacing: 0.5px; 47 + } 48 + 49 + textarea { 50 + flex: 1; 51 + background: #0a0a0a; 52 + border: 1px solid #222; 53 + border-radius: 4px; 54 + color: #ccc; 55 + font-size: 13px; 56 + padding: 10px; 57 + resize: none; 58 + font-family: inherit; 59 + line-height: 1.5; 60 + } 61 + 62 + textarea:focus { 63 + outline: none; 64 + border-color: #333; 65 + } 66 + 67 + textarea::placeholder { 68 + color: #444; 69 + } 70 + </style>
+18
src/lib/components/SlideCanvas.svelte
··· 13 13 14 14 const scale = (val: number) => `${val / 10}%`; 15 15 16 + // linkify text for presentation mode 17 + const linkify = (text: string): string => { 18 + const urlRegex = /(https?:\/\/[^\s<]+)/g; 19 + return text.replace(urlRegex, '<a href="$1" target="_blank" rel="noopener">$1</a>'); 20 + }; 21 + 16 22 // Selection is handled entirely in handleMouseDown to avoid double-firing 17 23 18 24 let draggingIndex = $state<number | null>(null); ··· 321 327 role="textbox" 322 328 tabindex="0" 323 329 ></div> 330 + {:else if editorState.isPresenting} 331 + <span class="text-content" style={element.fontFamily ? `font-family: ${element.fontFamily};` : ''}>{@html linkify(element.content || "")}</span> 324 332 {:else} 325 333 <span class="text-content" style={element.fontFamily ? `font-family: ${element.fontFamily};` : ''}>{element.content || "double-click to edit"}</span> 326 334 {/if} ··· 412 420 413 421 .canvas.presenting .element { 414 422 cursor: default; 423 + } 424 + 425 + .canvas.presenting .text-content :global(a) { 426 + color: var(--accent, #6366f1); 427 + text-decoration: underline; 428 + cursor: pointer; 429 + } 430 + 431 + .canvas.presenting .text-content :global(a:hover) { 432 + opacity: 0.8; 415 433 } 416 434 417 435 .text-editor {
+14 -2
src/routes/deck/[rkey]/+page.svelte
··· 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"; 10 + import NotesEditor from "$lib/components/NotesEditor.svelte"; 10 11 import PresenterView from "$lib/components/PresenterView.svelte"; 11 12 12 13 let { data } = $props(); ··· 92 93 <Toolbar /> 93 94 <div class="workspace"> 94 95 <SlideList /> 95 - <div class="canvas-area"> 96 - <SlideCanvas /> 96 + <div class="main-panel"> 97 + <div class="canvas-area"> 98 + <SlideCanvas /> 99 + </div> 100 + <NotesEditor /> 97 101 </div> 98 102 </div> 99 103 </div> ··· 165 169 overflow: hidden; 166 170 } 167 171 172 + .main-panel { 173 + flex: 1; 174 + display: flex; 175 + flex-direction: column; 176 + overflow: hidden; 177 + } 178 + 168 179 .canvas-area { 169 180 flex: 1; 170 181 display: flex; ··· 172 183 justify-content: center; 173 184 padding: 20px; 174 185 background: #0a0a0a; 186 + min-height: 0; 175 187 } 176 188 177 189 @media (max-width: 768px) {