From 69bb722590e7c14745e58778730e968c24bfc3c5 Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:29:17 -0500 Subject: [PATCH] =?UTF-8?q?android:=20the=20artist=20page,=20redesigned=20?= =?UTF-8?q?=E2=80=94=20the=20editorial=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kyle called the artist page "super glitchy" and asked for a rework in the style of other music apps on mobile; from three mockups he picked the editorial one (research/mobile-mockup, look B). What made it glitchy: six sections each popped in on their own as their data landed, so the page reflowed six times; the hero dissolved again every time a better picture arrived; a cloud of stat chips wrapped to three lines; and the discography tab jumped when the identity merge re-filed releases. The page now: a 300dp portrait with the name on it and the play disc beside it, Shuffle · Follow · one line of facts, the latest release as a card, Top Songs as swipeable columns of four with dots ("See All" opens the list), Albums and Singles & EPs as carousels with a link to the full discography, On this phone, Similar Artists, and About with the bio, tags and socials. The first sharp picture is the picture, locked; a cold visit renders skeletons the size of the sections that are coming and swaps once; there are no tabs on the page, the rails simply grow. No band over the hero — the shell's two floating discs are the only chrome, as everywhere else. The hero and its copy behind the scroller are both 300px now, as they must be. --- src/mobile/screens/Artist.svelte | 420 +++++++++++++++++-------------- src/mobile/shell.css | 92 ++++++- 2 files changed, 319 insertions(+), 193 deletions(-) diff --git a/src/mobile/screens/Artist.svelte b/src/mobile/screens/Artist.svelte index a671bda..4eb08f1 100644 --- a/src/mobile/screens/Artist.svelte +++ b/src/mobile/screens/Artist.svelte @@ -1,15 +1,25 @@ -
+
- {#if heroSrc} -
+ {#if heroShown} +
{/if}
-
- {#if logoUrl} - - {:else} -
{shownName}
- {/if} +
+
+
Artist
+ {#if logoUrl} + + {:else} +

{shownName}

+ {/if} +
+
-
- -
- - {#if releasedCount}{releasedCount} releases{/if} - {#if page?.artist?.fans || overview?.artist?.fans || seed?.fans}{fans(page?.artist?.fans || overview?.artist?.fans || seed?.fans)}{/if} - {#if years}{years}{/if} - {#if page?.top_genre}{page.top_genre}{/if} - {#if playTime}{playTime} played{/if} - {#if albums.length}{/if} - {#each socials as link (link.kind + link.url)} - - {/each} -
+
+ + + {#if facts}{facts}{/if}
-{#if entry?.status === "loading"} -
Looking up the rest on the catalog…
-{:else if entry?.status === "error"} -
The catalog did not answer — only what this phone holds is here.
-{/if} - -{#if popular.length} -

Popular

- {#each popular as p, i (p.t.id)} - - {/each} - {#if popularAll.length > 5} - - {/if} -{/if} - {#if latest} -

Latest release

-
-
-
- -
-
Latest · {(latest.record_type || "album").toUpperCase()}
-
{latest.title}
-
{yearOf(latest.release_date)}{latest.track_count ? ` · ${latest.track_count} tracks` : ""}{inLib(latest) ? " · in your library" : ""}
-
- - -
+
+ +
+
Latest release{yearOf(latest.release_date) ? ` · ${yearOf(latest.release_date)}` : ""}
+
{latest.title}
+
{latestKind}{latest.track_count ? ` · ${latest.track_count} ${latest.track_count === 1 ? "song" : "songs"}` : ""}{inLib(latest) ? " · on this phone" : ""}
+
+ +
+{:else if loading} + {/if} -{#if tabs.length} +{#if popularAll.length}
-

Discography · {releasedCount} on the catalog

- +

Top Songs

+ {#if popularAll.length > PAGE}{/if}
- {#if tabs.length > 1} -
- {#each tabs as t (t.key)} - + {#if showAllPopular} + {#each popularAll as p, i (p.t.id)} + + {/each} + {:else} +
+ {#each topPages as col, ci (ci)} +
+ {#each col as p (p.t.id)} + + {/each} +
{/each}
+ {#if topPages.length > 1} + + {/if} {/if} -
- {#each families as f (familyKeyOf(f))} +{:else if loading} +

Top Songs

+ +{/if} + +{#if albumFamilies.length} +
+

Albums · {tabs.find((t) => t.key === "album")?.items.length ?? albumFamilies.length}

+ +
+
+ {#each albumFamilies as f (familyKeyOf(f))} {@const l = familyLib(f)} 1 ? `${f.editions.length} editions` : (l ? "in your library" : null)} + badge={(f.editions?.length ?? 1) > 1 ? `${f.editions.length} editions` : (l ? "on this phone" : null)} onclick={() => openFamily(f)} onmore={() => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: name } })} /> {/each}
- {#if tabCount > GRID_CAP} - - {/if} +{:else if loading} +

Albums

+ {/if} -{#if timeline} -

Releases by year · {timeline.last - timeline.first + 1} years · {timeline.total} releases

-
- {#each timeline.cols as c (c.year)} -
- - {String(c.year).slice(2)} -
+{#if shortFamilies.length} +
+

Singles & EPs · {shortTabs.reduce((n, t) => n + t.items.length, 0)}

+ +
+
+ {#each shortFamilies as { f, kind } (familyKeyOf(f))} + {@const l = familyLib(f)} + openFamily(f)} + onmore={() => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: name } })} + /> + {/each} +
+{/if} + +{#if tabs.length && releasedCount > albumFamilies.length + shortFamilies.length} + +{/if} + +{#if albums.length} +

On this phone · {albums.length} record{albums.length === 1 ? "" : "s"}{tracks ? ` · ${tracks} tracks` : ""}

+ {#if plays.topTitle}
Most played here: {plays.topTitle} · {plays.topCount}×{plays.last ? " · last " + ago(plays.last) : ""}
{/if} +
+ {#each albums as a (a.id)} + push("album", { id: a.id }, screenEl)} /> {/each}
{/if} {#if extras?.related?.length} -

Fans also like

+

Similar Artists

{#each extras.related.slice(0, 12) as a (a.id)}
{/if} -{#if albums.length} -

On this phone · {albums.length} record{albums.length === 1 ? "" : "s"}{tracks ? ` · ${tracks} tracks` : ""}

- {#if plays.topTitle}
Most played here: {plays.topTitle} · {plays.topCount}×{plays.last ? " · last " + ago(plays.last) : ""}
{/if} -
- {#each albums as a (a.id)} - push("album", { id: a.id }, screenEl)} /> - {/each} -
-{/if} - -{#if extras?.bio || extras?.profile?.listeners || extras?.profile?.tags?.length} +{#if extras?.bio || extras?.profile?.listeners || extras?.profile?.tags?.length || socials.length || playTime}

About

-
- {#if extras.profile?.listeners || extras.profile?.plays} -
- {#if extras.profile.listeners}{fmtCount(extras.profile.listeners)} listeners{/if} - {#if extras.profile.plays}{fmtCount(extras.profile.plays)} plays{/if} +
+ {#if extras?.profile?.listeners || extras?.profile?.plays || playTime} +
+ {[extras?.profile?.listeners ? `${fmtCount(extras.profile.listeners)} listeners` : "", extras?.profile?.plays ? `${fmtCount(extras.profile.plays)} plays` : "", playTime ? `${playTime} played here` : ""].filter(Boolean).join(" · ")}
{/if} - {#if extras.bio} -

(bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}

+ {#if extras?.bio} +

(bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}

{/if} - {#if extras.profile?.tags?.length} -
{#each extras.profile.tags.slice(0, 6) as t (t)}{t}{/each}
+ {#if extras?.profile?.tags?.length} +
{#each extras.profile.tags.slice(0, 6) as t (t)}{t}{/each}
+ {/if} + {#if socials.length} +
+ {#each socials as link (link.kind + link.url)} + + {/each} +
{/if}
{/if} -{#if !albums.length && !popular.length && !tabs.length && entry?.status !== "loading"} +{#if entry?.status === "error" && !albums.length && !popularAll.length} +
The catalog did not answer — only what this phone holds is here.
+{/if} +{#if empty}
Nothing here yet.No records by {shownName} on this phone, and the catalog {entry?.status === "error" ? "didn't answer" : "had nothing to add"}.
{/if} diff --git a/src/mobile/shell.css b/src/mobile/shell.css index cdf6741..bdf2409 100644 --- a/src/mobile/shell.css +++ b/src/mobile/shell.css @@ -555,7 +555,7 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou /* ── the artist hero ────────────────────────────────────────────────────── The desktop hero is a wide banner beside the identity block. A phone has no beside, so the portrait goes full-bleed and the name sits ON it. */ -.hero { position: relative; height: 258px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; } +.hero { position: relative; height: 300px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; } .hero-img { position: absolute; inset: 0; background-size: cover; background-position: center 24%; transform: scale(1.04); } /* the blurred SEED: the picture the library already has, soft and oversized, until a sharp one exists */ @@ -568,18 +568,18 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou } /* The hero's picture again, behind the scroller and outside its mask, so the photograph is allowed under the clock while the words are not. Its geometry - has to match .hero/.hero-img exactly — 258px tall, the same 24% focal point, + has to match .hero/.hero-img exactly — 300px tall, the same 24% focal point, the same 1.04 scale — or the seam shows. */ .hero-behind { position: absolute; left: 0; right: 0; top: 0; - height: 258px; + height: 300px; z-index: 0; pointer-events: none; /* the clip .hero gives its own picture; without it the 1.04 below renders - 268px in a 258px box and five pixels of photograph appear under the + 312px in a 300px box and twelve pixels of photograph appear under the hero's foot, below the black the scrim fades to */ overflow: hidden; transform: translateY(var(--hero-shift, 0px)); @@ -1039,17 +1039,11 @@ html[data-material="solid"] .halfsheet { background: #1c1c20; backdrop-filter: n release on the app's featured surface (the artwork blurred behind itself); releases by year; About. */ .hero-logo { display: block; max-width: 78%; max-height: 96px; object-fit: contain; object-position: left bottom; filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.6)); } -.hero-acts { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 0; } -.hero-acts .stats { flex: 1; min-width: 0; } .stats button.s { cursor: pointer; } .stats .s.fb { background: rgba(255, 255, 255, 0.11); color: var(--text); } .stats .s.fb.on { background: var(--accent); color: #04121a; } .stats .s.fb:disabled { opacity: 0.5; } .stats .s.quiet { color: var(--text-secondary); } -.row.pop .rank { width: 16px; text-align: right; font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; flex: none; } -.lbar { display: flex; align-items: center; gap: 8px; margin-top: 5px; height: 10px; } -.lbar i { display: block; flex: 1; height: 3px; border-radius: 2px; background: var(--accent); transform-origin: left center; opacity: 0.85; } -.lbar em { font-style: normal; font-size: 10.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; min-width: 32px; text-align: right; } .more-link { display: block; padding: 8px var(--pad) 2px; font-size: 12.5px; font-weight: 750; color: var(--accent-hover); } .feat { position: relative; border-radius: var(--r-card); overflow: hidden; background: var(--surface); box-shadow: var(--ring); } .feat-bg { position: absolute; inset: -30px; background-size: cover; background-position: center; filter: blur(28px) saturate(1.2); opacity: 0.55; } @@ -1683,3 +1677,81 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion=" } .dg-eds { display: flex; flex-direction: column; gap: 6px; padding: 0 var(--pad) 12px calc(var(--pad) + 58px); } .dg-eds .ed { width: 100%; } + +/* ── the artist page, B (2026-09-18) ────────────────────────────────────── + The editorial shape Kyle picked from three mockups: the name ON the + 300dp portrait with the play disc beside it, one line of facts, the + latest release first, Top Songs as swipeable columns of four, the + records as carousels, About last. The bar over the hero is the shell's + two floating discs; nothing else. */ +.hero.ar .hero-in { padding-bottom: 14px; } +.hero-in.row2 { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; } +.hero-in.row2 .hero-id { min-width: 0; flex: 1; } +.hero-in.row2 .disc { flex: none; margin-bottom: 2px; } +.hero.ar .hero-name { margin: 0; overflow-wrap: anywhere; } +.ar-kick { font-size: 11px; font-weight: 800; letter-spacing: 0.9px; text-transform: uppercase; color: rgba(255, 255, 255, 0.7); margin-bottom: 5px; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5); } +.ar-acts { display: flex; align-items: center; gap: 10px; padding: 14px var(--pad) 6px; } +.ar-facts { margin-left: auto; min-width: 0; font-size: 12.5px; font-weight: 600; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ar-facts.wide { margin: 0 0 8px; white-space: normal; } +.btn-line { box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.22); color: #fff; background: transparent; } +.btn-line.on { box-shadow: inset 0 0 0 1.5px var(--accent); color: var(--accent-hover); } +.btn-line:disabled { opacity: 0.5; } +.btn.sm { height: 34px; padding: 0 14px; font-size: 13px; } + +/* the latest release */ +.ar-latest { display: flex; gap: 14px; margin: 6px var(--pad) 4px; padding: 12px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); } +.ar-latest-art { flex: none; width: 96px; height: 96px; border-radius: 10px; overflow: hidden; background: rgba(255, 255, 255, 0.06); display: block; padding: 0; } +.ar-latest-art img { width: 100%; height: 100%; object-fit: cover; display: block; } +.ar-latest-art .ph { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; color: var(--text-muted); } +.ar-latest-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; justify-content: center; } +.ar-latest-copy .k { font-size: 10.5px; letter-spacing: 0.8px; text-transform: uppercase; font-weight: 800; color: var(--accent-hover); } +.ar-latest-t { font-size: 15px; font-weight: 800; letter-spacing: -0.2px; line-height: 1.2; margin: 3px 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ar-latest-copy .m { font-size: 12px; color: var(--text-secondary); font-weight: 600; } +.ar-latest-acts { display: flex; gap: 8px; margin-top: 10px; } + +/* top songs, in columns of four */ +.row.ar-pop .ar-n { width: 18px; text-align: right; color: var(--text-muted); font-weight: 700; font-variant-numeric: tabular-nums; font-size: 13px; flex: none; } +.row.ar-pop .r { display: inline-flex; align-items: center; gap: 10px; color: var(--text-muted); } +.row.ar-pop .ar-dur { font-variant-numeric: tabular-nums; font-size: 12.5px; } +.ar-pages { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 var(--pad); scrollbar-width: none; scroll-padding-inline: var(--pad); } +.ar-pages::-webkit-scrollbar { display: none; } +.ar-col { flex: none; width: 86%; scroll-snap-align: start; } +.ar-col .row { margin: 0 calc(-1 * var(--pad)); } +.ar-col .row.tight { padding-top: 6px; padding-bottom: 6px; } +.ar-dots { display: flex; justify-content: center; gap: 6px; padding: 6px 0 2px; } +.ar-dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(255, 255, 255, 0.22); transition: background var(--m-fast); } +.ar-dots i.on { background: #fff; } + +/* the carousels */ +.rail { scroll-padding-inline: var(--pad); } +.ar-rail .release { width: 156px; } +.ar-rail.sm .release { width: 132px; } +.more-link.ar-disco { display: inline-flex; align-items: center; gap: 4px; padding: 14px var(--pad) 4px; } + +/* about */ +.ar-about { padding: 0 var(--pad); } +.ar-bio { font-size: 13.5px; line-height: 1.6; color: var(--text-secondary); display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; margin: 0; } +.ar-bio.open { display: block; } +.ar-tags { padding: 0; margin-top: 12px; } +.ar-socials { display: flex; gap: 6px; margin-top: 12px; } +.ar-socials .iconbtn { background: rgba(255, 255, 255, 0.055); } + +/* skeletons: the shape of what is coming, so the page never jumps when it + arrives (the mockup's .skel, at the app's radii) */ +.skel { display: block; background: rgba(255, 255, 255, 0.07); border-radius: 6px; animation: skel-breathe 1.4s ease-in-out infinite; } +.skel.ln { height: 11px; } +.skel.art { width: 46px; height: 46px; border-radius: 8px; flex: none; } +.skel.cover-wrap { aspect-ratio: 1; width: 100%; border-radius: var(--r-art); } +.skel-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; } +.skel-row .b { flex: 1; min-width: 0; } +.skel-card { pointer-events: none; } +.ar-latest.skel-card .ar-latest-art { background: rgba(255, 255, 255, 0.07); } +@keyframes skel-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } } + +/* the listener / progress bars other screens draw under a row (CatalogAlbum, + Transfers, Diagnostics) — the artist page's Popular used them too, once */ +.lbar { display: flex; align-items: center; gap: 8px; margin-top: 5px; height: 10px; } +.lbar i { display: block; flex: 1; height: 3px; border-radius: 2px; background: var(--accent); transform-origin: left center; opacity: 0.85; } +.lbar em { font-style: normal; font-size: 10.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; min-width: 32px; text-align: right; } +/* a rail's cards line up at the TOP — a two-line title must not lift its cover */ +.ar-rail { align-items: flex-start; }