Compare commits
6 Commits
04b620e3c7
...
1754933f7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1754933f7e | ||
|
|
4e5b1584c6 | ||
|
|
9589c38773 | ||
|
|
3baf67349f | ||
|
|
e100d47143 | ||
|
|
838798ca9b |
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.
|
||||
@@ -8669,6 +8669,7 @@ function renderSidebar() {
|
||||
// the fallback so a first-time visitor still gets the landing.
|
||||
// Driven from renderSidebar() so every playlist mutation refreshes both.
|
||||
function renderHomePlaylists() {
|
||||
if (window.BentoHub) BentoHub.playlists = () => data.playlists || [];
|
||||
const wrap = document.getElementById('homePlaylists');
|
||||
const hero = document.querySelector('#playerPlaceholder .hero');
|
||||
const grid = document.getElementById('homePlGrid');
|
||||
@@ -8818,6 +8819,8 @@ function resetListHeaderExtras() {
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
// Theme composition follows the real view without changing its controls.
|
||||
els.cards.dataset.view = view.type;
|
||||
resetListHeaderExtras();
|
||||
{ const ls = $('listSub'); if (ls) ls.textContent = ''; const ps = $('p2pStrip'); if (ps) ps.classList.add('hidden'); }
|
||||
if (view.type !== 'search') { const sm = $('searchMeta'); if (sm) sm.classList.add('hidden'); }
|
||||
@@ -10884,6 +10887,9 @@ function isStackedPlayerLayout() {
|
||||
if (document.documentElement.dataset.layout === 'classic') {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
40
frontend/bento-hub.js
Normal file
40
frontend/bento-hub.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* Bento-only artwork and measured clearance. Navigation and player nodes stay put. */
|
||||
(function(root){
|
||||
'use strict';
|
||||
function chromeClearance(height,boxes){
|
||||
const occupied=boxes.filter(b=>b.visible&&b.fixed&&b.height>0&&b.top>=height/2).map(b=>height-b.top);
|
||||
return Math.max(0,...occupied)+16;
|
||||
}
|
||||
function mount(doc){
|
||||
let frame=0,active=false;const html=doc.documentElement;
|
||||
const resize=new root.ResizeObserver(schedule),observer=new root.MutationObserver(schedule);
|
||||
function paint(){
|
||||
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,.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);}}
|
||||
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 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')){
|
||||
if(card.querySelector('.bento-collage'))continue;
|
||||
// Use actual playlist artwork; never invent an image or a verification badge.
|
||||
const name=card.querySelector('.home-pl-name')?.textContent;
|
||||
const playlist=root.BentoHub.playlists?.().find(p=>p.name===name);
|
||||
const images=(playlist?.videos||[]).filter(v=>v.thumbnail).slice(0,4);
|
||||
if(images.length<4)continue;
|
||||
const collage=doc.createElement('span');collage.className='bento-collage';collage.setAttribute('aria-hidden','true');
|
||||
for(const video of images){const img=doc.createElement('img');img.src=video.thumbnail;img.alt='';img.loading='lazy';collage.append(img);}
|
||||
card.querySelector('.home-pl-thumb')?.prepend(collage);
|
||||
}
|
||||
}
|
||||
function schedule(){if(!frame)frame=root.requestAnimationFrame(paint);}
|
||||
new root.MutationObserver(schedule).observe(html,{attributes:true,attributeFilter:['data-layout']});root.addEventListener('resize',schedule);paint();
|
||||
}
|
||||
const api={chromeClearance,playlists:null};
|
||||
if(typeof module!=='undefined')module.exports=api;
|
||||
else {root.BentoHub=api; if(root.document.readyState==='loading')root.document.addEventListener('DOMContentLoaded',()=>mount(root.document),{once:true});else mount(root.document);}
|
||||
})(typeof window==='undefined'?globalThis:window);
|
||||
12
frontend/bento-hub.test.js
Normal file
12
frontend/bento-hub.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const test=require('node:test'),assert=require('node:assert/strict');
|
||||
const {chromeClearance}=require('./bento-hub');
|
||||
test('clearance follows the highest visible floating control, including safe area',()=>{
|
||||
assert.equal(chromeClearance(844,[{visible:true,fixed:true,top:746,height:98},{visible:true,fixed:true,top:666,height:72},{visible:true,fixed:true,top:602,height:56}]),258);
|
||||
});
|
||||
test('hidden and scrolling content cannot reserve fullscreen bottom space',()=>{
|
||||
assert.equal(chromeClearance(844,[{visible:false,fixed:true,top:602,height:56},{visible:true,fixed:false,top:500,height:100},{visible:true,fixed:true,top:0,height:64}]),16);
|
||||
});
|
||||
test('desktop mini player reserves its real height and margin; no chrome is 16px',()=>{
|
||||
assert.equal(chromeClearance(844,[{visible:true,fixed:true,top:760,height:72}]),100);
|
||||
assert.equal(chromeClearance(844,[]),16);
|
||||
});
|
||||
93
frontend/fonts/PlusJakartaSans-OFL.txt
Normal file
93
frontend/fonts/PlusJakartaSans-OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
frontend/fonts/PlusJakartaSans-latin-ext.woff2
Normal file
BIN
frontend/fonts/PlusJakartaSans-latin-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/PlusJakartaSans-latin.woff2
Normal file
BIN
frontend/fonts/PlusJakartaSans-latin.woff2
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
/* Draw legacy control glyphs only in Glass; restore them on theme changes. */
|
||||
/* Draw legacy control glyphs in Glass and Bento; restore them on theme changes. */
|
||||
(function(root) {
|
||||
'use strict';
|
||||
const paths = {
|
||||
@@ -20,15 +20,17 @@
|
||||
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"/>',
|
||||
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"/>'
|
||||
};
|
||||
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 glyph = Object.keys(glyphs).find(key => rest.startsWith(key));
|
||||
const glyph = Object.keys(mapping).find(key => rest.startsWith(key));
|
||||
if (!glyph) return null;
|
||||
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) {
|
||||
const decorations = new Set(), tabLabels = new Map(); let scheduled=false;
|
||||
@@ -45,14 +47,15 @@
|
||||
}
|
||||
function paint() {
|
||||
scheduled=false;observer.disconnect();
|
||||
const active=doc.documentElement.dataset.layout==='glass-stage';
|
||||
const bento=doc.documentElement.dataset.layout==='bento-hub';
|
||||
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 record of decorations)if(!active || !record.svg.isConnected){restore(record);decorations.delete(record);}
|
||||
if(active) for(const parent of doc.querySelectorAll(selector)) {
|
||||
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.
|
||||
if(parent.querySelector('svg:not(.glass-control-icon),.legacy-nav-icon'))continue;
|
||||
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);
|
||||
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;
|
||||
|
||||
@@ -13,3 +13,10 @@ test('playlist trash, menu and music glyphs use drawn glass icons',()=>{
|
||||
assert.equal(leadingGlyph('⋮').kind,'more');
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<link rel="stylesheet" href="fullscreen-orientation.css" />
|
||||
<link rel="stylesheet" href="related-videos.css" />
|
||||
<link rel="stylesheet" href="download-actions.css" />
|
||||
<link rel="stylesheet" href="theme-bento-hub.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -678,6 +679,7 @@
|
||||
<script src="saved-page.js"></script>
|
||||
<script src="server-backfill.js"></script>
|
||||
<script src="glass-controls.js"></script>
|
||||
<script src="bento-hub.js"></script>
|
||||
<script src="glass-panel-layout.js"></script>
|
||||
<script src="party-dj.js"></script>
|
||||
<script src="lyrics-window.js"></script>
|
||||
|
||||
@@ -3345,84 +3345,6 @@ html[data-layout="contemplative"] .brand-mark {
|
||||
box-shadow: 0 6px 18px -4px rgba(45, 212, 191, 0.4), inset 0 1px 0 rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
/* --- Bento Hub -------------------------------------------------------------
|
||||
* GitHub-dark-ish with honey-gold accent. Tighter borders give a
|
||||
* "dashboard card" feel throughout.
|
||||
* ------------------------------------------------------------------------- */
|
||||
html[data-layout="bento-hub"] {
|
||||
--bg: #0d1117;
|
||||
--bg-1: #13181f;
|
||||
--bg-2: #1c2333;
|
||||
--bg-3: #252d3d;
|
||||
--line: #30363d;
|
||||
--line-soft: #21262d;
|
||||
--text: #e6edf3;
|
||||
--text-2: #8b949e;
|
||||
--text-dim: #484f58;
|
||||
--accent: #eab308;
|
||||
--accent-bright: #fbbf24;
|
||||
--accent-deep: #ca8a04;
|
||||
--accent-glow: rgba(234, 179, 8, 0.4);
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
}
|
||||
html[data-layout="bento-hub"] body::before {
|
||||
background:
|
||||
radial-gradient(900px 600px at 18% -10%, rgba(234, 179, 8, 0.06), transparent 60%),
|
||||
radial-gradient(700px 500px at 100% 110%, rgba(234, 179, 8, 0.03), transparent 55%);
|
||||
}
|
||||
html[data-layout="bento-hub"] .brand-mark {
|
||||
background: linear-gradient(145deg, #fbbf24, #ca8a04);
|
||||
box-shadow: 0 6px 18px -4px rgba(234, 179, 8, 0.45), inset 0 1px 0 rgba(255,255,255,0.25);
|
||||
}
|
||||
html[data-layout="bento-hub"] .sidebar {
|
||||
background: #13181f;
|
||||
border-right: 1px solid #30363d;
|
||||
}
|
||||
html[data-layout="bento-hub"] .brand {
|
||||
border-bottom: 1px solid #21262d;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
html[data-layout="bento-hub"] .nav-item {
|
||||
border-radius: 6px;
|
||||
}
|
||||
html[data-layout="bento-hub"] .nav-item.active {
|
||||
background: rgba(234, 179, 8, 0.08);
|
||||
color: #fbbf24;
|
||||
}
|
||||
html[data-layout="bento-hub"] .nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: #eab308;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
html[data-layout="bento-hub"] .playlist-item {
|
||||
background: #0d1117;
|
||||
border: 1px solid #21262d;
|
||||
border-radius: 8px;
|
||||
margin: 3px 0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
html[data-layout="bento-hub"] .playlist-item.active {
|
||||
border-color: #eab308;
|
||||
background: rgba(234, 179, 8, 0.05);
|
||||
}
|
||||
html[data-layout="bento-hub"] .topbar {
|
||||
background: #13181f;
|
||||
border-bottom: 2px solid #21262d;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
html[data-layout="bento-hub"] .mini-bar-inner {
|
||||
background: linear-gradient(180deg, #1c2333, #13181f);
|
||||
border-top: 1px solid #30363d;
|
||||
}
|
||||
|
||||
/* Grid-column overrides only apply on screens wide enough for an inline sidebar
|
||||
(≥861px). Below that the sidebar is position:fixed (out of grid flow) so
|
||||
a two-column grid would leave .main stranded in the narrow first column. */
|
||||
|
||||
@@ -59,6 +59,9 @@ const SHELL = [
|
||||
'/service-ui.css',
|
||||
'/refresh.css',
|
||||
'/theme-glass.css',
|
||||
'/theme-bento-hub.css',
|
||||
'/fonts/PlusJakartaSans-latin.woff2',
|
||||
'/fonts/PlusJakartaSans-latin-ext.woff2',
|
||||
'/theme-glass-panel.css',
|
||||
'/glass-panel-layout.js',
|
||||
'/theme-classic.css',
|
||||
@@ -105,6 +108,7 @@ const SHELL = [
|
||||
'/saved-page.js',
|
||||
'/server-backfill.js',
|
||||
'/glass-controls.js',
|
||||
'/bento-hub.js',
|
||||
'/theme-glass-controls.css',
|
||||
'/saved-page.css',
|
||||
'/party-dj.js',
|
||||
|
||||
363
frontend/theme-bento-hub.css
Normal file
363
frontend/theme-bento-hub.css
Normal file
@@ -0,0 +1,363 @@
|
||||
/* Bento Hub — obsidian tiles, solar controls and cyan listening cues.
|
||||
Reference: docs/mockups/bento-hub. No shared-theme overrides live here. */
|
||||
@font-face { font-family:'Plus Jakarta Sans'; font-style:normal; font-weight:400 800; font-display:swap; src:url('fonts/PlusJakartaSans-latin.woff2') format('woff2'); unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
|
||||
@font-face { font-family:'Plus Jakarta Sans'; font-style:normal; font-weight:400 800; font-display:swap; src:url('fonts/PlusJakartaSans-latin-ext.woff2') format('woff2'); unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
|
||||
html[data-layout="bento-hub"] {
|
||||
--bg:#090b0e; --bg-1:#11151c; --bg-2:#19202d; --bg-3:#222c3e; --bg-glass:rgba(17,21,28,.85);
|
||||
--line:rgba(255,255,255,.09); --line-soft:rgba(255,255,255,.04); --line-bright:rgba(255,255,255,.18);
|
||||
--text:#f1f5f9; --text-2:#94a3b8; --text-dim:#8594aa; --on-accent:#090b0e;
|
||||
--accent:#ffb703; --accent-bright:#ffc527; --accent-deep:#d97706; --accent-glow:rgba(255,183,3,.22);
|
||||
--accent-sec:#00f5d4; --accent-sec-deep:#00bbf9; --danger:#ef4444; --danger-text:#ff9393; --danger-bg:rgba(239,68,68,.12); --success:#10b981;
|
||||
--radius:16px; --radius-outer:16px; --radius-inner:10px; --radius-sm:6px; --tile-gap:16px;
|
||||
--shadow-sm:0 2px 8px rgb(0 0 0/.3); --shadow-md:0 8px 24px rgb(0 0 0/.45); --shadow-lg:0 16px 40px rgb(0 0 0/.6); --shadow:var(--shadow-md);
|
||||
--ui:'Plus Jakarta Sans',system-ui,sans-serif; --display:var(--ui); --mono:'JetBrains Mono',monospace; color-scheme:dark;
|
||||
}
|
||||
html[data-layout="bento-hub"][data-theme="light"] {
|
||||
--bg:#f4f6f8; --bg-1:#fff; --bg-2:#e9ecef; --bg-3:#dee2e6; --bg-glass:rgba(255,255,255,.88);
|
||||
--line:rgba(0,0,0,.08); --line-soft:rgba(0,0,0,.04); --line-bright:rgba(0,0,0,.16);
|
||||
--text:#0f172a; --text-2:#475569; --text-dim:#64748b; --on-accent:#fff;
|
||||
--accent:#b65f04; --accent-bright:#d97706; --accent-deep:#b45309; --accent-glow:rgba(217,119,6,.18);
|
||||
--accent-sec:#0d9488; --accent-sec-deep:#0f766e; --danger-text:#b91c1c;
|
||||
--shadow-sm:0 2px 6px rgb(0 0 0/.05); --shadow-md:0 8px 20px rgb(0 0 0/.08); --shadow-lg:0 16px 32px rgb(0 0 0/.12); color-scheme:light;
|
||||
}
|
||||
html[data-layout="bento-hub"][data-theme="contrast"] { --bg:#000; --bg-1:#080808; --bg-2:#161616; --line:#aaa; --line-soft:#666; --text:#fff; --text-2:#ddd; --text-dim:#bbb; }
|
||||
html[data-layout="bento-hub"] body { background:var(--bg); color:var(--text); font-family:var(--ui); }
|
||||
html[data-layout="bento-hub"] body::before { display:none; }
|
||||
html[data-layout="bento-hub"] ::selection { background:var(--accent); color:var(--on-accent); }
|
||||
html[data-layout="bento-hub"] :is(input,textarea) { caret-color:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] * { scrollbar-color:var(--bg-3) var(--bg-1); scrollbar-width:thin; }
|
||||
html[data-layout="bento-hub"] ::-webkit-scrollbar-thumb { background:var(--bg-3); border-radius:6px; }
|
||||
html[data-layout="bento-hub"] :is(button,a,input,select,textarea,summary,[tabindex]):focus-visible { outline:2px solid var(--accent-sec); outline-offset:3px; }
|
||||
html[data-layout="bento-hub"] :is(button,input,select,textarea) { font-family:var(--ui); }
|
||||
html[data-layout="bento-hub"] :is(button,select,summary) { min-height:44px; }
|
||||
html[data-layout="bento-hub"] button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
html[data-layout="bento-hub"] .glass-original-glyph,
|
||||
html[data-layout="bento-hub"] .legacy-nav-icon { display:none; }
|
||||
html[data-layout="bento-hub"] :is(.classic-nav-icon,.glass-control-icon) { display:inline-block; width:20px; height:20px; flex:none; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; vertical-align:middle; }
|
||||
html[data-layout="bento-hub"] .sidebar { background:var(--bg-1); border-right:1px solid var(--line); padding:20px 16px; gap:16px; }
|
||||
html[data-layout="bento-hub"] .brand { padding:0 0 16px; border-bottom:1px solid var(--line); gap:10px; font-size:18px; font-weight:800; letter-spacing:-.02em; }
|
||||
html[data-layout="bento-hub"] .brand-mark { width:34px; height:34px; border-radius:10px; background:linear-gradient(135deg,var(--accent),var(--accent-deep)); color:var(--on-accent); box-shadow:var(--shadow-sm); }
|
||||
html[data-layout="bento-hub"] .nav { gap:6px; }
|
||||
html[data-layout="bento-hub"] .nav-item { display:flex; align-items:center; gap:12px; min-height:44px; padding:10px 12px; border:1px solid transparent; border-radius:10px; color:var(--text-2); font-size:13px; font-weight:600; }
|
||||
html[data-layout="bento-hub"] .nav-item:hover { background:var(--bg-2); color:var(--text); }
|
||||
html[data-layout="bento-hub"] .nav-item.active { background:var(--bg-2); border-color:var(--line-bright); color:var(--accent); box-shadow:inset 3px 0 var(--accent); }
|
||||
html[data-layout="bento-hub"] .nav-item.active::before { display:none; }
|
||||
html[data-layout="bento-hub"] :is(.nav-badge,.pl-count) { background:var(--bg-3); color:var(--text-2); font:600 11px var(--mono); }
|
||||
html[data-layout="bento-hub"] .pl-header { font:700 12px var(--ui); color:var(--text-2); letter-spacing:0; }
|
||||
html[data-layout="bento-hub"] .playlist-item { display:flex; align-items:center; gap:10px; min-height:44px; padding:10px; border:1px solid var(--line); border-radius:10px; background:var(--bg); margin:6px 0; }
|
||||
html[data-layout="bento-hub"] .playlist-item.active { background:var(--bg-2); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .pl-name { min-width:0; font-size:13px; }
|
||||
html[data-layout="bento-hub"] .sidebar-footer { border-top:1px solid var(--line); color:var(--text-2); font-size:12px; }
|
||||
html[data-layout="bento-hub"] .topbar { background:var(--bg-1); border-bottom:1px solid var(--line); padding:12px 20px; gap:12px; backdrop-filter:none; }
|
||||
html[data-layout="bento-hub"] .search-form { max-width:640px; gap:8px; }
|
||||
html[data-layout="bento-hub"] #searchInput { border:1px solid var(--line); border-radius:999px; background:var(--bg-2); padding:12px 18px; font-size:14px; min-height:44px; box-shadow:none; }
|
||||
html[data-layout="bento-hub"] .search-btn { border:0; border-radius:999px; min-height:44px; background:var(--accent); color:var(--on-accent); font:700 13px var(--ui); box-shadow:none; }
|
||||
html[data-layout="bento-hub"] :is(.sidebar-toggle,.icon-btn,.update-icon-btn) { min-width:44px; width:44px; min-height:44px; border-radius:10px; background:var(--bg-2); color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .bottom-nav { height:calc(64px + env(safe-area-inset-bottom)); background:var(--bg-glass); border-top:1px solid var(--line); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px); padding:0 4px env(safe-area-inset-bottom); }
|
||||
html[data-layout="bento-hub"] .bottom-nav-btn { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; padding:8px 0; min-height:64px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .bottom-nav-icon { display:grid; place-items:center; width:24px; height:24px; margin:0; transform:none; }
|
||||
html[data-layout="bento-hub"] .bottom-nav-label { font-size:11px; font-weight:600; letter-spacing:0; }
|
||||
html[data-layout="bento-hub"] .bottom-nav-btn.active { color:var(--accent); background:transparent; }
|
||||
html[data-layout="bento-hub"] .bottom-nav-btn.active::before { content:''; position:absolute; top:0; width:24px; height:2px; border-radius:2px; background:var(--accent); }
|
||||
html[data-layout="bento-hub"] .bottom-nav-badge { left:calc(50% + 6px); top:4px; margin:0; height:18px; min-width:18px; font:700 11px var(--mono); color:var(--on-accent); background:var(--accent); }
|
||||
html[data-layout="bento-hub"] .player-pane { padding:20px; gap:16px; background:var(--bg); }
|
||||
html[data-layout="bento-hub"] .list-pane { background:var(--bg); border-left:1px solid var(--line); width:440px; }
|
||||
html[data-layout="bento-hub"] .player-placeholder { background:var(--bg); border:0; padding:0; }
|
||||
html[data-layout="bento-hub"] .hero { padding:24px; background:var(--bg-1); border:1px solid var(--line); border-radius:16px; }
|
||||
html[data-layout="bento-hub"] .hero-title { font:800 32px/1.2 var(--ui); color:var(--text); letter-spacing:-.03em; }
|
||||
html[data-layout="bento-hub"] .hero-tagline { font-size:14px; color:var(--text-2); max-width:65ch; line-height:1.6; }
|
||||
html[data-layout="bento-hub"] :is(.hero-pill,.chip) { background:var(--bg-2); color:var(--text-2); border:1px solid var(--line); border-radius:999px; font-size:12px; }
|
||||
html[data-layout="bento-hub"] .hero-pill::before { background:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] .home-pl { padding:0; }
|
||||
html[data-layout="bento-hub"] .home-pl-title { font:800 22px var(--ui); color:var(--text); }
|
||||
html[data-layout="bento-hub"] .home-pl-grid { grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:16px; padding:0 0 16px; }
|
||||
html[data-layout="bento-hub"] .home-pl-card { padding:12px; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); box-shadow:var(--shadow-sm); }
|
||||
html[data-layout="bento-hub"] .home-pl-thumb { border-radius:10px; aspect-ratio:16/9; }
|
||||
html[data-layout="bento-hub"] .home-pl-name { padding:10px 0 0; font:700 14px/1.4 var(--ui); }
|
||||
html[data-layout="bento-hub"] .home-pl-count { padding:4px 0 0; font-size:12px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .home-pl-play { width:44px; height:44px; color:var(--on-accent); background:var(--accent); border-radius:999px; }
|
||||
html[data-layout="bento-hub"] :is(.home-pl-new,.list-actions button,.np-btn,.dl-saved-cta,.dl-saved-link) { display:inline-flex; align-items:center; justify-content:center; gap:8px; border:1px solid var(--line); border-radius:10px; padding:10px 14px; background:var(--bg-2); color:var(--text); font:600 13px var(--ui); }
|
||||
html[data-layout="bento-hub"] :is(.home-pl-new,.list-actions .primary-btn,.primary-btn) { background:var(--accent); color:var(--on-accent); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .recommendations { border:1px solid var(--line); border-radius:16px; background:var(--bg-1); padding:16px; }
|
||||
html[data-layout="bento-hub"] .recommendations h2 { font:800 22px var(--ui); }
|
||||
html[data-layout="bento-hub"] .rec-card { background:var(--bg-2); border:1px solid var(--line); border-radius:10px; }
|
||||
html[data-layout="bento-hub"] .rec-title { font-size:13px; }
|
||||
html[data-layout="bento-hub"] .rec-channel { font-size:12px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .list-header { padding:20px 16px 12px; gap:12px; }
|
||||
html[data-layout="bento-hub"] .list-header h2 { font:800 22px/1.3 var(--ui); color:var(--text); }
|
||||
html[data-layout="bento-hub"] .list-filter-wrap { padding:0 16px 12px; }
|
||||
html[data-layout="bento-hub"] .list-filter { min-height:44px; background:var(--bg-2); border:1px solid var(--line); border-radius:10px; font-size:13px; }
|
||||
html[data-layout="bento-hub"] .cards { padding:4px 16px 24px; gap:12px; }
|
||||
html[data-layout="bento-hub"] .cards[data-view="search"],
|
||||
html[data-layout="bento-hub"] .cards[data-view="channel"],
|
||||
html[data-layout="bento-hub"] .cards[data-view="library"] { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); align-content:start; gap:16px; }
|
||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) .card { display:flex; flex-direction:column; align-items:stretch; gap:10px; padding:12px; }
|
||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) .thumb { width:100%; aspect-ratio:16/9; }
|
||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) .card-actions { position:static; display:flex; justify-content:flex-end; }
|
||||
html[data-layout="bento-hub"] .card { animation:none; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); box-shadow:var(--shadow-sm); }
|
||||
html[data-layout="bento-hub"] .card-title { font:700 13px/1.5 var(--ui); -webkit-line-clamp:2; }
|
||||
html[data-layout="bento-hub"] .card-channel { font-size:12px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .thumb { border-radius:10px; }
|
||||
html[data-layout="bento-hub"] .duration { font:500 11px var(--mono); background:rgba(0,0,0,.8); }
|
||||
html[data-layout="bento-hub"] .thumb::after { background:none; }
|
||||
html[data-layout="bento-hub"] .card:hover .thumb img { transform:none; }
|
||||
html[data-layout="bento-hub"] .card.playing { border-color:var(--accent-sec); background:var(--bg-2); }
|
||||
html[data-layout="bento-hub"] .card.playing::after { background:var(--accent-sec); }
|
||||
@media (hover:hover) {
|
||||
html[data-layout="bento-hub"] :is(.home-pl-card,.cards[data-view="search"] .card):hover { transform:translateY(-2px); border-color:var(--accent); box-shadow:var(--shadow-md); transition:transform .18s ease-out,box-shadow .18s ease-out; }
|
||||
}
|
||||
@media (min-width:1024px) { html[data-layout="bento-hub"] .app { grid-template-columns:260px minmax(0,1fr); } }
|
||||
@media (max-width:860px) {
|
||||
html[data-layout="bento-hub"] .topbar { padding:12px; gap:8px; }
|
||||
html[data-layout="bento-hub"] .player-pane { padding:12px; gap:12px; }
|
||||
html[data-layout="bento-hub"] .list-pane { width:100%; border-left:0; }
|
||||
html[data-layout="bento-hub"] .home-pl-grid { grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:12px; }
|
||||
html[data-layout="bento-hub"] .home-pl-title { font-size:18px; }
|
||||
html[data-layout="bento-hub"] .list-header { padding:16px 12px 12px; }
|
||||
html[data-layout="bento-hub"] .list-header h2 { font-size:18px; }
|
||||
html[data-layout="bento-hub"] .cards { padding-inline:12px; }
|
||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) { grid-template-columns:1fr; gap:12px; }
|
||||
html[data-layout="bento-hub"] .hero-title { font-size:24px; }
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce) { html[data-layout="bento-hub"] *,html[data-layout="bento-hub"] *::before,html[data-layout="bento-hub"] *::after { animation:none!important; transition:none!important; } }
|
||||
|
||||
/* Actual playlist artwork keeps the reference's collage without synthetic data. */
|
||||
html[data-layout="bento-hub"] .bento-collage { position:absolute; inset:0; display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:2px; }
|
||||
html[data-layout="bento-hub"] .bento-collage img { width:100%; height:100%; object-fit:cover; }
|
||||
html[data-layout="bento-hub"] .home-pl-thumb:has(.bento-collage) > img { visibility:hidden; }
|
||||
html[data-layout="bento-hub"] .cards:is([data-view="search"],[data-view="channel"],[data-view="library"]) .card-menu { align-self:flex-end; }
|
||||
@media(max-width:860px) { html[data-layout="bento-hub"] .bottom-nav { display:flex; } }
|
||||
html[data-layout="bento-hub"] body.service-open .bottom-nav { display:none; }
|
||||
|
||||
/* Library collections — compact rows, deliberate actions and truthful status. */
|
||||
html[data-layout="bento-hub"] .cards:not([data-view="search"]):not([data-view="channel"]):not([data-view="library"]) .card { padding:12px; gap:12px; align-items:center; }
|
||||
html[data-layout="bento-hub"] :is(.card-menu,.card-del,.card-note-btn,.card-act,.list-icon-btn,.saved-file-action) { position:static; flex:none; display:inline-flex; align-items:center; justify-content:center; width:44px; height:44px; min-width:44px; margin:0; padding:10px; border:1px solid var(--line); border-radius:10px; background:var(--bg-2); color:var(--text-2); opacity:1; }
|
||||
html[data-layout="bento-hub"] :is(.card-menu,.list-icon-btn):hover { border-color:var(--accent); color:var(--accent); background:var(--bg-3); }
|
||||
html[data-layout="bento-hub"] :is(.saved-clear,.card-del,.saved-file-action.danger,.list-icon-btn.danger) { color:var(--danger-text); background:var(--danger-bg); border-color:transparent; }
|
||||
html[data-layout="bento-hub"] :is(.saved-clear,.card-del,.saved-file-action.danger):hover { border-color:var(--danger-text); color:var(--danger-text); }
|
||||
html[data-layout="bento-hub"] :is(.saved-storage-summary,.dl-summary) { padding:12px 0 16px; color:var(--text-2); font-size:13px; }
|
||||
html[data-layout="bento-hub"] .saved-clear { border-radius:10px; padding:10px 12px; font-size:13px; }
|
||||
html[data-layout="bento-hub"] .card.saved-card { grid-template-columns:72px minmax(0,1fr) 96px; min-height:96px; }
|
||||
html[data-layout="bento-hub"] .saved-file-actions { gap:8px; }
|
||||
html[data-layout="bento-hub"] :is(.card .dur,.upnext-count,.saved-storage-text,.set-stat) { font-family:var(--mono); font-size:12px; font-variant-numeric:tabular-nums; }
|
||||
html[data-layout="bento-hub"] .card.playing::after { content:''; width:6px; height:6px; top:8px; right:8px; border-radius:50%; background:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] .saved-badge { color:var(--accent-sec); background:var(--bg-1); border:1px solid var(--line); }
|
||||
html[data-layout="bento-hub"] .pl-offline-badge { color:var(--accent-sec); font-size:11px; }
|
||||
html[data-layout="bento-hub"] .batch-bar { background:var(--bg-2); border:1px solid var(--accent); border-radius:10px; margin:0 16px 12px; padding:12px; gap:8px; color:var(--accent); box-shadow:var(--shadow-sm); }
|
||||
html[data-layout="bento-hub"] .batch-bar button { min-width:44px; background:var(--bg-1); color:var(--text); border:1px solid var(--line); border-radius:10px; font-size:13px; }
|
||||
html[data-layout="bento-hub"] .batch-bar .danger { color:var(--danger-text); background:var(--danger-bg); }
|
||||
html[data-layout="bento-hub"] .card.selected { border-color:var(--accent); background:var(--bg-2); }
|
||||
html[data-layout="bento-hub"] .batch-check { accent-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .dl-group { margin-bottom:24px; }
|
||||
html[data-layout="bento-hub"] .dl-group-title { font:700 16px var(--ui); margin:16px 0 10px; }
|
||||
html[data-layout="bento-hub"] .card.dl-row { grid-template-columns:72px minmax(0,1fr); padding:16px; border-radius:10px; gap:12px; }
|
||||
html[data-layout="bento-hub"] .card.dl-failed { border-color:color-mix(in srgb,var(--danger) 40%,var(--line)); }
|
||||
html[data-layout="bento-hub"] .dl-failed .card-channel { color:var(--danger-text); }
|
||||
html[data-layout="bento-hub"] .dl-progress { height:4px; border-radius:999px; overflow:hidden; background:var(--bg-3); }
|
||||
html[data-layout="bento-hub"] .dl-bar { background:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] :is(.dl-row,.download-job) .download-action { border-radius:10px; border-color:var(--line); background:var(--bg-2); color:var(--text); font-size:13px; }
|
||||
html[data-layout="bento-hub"] :is(.dl-row,.download-job) .download-action[aria-label^="Retry "] { background:var(--accent); color:var(--on-accent); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] :is(.dl-row,.download-job) .download-action.danger { background:var(--danger-bg); color:var(--danger-text); border-color:transparent; }
|
||||
html[data-layout="bento-hub"] :is(.dl-row,.download-job) .download-action:hover { border-color:var(--line-bright); filter:brightness(1.08); }
|
||||
html[data-layout="bento-hub"] .download-error-details { font-size:12px; }
|
||||
html[data-layout="bento-hub"] .download-error-details summary { color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .download-storage { border-top:1px solid var(--line); padding-top:16px; }
|
||||
html[data-layout="bento-hub"] :is(.channel-header,.saved-search-item,.p2p-row,.notes-video,.stat-card) { border:1px solid var(--line); border-radius:16px; background:var(--bg-1); padding:16px; }
|
||||
html[data-layout="bento-hub"] :is(.channel-header h2,.notes-video-title) { color:var(--text); font:700 18px var(--ui); }
|
||||
@media (max-width:640px) {
|
||||
html[data-layout="bento-hub"] .cards:not([data-view="search"]):not([data-view="channel"]):not([data-view="library"]) .card:not(.saved-card):not(.dl-row) { display:grid; grid-template-columns:72px minmax(0,1fr) 44px; gap:10px; }
|
||||
html[data-layout="bento-hub"] .cards:not([data-view="search"]):not([data-view="channel"]):not([data-view="library"]) .card:not(.saved-card):not(.dl-row) .thumb { width:72px; grid-column:1; grid-row:1; }
|
||||
html[data-layout="bento-hub"] .cards:not([data-view="search"]):not([data-view="channel"]):not([data-view="library"]) .card:not(.saved-card):not(.dl-row) .card-info { grid-column:2; grid-row:1; }
|
||||
html[data-layout="bento-hub"] .card-menu { grid-column:3; grid-row:1; }
|
||||
html[data-layout="bento-hub"] :is(.card-del,.card-note-btn) { grid-column:3; }
|
||||
html[data-layout="bento-hub"] .card.saved-card { gap:8px; padding:10px; grid-template-columns:56px minmax(0,1fr) 96px; }
|
||||
html[data-layout="bento-hub"] .saved-card .thumb { width:56px; }
|
||||
html[data-layout="bento-hub"] .card.dl-row { padding:12px; grid-template-columns:56px minmax(0,1fr); }
|
||||
html[data-layout="bento-hub"] .dl-row .thumb { width:56px; }
|
||||
html[data-layout="bento-hub"] .batch-bar { margin-inline:12px; flex-wrap:wrap; }
|
||||
}
|
||||
|
||||
/* Now Playing: a solar transport deck with cyan listening cues. */
|
||||
html[data-layout="bento-hub"] .player-stage { border-radius:16px; border:1px solid var(--line-bright); background:#000; box-shadow:var(--shadow-md); }
|
||||
html[data-layout="bento-hub"] .player-pane.empty .player-stage { border:0; border-radius:0; box-shadow:none; }
|
||||
html[data-layout="bento-hub"] :is(#controls,.now-meta,.notes-panel,.upnext,.related-panel) { flex-shrink:0; padding:16px; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); box-shadow:var(--shadow-sm); }
|
||||
html[data-layout="bento-hub"] #controls { gap:12px; }
|
||||
html[data-layout="bento-hub"] #controls .deck-main { gap:8px; }
|
||||
html[data-layout="bento-hub"] #controls .deck-main .ctrl { width:44px; height:44px; min-width:44px; padding:10px; background:var(--bg-2); color:var(--text); border:1px solid var(--line); border-radius:999px; box-shadow:none; }
|
||||
html[data-layout="bento-hub"] #controls .deck-main .ctrl.play { width:56px; height:56px; padding:14px; background:var(--accent); color:var(--on-accent); border:0; box-shadow:0 6px 18px -4px var(--accent-glow); }
|
||||
html[data-layout="bento-hub"] #controls .deck-main .ctrl.play svg { width:28px; height:28px; }
|
||||
html[data-layout="bento-hub"] #controls .deck-main .ctrl.active { color:var(--accent-sec); border-color:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] :is(.time,.mini-time,.vol-pct,.lyr-time,.marker-time) { font:500 12px var(--mono); color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] #controls :is(.dk-more,.ctrl:not(.play)) { background:var(--bg-2); border-radius:10px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] :is(.dk-more small,.dk-more i,.np-more small) { color:var(--text-2); font-size:12px; letter-spacing:0; }
|
||||
html[data-layout="bento-hub"] .np-title { font:800 22px/1.4 var(--ui); letter-spacing:-.02em; }
|
||||
html[data-layout="bento-hub"] .np-channel { font-size:13px; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .np-actions { gap:8px; }
|
||||
html[data-layout="bento-hub"] .np-btn svg { width:20px; height:20px; stroke-width:1.8; }
|
||||
html[data-layout="bento-hub"] .np-more { background:var(--bg-2); border:1px solid var(--line); border-radius:10px; color:var(--text); }
|
||||
html[data-layout="bento-hub"] #notesPanel { padding:0; overflow:hidden; container-type:inline-size; container-name:bento-notes; }
|
||||
html[data-layout="bento-hub"] .notes-tabs { display:flex; flex-wrap:nowrap; align-items:center; gap:4px; padding:8px; border-bottom:1px solid var(--line); background:var(--bg-1); }
|
||||
html[data-layout="bento-hub"] .notes-tab { display:inline-flex; align-items:center; gap:8px; min-width:44px; min-height:44px; flex:0 1 auto; padding:10px; border:1px solid transparent; border-radius:10px; background:transparent; color:var(--text-2); font:600 12px var(--ui); text-transform:none; letter-spacing:0; white-space:nowrap; }
|
||||
html[data-layout="bento-hub"] .notes-tab.active { background:var(--bg-2); color:var(--accent-sec); border-color:var(--line); }
|
||||
html[data-layout="bento-hub"] .glass-tab-label { overflow:hidden; text-overflow:ellipsis; }
|
||||
html[data-layout="bento-hub"] .notes-status { min-width:0; flex:1 1 0; font-size:11px; }
|
||||
html[data-layout="bento-hub"] .notes-fold { display:grid; place-items:center; min-width:44px; width:44px; height:44px; margin-left:auto; border-radius:10px; background:var(--bg-2); color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .notes-pane { padding:12px; }
|
||||
html[data-layout="bento-hub"] :is(.lyrics-toolbar,.markers-toolbar) { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:12px; }
|
||||
html[data-layout="bento-hub"] .lyrics-list { padding:0; max-height:320px; }
|
||||
html[data-layout="bento-hub"] .lyr-line { padding:12px; border-radius:10px; border-left:3px solid transparent; color:var(--text-2); font:500 14px/1.6 var(--ui); }
|
||||
html[data-layout="bento-hub"] .lyr-line.active { border-left-color:var(--accent-sec); background:color-mix(in srgb,var(--accent-sec) 8%,transparent); color:var(--accent-sec); font-weight:700; }
|
||||
html[data-layout="bento-hub"] :is(.lyr-off-btn,.marker-row,.ts-query,.ts-source) { min-height:44px; border:1px solid var(--line); border-radius:10px; color:var(--text); background:var(--bg-2); }
|
||||
html[data-layout="bento-hub"] .upnext-header { color:var(--text); font:700 16px var(--ui); text-transform:none; letter-spacing:0; }
|
||||
html[data-layout="bento-hub"] .upnext-count { background:var(--bg-2); color:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] .upnext-list { display:flex; flex-direction:column; gap:8px; }
|
||||
html[data-layout="bento-hub"] .upnext-item { min-height:64px; padding:8px; border:1px solid var(--line); border-radius:10px; background:var(--bg-2); }
|
||||
html[data-layout="bento-hub"] .upnext-title { font-size:13px; font-weight:700; }
|
||||
html[data-layout="bento-hub"] .upnext-channel { color:var(--text-2); font-size:12px; }
|
||||
html[data-layout="bento-hub"] .mini-bar { bottom:12px; left:calc(260px + 16px); right:16px; border:1px solid var(--line-bright); border-radius:16px; overflow:hidden; background:var(--bg-glass); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px); box-shadow:var(--shadow-lg); }
|
||||
html[data-layout="bento-hub"] .mini-bar-inner { padding:10px 12px; gap:10px; border:0; background:transparent; backdrop-filter:none; }
|
||||
html[data-layout="bento-hub"] .mini-btn { display:grid; place-items:center; width:44px; height:44px; min-width:44px; border-radius:999px; color:var(--text-2); border:1px solid var(--line); background:var(--bg-2); }
|
||||
html[data-layout="bento-hub"] #miniPlayBtn { background:var(--accent); color:var(--on-accent); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .mini-title { color:var(--text); font:700 13px var(--ui); }
|
||||
html[data-layout="bento-hub"] .mini-channel { color:var(--text-2); font-size:12px; }
|
||||
html[data-layout="bento-hub"] .mini-progress { height:3px; margin:0; background:var(--bg-3); }
|
||||
html[data-layout="bento-hub"] .mini-progress-fill { background:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] :is(.player-pane,.cards) { padding-bottom:var(--bento-bottom-clearance,16px); scroll-padding-bottom:var(--bento-bottom-clearance,16px); }
|
||||
html[data-layout="bento-hub"] .sec-rail,
|
||||
html[data-layout="bento-hub"] body:has(#miniBar:not(.hidden)) .sec-rail { position:fixed; top:auto; right:auto; left:50%; bottom:var(--rail-bottom,160px); transform:translateX(-50%); flex-direction:row; padding:6px; gap:4px; border-radius:999px; border:1px solid var(--line-bright); background:var(--bg-glass); }
|
||||
html[data-layout="bento-hub"] .sec-rail-btn { display:grid; place-items:center; min-width:44px; width:44px; height:44px; border-radius:999px; background:transparent; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .sec-rail-btn.active { color:var(--accent); background:var(--bg-2); box-shadow:none; }
|
||||
html[data-layout="bento-hub"] .sec-rail-label { left:50%; transform:translateX(-50%); color:var(--text); background:var(--bg-2); font:600 12px var(--ui); }
|
||||
@container bento-notes (max-width:460px) { html[data-layout="bento-hub"] .notes-tab[data-tab="markers"] .glass-tab-label { display:none; } }
|
||||
@container bento-notes (max-width:330px) { html[data-layout="bento-hub"] .notes-tab .glass-tab-label { display:none; } }
|
||||
@media(max-width:860px) {
|
||||
html[data-layout="bento-hub"] .mini-bar { left:12px; right:12px; bottom:calc(64px + env(safe-area-inset-bottom) + 8px); }
|
||||
html[data-layout="bento-hub"] .mini-time { display:none; }
|
||||
html[data-layout="bento-hub"] .body { padding-bottom:var(--bento-bottom-clearance,96px); scroll-padding-bottom:var(--bento-bottom-clearance,96px); }
|
||||
html[data-layout="bento-hub"] :is(.player-pane,.cards) { padding-bottom:16px; }
|
||||
html[data-layout="bento-hub"] :is(#controls,.now-meta,.upnext,.related-panel) { padding:12px; }
|
||||
html[data-layout="bento-hub"] .np-title { font-size:18px; }
|
||||
}
|
||||
/* Service stays a full-screen performance surface, with the user's text scale. */
|
||||
html[data-layout="bento-hub"] .service-mode { background:#05070a; color:#f1f5f9; --svc-ink:#f1f5f9; --svc-ink-2:#94a3b8; --svc-cyan:#00f5d4; }
|
||||
html[data-layout="bento-hub"] .service-mode::before { display:none; }
|
||||
html[data-layout="bento-hub"] :is(.svc-dock,.svc-tile,.svc-exit,.svc-font,.svc-wake) { background:#11151c; border-color:rgba(255,255,255,.09); color:#94a3b8; border-radius:10px; }
|
||||
html[data-layout="bento-hub"] .svc-wake.awake { color:#00f5d4; }
|
||||
html[data-layout="bento-hub"] .svc-label { font-size:12px; }
|
||||
html[data-layout="bento-hub"] .service-title { font:800 clamp(26px,3vw,36px)/1.3 var(--ui); color:#f1f5f9; }
|
||||
html[data-layout="bento-hub"] .service-channel { color:#94a3b8; font-size:14px; }
|
||||
html[data-layout="bento-hub"] .service-lyrics .sl-line.active { color:#00f5d4; text-shadow:none; }
|
||||
html[data-layout="bento-hub"] .service-btn { width:56px; height:56px; border:1px solid rgba(255,255,255,.09); border-radius:999px; background:#19202d; color:#f1f5f9; box-shadow:none; }
|
||||
html[data-layout="bento-hub"] .service-btn-play { width:64px; height:64px; background:#ffb703; color:#090b0e; border:0; }
|
||||
html[data-layout="bento-hub"] .svc-progress > i { background:#00f5d4; }
|
||||
html[data-layout="bento-hub"] .service-time { font:500 12px var(--mono); color:#94a3b8; }
|
||||
|
||||
/* Settings, sheets and empty states use the same tile and type system. */
|
||||
html[data-layout="bento-hub"] .settings-browser { padding-top:8px; }
|
||||
html[data-layout="bento-hub"] .settings-search input { min-height:44px; padding:12px 16px; border-radius:999px; border-color:var(--line); background:var(--bg-2); font-size:14px; }
|
||||
html[data-layout="bento-hub"] .settings-overview h3 { margin:20px 0 10px; font:700 13px var(--ui); letter-spacing:0; color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .settings-section-row { padding:14px; gap:12px; min-height:72px; margin-bottom:10px; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); }
|
||||
html[data-layout="bento-hub"] .settings-section-row svg { width:24px; height:24px; color:var(--accent); stroke-width:1.8; }
|
||||
html[data-layout="bento-hub"] .settings-section-row b { font:700 14px var(--ui); }
|
||||
html[data-layout="bento-hub"] .settings-section-row small { color:var(--text-2); font-size:12px; }
|
||||
html[data-layout="bento-hub"] .settings-section-row[aria-current="true"] { background:var(--bg-2); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .settings-back { border-radius:10px; background:var(--bg-2); color:var(--text); font-size:13px; }
|
||||
html[data-layout="bento-hub"] .set-group { padding:16px; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); margin-bottom:16px; }
|
||||
html[data-layout="bento-hub"] .set-group-title { padding:0 0 12px; font:700 16px var(--ui); color:var(--text); text-transform:none; letter-spacing:0; }
|
||||
html[data-layout="bento-hub"] .set-row { padding:14px 0; gap:12px; border-color:var(--line); }
|
||||
html[data-layout="bento-hub"] .set-row > span { font-size:14px; }
|
||||
html[data-layout="bento-hub"] .set-row small { color:var(--text-2); font-size:12px; line-height:1.6; }
|
||||
html[data-layout="bento-hub"] :is(.set-select,.set-input,.rename-input,.ts-query,.ts-source,.modal input,.modal textarea,.modal select) { min-height:44px; border:1px solid var(--line); border-radius:10px; background:var(--bg-2); color:var(--text); font-size:14px; box-shadow:none; }
|
||||
html[data-layout="bento-hub"] input[type="range"] { accent-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] input[type="checkbox"] { accent-color:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] .download-job { padding:16px; border:1px solid var(--line); border-radius:10px; background:var(--bg-2); margin:12px 0; }
|
||||
html[data-layout="bento-hub"] .download-job b { font-size:13px; }
|
||||
html[data-layout="bento-hub"] .download-job small { color:var(--text-2); font-size:12px; }
|
||||
html[data-layout="bento-hub"] .download-job progress { height:6px; accent-color:var(--accent-sec); }
|
||||
html[data-layout="bento-hub"] .modal-backdrop { background:rgba(0,0,0,.75); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); }
|
||||
html[data-layout="bento-hub"] .modal { padding:24px; border:1px solid var(--line-bright); border-radius:16px; background:var(--bg-1); box-shadow:var(--shadow-lg); }
|
||||
html[data-layout="bento-hub"] .modal h3 { font:800 22px/1.3 var(--ui); color:var(--text); }
|
||||
html[data-layout="bento-hub"] :is(.modal p,.modal-subhead,.modal-char-count) { color:var(--text-2); font-size:13px; line-height:1.6; }
|
||||
html[data-layout="bento-hub"] :is(.modal-list button,.modal-actions button,.sp-chip,.set-actions button,.piano-actions button,.setlist-import button) { min-height:44px; padding:10px 14px; border:1px solid var(--line); border-radius:10px; background:var(--bg-2); color:var(--text); font-size:13px; }
|
||||
html[data-layout="bento-hub"] .modal-actions .primary-btn { background:var(--accent); color:var(--on-accent); border-color:var(--accent); }
|
||||
html[data-layout="bento-hub"] .modal-actions .danger { color:var(--danger-text); background:var(--danger-bg); border-color:transparent; }
|
||||
html[data-layout="bento-hub"] .sp-chip.on { color:var(--on-accent); background:var(--accent); }
|
||||
html[data-layout="bento-hub"] .toast { border:1px solid var(--line-bright); border-radius:10px; background:var(--bg-2); color:var(--text); box-shadow:var(--shadow-md); font-size:13px; }
|
||||
html[data-layout="bento-hub"] .empty-state { width:100%; max-width:440px; padding:32px 24px; margin:24px auto; border:1px solid var(--line); border-radius:16px; background:var(--bg-1); }
|
||||
html[data-layout="bento-hub"] .empty-icon { display:grid; place-items:center; width:56px; height:56px; border:1px solid var(--line); border-radius:999px; background:var(--bg-2); color:var(--accent-sec); box-shadow:none; transform:none; }
|
||||
html[data-layout="bento-hub"] .empty-icon svg { width:24px; height:24px; }
|
||||
html[data-layout="bento-hub"] .empty-title { font:800 18px/1.3 var(--ui); color:var(--text); }
|
||||
html[data-layout="bento-hub"] :is(.empty-desc,.dl-how-to-save,.dl-storage-note) { font:400 13px/1.6 var(--ui); color:var(--text-2); }
|
||||
html[data-layout="bento-hub"] .empty-cta { min-height:44px; padding:10px 18px; border:0; border-radius:10px; background:var(--accent); color:var(--on-accent); font-weight:700; }
|
||||
html[data-layout="bento-hub"] .status { border-radius:10px; color:var(--text-2); font-size:14px; }
|
||||
@media(max-width:860px) {
|
||||
html[data-layout="bento-hub"] .modal { padding:20px 16px; }
|
||||
html[data-layout="bento-hub"] .modal h3 { font-size:18px; }
|
||||
html[data-layout="bento-hub"] .set-row { flex-wrap:wrap; }
|
||||
html[data-layout="bento-hub"] .set-row > span { flex:1 1 180px; }
|
||||
html[data-layout="bento-hub"] .set-row :is(select,input[type="text"],input[type="password"]) { max-width:100%; }
|
||||
html[data-layout="bento-hub"] .empty-state { padding:24px 16px; }
|
||||
}
|
||||
/* Browsing is a full workspace; playback continues in the floating mini player. */
|
||||
html[data-layout="bento-hub"] .body:has(#listPane:not(.hidden) .cards:is([data-view="search"],[data-view="channel"],[data-view="library"],[data-view="settings"],[data-view="queue"],[data-view="history"],[data-view="saved"],[data-view="downloads"])) .player-pane { display:none; }
|
||||
html[data-layout="bento-hub"] .body:has(.cards:is([data-view="search"],[data-view="channel"],[data-view="library"],[data-view="settings"],[data-view="queue"],[data-view="history"],[data-view="saved"],[data-view="downloads"])) .list-pane { flex:1; width:100%; border-left:0; }
|
||||
@media(min-width:900px) {
|
||||
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)); }
|
||||
}
|
||||
|
||||
/* 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; }
|
||||
2
playwright.bento.config.js
Normal file
2
playwright.bento.config.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
module.exports = defineConfig({ testDir:'./tests', testMatch:/bento-hub\.spec\.js/, timeout:30000, workers:2, projects:[{name:'chromium',use:{browserName:'chromium'}},{name:'webkit',use:{browserName:'webkit'}}], use:{baseURL:'http://localhost:8105',serviceWorkers:'block'}, webServer:{command:`npx serve ${process.env.BENTO_BASELINE || 'frontend'} -l 8105 --no-clipboard`,port:8105} });
|
||||
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});}
|
||||
102
tests/bento-hub.spec.js
Normal file
102
tests/bento-hub.spec.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const {test,expect}=require('@playwright/test');
|
||||
const {openClassic,showPlayingFixture}=require('./helpers/classic-fixture');
|
||||
const {mkdirSync,readFileSync,existsSync}=require('node:fs');
|
||||
const {join}=require('node:path');
|
||||
const {homedir}=require('node:os');
|
||||
const {differentPixels}=require('./helpers/png-diff');
|
||||
const out=process.env.BENTO_SCREENSHOT_DIR||join(homedir(),'deliverables','ytplayer-done13');mkdirSync(out,{recursive:true});
|
||||
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','sheet','stage-lyrics','batch','library','empty-queue','empty-saved','empty-downloads'];
|
||||
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 page.evaluate(({layout,screen})=>{
|
||||
data.settings.layout=layout;applyAppearance();
|
||||
const videos=data.playlists[0].videos;
|
||||
API.cacheList=async()=>({ok:true,items:screen==='empty-saved'?[]:videos.map(v=>({...v,size:84000000})),total:screen==='empty-saved'?0:1008000000});
|
||||
Downloads.configure({slots:()=>4,files:async()=>[],playlists:()=>data.playlists,jobs:()=>!['downloads','settings-detail'].includes(screen)?[]:['active','queued','paused','failed'].map((status,i)=>({...videos[i],status,error:status==='failed'?'ERROR: [youtube] Video unavailable':null,progress:status==='active'?{received:24000000,total:84000000}:null})),action:()=>{}});
|
||||
channelData={name:'Preview fixtures',url:'https://www.youtube.com/@preview',key:'preview',results:videos,loading:false};
|
||||
searchResults=screen==='home'?[]:videos; searchAttempted=screen!=='home';
|
||||
if(screen==='empty-queue'){queue=[];data.queue=[];}
|
||||
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(['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'};
|
||||
render();renderUpNext();
|
||||
if(screen!=='home')showMiniBar();
|
||||
if(['lyrics','chapters','lyric-search'].includes(screen)){
|
||||
$('notesPanel').classList.remove('hidden');
|
||||
const tab=screen==='lyrics'?'lyrics':screen==='chapters'?'markers':'search';
|
||||
document.querySelectorAll('.notes-tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));
|
||||
['lyrics','markers','search'].forEach(t=>$(t+'Pane').classList.toggle('hidden',t!==tab));
|
||||
$('lyricsList').innerHTML='<div class="lyr-line"><span class="lyr-text">Because You are God</span></div><div class="lyr-line active"><span class="lyr-text">You can do anything<br>You can do anything</span></div><div class="lyr-line"><span class="lyr-text">Your grace is enough</span></div>';
|
||||
$('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==='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}]);
|
||||
},{layout,screen});
|
||||
if(screen==='settings-detail')await page.getByRole('button',{name:/Downloads & storage/}).click();
|
||||
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);
|
||||
// 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)=>{
|
||||
await page.setViewportSize({width,height:844});await scene(page,layout,screen);
|
||||
const name=`${layout}-${width}-${screen}-${info.project.name}.png`;
|
||||
const buffer=await page.screenshot({path:`${out}/${before?'before':'after'}-${name}`});
|
||||
if(before)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);
|
||||
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);
|
||||
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)=>{
|
||||
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.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};
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
module.exports={differentPixels};
|
||||
|
||||
Reference in New Issue
Block a user