Line up the feed sidebar

Every row reserves the chevron slot, so artwork and titles share one
column instead of stepping left when a feed has no children. Children
keep a single icon size and read as nested from the indent alone. Labels
stack on one line-height, and the unread count has a min-width so a
three-digit feed doesn't shove its own title.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 18:39:25 +00:00
parent 470f3e1ff1
commit dfcdf47143
3 changed files with 25 additions and 11 deletions

View File

@@ -56,6 +56,20 @@ and until now nothing set them.
--- ---
## 2026-09-10 — Sidebar alignment
The feed list had four different left edges: a row with no children skipped the chevron entirely, so
its artwork sat a chevron-width left of a folder's; children then used a different indent *and* a
smaller icon. Nothing lined up with anything.
Every row now reserves the chevron slot whether or not it opens (an empty one is
`pointer-events:none`, so the click falls through to the row), all artwork is one size, and nesting
reads from the indent alone. Both label lines are `display:block` on a shared line-height instead of
an inline baseline, and the unread count has a `min-width` so three-digit feeds don't shove the
title. Rows came out shorter, so more feeds fit without scrolling.
---
## 2026-09-10 — An item's picture ## 2026-09-10 — An item's picture
An item's artwork now resolves in order of how deliberate the source is: `itunes:image`, then Media An item's artwork now resolves in order of how deliberate the source is: `itunes:image`, then Media

View File

@@ -174,7 +174,8 @@ test('an OPML subscription is a collapsible folder', async ({ page }) => {
body: JSON.stringify({ feed: 'test-subscriptions', force: true }), body: JSON.stringify({ feed: 'test-subscriptions', force: true }),
})); }));
const chev = page.locator('.feed .chev'); // Every row reserves the chevron slot for alignment; only a folder's is clickable.
const chev = page.locator('.feed.group .chev');
await expect(chev).toBeVisible({ timeout: 20_000 }); await expect(chev).toBeVisible({ timeout: 20_000 });
// Closed by default: the child is not listed until the folder is opened. // Closed by default: the child is not listed until the folder is opened.

View File

@@ -92,13 +92,13 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
} }
.feed:hover{background:var(--panel2)} .feed:hover{background:var(--panel2)}
.feed.sel{background:var(--raise)} .feed.sel{background:var(--raise)}
.feed.child{margin-left:14px} .feed.child{margin-left:16px}
.feed.child .art{width:28px;height:28px;font-size:11px}
.chev{ .chev{
flex:none;width:18px;height:18px;display:grid;place-items:center;border-radius:4px; flex:none;width:18px;height:18px;display:grid;place-items:center;border-radius:4px;
color:var(--faint);font-size:11px;transition:transform .12s; color:var(--faint);font-size:11px;transition:transform .12s;
} }
.chev:hover{background:var(--raise);color:var(--fg)} .chev:empty{pointer-events:none}
.chev:not(:empty):hover{background:var(--raise);color:var(--fg)}
.chev.open{transform:rotate(90deg)} .chev.open{transform:rotate(90deg)}
.childlist{display:grid;gap:4px;margin-top:10px} .childlist{display:grid;gap:4px;margin-top:10px}
.childrow{ .childrow{
@@ -117,13 +117,13 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
border-radius:7px;object-fit:cover;background:var(--raise);flex:none; border-radius:7px;object-fit:cover;background:var(--raise);flex:none;
display:grid;place-items:center;color:var(--faint);font-weight:700;overflow:hidden; display:grid;place-items:center;color:var(--faint);font-weight:700;overflow:hidden;
} }
.feed .art{width:38px;height:38px;font-size:14px} .feed .art{width:34px;height:34px;font-size:13px}
.feed .txt{min-width:0;flex:1} .feed .txt{min-width:0;flex:1}
.feed .txt b{display:block;font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .feed .txt b{display:block;font-weight:600;font-size:13.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.feed .txt small{color:var(--faint);font-size:11.5px} .feed .txt small{display:block;color:var(--faint);font-size:11.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.badge{ .badge{
background:var(--accent);color:var(--ink);border-radius:20px;padding:1px 7px; background:var(--accent);color:var(--ink);border-radius:20px;padding:1px 7px;
font-size:11px;font-weight:700;flex:none; font-size:11px;font-weight:700;flex:none;min-width:26px;text-align:center;
} }
.badge.zero{background:var(--raise);color:var(--faint)} .badge.zero{background:var(--raise);color:var(--faint)}
@@ -461,7 +461,7 @@ function renderFeeds(){
el.className='feed'+(S.feed===f.id?' sel':'')+(depth?' child':'')+(kids?' group':''); el.className='feed'+(S.feed===f.id?' sel':'')+(depth?' child':'')+(kids?' group':'');
const open = kids && (expanded.has(f.id) || q); const open = kids && (expanded.has(f.id) || q);
el.innerHTML = el.innerHTML =
(kids?`<span class="chev${open?' open':''}" title="Show or hide the feeds inside">▶</span>`:'')+ `<span class="chev${open?' open':''}"${kids?' title="Show or hide the feeds inside"':''}>${kids?'▶':''}</span>`+
artHTML(f.image,f.title||f.id)+ artHTML(f.image,f.title||f.id)+
`<div class="txt"><b>${esc(f.title||f.id)}</b><small>`+ `<div class="txt"><b>${esc(f.title||f.id)}</b><small>`+
(kids?`${kids} feed${kids===1?'':'s'}${f.unread?` · ${f.unread} unread`:''}` (kids?`${kids} feed${kids===1?'':'s'}${f.unread?` · ${f.unread} unread`:''}`
@@ -470,8 +470,7 @@ function renderFeeds(){
(f.orphaned?'<span class="tag" title="No longer listed in the OPML, kept because it has downloads">gone</span>':'')+ (f.orphaned?'<span class="tag" title="No longer listed in the OPML, kept because it has downloads">gone</span>':'')+
`<span class="badge${f.unread?'':' zero'}">${f.unread}</span>`; `<span class="badge${f.unread?'':' zero'}">${f.unread}</span>`;
el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); }; el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); };
const chev=$('.chev',el); if(kids) $('.chev',el).onclick=ev=>{ ev.stopPropagation(); toggleGroup(f.id); };
if(chev) chev.onclick=ev=>{ ev.stopPropagation(); toggleGroup(f.id); };
list.appendChild(el); list.appendChild(el);
} }
} }