Polish Bento scrolling and controls with full screen coverage
This commit is contained in:
80
docs/theme-bento-hub.md
Normal file
80
docs/theme-bento-hub.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# New Bento Hub theme
|
||||||
|
|
||||||
|
The old Bento rules in `frontend/styles.css` have been removed. The replacement
|
||||||
|
is `frontend/theme-bento-hub.css`, scoped to `html[data-layout="bento-hub"]`.
|
||||||
|
The stylesheet, small theme helper, and self-hosted Plus Jakarta Sans fonts are
|
||||||
|
included in the service-worker shell cache. Classic remains the default.
|
||||||
|
|
||||||
|
## Match to the reference
|
||||||
|
|
||||||
|
The reference is `docs/mockups/bento-hub/index.html` and `SPEC.md` (local design
|
||||||
|
sources). The implementation uses their obsidian surfaces, solar-gold primary
|
||||||
|
controls, cyan listening/status cues, Plus Jakarta Sans and JetBrains Mono,
|
||||||
|
16px outer / 10px inner corners, stroke SVG icons, search capsule, playlist
|
||||||
|
artwork collages, bordered collection rows, and floating mini player.
|
||||||
|
|
||||||
|
Coverage includes the desktop sidebar and phone navigation, Home and search,
|
||||||
|
Now Playing, all three notes tabs, stage lyrics, up next, playlist and channel
|
||||||
|
views, Queue and batch selection, History, Saved, all download states and their
|
||||||
|
Retry/Cancel controls, Settings overview and sections, Service mode, modals,
|
||||||
|
control sheets, and empty states. Floating chrome has measured clearance;
|
||||||
|
content scrolls inside the remaining viewport. `#sectionRail` stays fixed and
|
||||||
|
attached directly to `body`. It is never moved into a panel or scroll container.
|
||||||
|
|
||||||
|
## Deliberate differences
|
||||||
|
|
||||||
|
- Content and navigation remain the real app's. Home shows actual playlists
|
||||||
|
and recommendations. Counts, artwork, progress, lyrics and wake-lock state
|
||||||
|
come from existing data; there are no invented verification badges, transfer
|
||||||
|
speeds, subscriber counts, or guarantees that the screen will stay awake.
|
||||||
|
- The desktop sidebar uses the spec's 260px width rather than the HTML mockup's
|
||||||
|
240px. The drawer extends through 1023px. The existing app stacks its player
|
||||||
|
at phone widths; its browsing and player controls remain accessible there.
|
||||||
|
- Now Playing keeps the real control deck, playlist panel, volume, looping,
|
||||||
|
fullscreen, notes and More controls. Notes remain with the player instead of
|
||||||
|
reproducing the mockup's separate static lyrics column. Service keeps its
|
||||||
|
view, autoscroll, floating lyrics, reporting and text-size controls; these
|
||||||
|
wrap when necessary rather than overlapping.
|
||||||
|
- Settings keeps its section registry, back/history behavior, live summaries,
|
||||||
|
search and desktop two-pane navigation. Layout selection remains the existing
|
||||||
|
select rather than duplicating it with mockup-only theme chips.
|
||||||
|
- Functional text has an 11px floor and controls have 44px minimum targets.
|
||||||
|
Muted dark text and the light-theme gold were adjusted for contrast. Existing
|
||||||
|
font scaling, density, contrast, reduced-motion and performance preferences
|
||||||
|
continue to apply. Service lyrics keep their adjustable sizing.
|
||||||
|
- Mockup frames omit the shell on several screens and contain empty space or
|
||||||
|
sample rows. Real pages retain the full shell and scrollable content. Video
|
||||||
|
fixture art is synthetic; a real playing video fills the video stage.
|
||||||
|
|
||||||
|
## Verification and screenshots
|
||||||
|
|
||||||
|
Run `node scripts/test-bento-theme.js` for a reproducible comparison. It extracts
|
||||||
|
pre-Bento commit `04b620e` into a temporary directory, captures Classic and Glass
|
||||||
|
before shots, then checks the current app. It does not modify the checkout.
|
||||||
|
`BENTO_BASE_COMMIT` can select another baseline; `BENTO_SCREENSHOT_DIR` can select
|
||||||
|
an output directory. Default output is `~/deliverables/ytplayer-done13/`.
|
||||||
|
|
||||||
|
The Playwright matrix covers 23 screens at 390px and 1440px in Chromium and
|
||||||
|
WebKit. It also checks centered navigation icons, download action appearance,
|
||||||
|
fixed rail ownership, sidebar and mini-player navigation, floating clearance,
|
||||||
|
theme switching restoration, and light/high-contrast appearance. The reference
|
||||||
|
is opened at both widths and each reference frame is captured when the local
|
||||||
|
mockup is present. No local mockup files are added to these implementation
|
||||||
|
commits. Static fixtures freeze the Classic mini-player's initial observer and
|
||||||
|
native indeterminate progress to avoid unrelated animation/scroll races.
|
||||||
|
Before/after comparisons allow at most 40 subpixel rasterization differences
|
||||||
|
in a whole screenshot; substantial geometry or styling differences fail. The
|
||||||
|
additional theme-switch check tolerates one RGB level per channel to account
|
||||||
|
for Chromium recompositing rounded corners, while retaining the 40-pixel limit.
|
||||||
|
|
||||||
|
Frontend unit tests include floating-chrome clearance with and without safe
|
||||||
|
areas/hidden bars and Bento-only SVG mapping without changing Glass glyphs.
|
||||||
|
The usual frontend tests, app syntax check and server build are run after each
|
||||||
|
screen group. Server source and imports are unchanged.
|
||||||
|
|
||||||
|
On a real iPhone, review safe-area clearance in Safari and installed mode,
|
||||||
|
keyboard and orientation changes, the floating rail, mini-player return,
|
||||||
|
Service controls, and actual video playback. On desktop, review long titles,
|
||||||
|
sidebar drawer at tablet widths, keyboard focus, Settings navigation and theme
|
||||||
|
switching. WebKit automation checks layout; it does not emulate all iOS media
|
||||||
|
or safe-area behavior. Nothing was pushed or deployed.
|
||||||
@@ -10887,6 +10887,9 @@ function isStackedPlayerLayout() {
|
|||||||
if (document.documentElement.dataset.layout === 'classic') {
|
if (document.documentElement.dataset.layout === 'classic') {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue('--classic-stacked').trim() === '1';
|
return getComputedStyle(document.documentElement).getPropertyValue('--classic-stacked').trim() === '1';
|
||||||
}
|
}
|
||||||
|
if (document.documentElement.dataset.layout === 'bento-hub') {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue('--bento-stacked').trim() === '1';
|
||||||
|
}
|
||||||
return isPortraitPWA();
|
return isPortraitPWA();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,13 @@
|
|||||||
const resize=new root.ResizeObserver(schedule),observer=new root.MutationObserver(schedule);
|
const resize=new root.ResizeObserver(schedule),observer=new root.MutationObserver(schedule);
|
||||||
function paint(){
|
function paint(){
|
||||||
frame=0;const on=html.dataset.layout==='bento-hub';
|
frame=0;const on=html.dataset.layout==='bento-hub';
|
||||||
if(!on){if(active){resize.disconnect();observer.disconnect();html.style.removeProperty('--bento-bottom-clearance');doc.querySelectorAll('.bento-collage').forEach(n=>n.remove());}active=false;return;}
|
if(!on){if(active){resize.disconnect();observer.disconnect();html.style.removeProperty('--bento-bottom-clearance');doc.querySelectorAll('.bento-collage,.bento-search-icon').forEach(n=>n.remove());doc.querySelectorAll('.bento-search-label').forEach(n=>n.replaceWith(...n.childNodes));}active=false;return;}
|
||||||
if(!active){active=true;observer.observe(doc.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class','hidden']});for(const id of ['bottomNav','miniBar','sectionRail']){const el=doc.getElementById(id);if(el)resize.observe(el);}}
|
if(!active){active=true;observer.observe(doc.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class','hidden']});for(const id of ['bottomNav','miniBar','sectionRail']){const el=doc.getElementById(id);if(el)resize.observe(el);}}
|
||||||
|
const search=doc.querySelector('.search-btn');
|
||||||
|
if(search&&!search.querySelector('.bento-search-icon')){
|
||||||
|
const label=doc.createElement('span');label.className='bento-search-label';label.append(...search.childNodes);
|
||||||
|
const icon=doc.createElementNS('http://www.w3.org/2000/svg','svg');icon.classList.add('bento-search-icon');icon.setAttribute('aria-hidden','true');icon.setAttribute('viewBox','0 0 24 24');icon.innerHTML='<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>';search.append(label,icon);
|
||||||
|
}
|
||||||
const boxes=['bottomNav','miniBar','sectionRail'].map(id=>doc.getElementById(id)).filter(Boolean).map(el=>{const r=el.getBoundingClientRect(),s=root.getComputedStyle(el);return {top:r.top,height:r.height,fixed:s.position==='fixed',visible:s.display!=='none'&&s.visibility!=='hidden'};});
|
const boxes=['bottomNav','miniBar','sectionRail'].map(id=>doc.getElementById(id)).filter(Boolean).map(el=>{const r=el.getBoundingClientRect(),s=root.getComputedStyle(el);return {top:r.top,height:r.height,fixed:s.position==='fixed',visible:s.display!=='none'&&s.visibility!=='hidden'};});
|
||||||
const clearance=chromeClearance(root.innerHeight,boxes)+'px';if(html.style.getPropertyValue('--bento-bottom-clearance')!==clearance)html.style.setProperty('--bento-bottom-clearance',clearance);
|
const clearance=chromeClearance(root.innerHeight,boxes)+'px';if(html.style.getPropertyValue('--bento-bottom-clearance')!==clearance)html.style.setProperty('--bento-bottom-clearance',clearance);
|
||||||
for(const card of doc.querySelectorAll('.home-pl-card')){
|
for(const card of doc.querySelectorAll('.home-pl-card')){
|
||||||
|
|||||||
@@ -20,15 +20,17 @@
|
|||||||
clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||||
shuffle:'<path d="m18 3 3 3-3 3M3 6h3c5 0 7 12 12 12h3m-3-3 3 3-3 3M3 18h3c2 0 4-3 6-6s4-6 6-6h3"/>',
|
shuffle:'<path d="m18 3 3 3-3 3M3 6h3c5 0 7 12 12 12h3m-3-3 3 3-3 3M3 18h3c2 0 4-3 6-6s4-6 6-6h3"/>',
|
||||||
down:'<path d="m6 9 6 6 6-6"/>', right:'<path d="m9 6 6 6-6 6"/>', back:'<path d="M20 12H4m6-6-6 6 6 6"/>', more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
|
down:'<path d="m6 9 6 6 6-6"/>', right:'<path d="m9 6 6 6-6 6"/>', back:'<path d="M20 12H4m6-6-6 6 6 6"/>', more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
|
||||||
|
television:'<rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8m-4-4v4"/>', pin:'<path d="M9 3h6l-1 6 4 4H6l4-4zM12 13v8"/>',
|
||||||
devices:'<rect x="3" y="4" width="14" height="12" rx="2"/><path d="M7 20h6M10 16v4"/><rect x="17" y="10" width="5" height="11" rx="1"/>'
|
devices:'<rect x="3" y="4" width="14" height="12" rx="2"/><path d="M7 20h6M10 16v4"/><rect x="17" y="10" width="5" height="11" rx="1"/>'
|
||||||
};
|
};
|
||||||
const glyphs = {'+':'add','✕':'close','×':'close','✖':'close','🔍':'search','🔎':'search','⬇':'save','⤓':'save','💾':'save','▶':'play','⏸':'pause','⏭':'next','⏮':'previous','↻':'repeat','↺':'repeat','✎':'edit','✏':'edit','📝':'edit','🎤':'lyrics','↗':'share','⧉':'copy','✓':'check','📁':'folder','⚙':'settings','📋':'chapters','🕘':'clock','🔀':'shuffle','📡':'devices','▾':'down','▸':'right','←':'back','⋯':'more','⋮':'more','🗑':'trash','🎵':'music'};
|
const glyphs = {'+':'add','✕':'close','×':'close','✖':'close','🔍':'search','🔎':'search','⬇':'save','⤓':'save','💾':'save','▶':'play','⏸':'pause','⏭':'next','⏮':'previous','↻':'repeat','↺':'repeat','✎':'edit','✏':'edit','📝':'edit','🎤':'lyrics','↗':'share','⧉':'copy','✓':'check','📁':'folder','⚙':'settings','📋':'chapters','🕘':'clock','🔀':'shuffle','📡':'devices','▾':'down','▸':'right','←':'back','⋯':'more','⋮':'more','🗑':'trash','🎵':'music'};
|
||||||
function leadingGlyph(text) {
|
function leadingGlyph(text,bento=false) {
|
||||||
|
const mapping=bento?{...glyphs,'📺':'television','⏱':'clock','⇅':'settings','📌':'pin','🎧':'music','📚':'folder'}:glyphs;
|
||||||
const prefix = text.match(/^\s*/)[0], rest = text.slice(prefix.length);
|
const prefix = text.match(/^\s*/)[0], rest = text.slice(prefix.length);
|
||||||
const glyph = Object.keys(glyphs).find(key => rest.startsWith(key));
|
const glyph = Object.keys(mapping).find(key => rest.startsWith(key));
|
||||||
if (!glyph) return null;
|
if (!glyph) return null;
|
||||||
const length = glyph.length + (rest[glyph.length] === '\uFE0F' ? 1 : 0);
|
const length = glyph.length + (rest[glyph.length] === '\uFE0F' ? 1 : 0);
|
||||||
return { kind:glyphs[glyph], original:prefix+rest.slice(0,length), rest:rest.slice(length) };
|
return { kind:mapping[glyph], original:prefix+rest.slice(0,length), rest:rest.slice(length) };
|
||||||
}
|
}
|
||||||
function mount(doc) {
|
function mount(doc) {
|
||||||
const decorations = new Set(), tabLabels = new Map(); let scheduled=false;
|
const decorations = new Set(), tabLabels = new Map(); let scheduled=false;
|
||||||
@@ -49,11 +51,11 @@
|
|||||||
const active=bento || doc.documentElement.dataset.layout==='glass-stage';
|
const active=bento || doc.documentElement.dataset.layout==='glass-stage';
|
||||||
for(const [parent,record] of tabLabels)if(!active || !parent.isConnected){restoreTab(parent,record);tabLabels.delete(parent);}
|
for(const [parent,record] of tabLabels)if(!active || !parent.isConnected){restoreTab(parent,record);tabLabels.delete(parent);}
|
||||||
for(const record of decorations)if(!active || !record.svg.isConnected){restore(record);decorations.delete(record);}
|
for(const record of decorations)if(!active || !record.svg.isConnected){restore(record);decorations.delete(record);}
|
||||||
if(active) for(const parent of doc.querySelectorAll(selector+(bento?', .service-btn, .np-btn, .ctrl, .empty-icon, .dl-saved-cta, .brand-mark, .upnext-head button':''))) {
|
if(active) for(const parent of doc.querySelectorAll(selector+(bento?', .service-btn, .np-btn, .ctrl, .empty-icon, .dl-saved-cta, .brand-mark, .upnext-head button, .ph-logo, .saved-badge':''))) {
|
||||||
// Existing SVGs already express the action; only legacy glyphs change.
|
// Existing SVGs already express the action; only legacy glyphs change.
|
||||||
if(parent.querySelector('svg:not(.glass-control-icon),.legacy-nav-icon'))continue;
|
if(parent.querySelector('svg:not(.glass-control-icon),.legacy-nav-icon'))continue;
|
||||||
for(const node of [...parent.childNodes]) {
|
for(const node of [...parent.childNodes]) {
|
||||||
if(node.nodeType!==3)continue;const match=leadingGlyph(node.textContent);if(!match)continue;
|
if(node.nodeType!==3)continue;const match=leadingGlyph(node.textContent,bento);if(!match)continue;
|
||||||
const icon=svg(match.kind);
|
const icon=svg(match.kind);
|
||||||
if(parent.matches('.pl-offline-badge,.home-pl-offline')) { icon.removeAttribute('aria-hidden'); icon.setAttribute('role','img'); icon.setAttribute('aria-label',parent.title || 'Available offline'); }
|
if(parent.matches('.pl-offline-badge,.home-pl-offline')) { icon.removeAttribute('aria-hidden'); icon.setAttribute('role','img'); icon.setAttribute('aria-label',parent.title || 'Available offline'); }
|
||||||
const original=doc.createElement('span'); original.className='glass-original-glyph';original.textContent=match.original;
|
const original=doc.createElement('span'); original.className='glass-original-glyph';original.textContent=match.original;
|
||||||
|
|||||||
@@ -13,3 +13,10 @@ test('playlist trash, menu and music glyphs use drawn glass icons',()=>{
|
|||||||
assert.equal(leadingGlyph('⋮').kind,'more');
|
assert.equal(leadingGlyph('⋮').kind,'more');
|
||||||
assert.equal(leadingGlyph('🎵').kind,'music');
|
assert.equal(leadingGlyph('🎵').kind,'music');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Bento-only legacy icons leave the Glass glyph set unchanged',()=>{
|
||||||
|
assert.equal(leadingGlyph('📺 Show video'),null);
|
||||||
|
assert.deepEqual(leadingGlyph('📺 Show video',true),{kind:'television',original:'📺',rest:' Show video'});
|
||||||
|
assert.equal(leadingGlyph('⏱ Tap-sync',true).kind,'clock');
|
||||||
|
assert.equal(leadingGlyph('📌 Bookmark',true).kind,'pin');
|
||||||
|
});
|
||||||
|
|||||||
@@ -305,3 +305,59 @@ html[data-layout="bento-hub"] .body:has(.cards:is([data-view="search"],[data-vie
|
|||||||
html[data-layout="bento-hub"] .settings-grid { grid-template-columns:minmax(240px,.65fr) minmax(0,1.35fr); gap:24px; }
|
html[data-layout="bento-hub"] .settings-grid { grid-template-columns:minmax(240px,.65fr) minmax(0,1.35fr); gap:24px; }
|
||||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) { grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); }
|
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) { grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Batched reference polish: one mobile scroller, generous controls, no clipping. */
|
||||||
|
html[data-layout="bento-hub"] { --bento-stacked:0; --ease-bento:cubic-bezier(.16,1,.3,1); }
|
||||||
|
html[data-layout="bento-hub"] button { min-width:44px; }
|
||||||
|
html[data-layout="bento-hub"] .switch { min-height:44px; }
|
||||||
|
html[data-layout="bento-hub"] .hero-title { background:none; -webkit-text-fill-color:var(--text); }
|
||||||
|
html[data-layout="bento-hub"] .ph-logo { width:56px; height:56px; border-radius:16px; color:var(--on-accent); animation:none; }
|
||||||
|
html[data-layout="bento-hub"] .player-pane.empty :is(#controls,#nowPlayingMeta,#notesPanel,.upnext) { display:none; }
|
||||||
|
html[data-layout="bento-hub"] .body:has(.cards[data-view="settings"]) #listFilterBar { display:none; }
|
||||||
|
html[data-layout="bento-hub"] #controls .deck-main .ctrl { border-radius:999px; }
|
||||||
|
html[data-layout="bento-hub"] #controls input[type="range"]::-webkit-slider-thumb { border:0; box-shadow:var(--shadow-sm); }
|
||||||
|
html[data-layout="bento-hub"] #nowPlayingMeta .np-actions :is(#addBtn,#saveBtn,#exportBtn,#serviceBtn) { border:1px solid var(--line); border-radius:10px; background:var(--bg-2); color:var(--text); box-shadow:none; min-width:44px; }
|
||||||
|
html[data-layout="bento-hub"] #nowPlayingMeta .np-actions #saveBtn.done { color:var(--accent-sec); }
|
||||||
|
html[data-layout="bento-hub"] .stage-lyrics { background:#05070a; }
|
||||||
|
html[data-layout="bento-hub"] :is(.stage-lyrics,.service-lyrics) .sl-line.active { color:#00f5d4; background:rgba(0,245,212,.08); text-shadow:none; }
|
||||||
|
html[data-layout="bento-hub"] .svc-dock { flex-wrap:wrap; gap:8px; padding:8px; }
|
||||||
|
html[data-layout="bento-hub"] .svc-dock > :is(.svc-tile,.np-btn) { flex:1 0 96px; min-width:96px; min-height:56px; }
|
||||||
|
html[data-layout="bento-hub"] .svc-dock > .np-btn { background:#19202d; border-color:rgba(255,255,255,.09); color:#f1f5f9; }
|
||||||
|
html[data-layout="bento-hub"] .svc-font { flex:1 0 112px; justify-content:center; min-height:56px; }
|
||||||
|
html[data-layout="bento-hub"] .svc-label { white-space:nowrap; }
|
||||||
|
html[data-layout="bento-hub"] .shortcut-overlay { background:rgba(0,0,0,.75); backdrop-filter:blur(8px); }
|
||||||
|
html[data-layout="bento-hub"] .shortcut-panel { border:1px solid var(--line); border-radius:16px; background:var(--bg-1); }
|
||||||
|
html[data-layout="bento-hub"] .shortcut-key { border-radius:6px; background:var(--bg-2); color:var(--text); font:500 12px var(--mono); }
|
||||||
|
html[data-layout="bento-hub"] :is(.modal-list button,.mc-row,.export-actions .btn) { min-height:44px; border-radius:10px; background:var(--bg-2); color:var(--text); border-color:var(--line); }
|
||||||
|
html[data-layout="bento-hub"] .bento-search-label { position:absolute; width:1px; height:1px; padding:0; margin:-1px; clip-path:inset(50%); overflow:hidden; white-space:nowrap; }
|
||||||
|
html[data-layout="bento-hub"] .bento-search-icon { display:none; width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
|
||||||
|
html[data-layout="bento-hub"] .saved-badge .glass-control-icon { width:14px; height:14px; }
|
||||||
|
html[data-layout="bento-hub"] .card.downloading .saved-badge::before { content:''; }
|
||||||
|
@media(max-width:1023px) {
|
||||||
|
html[data-layout="bento-hub"] .app { grid-template-columns:minmax(0,1fr); }
|
||||||
|
html[data-layout="bento-hub"] .sidebar { position:fixed; inset:0 auto 0 0; width:260px; max-width:calc(100vw - 48px); z-index:350; transform:translateX(-100%); transition:transform .25s var(--ease-bento); }
|
||||||
|
html[data-layout="bento-hub"] .app.sidebar-open .sidebar { transform:translateX(0); }
|
||||||
|
html[data-layout="bento-hub"] .app.sidebar-open .sidebar-backdrop { display:block; opacity:1; z-index:340; }
|
||||||
|
html[data-layout="bento-hub"] .sidebar-toggle { display:inline-flex; }
|
||||||
|
html[data-layout="bento-hub"] .mini-bar { left:12px; }
|
||||||
|
}
|
||||||
|
@media(max-width:860px) {
|
||||||
|
html[data-layout="bento-hub"] { --bento-stacked:1; }
|
||||||
|
html[data-layout="bento-hub"] .body { flex-direction:column; overflow-y:auto; overflow-x:hidden; min-height:0; }
|
||||||
|
html[data-layout="bento-hub"] .player-pane { flex:none; width:100%; overflow:visible; }
|
||||||
|
html[data-layout="bento-hub"] .list-pane { flex:none!important; min-height:100%; overflow:visible; padding-bottom:16px; }
|
||||||
|
html[data-layout="bento-hub"] .cards { flex:none; overflow:visible; }
|
||||||
|
html[data-layout="bento-hub"] .player-pane.empty.home-playlists .player-stage { max-height:none; }
|
||||||
|
html[data-layout="bento-hub"] .search-btn { display:grid; place-items:center; flex:0 0 44px; width:44px; min-width:44px; padding:0; border-radius:999px; }
|
||||||
|
html[data-layout="bento-hub"] .bento-search-icon { display:block; }
|
||||||
|
html[data-layout="bento-hub"] .search-form { min-width:0; flex:1; gap:8px; }
|
||||||
|
html[data-layout="bento-hub"] #searchInput { min-width:0; padding-inline:12px; }
|
||||||
|
html[data-layout="bento-hub"] .home-pl-grid { grid-template-columns:1fr; }
|
||||||
|
}
|
||||||
|
@media(min-width:861px) { html[data-layout="bento-hub"] .bento-search-label { position:static; width:auto; height:auto; margin:0; clip-path:none; overflow:visible; } }
|
||||||
|
/* Bound the scroll viewport above floating chrome, rather than painting rows
|
||||||
|
underneath it and relying only on extra padding at the end of the list. */
|
||||||
|
html[data-layout="bento-hub"] .main { padding-bottom:var(--bento-bottom-clearance,16px); min-height:0; }
|
||||||
|
html[data-layout="bento-hub"] :is(.body,.player-pane,.cards) { padding-bottom:16px; }
|
||||||
|
html[data-layout="bento-hub"] .modal-backdrop { z-index:600; }
|
||||||
|
html[data-layout="bento-hub"] .shortcut-overlay { z-index:600; }
|
||||||
|
|||||||
12
scripts/test-bento-theme.js
Normal file
12
scripts/test-bento-theme.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* Reproducible before/after comparison without changing the working tree. */
|
||||||
|
const {execFileSync,spawnSync}=require('node:child_process');
|
||||||
|
const {mkdtempSync,rmSync}=require('node:fs');
|
||||||
|
const {join}=require('node:path');
|
||||||
|
const {tmpdir}=require('node:os');
|
||||||
|
const baseline=mkdtempSync(join(tmpdir(),'ytplayer-bento-baseline-'));
|
||||||
|
try {
|
||||||
|
const archive=execFileSync('git',['archive',process.env.BENTO_BASE_COMMIT||'04b620e','frontend']);
|
||||||
|
execFileSync('tar',['-x','-C',baseline],{input:archive});
|
||||||
|
const run=env=>{const result=spawnSync('npx',['playwright','test','--config','playwright.bento.config.js','--workers','4'],{stdio:'inherit',env:{...process.env,...env}});if(result.status!==0)process.exitCode=result.status||1;return result.status===0;};
|
||||||
|
if(run({CAPTURE_BEFORE:'1',BENTO_BASELINE:join(baseline,'frontend')}))run({CAPTURE_BEFORE:'0',BENTO_BASELINE:''});
|
||||||
|
} finally {rmSync(baseline,{recursive:true,force:true});}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
const {test,expect}=require('@playwright/test');
|
const {test,expect}=require('@playwright/test');
|
||||||
const {openClassic,showPlayingFixture}=require('./helpers/classic-fixture');
|
const {openClassic,showPlayingFixture}=require('./helpers/classic-fixture');
|
||||||
const {mkdirSync,readFileSync}=require('node:fs');
|
const {mkdirSync,readFileSync,existsSync}=require('node:fs');
|
||||||
|
const {join}=require('node:path');
|
||||||
|
const {homedir}=require('node:os');
|
||||||
const {differentPixels}=require('./helpers/png-diff');
|
const {differentPixels}=require('./helpers/png-diff');
|
||||||
const out='/home/josh/deliverables/ytplayer-done13';mkdirSync(out,{recursive:true});
|
const out=process.env.BENTO_SCREENSHOT_DIR||join(homedir(),'deliverables','ytplayer-done13');mkdirSync(out,{recursive:true});
|
||||||
const before=process.env.CAPTURE_BEFORE==='1';
|
const before=process.env.CAPTURE_BEFORE==='1';
|
||||||
const screens=['home','search','playlist','queue','history','saved','downloads','channel','settings','settings-detail','now-playing','lyrics','chapters','lyric-search','service','modal','empty-queue','empty-saved','empty-downloads'];
|
const screens=['home','search','playlist','queue','history','saved','downloads','channel','settings','settings-detail','now-playing','lyrics','chapters','lyric-search','service','modal','sheet','stage-lyrics','batch','library','empty-queue','empty-saved','empty-downloads'];
|
||||||
async function scene(page,layout,screen){
|
async function scene(page,layout,screen){
|
||||||
|
// Freeze the automatic Classic mini-player observer in these static scenes.
|
||||||
|
// Otherwise its pending first callback races our intentionally visible mini bar.
|
||||||
|
await page.addInitScript(()=>{const Observer=window.IntersectionObserver;window.IntersectionObserver=class extends Observer{observe(el){if(el.id!=='controls')super.observe(el);}};});
|
||||||
await openClassic(page,{populated:true,standalone:page.viewportSize().width<900});await showPlayingFixture(page);
|
await openClassic(page,{populated:true,standalone:page.viewportSize().width<900});await showPlayingFixture(page);
|
||||||
await page.evaluate(({layout,screen})=>{
|
await page.evaluate(({layout,screen})=>{
|
||||||
data.settings.layout=layout;applyAppearance();
|
data.settings.layout=layout;applyAppearance();
|
||||||
@@ -17,7 +22,7 @@ async function scene(page,layout,screen){
|
|||||||
if(screen==='empty-queue'){queue=[];data.queue=[];}
|
if(screen==='empty-queue'){queue=[];data.queue=[];}
|
||||||
view=screen==='home'?{type:'search'}:screen==='playlist'?{type:'playlist',id:'studio'}:{type:screen.replace('empty-','')};
|
view=screen==='home'?{type:'search'}:screen==='playlist'?{type:'playlist',id:'studio'}:{type:screen.replace('empty-','')};
|
||||||
if(screen==='home'){current=null;els.playerPane.classList.add('empty');els.placeholder.classList.remove('hidden');}
|
if(screen==='home'){current=null;els.playerPane.classList.add('empty');els.placeholder.classList.remove('hidden');}
|
||||||
if(['now-playing','lyrics','chapters','lyric-search','service','modal'].includes(screen))view={type:'playlist',id:'studio'};
|
if(['now-playing','lyrics','chapters','lyric-search','service','modal','sheet','stage-lyrics','batch','library'].includes(screen))view={type:'playlist',id:'studio'};
|
||||||
if(screen==='settings-detail')view={type:'settings'};
|
if(screen==='settings-detail')view={type:'settings'};
|
||||||
render();renderUpNext();
|
render();renderUpNext();
|
||||||
if(screen!=='home')showMiniBar();
|
if(screen!=='home')showMiniBar();
|
||||||
@@ -30,11 +35,22 @@ async function scene(page,layout,screen){
|
|||||||
$('chapterList').innerHTML='<button class="marker-row"><span>0:00</span><b>Opening verse</b></button><button class="marker-row"><span>1:24</span><b>Chorus</b></button>';
|
$('chapterList').innerHTML='<button class="marker-row"><span>0:00</span><b>Opening verse</b></button><button class="marker-row"><span>1:24</span><b>Chorus</b></button>';
|
||||||
}
|
}
|
||||||
if(screen==='service'){data.settings.serviceLyrics=true;enterServiceMode(data.playlists[0]);$('serviceLyrics').classList.remove('hidden');$('serviceLyricsList').innerHTML='<div class="sl-line">Because You are God</div><div class="sl-line active">You can do anything</div>';}
|
if(screen==='service'){data.settings.serviceLyrics=true;enterServiceMode(data.playlists[0]);$('serviceLyrics').classList.remove('hidden');$('serviceLyricsList').innerHTML='<div class="sl-line">Because You are God</div><div class="sl-line active">You can do anything</div>';}
|
||||||
|
if(screen==='batch'){selectMode=true;selectedIds=new Set(videos.slice(0,2).map(v=>v.id));renderList();}
|
||||||
|
if(screen==='sheet')openMoreControls();
|
||||||
|
if(screen==='stage-lyrics'){ $('stageLyrics').classList.remove('hidden');$('stageLyricsList').innerHTML='<div class="sl-line">Because You are God</div><div class="sl-line active">You can do anything</div>'; }
|
||||||
if(screen==='modal')showModal('Add to playlist',document.createTextNode('Keep this song in your Sunday setlist.'),[{label:'Cancel',onClick:closeModal},{label:'Add',primary:true,onClick:closeModal}]);
|
if(screen==='modal')showModal('Add to playlist',document.createTextNode('Keep this song in your Sunday setlist.'),[{label:'Cancel',onClick:closeModal},{label:'Add',primary:true,onClick:closeModal}]);
|
||||||
},{layout,screen});
|
},{layout,screen});
|
||||||
if(screen==='settings-detail')await page.getByRole('button',{name:/Downloads & storage/}).click();
|
if(screen==='settings-detail')await page.getByRole('button',{name:/Downloads & storage/}).click();
|
||||||
await page.addStyleTag({content:'*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}'});
|
if(layout==='bento-hub'&&['now-playing','lyrics','chapters','lyric-search','stage-lyrics'].includes(screen))await page.evaluate(()=>document.querySelector('.body').scrollTop=0);
|
||||||
|
await page.evaluate(()=>document.querySelectorAll('progress:not([value])').forEach(el=>el.value=0));
|
||||||
|
await page.addStyleTag({content:'*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important;scroll-behavior:auto!important}'});
|
||||||
await page.evaluate(async()=>{ await document.fonts.ready; await Promise.all([...document.images].map(img=>{img.loading='eager';return img.decode().catch(()=>{});})); if(document.activeElement instanceof HTMLElement)document.activeElement.blur(); });await page.waitForTimeout(300);
|
await page.evaluate(async()=>{ await document.fonts.ready; await Promise.all([...document.images].map(img=>{img.loading='eager';return img.decode().catch(()=>{});})); if(document.activeElement instanceof HTMLElement)document.activeElement.blur(); });await page.waitForTimeout(300);
|
||||||
|
// Short Classic empty lists otherwise clamp navigation one pixel before
|
||||||
|
// their header on WebKit. An offscreen spacer makes the static route exact.
|
||||||
|
if(layout==='classic'&&screen==='empty-saved'&&page.viewportSize().width===390)await page.evaluate(()=>{const body=document.querySelector('.body');$('listPane').style.minHeight=(body.clientHeight+8)+'px';});
|
||||||
|
// render() navigates before fonts finish; settle the real list route once
|
||||||
|
// after font metrics are known, so WebKit cannot capture an old scroll offset.
|
||||||
|
if(!['home','now-playing','lyrics','chapters','lyric-search','stage-lyrics','service','modal','sheet','batch'].includes(screen)){await page.evaluate(()=>scrollListIntoViewPortrait());await page.waitForTimeout(100);}
|
||||||
}
|
}
|
||||||
for(const width of [390,1440])for(const screen of screens)for(const layout of before?['classic','glass-stage']:['classic','glass-stage','bento-hub'])test(`${layout} ${width} ${screen}`,async({page},info)=>{
|
for(const width of [390,1440])for(const screen of screens)for(const layout of before?['classic','glass-stage']:['classic','glass-stage','bento-hub'])test(`${layout} ${width} ${screen}`,async({page},info)=>{
|
||||||
await page.setViewportSize({width,height:844});await scene(page,layout,screen);
|
await page.setViewportSize({width,height:844});await scene(page,layout,screen);
|
||||||
@@ -43,10 +59,44 @@ for(const width of [390,1440])for(const screen of screens)for(const layout of be
|
|||||||
if(before)return;
|
if(before)return;
|
||||||
if(layout!=='bento-hub'){expect(differentPixels(readFileSync(`${out}/before-${name}`),buffer)).toBeLessThanOrEqual(40);return;}
|
if(layout!=='bento-hub'){expect(differentPixels(readFileSync(`${out}/before-${name}`),buffer)).toBeLessThanOrEqual(40);return;}
|
||||||
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);
|
||||||
|
if(['lyrics','chapters','lyric-search'].includes(screen)){await page.locator('#notesPanel').scrollIntoViewIfNeeded();await page.screenshot({path:`${out}/panel-${name}`});}
|
||||||
|
if(screen==='now-playing')await expect(page.locator('#controls')).toBeVisible();
|
||||||
|
if(screen==='downloads'){const retry=page.getByRole('button',{name:/^Retry /});await expect(retry).toBeVisible();expect(await retry.evaluate(el=>getComputedStyle(el).backgroundColor)).toBe('rgb(255, 183, 3)');}
|
||||||
|
if(width===390&&!['service','modal','sheet'].includes(screen)){await expect(page.locator('#bottomNav')).toBeVisible(); for(const button of await page.locator('#bottomNav button').all()){const icon=await button.locator('.bottom-nav-icon').boundingBox(),label=await button.locator('.bottom-nav-label').boundingBox();expect(Math.abs(icon.x+icon.width/2-label.x-label.width/2)).toBeLessThan(1);}}
|
||||||
expect(await page.locator('#sectionRail').evaluate(el=>el.parentElement===document.body)).toBe(true);
|
expect(await page.locator('#sectionRail').evaluate(el=>el.parentElement===document.body)).toBe(true);
|
||||||
if(!['service','modal'].includes(screen)&&width===390){const tabs=page.locator('#bottomNav button');for(const b of await tabs.all()){if(await b.isVisible()){const r=await b.boundingBox();expect(r.height).toBeGreaterThanOrEqual(44);}}}
|
if(!['service','modal','sheet','stage-lyrics','batch','library'].includes(screen)&&width===390){const tabs=page.locator('#bottomNav button');for(const b of await tabs.all()){if(await b.isVisible()){const r=await b.boundingBox();expect(r.height).toBeGreaterThanOrEqual(44);}}}
|
||||||
});
|
});
|
||||||
for(const width of [390,1440])test(`reference ${width}`,async({page},info)=>{
|
for(const width of [390,1440])test(`reference ${width}`,async({page},info)=>{
|
||||||
|
test.skip(!existsSync('docs/mockups/bento-hub/index.html'),'The design reference is an optional local deliverable.');
|
||||||
await page.setViewportSize({width,height:844});await page.route('**/__bento-reference',r=>r.fulfill({contentType:'text/html',body:readFileSync('docs/mockups/bento-hub/index.html','utf8')}));await page.goto('/__bento-reference');await page.evaluate(()=>document.fonts.ready);
|
await page.setViewportSize({width,height:844});await page.route('**/__bento-reference',r=>r.fulfill({contentType:'text/html',body:readFileSync('docs/mockups/bento-hub/index.html','utf8')}));await page.goto('/__bento-reference');await page.evaluate(()=>document.fonts.ready);
|
||||||
await page.screenshot({path:`${out}/reference-${width}-${info.project.name}.png`,fullPage:true});
|
await page.screenshot({path:`${out}/reference-${width}-${info.project.name}.png`,fullPage:true});
|
||||||
|
for(const section of await page.locator('.screen-section').all()){
|
||||||
|
await section.evaluate(el=>{document.querySelectorAll('.screen-section').forEach(s=>s.classList.remove('active'));el.classList.add('active');});
|
||||||
|
const id=await section.getAttribute('id');
|
||||||
|
const frame=section.locator(width===390?'.frame-phone':'.frame-desktop');
|
||||||
|
if(await frame.count())await frame.screenshot({path:`${out}/reference-${id}-${width}-${info.project.name}.png`});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(!before){
|
||||||
|
for(const width of [390,960])test(`Bento ${width} navigation and floating clearance`,async({page})=>{
|
||||||
|
await page.setViewportSize({width,height:844});await scene(page,'bento-hub','downloads');
|
||||||
|
await page.locator('#sidebarToggle').click();await expect(page.locator('.app')).toHaveClass(/sidebar-open/);await page.locator('#sidebarBackdrop').click({position:{x:width-8,y:20}});
|
||||||
|
const chrome=await page.evaluate(()=>{const body=document.querySelector('.body').getBoundingClientRect(),mini=$('miniBar').getBoundingClientRect();return {bodyBottom:body.bottom,miniTop:mini.top};});
|
||||||
|
expect(chrome.bodyBottom).toBeLessThan(chrome.miniTop);
|
||||||
|
if(width===390){await page.locator('#miniTitle').click();await expect(page.locator('#controls')).toBeInViewport();}
|
||||||
|
});
|
||||||
|
for(const layout of ['classic','glass-stage'])test(`Bento restores ${layout} after switching`,async({page})=>{
|
||||||
|
await page.setViewportSize({width:390,height:844});await scene(page,layout,'home');await page.evaluate(()=>document.querySelector('.body').scrollTop=0);const original=await page.screenshot();
|
||||||
|
await page.evaluate(()=>{data.settings.layout='bento-hub';applyAppearance();});await page.waitForTimeout(100);
|
||||||
|
await page.evaluate(layout=>{data.settings.layout=layout;applyAppearance();},layout);await page.waitForTimeout(100);await page.evaluate(()=>document.querySelector('.body').scrollTop=0);
|
||||||
|
// Chromium may rerasterize composited corners by one RGB level after a theme switch.
|
||||||
|
expect(differentPixels(original,await page.screenshot(),1)).toBeLessThanOrEqual(40);
|
||||||
|
});
|
||||||
|
for(const theme of ['light','contrast'])test(`Bento ${theme} appearance`,async({page},info)=>{
|
||||||
|
await page.setViewportSize({width:390,height:844});await scene(page,'bento-hub','saved');await page.evaluate(theme=>{data.settings.theme=theme;applyAppearance();},theme);
|
||||||
|
await page.screenshot({path:`${out}/after-bento-hub-390-${theme}-${info.project.name}.png`});
|
||||||
|
await expect(page.locator('.saved-file-action').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ function decode(buffer) {
|
|||||||
}
|
}
|
||||||
return {width,height,channels,pixels};
|
return {width,height,channels,pixels};
|
||||||
}
|
}
|
||||||
function differentPixels(first,second){
|
function differentPixels(first,second,channelTolerance=0){
|
||||||
const a=decode(first),b=decode(second);if(a.width!==b.width||a.height!==b.height||a.channels!==b.channels)return Infinity;
|
const a=decode(first),b=decode(second);if(a.width!==b.width||a.height!==b.height||a.channels!==b.channels)return Infinity;
|
||||||
let count=0;for(let i=0;i<a.pixels.length;i+=a.channels)if(!a.pixels.subarray(i,i+a.channels).equals(b.pixels.subarray(i,i+a.channels)))count++;
|
let count=0;for(let i=0;i<a.pixels.length;i+=a.channels)if([...a.pixels.subarray(i,i+a.channels)].some((value,channel)=>Math.abs(value-b.pixels[i+channel])>channelTolerance))count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
module.exports={differentPixels};
|
module.exports={differentPixels};
|
||||||
|
|||||||
Reference in New Issue
Block a user