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(sheets): double borders, xlsx import cutoff, arithmetic formulas' (#67) from fix/borders-import-formulas into main

scott fbb3655f 52c21b85

+169 -7
+15
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ## [0.7.2] — 2026-03-19 11 + 12 + ### Fixed 13 + - **Double horizontal cell borders**: switched th/td from `border: 1px solid` to `border-right` + `border-bottom` only — eliminates double-line artifacts with `border-collapse` + `position: sticky` (#170) 14 + - **Imported data not showing past ~row 50**: xlsx import stored rowHeights as JSON string instead of Y.Map, causing `getRowHeight()` to crash — now stores into proper Y.Map (#170) 15 + 16 + ### Added 17 + - **Arithmetic formula functions**: ADD, MINUS, MULTIPLY, DIVIDE with full autocomplete and tooltip support (#170) 18 + 19 + ### Tests 20 + - 3040 unit tests across 101 test files (+21 from v0.7.1) 21 + 10 22 ## [0.7.1] — 2026-03-19 11 23 12 24 ### Fixed 25 + - Fix cell border double-line (black+white) from border-separate (#167) 13 26 - **Double-line cell borders**: reverted `border-separate` to `border-collapse` with `background-clip: padding-box` — colored cells now keep visible borders without the black+white double-line artifact 14 27 15 28 ### Added ··· 197 210 - **2048 unit tests** across 77 test files 198 211 199 212 ### Changed 213 + - Merge open PRs and verify deployment (#169) 214 + - Sheets UX iteration 3: cell editor, drag-fill preview, performance (#166) 200 215 - CSS/accessibility improvements for toolbar (#165) 201 216 - Sheets UX iteration 2: performance, polish, and visual testing (#164) 202 217 - **Full TypeScript migration**: all 122 source + test files, zero `any` types
+2 -2
package-lock.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.3.0", 3 + "version": "0.7.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "tools", 9 - "version": "0.3.0", 9 + "version": "0.7.1", 10 10 "dependencies": { 11 11 "@tiptap/core": "^2.11.0", 12 12 "@tiptap/extension-collaboration": "^2.11.0",
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.7.1", 3 + "version": "0.7.2", 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": {
+14 -2
src/css/app.css
··· 1619 1619 1620 1620 .sheet-grid th { 1621 1621 background: var(--color-surface); 1622 - border: 1px solid var(--color-grid-header-line); 1622 + border: none; 1623 + border-right: 1px solid var(--color-grid-header-line); 1624 + border-bottom: 1px solid var(--color-grid-header-line); 1623 1625 padding: 0.25rem 0.5rem; 1624 1626 font-weight: 500; 1625 1627 font-size: 0.7rem; ··· 1788 1790 } 1789 1791 1790 1792 .sheet-grid td { 1791 - border: 1px solid var(--color-grid-line); 1793 + border: none; 1794 + border-right: 1px solid var(--color-grid-line); 1795 + border-bottom: 1px solid var(--color-grid-line); 1792 1796 padding: 0; 1793 1797 height: 26px; 1794 1798 min-height: 26px; ··· 1796 1800 position: relative; 1797 1801 overflow: hidden; 1798 1802 background-clip: padding-box; 1803 + } 1804 + 1805 + /* Virtual scroll spacer rows: ensure they maintain their declared height */ 1806 + .sheet-grid .virtual-spacer-top td, 1807 + .sheet-grid .virtual-spacer-bottom td { 1808 + border: none; 1809 + padding: 0; 1810 + line-height: 0; 1799 1811 } 1800 1812 1801 1813 .sheet-grid td .cell-display {
+4
src/sheets/formula-autocomplete.ts
··· 21 21 { name: 'MAX', signature: 'MAX(range1, [range2], ...)' }, 22 22 { name: 'MEDIAN', signature: 'MEDIAN(range1, [range2], ...)' }, 23 23 { name: 'STDEV', signature: 'STDEV(range1, [range2], ...)' }, 24 + { name: 'ADD', signature: 'ADD(value1, value2)' }, 25 + { name: 'MINUS', signature: 'MINUS(value1, value2)' }, 26 + { name: 'MULTIPLY', signature: 'MULTIPLY(factor1, factor2)' }, 27 + { name: 'DIVIDE', signature: 'DIVIDE(dividend, divisor)' }, 24 28 { name: 'ABS', signature: 'ABS(number)' }, 25 29 { name: 'ROUND', signature: 'ROUND(number, [num_digits])' }, 26 30 { name: 'ROUNDUP', signature: 'ROUNDUP(number, [num_digits])' },
+28
src/sheets/formula-tooltip.ts
··· 70 70 { name: 'range2', desc: 'Additional ranges', required: false }, 71 71 ], 72 72 }, 73 + ADD: { 74 + desc: 'Returns the sum of two values (equivalent to value1 + value2)', 75 + params: [ 76 + { name: 'value1', desc: 'The first addend', required: true }, 77 + { name: 'value2', desc: 'The second addend', required: true }, 78 + ], 79 + }, 80 + MINUS: { 81 + desc: 'Returns the difference of two values (equivalent to value1 - value2)', 82 + params: [ 83 + { name: 'value1', desc: 'The value to subtract from', required: true }, 84 + { name: 'value2', desc: 'The value to subtract', required: true }, 85 + ], 86 + }, 87 + MULTIPLY: { 88 + desc: 'Returns the product of two values (equivalent to factor1 * factor2)', 89 + params: [ 90 + { name: 'factor1', desc: 'The first factor', required: true }, 91 + { name: 'factor2', desc: 'The second factor', required: true }, 92 + ], 93 + }, 94 + DIVIDE: { 95 + desc: 'Returns the result of dividing one value by another (equivalent to dividend / divisor)', 96 + params: [ 97 + { name: 'dividend', desc: 'The number to be divided', required: true }, 98 + { name: 'divisor', desc: 'The number to divide by', required: true }, 99 + ], 100 + }, 73 101 ABS: { 74 102 desc: 'Returns the absolute value of a number', 75 103 params: [
+4
src/sheets/formulas.ts
··· 659 659 case 'COUNTA': return flat(args).length; 660 660 case 'MIN': { const n = nums(args); return n.length ? Math.min(...n) : 0; } 661 661 case 'MAX': { const n = nums(args); return n.length ? Math.max(...n) : 0; } 662 + case 'ADD': return toNum(args[0]) + toNum(args[1]); 663 + case 'MINUS': return toNum(args[0]) - toNum(args[1]); 664 + case 'MULTIPLY': return toNum(args[0]) * toNum(args[1]); 665 + case 'DIVIDE': { const d = toNum(args[1]); return d === 0 ? '#DIV/0!' : toNum(args[0]) / d; } 662 666 case 'ABS': return Math.abs(toNum(args[0])); 663 667 case 'ROUND': return Math.round(toNum(args[0]) * Math.pow(10, toNum(args[1] ?? 0))) / Math.pow(10, toNum(args[1] ?? 0)); 664 668 case 'ROUNDUP': { const f = Math.pow(10, toNum(args[1] ?? 0)); return Math.ceil(toNum(args[0]) * f) / f; }
+9 -2
src/sheets/xlsx-import.ts
··· 429 429 } 430 430 } 431 431 432 - // Store row heights as JSON (no existing Y.Map pattern for row heights) 432 + // Store row heights into the Yjs rowHeights Y.Map 433 433 if (parsed.rowHeights && Object.keys(parsed.rowHeights).length > 0) { 434 - sheet.set('rowHeights', JSON.stringify(parsed.rowHeights)); 434 + let rowHeightsMap = sheet.get('rowHeights'); 435 + if (!rowHeightsMap || typeof rowHeightsMap.set !== 'function') { 436 + rowHeightsMap = new (cells.constructor)(); 437 + sheet.set('rowHeights', rowHeightsMap); 438 + } 439 + for (const [rowIdx, height] of Object.entries(parsed.rowHeights)) { 440 + rowHeightsMap.set(String(rowIdx), height); 441 + } 435 442 } 436 443 437 444 // Expand grid dimensions if needed
+92
tests/formulas.test.ts
··· 643 643 expect(result).toBe(0); 644 644 }); 645 645 }); 646 + 647 + describe('ADD function', () => { 648 + it('adds two numbers', () => { 649 + expect(evalWith('ADD(2, 3)')).toBe(5); 650 + }); 651 + 652 + it('adds negative numbers', () => { 653 + expect(evalWith('ADD(-5, 3)')).toBe(-2); 654 + }); 655 + 656 + it('adds cell references', () => { 657 + expect(evalWith('ADD(A1, B1)', { A1: 10, B1: 20 })).toBe(30); 658 + }); 659 + 660 + it('coerces strings to numbers', () => { 661 + expect(evalWith('ADD(A1, B1)', { A1: '7', B1: '3' })).toBe(10); 662 + }); 663 + 664 + it('treats empty cells as 0', () => { 665 + expect(evalWith('ADD(A1, 5)', {})).toBe(5); 666 + }); 667 + }); 668 + 669 + describe('MINUS function', () => { 670 + it('subtracts two numbers', () => { 671 + expect(evalWith('MINUS(10, 3)')).toBe(7); 672 + }); 673 + 674 + it('returns negative result', () => { 675 + expect(evalWith('MINUS(3, 10)')).toBe(-7); 676 + }); 677 + 678 + it('subtracts cell references', () => { 679 + expect(evalWith('MINUS(A1, B1)', { A1: 100, B1: 40 })).toBe(60); 680 + }); 681 + 682 + it('coerces strings to numbers', () => { 683 + expect(evalWith('MINUS(A1, B1)', { A1: '20', B1: '8' })).toBe(12); 684 + }); 685 + 686 + it('treats empty cells as 0', () => { 687 + expect(evalWith('MINUS(5, A1)', {})).toBe(5); 688 + }); 689 + }); 690 + 691 + describe('MULTIPLY function', () => { 692 + it('multiplies two numbers', () => { 693 + expect(evalWith('MULTIPLY(4, 5)')).toBe(20); 694 + }); 695 + 696 + it('multiplies by zero', () => { 697 + expect(evalWith('MULTIPLY(100, 0)')).toBe(0); 698 + }); 699 + 700 + it('multiplies negative numbers', () => { 701 + expect(evalWith('MULTIPLY(-3, 7)')).toBe(-21); 702 + }); 703 + 704 + it('multiplies cell references', () => { 705 + expect(evalWith('MULTIPLY(A1, B1)', { A1: 6, B1: 8 })).toBe(48); 706 + }); 707 + 708 + it('coerces strings to numbers', () => { 709 + expect(evalWith('MULTIPLY(A1, B1)', { A1: '3', B1: '9' })).toBe(27); 710 + }); 711 + }); 712 + 713 + describe('DIVIDE function', () => { 714 + it('divides two numbers', () => { 715 + expect(evalWith('DIVIDE(10, 2)')).toBe(5); 716 + }); 717 + 718 + it('returns decimal result', () => { 719 + expect(evalWith('DIVIDE(7, 2)')).toBe(3.5); 720 + }); 721 + 722 + it('returns #DIV/0! for division by zero', () => { 723 + expect(evalWith('DIVIDE(10, 0)')).toBe('#DIV/0!'); 724 + }); 725 + 726 + it('divides cell references', () => { 727 + expect(evalWith('DIVIDE(A1, B1)', { A1: 100, B1: 4 })).toBe(25); 728 + }); 729 + 730 + it('coerces strings to numbers', () => { 731 + expect(evalWith('DIVIDE(A1, B1)', { A1: '15', B1: '3' })).toBe(5); 732 + }); 733 + 734 + it('divides negative numbers', () => { 735 + expect(evalWith('DIVIDE(-20, 4)')).toBe(-5); 736 + }); 737 + });