Make the UI work on a phone
The ☰ button lived in the player bar, which is hidden until something plays, so the feed list was unreachable on a phone. It moves to a bar that is always present, and the sidebar gets a scrim. The reading pane takes the whole screen over the list with a back button, the player stacks into two rows above it, and the page no longer scrolls sideways -- a grid column is min-content wide by default, so one long headline dragged everything off the right edge. Covered by a Playwright case at 390x844. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -58,7 +58,7 @@ a{color:var(--accent)}
|
||||
::-webkit-scrollbar-track{background:transparent}
|
||||
|
||||
/* ---------- shell ---------- */
|
||||
#shell{display:grid;grid-template-columns:290px 1fr;min-height:0;overflow:hidden}
|
||||
#shell{display:grid;grid-template-columns:290px 1fr;min-height:0;min-width:0;overflow:hidden}
|
||||
#sidebar{
|
||||
background:var(--panel);border-right:1px solid var(--line);
|
||||
display:flex;flex-direction:column;min-height:0;
|
||||
@@ -100,7 +100,7 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||||
.chev:empty{pointer-events:none}
|
||||
.chev:not(:empty):hover{background:var(--raise);color:var(--fg)}
|
||||
.chev.open{transform:rotate(90deg)}
|
||||
.childlist{display:grid;gap:4px;margin-top:10px}
|
||||
.childlist{display:grid;grid-template-columns:minmax(0,1fr);gap:4px;margin-top:10px}
|
||||
.childrow{
|
||||
display:flex;gap:10px;align-items:center;padding:7px 9px;border:1px solid var(--line);
|
||||
border-radius:9px;cursor:pointer;background:var(--panel);
|
||||
@@ -128,10 +128,10 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||||
.badge.zero{background:var(--raise);color:var(--faint)}
|
||||
|
||||
/* ---------- main ---------- */
|
||||
#main{overflow:hidden;min-height:0;display:flex;flex-direction:column}
|
||||
.wrap{flex:1;min-height:0;display:flex;flex-direction:column}
|
||||
#main{overflow:hidden;min-height:0;min-width:0;display:flex;flex-direction:column}
|
||||
.wrap{flex:1;min-height:0;min-width:0;display:flex;flex-direction:column}
|
||||
.wrap.plain{overflow-y:auto;padding:18px 22px 40px;display:block}
|
||||
#content{display:flex;flex-direction:column;min-height:0;flex:1}
|
||||
#content{display:flex;flex-direction:column;min-height:0;min-width:0;flex:1}
|
||||
#content>.fhead,#content>.toolbar{padding-left:20px;padding-right:20px;flex:none}
|
||||
#content>.fhead{padding-top:16px}
|
||||
|
||||
@@ -141,7 +141,7 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||||
#grab{cursor:row-resize;background:var(--line)}
|
||||
#grab:hover{background:var(--accent)}
|
||||
#detail{overflow-y:auto;padding:16px 20px 28px;background:var(--panel);border-top:1px solid var(--line)}
|
||||
.dt{font-size:18px;font-weight:650;margin:0 0 5px;line-height:1.3}
|
||||
.dt{font-size:18px;font-weight:650;margin:0 0 5px;line-height:1.3;overflow-wrap:anywhere}
|
||||
.dmeta{color:var(--faint);font-size:12.5px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
|
||||
.dmeta .btn{padding:3px 9px;font-size:12px}
|
||||
.dbody{font-size:14.5px;line-height:1.65;overflow-wrap:anywhere;color:var(--fg)}
|
||||
@@ -156,7 +156,7 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||||
.fhead{display:flex;gap:18px;margin-bottom:18px}
|
||||
.fhead .art{width:118px;height:118px;font-size:34px;box-shadow:var(--shadow)}
|
||||
.fhead .meta{min-width:0;flex:1;display:flex;flex-direction:column}
|
||||
.fhead h2{margin:0 0 3px;font-size:23px;line-height:1.2}
|
||||
.fhead h2{margin:0 0 3px;font-size:23px;line-height:1.2;overflow-wrap:anywhere}
|
||||
.fhead .sub{color:var(--dim);font-size:13px;margin-bottom:8px}
|
||||
.fhead .sub a{color:var(--dim);text-decoration:none}
|
||||
.fhead .sub a:hover{color:var(--accent)}
|
||||
@@ -289,18 +289,55 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
|
||||
}
|
||||
.toast.bad{border-color:var(--bad);color:var(--bad)}
|
||||
@keyframes in{from{opacity:0;transform:translateY(6px)}}
|
||||
#burger{display:none}
|
||||
#mbar,#dback{display:none}
|
||||
|
||||
@media (max-width:820px){
|
||||
#shell{grid-template-columns:1fr}
|
||||
#sidebar{position:fixed;inset:0 auto 0 0;width:280px;z-index:40;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow)}
|
||||
#sidebar{position:fixed;inset:0 auto 0 0;width:min(300px,86vw);z-index:40;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow)}
|
||||
#sidebar.open{transform:none}
|
||||
#burger{display:grid}
|
||||
.fhead .art{width:78px;height:78px;font-size:24px}
|
||||
.fhead h2{font-size:19px}
|
||||
#pnow{width:auto;flex:1}
|
||||
#scrim{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:41}
|
||||
#sidebar{z-index:42}
|
||||
#player{position:relative;z-index:39;padding:7px 10px;gap:8px}
|
||||
|
||||
/* The feed list is reachable whether or not anything is playing. */
|
||||
#mbar{
|
||||
display:flex;align-items:center;gap:8px;flex:none;padding:7px 10px;
|
||||
border-bottom:1px solid var(--line);background:var(--panel);font-weight:650;
|
||||
}
|
||||
.iconbtn{width:38px;height:38px}
|
||||
|
||||
/* One pane at a time: the list, then the item over it. */
|
||||
#split{grid-template-rows:1fr}
|
||||
#grab{display:none}
|
||||
#detail{position:fixed;inset:0;z-index:38;display:none;border-top:0;padding:12px 16px 90px}
|
||||
body.reading #detail{display:block}
|
||||
#dback{display:inline-block;margin-bottom:10px}
|
||||
|
||||
#content>.fhead,#content>.toolbar{padding-left:14px;padding-right:14px}
|
||||
#content>.fhead{padding-top:12px}
|
||||
.fhead{gap:12px;margin-bottom:12px}
|
||||
.fhead .art{width:64px;height:64px;font-size:20px}
|
||||
.fhead h2{font-size:18px}
|
||||
.fhead .sub{font-size:12px;margin-bottom:6px}
|
||||
.acts{gap:6px}
|
||||
.acts .btn{padding:7px 10px;font-size:12.5px}
|
||||
.toolbar{gap:8px}
|
||||
#list{padding:0 12px 10px}
|
||||
.ep .art{width:44px;height:44px;font-size:14px}
|
||||
#player{grid-template-columns:1fr auto;grid-template-areas:"now right" "mid mid";gap:2px 8px}
|
||||
#pnow{grid-area:now;width:auto}
|
||||
#pright{grid-area:right}
|
||||
#pmid{grid-area:mid}
|
||||
#pnow .art{width:38px;height:38px}
|
||||
#pnow .txt small,#pright #vol{display:none}
|
||||
.wrap{padding:14px}
|
||||
.wrap.plain{padding:14px}
|
||||
.card{padding:16px}
|
||||
|
||||
/* A log line has no room for four columns: keep time and message, wrap as prose. */
|
||||
#logbox{font-size:11.5px;padding:8px}
|
||||
#logbox .l{flex-wrap:wrap;gap:6px}
|
||||
#logbox .lv{width:auto}
|
||||
#logbox .tg{display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -321,12 +358,18 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
|
||||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||||
<div id="feedlist"></div>
|
||||
</aside>
|
||||
<div id="main"><div class="wrap" id="content"></div></div>
|
||||
<div id="main">
|
||||
<div id="mbar">
|
||||
<button class="iconbtn" id="burger" title="Feeds">☰</button>
|
||||
<span>iPodderX</span>
|
||||
</div>
|
||||
<div class="wrap" id="content"></div>
|
||||
</div>
|
||||
<div id="scrim" hidden></div>
|
||||
</div>
|
||||
|
||||
<div id="player">
|
||||
<div id="pnow">
|
||||
<button class="iconbtn" id="burger" title="Feeds">☰</button>
|
||||
<div id="partwrap"></div>
|
||||
<div class="txt"><b id="ptitle"></b><small id="pfeed"></small></div>
|
||||
</div>
|
||||
@@ -425,6 +468,9 @@ function artHTML(url,name,cls){
|
||||
: `<div class="art ${cls||''}">${esc(initials(name))}</div>`;
|
||||
}
|
||||
|
||||
/// The sidebar slides over the page on a phone, so it needs a scrim to tap away.
|
||||
function nav(on){ $('#sidebar').classList.toggle('open',on); $('#scrim').hidden=!on; }
|
||||
|
||||
/* ---------------- state ---------------- */
|
||||
const S = {
|
||||
feeds:[], feed:null, entries:[], total:0, offset:0, limit:50,
|
||||
@@ -476,7 +522,7 @@ function renderFeeds(){
|
||||
`</small></div>`+
|
||||
(f.orphaned?'<span class="tag" title="No longer listed in the OPML, kept because it has downloads">gone</span>':'')+
|
||||
`<span class="badge${unread?'':' zero'}" title="${unread} unread">${unread>999?'999+':unread}</span>`;
|
||||
el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); };
|
||||
el.onclick=()=>{ selectFeed(f.id); nav(false); };
|
||||
if(kids) $('.chev',el).onclick=ev=>{ ev.stopPropagation(); toggleGroup(f.id); };
|
||||
list.appendChild(el);
|
||||
}
|
||||
@@ -735,10 +781,12 @@ function dragSplit(){
|
||||
/// The item's text and its enclosures, in the pane below the list.
|
||||
function showDetail(e){
|
||||
const box=$('#detail'); if(!box) return;
|
||||
document.body.classList.toggle('reading',!!e);
|
||||
if(!e){ box.innerHTML='<p class="empty">Pick an episode to read it.</p>'; return; }
|
||||
const f=S.feeds.find(x=>x.id===e.feed_id);
|
||||
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
|
||||
box.innerHTML=`
|
||||
<button class="btn" id="dback">← Episodes</button>
|
||||
<h3 class="dt">${esc(e.title||'(untitled)')}</h3>
|
||||
<div class="dmeta">
|
||||
${f?`<span>${esc(f.title||f.id)}</span><span class="dot"></span>`:''}
|
||||
@@ -752,6 +800,7 @@ function showDetail(e){
|
||||
<div class="dbody">${(e.description&&e.description.trim())||'<em>No show notes.</em>'}</div>
|
||||
${e.enclosures.map(encBox).join('')}`;
|
||||
// description was sanitized server-side with ammonia before it ever reached here
|
||||
$('#dback').onclick=()=>showDetail(null);
|
||||
$$('button[data-a]',box).forEach(b=>
|
||||
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
|
||||
for(const x of e.enclosures){
|
||||
@@ -908,7 +957,7 @@ document.addEventListener('keydown',ev=>{
|
||||
else if(ev.key==='ArrowLeft'&&player.guid){audio.currentTime-=15}
|
||||
else if(ev.key==='ArrowRight'&&player.guid){audio.currentTime+=30}
|
||||
else if(ev.key==='/'){ev.preventDefault();$('#epSearch')?.focus()}
|
||||
else if(ev.key==='Escape'){closeModal();$('#sidebar').classList.remove('open')}
|
||||
else if(ev.key==='Escape'){closeModal();nav(false)}
|
||||
});
|
||||
|
||||
/* ---------------- modals ---------------- */
|
||||
@@ -1232,7 +1281,8 @@ $('#scanAll').onclick=async()=>{ toast('Scanning all feeds…'); await api('/api
|
||||
on('#prefs','onclick',prefsModal);
|
||||
on('#logs','onclick',logsModal);
|
||||
$('#feedFilter').oninput=renderFeeds;
|
||||
$('#burger').onclick=()=>$('#sidebar').classList.toggle('open');
|
||||
$('#burger').onclick=()=>nav(!$('#sidebar').classList.contains('open'));
|
||||
$('#scrim').onclick=()=>nav(false);
|
||||
$('#theme').onclick=()=>{
|
||||
const cur=document.documentElement.dataset.theme==='light'?'dark':'light';
|
||||
document.documentElement.dataset.theme=cur; localStorage.setItem('ipx.theme',cur);
|
||||
|
||||
Reference in New Issue
Block a user