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

Configure Feed

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

Merge pull request 'feat(slides): mobile panel toggle for phones (v0.54.0 first slice)' (#401) from feat/v0.54.0-mobile-viability into main

scott d4546a68 5217b575

+106
+1
CHANGELOG.md
··· 8 8 ## [Unreleased] 9 9 10 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) 11 12 - Shared export/import toast helper (`src/lib/export-feedback.ts`): `exportSuccess`/`exportError`/`importSuccess`/`importError` centralize user-facing copy with built-in noun pluralization and Error-message extraction, so every editor's export path surfaces the same phrasing ("Exported 12 rows as CSV", "CSV export failed: bad quote") instead of each editor re-implementing feedback. 12 unit tests pinning the contract. (#686) 12 13 - Slides: JSON deck export — the previously dead "Export" button in slides now downloads the full deck state (slides, theme, transitions, animations) as a `.deck.json` file. Enables backup/clone-a-deck workflows until native PPTX/PDF export lands. (#686) 13 14
+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 // --- Export (JSON deck backup) --- 227 253 function exportDeckAsJson(): number { 228 254 const title = refs.deckTitle.value.trim() || 'Untitled Presentation';
+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 + });