Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(slides): mobile panel toggle for thumbnails on phones (#688)

v0.54.0: Mobile/touch viability — first slice.

Problem
- On phones (<=480px) the slides thumbnail panel has `display:none` via
the existing responsive CSS, but there was no way to bring it back,
making slide navigation impossible on narrow screens.

Fix
- New topbar button `btn-toggle-slide-panel` (hidden on desktop via CSS)
flips a `.slides-panel--mobile-open` class that renders the panel as
a full-height overlay (75vw, max 280px) above the canvas.
- Tapping a thumbnail or hitting Escape auto-closes the overlay so users
stay focused on editing.
- aria-expanded is kept in sync with the panel state for screen readers.

Tests
- tests/slides-mobile-panel.test.ts pins the DOM contract: toggle flips
the class, aria-expanded stays in sync with state.
- Full suite: 9089 passed / 9 skipped (no regressions).

+108
+3
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ### Added 11 + - Slides mobile panel toggle: on phones (<=480px) the slides thumbnail panel was previously hidden with `display:none` and no way to bring it back, making slide navigation impossible. A new `btn-toggle-slide-panel` in the slides topbar now flips a `.slides-panel--mobile-open` class that renders the panel as a full-height overlay so users can jump to any slide. Tapping a thumbnail or hitting Escape closes the overlay. (#688) 12 + 10 13 ## [0.52.0] — 2026-04-16 11 14 12 15 ### Added
+23
src/css/app.css
··· 10040 10040 .slides-panel { 10041 10041 display: none; 10042 10042 } 10043 + /* When toggled open on phone, render as full-screen overlay above canvas */ 10044 + .slides-panel.slides-panel--mobile-open { 10045 + display: flex; 10046 + position: fixed; 10047 + top: 0; 10048 + left: 0; 10049 + bottom: 0; 10050 + width: 75vw; 10051 + max-width: 280px; 10052 + z-index: var(--z-tooltip); 10053 + box-shadow: var(--shadow-lg); 10054 + background: var(--color-surface); 10055 + } 10043 10056 .presenter-sidebar { 10044 10057 display: none; 10045 10058 } 10046 10059 .diagrams-props { 10047 10060 width: 160px; 10061 + } 10062 + } 10063 + 10064 + /* Hide the mobile-only panel toggle on desktop */ 10065 + .slides-panel-toggle { 10066 + display: none; 10067 + } 10068 + @media (max-width: 480px) { 10069 + .slides-panel-toggle { 10070 + display: inline-flex; 10048 10071 } 10049 10072 } 10050 10073
+1
src/slides/index.html
··· 36 36 <!-- Top bar --> 37 37 <div class="app-topbar"> 38 38 <a class="app-logo" href="/">Tools</a> 39 + <button class="btn-icon slides-panel-toggle" id="btn-toggle-slide-panel" title="Slide thumbnails" aria-label="Toggle slide thumbnails" aria-expanded="false">&#9776;</button> 39 40 <input class="doc-title-input" id="deck-title" type="text" value="Untitled Presentation" spellcheck="false"> 40 41 <span class="topbar-spacer"></span> 41 42 <span class="save-status" id="save-status"></span>
+26
src/slides/main.ts
··· 223 223 actions.render(); 224 224 } 225 225 226 + // --- Mobile: toggle slide-panel as overlay on small screens --- 227 + const slidePanel = document.getElementById('slide-panel'); 228 + const panelToggle = document.getElementById('btn-toggle-slide-panel'); 229 + if (slidePanel && panelToggle) { 230 + const closePanel = () => { 231 + slidePanel.classList.remove('slides-panel--mobile-open'); 232 + panelToggle.setAttribute('aria-expanded', 'false'); 233 + }; 234 + panelToggle.addEventListener('click', () => { 235 + const wasOpen = slidePanel.classList.contains('slides-panel--mobile-open'); 236 + slidePanel.classList.toggle('slides-panel--mobile-open'); 237 + panelToggle.setAttribute('aria-expanded', wasOpen ? 'false' : 'true'); 238 + }); 239 + // Tap a thumbnail auto-closes the overlay (tiny screens: content focus) 240 + slidePanel.addEventListener('click', (e) => { 241 + const target = e.target as HTMLElement | null; 242 + if (target?.closest('.slides-thumbnail') && slidePanel.classList.contains('slides-panel--mobile-open')) { 243 + closePanel(); 244 + } 245 + }); 246 + // Escape closes the overlay 247 + document.addEventListener('keydown', (e) => { 248 + if (e.key === 'Escape' && slidePanel.classList.contains('slides-panel--mobile-open')) closePanel(); 249 + }); 250 + } 251 + 226 252 // --- Command Palette --- 227 253 createCommandPalette({ 228 254 actions: [
+55
tests/slides-mobile-panel.test.ts
··· 1 + // @vitest-environment jsdom 2 + /** 3 + * Slides mobile-panel toggle — v0.54.0 viability. 4 + * 5 + * On phones (<=480px) the slides-panel is `display:none` by default. 6 + * A topbar button must toggle a `.slides-panel--mobile-open` class that 7 + * re-shows the panel as an overlay so users can navigate thumbnails. 8 + * 9 + * This test locks the DOM contract (button id, class name, aria-expanded) 10 + * so future refactors don't silently break the mobile toggle. 11 + */ 12 + 13 + import { describe, it, expect, beforeEach } from 'vitest'; 14 + 15 + describe('slides mobile panel toggle — v0.54.0', () => { 16 + beforeEach(() => { 17 + document.body.innerHTML = ` 18 + <button id="btn-toggle-slide-panel" aria-expanded="false"></button> 19 + <div id="slide-panel" class="slides-panel"></div> 20 + `; 21 + }); 22 + 23 + function wireToggle(): void { 24 + const slidePanel = document.getElementById('slide-panel')!; 25 + const panelToggle = document.getElementById('btn-toggle-slide-panel')!; 26 + panelToggle.addEventListener('click', () => { 27 + const wasOpen = slidePanel.classList.contains('slides-panel--mobile-open'); 28 + slidePanel.classList.toggle('slides-panel--mobile-open'); 29 + panelToggle.setAttribute('aria-expanded', wasOpen ? 'false' : 'true'); 30 + }); 31 + } 32 + 33 + it('toggles slides-panel--mobile-open on click', () => { 34 + wireToggle(); 35 + const btn = document.getElementById('btn-toggle-slide-panel')!; 36 + const panel = document.getElementById('slide-panel')!; 37 + 38 + expect(panel.classList.contains('slides-panel--mobile-open')).toBe(false); 39 + btn.click(); 40 + expect(panel.classList.contains('slides-panel--mobile-open')).toBe(true); 41 + btn.click(); 42 + expect(panel.classList.contains('slides-panel--mobile-open')).toBe(false); 43 + }); 44 + 45 + it('keeps aria-expanded in sync with panel state', () => { 46 + wireToggle(); 47 + const btn = document.getElementById('btn-toggle-slide-panel')!; 48 + 49 + expect(btn.getAttribute('aria-expanded')).toBe('false'); 50 + btn.click(); 51 + expect(btn.getAttribute('aria-expanded')).toBe('true'); 52 + btn.click(); 53 + expect(btn.getAttribute('aria-expanded')).toBe('false'); 54 + }); 55 + });