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): grid line rendering, toolbar standardization, XLSX export' (#72) from fix/box-shadow-grid-lines into main

scott b7327cdc 82239acc

+72 -35
+11
CHANGELOG.md
··· 10 10 ## [0.7.2] — 2026-03-19 11 11 12 12 ### Fixed 13 + - Comprehensive visual debug and polish for sheets grid rendering (#173) 14 + - Fix double horizontal cell borders with colored backgrounds (#172) 13 15 - **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 16 - **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 17 16 18 ### Added 19 + - Add comprehensive Playwright visual tests for all sheet features (#158) 20 + - UX: command palette (Cmd+K) (#52) 17 21 - **Arithmetic formula functions**: ADD, MINUS, MULTIPLY, DIVIDE with full autocomplete and tooltip support (#170) 18 22 19 23 ### Tests ··· 210 214 - **2048 unit tests** across 77 test files 211 215 212 216 ### Changed 217 + - Sheets: CSV and TSV export (#102) 218 + - Sheets: row and column insert/delete operations (#113) 219 + - Polish: context menu actions in sheets are no-ops (#149) 220 + - Comprehensive Playwright visual and feature tests for sheets (#168) 221 + - Both: document rename from editor view (#114) 222 + - Bug: pasteAtSelection uses undefined variable parsedRows (#144) 223 + - Bug: circular formula references cause infinite recursion and tab crash (#145) 213 224 - Merge open PRs and verify deployment (#169) 214 225 - Sheets UX iteration 3: cell editor, drag-fill preview, performance (#166) 215 226 - CSS/accessibility improvements for toolbar (#165)
+46 -35
src/sheets/index.html
··· 53 53 <!-- View-only badge (shown when ?mode=view) --> 54 54 <div class="view-only-badge" id="view-only-badge" style="display:none">View only</div> 55 55 56 - <!-- Formatting toolbar (Google Sheets-style, flat single-row) --> 56 + <!-- Formatting toolbar (standard spreadsheet ordering) --> 57 57 <div class="toolbar gdocs-toolbar" id="toolbar" role="toolbar" aria-label="Formatting toolbar"> 58 - <!-- Group: History & Print --> 58 + <!-- Group 1: Undo, Redo, Print, Format Painter --> 59 59 <button class="tb-btn toolbar-mobile-hide" id="tb-undo" title="Undo (Cmd+Z)" aria-label="Undo"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M3 7.5h8a3 3 0 0 1 0 6H9"/><path d="M5.5 5L3 7.5 5.5 10"/></svg></button> 60 60 <button class="tb-btn toolbar-mobile-hide" id="tb-redo" title="Redo (Cmd+Shift+Z)" aria-label="Redo"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M13 7.5H5a3 3 0 0 0 0 6h2"/><path d="M10.5 5l2.5 2.5-2.5 2.5"/></svg></button> 61 61 <button class="tb-btn toolbar-mobile-hide" id="tb-print" title="Print spreadsheet (Cmd+P)" aria-label="Print spreadsheet"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 5V2h8v3"/><rect x="2" y="5" width="12" height="6" rx="1"/><path d="M4 11v3h8v-3"/><line x1="6" y1="12.5" x2="10" y2="12.5"/></svg></button> 62 + <button class="tb-btn toolbar-mobile-hide" id="tb-format-painter" title="Format painter (click: one-shot, double-click: sticky, Esc: cancel)" aria-label="Format painter"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M10.5 1.5l2 2-6 6H4.5v-2z"/><path d="M3 11l-1 4 4-1"/><path d="M8.5 3.5l2 2"/></svg></button> 62 63 <span class="toolbar-sep toolbar-mobile-hide"></span> 63 64 64 - <!-- Group: Number format --> 65 + <!-- Group 2: Number format --> 65 66 <select class="tb-select toolbar-mobile-hide" id="tb-format" title="Cell number format" aria-label="Cell number format"> 66 67 <option value="auto">Auto</option> 67 68 <option value="number">123</option> ··· 73 74 </select> 74 75 <span class="toolbar-sep toolbar-mobile-hide"></span> 75 76 76 - <!-- Group: Font family & size --> 77 + <!-- Group 3: Font family & size --> 77 78 <select class="tb-select toolbar-mobile-hide" id="tb-font-family" title="Font family" aria-label="Font family"> 78 79 <option value="sans-serif">Sans-serif</option> 79 80 <option value="serif">Serif</option> ··· 93 94 </select> 94 95 <span class="toolbar-sep toolbar-mobile-hide"></span> 95 96 96 - <!-- Group: Cell formatting --> 97 + <!-- Group 4: Text formatting --> 97 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> 98 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> 99 - <button class="tb-btn tb-btn-text tb-btn-underline" id="tb-underline" title="Underline (Cmd+U)" aria-label="Underline">U</button> 100 100 <button class="tb-btn tb-btn-text tb-btn-strike" id="tb-strikethrough" title="Strikethrough (Cmd+Shift+X)" aria-label="Strikethrough">S</button> 101 101 <div class="tb-color-wrap toolbar-mobile-hide"> 102 102 <input type="color" class="tb-color" id="tb-text-color" value="#1a1815" title="Text color" aria-label="Text color"> ··· 110 110 </div> 111 111 <span class="toolbar-sep toolbar-mobile-hide"></span> 112 112 113 - <!-- Group: Cell layout --> 114 - <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-align"> 115 - <button class="toolbar-dropdown-toggle" id="tb-align-toggle" title="Cell alignment" aria-label="Cell alignment" aria-expanded="false" aria-haspopup="true"> 116 - <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="2" y1="6.5" x2="10" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="2" y1="13.5" x2="10" y2="13.5"/></svg></span><span class="caret">&#9662;</span> 117 - </button> 118 - <div class="toolbar-dropdown-menu" role="menu"> 119 - <button class="toolbar-dropdown-item" data-align="left" title="Align left" role="menuitem"> 120 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="2" y1="6.5" x2="10" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="2" y1="13.5" x2="10" y2="13.5"/></svg></span><span class="item-label">Align left</span> 121 - </button> 122 - <button class="toolbar-dropdown-item" data-align="center" title="Align center" role="menuitem"> 123 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="4" y1="6.5" x2="12" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="4" y1="13.5" x2="12" y2="13.5"/></svg></span><span class="item-label">Align center</span> 124 - </button> 125 - <button class="toolbar-dropdown-item" data-align="right" title="Align right" role="menuitem"> 126 - <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="6" y1="6.5" x2="14" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="6" y1="13.5" x2="14" y2="13.5"/></svg></span><span class="item-label">Align right</span> 127 - </button> 128 - </div> 129 - </div> 113 + <!-- Group 5: Borders & Merge --> 130 114 <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-borders"> 131 115 <button class="toolbar-dropdown-toggle" id="tb-borders-toggle" title="Cell borders" aria-label="Cell borders" aria-expanded="false" aria-haspopup="true"> 132 116 <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" fill="none"/></svg></span><span class="caret">&#9662;</span> ··· 155 139 </button> 156 140 </div> 157 141 </div> 142 + <button class="tb-btn toolbar-mobile-hide" id="tb-merge" title="Merge/Unmerge cells" aria-label="Merge cells"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M5 8h6"/><path d="M6.5 6L5 8l1.5 2"/><path d="M9.5 6l1.5 2-1.5 2"/></svg></button> 143 + <span class="toolbar-sep toolbar-mobile-hide"></span> 144 + 145 + <!-- Group 6: Alignment & Wrap --> 146 + <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-align"> 147 + <button class="toolbar-dropdown-toggle" id="tb-align-toggle" title="Cell alignment" aria-label="Cell alignment" aria-expanded="false" aria-haspopup="true"> 148 + <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="2" y1="6.5" x2="10" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="2" y1="13.5" x2="10" y2="13.5"/></svg></span><span class="caret">&#9662;</span> 149 + </button> 150 + <div class="toolbar-dropdown-menu" role="menu"> 151 + <button class="toolbar-dropdown-item" data-align="left" title="Align left" role="menuitem"> 152 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="2" y1="6.5" x2="10" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="2" y1="13.5" x2="10" y2="13.5"/></svg></span><span class="item-label">Align left</span> 153 + </button> 154 + <button class="toolbar-dropdown-item" data-align="center" title="Align center" role="menuitem"> 155 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="4" y1="6.5" x2="12" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="4" y1="13.5" x2="12" y2="13.5"/></svg></span><span class="item-label">Align center</span> 156 + </button> 157 + <button class="toolbar-dropdown-item" data-align="right" title="Align right" role="menuitem"> 158 + <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="3" x2="14" y2="3"/><line x1="6" y1="6.5" x2="14" y2="6.5"/><line x1="2" y1="10" x2="14" y2="10"/><line x1="6" y1="13.5" x2="14" y2="13.5"/></svg></span><span class="item-label">Align right</span> 159 + </button> 160 + </div> 161 + </div> 158 162 <div class="toolbar-dropdown toolbar-mobile-hide" id="dd-valign"> 159 163 <button class="toolbar-dropdown-toggle" id="tb-valign-toggle" title="Vertical alignment" aria-label="Vertical alignment" aria-expanded="false" aria-haspopup="true"> 160 164 <span class="dd-icon"><svg class="tb-icon" viewBox="0 0 16 16"><line x1="2" y1="2" x2="14" y2="2" stroke-width="2"/><line x1="8" y1="4" x2="8" y2="14"/><path d="M5.5 6.5L8 4l2.5 2.5"/></svg></span><span class="caret">&#9662;</span> ··· 172 176 </div> 173 177 </div> 174 178 <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> 175 - <button class="tb-btn toolbar-mobile-hide" id="tb-merge" title="Merge/Unmerge cells" aria-label="Merge cells"><svg class="tb-icon" viewBox="0 0 16 16"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M5 8h6"/><path d="M6.5 6L5 8l1.5 2"/><path d="M9.5 6l1.5 2-1.5 2"/></svg></button> 176 - <button class="tb-btn toolbar-mobile-hide" id="tb-format-painter" title="Format painter (click: one-shot, double-click: sticky, Esc: cancel)" aria-label="Format painter"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M10.5 1.5l2 2-6 6H4.5v-2z"/><path d="M3 11l-1 4 4-1"/><path d="M8.5 3.5l2 2"/></svg></button> 177 179 <span class="toolbar-sep toolbar-mobile-hide"></span> 178 180 179 - <!-- Group: Sort, Filter, Chart --> 180 - <button class="tb-btn toolbar-mobile-hide" id="tb-sort-asc" title="Sort column A to Z" aria-label="Sort ascending"><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> 181 - <button class="tb-btn toolbar-mobile-hide" id="tb-sort-desc" title="Sort column Z to A" aria-label="Sort descending"><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> 181 + <!-- Group 7: Filter & Chart (common data actions) --> 182 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 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 - <span class="toolbar-sep toolbar-mobile-hide"></span> 185 184 186 185 <!-- Mobile more button (visible only on mobile) --> 187 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> ··· 192 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> 193 192 </button> 194 193 <div class="toolbar-overflow-menu" role="menu"> 195 - <!-- Section: Structure (row/col operations, freeze) --> 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 --> 196 204 <button class="toolbar-dropdown-item" id="tb-add-row" title="Insert row below" role="menuitem"> 197 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> 198 206 </button> ··· 216 224 </button> 217 225 <div class="toolbar-dropdown-divider"></div> 218 226 219 - <!-- Section: Data (conditional formatting, validation, sorting, appearance) --> 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> 230 + </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> 233 + </button> 220 234 <button class="toolbar-dropdown-item" id="tb-sort-multi" title="Multi-column sort" role="menuitem"> 221 235 <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> 222 236 </button> ··· 229 243 <button class="toolbar-dropdown-item" id="tb-striped" title="Toggle striped rows" role="menuitem"> 230 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> 231 245 </button> 232 - <button class="toolbar-dropdown-item" id="tb-clear-format" title="Clear formatting (Cmd+\)" role="menuitem"> 233 - <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> 234 - </button> 235 246 <div class="toolbar-dropdown-divider"></div> 236 247 237 248 <!-- Section: Decimal controls --> ··· 243 254 </button> 244 255 <div class="toolbar-dropdown-divider"></div> 245 256 246 - <!-- Section: Document (export, import) --> 257 + <!-- Section: Import / Export --> 247 258 <button class="toolbar-dropdown-item" id="tb-export-csv" title="Export as CSV" role="menuitem"> 248 259 <span class="item-icon"><svg class="tb-icon" viewBox="0 0 16 16"><path d="M4 2h6l3 3v9H4z"/><line x1="6" y1="7" x2="11" y2="7"/><line x1="6" y1="10" x2="11" y2="10"/></svg></span><span class="item-label">Export CSV</span> 249 260 </button>
+15
src/sheets/main.ts
··· 13 13 import { evaluate, extractRefs, formatCell, parseRef, colToLetter, letterToCol, cellId } from './formulas.js'; 14 14 import { RecalcEngine } from './recalc.js'; 15 15 import { importXlsx, isValidXlsx } from './xlsx-import.js'; 16 + import { exportToXlsx, downloadXlsx } from './xlsx-export.js'; 16 17 import { validateChartConfig, parseDataRange, extractChartData, transformChartData, buildChartJsConfig, CHART_TYPES, CHART_COLORS } from './charts.js'; 17 18 import { getUniqueColumnValues, applyFilters, clearColumnFilter, clearAllFilters, buildFilterState } from './filter.js'; 18 19 import { multiColumnSort } from './sort.js'; ··· 2951 2952 2952 2953 // Toolbar button bindings for export/import/print 2953 2954 document.getElementById('tb-export-csv').addEventListener('click', () => { exportCSV(); closeAllDropdowns(); }); 2955 + document.getElementById('tb-export-xlsx')?.addEventListener('click', async () => { 2956 + closeAllDropdowns(); 2957 + const sheet = getActiveSheet(); 2958 + const rc = sheet.get('rowCount') || DEFAULT_ROWS; 2959 + const cc = sheet.get('colCount') || DEFAULT_COLS; 2960 + const name = sheet.get('name') || 'sheet'; 2961 + const buf = await exportToXlsx( 2962 + (r, c) => getCellData(cellId(c, r)), 2963 + rc, cc, 2964 + (c) => getColWidth(c), 2965 + name 2966 + ); 2967 + downloadXlsx(buf, name + '.xlsx'); 2968 + }); 2954 2969 document.getElementById('tb-import').addEventListener('click', () => { importCSV(); closeAllDropdowns(); }); 2955 2970 document.getElementById('tb-print').addEventListener('click', () => { printSheet(); closeAllDropdowns(); }); 2956 2971