Fix responsive DSP settings controls
@@ -1,4 +1,27 @@
|
|||||||
.settings-browser { display: block; width: 100%; min-width: 0; }
|
.settings-browser { display: block; width: 100%; min-width: 0; }
|
||||||
|
.settings-browser, .settings-grid, .settings-overview, .settings-detail, .settings-section,
|
||||||
|
.settings-browser .settings, .settings-browser .set-group, .settings-browser .dsp-settings,
|
||||||
|
.settings-browser .dsp-subsection, .settings-browser .dsp-param-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/* Settings is a workspace, not a sidebar: give the section browser the full
|
||||||
|
content area on desktop instead of leaving it inside the fixed 452px list. */
|
||||||
|
.body:has(#listPane .cards[data-view="settings"]) .player-pane { display: none; }
|
||||||
|
.body:has(#listPane .cards[data-view="settings"]) #listPane {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
.body:has(#listPane .cards[data-view="settings"]) #listPane .cards {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
.settings-browser [hidden] { display: none !important; }
|
.settings-browser [hidden] { display: none !important; }
|
||||||
.settings-search { margin-bottom: 12px; }
|
.settings-search { margin-bottom: 12px; }
|
||||||
.settings-search input { width: 100%; min-height: 44px; background: var(--bg-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font: inherit; }
|
.settings-search input { width: 100%; min-height: 44px; background: var(--bg-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font: inherit; }
|
||||||
@@ -11,10 +34,81 @@
|
|||||||
.settings-section-row:hover, .settings-section-row[aria-current="true"] { background: var(--bg-3); border-radius: 8px; }
|
.settings-section-row:hover, .settings-section-row[aria-current="true"] { background: var(--bg-3); border-radius: 8px; }
|
||||||
.settings-browser :is(button,input,select,textarea):focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
.settings-browser :is(button,input,select,textarea):focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||||
.settings-back { min-height: 44px; padding: 8px 12px; margin-bottom: 12px; background: var(--bg-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; font: inherit; cursor: pointer; }
|
.settings-back { min-height: 44px; padding: 8px 12px; margin-bottom: 12px; background: var(--bg-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; font: inherit; cursor: pointer; }
|
||||||
.settings-browser .set-row { min-height: 44px; }
|
.settings-browser .set-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, min(40%, 260px));
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.settings-browser .set-row > :is(span, input, select, button, output) { min-width: 0; max-width: 100%; }
|
||||||
|
.settings-browser .set-row > :is(input, select, button, output) { box-sizing: border-box; }
|
||||||
|
.settings-browser .set-control-wrap { display: block; width: 100%; min-width: 0; max-width: 100%; overflow: hidden; }
|
||||||
|
#listPane #cards .settings-browser .set-row select,
|
||||||
|
#listPane #cards .settings-browser .set-control-wrap select,
|
||||||
|
.settings-browser .set-row select,
|
||||||
|
.settings-browser .set-control-wrap select {
|
||||||
|
box-sizing: border-box;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-right: 28px;
|
||||||
|
background-color: var(--bg-3);
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%), linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
|
||||||
|
background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.settings-browser .set-row > span:first-child { overflow-wrap: anywhere; }
|
||||||
|
.settings-browser .set-row:has(> span:only-child) { grid-template-columns: minmax(0, 1fr); }
|
||||||
.settings-browser .set-row small { font-size: 12px; max-width: 100%; }
|
.settings-browser .set-row small { font-size: 12px; max-width: 100%; }
|
||||||
.settings-browser .set-row > span { min-width: 0; }
|
.settings-browser .set-row > span { min-width: 0; }
|
||||||
.settings-browser .set-row input[type="range"] { max-width: 42%; }
|
.settings-browser .set-row input[type="range"] { width: calc(100% - 16px); max-width: calc(100% - 16px); min-width: 0; }
|
||||||
|
.settings-browser .set-row input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
justify-self: end;
|
||||||
|
width: 46px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 46px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: radial-gradient(circle at 14px 50%, var(--text) 0 9px, transparent 10px), var(--bg-3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .16s ease, border-color .16s ease;
|
||||||
|
}
|
||||||
|
.settings-browser .set-row input[type="checkbox"]:checked {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: radial-gradient(circle at 32px 50%, var(--on-accent, #fff) 0 9px, transparent 10px), var(--accent);
|
||||||
|
}
|
||||||
|
.settings-browser .set-row input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||||
|
.dsp-file-control {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.dsp-file-control input[type="file"] { position: absolute; width: 1px; height: 1px; min-width: 0; max-width: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||||
|
.dsp-file-control:focus-within { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 8px; }
|
||||||
|
.dsp-file-button { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 7px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg-3); color: var(--text); font: 600 12px var(--ui); white-space: nowrap; }
|
||||||
|
.dsp-file-name { display: block; min-width: 0; overflow: hidden; color: var(--text-2); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||||
|
.dsp-subsection .sp-note { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
||||||
|
.dsp-subsection > .btn { min-width: 0; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.dsp-actions { min-width: 0; max-width: 100%; }
|
||||||
|
.dsp-param-list { grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); }
|
||||||
|
.dsp-param-row input[type="range"] { width: calc(100% - 16px); min-width: 0; max-width: calc(100% - 16px); }
|
||||||
.settings-browser .settings-detail { display: none; min-width: 0; }
|
.settings-browser .settings-detail { display: none; min-width: 0; }
|
||||||
.settings-browser[data-detail="1"] .settings-overview { display: none; }
|
.settings-browser[data-detail="1"] .settings-overview { display: none; }
|
||||||
.settings-browser[data-detail="1"] .settings-detail { display: block; }
|
.settings-browser[data-detail="1"] .settings-detail { display: block; }
|
||||||
@@ -22,7 +116,7 @@
|
|||||||
.settings-grid { display: grid; grid-template-columns: minmax(160px, .8fr) minmax(0, 1.4fr); gap: 16px; }
|
.settings-grid { display: grid; grid-template-columns: minmax(160px, .8fr) minmax(0, 1.4fr); gap: 16px; }
|
||||||
.settings-browser .settings-overview, .settings-browser[data-detail="1"] .settings-overview, .settings-browser .settings-detail { display: block; }
|
.settings-browser .settings-overview, .settings-browser[data-detail="1"] .settings-overview, .settings-browser .settings-detail { display: block; }
|
||||||
.settings-back { display: none; }
|
.settings-back { display: none; }
|
||||||
.settings-browser .set-row { flex-wrap: wrap; gap: 12px; }
|
.settings-browser .set-row { flex-wrap: nowrap; gap: 12px; }
|
||||||
}
|
}
|
||||||
.download-job { padding: 14px 0; border-bottom: 1px solid var(--line); display: grid; gap: 8px; }
|
.download-job { padding: 14px 0; border-bottom: 1px solid var(--line); display: grid; gap: 8px; }
|
||||||
.download-job b { overflow-wrap: anywhere; }
|
.download-job b { overflow-wrap: anywhere; }
|
||||||
|
|||||||
@@ -741,9 +741,9 @@ async function renderSettings() {
|
|||||||
const dspRange = (key, label, min, max, step = 1, unit = '') => `<label class="set-row dsp-param-row"><span>${label}<small>${dspParam(key)}${unit}</small></span><input type="range" data-dsp-param="${key}" min="${min}" max="${max}" step="${step}" value="${dspParam(key)}" /></label>`;
|
const dspRange = (key, label, min, max, step = 1, unit = '') => `<label class="set-row dsp-param-row"><span>${label}<small>${dspParam(key)}${unit}</small></span><input type="range" data-dsp-param="${key}" min="${min}" max="${max}" step="${step}" value="${dspParam(key)}" /></label>`;
|
||||||
const dspBandRows = dspBands.map((hz, i) => dspRange(`eq.band${i}`, `${hz >= 1000 ? (hz / 1000) + ' kHz' : hz + ' Hz'}`, -12, 12, 0.5, ' dB')).join('');
|
const dspBandRows = dspBands.map((hz, i) => dspRange(`eq.band${i}`, `${hz >= 1000 ? (hz / 1000) + ' kHz' : hz + ' Hz'}`, -12, 12, 0.5, ' dB')).join('');
|
||||||
|
|
||||||
const sel = (id, val, opts) => `<select id="${id}" class="set-select">` +
|
const sel = (id, val, opts) => `<span class="set-control-wrap"><select id="${id}" class="set-select">` +
|
||||||
opts.map(([v, label]) => `<option value="${v}"${val === v ? ' selected' : ''}>${label}</option>`).join('') +
|
opts.map(([v, label]) => `<option value="${v}"${val === v ? ' selected' : ''}>${label}</option>`).join('') +
|
||||||
`</select>`;
|
`</select></span>`;
|
||||||
const dspBuiltinIRSelect = `<select id="setDspBuiltinIR" class="set-select">${['Small room','Hall','Plate','Cathedral'].map(name=>`<option value="${name}"${dspLowEnd&&name!=='Small room'?' disabled':''}>${name}</option>`).join('')}</select>`;
|
const dspBuiltinIRSelect = `<select id="setDspBuiltinIR" class="set-select">${['Small room','Hall','Plate','Cathedral'].map(name=>`<option value="${name}"${dspLowEnd&&name!=='Small room'?' disabled':''}>${name}</option>`).join('')}</select>`;
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
@@ -837,10 +837,10 @@ async function renderSettings() {
|
|||||||
<label class="set-row"><span>Enable convolution</span><input id="setDspConvolver" type="checkbox" ${dspParam('convolver.enabled') ? 'checked' : ''} /></label>
|
<label class="set-row"><span>Enable convolution</span><input id="setDspConvolver" type="checkbox" ${dspParam('convolver.enabled') ? 'checked' : ''} /></label>
|
||||||
<label class="set-row"><span>Dry / wet<small>${Math.round(dspParam('convolver.mix')*100)}%</small></span><input type="range" data-dsp-param="convolver.mix" min="0" max="1" step="0.01" value="${dspParam('convolver.mix')}" /></label>
|
<label class="set-row"><span>Dry / wet<small>${Math.round(dspParam('convolver.mix')*100)}%</small></span><input type="range" data-dsp-param="convolver.mix" min="0" max="1" step="0.01" value="${dspParam('convolver.mix')}" /></label>
|
||||||
<label class="set-row"><span>Pre-delay<small>${dspParam('convolver.predelayMs')} ms</small></span><input type="range" data-dsp-param="convolver.predelayMs" min="0" max="250" step="1" value="${dspParam('convolver.predelayMs')}" /></label>
|
<label class="set-row"><span>Pre-delay<small>${dspParam('convolver.predelayMs')} ms</small></span><input type="range" data-dsp-param="convolver.predelayMs" min="0" max="250" step="1" value="${dspParam('convolver.predelayMs')}" /></label>
|
||||||
<label class="set-row"><span>Built-in response</span>${dspBuiltinIRSelect}</label>
|
<label class="set-row"><span>Built-in response</span><span class="set-control-wrap">${dspBuiltinIRSelect}</span></label>
|
||||||
<button id="setDspLoadBuiltin" class="btn" type="button">Generate and load</button>
|
<button id="setDspLoadBuiltin" class="btn" type="button">Generate and load</button>
|
||||||
<label class="set-row"><span>Upload impulse response<small>WAV or other browser-decodable audio; up to 65,536 frames at the internal sample rate.</small></span><input id="dspIrFile" type="file" accept="audio/*,.wav" /></label>
|
<label class="set-row dsp-upload-row"><span>Upload impulse response<small>WAV or other browser-decodable audio; up to 65,536 frames at the internal sample rate.</small></span><span class="dsp-file-control"><input id="dspIrFile" type="file" accept="audio/*,.wav" aria-label="Upload impulse response" /><button id="dspIrChooseFile" class="dsp-file-button" type="button">Choose file</button><span class="dsp-file-name" id="dspIrFileName" aria-live="polite">No file chosen</span></span></label>
|
||||||
<label class="set-row"><span>Stored responses</span><select id="setDspSavedIR" class="set-select"><option value="">Choose a saved response</option></select></label>
|
<label class="set-row"><span>Stored responses</span><span class="set-control-wrap"><select id="setDspSavedIR" class="set-select"><option value="">Choose a saved response</option></select></span></label>
|
||||||
<button id="setDspLoadSavedIR" class="btn" type="button">Load saved response</button>
|
<button id="setDspLoadSavedIR" class="btn" type="button">Load saved response</button>
|
||||||
<p id="dspIrStatus" class="sp-note">${jamesDsp.irRef?.name ? `Loaded: ${jamesDsp.irRef.name}` : 'No impulse response loaded.'}</p>
|
<p id="dspIrStatus" class="sp-note">${jamesDsp.irRef?.name ? `Loaded: ${jamesDsp.irRef.name}` : 'No impulse response loaded.'}</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -1196,8 +1196,10 @@ async function renderSettings() {
|
|||||||
try{const ref=await EQ.loadDspSavedIR(name);if($('dspIrStatus'))$('dspIrStatus').textContent=`Loaded saved response: ${name}`;$('setDspConvolver').checked=true;await refreshDspIrList();}
|
try{const ref=await EQ.loadDspSavedIR(name);if($('dspIrStatus'))$('dspIrStatus').textContent=`Loaded saved response: ${name}`;$('setDspConvolver').checked=true;await refreshDspIrList();}
|
||||||
catch(error){toast(`Saved response failed: ${error.message}`);}
|
catch(error){toast(`Saved response failed: ${error.message}`);}
|
||||||
});
|
});
|
||||||
|
if ($('dspIrChooseFile')) $('dspIrChooseFile').addEventListener('click', e => { e.preventDefault(); $('dspIrFile')?.click(); });
|
||||||
if ($('dspIrFile')) $('dspIrFile').addEventListener('change',async e=>{
|
if ($('dspIrFile')) $('dspIrFile').addEventListener('change',async e=>{
|
||||||
const file=e.target.files?.[0];if(!file)return;
|
const file=e.target.files?.[0];if(!file)return;
|
||||||
|
if($('dspIrFileName'))$('dspIrFileName').textContent=file.name;
|
||||||
try{const ref=await EQ.loadDspIR(file);if($('dspIrStatus'))$('dspIrStatus').textContent=`Loaded ${ref?.name||file.name}${ref?' and saved it for this device':' for this session only'}.`;$('setDspConvolver').checked=true;await refreshDspIrList();}
|
try{const ref=await EQ.loadDspIR(file);if($('dspIrStatus'))$('dspIrStatus').textContent=`Loaded ${ref?.name||file.name}${ref?' and saved it for this device':' for this session only'}.`;$('setDspConvolver').checked=true;await refreshDspIrList();}
|
||||||
catch(error){toast(`Impulse response failed: ${error.message}`);}
|
catch(error){toast(`Impulse response failed: ${error.message}`);}
|
||||||
finally{e.target.value='';}
|
finally{e.target.value='';}
|
||||||
|
|||||||
@@ -489,3 +489,10 @@ remote toggles against a mocked relay. Transport previews and remote screenshots
|
|||||||
are saved in ignored `plans/transport-shots/`. Run `node perf/ui-geometry.mjs`
|
are saved in ignored `plans/transport-shots/`. Run `node perf/ui-geometry.mjs`
|
||||||
for the 320–430px search-bar and Saved-row geometry matrix. OS lock-screen buttons
|
for the 320–430px search-bar and Saved-row geometry matrix. OS lock-screen buttons
|
||||||
remain platform-rendered through the existing MediaSession handlers.
|
remain platform-rendered through the existing MediaSession handlers.
|
||||||
|
|
||||||
|
Run `node perf/settings-layout.mjs` for the Settings workspace and JamesDSP
|
||||||
|
control matrix: Chromium and WebKit, all four layouts, dark/light/contrast, and
|
||||||
|
390/768/1280/1440/1920px widths. It checks horizontal overflow, responsive row
|
||||||
|
geometry, themed selects and switches, the clipped native file picker, and
|
||||||
|
button wrapping; desktop screenshots and measurements are written to
|
||||||
|
`plans/settings-shots/`.
|
||||||
|
|||||||
84
perf/settings-layout.mjs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Settings layout regression checks across responsive widths, layouts and themes.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { readFile, mkdir } from 'node:fs/promises';
|
||||||
|
import { resolve, extname } from 'node:path';
|
||||||
|
import { chromium, webkit } from 'playwright';
|
||||||
|
|
||||||
|
const root = resolve('frontend'), shots = resolve('plans/settings-shots');
|
||||||
|
await mkdir(shots, { recursive: true });
|
||||||
|
const mime = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript', '.css':'text/css', '.json':'application/json', '.woff2':'font/woff2', '.svg':'image/svg+xml', '.png':'image/png' };
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url, 'http://localhost');
|
||||||
|
const path = resolve(root, '.' + (url.pathname === '/' ? '/index.html' : url.pathname));
|
||||||
|
if (!path.startsWith(root + '/')) { res.writeHead(403).end(); return; }
|
||||||
|
try { const body = await readFile(path); res.writeHead(200, { 'Content-Type':mime[extname(path)] || 'application/octet-stream' }).end(body); }
|
||||||
|
catch { res.writeHead(404).end(); }
|
||||||
|
});
|
||||||
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||||
|
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||||
|
const layouts = ['classic','f7-swipe','glass-stage','bento-hub'], themes = ['dark','light','contrast'], widths = [390,768,1280,1440,1920];
|
||||||
|
const failures = [], results = [];
|
||||||
|
try {
|
||||||
|
for (const [engineName, engine] of Object.entries({ chromium, webkit })) {
|
||||||
|
const browser = await engine.launch();
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext({ viewport:{width:1280,height:1000}, serviceWorkers:'block' });
|
||||||
|
await context.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.abort());
|
||||||
|
await context.addInitScript(() => localStorage.setItem('_ytpdata', JSON.stringify({
|
||||||
|
playlists:[], history:[], queue:[], settings:{layout:'glass-stage',theme:'dark',quality:'auto',jamesDsp:{enabled:true,downloadForOffline:false,preset:'warm',params:{'convolver.enabled':1,'spatial.enabled':1},irRef:{name:'Small room'}}}
|
||||||
|
})));
|
||||||
|
const page = await context.newPage();
|
||||||
|
page.on('pageerror', error => failures.push(`${engineName}: pageerror ${error.message}`));
|
||||||
|
await page.goto(origin);
|
||||||
|
await page.waitForFunction(() => typeof data !== 'undefined' && typeof Lazy !== 'undefined');
|
||||||
|
await page.waitForFunction(() => performance.getEntriesByName('ytp:boot').length > 0);
|
||||||
|
for (const css of ['theme-classic.css','theme-f7.css','theme-glass.css','theme-glass-panel.css','theme-glass-controls.css','theme-bento-hub.css']) await page.addStyleTag({ path:resolve(root,css) });
|
||||||
|
for (const layout of layouts) {
|
||||||
|
for (const theme of themes) for (const width of widths) {
|
||||||
|
await page.setViewportSize({ width, height:1000 });
|
||||||
|
await page.evaluate(({theme,layout}) => { data.settings.theme=theme; data.settings.layout=layout; data.settings.jamesDsp.enabled=true; document.documentElement.dataset.theme=theme; document.documentElement.dataset.layout=layout; if(typeof EQ!=='undefined')EQ.listDspIRs=async()=>[]; view={type:'settings'}; render(); }, {theme,layout});
|
||||||
|
await page.waitForSelector('.settings-browser .settings-section-row[data-section="playback"]',{state:'attached'});
|
||||||
|
await page.evaluate(()=>{const host=document.querySelector('.settings-browser');host.dataset.detail='1';document.querySelectorAll('.settings-section').forEach(panel=>panel.hidden=panel.id!=='settings-section-playback');document.getElementById('jamesDspControls').hidden=false;});
|
||||||
|
await page.waitForSelector('#setDspSpatialMode',{state:'attached'});
|
||||||
|
const measured = await page.evaluate(() => {
|
||||||
|
const rect = e => { const r=e.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right}; };
|
||||||
|
const selectors=['.settings-browser','.settings-grid','.settings-detail','#listPane .cards','#jamesDspSettings','.dsp-subsection'];
|
||||||
|
const boxes=Object.fromEntries(selectors.map(s=>{const e=document.querySelector(s);return [s,e?{clientWidth:e.clientWidth,scrollWidth:e.scrollWidth,rect:rect(e)}:null];}));
|
||||||
|
const rows=[...document.querySelectorAll('#jamesDspSettings .set-row')].map(row=>{
|
||||||
|
const label=row.querySelector(':scope > span:first-child'),control=row.querySelector(':scope > input,:scope > select,:scope > .set-control-wrap,:scope > .dsp-file-control');
|
||||||
|
return {text:row.textContent.trim().slice(0,90),label:label?rect(label):null,control:control?{...rect(control),scrollWidth:control.scrollWidth,clientWidth:control.clientWidth,overflow:getComputedStyle(control).overflow,select:control.querySelector('select')?{...rect(control.querySelector('select')),scrollWidth:control.querySelector('select').scrollWidth,clientWidth:control.querySelector('select').clientWidth}:null}:null,row:rect(row)};
|
||||||
|
});
|
||||||
|
const file=document.querySelector('#dspIrFile'),checkbox=document.querySelector('#setDspConvolver'),buttons=[...document.querySelectorAll('.dsp-subsection > button')];
|
||||||
|
const selects=[...document.querySelectorAll('.settings-browser select')].map(s=>({id:s.id,appearance:getComputedStyle(s).appearance,backgroundImage:getComputedStyle(s).backgroundImage,backgroundColor:getComputedStyle(s).backgroundColor}));
|
||||||
|
const overflows=[...document.querySelectorAll('#jamesDspSettings *')].filter(e=>e.scrollWidth>e.clientWidth+1).map(e=>({tag:e.tagName,id:e.id,cls:e.className,text:e.textContent.trim().slice(0,100),scroll:e.scrollWidth,client:e.clientWidth,rect:rect(e)}));
|
||||||
|
return {boxes,rows,file:{clipPath:getComputedStyle(file).clipPath,rect:rect(file),parent:rect(file.parentElement)},checkbox:{appearance:getComputedStyle(checkbox).appearance,width:rect(checkbox).width},selects,buttons:buttons.map(b=>({whiteSpace:getComputedStyle(b).whiteSpace,scrollWidth:b.scrollWidth,clientWidth:b.clientWidth,rect:rect(b)})),overflows,page:{scrollWidth:document.documentElement.scrollWidth,clientWidth:document.documentElement.clientWidth}};
|
||||||
|
});
|
||||||
|
const key=`${engineName}/${layout}/${theme}/${width}`;
|
||||||
|
for (const [selector,box] of Object.entries(measured.boxes)) if(box) {
|
||||||
|
if(box.scrollWidth>box.clientWidth+1) failures.push(`${key}: ${selector} horizontal overflow ${box.scrollWidth}/${box.clientWidth}`);
|
||||||
|
}
|
||||||
|
if(measured.page.scrollWidth>measured.page.clientWidth+1) failures.push(`${key}: document horizontal overflow ${measured.page.scrollWidth}/${measured.page.clientWidth}`);
|
||||||
|
for(const row of measured.rows) if(row.label&&row.control&&row.control.x<row.label.x+Math.min(row.label.width,80)) failures.push(`${key}: control overlaps/collapses into label`);
|
||||||
|
if(measured.checkbox.appearance!=='none'||measured.checkbox.width<40) failures.push(`${key}: DSP checkbox is not a themed switch`);
|
||||||
|
for(const select of measured.selects) if(select.appearance!=='none'||select.backgroundImage==='none'||select.backgroundColor==='rgba(0, 0, 0, 0)') failures.push(`${key}: select #${select.id} is not themed`);
|
||||||
|
if(measured.file.rect.width>1||measured.file.clipPath==='none') failures.push(`${key}: native file picker is visible`);
|
||||||
|
for(const b of measured.buttons) if(b.whiteSpace!=='nowrap'||b.rect.height>50) failures.push(`${key}: DSP action button wraps vertically`);
|
||||||
|
results.push({engine:engineName,layout,theme,width,...measured});
|
||||||
|
if(width===1280) await page.screenshot({path:resolve(shots,`${engineName}-${layout}-${theme}.png`),fullPage:true});
|
||||||
|
if(width===1280&&layout==='glass-stage'&&theme==='dark') {
|
||||||
|
await page.locator('#dspConvolverTitle').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({path:resolve(shots,`${engineName}-glass-stage-dark-dsp.png`),fullPage:true});
|
||||||
|
await page.locator('#dspSpatialTitle').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({path:resolve(shots,`${engineName}-glass-stage-dark-spatial.png`),fullPage:true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await context.close();
|
||||||
|
} finally { await browser.close(); }
|
||||||
|
}
|
||||||
|
} finally { await new Promise(r=>server.close(r)); }
|
||||||
|
await (await import('node:fs/promises')).writeFile(resolve(shots,'measurements.json'),JSON.stringify({cases:results.length,failures,screenshots:results.filter(r=>r.width===1280).map(r=>`${r.engine}-${r.layout}-${r.theme}.png`)},null,2));
|
||||||
|
console.log(`Settings layout: ${results.length} cases, ${failures.length} failures; screenshots in plans/settings-shots/`);
|
||||||
|
if(failures.length) { console.error(failures.slice(0,30).join('\n')); process.exitCode=1; }
|
||||||
BIN
plans/settings-shots/chromium-bento-hub-contrast.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
plans/settings-shots/chromium-bento-hub-dark.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
plans/settings-shots/chromium-bento-hub-light.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
plans/settings-shots/chromium-classic-contrast.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
plans/settings-shots/chromium-classic-dark.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
plans/settings-shots/chromium-classic-light.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
plans/settings-shots/chromium-f7-swipe-contrast.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
plans/settings-shots/chromium-f7-swipe-dark.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
plans/settings-shots/chromium-f7-swipe-light.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
plans/settings-shots/chromium-glass-stage-contrast.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
plans/settings-shots/chromium-glass-stage-dark-dsp.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
plans/settings-shots/chromium-glass-stage-dark-spatial.png
Normal file
|
After Width: | Height: | Size: 419 KiB |
BIN
plans/settings-shots/chromium-glass-stage-dark.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
plans/settings-shots/chromium-glass-stage-light.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
30
plans/settings-shots/measurements.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"cases": 120,
|
||||||
|
"failures": [],
|
||||||
|
"screenshots": [
|
||||||
|
"chromium-classic-dark.png",
|
||||||
|
"chromium-classic-light.png",
|
||||||
|
"chromium-classic-contrast.png",
|
||||||
|
"chromium-f7-swipe-dark.png",
|
||||||
|
"chromium-f7-swipe-light.png",
|
||||||
|
"chromium-f7-swipe-contrast.png",
|
||||||
|
"chromium-glass-stage-dark.png",
|
||||||
|
"chromium-glass-stage-light.png",
|
||||||
|
"chromium-glass-stage-contrast.png",
|
||||||
|
"chromium-bento-hub-dark.png",
|
||||||
|
"chromium-bento-hub-light.png",
|
||||||
|
"chromium-bento-hub-contrast.png",
|
||||||
|
"webkit-classic-dark.png",
|
||||||
|
"webkit-classic-light.png",
|
||||||
|
"webkit-classic-contrast.png",
|
||||||
|
"webkit-f7-swipe-dark.png",
|
||||||
|
"webkit-f7-swipe-light.png",
|
||||||
|
"webkit-f7-swipe-contrast.png",
|
||||||
|
"webkit-glass-stage-dark.png",
|
||||||
|
"webkit-glass-stage-light.png",
|
||||||
|
"webkit-glass-stage-contrast.png",
|
||||||
|
"webkit-bento-hub-dark.png",
|
||||||
|
"webkit-bento-hub-light.png",
|
||||||
|
"webkit-bento-hub-contrast.png"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
plans/settings-shots/webkit-bento-hub-contrast.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
plans/settings-shots/webkit-bento-hub-dark.png
Normal file
|
After Width: | Height: | Size: 363 KiB |
BIN
plans/settings-shots/webkit-bento-hub-light.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
plans/settings-shots/webkit-classic-contrast.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
plans/settings-shots/webkit-classic-dark.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
plans/settings-shots/webkit-classic-light.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
plans/settings-shots/webkit-f7-swipe-contrast.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
plans/settings-shots/webkit-f7-swipe-dark.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
plans/settings-shots/webkit-f7-swipe-light.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
plans/settings-shots/webkit-glass-stage-contrast.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
plans/settings-shots/webkit-glass-stage-dark-dsp.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
plans/settings-shots/webkit-glass-stage-dark-spatial.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
plans/settings-shots/webkit-glass-stage-dark.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
plans/settings-shots/webkit-glass-stage-light.png
Normal file
|
After Width: | Height: | Size: 358 KiB |