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

Configure Feed

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

fix: polish batch — landing search, docs selection, slides theme, chart scoping

- Landing: search matches tags + type labels (#712)
- Docs: normalize cursor to inline position on load (#713)
- Slides: theme applies to canvas bg (#718)
- Sheets: charts re-render on sheet switch (#726)

Closes #712 #713 #718 #726

+93 -6
+4
CHANGELOG.md
··· 8 8 ## [Unreleased] 9 9 10 10 ### Fixed 11 + - Landing: search box now matches document tags and type labels in addition to names, so typing `forge` surfaces docs tagged `forge-workspace` or `forge-report`, and typing `spreadsheet` / `presentation` filters by type (v0.62.19, #712). `filterBySearch()` previously only searched `_decryptedName`; now it also scans `doc.type`, the human-readable `DOC_TYPE_LABELS[doc.type]`, and any parsed tag from the (encrypted) `doc.tags` JSON array. (#712) 12 + - Docs: opening a document whose saved cursor position sits on a non-inline block (e.g. a table wrapper, not a `td > p`) no longer prints the TipTap warning `TextSelection endpoint not pointing into a node with inline content` on every load (v0.62.19, #713). Added a one-shot `transaction` listener in `src/docs/main.ts` that snaps the selection to the nearest inline position via `TextSelection.near` when the initial anchor lands in a non-inline-content parent. (#713) 13 + - Slides: selecting a theme (Dark / Ocean / Forest / Sunset) now changes the main canvas background, not just the thumbnail (v0.62.19, #718). `renderCanvas` and `renderThumbnails` in `src/slides/rendering.ts` were both using the hard-coded `slide.background` default (`#ffffff`) and ignoring the theme palette. Added a `slideBackground(slideBg, theme)` helper that falls back to `theme.palette.background` when the slide carries the default white, so theme switches take effect immediately while explicit per-slide backgrounds still win. (#718) 14 + - Sheets: charts are now scoped to the sheet they were inserted on — switching to Sheet 2 (or any new sheet) no longer shows Sheet 1's chart in the same canvas slot (v0.62.19, #726). `getCharts()` always read from the active sheet's Y.Map, but `renderCharts()` was only fired by the initial observeDeep, not on sheet-switch. Added a `renderOverlays` hook to `SheetTabsDeps` and invoke it from tab-click, delete, duplicate, and the `+ Add sheet` button so the chart section clears and re-renders with the new sheet's charts. (#726) 11 15 - Docs: typing `- [ ] ` or `- [x] ` at the start of a paragraph now autoformats into a GFM task-list item with a checkbox — previously only the `- ` prefix triggered a plain bullet list and `[ ]` appeared as literal text (v0.62.18, #722). Added an InputRule in `src/docs/extensions/markdown-autoformat.ts` that replaces the current paragraph with a `taskList > taskItem > paragraph` node and sets `checked` based on `[x]` vs `[ ]`. (#722) 12 16 - Docs: TipTap autolink no longer false-positives on `Y.Map`, `U.S.`, or other `word.word` patterns that aren't real URLs (v0.62.18, #721). Tightened the Link extension config in `src/docs/main.ts` to only autolink URLs starting with `http://`, `https://`, or `mailto:` via an explicit `validate` callback + `protocols: ['http', 'https', 'mailto']`. Manual link insertion via Cmd+K / toolbar remains unrestricted. (#721) 13 17 - Slides: thumbnail element-count badge no longer visually merges with the slide-number, so two slides with single elements used to read as "11"/"22" etc. (v0.62.17, #727). The thumbnail previously rendered `<span class="slides-thumb-num">{N}</span>` followed by a bare `<span class="slides-thumb-count">{M}</span>` inside the preview — with no separator they appeared flush together. Prefixed the count with a `▦` glyph + space and added `aria-label="{M} element(s)"` so screen readers hear "N elements" rather than the number alone. (#727)
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.62.18", 3 + "version": "0.62.19", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",
+22
src/docs/main.ts
··· 296 296 autofocus: true, 297 297 }); 298 298 299 + // --- Selection Normalization (#713) --- 300 + // TipTap v3 warns 'TextSelection endpoint not pointing into a node with inline 301 + // content' when the initial/restored cursor lands on a block node such as a 302 + // table wrapper (not a td > paragraph). Run TextSelection.near on first 303 + // transaction to snap to the nearest inline position. 304 + { 305 + let normalized = false; 306 + editor.on('transaction', ({ transaction }) => { 307 + if (normalized) return; 308 + normalized = true; 309 + const sel = transaction.selection; 310 + const $pos = sel.$anchor; 311 + // parent.inlineContent is true when the selection is inside a node that 312 + // accepts inline content (paragraph, heading, etc.) — leave it alone. 313 + if ($pos.parent.inlineContent) return; 314 + import('prosemirror-state').then(({ TextSelection }) => { 315 + const near = TextSelection.near($pos, 1); 316 + editor.view.dispatch(editor.state.tr.setSelection(near)); 317 + }).catch(() => { /* best-effort */ }); 318 + }); 319 + } 320 + 299 321 // --- Markdown Paste Handler --- 300 322 editor.setOptions({ 301 323 editorProps: {
+14 -2
src/landing-utils.ts
··· 13 13 UsernameValidationResult, 14 14 PurgeResult, 15 15 } from './landing-types.js'; 16 + import { parseTags } from './tags.js'; 16 17 17 18 // ============================================================ 18 19 // Sort ··· 286 287 // ============================================================ 287 288 288 289 /** 289 - * Filter documents by search query (matched against decrypted name). 290 + * Filter documents by search query. 291 + * 292 + * Matches against: decrypted name, type label ("Spreadsheet"), type id ("sheet"), 293 + * and any tag. Tags are stored as a JSON-encoded string array. 290 294 */ 291 295 export function filterBySearch(docs: DocumentMeta[], query: string | null | undefined): DocumentMeta[] { 292 296 if (!query || !query.trim()) return docs; 293 297 const q = query.trim().toLowerCase(); 294 298 return docs.filter(doc => { 295 299 const name = (doc._decryptedName || 'Encrypted Document').toLowerCase(); 296 - return name.includes(q); 300 + if (name.includes(q)) return true; 301 + const typeId = (doc.type || '').toLowerCase(); 302 + if (typeId && typeId.includes(q)) return true; 303 + const typeLabel = (doc.type ? (DOC_TYPE_LABELS[doc.type] || '') : '').toLowerCase(); 304 + if (typeLabel && typeLabel.includes(q)) return true; 305 + for (const tag of parseTags(doc.tags)) { 306 + if (tag.toLowerCase().includes(q)) return true; 307 + } 308 + return false; 297 309 }); 298 310 } 299 311
+5 -1
src/sheets/main.ts
··· 269 269 return { 270 270 ySheets, ydoc, getActiveSheetIdx, setActiveSheetIdx, 271 271 ensureSheet, evalCache, clearSpillMaps: spillClearMaps, 272 - invalidateRecalcEngine, renderGrid, hideActiveContextMenu, setActiveContextMenu, sheetTabsContainer, 272 + invalidateRecalcEngine, renderGrid, 273 + // #726: charts are per-sheet; re-render overlays on every sheet switch. 274 + renderOverlays: () => { renderCharts(); }, 275 + hideActiveContextMenu, setActiveContextMenu, sheetTabsContainer, 273 276 }; 274 277 } 275 278 ··· 523 526 let count = 0; 524 527 getYSheets().forEach((_, key) => { if (key.startsWith('sheet_')) count++; }); 525 528 ensureSheet(count); setActiveSheetIdx(count); renderSheetTabs(); clearEvalCache(); spillClearMaps(); invalidateRecalcEngine(); renderGrid(); 529 + renderCharts(); // #726: clear stale charts-UI from previous sheet 526 530 }); 527 531 528 532 document.getElementById('tb-chart')!.addEventListener('click', () => { showChartDialog(null, null); closeAllDropdowns(); });
+5
src/sheets/sheet-tabs-ui.ts
··· 21 21 clearSpillMaps: () => void; 22 22 invalidateRecalcEngine: () => void; 23 23 renderGrid: () => void; 24 + /** Re-render per-sheet overlays (charts, etc.) after sheet switch. See #726. */ 25 + renderOverlays?: () => void; 24 26 hideActiveContextMenu: () => void; 25 27 setActiveContextMenu: (menu: any) => void; 26 28 sheetTabsContainer: HTMLElement; ··· 260 262 deps.invalidateRecalcEngine(); 261 263 renderSheetTabs(deps); 262 264 deps.renderGrid(); 265 + deps.renderOverlays?.(); 263 266 } 264 267 } 265 268 ··· 275 278 deps.invalidateRecalcEngine(); 276 279 renderSheetTabs(deps); 277 280 deps.renderGrid(); 281 + deps.renderOverlays?.(); 278 282 } 279 283 } 280 284 ··· 350 354 renderSheetTabs(deps); 351 355 deps.evalCache.clear(); deps.clearSpillMaps(); deps.invalidateRecalcEngine(); 352 356 deps.renderGrid(); 357 + deps.renderOverlays?.(); 353 358 }); 354 359 355 360 tab.addEventListener('dblclick', (e) => {
+15 -2
src/slides/rendering.ts
··· 16 16 /** 17 17 * Render the thumbnail sidebar for slide navigation. 18 18 */ 19 + /** 20 + * Pick the effective slide background: prefer the theme's background color 21 + * unless the slide carries an explicit non-default override. See #718. 22 + * Default-new slides are created with '#ffffff'; only treat that exact value 23 + * as "use the theme". 24 + */ 25 + function slideBackground(slideBg: string, theme: ReturnType<typeof getTheme>): string { 26 + if (!theme) return slideBg; 27 + if (slideBg === '#ffffff' || slideBg === '#fff' || !slideBg) return theme.palette.background; 28 + return slideBg; 29 + } 30 + 19 31 export function renderThumbnails(refs: DOMRefs, actions: AppActions): void { 20 32 const { deck, themedDeck } = actions.getState(); 21 33 refs.thumbnailList.innerHTML = ''; 22 34 35 + const theme = getTheme(themedDeck.themeId); 23 36 deck.slides.forEach((slide, i) => { 24 37 const thumb = document.createElement('div'); 25 38 thumb.className = 'slides-thumbnail' + (i === deck.currentSlide ? ' active' : ''); ··· 31 44 32 45 const preview = document.createElement('div'); 33 46 preview.className = 'slides-thumb-preview'; 34 - preview.style.background = slide.background; 47 + preview.style.background = slideBackground(slide.background, theme); 35 48 preview.style.aspectRatio = '16/9'; 36 49 37 50 // Element count as a corner badge — previously this rendered as bare ··· 246 259 const theme = getTheme(themedDeck.themeId); 247 260 const cssVars = theme ? themeToCSS(theme) : {}; 248 261 249 - let style = `width:${SLIDE_WIDTH}px;height:${SLIDE_HEIGHT}px;position:relative;overflow:hidden;background:${slide.background};`; 262 + let style = `width:${SLIDE_WIDTH}px;height:${SLIDE_HEIGHT}px;position:relative;overflow:hidden;background:${slideBackground(slide.background, theme)};`; 250 263 for (const [k, v] of Object.entries(cssVars)) { 251 264 style += `${k}:${v};`; 252 265 }
+27
tests/landing-utils.test.ts
··· 566 566 it('returns empty for no match', () => { 567 567 expect(filterBySearch(docs, 'zzzzz')).toHaveLength(0); 568 568 }); 569 + 570 + it('matches by tag (#712)', () => { 571 + const tagged = [ 572 + makeDoc({ id: 'fw', _decryptedName: 'Workspace', tags: JSON.stringify(['forge-workspace']) }), 573 + makeDoc({ id: 'fr', _decryptedName: 'Report', tags: JSON.stringify(['forge-report']) }), 574 + makeDoc({ id: 'other', _decryptedName: 'Other', tags: JSON.stringify(['personal']) }), 575 + ]; 576 + const result = filterBySearch(tagged, 'forge'); 577 + expect(result.map(d => d.id).sort()).toEqual(['fr', 'fw']); 578 + }); 579 + 580 + it('matches by type id (#712)', () => { 581 + const mixed = [ 582 + makeDoc({ id: 'd1', _decryptedName: 'Alpha', type: 'sheet' }), 583 + makeDoc({ id: 'd2', _decryptedName: 'Beta', type: 'doc' }), 584 + ]; 585 + expect(filterBySearch(mixed, 'sheet').map(d => d.id)).toEqual(['d1']); 586 + }); 587 + 588 + it('matches by type label (#712)', () => { 589 + const mixed = [ 590 + makeDoc({ id: 'd1', _decryptedName: 'Alpha', type: 'sheet' }), 591 + makeDoc({ id: 'd2', _decryptedName: 'Beta', type: 'slide' }), 592 + ]; 593 + expect(filterBySearch(mixed, 'spreadsheet').map(d => d.id)).toEqual(['d1']); 594 + expect(filterBySearch(mixed, 'presentation').map(d => d.id)).toEqual(['d2']); 595 + }); 569 596 }); 570 597 571 598 // =====================================================================