// fixtures/shortcuts.mjs — the keyboard shortcuts sheet. // // Built by READING THE MENU BAR'S OWN DOM rather than from a list. // // That is the whole design, and it is worth being explicit about why. A // shortcuts sheet is the single most drift-prone screen in an // application: every binding is stated twice, once where it works and // once where it is documented, and the second copy is wrong within a // month of anybody touching the first. The menu bar already renders // every command's label and its shortcut, from the catalog, in the // person's own locale. Reading that is one source of truth and no new // strings beyond the dialog's title. // // It also means a shortcut that stops being offered stops being // documented, automatically, which is the half nobody remembers to do // by hand. // // What it cannot see is a binding with no menu item. Two of those exist // and they are the ones a person most needs told, so they are passed in // by the caller rather than invented here. // buildRows reads one menu bar and returns {group, label, keys} rows. // Exported for the test, which can hand it a small fragment rather than // the real page. export function buildRows(menubar) { const rows = [] if (!menubar) return rows for (const menu of menubar.querySelectorAll('[estilo-menubar-menu]')) { const title = menu.querySelector('[estilo-menubar-title]') const group = title ? title.textContent.trim() : '' for (const item of menu.querySelectorAll('[estilo-menu-item]')) { // Already formatted for this platform by the server (estilo's // menu.Shortcut), so the sheet and the menu cannot disagree. const keys = item.getAttribute('data-docs-shortcut') if (!keys) continue // The label is its own element (estilo's [estilo-menu-label]), // apart from the check and key columns. The sheet lists commands, // not menu items, so an item's ellipsis ("Print…") is dropped. const labelEl = item.querySelector('[estilo-menu-label]') const label = (labelEl ? labelEl.textContent : item.textContent).trim().replace(/…$/, '') rows.push({ group, label, keys }) } } return rows } // mountShortcuts fills the dialog and returns the command the menu // runs, or null when the template renders no dialog. // // extraRows are the bindings no menu item carries. Passed in rather // than hardcoded here so the strings stay in the catalog where every // other user-visible string in this app lives. export function mountShortcuts(dialog, { menubar, extraRows }) { if (!dialog || typeof dialog.showModal !== 'function') return null const body = dialog.querySelector('[docs-shortcuts-body]') if (!body) return null return () => { const rows = [...buildRows(menubar), ...(extraRows || [])] body.textContent = '' let group = null for (const row of rows) { if (row.group && row.group !== group) { group = row.group const h = document.createElement('h3') h.setAttribute('docs-shortcuts-group', '') h.textContent = group body.appendChild(h) } const line = document.createElement('div') line.setAttribute('docs-shortcuts-row', '') const label = document.createElement('span') label.textContent = row.label const keys = document.createElement('kbd') keys.textContent = row.keys line.append(label, keys) body.appendChild(line) } dialog.showModal() return true } }