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: styled CSS tooltips and consistent icon sizing' (#239) from feat/tooltips-icon-sizing into main

scott 68d1119f 77cc06e7

+108 -9
+52
src/css/app.css
··· 329 329 align-items: center; 330 330 justify-content: center; 331 331 transition: all var(--transition-fast); 332 + font-size: 16px; 333 + line-height: 1; 334 + min-width: 28px; 335 + min-height: 28px; 332 336 } 333 337 .btn-icon:hover { color: var(--color-text); background: var(--color-hover); } 334 338 .btn-icon.active { color: var(--color-accent); background: var(--color-btn-active-bg); } ··· 340 344 opacity: 0.35; 341 345 cursor: default; 342 346 pointer-events: none; 347 + } 348 + 349 + /* --- Styled Tooltips --- */ 350 + [data-tooltip] { 351 + position: relative; 352 + } 353 + [data-tooltip]::after { 354 + content: attr(data-tooltip); 355 + position: absolute; 356 + bottom: calc(100% + 6px); 357 + left: 50%; 358 + transform: translateX(-50%); 359 + padding: 4px 8px; 360 + border-radius: var(--radius-sm); 361 + background: oklch(0.2 0 0); 362 + color: oklch(0.95 0 0); 363 + font-size: 11px; 364 + font-weight: 500; 365 + line-height: 1.3; 366 + white-space: nowrap; 367 + pointer-events: none; 368 + opacity: 0; 369 + transition: opacity 150ms ease; 370 + z-index: 9999; 371 + } 372 + [data-tooltip]:hover::after { 373 + opacity: 1; 374 + transition-delay: 400ms; 375 + } 376 + [data-tooltip]:active::after { 377 + opacity: 0; 378 + transition-delay: 0ms; 379 + } 380 + /* Bottom variant for topbar buttons */ 381 + [data-tooltip-pos="bottom"]::after { 382 + bottom: auto; 383 + top: calc(100% + 6px); 384 + } 385 + /* Dark mode tooltip */ 386 + [data-theme="dark"] [data-tooltip]::after { 387 + background: oklch(0.85 0 0); 388 + color: oklch(0.15 0 0); 389 + } 390 + @media (prefers-color-scheme: dark) { 391 + :root:not([data-theme="light"]) [data-tooltip]::after { 392 + background: oklch(0.85 0 0); 393 + color: oklch(0.15 0 0); 394 + } 343 395 } 344 396 345 397 /* --- Landing Page --- */
+2
src/diagrams/main.ts
··· 8 8 import * as Y from 'yjs'; 9 9 import { importKey } from '../lib/crypto.js'; 10 10 import { EncryptedProvider } from '../lib/provider.js'; 11 + import { setupTooltips } from '../lib/tooltips.js'; 11 12 import { 12 13 createWhiteboard, addShape, removeShape, removeShapes, moveShape, moveShapes, 13 14 resizeShape, setShapeLabel, addArrow, removeArrow, toggleSnap, setZoom, ··· 2113 2114 async function init() { 2114 2115 ensureArrowheadMarker(); 2115 2116 await initCrypto(); 2117 + setupTooltips(); 2116 2118 2117 2119 // Push initial state to history 2118 2120 pushHistory();
+1 -1
src/docs/index.html
··· 476 476 function updateIcon() { 477 477 var theme = getEffectiveTheme(); 478 478 toggle.textContent = theme === 'dark' ? '\u263E' : '\u2600'; 479 - toggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; 479 + toggle.setAttribute('data-tooltip', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); 480 480 } 481 481 toggle.addEventListener('click', function() { 482 482 var current = getEffectiveTheme();
+4
src/docs/main.ts
··· 8 8 import * as Y from 'yjs'; 9 9 import { Editor } from '@tiptap/core'; 10 10 import StarterKit from '@tiptap/starter-kit'; 11 + import { setupTooltips } from '../lib/tooltips.js'; 11 12 import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; 12 13 import { common, createLowlight } from 'lowlight'; 13 14 import Underline from '@tiptap/extension-underline'; ··· 2749 2750 // Initial load of comments from yjs 2750 2751 setTimeout(loadCommentsFromYjs, 500); 2751 2752 2753 + // Styled tooltips 2754 + setupTooltips(); 2755 +
+2
src/forms/main.ts
··· 8 8 import * as Y from 'yjs'; 9 9 import { importKey, encryptString, decryptString } from '../lib/crypto.js'; 10 10 import { EncryptedProvider } from '../lib/provider.js'; 11 + import { setupTooltips } from '../lib/tooltips.js'; 11 12 import { 12 13 createForm, 13 14 addQuestion, ··· 584 585 // --- Initialize --- 585 586 async function init() { 586 587 await initCrypto(); 588 + setupTooltips(); 587 589 588 590 if (cryptoKey) { 589 591 const provider = new EncryptedProvider(ydoc, docId, cryptoKey);
+1 -1
src/index.html
··· 196 196 function updateIcon() { 197 197 var theme = getEffectiveTheme(); 198 198 toggle.textContent = theme === 'dark' ? '\u263E' : '\u2600'; 199 - toggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; 199 + toggle.setAttribute('data-tooltip', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); 200 200 } 201 201 toggle.addEventListener('click', function() { 202 202 var current = getEffectiveTheme();
+35
src/lib/tooltips.ts
··· 1 + /** 2 + * Styled tooltip system — converts title attributes to data-tooltip 3 + * for CSS-rendered tooltips instead of browser-native ones. 4 + * 5 + * Call setupTooltips() once after DOM is ready. 6 + */ 7 + 8 + const TOOLTIP_CONTAINERS = [ 9 + '.toolbar', '.gdocs-toolbar', '.app-topbar', 10 + '.diagrams-toolbar', '.slides-toolbar', '.form-builder-toolbar', 11 + ]; 12 + 13 + /** Convert title → data-tooltip on all buttons/selects within toolbar containers */ 14 + export function setupTooltips(): void { 15 + const selector = TOOLTIP_CONTAINERS.map(c => `${c} [title]`).join(', '); 16 + const elements = document.querySelectorAll(selector); 17 + 18 + for (const el of elements) { 19 + const title = el.getAttribute('title'); 20 + if (title) { 21 + el.setAttribute('data-tooltip', title); 22 + // Keep aria-label for screen readers, remove title to prevent double tooltip 23 + if (!el.getAttribute('aria-label')) { 24 + el.setAttribute('aria-label', title); 25 + } 26 + el.removeAttribute('title'); 27 + } 28 + } 29 + 30 + // Position topbar tooltips below (they're at the top of the page) 31 + const topbarEls = document.querySelectorAll('.app-topbar [data-tooltip]'); 32 + for (const el of topbarEls) { 33 + el.setAttribute('data-tooltip-pos', 'bottom'); 34 + } 35 + }
+1 -1
src/sheets/index.html
··· 399 399 function updateIcon() { 400 400 var theme = getEffectiveTheme(); 401 401 toggle.textContent = theme === 'dark' ? '\u263E' : '\u2600'; 402 - toggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; 402 + toggle.setAttribute('data-tooltip', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); 403 403 } 404 404 toggle.addEventListener('click', function() { 405 405 var current = getEffectiveTheme();
+8 -6
src/sheets/main.ts
··· 8 8 9 9 import * as Y from 'yjs'; 10 10 import { importKey, encryptString, decryptString } from '../lib/crypto.js'; 11 + import { setupTooltips } from '../lib/tooltips.js'; 11 12 import { storeKey, pushKeysToServer, fetchServerKeys, getLocalKeys } from '../lib/key-sync.js'; 12 13 import { EncryptedProvider } from '../lib/provider.js'; 13 14 import { createVersionPanel } from '../version-panel.js'; ··· 2524 2525 const canUndo = undoManager && undoManager.undoStack.length > 0; 2525 2526 undoBtn.classList.toggle('btn-disabled', !canUndo); 2526 2527 undoBtn.setAttribute('aria-disabled', String(!canUndo)); 2527 - undoBtn.title = canUndo ? 'Undo (' + undoManager.undoStack.length + ')' : 'Nothing to undo'; 2528 + undoBtn.dataset.tooltip = canUndo ? 'Undo (' + undoManager.undoStack.length + ')' : 'Nothing to undo'; 2528 2529 } 2529 2530 if (redoBtn) { 2530 2531 const canRedo = undoManager && undoManager.redoStack.length > 0; 2531 2532 redoBtn.classList.toggle('btn-disabled', !canRedo); 2532 2533 redoBtn.setAttribute('aria-disabled', String(!canRedo)); 2533 - redoBtn.title = canRedo ? 'Redo (' + undoManager.redoStack.length + ')' : 'Nothing to redo'; 2534 + redoBtn.dataset.tooltip = canRedo ? 'Redo (' + undoManager.redoStack.length + ')' : 'Nothing to redo'; 2534 2535 } 2535 2536 } 2536 2537 document.getElementById('tb-undo').addEventListener('click', () => { ··· 2857 2858 const frBtn = document.getElementById('tb-freeze-rows'); 2858 2859 const fcBtn = document.getElementById('tb-freeze-cols'); 2859 2860 const freezeToggleBtn = document.getElementById('tb-freeze-toggle'); 2860 - if (frBtn) { frBtn.title = fr > 0 ? 'Frozen: ' + fr + ' rows' : 'Freeze rows above cursor'; frBtn.classList.toggle('active', fr > 0); } 2861 - if (fcBtn) { fcBtn.title = fc > 0 ? 'Frozen: ' + fc + ' cols' : 'Freeze columns left of cursor'; fcBtn.classList.toggle('active', fc > 0); } 2861 + if (frBtn) { frBtn.dataset.tooltip = fr > 0 ? 'Frozen: ' + fr + ' rows' : 'Freeze rows above cursor'; frBtn.classList.toggle('active', fr > 0); } 2862 + if (fcBtn) { fcBtn.dataset.tooltip = fc > 0 ? 'Frozen: ' + fc + ' cols' : 'Freeze columns left of cursor'; fcBtn.classList.toggle('active', fc > 0); } 2862 2863 if (freezeToggleBtn) { freezeToggleBtn.classList.toggle('active', fr > 0 || fc > 0); } 2863 2864 } 2864 2865 ··· 3760 3761 const merge = isCellMerged(selectedCell.col, selectedCell.row); 3761 3762 if (merge) { 3762 3763 mergeBtn.classList.add('merge-active'); 3763 - mergeBtn.title = 'Unmerge cells'; 3764 + mergeBtn.dataset.tooltip = 'Unmerge cells'; 3764 3765 } else { 3765 3766 mergeBtn.classList.remove('merge-active'); 3766 - mergeBtn.title = 'Merge/Unmerge cells'; 3767 + mergeBtn.dataset.tooltip = 'Merge/Unmerge cells'; 3767 3768 } 3768 3769 } 3769 3770 ··· 6336 6337 renderPivots(); 6337 6338 updateStripedButtonState(); 6338 6339 renderNoteIndicators(); 6340 + setupTooltips();
+2
src/slides/main.ts
··· 7 7 import * as Y from 'yjs'; 8 8 import { importKey, encryptString, decryptString } from '../lib/crypto.js'; 9 9 import { EncryptedProvider } from '../lib/provider.js'; 10 + import { setupTooltips } from '../lib/tooltips.js'; 10 11 import { 11 12 createDeck, addSlide, removeSlide, moveSlide, duplicateSlide, goToSlide, 12 13 addElement, removeElement, moveElement, resizeElement, bringToFront, sendToBack, ··· 597 598 async function init() { 598 599 await initCrypto(); 599 600 initDropdowns(); 601 + setupTooltips(); 600 602 601 603 if (cryptoKey) { 602 604 const provider = new EncryptedProvider(ydoc, docId, cryptoKey);