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

Configure Feed

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

fix: cross-cutting consistency — DocType, command palette, PWA, meta tags

- Unify DocType to 'doc' | 'sheet' | 'form' | 'slide' | 'diagram' across
workspace.ts, cross-doc-links.ts, and server/index.ts
- Fix countByType using wrong plural keys (docs/sheets/slides → doc/sheet/slide)
- Add command palette (Cmd+K) to sheets, diagrams, slides, and forms editors
- Wire up PWA shortcut actions (?action=new-doc etc) in landing page
- Add 3 missing PWA manifest shortcuts (form, slide, diagram)
- Add missing pages to SW precache, bump cache to v5
- Add theme-color meta and apple-touch-icon to all 6 HTML files
- Add OG meta tags and a11y focus detection to diagrams/slides/forms HTML
- Fix command palette icons and fetchDocuments path/icon maps for all 5 types
- Fix localStorage migration non-null assertions in landing.ts

+180 -37
+15
public/manifest.json
··· 21 21 "name": "New Spreadsheet", 22 22 "url": "/?action=new-sheet", 23 23 "icons": [{ "src": "/favicon.svg", "sizes": "any" }] 24 + }, 25 + { 26 + "name": "New Form", 27 + "url": "/?action=new-form", 28 + "icons": [{ "src": "/favicon.svg", "sizes": "any" }] 29 + }, 30 + { 31 + "name": "New Presentation", 32 + "url": "/?action=new-slide", 33 + "icons": [{ "src": "/favicon.svg", "sizes": "any" }] 34 + }, 35 + { 36 + "name": "New Diagram", 37 + "url": "/?action=new-diagram", 38 + "icons": [{ "src": "/favicon.svg", "sizes": "any" }] 24 39 } 25 40 ], 26 41 "categories": ["productivity", "utilities"]
+4 -1
public/sw.js
··· 8 8 * - API/WS/health: network-only (never cached) 9 9 */ 10 10 11 - const CACHE_NAME = 'tools-v4'; 11 + const CACHE_NAME = 'tools-v5'; 12 12 13 13 // Static assets to pre-cache on install 14 14 const PRECACHE_URLS = [ ··· 16 16 '/index.html', 17 17 '/docs/index.html', 18 18 '/sheets/index.html', 19 + '/diagrams/index.html', 20 + '/slides/index.html', 21 + '/forms/index.html', 19 22 ]; 20 23 21 24 // Extensions we consider "static" (cache-first)
+4 -2
server/index.ts
··· 13 13 14 14 // --- Interfaces --- 15 15 16 + type DocType = 'doc' | 'sheet' | 'form' | 'slide' | 'diagram'; 17 + 16 18 interface DocumentRow { 17 19 id: string; 18 - type: 'doc' | 'sheet'; 20 + type: DocType; 19 21 name_encrypted: string | null; 20 22 snapshot: Buffer | null; 21 23 share_mode: 'edit' | 'view' | null; ··· 27 29 28 30 interface DocumentListRow { 29 31 id: string; 30 - type: 'doc' | 'sheet'; 32 + type: DocType; 31 33 name_encrypted: string | null; 32 34 share_mode: 'edit' | 'view' | null; 33 35 expires_at: string | null;
+12
src/diagrams/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 7 <meta name="description" content="E2EE diagrams and whiteboard. End-to-end encrypted, real-time collaboration."> 8 + <meta property="og:title" content="Tools — Diagrams"> 9 + <meta property="og:description" content="E2EE diagrams and whiteboard. End-to-end encrypted, real-time collaboration."> 10 + <meta property="og:type" content="website"> 11 + <meta property="og:image" content="/favicon.svg"> 8 12 <title>Tools — Diagrams</title> 13 + <meta name="theme-color" content="#3a8a7a"> 9 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 10 16 <link rel="stylesheet" href="../css/app.css"> 11 17 <script> 12 18 (function() { ··· 15 21 document.documentElement.setAttribute('data-theme', saved); 16 22 } 17 23 if (window.electronAPI) document.documentElement.classList.add('is-electron'); 24 + document.addEventListener('keydown', function(e) { 25 + if (e.key === 'Tab') document.documentElement.setAttribute('data-a11y-focus', ''); 26 + }); 27 + document.addEventListener('mousedown', function() { 28 + document.documentElement.removeAttribute('data-a11y-focus'); 29 + }); 18 30 })(); 19 31 </script> 20 32 </head>
+19
src/diagrams/main.ts
··· 2141 2141 render(); 2142 2142 } 2143 2143 2144 + // --- Command Palette --- 2145 + import { createCommandPalette } from '../command-palette.js'; 2146 + createCommandPalette({ 2147 + actions: [ 2148 + { id: 'back', label: 'Back to Documents', category: 'action', icon: '\u2190', action: () => { window.location.href = '/'; } }, 2149 + { id: 'new-doc', label: 'New Document', category: 'action', icon: '\u270e', action: () => { window.open('/', '_blank'); } }, 2150 + { id: 'new-diagram', label: 'New Diagram', category: 'action', icon: '\u25d3', action: () => { window.open('/', '_blank'); } }, 2151 + { id: 'select', label: 'Select Tool', category: 'action', icon: '\u2196', shortcut: 'V', action: () => { document.getElementById('tool-select')?.click(); } }, 2152 + { id: 'hand', label: 'Hand Tool', category: 'action', icon: '\u25ce', shortcut: 'H', action: () => { document.getElementById('tool-hand')?.click(); } }, 2153 + { id: 'rectangle', label: 'Rectangle Tool', category: 'action', icon: '\u25a2', shortcut: 'R', action: () => { document.getElementById('tool-rectangle')?.click(); } }, 2154 + { id: 'ellipse', label: 'Ellipse Tool', category: 'action', icon: '\u25ef', shortcut: 'E', action: () => { document.getElementById('tool-ellipse')?.click(); } }, 2155 + { id: 'arrow', label: 'Arrow Tool', category: 'action', icon: '\u2192', shortcut: 'A', action: () => { document.getElementById('tool-arrow')?.click(); } }, 2156 + { id: 'freehand', label: 'Freehand Tool', category: 'action', icon: '\u2710', shortcut: 'P', action: () => { document.getElementById('tool-freehand')?.click(); } }, 2157 + { id: 'text', label: 'Text Tool', category: 'action', icon: 'T', shortcut: 'T', action: () => { document.getElementById('tool-text')?.click(); } }, 2158 + { id: 'zoom-fit', label: 'Zoom to Fit', category: 'action', icon: '\u21f2', action: () => { document.getElementById('btn-zoom-fit')?.click(); } }, 2159 + { id: 'delete', label: 'Delete Selected', category: 'action', icon: '\u2715', shortcut: 'Del', action: () => { document.getElementById('btn-delete')?.click(); } }, 2160 + ], 2161 + }); 2162 + 2144 2163 init();
+2
src/docs/index.html
··· 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 12 <title>Tools — Docs</title> 13 + <meta name="theme-color" content="#3a8a7a"> 13 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 14 16 <link rel="stylesheet" href="../css/app.css"> 15 17 <script> 16 18 (function() {
+4 -2
src/docs/main.ts
··· 2402 2402 if (doc.id === docId) continue; // skip current doc 2403 2403 const keyStr = keys[doc.id]; 2404 2404 if (!keyStr) continue; 2405 - const path = doc.type === 'doc' ? '/docs' : '/sheets'; 2405 + const pathMap: Record<string, string> = { doc: '/docs', sheet: '/sheets', form: '/forms', slide: '/slides', diagram: '/diagrams' }; 2406 + const iconMap: Record<string, string> = { doc: '\u270e', sheet: '\u25a6', form: '\u2637', slide: '\u25eb', diagram: '\u25d3' }; 2407 + const path = pathMap[doc.type] || '/docs'; 2406 2408 let name = 'Encrypted Document'; 2407 2409 if (keyStr && doc.name_encrypted) { 2408 2410 try { ··· 2415 2417 id: `doc-${doc.id}`, 2416 2418 label: name, 2417 2419 category: 'document', 2418 - icon: doc.type === 'doc' ? '\u270e' : '\u25a6', 2420 + icon: iconMap[doc.type] || '\u25a6', 2419 2421 action: () => { window.location.href = `${path}/${doc.id}#${keyStr}`; }, 2420 2422 }); 2421 2423 }
+12
src/forms/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 7 <meta name="description" content="E2EE form builder. End-to-end encrypted, real-time collaboration."> 8 + <meta property="og:title" content="Tools — Forms"> 9 + <meta property="og:description" content="E2EE form builder. End-to-end encrypted, real-time collaboration."> 10 + <meta property="og:type" content="website"> 11 + <meta property="og:image" content="/favicon.svg"> 8 12 <title>Tools — Forms</title> 13 + <meta name="theme-color" content="#3a8a7a"> 9 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 10 16 <link rel="stylesheet" href="../css/app.css"> 11 17 <script> 12 18 (function() { ··· 15 21 document.documentElement.setAttribute('data-theme', saved); 16 22 } 17 23 if (window.electronAPI) document.documentElement.classList.add('is-electron'); 24 + document.addEventListener('keydown', function(e) { 25 + if (e.key === 'Tab') document.documentElement.setAttribute('data-a11y-focus', ''); 26 + }); 27 + document.addEventListener('mousedown', function() { 28 + document.documentElement.removeAttribute('data-a11y-focus'); 29 + }); 18 30 })(); 19 31 </script> 20 32 </head>
+13
src/forms/main.ts
··· 600 600 } 601 601 } 602 602 603 + // --- Command Palette --- 604 + import { createCommandPalette } from '../command-palette.js'; 605 + createCommandPalette({ 606 + actions: [ 607 + { id: 'back', label: 'Back to Documents', category: 'action', icon: '\u2190', action: () => { window.location.href = '/'; } }, 608 + { id: 'new-form', label: 'New Form', category: 'action', icon: '\u2637', action: () => { window.open('/', '_blank'); } }, 609 + { id: 'add-question', label: 'Add Question', category: 'action', icon: '+', action: () => { document.getElementById('btn-add-question')?.click(); } }, 610 + { id: 'preview', label: 'Preview Form', category: 'action', icon: '\u25b7', action: () => { document.getElementById('btn-preview')?.click(); } }, 611 + { id: 'responses', label: 'View Responses', category: 'action', icon: '\u25a6', action: () => { document.getElementById('btn-responses')?.click(); } }, 612 + { id: 'settings', label: 'Form Settings', category: 'action', icon: '\u2699', action: () => { document.getElementById('btn-settings')?.click(); } }, 613 + ], 614 + }); 615 + 603 616 init();
+2
src/index.html
··· 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 12 <title>Tools — Encrypted Office</title> 13 + <meta name="theme-color" content="#3a8a7a"> 13 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 14 16 <link rel="stylesheet" href="./css/app.css"> 15 17 <script> 16 18 // Theme init — runs before paint to avoid FOUC
+25 -7
src/landing.ts
··· 101 101 102 102 // --- Migrate legacy localStorage keys --- 103 103 if (localStorage.getItem('crypt-keys') && !localStorage.getItem('tools-keys')) { 104 - localStorage.setItem('tools-keys', localStorage.getItem('crypt-keys')); 104 + localStorage.setItem('tools-keys', localStorage.getItem('crypt-keys')!); 105 105 localStorage.removeItem('crypt-keys'); 106 106 } 107 107 if (localStorage.getItem('crypt-username') && !localStorage.getItem('tools-username')) { 108 - localStorage.setItem('tools-username', localStorage.getItem('crypt-username')); 108 + localStorage.setItem('tools-username', localStorage.getItem('crypt-username')!); 109 109 localStorage.removeItem('crypt-username'); 110 110 } 111 111 ··· 1270 1270 id: 'new-form', 1271 1271 label: 'New Form', 1272 1272 category: 'action', 1273 - icon: '\u2610', 1273 + icon: '\u2637', 1274 1274 action: () => createDocument('form'), 1275 1275 }, 1276 1276 { 1277 1277 id: 'new-slide', 1278 1278 label: 'New Presentation', 1279 1279 category: 'action', 1280 - icon: '\u25b6', 1280 + icon: '\u25eb', 1281 1281 action: () => createDocument('slide'), 1282 1282 }, 1283 1283 { 1284 1284 id: 'new-diagram', 1285 1285 label: 'New Diagram', 1286 1286 category: 'action', 1287 - icon: '\u270e', 1287 + icon: '\u25d3', 1288 1288 action: () => createDocument('diagram'), 1289 1289 }, 1290 1290 { 1291 1291 id: 'daily-note', 1292 1292 label: "Today's Note", 1293 1293 category: 'action', 1294 - icon: '\uD83D\uDCC5', 1294 + icon: '\u2666', 1295 1295 action: () => openDailyNote(), 1296 1296 }, 1297 1297 { ··· 1326 1326 id: `doc-${doc.id}`, 1327 1327 label: doc._decryptedName || 'Encrypted Document', 1328 1328 category: 'document' as const, 1329 - icon: doc.type === 'doc' ? '\u270e' : doc.type === 'diagram' ? '\u25c3' : '\u25a6', 1329 + icon: { doc: '\u270e', sheet: '\u25a6', form: '\u2637', slide: '\u25eb', diagram: '\u25d3' }[doc.type] || '\u25a6', 1330 1330 action: () => { window.location.href = `${path}/${doc.id}#${doc._keyStr}`; }, 1331 1331 }; 1332 1332 }); ··· 1357 1357 initUsername(); 1358 1358 syncKeys().then(() => loadDocuments()); 1359 1359 initDesktopDownload(); 1360 + 1361 + // --- Handle PWA shortcut actions (?action=new-doc, etc.) --- 1362 + const urlAction = new URLSearchParams(window.location.search).get('action'); 1363 + if (urlAction) { 1364 + const actionMap: Record<string, () => void> = { 1365 + 'new-doc': () => createDocument('doc'), 1366 + 'new-sheet': () => createDocument('sheet'), 1367 + 'new-form': () => createDocument('form'), 1368 + 'new-slide': () => createDocument('slide'), 1369 + 'new-diagram': () => createDocument('diagram'), 1370 + }; 1371 + const handler = actionMap[urlAction]; 1372 + if (handler) { 1373 + // Clear the action param from URL to prevent re-trigger on refresh 1374 + history.replaceState(null, '', '/'); 1375 + handler(); 1376 + } 1377 + }
+1 -1
src/lib/cross-doc-links.ts
··· 5 5 * Live syncing handled by the collaboration layer. 6 6 */ 7 7 8 - export type DocType = 'doc' | 'sheet' | 'form' | 'slide'; 8 + export type DocType = 'doc' | 'sheet' | 'form' | 'slide' | 'diagram'; 9 9 10 10 export interface CrossDocLink { 11 11 id: string;
+2 -2
src/lib/workspace.ts
··· 6 6 */ 7 7 8 8 export type ViewMode = 'list' | 'grid' | 'board'; 9 - export type DocType = 'docs' | 'sheets' | 'slides'; 9 + export type DocType = 'doc' | 'sheet' | 'form' | 'slide' | 'diagram'; 10 10 11 11 export interface WorkspaceDoc { 12 12 id: string; ··· 311 311 * Get document count by type. 312 312 */ 313 313 export function countByType(state: WorkspaceState): Record<DocType, number> { 314 - const counts: Record<DocType, number> = { docs: 0, sheets: 0, slides: 0 }; 314 + const counts: Record<DocType, number> = { doc: 0, sheet: 0, form: 0, slide: 0, diagram: 0 }; 315 315 for (const doc of state.docs.values()) { 316 316 counts[doc.type]++; 317 317 }
+2
src/sheets/index.html
··· 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 12 <title>Tools — Sheets</title> 13 + <meta name="theme-color" content="#3a8a7a"> 13 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 14 16 <link rel="stylesheet" href="../css/app.css"> 15 17 <script> 16 18 (function() {
+14
src/sheets/main.ts
··· 6312 6312 } 6313 6313 6314 6314 6315 + // --- Command Palette --- 6316 + import { createCommandPalette } from '../command-palette.js'; 6317 + createCommandPalette({ 6318 + actions: [ 6319 + { id: 'back', label: 'Back to Documents', category: 'action', icon: '\u2190', action: () => { window.location.href = '/'; } }, 6320 + { id: 'new-doc', label: 'New Document', category: 'action', icon: '\u270e', action: () => { window.open('/', '_blank'); } }, 6321 + { id: 'new-sheet', label: 'New Spreadsheet', category: 'action', icon: '\u25a6', action: () => { window.open('/', '_blank'); } }, 6322 + { id: 'find', label: 'Find & Replace', category: 'action', icon: '\u2315', shortcut: '\u2318F', action: () => { showFindReplaceBar(false); } }, 6323 + { id: 'export-xlsx', label: 'Export as Excel', category: 'action', icon: '\u2193', action: () => { document.getElementById('btn-export-xlsx')?.click(); } }, 6324 + { id: 'export-csv', label: 'Export as CSV', category: 'action', icon: '\u2193', action: () => { document.getElementById('btn-export-csv')?.click(); } }, 6325 + { id: 'add-sheet', label: 'Add Sheet Tab', category: 'action', icon: '+', action: () => { document.getElementById('add-sheet-btn')?.click(); } }, 6326 + ], 6327 + }); 6328 + 6315 6329 // --- Initial render --- 6316 6330 ensureSheet(0); 6317 6331 renderGrid();
+12
src/slides/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 7 <meta name="description" content="E2EE slide presentations. End-to-end encrypted, real-time collaboration."> 8 + <meta property="og:title" content="Tools — Slides"> 9 + <meta property="og:description" content="E2EE slide presentations. End-to-end encrypted, real-time collaboration."> 10 + <meta property="og:type" content="website"> 11 + <meta property="og:image" content="/favicon.svg"> 8 12 <title>Tools — Slides</title> 13 + <meta name="theme-color" content="#3a8a7a"> 9 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 + <link rel="apple-touch-icon" href="/favicon.svg"> 10 16 <link rel="stylesheet" href="../css/app.css"> 11 17 <script> 12 18 (function() { ··· 15 21 document.documentElement.setAttribute('data-theme', saved); 16 22 } 17 23 if (window.electronAPI) document.documentElement.classList.add('is-electron'); 24 + document.addEventListener('keydown', function(e) { 25 + if (e.key === 'Tab') document.documentElement.setAttribute('data-a11y-focus', ''); 26 + }); 27 + document.addEventListener('mousedown', function() { 28 + document.documentElement.removeAttribute('data-a11y-focus'); 29 + }); 18 30 })(); 19 31 </script> 20 32 </head>
+15
src/slides/main.ts
··· 614 614 render(); 615 615 } 616 616 617 + // --- Command Palette --- 618 + import { createCommandPalette } from '../command-palette.js'; 619 + createCommandPalette({ 620 + actions: [ 621 + { id: 'back', label: 'Back to Documents', category: 'action', icon: '\u2190', action: () => { window.location.href = '/'; } }, 622 + { id: 'new-slide-deck', label: 'New Presentation', category: 'action', icon: '\u25eb', action: () => { window.open('/', '_blank'); } }, 623 + { id: 'present', label: 'Present', category: 'action', icon: '\u25b7', shortcut: 'F5', action: () => { document.getElementById('btn-present')?.click(); } }, 624 + { id: 'add-slide', label: 'Add Slide', category: 'action', icon: '+', action: () => { document.getElementById('btn-add-slide')?.click(); } }, 625 + { id: 'add-text', label: 'Add Text Element', category: 'action', icon: 'T', action: () => { document.getElementById('btn-add-text')?.click(); } }, 626 + { id: 'add-shape', label: 'Add Shape Element', category: 'action', icon: '\u25a0', action: () => { document.getElementById('btn-add-shape')?.click(); } }, 627 + { id: 'add-image', label: 'Add Image Element', category: 'action', icon: '\u25a3', action: () => { document.getElementById('btn-add-image')?.click(); } }, 628 + { id: 'export', label: 'Export Presentation', category: 'action', icon: '\u2193', action: () => { document.getElementById('btn-export')?.click(); } }, 629 + ], 630 + }); 631 + 617 632 init();
+22 -22
tests/workspace.test.ts
··· 50 50 let state = createWorkspaceState(); 51 51 state = addWorkspace(state, 'WS1'); 52 52 const wsId = [...state.workspaces.keys()][0]; 53 - state = registerDoc(state, 'doc-1', 'Notes', 'docs', wsId); 53 + state = registerDoc(state, 'doc-1', 'Notes', 'doc', wsId); 54 54 state = removeWorkspace(state, wsId); 55 55 expect(state.workspaces.size).toBe(0); 56 56 expect(state.docs.get('doc-1')!.workspaceId).toBe(''); ··· 68 68 let state = createWorkspaceState(); 69 69 state = addTag(state, 'Urgent'); 70 70 const tagId = [...state.tags.keys()][0]; 71 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 71 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 72 72 state = tagDoc(state, 'doc-1', tagId); 73 73 state = removeTag(state, tagId); 74 74 expect(state.tags.size).toBe(0); ··· 79 79 describe('registerDoc / updateDoc', () => { 80 80 it('registers a document', () => { 81 81 let state = createWorkspaceState(); 82 - state = registerDoc(state, 'doc-1', 'Budget', 'sheets'); 82 + state = registerDoc(state, 'doc-1', 'Budget', 'sheet'); 83 83 expect(state.docs.size).toBe(1); 84 84 const doc = state.docs.get('doc-1')!; 85 85 expect(doc.title).toBe('Budget'); 86 - expect(doc.type).toBe('sheets'); 86 + expect(doc.type).toBe('sheet'); 87 87 expect(doc.pinned).toBe(false); 88 88 }); 89 89 90 90 it('updates a document', () => { 91 91 let state = createWorkspaceState(); 92 - state = registerDoc(state, 'doc-1', 'Old Title', 'docs'); 92 + state = registerDoc(state, 'doc-1', 'Old Title', 'doc'); 93 93 state = updateDoc(state, 'doc-1', { title: 'New Title' }); 94 94 expect(state.docs.get('doc-1')!.title).toBe('New Title'); 95 95 }); ··· 103 103 describe('togglePin', () => { 104 104 it('pins and unpins a doc', () => { 105 105 let state = createWorkspaceState(); 106 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 106 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 107 107 state = togglePin(state, 'doc-1'); 108 108 expect(state.docs.get('doc-1')!.pinned).toBe(true); 109 109 state = togglePin(state, 'doc-1'); ··· 116 116 let state = createWorkspaceState(); 117 117 state = addTag(state, 'Urgent'); 118 118 const tagId = [...state.tags.keys()][0]; 119 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 119 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 120 120 state = tagDoc(state, 'doc-1', tagId); 121 121 expect(state.docs.get('doc-1')!.tags).toContain(tagId); 122 122 }); ··· 125 125 let state = createWorkspaceState(); 126 126 state = addTag(state, 'Urgent'); 127 127 const tagId = [...state.tags.keys()][0]; 128 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 128 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 129 129 state = tagDoc(state, 'doc-1', tagId); 130 130 state = tagDoc(state, 'doc-1', tagId); 131 131 expect(state.docs.get('doc-1')!.tags).toHaveLength(1); ··· 135 135 let state = createWorkspaceState(); 136 136 state = addTag(state, 'Urgent'); 137 137 const tagId = [...state.tags.keys()][0]; 138 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 138 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 139 139 state = tagDoc(state, 'doc-1', tagId); 140 140 state = untagDoc(state, 'doc-1', tagId); 141 141 expect(state.docs.get('doc-1')!.tags).toHaveLength(0); ··· 147 147 let state = createWorkspaceState(); 148 148 state = addWorkspace(state, 'WS1'); 149 149 const wsId = [...state.workspaces.keys()][0]; 150 - state = registerDoc(state, 'doc-1', 'Notes', 'docs'); 150 + state = registerDoc(state, 'doc-1', 'Notes', 'doc'); 151 151 state = moveToWorkspace(state, 'doc-1', wsId); 152 152 expect(state.docs.get('doc-1')!.workspaceId).toBe(wsId); 153 153 }); ··· 212 212 state = addTag(state, 'Urgent'); 213 213 const wsId = [...state.workspaces.keys()][0]; 214 214 const tagId = [...state.tags.keys()][0]; 215 - state = registerDoc(state, 'd1', 'Budget Report', 'sheets', wsId); 216 - state = registerDoc(state, 'd2', 'Meeting Notes', 'docs', wsId); 217 - state = registerDoc(state, 'd3', 'Pitch Deck', 'slides', ''); 215 + state = registerDoc(state, 'd1', 'Budget Report', 'sheet', wsId); 216 + state = registerDoc(state, 'd2', 'Meeting Notes', 'doc', wsId); 217 + state = registerDoc(state, 'd3', 'Pitch Deck', 'slide', ''); 218 218 state = tagDoc(state, 'd1', tagId); 219 219 state = togglePin(state, 'd3'); 220 220 return { state, wsId, tagId }; ··· 255 255 describe('getPinnedDocs', () => { 256 256 it('returns only pinned docs', () => { 257 257 let state = createWorkspaceState(); 258 - state = registerDoc(state, 'd1', 'A', 'docs'); 259 - state = registerDoc(state, 'd2', 'B', 'docs'); 258 + state = registerDoc(state, 'd1', 'A', 'doc'); 259 + state = registerDoc(state, 'd2', 'B', 'doc'); 260 260 state = togglePin(state, 'd1'); 261 261 const pinned = getPinnedDocs(state); 262 262 expect(pinned).toHaveLength(1); ··· 301 301 describe('countByType', () => { 302 302 it('counts docs by type', () => { 303 303 let state = createWorkspaceState(); 304 - state = registerDoc(state, 'd1', 'A', 'docs'); 305 - state = registerDoc(state, 'd2', 'B', 'sheets'); 306 - state = registerDoc(state, 'd3', 'C', 'sheets'); 307 - state = registerDoc(state, 'd4', 'D', 'slides'); 304 + state = registerDoc(state, 'd1', 'A', 'doc'); 305 + state = registerDoc(state, 'd2', 'B', 'sheet'); 306 + state = registerDoc(state, 'd3', 'C', 'sheet'); 307 + state = registerDoc(state, 'd4', 'D', 'slide'); 308 308 const counts = countByType(state); 309 - expect(counts.docs).toBe(1); 310 - expect(counts.sheets).toBe(2); 311 - expect(counts.slides).toBe(1); 309 + expect(counts.doc).toBe(1); 310 + expect(counts.sheet).toBe(2); 311 + expect(counts.slide).toBe(1); 312 312 }); 313 313 }); 314 314 });