From ae47e31a97892071d1cb8ed41cda77058ef4c7c1 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 11 Sep 2026 15:00:30 +0000 Subject: [PATCH] Classic theme after the 2004 Mac app; header and dialog buttons as icons - A Classic theme beside Dark and Light: brushed-metal toolbar and status bar, a pale blue-grey source list, Aqua blue for whatever is selected, red unread badges, a striped table with blue titles, a blue bar behind the item's title, and Lucida Grande. The theme button steps through all three and remembers the choice. - The feed and OPML headers' buttons (scan, download latest, mark all read, settings, unsubscribe) and the Settings, feed settings and Download latest dialogs' buttons (save, download, cancel) are icons, with the words in title and aria-label. - Tests: the theme button reaches Classic and it survives a reload; the header buttons are found by action. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn --- CHANGELOG.md | 9 +++- tests/ui/app.spec.js | 16 +++++- web/index.html | 115 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 116 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6561c97..a4370e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ The long form, with what was wrong before and how it was found, is in ### Added +- A Classic theme after the 2004 Mac app, beside Dark and Light: brushed-metal toolbar, Aqua + blue selection, red unread badges, a striped table and Lucida Grande. The theme button steps + through all three and remembers the choice. - A toolbar across the top, after the original iPodderX: add and unsubscribe, play, mark read and keep for the selected item, scan, a search box for what is showing, and Settings and Log. - Directory, Popular and All Subscriptions at the top of the feed list, opening in the main pane. @@ -28,8 +31,10 @@ The long form, with what was wrong before and how it was found, is in items show without pressing Scan. - A file deleted to save space, or by hand, looks as if it was never downloaded: no "reaped" label, just the Download button. The retention summary says "deleted", not "reaped". -- The Files pane and an item's own buttons are icons, with the words in their tooltips: save, - delete, view, download, mark read, keep, and open the original. +- Buttons are icons, with the words in their tooltips: the Files pane (save, delete, view, + download), an item's own buttons (mark read, keep, open the original), the feed header (scan, + download latest, mark all read, settings, unsubscribe), and the Settings, feed settings and + Download latest dialogs (save, download, cancel). ### Security diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index f23e1d7..19de1a0 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -29,6 +29,18 @@ test('the theme toggle actually changes the theme', async ({ page }) => { .not.toBe(before); }); +test('the theme button steps through dark, light and classic, and remembers', async ({ page }) => { + const theme = () => page.evaluate(() => document.documentElement.dataset.theme); + for (let i = 0; i < 3 && (await theme()) !== 'classic'; i++) await page.locator('#theme').click(); + expect(await theme()).toBe('classic'); + await expect(page.locator('#theme')).toHaveAttribute('title', /Classic.*Click for Dark/); + + await page.reload(); + await expect.poll(theme).toBe('classic'); + // The 2004 Mac app set its type in Lucida Grande. + expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande'); +}); + test('settings opens and saves the global schedule', async ({ page }) => { await page.locator('#prefs').click(); await expect(page.locator('#modal.on')).toBeVisible(); @@ -136,7 +148,7 @@ test('the filter tabs change what is listed', async ({ page }) => { test('a feed URL is editable and has a copy button', async ({ page }) => { await page.getByText('Test Show').click(); - await page.locator('.btn', { hasText: 'Settings' }).first().click(); + await page.locator('#content .acts [data-a="settings"]').click(); await expect(page.locator('#surl')).toHaveValue(/show\.xml/); await expect(page.locator('#scopy')).toBeVisible(); @@ -239,7 +251,7 @@ test('marking an OPML subscription read covers the feeds inside it', async ({ pa await expect(folder.locator('.badge')).not.toHaveText('0'); await folder.click(); - await page.locator('#content .acts button', { hasText: 'Mark all read' }).click(); + await page.locator('#content .acts [data-a="read"]').click(); await expect(folder.locator('.badge')).toHaveText('0'); }); diff --git a/web/index.html b/web/index.html index 5b94539..9d6f933 100644 --- a/web/index.html +++ b/web/index.html @@ -44,6 +44,26 @@ --bad:#b3402f; --shadow:0 8px 28px rgba(45,83,145,.14); } +/* Classic: the 2004 Mac app. Colours here; the chrome it needs is at the end of the sheet. */ +:root[data-theme="classic"] { + color-scheme:light; + --bg:#ffffff; + --panel:#e7ebf1; /* the source list's pale blue-grey */ + --panel2:#f2f2f2; + --raise:#d5dce7; + --line:#a9a9a9; + --fg:#000000; + --dim:#444444; + --faint:#666666; + --accent:#3875d7; /* Aqua selection blue */ + --accent2:#2a5db0; + --ink:#ffffff; + --good:#237a23; + --warn:#a15f00; + --bad:#c42b1c; + --shadow:0 4px 16px rgba(0,0,0,.28); + --r:5px; +} *{box-sizing:border-box} /* A rule that sets display beats the UA's [hidden], and several below do. */ [hidden]{display:none!important} @@ -427,6 +447,54 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu #logbox .lv{width:auto} #logbox .tg{display:none} } + +/* ---------- Classic: the 2004 Mac app ---------- */ +/* After the iPodderX screenshots: brushed-metal chrome, a pale blue-grey source list, Aqua blue + for whatever is selected, red unread badges, a striped table and Lucida Grande. */ +:root[data-theme="classic"] body{ + font-family:"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana,sans-serif;font-size:13px; +} +:root[data-theme="classic"] #topbar, +:root[data-theme="classic"] #status, +:root[data-theme="classic"] #player{background:linear-gradient(#ececec,#c9c9c9);border-color:#8e8e8e;color:#1a1a1a} +:root[data-theme="classic"] #status{text-align:center} +:root[data-theme="classic"] #sidebar{background:#e7ebf1;border-right-color:#a3a3a3} +:root[data-theme="classic"] #files{background:#f4f4f4} +:root[data-theme="classic"] .tgroup{background:none;border-color:#8e8e8e;border-radius:6px} +:root[data-theme="classic"] .tgroup button+button{border-left-color:#8e8e8e} +:root[data-theme="classic"] .tgroup button, +:root[data-theme="classic"] .btn, +:root[data-theme="classic"] .tabs{background:linear-gradient(#ffffff,#d9d9d9);border-color:#8e8e8e;color:#1a1a1a} +:root[data-theme="classic"] .tabs{border:1px solid #8e8e8e;border-radius:6px} +:root[data-theme="classic"] .tgroup button:hover:not(:disabled), +:root[data-theme="classic"] .btn:hover{background:linear-gradient(#f5f9ff,#cbdbf4)} +:root[data-theme="classic"] .btn.primary, +:root[data-theme="classic"] .tabs button.on, +:root[data-theme="classic"] .feed.sel, +:root[data-theme="classic"] .place.sel{background:linear-gradient(#86b0ec,#3a73cf);border-color:#2d5eae;color:#fff} +:root[data-theme="classic"] .feed.sel .txt small, +:root[data-theme="classic"] .place.sel .ico{color:#e6eeff} +:root[data-theme="classic"] #epSearch{border-radius:14px} +:root[data-theme="classic"] .badge{background:#c9302c;color:#fff} +:root[data-theme="classic"] .badge.zero{background:#b4bdc9;color:#fff} +:root[data-theme="classic"] .ephead, +:root[data-theme="classic"] .fhd{ + background:linear-gradient(#fbfbfb,#dcdcdc);color:#1a1a1a;text-transform:none;letter-spacing:0;font-size:11px; +} +:root[data-theme="classic"] .fhd{padding:3px 6px;border:1px solid #c2c2c2} +:root[data-theme="classic"] .ephead{border-bottom-color:#a3a3a3} +:root[data-theme="classic"] .ep{border-radius:0;margin:0} +:root[data-theme="classic"] #eps .ep:nth-child(even){background:#edf3fe} +:root[data-theme="classic"] .ep .t{color:#1c4fa8} +:root[data-theme="classic"] .ep.read .t{color:#333} +:root[data-theme="classic"] #eps .ep.sel{background:#3875d7;border-color:#3875d7} +:root[data-theme="classic"] .ep.sel .t, +:root[data-theme="classic"] .ep.sel .fd, +:root[data-theme="classic"] .ep.sel .date, +:root[data-theme="classic"] .ep.sel .line, +:root[data-theme="classic"] .ep.sel .st, +:root[data-theme="classic"] .ep.sel .fl{color:#fff} +:root[data-theme="classic"] .dt{background:linear-gradient(#80aae6,#3f78cf);color:#fff;padding:6px 12px;border-radius:4px} @@ -692,11 +760,11 @@ function renderFeed(){ ${f.group?`
From the OPML subscription ${esc(f.group)}
`:''}
- - - - - + + + + +
` : `
@@ -748,10 +816,10 @@ function renderGroup(f,kids){ ${gone?`
${gone} feed${gone===1?' is':'s are'} no longer listed in this OPML but kept because ${gone===1?'it has':'they have'} downloads.
`:''}
- - - - + + + +
@@ -1442,8 +1510,8 @@ async function prefsModal(){
Add and remove the people who can sign in, and choose who is an admin.
-
-
`); +
+
`); $('#gopml').onclick=opmlModal; $('#gusers').onclick=usersModal; $('#gsave').onclick=async()=>{ @@ -1539,8 +1607,8 @@ function settingsModal(f){ Where the files land. There is one copy however many people subscribe, so this is the same for everyone.`:''} -
-
`); +
+
`); $('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy')); $('#ssave').onclick=async()=>{ const max=$('#smax').value; @@ -1566,8 +1634,8 @@ function downloadLatestModal(f){
Queued immediately, ignoring the per-scan limit.
-
-
`); +
+
`); $('#dgo').onclick=async()=>{ const n=Number($('#dcount').value)||5; try{ @@ -1640,11 +1708,18 @@ on('#logs','onclick',logsModal); $('#feedFilter').oninput=renderFeeds; $('#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); -}; -if(localStorage.getItem('ipx.theme')) document.documentElement.dataset.theme=localStorage.getItem('ipx.theme'); +// Dark, light, and the 2004 Mac app. The button steps through them; the choice is remembered. +const THEMES={dark:'Dark',light:'Light',classic:'Classic, the 2004 Mac app'}; +const THEME_ORDER=Object.keys(THEMES); +const nextTheme=t=>THEME_ORDER[(THEME_ORDER.indexOf(t)+1)%THEME_ORDER.length]; +function setTheme(t){ + if(!THEMES[t]) t='dark'; + document.documentElement.dataset.theme=t; + $('#theme').title=`Theme: ${THEMES[t]}. Click for ${THEMES[nextTheme(t)]}`; + try{ localStorage.setItem('ipx.theme',t); }catch{} +} +$('#theme').onclick=()=>setTheme(nextTheme(document.documentElement.dataset.theme)); +try{ setTheme(localStorage.getItem('ipx.theme')); }catch{ setTheme('dark'); } /* ---------------- live events ---------------- */ let sse;