aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 19:11:43 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 19:11:43 +0200
commitd053d083627f1f9f010752f8ad67941e22d49f27 (patch)
treea72e5cfd4b80fae79fad66944d3db24d6a1a3547 /packages/meshbay-hub/src/meshbay_hub
parent251d2c587cad16e2464851b1958203c41e994fb4 (diff)
downloadmeshbay-d053d083627f1f9f010752f8ad67941e22d49f27.tar.gz
feat(hub): draw an actual CD for covers with no art, not a flat icon
The monochrome disc icon from the previous commit read as placeholder UI chrome, not as artwork - too plain for something that is, for most tiles in a real library, the default look of the grid. - DiscPlaceholder (music-app.js): a small inline SVG illustration - dark disc base, an iridescent radial-gradient sheen mimicking the rainbow reflection a real CD's data side has, two faint groove rings, a light label ring, a dark spindle hole. Each instance gets its own gradient id (a module-level counter) rather than one literal id repeated - a grid renders many of these at once, and a shared id would leave every disc after the first pointing at whichever <radialGradient> the browser happened to resolve. - AlbumCard and MusicDetailModal now branch on coverHash directly: MediaThumb (the real chunk-path image) when there is one, DiscPlaceholder when there isn't - rather than routing "no cover" through MediaThumb's own generic small-icon fallback, which is still right for its other callers (list rows, Videos). - icon.js: the flat monochrome "disc" icon this replaces is removed - nothing else used it. - style.css: .music-disc-empty/.music-disc-svg replace the old .video-thumb-empty overrides; still sized relative to the tile (82%) so it scales with .music-grid's auto-fill columns. Client-side only. npm run sync-ui re-run. Full suite: 1129 passed, no regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/icon.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app.js56
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css32
3 files changed, 68 insertions, 22 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/icon.js b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
index 87b56ee..0ecbd70 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/icon.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
@@ -74,8 +74,6 @@ const ICON_PATHS = {
music: ['M9 18V5l12-2v13',
'M4 18a2 2 0 1 0 4 0 2 2 0 1 0 -4 0',
'M16 16a2 2 0 1 0 4 0 2 2 0 1 0 -4 0'],
- disc: ['M3 12a9 9 0 1 0 18 0 9 9 0 1 0 -18 0',
- 'M9 12a3 3 0 1 0 6 0 3 3 0 1 0 -6 0'],
pause: ['M7 5.5v13', 'M17 5.5v13'],
'skip-next': ['M5 4l10 8-10 8z', 'M19 5v14'],
'skip-prev': ['M19 4L9 12l10 8z', 'M5 5v14'],
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
index b8ee657..87e1319 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
@@ -112,6 +112,48 @@ function useMusicMeta(transportRef, path, active) {
return meta;
}
+// A drawn CD standing in for a cover nothing supplied one for -- most tiles
+// in a real, older/well-ripped library land here (musicbay.md's own
+// measurement: ~11% embedded art, ~26% once sibling image files are counted
+// too), so this is the *default* look of the grid, not a rare fallback, and
+// needed to read as a deliberate piece of art rather than a broken image.
+// A flat single-color icon (the first version of this) looked exactly like
+// "missing", not "no cover" -- an actual disc, with the iridescent sheen a
+// real CD's data side has, reads as intentional at a glance. Genuinely
+// unique gradient ids: a `<radialGradient>` id is a plain DOM id, and a grid
+// full of these renders many instances at once -- reusing one literal id
+// would leave every disc after the first pointing at whichever def the
+// browser resolves first.
+let _discIdSeq = 0;
+
+function DiscPlaceholder({ cls }) {
+ const [gradId] = useState(() => `music-disc-sheen-${_discIdSeq++}`);
+ return html`
+ <div class="${cls} music-disc-empty">
+ <svg class="music-disc-svg" viewBox="0 0 100 100" aria-hidden="true">
+ <defs>
+ <radialGradient id=${gradId} cx="36%" cy="30%" r="80%">
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.95" />
+ <stop offset="14%" stop-color="#bfe3ff" stop-opacity="0.55" />
+ <stop offset="32%" stop-color="#b48cf2" stop-opacity="0.42" />
+ <stop offset="52%" stop-color="#f472b6" stop-opacity="0.32" />
+ <stop offset="72%" stop-color="#38bdf8" stop-opacity="0.22" />
+ <stop offset="100%" stop-color="#0f172a" stop-opacity="0" />
+ </radialGradient>
+ </defs>
+ <circle cx="50" cy="50" r="47" fill="#161b26" />
+ <circle cx="50" cy="50" r="47" fill="url(#${gradId})" />
+ <circle cx="50" cy="50" r="47" fill="none" stroke="rgba(255,255,255,0.14)" stroke-width="1" />
+ <circle cx="50" cy="50" r="34" fill="none" stroke="rgba(255,255,255,0.07)" stroke-width="0.6" />
+ <circle cx="50" cy="50" r="25" fill="none" stroke="rgba(255,255,255,0.07)" stroke-width="0.6" />
+ <circle cx="50" cy="50" r="15" fill="#e4e7ec" />
+ <circle cx="50" cy="50" r="15" fill="none" stroke="rgba(0,0,0,0.18)" stroke-width="1" />
+ <circle cx="50" cy="50" r="4.2" fill="#161b26" />
+ </svg>
+ </div>
+ `;
+}
+
function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) {
const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0];
// Only when nothing in the library already gives us a cover -- the common
@@ -122,9 +164,10 @@ function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen })
return html`
<div class="music-card" onClick=${onOpen}>
- <${MediaThumb} thumbHash=${coverHash} alt=${album.album}
- cls="music-cover" emptyIcon="disc"
- transportRef=${transportRef} gekRef=${gekRef} />
+ ${coverHash
+ ? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album}
+ cls="music-cover" transportRef=${transportRef} gekRef=${gekRef} />`
+ : html`<${DiscPlaceholder} cls="music-cover" />`}
<div class="music-card-info">
<div class="music-card-title">${album.album}</div>
<div class="music-card-sub">${album.artist}</div>
@@ -153,9 +196,10 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC
</div>
<div class="music-detail-body">
<div class="music-detail-header">
- <${MediaThumb} thumbHash=${coverHash} alt=${album.album}
- cls="music-detail-cover" emptyIcon="disc"
- transportRef=${transportRef} gekRef=${gekRef} />
+ ${coverHash
+ ? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album}
+ cls="music-detail-cover" transportRef=${transportRef} gekRef=${gekRef} />`
+ : html`<${DiscPlaceholder} cls="music-detail-cover" />`}
<div class="music-detail-meta">
<div class="music-detail-artist">${album.artist}</div>
${meta && meta.confidence ? html`<div class="music-detail-date">${meta.release_date || ''}</div>` : ''}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index 6c3c0f6..3d411f7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -2726,17 +2726,19 @@ a.transfer-name {
display: block;
background: var(--bg-surface);
}
-.music-cover.video-thumb-empty { aspect-ratio: 1 / 1; }
-/* A big, plain disc rather than the small note icon shared with list rows —
- most tiles land here (few files carry embedded/sibling art), so this is
- the default look of the grid, not a rare fallback; it needs to read as
- deliberate, not broken. Sized relative to the tile so it scales with
- .music-grid's auto-fill columns instead of one fixed px value. */
-.music-cover.video-thumb-empty .icon {
- width: 55%;
- height: 55%;
- color: var(--text-dim);
+/* The drawn-CD placeholder (DiscPlaceholder, music-app.js) — sized relative
+ to the tile so it scales with .music-grid's auto-fill columns instead of
+ one fixed px value, and never clipped by the square tile's own overflow. */
+.music-disc-empty {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-surface);
+ overflow: hidden;
}
+.music-disc-svg { width: 82%; height: 82%; }
.music-card-info { padding: 8px 10px; }
.music-card-title {
@@ -2792,10 +2794,12 @@ a.transfer-name {
flex-shrink: 0;
background: var(--bg-raised);
}
-.music-detail-cover.video-thumb-empty .icon {
- width: 55%;
- height: 55%;
- color: var(--text-dim);
+.music-detail-cover.music-disc-empty {
+ width: 96px;
+ height: 96px;
+ border-radius: 6px;
+ flex-shrink: 0;
+ background: var(--bg-raised);
}
.music-detail-meta { min-width: 0; }
.music-detail-artist { font-weight: 600; font-size: 0.95em; }