Oregami
Repositories/oxedyne/daimond

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
14import fs from 'node:fs';
15import path from 'node:path';
16import { open, errors, scratch } from './harness.mjs';
17
18const argv = process.argv.slice(2);
19const value = (n, d) => { const i = argv.indexOf(n); return (i >= 0 && argv[i + 1]) ? argv[i + 1] : d; };
20const OUT = value('--out', scratch('qc-sheets'));
21fs.mkdirSync(OUT, { recursive: true });
22
23const 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
29const s = await open({ name: 'qcsheets', connect: true });
30const { page } = s;
31const pause = (ms) => page.waitForTimeout(ms);
32const 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.
37const 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
82try {
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}