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(sheets): UX polish — toolbar redesign, date fixes, column unhide' (#88) from feat/sheets-ux-polish into main

scott b91059ba b8c7247b

+175 -43
+20
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ## [0.9.0] — 2026-03-22 11 + 12 + ### Fixed 13 + - Fix XLSX export writing timestamps as Excel serial numbers instead of dates (#192) 14 + - Fix XLSX import losing Date type on formula results (#192) 15 + 16 + ### Added 17 + - **Toolbar redesign**: promoted underline, freeze panes, sort, and insert to main toolbar; freeze and insert are now dropdowns; overflow menu slimmed to less-common actions (#196) 18 + - **Column unhide indicator**: clickable teal bar on column headers with tooltip, matching the existing row unhide pattern (#193) 19 + - **Header hover states**: row and column headers highlight on hover (#195) 20 + - **Toast feedback**: freeze, merge, and unhide actions now show confirmation toasts (#197) 21 + 22 + ### Tests 23 + - 3113 unit tests across 103 test files 24 + 10 25 ## [0.8.1] — 2026-03-19 11 26 12 27 ### Fixed 28 + - Fix hiding rows and columns (#190) 29 + - Fix save/reload discarding cell contents (#189) 30 + - Fix dates losing type after save/reload and import (#188) 31 + - Fix freeze panes visual glitches during scrolling (#187) 32 + - Fix freeze panes z-index layering and missing frozen-corner class (#186) 13 33 - **Virtual scroll jumpiness**: fixed broken range cache (every scroll triggered full re-render), added scroll position preservation across innerHTML replacement, suppressed recursive scroll events (#177) 14 34 - **Virtual scroll not rendering past viewport**: `rh()` was scoped inside `renderGrid()` but referenced from module-level scroll handler — replaced with `getRowHeight()` (#177) 15 35
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.8.2", 3 + "version": "0.9.0", 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": {
+62
src/css/app.css
··· 1775 1775 border-right: 2px solid var(--color-teal) !important; 1776 1776 position: relative; 1777 1777 } 1778 + 1779 + /* Clickable column unhide indicator (matches row indicator pattern) */ 1780 + .hidden-col-indicator { 1781 + position: absolute; 1782 + top: 0; 1783 + right: -4px; 1784 + width: 8px; 1785 + height: 100%; 1786 + cursor: pointer; 1787 + z-index: 6; 1788 + display: flex; 1789 + align-items: center; 1790 + justify-content: center; 1791 + } 1792 + .hidden-col-indicator-bar { 1793 + width: 4px; 1794 + height: 100%; 1795 + background: var(--color-teal); 1796 + opacity: 0.5; 1797 + border-radius: 2px; 1798 + transition: opacity 150ms ease, width 150ms ease; 1799 + } 1800 + .hidden-col-indicator:hover .hidden-col-indicator-bar { 1801 + opacity: 0.85; 1802 + width: 6px; 1803 + } 1804 + .hidden-col-indicator::after { 1805 + content: 'Click to unhide'; 1806 + position: absolute; 1807 + top: 100%; 1808 + left: 50%; 1809 + transform: translateX(-50%); 1810 + margin-top: 4px; 1811 + font-size: 0.6rem; 1812 + font-family: var(--font-body); 1813 + color: var(--color-bg); 1814 + background: var(--color-text); 1815 + padding: 2px 6px; 1816 + border-radius: 3px; 1817 + white-space: nowrap; 1818 + opacity: 0; 1819 + pointer-events: none; 1820 + transition: opacity 150ms ease; 1821 + z-index: 20; 1822 + } 1823 + .hidden-col-indicator:hover::after { opacity: 1; } 1778 1824 .hidden-row-boundary { 1779 1825 border-bottom: 2px solid var(--color-teal) !important; 1780 1826 position: relative; ··· 2498 2544 .sheet-grid td.range-bottom { border-bottom: 2px solid var(--color-teal); } 2499 2545 .sheet-grid td.range-left { border-left: 2px solid var(--color-teal); } 2500 2546 .sheet-grid td.range-right { border-right: 2px solid var(--color-teal); } 2547 + 2548 + /* Header hover states */ 2549 + .sheet-grid th[data-col]:hover, 2550 + .sheet-grid th.row-header:hover { 2551 + background: oklch(0.93 0.01 240); 2552 + } 2553 + [data-theme="dark"] .sheet-grid th[data-col]:hover, 2554 + [data-theme="dark"] .sheet-grid th.row-header:hover { 2555 + background: oklch(0.22 0.01 240); 2556 + } 2557 + @media (prefers-color-scheme: dark) { 2558 + :root:not([data-theme="light"]) .sheet-grid th[data-col]:hover, 2559 + :root:not([data-theme="light"]) .sheet-grid th.row-header:hover { 2560 + background: oklch(0.22 0.01 240); 2561 + } 2562 + } 2501 2563 2502 2564 .sheet-grid th.col-selected { 2503 2565 background: var(--color-range-header-bg);
+46 -34
src/sheets/index.html
··· 94 94 </select> 95 95 <span class="toolbar-sep toolbar-mobile-hide"></span> 96 96 97 - <!-- Group 4: Text formatting --> 97 + <!-- Group 4: Text formatting (Bold, Italic, Underline, Strikethrough, Colors) --> 98 98 <button class="tb-btn" id="tb-bold" title="Bold (Cmd+B)" aria-label="Bold"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4.5 2.5h4.5a2.5 2.5 0 0 1 0 5h-4.5zM4.5 7.5h5a3 3 0 0 1 0 6h-5z" stroke-width="2"/></svg></button> 99 99 <button class="tb-btn" id="tb-italic" title="Italic (Cmd+I)" aria-label="Italic"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="10" y1="2.5" x2="6" y2="13.5"/><line x1="7" y1="2.5" x2="11" y2="2.5"/><line x1="5" y1="13.5" x2="9" y2="13.5"/></svg></button> 100 + <button class="tb-btn" id="tb-underline" title="Underline (Cmd+U)" aria-label="Underline"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 2v6a4 4 0 0 0 8 0V2"/><line x1="3" y1="14" x2="13" y2="14"/></svg></button> 100 101 <button class="tb-btn tb-btn-text tb-btn-strike" id="tb-strikethrough" title="Strikethrough (Cmd+Shift+X)" aria-label="Strikethrough">S</button> 101 102 <div class="tb-color-wrap toolbar-mobile-hide"> 102 103 <input type="color" class="tb-color" id="tb-text-color" value="#1a1815" title="Text color" aria-label="Text color"> ··· 178 179 <button class="tb-btn toolbar-mobile-hide" id="tb-wrap" title="Wrap text" aria-label="Wrap text"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 3h12M2 7h9a2 2 0 0 1 0 4H9"/><path d="M10 9.5l-1.5 1.5L10 12.5"/><line x1="2" y1="13" x2="7" y2="13"/></svg></button> 179 180 <span class="toolbar-sep toolbar-mobile-hide"></span> 180 181 181 - <!-- Group 7: Filter & Chart (common data actions) --> 182 - <button class="tb-btn toolbar-mobile-hide" id="tb-filter" title="Toggle column filters" aria-label="Toggle column filters"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 3h12l-4 5v4l-4 2V8z"/></svg></button> 183 - <button class="tb-btn toolbar-mobile-hide" id="tb-chart" title="Insert chart" aria-label="Insert chart"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="8" width="3" height="6" rx="0.5"/><rect x="6.5" y="4" width="3" height="10" rx="0.5"/><rect x="11" y="6" width="3" height="8" rx="0.5"/></svg></button> 184 - 185 - <!-- Mobile more button (visible only on mobile) --> 186 - <button class="tb-btn toolbar-mobile-more" id="tb-mobile-more" title="More formatting" aria-label="More formatting" aria-expanded="false" aria-haspopup="true"><svg class="tb-icon" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="13" cy="8" r="1.25" fill="currentColor" stroke="none"/></svg></button> 187 - 188 - <!-- More overflow menu --> 189 - <div class="toolbar-overflow" id="overflow-menu"> 190 - <button class="toolbar-overflow-toggle" id="overflow-toggle" title="More options" aria-label="More options" aria-expanded="false" aria-haspopup="true"> 191 - <svg class="tb-icon" viewBox="0 0 16 16"><circle cx="8" cy="3" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="13" r="1.25" fill="currentColor" stroke="none"/></svg> 182 + <!-- Group 7: Insert --> 183 + <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-insert"> 184 + <button class="toolbar-dropdown-toggle" title="Insert rows or columns" aria-label="Insert" aria-expanded="false" aria-haspopup="true"> 185 + <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M8 5v6"/><path d="M5 8h6"/></svg></span><span class="caret">&#9662;</span> 192 186 </button> 193 - <div class="toolbar-overflow-menu" role="menu"> 194 - <!-- Section: Text --> 195 - <button class="toolbar-dropdown-item" id="tb-underline" title="Underline (Cmd+U)" role="menuitem"> 196 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 2v6a4 4 0 0 0 8 0V2"/><line x1="3" y1="14" x2="13" y2="14"/></svg></span><span class="item-label">Underline</span> 197 - </button> 198 - <button class="toolbar-dropdown-item" id="tb-clear-format" title="Clear formatting (Cmd+\)" role="menuitem"> 199 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M3 3l10 10"/><path d="M5 2h6l-2 6"/><line x1="3" y1="14" x2="13" y2="14"/></svg></span><span class="item-label">Clear formatting</span> 200 - </button> 201 - <div class="toolbar-dropdown-divider"></div> 202 - 203 - <!-- Section: Structure --> 187 + <div class="toolbar-dropdown-menu" role="menu"> 204 188 <button class="toolbar-dropdown-item" id="tb-add-row" title="Insert row below" role="menuitem"> 205 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M8 5v6"/><path d="M5 8h6"/></svg></span><span class="item-label">Insert row</span> 189 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 8h12"/><path d="M2 2h12v12H2z" fill="none"/><path d="M8 5v6"/></svg></span><span class="item-label">Insert row</span> 206 190 </button> 207 191 <button class="toolbar-dropdown-item" id="tb-add-col" title="Insert column right" role="menuitem"> 208 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M8 5v6"/><path d="M5 8h6"/></svg></span><span class="item-label">Insert column</span> 192 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M8 2v12"/><path d="M2 2h12v12H2z" fill="none"/><path d="M5 8h6"/></svg></span><span class="item-label">Insert column</span> 209 193 </button> 194 + <div class="toolbar-dropdown-divider"></div> 210 195 <button class="toolbar-dropdown-item" id="tb-del-row" title="Delete last row" role="menuitem"> 211 196 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M5 8h6"/></svg></span><span class="item-label">Delete row</span> 212 197 </button> 213 198 <button class="toolbar-dropdown-item" id="tb-del-col" title="Delete last column" role="menuitem"> 214 199 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M5 8h6"/></svg></span><span class="item-label">Delete column</span> 215 200 </button> 201 + </div> 202 + </div> 203 + 204 + <!-- Group 8: Freeze panes --> 205 + <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-freeze"> 206 + <button class="toolbar-dropdown-toggle" id="tb-freeze-toggle" title="Freeze panes" aria-label="Freeze panes" aria-expanded="false" aria-haspopup="true"> 207 + <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 5h12"/><path d="M5 2v12"/><path d="M2 2h3v3H2z" fill="currentColor" opacity="0.15" stroke="none"/></svg></span><span class="caret">&#9662;</span> 208 + </button> 209 + <div class="toolbar-dropdown-menu" role="menu"> 216 210 <button class="toolbar-dropdown-item" id="tb-freeze-rows" title="Freeze rows above cursor" role="menuitem"> 217 211 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 5h12"/><path d="M2 5v-2h12v2" fill="currentColor" opacity="0.15" stroke="none"/><path d="M6 5v8"/><path d="M10 5v8"/></svg></span><span class="item-label">Freeze rows</span> 218 212 </button> 219 213 <button class="toolbar-dropdown-item" id="tb-freeze-cols" title="Freeze columns left of cursor" role="menuitem"> 220 214 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M5 2v12"/><path d="M5 2h-2v12h2" fill="currentColor" opacity="0.15" stroke="none"/><path d="M5 6h8"/><path d="M5 10h8"/></svg></span><span class="item-label">Freeze columns</span> 221 215 </button> 216 + <div class="toolbar-dropdown-divider"></div> 222 217 <button class="toolbar-dropdown-item" id="tb-unfreeze" title="Unfreeze all panes" role="menuitem"> 223 218 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M3 3l10 10"/><path d="M13 3L3 13"/></svg></span><span class="item-label">Unfreeze all</span> 224 219 </button> 225 - <div class="toolbar-dropdown-divider"></div> 220 + </div> 221 + </div> 222 + <span class="toolbar-sep toolbar-mobile-hide"></span> 223 + 224 + <!-- Group 9: Data — Filter, Sort, Chart --> 225 + <button class="tb-btn toolbar-mobile-hide" id="tb-filter" title="Toggle column filters" aria-label="Toggle column filters"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M2 3h12l-4 5v4l-4 2V8z"/></svg></button> 226 + <button class="tb-btn toolbar-mobile-hide" id="tb-sort-asc" title="Sort column A to Z" aria-label="Sort A to Z"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 11V3"/><path d="M2 5l2-2 2 2"/><line x1="8" y1="4" x2="14" y2="4"/><line x1="8" y1="8" x2="12" y2="8"/><line x1="8" y1="12" x2="10" y2="12"/></svg></button> 227 + <button class="tb-btn toolbar-mobile-hide" id="tb-sort-desc" title="Sort column Z to A" aria-label="Sort Z to A"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 5v8"/><path d="M2 11l2 2 2-2"/><line x1="8" y1="4" x2="10" y2="4"/><line x1="8" y1="8" x2="12" y2="8"/><line x1="8" y1="12" x2="14" y2="12"/></svg></button> 228 + <button class="tb-btn toolbar-mobile-hide" id="tb-chart" title="Insert chart" aria-label="Insert chart"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="8" width="3" height="6" rx="0.5"/><rect x="6.5" y="4" width="3" height="10" rx="0.5"/><rect x="11" y="6" width="3" height="8" rx="0.5"/></svg></button> 229 + 230 + <!-- Mobile more button (visible only on mobile) --> 231 + <button class="tb-btn toolbar-mobile-more" id="tb-mobile-more" title="More formatting" aria-label="More formatting" aria-expanded="false" aria-haspopup="true"><svg class="tb-icon" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="13" cy="8" r="1.25" fill="currentColor" stroke="none"/></svg></button> 226 232 227 - <!-- Section: Data tools --> 228 - <button class="toolbar-dropdown-item" id="tb-sort-asc" title="Sort column A to Z" role="menuitem"> 229 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 11V3"/><path d="M2 5l2-2 2 2"/><line x1="8" y1="4" x2="14" y2="4"/><line x1="8" y1="8" x2="12" y2="8"/><line x1="8" y1="12" x2="10" y2="12"/></svg></span><span class="item-label">Sort A to Z</span> 233 + <!-- More overflow menu (less common actions) --> 234 + <div class="toolbar-overflow" id="overflow-menu"> 235 + <button class="toolbar-overflow-toggle" id="overflow-toggle" title="More options" aria-label="More options" aria-expanded="false" aria-haspopup="true"> 236 + <svg class="tb-icon" viewBox="0 0 16 16"><circle cx="8" cy="3" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="8" r="1.25" fill="currentColor" stroke="none"/><circle cx="8" cy="13" r="1.25" fill="currentColor" stroke="none"/></svg> 237 + </button> 238 + <div class="toolbar-overflow-menu" role="menu"> 239 + <!-- Section: Formatting --> 240 + <button class="toolbar-dropdown-item" id="tb-clear-format" title="Clear formatting (Cmd+\)" role="menuitem"> 241 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M3 3l10 10"/><path d="M5 2h6l-2 6"/><line x1="3" y1="14" x2="13" y2="14"/></svg></span><span class="item-label">Clear formatting</span> 230 242 </button> 231 - <button class="toolbar-dropdown-item" id="tb-sort-desc" title="Sort column Z to A" role="menuitem"> 232 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 5v8"/><path d="M2 11l2 2 2-2"/><line x1="8" y1="4" x2="10" y2="4"/><line x1="8" y1="8" x2="12" y2="8"/><line x1="8" y1="12" x2="14" y2="12"/></svg></span><span class="item-label">Sort Z to A</span> 243 + <button class="toolbar-dropdown-item" id="tb-striped" title="Toggle striped rows" role="menuitem"> 244 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="3" fill="currentColor" opacity="0.15" stroke="none"/><rect x="2" y="8" width="12" height="3" fill="currentColor" opacity="0.15" stroke="none"/><rect x="2" y="2" width="12" height="12" fill="none"/></svg></span><span class="item-label">Striped rows</span> 233 245 </button> 246 + <div class="toolbar-dropdown-divider"></div> 247 + 248 + <!-- Section: Data tools --> 234 249 <button class="toolbar-dropdown-item" id="tb-sort-multi" title="Multi-column sort" role="menuitem"> 235 250 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 11V3"/><path d="M2 5l2-2 2 2"/><path d="M12 5v8"/><path d="M10 11l2 2 2-2"/></svg></span><span class="item-label">Multi-column sort</span> 236 251 </button> ··· 239 254 </button> 240 255 <button class="toolbar-dropdown-item" id="tb-validation" title="Data validation" role="menuitem"> 241 256 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M3 8l3 3 7-7"/></svg></span><span class="item-label">Data validation</span> 242 - </button> 243 - <button class="toolbar-dropdown-item" id="tb-striped" title="Toggle striped rows" role="menuitem"> 244 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="3" fill="currentColor" opacity="0.15" stroke="none"/><rect x="2" y="8" width="12" height="3" fill="currentColor" opacity="0.15" stroke="none"/><rect x="2" y="2" width="12" height="12" fill="none"/></svg></span><span class="item-label">Striped rows</span> 245 257 </button> 246 258 <div class="toolbar-dropdown-divider"></div> 247 259
+33 -6
src/sheets/main.ts
··· 441 441 442 442 const leftStyle = c <= freezeC ? 'left:' + frozenLeftOffsets[c] + 'px;' : ''; 443 443 const classAttr = cls.length ? ' class="' + cls.join(' ') + '"' : ''; 444 - headHtml += '<th data-col="' + c + '"' + classAttr + ' role="columnheader" aria-colindex="' + c + '" style="' + leftStyle + '">' + colToLetter(c) + '<div class="col-resize-handle" data-resize-col="' + c + '"></div></th>'; 444 + const colUnhideIndicator = cls.includes('hidden-col-boundary') ? '<div class="hidden-col-indicator" data-unhide-col="' + c + '"><div class="hidden-col-indicator-bar"></div></div>' : ''; 445 + headHtml += '<th data-col="' + c + '"' + classAttr + ' role="columnheader" aria-colindex="' + c + '" style="' + leftStyle + '">' + colToLetter(c) + '<div class="col-resize-handle" data-resize-col="' + c + '"></div>' + colUnhideIndicator + '</th>'; 445 446 } 446 447 headHtml += '</tr></thead>'; 447 448 ··· 828 829 unhideAdjacentRows(row); 829 830 showToast('Rows unhidden'); 830 831 } 832 + } 833 + return; 834 + } 835 + // Click hidden-col indicator to unhide 836 + const colIndicator = e.target.closest('.hidden-col-indicator'); 837 + if (colIndicator) { 838 + e.preventDefault(); 839 + e.stopPropagation(); 840 + const col = parseInt(colIndicator.dataset.unhideCol); 841 + if (!isNaN(col)) { 842 + unhideAdjacentCols(col); 843 + showToast('Columns unhidden'); 831 844 } 832 845 return; 833 846 } ··· 2194 2207 } 2195 2208 2196 2209 // --- Frozen panes toolbar (Issue #7) --- 2197 - document.getElementById('tb-freeze-rows').addEventListener('click', () => { const rows = selectedCell.row - 1; setFreezeRows(rows > 0 ? rows : 0); renderGrid(); closeAllDropdowns(); }); 2198 - document.getElementById('tb-freeze-cols').addEventListener('click', () => { const cols = selectedCell.col - 1; setFreezeCols(cols > 0 ? cols : 0); renderGrid(); closeAllDropdowns(); }); 2199 - document.getElementById('tb-unfreeze').addEventListener('click', () => { setFreezeRows(0); setFreezeCols(0); renderGrid(); closeAllDropdowns(); }); 2210 + const freezeDropdown = document.getElementById('dd-freeze'); 2211 + if (freezeDropdown) { 2212 + const freezeToggle = freezeDropdown.querySelector('.toolbar-dropdown-toggle'); 2213 + if (freezeToggle) freezeToggle.addEventListener('click', (e) => { e.stopPropagation(); toggleDropdown(freezeDropdown); }); 2214 + } 2215 + document.getElementById('tb-freeze-rows').addEventListener('click', () => { const rows = selectedCell.row - 1; setFreezeRows(rows > 0 ? rows : 0); renderGrid(); closeAllDropdowns(); showToast(rows > 0 ? `Froze ${rows} row${rows > 1 ? 's' : ''}` : 'Rows unfrozen'); }); 2216 + document.getElementById('tb-freeze-cols').addEventListener('click', () => { const cols = selectedCell.col - 1; setFreezeCols(cols > 0 ? cols : 0); renderGrid(); closeAllDropdowns(); showToast(cols > 0 ? `Froze ${cols} column${cols > 1 ? 's' : ''}` : 'Columns unfrozen'); }); 2217 + document.getElementById('tb-unfreeze').addEventListener('click', () => { setFreezeRows(0); setFreezeCols(0); renderGrid(); closeAllDropdowns(); showToast('Panes unfrozen'); }); 2218 + 2219 + // --- Insert dropdown --- 2220 + const insertDropdown = document.getElementById('dd-insert'); 2221 + if (insertDropdown) { 2222 + const insertToggle = insertDropdown.querySelector('.toolbar-dropdown-toggle'); 2223 + if (insertToggle) insertToggle.addEventListener('click', (e) => { e.stopPropagation(); toggleDropdown(insertDropdown); }); 2224 + } 2200 2225 2201 2226 function updateFreezeToolbarState() { 2202 2227 const fr = getFreezeRows(); 2203 2228 const fc = getFreezeCols(); 2204 2229 const frBtn = document.getElementById('tb-freeze-rows'); 2205 2230 const fcBtn = document.getElementById('tb-freeze-cols'); 2231 + const freezeToggleBtn = document.getElementById('tb-freeze-toggle'); 2206 2232 if (frBtn) { frBtn.title = fr > 0 ? 'Frozen: ' + fr + ' rows' : 'Freeze rows above cursor'; frBtn.classList.toggle('active', fr > 0); } 2207 2233 if (fcBtn) { fcBtn.title = fc > 0 ? 'Frozen: ' + fc + ' cols' : 'Freeze columns left of cursor'; fcBtn.classList.toggle('active', fc > 0); } 2234 + if (freezeToggleBtn) { freezeToggleBtn.classList.toggle('active', fr > 0 || fc > 0); } 2208 2235 } 2209 2236 2210 2237 // --- Sheet tabs --- ··· 3058 3085 if (merge) { 3059 3086 const merges = getMerges(); 3060 3087 ydoc.transact(() => { merges.delete(cellId(merge.startCol, merge.startRow)); }); 3061 - updateMergeButtonState(); renderGrid(); return; 3088 + updateMergeButtonState(); renderGrid(); showToast('Cells unmerged'); return; 3062 3089 } 3063 - mergeCells(); 3090 + mergeCells(); showToast('Cells merged'); 3064 3091 }); 3065 3092 3066 3093 // --- Autosave indicator (#17) ---
+6 -1
src/sheets/xlsx-export.ts
··· 121 121 if (data.f) { 122 122 cell.value = { formula: data.f, result: typeof data.v === 'number' ? data.v : (data.v || '') }; 123 123 } else if (data.v !== undefined && data.v !== null && data.v !== '') { 124 - cell.value = data.v; 124 + // Convert timestamp back to Date for ExcelJS when format is 'date' 125 + if (data.s?.format === 'date' && typeof data.v === 'number' && data.v > 946684800000) { 126 + cell.value = new Date(data.v); 127 + } else { 128 + cell.value = data.v; 129 + } 125 130 } 126 131 127 132 // Styles
+7 -1
src/sheets/xlsx-import.ts
··· 208 208 // Formula cell — ExcelJS stores as { formula, result } 209 209 const formulaValue = cell.value as { formula?: string; result?: unknown }; 210 210 data.f = formulaValue.formula || ''; 211 - data.v = formulaValue.result ?? ''; 211 + // Convert Date results to timestamps — Date objects don't survive Yjs serialization 212 + if (formulaValue.result instanceof Date) { 213 + data.v = formulaValue.result.getTime(); 214 + if (!data.s.format) data.s.format = 'date'; 215 + } else { 216 + data.v = formulaValue.result ?? ''; 217 + } 212 218 } else if (cell.value instanceof Date) { 213 219 // Store dates as numeric timestamps — Date objects don't survive Yjs serialization 214 220 data.v = cell.value.getTime();