oxedyne/daimond/dev/shot_qc_sheets.mjs
8.4 KiB, 1 run
created by r2519314175:181, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // shot_qc_sheets.mjs — the phone's sheets, every destination, both detents. |
| 2 | // |
| 3 | // The bottom bar puts four destinations one tap away and each of them raises a |
| 4 | // sheet with the panel inside it. The sheet keeps the title; the guest keeps its |
| 5 | // controls — which means the grabber row holds a title of unknown length beside |
| 6 | // a control row of unknown width, and that is a row that can wrap. When it does |
| 7 | // it leaves a void beside the title and drops the close button to a line of its |
| 8 | // own, which is what a reader notices before anything else on the screen. |
| 9 | // |
| 10 | // So this opens each sheet, at each detent, and MEASURES the grabber: how many |
| 11 | // lines it is on, and whether the guest's own head has wrapped underneath it. |
| 12 | // |
| 13 | // node dev/shot_qc_sheets.mjs --out ~/.cache/daimond/qc-sheets |
| 14 | import fs from 'node:fs'; |
| 15 | import path from 'node:path'; |
| 16 | import { open, errors, scratch } from './harness.mjs'; |
| 17 | |
| 18 | const argv = process.argv.slice(2); |
| 19 | const value = (n, d) => { const i = argv.indexOf(n); return (i >= 0 && argv[i + 1]) ? argv[i + 1] : d; }; |
| 20 | const OUT = value('--out', scratch('qc-sheets')); |
| 21 | fs.mkdirSync(OUT, { recursive: true }); |
| 22 | |
| 23 | const CELLS = [ |
| 24 | { tag: 'sharp-dark-380', skin: 'sharp', theme: 'dark', w: 380, h: 780 }, |
| 25 | { tag: 'warm-light-380', skin: 'warm', theme: 'light', w: 380, h: 780 }, |
| 26 | { tag: 'sharp-dark-320', skin: 'sharp', theme: 'dark', w: 320, h: 568 }, |
| 27 | ]; |
| 28 | |
| 29 | const s = await open({ name: 'qcsheets', connect: true }); |
| 30 | const { page } = s; |
| 31 | const pause = (ms) => page.waitForTimeout(ms); |
| 32 | const rows = []; |
| 33 | |
| 34 | /// The grabber row, measured. A single-line row has all three children on one |
| 35 | /// baseline band; a wrapped one does not, and the void it leaves is the height |
| 36 | /// of the line that moved. |
| 37 | const GRAB = function () { |
| 38 | const sheet = document.getElementById('msheet'); |
| 39 | if (!sheet || !sheet.classList.contains('open')) return null; |
| 40 | const grab = sheet.querySelector('.msheet-grab'); |
| 41 | const title = sheet.querySelector('.msheet-title'); |
| 42 | const close = sheet.querySelector('.msheet-close'); |
| 43 | if (!grab) return null; |
| 44 | const r = (el) => { const b = el.getBoundingClientRect(); return { x: Math.round(b.x), y: Math.round(b.y), w: Math.round(b.width * 10) / 10, h: Math.round(b.height * 10) / 10 }; }; |
| 45 | const gr = grab.getBoundingClientRect(); |
| 46 | // The guest panel's own header row, which sits directly under the grabber |
| 47 | // and carries the controls the sheet did not take. |
| 48 | const guest = sheet.querySelector('.panel'); |
| 49 | const head = guest && guest.querySelector('.railhead, .chead'); |
| 50 | let headWrap = null; |
| 51 | if (head && head.getClientRects().length) { |
| 52 | const kids = [...head.children].filter((k) => k.getClientRects().length); |
| 53 | const rs = kids.map((k) => k.getBoundingClientRect()); |
| 54 | if (rs.length > 1) { |
| 55 | const spread = Math.max(...rs.map((x) => x.top)) - Math.min(...rs.map((x) => x.top)); |
| 56 | const tallest = Math.max(...rs.map((x) => x.height)); |
| 57 | headWrap = { |
| 58 | cls: head.className, wrapped: spread > tallest * 0.8, |
| 59 | spreadY: Math.round(spread * 10) / 10, tallest: Math.round(tallest * 10) / 10, |
| 60 | height: Math.round(head.getBoundingClientRect().height * 10) / 10, |
| 61 | n: kids.length, |
| 62 | }; |
| 63 | } |
| 64 | } |
| 65 | return { |
| 66 | title: (title && title.textContent) || '', |
| 67 | grabHeight: Math.round(gr.height * 10) / 10, |
| 68 | titleRect: title ? r(title) : null, |
| 69 | closeRect: close ? r(close) : null, |
| 70 | // Wrapped when the title and the closer no longer share a line band. |
| 71 | wrapped: !!(title && close |
| 72 | && Math.abs(title.getBoundingClientRect().top - close.getBoundingClientRect().top) |
| 73 | > Math.max(title.getBoundingClientRect().height, close.getBoundingClientRect().height) * 0.8), |
| 74 | // The closer must not sit over the bottom bar, and the sheet must not |
| 75 | // sit over it either. |
| 76 | barTop: (() => { const b = document.getElementById('mnav'); return b ? Math.round(b.getBoundingClientRect().top) : null; })(), |
| 77 | sheetBottom: Math.round(sheet.getBoundingClientRect().bottom), |
| 78 | sheetHeight: Math.round(sheet.getBoundingClientRect().height), |
| 79 | }; |
| 80 | }; |
| 81 | |
| 82 | try { |
| 83 | await page.waitForFunction(() => !!(window.DaimondPanels && DaimondPanels.panels), null, { timeout: 20000 }); |
| 84 | await pause(600); |
| 85 | await page.evaluate(() => { const c = document.getElementById('admin-close'); if (c && c.getClientRects().length) c.click(); }); |
| 86 | |
| 87 | for (const cell of CELLS) { |
| 88 | console.log(`── ${cell.tag}`); |
| 89 | await page.setViewportSize({ width: cell.w, height: cell.h }); |
| 90 | await page.evaluate((c) => { |
| 91 | document.documentElement.setAttribute('data-skin', c.skin); |
| 92 | document.documentElement.setAttribute('data-theme', c.theme); |
| 93 | document.documentElement.setAttribute('data-tone', c.theme === 'light' ? 'light' : 'dark'); |
| 94 | document.documentElement.setAttribute('data-ink', c.theme === 'light' ? 'dark' : 'light'); |
| 95 | }, cell); |
| 96 | await pause(400); |
| 97 | |
| 98 | // The four bottom-bar destinations SWITCH the floor; they do not raise a |
| 99 | // sheet. A sheet is raised for a guest — every other panel — through |
| 100 | // `DaimondSheet.open`, which is what a chip tap and the palette both |
| 101 | // call. That is the surface with the title-beside-controls row. |
| 102 | const tabs = await page.evaluate(() => { |
| 103 | const dest = new Set(['ai', 'mail', 'work', 'agents']); |
| 104 | return DaimondPanels.panels().map((p) => p.id).filter((id) => !dest.has(id)) |
| 105 | .map((id, i) => ({ i, id, label: id })); |
| 106 | }); |
| 107 | if (!tabs.length) { console.log(' no guests to raise at this width'); continue; } |
| 108 | |
| 109 | // The bar itself, before any sheet is up: four destinations that must |
| 110 | // divide the bar evenly and each carry a target a thumb can hit. |
| 111 | const bar = await page.evaluate(() => { |
| 112 | const b = document.getElementById('mnav'); |
| 113 | const bs = [...b.querySelectorAll('button')]; |
| 114 | const ws = bs.map((x) => Math.round(x.getBoundingClientRect().width * 10) / 10); |
| 115 | const hs = bs.map((x) => Math.round(x.getBoundingClientRect().height * 10) / 10); |
| 116 | return { widths: ws, heights: hs, spread: Math.round((Math.max(...ws) - Math.min(...ws)) * 10) / 10, |
| 117 | barHeight: Math.round(b.getBoundingClientRect().height * 10) / 10 }; |
| 118 | }); |
| 119 | console.log(` bar ${bar.barHeight}px, buttons ${bar.widths.join('/')} wide (spread ${bar.spread}px), ${bar.heights.join('/')} tall`); |
| 120 | rows.push({ cell: cell.tag, what: 'mnav', ...bar }); |
| 121 | await page.screenshot({ path: path.join(OUT, `${cell.tag}--mnav.png`), |
| 122 | clip: { x: 0, y: cell.h - bar.barHeight - 4, width: cell.w, height: bar.barHeight + 4 } }).catch(() => {}); |
| 123 | |
| 124 | for (const t of tabs) { |
| 125 | await page.evaluate((id) => { try { DaimondSheet.open(id); } catch (e) {} }, t.id); |
| 126 | await pause(600); |
| 127 | for (const detent of ['half', 'full']) { |
| 128 | await page.evaluate((d) => { |
| 129 | const sh = document.getElementById('msheet'); |
| 130 | if (!sh || !sh.classList.contains('open')) return; |
| 131 | // The controller sets the height; drive it the way the drag does. |
| 132 | // The controller keeps `snapTo` private, so the detent height is |
| 133 | // computed the way it computes it (mobile.js: BAR 58, TOPBAR 50, |
| 134 | // PEEK 56) rather than guessed in viewport units. |
| 135 | const maxH = Math.max(56, window.innerHeight - 50 - 58); |
| 136 | sh.style.height = (d === 'full' ? maxH : Math.min(maxH, Math.round(window.innerHeight * 0.52))) + 'px'; |
| 137 | }, detent); |
| 138 | await pause(350); |
| 139 | const g = await page.evaluate(GRAB); |
| 140 | if (!g) { if (detent === 'half') console.log(` ${t.label}: no sheet`); continue; } |
| 141 | const file = path.join(OUT, `${cell.tag}--sheet-${t.label.replace(/\W+/g, '')}-${detent}.png`); |
| 142 | await page.screenshot({ path: file }).catch(() => {}); |
| 143 | const flags = []; |
| 144 | if (g.wrapped) flags.push(`GRABBER WRAPPED (title y${g.titleRect.y} h${g.titleRect.h}, close y${g.closeRect.y} h${g.closeRect.h}, row ${g.grabHeight}px)`); |
| 145 | if (g.headWrap && g.headWrap.wrapped) flags.push(`GUEST HEAD WRAPPED (${g.headWrap.cls}, ${g.headWrap.height}px)`); |
| 146 | if (g.barTop !== null && g.sheetBottom > g.barTop + 1) flags.push(`sheet foot ${g.sheetBottom} is over the bar at ${g.barTop}`); |
| 147 | console.log(` ${flags.length ? 'LOOK' : ' ok '} ${t.label} @${detent} "${g.title}" ${g.sheetHeight}px${flags.length ? ' — ' + flags.join('; ') : ''}`); |
| 148 | rows.push({ cell: cell.tag, tab: t.label, detent, ...g, flags }); |
| 149 | } |
| 150 | await page.evaluate(() => { const c = document.getElementById('msheet-close'); if (c) c.click(); }); |
| 151 | await pause(300); |
| 152 | } |
| 153 | } |
| 154 | fs.writeFileSync(path.join(OUT, 'sheets.json'), JSON.stringify(rows, null, '\t')); |
| 155 | console.log(`\n${OUT}`); |
| 156 | const errs = errors(s).filter((e) => !/502|Bad Gateway/.test(e)); |
| 157 | if (errs.length) console.log(`${errs.length} console error(s): ` + errs.slice(0, 5).join(' | ')); |
| 158 | } finally { |
| 159 | await s.close(); |
| 160 | } |