Merge branch 'codex/ui-fixes'

This commit is contained in:
Jonathan Sykes
2026-10-09 14:37:51 +08:00
23 changed files with 511 additions and 67 deletions

25
plans/ui-fixes-report.md Normal file
View File

@@ -0,0 +1,25 @@
# iPhone UI fixes — 2026-10-09
Work confined to wt-ui/codex/ui-fixes. Retained interrupted commits 3de4cba (A) and 66bbda5 (B). No loading, service-worker, boot or media behavior changed.
## A: toolbar
Phase 2 measurements have ≤1px center spread. Phase 3 measurements rise to 10px (classic), 8px (F7), 9px (Glass), while Bento stays aligned. Commit 5d6a154 moved car-mode.css out of the eager path, including the mic's min-height/min-width rule; absent search-form align-items:center left it at the top. The 26px car target was already small in the old shell. Shared eager styles now provide align-items:center, 44px targets, 22px icons, mobile gaps and safe-area padding. Relevant rules: frontend/styles.css:276 and :4260; obsolete feature-only mic sizing removed from frontend/car-mode.css.
## B: Saved
The existing 72px grid track could contain a theme-widened thumbnail/image; later-loaded layout rules made that visible. Phase 2 minimum measured image/text gap was 12px; Phase 3 or subsequent layout switching produced -4px. The original weak rule comes from ca836e0; Phase 3 dynamic stylesheet ordering exposed it. frontend/saved-page.css:5 now bounds thumbnail and image to the grid track with specific selectors, overflow containment and fixed participation; the text track remains minmax(0,1fr) with min-width:0 and clamped/ellipsized long titles. Final gap is 12px across all tested layouts and widths.
## C: all media icons
Legacy text/emoji download/save icons predate Phases 3–5. frontend/download-actions.js:4 supplies shared SVG concepts (download, offline check, progress), with layout-specific shapes switched by CSS, currentColor and theme tokens. Consumers include Queue/card badges, Now Playing save/export, Saved actions, Downloads/empty states, sidebar/home offline badges, bottom navigation, playlist offline actions, Settings imports, P2P export, Share/External exports and admin LRCLIB/JSON export. Accessible text, IDs and handlers remain intact. Hard-coded done/download badge colours now use accent tokens. Full file:line inventory: [ui-icon-inventory.md](ui-icon-inventory.md). Keyboard-arrow prose is the only narrow glyph allowlist; manifest/app logos have no media-action glyphs.
## Verification
Commands: node perf/ui-geometry.mjs; node perf/ui-icons.mjs; node --test frontend/*.test.js; (cd server && bun install && bun run test).
Icons: 24 layout/theme cases passed across both engines, including runtime shape/colour switching and save/downloading/saved state changes. The glyph guard passed. Frontend suite: 199 passed. Server: 124 passed; only the two permitted worker failures (missing Python yt_dlp).
Geometry: 384 cases, zero failed checks on Chromium and WebKit; four layouts, widths 320/360/375/390/414/430, mic on/off, update on/off, cold and switched layouts. Final center spread 0px, mic/car targets 44×44, image/text gaps 12px, no clipped car or horizontal overflow. Recorded screenshots and raw phase comparisons are git-ignored under plans/ui-shots/. Inspected old and new classic plus the owner's F7 screenshot. Classic Saved-heading and title-region pixel comparisons are identical; full application-wide pixel identity was not tested.
Native iPhone follow-up: installed-PWA safe-area/keyboard sizing, real speech permission and voice search, touch targets at 320px, runtime layout switching, and light/contrast appearance. Browser fixtures stub speech capability and media metadata, not native speech or OPFS.

View File

@@ -0,0 +1,71 @@
# Download and offline icon inventory
All production references after conversion, including SVG definitions, consumers and CSS. Generated from the final frontend; tests excluded.
| File:line | Reference |
| --- | --- |
| `frontend/admin.html:315` | `.media-shape { display:none; } .media-shape-classic { display:inline; } ` |
| `frontend/admin.html:316` | `.media-icon { width:18px; height:18px; vertical-align:middle; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; } ` |
| `frontend/admin.html:412` | `<button class="btn sm" id="webBtn" type="button" title="Fetch lyrics from LRCLIB"><svg class="media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg> Pull from LRCLIB</button> ` |
| `frontend/admin.html:513` | `<a class="btn sm" href="/api/admin/notes/export"><svg class="media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg> Export all (JSON)</a></div> ` |
| `frontend/app.js:3562` | `${isUploadId(st.videoId) ? '' : <button class="np-btn" type="button" data-act="captions">${MediaIcons.svg('download')} Import YouTube captions</button>} ` |
| `frontend/app.js:3804` | `<button type="button" class="np-btn" data-act="captions">${MediaIcons.svg('download')} Import YouTube captions</button> ` |
| `frontend/app.js:6994` | `exportBtn.innerHTML = MediaIcons.svg('download') + ' Save to gallery'; ` |
| `frontend/app.js:7008` | `saveBtn.innerHTML = MediaIcons.svg(isSaved ? 'offline' : 'download') + (isSaved ? ' Saved offline — remove' : ' Save for offline'); ` |
| `frontend/app.js:7235` | `if (iconOnly) { setListIcon(btn, 'offlineDone'); btn.classList.add('done'); } else btn.innerHTML = MediaIcons.svg('offline') + ' Available offline'; ` |
| `frontend/app.js:7240` | `if (iconOnly) { btn.innerHTML = MediaIcons.svg('progress') + <span class="lib-progress">${stats.cached}/${stats.total}</span>; btn.classList.add('busy'); } else btn.innerHTML = MediaIcons.svg('progress') + Saving ${stats.cached}/${stats.total}…; ` |
| `frontend/app.js:7245` | `if (iconOnly) setListIcon(btn, 'offline'); else btn.innerHTML = MediaIcons.svg('download') + ' Make available offline'; ` |
| `frontend/app.js:7262` | `btn.innerHTML = MediaIcons.svg('progress') + <span class="lib-progress">${stats.cached}/${stats.total}</span>; ` |
| `frontend/download-actions.js:29` | `const svg = (kind, extra = '') => <svg class="media-icon ${extra}" data-media-icon="${kind}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${use(kind)}</svg>; ` |
| `frontend/download-actions.js:35` | `defs.innerHTML = '<defs>' + Object.keys(mediaPaths).map(kind => <g id="media-${kind}">${Object.entries(variants).map(([layout, paths]) => <g class="media-shape media-shape-${layout}">${paths[kind]}</g>).join('')}</g>).join('') + '</defs>'; ` |
| `frontend/download-actions.js:37` | `root.MediaIcons = media; ` |
| `frontend/downloads-page.js:122` | `<svg class="dl-empty-svg media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg> ` |
| `frontend/downloads.js:15` | `root.SettingsSections.register({ id: 'downloads-storage', title: 'Downloads & storage', cluster: 'Library & storage', description: 'Active queued paused failed saves, disk usage and playlist sizes', icon: '<use href="#media-download"/>', summary: () => ${adapter.slots()} at once · ${bytes(total)}, render(container) { ` |
| `frontend/glass-controls.js:9` | `save:'<use href="#media-download"/>', ` |
| `frontend/index.html:50` | `<button class="nav-item" data-view="saved"><svg class="media-icon media-nav-icon" data-media-icon="offline" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-offline"/></svg> Saved</button> ` |
| `frontend/index.html:51` | `<button class="nav-item" data-view="downloads"><svg class="media-icon media-nav-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg> Downloads <span id="navDlCount" class="nav-badge hidden">0</span></button> ` |
| `frontend/index.html:271` | `<button id="saveBtn" class="np-btn" title="Save this video for offline playback"><svg class="ti media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use class="i-save" href="#media-download"/><use class="i-done" href="#media-offline"/><use class="i-progress" href="#media-progress"/></svg><span class="tl">Save</span></button> ` |
| `frontend/index.html:272` | `<button id="exportBtn" class="np-btn" title="Save this video to your gallery, Files app or Downloads folder — works while streaming too"><svg class="ti media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg><span class="tl">Gallery</span></button> ` |
| `frontend/index.html:341` | `<button id="exportMarkersBtn" class="np-btn" type="button"><svg class="media-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg> Export</button> ` |
| `frontend/index.html:601` | `<span class="bottom-nav-icon"><svg class="media-icon media-nav-icon" data-media-icon="offline" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-offline"/></svg></span> ` |
| `frontend/index.html:605` | `<span class="bottom-nav-icon"><svg class="media-icon media-nav-icon" data-media-icon="download" viewBox="0 0 24 24" aria-hidden="true"><use href="#media-download"/></svg></span> ` |
| `frontend/saved-page.js:6` | `export: '<use href="#media-download"/>', ` |
| `frontend/saved-page.js:13` | `b.innerHTML = <svg class="media-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[kind]}</svg>; ` |
| `frontend/share-external.js:74` | `<div class="sm-gif-actions"><a class="np-btn" href="${url}" download="${escapeHtml(name)}">${MediaIcons.svg('download')} Download</a> ` |
| `frontend/share-external.js:101` | `<div class="sm-gif-actions"><a class="np-btn" href="${url}" download="${escapeHtml(name)}">${MediaIcons.svg('download')} Download</a> ` |
| `frontend/share-external.js:179` | `body.innerHTML = <p class="modal-text">The server hasn't finished caching this video yet, and other players need that single-file copy. Keep it playing for a minute (or tap ${MediaIcons.svg('download')} Save), then try again.</p>; ` |
| `frontend/share-external.js:196` | `<button class="np-btn" type="button" data-act="m3u">${MediaIcons.svg('download')} .m3u (this video)</button> ` |
| `frontend/share-external.js:197` | `${inList ? <button class="np-btn" type="button" data-act="m3u-list">${MediaIcons.svg('download')} .m3u (whole list)</button> : ''} ` |
| `frontend/shell-core.js:94` | `offlineBadge = <span class="pl-offline-badge all" title="Available offline">${MediaIcons.svg('offline')}</span>; ` |
| `frontend/shell-core.js:144` | `offlineHtml = <span class="home-pl-offline all" title="Available offline">${MediaIcons.svg('offline')}</span>; ` |
| `frontend/styles.css:1113` | `.saved-badge .media-icon { width:16px; height:16px; filter:drop-shadow(0 1px 2px #000); } ` |
| `frontend/styles.css:4337` | `.media-icon { display:inline-block; width:20px; height:20px; flex:none; vertical-align:middle; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; pointer-events:none; } ` |
| `frontend/styles.css:4339` | `.empty-icon .media-icon { width:48px; height:48px; } ` |
| `frontend/styles.css:4340` | `:is(.pl-offline-badge,.home-pl-offline) .media-icon { width:14px; height:14px; } ` |
| `frontend/styles.css:4341` | `html[data-layout="classic"] .media-icon { stroke-width:1.8; } ` |
| `frontend/styles.css:4342` | `html[data-layout="f7-swipe"] .media-icon { stroke-width:2; } ` |
| `frontend/styles.css:4343` | `html[data-layout="glass-stage"] .media-icon { stroke-width:1.9; } ` |
| `frontend/styles.css:4344` | `html[data-layout="bento-hub"] .media-icon { stroke-width:1.8; } ` |
| `frontend/styles.css:4348` | `:is([data-media-icon="progress"], .np-btn.saving .media-icon) { animation:mediaSpin 1s linear infinite; transform-origin:center; } ` |
| `frontend/styles.css:4350` | `@media(prefers-reduced-motion:reduce) { :is([data-media-icon="progress"], .np-btn.saving .media-icon) { animation:none; } } ` |
| `frontend/styles.css:4351` | `.media-shape { display:none; } ` |
| `frontend/styles.css:4352` | `.media-shape-classic { display:inline; } ` |
| `frontend/styles.css:4353` | `html[data-layout="f7-swipe"] .media-shape-classic, ` |
| `frontend/styles.css:4354` | `html[data-layout="glass-stage"] .media-shape-classic, ` |
| `frontend/styles.css:4355` | `html[data-layout="bento-hub"] .media-shape-classic { display:none; } ` |
| `frontend/styles.css:4356` | `html[data-layout="f7-swipe"] .media-shape-f7-swipe, ` |
| `frontend/styles.css:4357` | `html[data-layout="glass-stage"] .media-shape-glass-stage, ` |
| `frontend/styles.css:4358` | `html[data-layout="bento-hub"] .media-shape-bento-hub { display:inline; } ` |
| `frontend/views-core.js:47` | `els.listActions.innerHTML = groups.length ? <button class="np-btn" id="notesExportBtn" type="button">${MediaIcons.svg('download')} Export Markdown</button> : ''; ` |
| `frontend/views-core.js:133` | `offline: '<use href="#media-download"/>', ` |
| `frontend/views-core.js:134` | `offlineDone: '<use href="#media-offline"/>', ` |
| `frontend/views-core.js:419` | `row.innerHTML = <span class="ps-ic">📡</span><span class="ps-txt"><b></b><small></small></span><button type="button" class="ps-dl" aria-label="Save to this device">${MediaIcons.svg('download')}</button>; ` |
| `frontend/views-core.js:520` | `<button class="card-act" data-act="export" title="Save to this device" aria-label="Save to this device">${MediaIcons.svg('download')}</button> ` |
| `frontend/views-core.js:553` | `<div class="empty-icon">${MediaIcons.svg('offline')}</div> ` |
| `frontend/views-core.js:569` | `<div class="empty-icon">${MediaIcons.svg('offline')}</div> ` |
| `frontend/views-core.js:571` | `<p class="empty-desc">Use <strong>${MediaIcons.svg('download')} Save</strong> while playing, or add videos to a playlist to keep them offline.</p>; ` |
| `frontend/views-core.js:906` | `<button id="importYtBtn" class="btn">${MediaIcons.svg('download')} Import from YouTube</button> ` |
| `frontend/views-core.js:978` | `<button id="profileExportCredBtn" class="btn" style="display:none">${MediaIcons.svg('download')} Credentials file</button> ` |
| `frontend/views-core.js:1339` | `<span class="saved-badge" title="${isDownloading ? 'Downloading…' : 'Saved offline'}">${MediaIcons.svg('offline', 'badge-offline')}${MediaIcons.svg('progress', 'badge-progress')}</span> ` |
The only remaining requested arrow glyphs are keyboard shortcut prose in index.html and admin.html, explicitly allowlisted by media-icons.test.js. Webmanifest/shortcut/icon images contain no download/save action glyph; generic app logos stay unchanged. Modal titles that previously prefixed a glyph now retain their accessible plain-text name.