aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-16 16:07:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-16 16:07:06 +0200
commit25f62e169e049f0757ef611d5b409e7523958dee (patch)
treecb755a6ab7364b1f4bc2144caf04b7310862aa13 /packages/meshbay-hub/src/meshbay_hub/static/music-app.js
parent8889e90b8f1629f983c7ed4e44a2badbda525095 (diff)
downloadmeshbay-25f62e169e049f0757ef611d5b409e7523958dee.tar.gz
music: pool single-album artists into shared rows
An artist with one album got a heading and one cover on a row that fits five, and a library is mostly single-album artists. Consecutive singles share one grid, in place, so the page stays in artist order. Each pooled cover keeps its artist's name above it in the same type as a section heading. Dropping it was the first version and it was wrong: scrolling then alternates between artists written large and small. Measured on the probe's fixture: 4208px to 1895px, and a walk of the page reaches all 21 covers instead of 9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app.js94
1 files changed, 76 insertions, 18 deletions
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 8fc6cb3..baa3520 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
@@ -348,10 +348,29 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC
// -- Mode A: album grid -----------------------------------------------------
-// `units` is one page of `{ artist, album }`. An artist whose albums straddle
-// two pages gets its heading on both.
-function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueue, onMenu }) {
- const [detail, setDetail] = useState(null); // the album object
+/**
+ * One page of `{ artist, album }` as the sections that get drawn.
+ *
+ * An artist with two or more albums gets a heading and a grid of their own. An
+ * artist with **one** does not: a heading plus a single cover is a whole row of
+ * whitespace, and a library is mostly single-album artists — a compilation
+ * bought once, one album of someone's, a soundtrack. Consecutive singles share
+ * one grid instead, so five of them fill a row that five headings would
+ * otherwise have spent five rows on.
+ *
+ * Pooled in place rather than swept into a bin at the end: the page is drawn in
+ * artist order and a reader scrolling it is relying on that. A pooled run sits
+ * exactly where its artists would have been.
+ *
+ * **Each pooled cover keeps its artist's name above it, in the same type as a
+ * multi-album artist's heading.** The first version of this dropped the heading
+ * on the grounds that the card already names its artist underneath — and that
+ * was wrong: scrolling then alternates between artists written large and
+ * artists written small, and the eye has to work out which kind of row it is
+ * looking at. The heading moves *into* the cell rather than going away, so a
+ * row of five costs one heading's height between them instead of five rows.
+ */
+function albumSections(units) {
const artists = [];
for (const u of units) {
const prev = artists[artists.length - 1];
@@ -359,21 +378,60 @@ function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueu
else artists.push({ artist: u.artist, albums: [u.album] });
}
+ const sections = [];
+ for (const a of artists) {
+ if (a.albums.length >= 2) { sections.push({ kind: 'artist', ...a }); continue; }
+ const prev = sections[sections.length - 1];
+ if (prev && prev.kind === 'pool') prev.albums.push(a.albums[0]);
+ else sections.push({ kind: 'pool', albums: [a.albums[0]] });
+ }
+ return sections;
+}
+
+// `units` is one page of `{ artist, album }`. An artist whose albums straddle
+// two pages gets its heading on both — and, with one album on each page, is
+// pooled on both.
+function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueue, onMenu }) {
+ const [detail, setDetail] = useState(null); // the album object
+ const sections = useMemo(() => albumSections(units), [units]);
+
+ const tile = (album) => html`
+ <${LazyTile} key=${album.artist + '::' + album.album} cls="music-tile-slot">
+ <${AlbumCard} album=${album} transportRef=${transportRef} gekRef=${gekRef}
+ musicbrainzEnabled=${musicbrainzEnabled} onOpen=${() => setDetail(album)}
+ onMenu=${onMenu} />
+ </${LazyTile}>
+ `;
+
+ // Keyed on the first album rather than on the index: a pool's position shifts
+ // whenever a neighbouring artist gains or loses an album, and an index key
+ // would make preact reuse the wrong tiles across that change.
+ const sectionKey = (s) => (s.kind === 'artist'
+ ? `artist:${s.artist}`
+ : `pool:${s.albums[0].artist}::${s.albums[0].album}`);
+
return html`
- ${artists.map((a) => html`
- <div class="music-artist-section" key=${a.artist}>
- <h3 class="music-artist-heading">${a.artist}</h3>
- <div class="music-grid">
- ${a.albums.map((album) => html`
- <${LazyTile} key=${album.artist + '::' + album.album} cls="music-tile-slot">
- <${AlbumCard} album=${album} transportRef=${transportRef} gekRef=${gekRef}
- musicbrainzEnabled=${musicbrainzEnabled} onOpen=${() => setDetail(album)}
- onMenu=${onMenu} />
- </${LazyTile}>
- `)}
- </div>
- </div>
- `)}
+ ${sections.map((s) => (s.kind === 'artist'
+ ? html`
+ <div class="music-artist-section" key=${sectionKey(s)}>
+ <h3 class="music-artist-heading">${s.artist}</h3>
+ <div class="music-grid">${s.albums.map(tile)}</div>
+ </div>`
+ : html`
+ <div class="music-artist-section music-artist-pool" key=${sectionKey(s)}>
+ <div class="music-grid">
+ ${s.albums.map((album) => html`
+ <div class="music-pool-cell" key=${album.artist + '::' + album.album}>
+ ${/* One line, clipped, with the full name on hover: a cell is
+ ~170px wide and a heading that wraps to two lines would
+ push its own cover below the others on the row. */''}
+ <h3 class="music-artist-heading music-pool-heading"
+ title=${album.artist}>${album.artist}</h3>
+ ${tile(album)}
+ </div>
+ `)}
+ </div>
+ </div>`))}
${detail && html`
<${MusicDetailModal} album=${detail} transportRef=${transportRef} gekRef=${gekRef}
musicbrainzEnabled=${musicbrainzEnabled} onMenu=${onMenu}