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

Configure Feed

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

feat(sheets): status bar enhancements, prettier grid borders, consistent cell sizing

- Status bar now always visible: shows cell reference, selection dimensions, freeze state
- Click freeze indicator in status bar to unfreeze panes
- Grid uses border-separate with CSS custom properties (--color-grid-line, --color-grid-header-line)
- Borders between colored cells now render distinctly (no more disappearing borders)
- Cell height pinned to 26px with box-sizing: border-box for consistent sizing
- Hidden row indicators enlarged (4px → 6px) with hover tooltip "Click to unhide"
- Click hidden row indicator to unhide rows directly
- Validation error tooltips: hover invalid cells to see why (e.g., "Value must be one of: A, B, C")
- Dark mode grid line variables added
- Toolbar select and color inputs get focus-visible outlines

+179 -27
+110 -10
src/css/app.css
··· 18 18 --color-teal-light: oklch(0.88 0.04 195); 19 19 --color-border: oklch(0.87 0.008 75); 20 20 --color-border-strong:oklch(0.78 0.01 75); 21 + --color-grid-line: oklch(0.84 0.01 75); 22 + --color-grid-header-line: oklch(0.82 0.012 75); 21 23 --color-hover: oklch(0.92 0.01 75); 22 24 --color-focus: oklch(0.48 0.1 195 / 0.3); 23 25 --color-success: oklch(0.6 0.14 155); ··· 93 95 --color-teal-light: oklch(0.28 0.04 195); 94 96 --color-border: oklch(0.28 0.008 75); 95 97 --color-border-strong:oklch(0.38 0.01 75); 98 + --color-grid-line: oklch(0.26 0.008 75); 99 + --color-grid-header-line: oklch(0.30 0.01 75); 96 100 --color-hover: oklch(0.23 0.01 75); 97 101 --color-focus: oklch(0.60 0.1 195 / 0.3); 98 102 --color-success: oklch(0.65 0.14 155); ··· 139 143 --color-teal-light: oklch(0.28 0.04 195); 140 144 --color-border: oklch(0.28 0.008 75); 141 145 --color-border-strong:oklch(0.38 0.01 75); 146 + --color-grid-line: oklch(0.26 0.008 75); 147 + --color-grid-header-line: oklch(0.30 0.01 75); 142 148 --color-hover: oklch(0.23 0.01 75); 143 149 --color-focus: oklch(0.60 0.1 195 / 0.3); 144 150 --color-success: oklch(0.65 0.14 155); ··· 1603 1609 } 1604 1610 1605 1611 .sheet-grid { 1606 - border-collapse: collapse; 1612 + border-collapse: separate; 1613 + border-spacing: 0; 1607 1614 table-layout: fixed; 1608 1615 font-family: var(--font-mono); 1609 1616 font-size: 0.8rem; ··· 1613 1620 1614 1621 .sheet-grid th { 1615 1622 background: var(--color-surface); 1616 - border: 1px solid var(--color-border); 1623 + border-right: 1px solid var(--color-grid-header-line); 1624 + border-bottom: 1px solid var(--color-grid-header-line); 1625 + border-left: none; 1626 + border-top: none; 1617 1627 padding: 0.25rem 0.5rem; 1618 1628 font-weight: 500; 1619 1629 font-size: 0.7rem; ··· 1622 1632 z-index: 2; 1623 1633 text-align: center; 1624 1634 min-width: 3rem; 1635 + height: 26px; 1636 + box-sizing: border-box; 1625 1637 } 1626 1638 1627 1639 /* Column resize handle in header */ ··· 1666 1678 position: sticky; 1667 1679 z-index: 3; 1668 1680 background: var(--color-bg-subtle); 1681 + border-top: 1px solid var(--color-grid-header-line); 1682 + } 1683 + 1684 + /* First column gets left border (border-separate: no shared borders) */ 1685 + .sheet-grid td:first-child, 1686 + .sheet-grid th:first-child { 1687 + border-left: 1px solid var(--color-grid-header-line); 1688 + } 1689 + .sheet-grid tbody tr:first-child td { 1690 + border-top: 1px solid var(--color-grid-line); 1669 1691 } 1670 1692 1671 1693 /* Column header resize handle */ ··· 1725 1747 } 1726 1748 1727 1749 /* Hidden row/column indicators */ 1728 - .hidden-row-indicator { height: 4px; } 1750 + .hidden-row-indicator { height: 6px; } 1729 1751 .hidden-row-indicator td { padding: 0 !important; border: none !important; } 1730 1752 .hidden-row-indicator-line { 1731 - height: 3px; 1753 + height: 4px; 1732 1754 background: var(--color-teal); 1733 1755 opacity: 0.5; 1734 1756 cursor: pointer; 1735 - border-radius: 1px; 1757 + border-radius: 2px; 1758 + transition: opacity 150ms ease, height 150ms ease; 1759 + position: relative; 1760 + } 1761 + .hidden-row-indicator-line:hover { 1762 + opacity: 0.85; 1763 + height: 6px; 1736 1764 } 1737 - .hidden-row-indicator-line:hover { opacity: 0.8; } 1738 - .hidden-col-boundary { border-right: 2px solid var(--color-teal) !important; } 1739 - .hidden-row-boundary { border-bottom: 2px solid var(--color-teal) !important; } 1765 + .hidden-row-indicator-line::after { 1766 + content: 'Click to unhide'; 1767 + position: absolute; 1768 + left: 50%; 1769 + top: -22px; 1770 + transform: translateX(-50%); 1771 + font-size: 0.6rem; 1772 + font-family: var(--font-body); 1773 + color: var(--color-bg); 1774 + background: var(--color-text); 1775 + padding: 2px 6px; 1776 + border-radius: 3px; 1777 + white-space: nowrap; 1778 + opacity: 0; 1779 + pointer-events: none; 1780 + transition: opacity 150ms ease; 1781 + } 1782 + .hidden-row-indicator-line:hover::after { opacity: 1; } 1783 + .hidden-col-boundary { 1784 + border-right: 2px solid var(--color-teal) !important; 1785 + position: relative; 1786 + } 1787 + .hidden-row-boundary { 1788 + border-bottom: 2px solid var(--color-teal) !important; 1789 + position: relative; 1790 + } 1740 1791 1741 1792 .sheet-grid .row-header { 1742 1793 position: sticky; ··· 1748 1799 } 1749 1800 1750 1801 .sheet-grid td { 1751 - border: 1px solid var(--color-border); 1802 + border-right: 1px solid var(--color-grid-line); 1803 + border-bottom: 1px solid var(--color-grid-line); 1804 + border-left: none; 1805 + border-top: none; 1752 1806 padding: 0; 1753 - height: 1.6rem; 1807 + height: 26px; 1808 + min-height: 26px; 1809 + box-sizing: border-box; 1754 1810 position: relative; 1755 1811 overflow: hidden; 1756 1812 } ··· 4500 4556 font-family: var(--font-mono); 4501 4557 font-size: 0.65rem; 4502 4558 color: var(--color-text-faint); 4559 + } 4560 + 4561 + .status-bar-cell-ref { 4562 + padding: 1px 4px; 4563 + border-radius: 2px; 4564 + background: var(--color-surface-alt); 4565 + color: var(--color-text-muted); 4566 + font-weight: 500; 4567 + } 4568 + 4569 + .status-bar-dim { 4570 + color: var(--color-text-faint); 4571 + } 4572 + 4573 + .status-bar-freeze { 4574 + padding: 1px 6px; 4575 + border-radius: 2px; 4576 + background: oklch(0.75 0.1 195 / 0.15); 4577 + color: oklch(0.45 0.12 195); 4578 + font-weight: 500; 4579 + font-family: var(--font-body); 4580 + font-size: 0.65rem; 4581 + cursor: pointer; 4582 + transition: background 150ms ease; 4583 + } 4584 + .status-bar-freeze:hover { 4585 + background: oklch(0.75 0.1 195 / 0.25); 4586 + } 4587 + 4588 + [data-theme="dark"] .status-bar-freeze { 4589 + background: oklch(0.35 0.08 195 / 0.3); 4590 + color: oklch(0.7 0.1 195); 4591 + } 4592 + [data-theme="dark"] .status-bar-freeze:hover { 4593 + background: oklch(0.35 0.08 195 / 0.45); 4594 + } 4595 + @media (prefers-color-scheme: dark) { 4596 + :root:not([data-theme="light"]) .status-bar-freeze { 4597 + background: oklch(0.35 0.08 195 / 0.3); 4598 + color: oklch(0.7 0.1 195); 4599 + } 4600 + :root:not([data-theme="light"]) .status-bar-freeze:hover { 4601 + background: oklch(0.35 0.08 195 / 0.45); 4602 + } 4503 4603 } 4504 4604 4505 4605 .status-bar-range {
+3 -2
src/sheets/index.html
··· 275 275 <!-- Charts section --> 276 276 <div class="charts-section" id="charts-section"></div> 277 277 278 - <!-- Status bar (selection stats) --> 279 - <div class="status-bar" id="status-bar" style="display:none"> 278 + <!-- Status bar --> 279 + <div class="status-bar" id="status-bar"> 280 + <div class="status-bar-info" id="status-bar-info"></div> 280 281 <div class="status-bar-stats" id="status-bar-stats"></div> 281 282 </div> 282 283
+66 -15
src/sheets/main.ts
··· 537 537 538 538 // Data validation — check for invalid and dropdown 539 539 const validation = getValidationForCell(id); 540 + let validationMsg = ''; 540 541 if (validation && cellData) { 541 542 const valResult = validateCell(displayValue, validation); 542 - if (!valResult.valid) tdCls.push('validation-invalid'); 543 + if (!valResult.valid) { 544 + tdCls.push('validation-invalid'); 545 + validationMsg = valResult.message || 'Invalid value'; 546 + } 543 547 } 544 548 545 549 // Find & replace highlighting ··· 558 562 if (mergeInfo.colspan > 1) spanAttrs += ' colspan="' + mergeInfo.colspan + '"'; 559 563 if (mergeInfo.rowspan > 1) spanAttrs += ' rowspan="' + mergeInfo.rowspan + '"'; 560 564 } 561 - html += '<td data-col="' + c + '" data-row="' + r + '" data-id="' + id + '" class="' + tdCls.join(' ') + '"' + styleAttr + spanAttrs + '>'; 565 + const valTitleAttr = validationMsg ? ' title="' + escapeHtml(validationMsg) + '"' : ''; 566 + html += '<td data-col="' + c + '" data-row="' + r + '" data-id="' + id + '" class="' + tdCls.join(' ') + '"' + styleAttr + spanAttrs + valTitleAttr + '>'; 562 567 563 568 // Sparkline: render canvas instead of text 564 569 if (isSparklineResult(displayValue)) { ··· 818 823 } 819 824 820 825 function onGridMouseDown(e) { 826 + // Click hidden-row indicator to unhide 827 + const hiddenIndicator = e.target.closest('.hidden-row-indicator-line'); 828 + if (hiddenIndicator) { 829 + e.preventDefault(); 830 + const indicatorRow = hiddenIndicator.closest('.hidden-row-indicator'); 831 + // Find adjacent visible row above the indicator 832 + const prevRow = indicatorRow?.previousElementSibling; 833 + if (prevRow) { 834 + const lastTh = prevRow.querySelector('th[data-row]'); 835 + if (lastTh) { 836 + const row = parseInt(lastTh.dataset.row); 837 + unhideAdjacentRows(row); 838 + showToast('Rows unhidden'); 839 + } 840 + } 841 + return; 842 + } 821 843 const handle = e.target.closest('.col-resize-handle'); 822 844 if (handle) { 823 845 e.preventDefault(); ··· 3896 3918 3897 3919 const statusBar = document.getElementById('status-bar'); 3898 3920 const statusBarStats = document.getElementById('status-bar-stats'); 3921 + const statusBarInfo = document.getElementById('status-bar-info'); 3899 3922 3900 3923 function updateStatusBar() { 3901 - if (!selectionRange) { 3902 - statusBar.style.display = 'none'; 3903 - return; 3924 + // Left side: cell reference + freeze indicator 3925 + let infoHtml = ''; 3926 + if (selectionRange) { 3927 + const norm = normalizeRange(selectionRange); 3928 + const isMulti = norm.startCol !== norm.endCol || norm.startRow !== norm.endRow; 3929 + if (isMulti) { 3930 + infoHtml += '<span class="status-bar-cell-ref">' + colToLetter(norm.startCol) + norm.startRow + ':' + colToLetter(norm.endCol) + norm.endRow + '</span>'; 3931 + const rows = norm.endRow - norm.startRow + 1; 3932 + const cols = norm.endCol - norm.startCol + 1; 3933 + infoHtml += '<span class="status-bar-dim">' + rows + 'R \u00d7 ' + cols + 'C</span>'; 3934 + } else { 3935 + infoHtml += '<span class="status-bar-cell-ref">' + colToLetter(selectedCell.col) + selectedCell.row + '</span>'; 3936 + } 3904 3937 } 3938 + const fr = getFreezeRows(); 3939 + const fc = getFreezeCols(); 3940 + if (fr > 0 || fc > 0) { 3941 + const parts = []; 3942 + if (fr > 0) parts.push(fr + ' row' + (fr > 1 ? 's' : '')); 3943 + if (fc > 0) parts.push(fc + ' col' + (fc > 1 ? 's' : '')); 3944 + infoHtml += '<span class="status-bar-freeze" title="Click to unfreeze">Frozen: ' + parts.join(', ') + '</span>'; 3945 + } 3946 + if (statusBarInfo) statusBarInfo.innerHTML = infoHtml; 3947 + 3948 + // Right side: selection stats (only for multi-cell) 3949 + if (!selectionRange) { statusBarStats.innerHTML = ''; return; } 3905 3950 const { startCol, startRow, endCol, endRow } = normalizeRange(selectionRange); 3906 3951 const isMultiCell = startCol !== endCol || startRow !== endRow; 3907 - if (!isMultiCell) { 3908 - statusBar.style.display = 'none'; 3909 - return; 3910 - } 3952 + if (!isMultiCell) { statusBarStats.innerHTML = ''; return; } 3911 3953 3912 - // Collect cell values from the selection 3913 3954 const values = []; 3914 3955 for (let r = startRow; r <= endRow; r++) { 3915 3956 for (let c = startCol; c <= endCol; c++) { ··· 3921 3962 } 3922 3963 3923 3964 const stats = computeSelectionStats(values); 3924 - if (!stats) { 3925 - statusBar.style.display = 'none'; 3926 - return; 3927 - } 3965 + if (!stats) { statusBarStats.innerHTML = ''; return; } 3928 3966 3929 - statusBar.style.display = ''; 3930 3967 let html = ''; 3931 3968 if (stats.count > 0) { 3932 3969 html += '<span class="status-bar-stat"><span class="status-bar-stat-label">Sum</span><span class="status-bar-stat-value">' + formatStatValue(stats.sum) + '</span></span>'; ··· 3938 3975 html += '<span class="status-bar-stat"><span class="status-bar-stat-label">Count</span><span class="status-bar-stat-value">0</span></span>'; 3939 3976 } 3940 3977 statusBarStats.innerHTML = html; 3978 + } 3979 + 3980 + // Click on freeze indicator to unfreeze 3981 + if (statusBarInfo) { 3982 + statusBarInfo.addEventListener('click', (e) => { 3983 + const freezeEl = (e.target as HTMLElement).closest('.status-bar-freeze'); 3984 + if (freezeEl) { 3985 + setFreezeRows(0); 3986 + setFreezeCols(0); 3987 + renderGrid(); 3988 + updateStatusBar(); 3989 + showToast('Panes unfrozen'); 3990 + } 3991 + }); 3941 3992 } 3942 3993 3943 3994 // ========================================================