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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
This commit is contained in:
2026-09-11 15:00:30 +00:00
parent 8a309eb652
commit ae47e31a97
3 changed files with 116 additions and 24 deletions

View File

@@ -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}
</style>
</head>
<body>
@@ -692,11 +760,11 @@ function renderFeed(){
${f.group?`<div class="sub">From the OPML subscription <b>${esc(f.group)}</b></div>`:''}
</div>
<div class="acts">
<button class="btn primary" data-a="scan"><i>⟳</i>Scan now</button>
<button class="btn" data-a="dl"><i>↓</i>Download latest</button>
<button class="btn" data-a="read"><i>✓</i>Mark all read</button>
<button class="btn" data-a="settings"><i>⚙</i>Settings</button>
<button class="btn danger last" data-a="rm"><i>✕</i>Unsubscribe</button>
<button class="btn ico primary" data-a="scan" title="Scan now" aria-label="Scan now">⟳</button>
<button class="btn ico" data-a="dl" title="Download latest…" aria-label="Download latest">⤓</button>
<button class="btn ico" data-a="read" title="Mark all read" aria-label="Mark all read">✓✓</button>
<button class="btn ico" data-a="settings" title="Settings" aria-label="Settings">⚙</button>
<button class="btn ico danger last" data-a="rm" title="Unsubscribe" aria-label="Unsubscribe">✕</button>
</div>
</div>` : `
<div class="fhead slim">
@@ -748,10 +816,10 @@ function renderGroup(f,kids){
${gone?`<div class="sub" style="color:var(--warn)">${gone} feed${gone===1?' is':'s are'} no longer
listed in this OPML but kept because ${gone===1?'it has':'they have'} downloads.</div>`:''}
<div class="acts">
<button class="btn primary" data-a="scan"><i>⟳</i>Re-read OPML</button>
<button class="btn" data-a="read"><i>✓</i>Mark all read</button>
<button class="btn" data-a="settings"><i>⚙</i>Settings</button>
<button class="btn danger last" data-a="rm"><i>✕</i>Unsubscribe</button>
<button class="btn ico primary" data-a="scan" title="Re-read the OPML" aria-label="Re-read the OPML">⟳</button>
<button class="btn ico" data-a="read" title="Mark all read" aria-label="Mark all read">✓✓</button>
<button class="btn ico" data-a="settings" title="Settings" aria-label="Settings">⚙</button>
<button class="btn ico danger last" data-a="rm" title="Unsubscribe" aria-label="Unsubscribe">✕</button>
</div>
</div>
</div>
@@ -1442,8 +1510,8 @@ async function prefsModal(){
<div class="field"><label>Users</label>
<div class="inline"><button class="btn" id="gusers">Manage users…</button></div>
<span class="hint">Add and remove the people who can sign in, and choose who is an admin.</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="gsave">Save</button></div>`);
<div class="cardacts"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">✕</button>
<button class="btn ico primary" id="gsave" title="Save" aria-label="Save">✓</button></div>`);
$('#gopml').onclick=opmlModal;
$('#gusers').onclick=usersModal;
$('#gsave').onclick=async()=>{
@@ -1539,8 +1607,8 @@ function settingsModal(f){
<input type="text" id="sfolder" value="${esc(f.folder||'')}" placeholder="${esc(f.title||f.id)}">
<span class="hint">Where the files land. There is one copy however many people
subscribe, so this is the same for everyone.</span></div>`:''}
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="ssave">Save</button></div>`);
<div class="cardacts"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">✕</button>
<button class="btn ico primary" id="ssave" title="Save" aria-label="Save">✓</button></div>`);
$('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy'));
$('#ssave').onclick=async()=>{
const max=$('#smax').value;
@@ -1566,8 +1634,8 @@ function downloadLatestModal(f){
<div class="field"><label>How many of the newest undownloaded items?</label>
<input type="number" id="dcount" min="1" max="100" value="5">
<span class="hint">Queued immediately, ignoring the per-scan limit.</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="dgo">Download</button></div>`);
<div class="cardacts"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">✕</button>
<button class="btn ico primary" id="dgo" title="Download" aria-label="Download">⤓</button></div>`);
$('#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;