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 'fix: PDS sync UX — multiple modals, Local text, sync feedback' (#19) from fix/pds-sync-race into main

scott 0dd2f362 0545e707

+92 -45
+1 -1
src/calendar/index.html
··· 30 30 </div> 31 31 <div class="status-indicator" id="status"> 32 32 <span class="status-dot connected" id="status-dot"></span> 33 - <span id="status-text">Local</span> 33 + <span id="status-text">Saved</span> 34 34 </div> 35 35 <button class="btn-icon" id="btn-cal-settings" title="Calendar settings" aria-label="Calendar settings"> 36 36 <svg class="tb-icon icon-gear" viewBox="0 0 16 16" style="width:16px;height:16px" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.57 6.24 L14.57 9.76 L12.44 9.19 L11.98 10.30 L13.89 11.40 L11.40 13.89 L10.30 11.98 L9.19 12.44 L9.76 14.57 L6.24 14.57 L6.81 12.44 L5.70 11.98 L4.60 13.89 L2.11 11.40 L4.02 10.30 L3.56 9.19 L1.43 9.76 L1.43 6.24 L3.56 6.81 L4.02 5.70 L2.11 4.60 L4.60 2.11 L5.70 4.02 L6.81 3.56 L6.24 1.43 L9.76 1.43 L9.19 3.56 L10.30 4.02 L11.40 2.11 L13.89 4.60 L11.98 5.70 L12.44 6.81 Z"/><circle cx="8" cy="8" r="2.2"/></svg>
+1 -1
src/diagrams/index.html
··· 41 41 </div> 42 42 <div class="status-indicator" id="status"> 43 43 <span class="status-dot connected" id="status-dot"></span> 44 - <span id="status-text">Local</span> 44 + <span id="status-text">Saved</span> 45 45 </div> 46 46 </div> 47 47
+1 -1
src/docs/index.html
··· 69 69 </div> 70 70 <div class="status-indicator" id="status"> 71 71 <span class="status-dot connected" id="status-dot"></span> 72 - <span id="status-text">Local</span> 72 + <span id="status-text">Saved</span> 73 73 </div> 74 74 <button class="btn-icon" id="btn-shortcuts" title="Keyboard shortcuts">?</button> 75 75 <button class="theme-toggle" id="theme-toggle" title="Toggle dark mode"></button>
+1 -1
src/forms/index.html
··· 31 31 </div> 32 32 <div class="status-indicator" id="status"> 33 33 <span class="status-dot connected" id="status-dot"></span> 34 - <span id="status-text">Local</span> 34 + <span id="status-text">Saved</span> 35 35 </div> 36 36 <button class="btn-icon" id="btn-ai-chat" title="AI Chat (Cmd+Shift+L)"> 37 37 <svg class="tb-icon" viewBox="0 0 16 16" style="width:16px;height:16px" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5l-3 3V4a1 1 0 0 1 1-1z"/><circle cx="5.5" cy="7.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="7.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="10.5" cy="7.5" r="0.5" fill="currentColor" stroke="none"/></svg>
+1 -1
src/index.html
··· 129 129 <footer class="site-footer"> 130 130 <div class="encryption-bar"> 131 131 <span class="encryption-dot"></span> 132 - <span>All documents are stored locally in your browser.</span> 132 + <span id="storage-status-text">All documents are stored locally in your browser.</span> 133 133 <button class="instance-info-btn" id="instance-info-btn" title="About this instance" aria-label="About this instance">?</button> 134 134 </div> 135 135 <div class="site-footer-links">
+2
src/landing.ts
··· 349 349 const { ensurePdsIdentity } = await import('./lib/pds-setup.js'); 350 350 const ready = await ensurePdsIdentity(); 351 351 if (ready) { 352 + const storageText = document.getElementById('storage-status-text'); 353 + if (storageText) storageText.textContent = 'Documents are encrypted and synced to your PDS.'; 352 354 const { pushLocalDocuments } = await import('./lib/pds-push-sync.js'); 353 355 await pushLocalDocuments(); 354 356 const { pullRemoteDocuments } = await import('./lib/pds-pull-sync.js');
+38 -22
src/lib/pds-setup.ts
··· 10 10 */ 11 11 12 12 import { getSession, initAuth, type AtmosSession } from './auth.js'; 13 - import { getInstanceInfo } from './instance-info.js'; 14 13 import { getSetupStatus, setupIdentity, recoverIdentity } from './pds-documents.js'; 15 14 import { showPassphraseModal } from './key-passphrase.js'; 16 15 17 16 let _initialized = false; 17 + let _initPromise: Promise<boolean> | null = null; 18 18 19 19 /** 20 20 * Run the PDS identity check after sign-in. 21 21 * Returns true if identity is ready for PDS operations, false if sync is disabled 22 - * or user cancelled setup. 22 + * or user cancelled setup. Cached — safe to call multiple times. 23 23 */ 24 - export async function ensurePdsIdentity(): Promise<boolean> { 25 - const info = await getInstanceInfo(); 26 - if (!info.features.sync) return false; 24 + export function ensurePdsIdentity(): Promise<boolean> { 25 + if (_initialized) return Promise.resolve(true); 26 + if (_initPromise) return _initPromise; 27 + _initPromise = _doEnsurePdsIdentity(); 28 + return _initPromise; 29 + } 27 30 28 - let session = getSession(); 29 - if (!session) { 30 - session = await initAuth(); 31 - } 32 - if (!session) return false; 31 + async function _doEnsurePdsIdentity(): Promise<boolean> { 32 + try { 33 + let session = getSession(); 34 + if (!session) { 35 + session = await initAuth(); 36 + } 37 + if (!session) return false; 33 38 34 - const status = await getSetupStatus(session.agent, session.did); 39 + const status = await getSetupStatus(session.agent, session.did); 35 40 36 - switch (status.state) { 37 - case 'no-sync': 38 - case 'no-session': 39 - return false; 41 + switch (status.state) { 42 + case 'no-sync': 43 + case 'no-session': 44 + return false; 40 45 41 - case 'ready': 42 - _initialized = true; 43 - return true; 46 + case 'ready': 47 + _initialized = true; 48 + return true; 44 49 45 - case 'needs-recovery': 46 - return await promptRecovery(session); 50 + case 'needs-recovery': 51 + return await promptRecovery(session); 47 52 48 - case 'needs-setup': 49 - return await promptSetup(session); 53 + case 'needs-setup': 54 + return await promptSetup(session); 55 + } 56 + } finally { 57 + if (!_initialized) _initPromise = null; 50 58 } 51 59 } 52 60 ··· 57 65 try { 58 66 await setupIdentity(session.agent, session.did, passphrase); 59 67 _initialized = true; 68 + notify('PDS sync enabled — your documents will sync across devices.'); 60 69 return true; 61 70 } catch (err) { 62 71 console.error('Identity setup failed:', err); 72 + notify('PDS sync setup failed. Check console for details.', true); 63 73 return false; 64 74 } 65 75 } ··· 71 81 try { 72 82 await recoverIdentity(session.agent, session.did, passphrase); 73 83 _initialized = true; 84 + notify('Keys recovered — PDS sync active.'); 74 85 return true; 75 86 } catch (err) { 76 87 console.error('Key recovery failed:', err); 88 + notify('Key recovery failed — wrong passphrase?', true); 77 89 return false; 78 90 } 91 + } 92 + 93 + function notify(msg: string, isError = false): void { 94 + import('../landing-toast.js').then(m => m.showToast(msg, isError ? 5000 : 3000, isError)).catch(() => {}); 79 95 } 80 96 81 97 export function isPdsReady(): boolean {
+22 -9
src/lib/status-chips.ts
··· 1 1 /** 2 - * Status chips — "Local" indicator rendered next to the shared "Saved" 3 - * indicator in every editor's topbar. 2 + * Status chips — save/sync indicator rendered in every editor's topbar. 4 3 * 5 - * In local-only mode the status chip stays at "Local" (green dot). 6 - * This helper wires the chip to provider events so it can reflect 7 - * save errors if they occur. 4 + * Reflects actual state: "Saved" for local-only, "Synced" when PDS sync 5 + * succeeds, "Saving…" during save, "Error" on failure. 8 6 */ 9 7 8 + import { isPdsReady } from './pds-setup.js'; 9 + 10 10 export interface StatusChipsDeps { 11 11 provider: { on: (event: any, handler: any) => void }; 12 12 } ··· 15 15 const statusDot = document.getElementById('status-dot'); 16 16 const statusText = document.getElementById('status-text'); 17 17 18 - // Host page hasn't opted in — no-op gracefully so the helper is safe to 19 - // call from every editor regardless of markup. 20 18 if (!statusDot || !statusText) return; 21 19 22 20 deps.provider.on('status', (payload: { connected: boolean }) => { 23 21 statusDot.classList.toggle('connected', payload.connected); 24 - statusText.textContent = payload.connected ? 'Local' : 'Error'; 25 22 }); 26 23 27 24 deps.provider.on('sync', () => { 28 - statusText.textContent = 'Local'; 29 25 statusDot.classList.add('connected'); 26 + statusText.textContent = isPdsReady() ? 'Synced' : 'Saved'; 27 + }); 28 + 29 + deps.provider.on('save-status', (payload: { status: string }) => { 30 + switch (payload.status) { 31 + case 'saving': 32 + statusText.textContent = 'Saving\u2026'; 33 + break; 34 + case 'saved': 35 + statusText.textContent = isPdsReady() ? 'Synced' : 'Saved'; 36 + statusDot.classList.add('connected'); 37 + break; 38 + case 'error': 39 + statusText.textContent = 'Error'; 40 + statusDot.classList.remove('connected'); 41 + break; 42 + } 30 43 }); 31 44 }
+1 -1
src/sheets/index.html
··· 44 44 </div> 45 45 <div class="status-indicator" id="status"> 46 46 <span class="status-dot connected" id="status-dot"></span> 47 - <span id="status-text">Local</span> 47 + <span id="status-text">Saved</span> 48 48 </div> 49 49 <button class="btn-icon" id="btn-shortcuts" title="Keyboard shortcuts">?</button> 50 50 <button class="theme-toggle" id="theme-toggle" title="Toggle dark mode"></button>
+1 -1
src/slides/index.html
··· 32 32 </div> 33 33 <div class="status-indicator" id="status"> 34 34 <span class="status-dot connected" id="status-dot"></span> 35 - <span id="status-text">Local</span> 35 + <span id="status-text">Saved</span> 36 36 </div> 37 37 <button class="btn-secondary" id="btn-present" title="Present (F5)">&#9655; Present</button> 38 38 <button class="btn-secondary" id="btn-export" title="Export">Export</button>
+23 -7
tests/status-chips.test.ts
··· 37 37 it('flips status-dot .connected on provider status events', () => { 38 38 document.body.innerHTML = ` 39 39 <span id="status-dot"></span> 40 - <span id="status-text">Connecting…</span> 40 + <span id="status-text">Saved</span> 41 41 `; 42 42 const provider = mockProvider(); 43 43 wireStatusChips({ provider }); 44 44 45 45 const dot = document.getElementById('status-dot')!; 46 - const text = document.getElementById('status-text')!; 47 46 expect(dot.classList.contains('connected')).toBe(false); 48 47 49 48 provider.emit('status', { connected: true }); 50 49 expect(dot.classList.contains('connected')).toBe(true); 51 - expect(text.textContent).toBe('Local'); 52 50 53 51 provider.emit('status', { connected: false }); 54 52 expect(dot.classList.contains('connected')).toBe(false); 55 - expect(text.textContent).toBe('Error'); 56 53 }); 57 54 58 - it('sets status-text to "Local" on provider sync event and marks dot connected', () => { 55 + it('sets status-text to "Saved" on provider sync event and marks dot connected', () => { 59 56 document.body.innerHTML = ` 60 57 <span id="status-dot"></span> 61 - <span id="status-text">Local</span> 58 + <span id="status-text">Saved</span> 62 59 `; 63 60 const provider = mockProvider(); 64 61 wireStatusChips({ provider }); 65 62 66 63 provider.emit('sync'); 67 - expect(document.getElementById('status-text')!.textContent).toBe('Local'); 64 + expect(document.getElementById('status-text')!.textContent).toBe('Saved'); 68 65 expect(document.getElementById('status-dot')!.classList.contains('connected')).toBe(true); 66 + }); 67 + 68 + it('shows save-status transitions correctly', () => { 69 + document.body.innerHTML = ` 70 + <span id="status-dot"></span> 71 + <span id="status-text">Saved</span> 72 + `; 73 + const provider = mockProvider(); 74 + wireStatusChips({ provider }); 75 + 76 + provider.emit('save-status', { status: 'saving' }); 77 + expect(document.getElementById('status-text')!.textContent).toBe('Saving\u2026'); 78 + 79 + provider.emit('save-status', { status: 'saved' }); 80 + expect(document.getElementById('status-text')!.textContent).toBe('Saved'); 81 + 82 + provider.emit('save-status', { status: 'error' }); 83 + expect(document.getElementById('status-text')!.textContent).toBe('Error'); 84 + expect(document.getElementById('status-dot')!.classList.contains('connected')).toBe(false); 69 85 }); 70 86 71 87 it('is a no-op when the host page has no status-indicator markup', () => {