forked from Keelrleek/SoulStream
home: rows that fill the window, and news across the whole of it
Kyle picked this off an interactive prototype after four passes: news as a band rather than a sidebar, cards at 150px, rows filling to width, the banner capped at 1400, and no "show all". The news sidebar and the desk grid are gone. News is a full-width band whose column count follows the window — two at 1152, six at 2560 — and the 312px aside stops taking 338px off every row at every width. Rows are wrapping grids now, repeat(auto-fill, 150px), which is the artist page's own release grid. The paging rails went with them, and with the rails went their arrows, their per-rail ResizeObservers and the edge-fade mask. Row caps follow the window instead of being constants: perRow is floor((pageW + 16) / 166) and every row is exactly two lines at any width, which replaced GRID_MAX = 12 and the 6/6/10/6 breaks. Back catalogue goes from 8 cards at 1152 to 24 at 2560 off the same rule. Out soon is dropped on Kyle's instruction — the feed already leads with the same upcoming record as its featured card, so the panel repeated the top of the page. Complete the set and Following come back as bands; the three sections share one snippet, and the news-* classes are band-* because the shape now holds artists as well as headlines. Two bugs fixed on the way. .ax-card is a button with display: block, and a button's content box is vertically centred by the UA, so as a grid item stretched to the row height the slack split above and below the artwork — three-line sub-lines put the image at 9px and one-line at 25px. It is a flex column now. And the entrance dealt a hardcoded eight cards while fill-to-width puts 8 to 24 on screen, so everything past the eighth sat at full opacity while its neighbours faded in; it walks the sections and deals the ones actually on screen, shortening the stagger as the count grows. The news row stays newest-first with no per-artist quota. A round-robin was tried and rejected: it is a news row, and if nothing is happening with an artist there is nothing to say about them. Repetition of one story is bounded in Rust, where it belongs.
This commit is contained in:
1 parent
d397b78887
commit
10e4c75b33
1 file changed
+232
-333
+232
-333
@@ -44,7 +44,20 @@
|
|||||||
* already announces. Applied to titles because Google News RSS cannot
|
* already announces. Applied to titles because Google News RSS cannot
|
||||||
* express it in the query. */
|
* express it in the query. */
|
||||||
const REVIEW_RE = /\breview(s|ed)?\b|album of the week|\branked\b|best songs of|best albums of/i;
|
const REVIEW_RE = /\breview(s|ed)?\b|album of the week|\branked\b|best songs of|best albums of/i;
|
||||||
const GRID_MAX = 12;
|
|
||||||
|
/* Rows follow the WINDOW, not a constant. Cards are the artist page's
|
||||||
|
* fixed 150 + 16 gutter, so how many fit across is arithmetic, and every
|
||||||
|
* row carries exactly two of them: 8 cards at 1152, 24 at 2560.
|
||||||
|
*
|
||||||
|
* This replaces GRID_MAX = 12 and the 6/6/10/6 breaks below. Measured on
|
||||||
|
* the layout prototype with Kyle's real follows, those constants were
|
||||||
|
* throwing work away — the back catalogue had 92 albums to offer and
|
||||||
|
* showed six of them at every width. */
|
||||||
|
const CARD = 150;
|
||||||
|
const GAP = 16;
|
||||||
|
/** Must match the `minmax(300px, 1fr)` and 30px gutter of .band. */
|
||||||
|
const BAND_COL = 300;
|
||||||
|
const BAND_GAP = 30;
|
||||||
|
|
||||||
/* ------------------------------------------------------------- helpers */
|
/* ------------------------------------------------------------- helpers */
|
||||||
const DAY = 86400000;
|
const DAY = 86400000;
|
||||||
@@ -156,11 +169,19 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The page's own content width, bound off .home-page below. Every row
|
||||||
|
* cap hangs off it. perRow only changes when a whole card's worth of
|
||||||
|
* width appears, so the caps re-derive on a pixel drag but their VALUE
|
||||||
|
* holds steady and nothing downstream re-runs. */
|
||||||
|
let pageW = $state(0);
|
||||||
|
const perRow = $derived(Math.max(1, Math.floor((pageW + GAP) / (CARD + GAP))));
|
||||||
|
const rowCap = $derived(Math.max(8, perRow * 2));
|
||||||
|
const bandCols = $derived(Math.max(1, Math.floor((pageW + BAND_GAP) / (BAND_COL + BAND_GAP))));
|
||||||
|
|
||||||
const feed = $derived(assembleFeed(WINDOW_DAYS));
|
const feed = $derived(assembleFeed(WINDOW_DAYS));
|
||||||
const lead = $derived(feed[0] ?? null);
|
const lead = $derived(feed[0] ?? null);
|
||||||
const gridItems = $derived(feed.slice(1, 1 + GRID_MAX));
|
const gridItems = $derived(feed.slice(1, 1 + rowCap));
|
||||||
const fresh31 = $derived(feed.filter((r) => !r.upcoming && daysFrom(r.release_date) <= 31).length);
|
const fresh31 = $derived(feed.filter((r) => !r.upcoming && daysFrom(r.release_date) <= 31).length);
|
||||||
const outSoon = $derived(assembleFeed(3650).filter((r) => r.upcoming));
|
|
||||||
|
|
||||||
/* Recents, minus edition dupes (Ill Times + its Deluxe is one record) —
|
/* Recents, minus edition dupes (Ill Times + its Deluxe is one record) —
|
||||||
* NOT deduped against the feed: a record can be new AND what you were
|
* NOT deduped against the feed: a record can be new AND what you were
|
||||||
@@ -173,7 +194,7 @@
|
|||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) continue;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
rows.push(r);
|
rows.push(r);
|
||||||
if (rows.length >= 6) break;
|
if (rows.length >= rowCap) break;
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
});
|
});
|
||||||
@@ -195,7 +216,7 @@
|
|||||||
const picks = [];
|
const picks = [];
|
||||||
for (const pass of [0, 1]) {
|
for (const pass of [0, 1]) {
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
if (picks.length >= 6) break;
|
if (picks.length >= rowCap) break;
|
||||||
const once = picks.some((p) => p.artistName === r.artistName);
|
const once = picks.some((p) => p.artistName === r.artistName);
|
||||||
if (pass === 0 && once) continue;
|
if (pass === 0 && once) continue;
|
||||||
const key = r.artistName + "|" + baseTitle(r.title);
|
const key = r.artistName + "|" + baseTitle(r.title);
|
||||||
@@ -212,24 +233,34 @@
|
|||||||
const followed = new Set(slots.map((s) => s.name));
|
const followed = new Set(slots.map((s) => s.name));
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const pool = [];
|
const pool = [];
|
||||||
for (let round = 0; round < 2 && pool.length < 10; round++) {
|
// Rounds run until the well is dry rather than stopping at two: with
|
||||||
|
// a wide window the row wants more than the ten that produced.
|
||||||
|
for (let round = 0; pool.length < rowCap; round++) {
|
||||||
|
let added = 0;
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
const pick = (slot.related ?? []).filter(
|
const pick = (slot.related ?? []).filter(
|
||||||
(r) => !followed.has(r.name) && !seen.has(r.name),
|
(r) => !followed.has(r.name) && !seen.has(r.name),
|
||||||
)[round === 0 ? 0 : 1];
|
)[round];
|
||||||
if (pick) {
|
if (pick) {
|
||||||
seen.add(pick.name);
|
seen.add(pick.name);
|
||||||
pool.push({ ...pick, because: slot.name });
|
pool.push({ ...pick, because: slot.name });
|
||||||
if (pool.length >= 10) break;
|
added++;
|
||||||
|
if (pool.length >= rowCap) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!added) break;
|
||||||
}
|
}
|
||||||
return pool;
|
return pool;
|
||||||
});
|
});
|
||||||
|
|
||||||
const libraryRows = $derived(libraryStore.albums.slice(0, 6));
|
const libraryRows = $derived(libraryStore.albums.slice(0, rowCap));
|
||||||
|
|
||||||
/* IDEA (approved): own some of an artist's albums with a new one coming —
|
/* Two of the three sidebar panels come back as bands — the news
|
||||||
|
* treatment, applied again. "Out soon" does NOT: the feed already leads
|
||||||
|
* with the same upcoming record as its featured card, so the panel was
|
||||||
|
* repeating the top of the page. Kyle dropped it on 2026-08-05.
|
||||||
|
*
|
||||||
|
* IDEA (approved): own some of an artist's albums with a new one coming —
|
||||||
* the shelf gap. Name-matched against the library, the same way the
|
* the shelf gap. Name-matched against the library, the same way the
|
||||||
* artist page's own owned-count works. */
|
* artist page's own owned-count works. */
|
||||||
const completeSet = $derived.by(() => {
|
const completeSet = $derived.by(() => {
|
||||||
@@ -245,6 +276,9 @@
|
|||||||
return { slot: s, own: owned[s.name], albums, up };
|
return { slot: s, own: owned[s.name], albums, up };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const setNote = (row) =>
|
||||||
|
`You own ${row.own} of ${row.albums} albums`
|
||||||
|
+ (row.up ? ` · new one ${inDays(row.up.release_date)}` : "");
|
||||||
|
|
||||||
const following = $derived(
|
const following = $derived(
|
||||||
slots.map((s) => {
|
slots.map((s) => {
|
||||||
@@ -254,15 +288,35 @@
|
|||||||
return { slot: s, latest };
|
return { slot: s, latest };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const followNote = (row) =>
|
||||||
|
row.latest
|
||||||
|
? future(row.latest.release_date)
|
||||||
|
? "next " + inDays(row.latest.release_date)
|
||||||
|
: "latest " + age(row.latest.release_date)
|
||||||
|
: "no releases known";
|
||||||
|
|
||||||
|
/* Newest first across every follow, with NO per-artist quota.
|
||||||
|
*
|
||||||
|
* This is a deliberate decision, not an oversight. Rust now returns up to
|
||||||
|
* 40 relevant deduplicated items per artist, so a busy week for one band
|
||||||
|
* can take most of the band — measured 2026-08-05, King Gizzard held 10
|
||||||
|
* of 12 slots while four follows appeared not at all. A round-robin quota
|
||||||
|
* was tried and rejected: it is a NEWS row, and if nothing is happening
|
||||||
|
* with an artist there is nothing to say about them. Spreading the slots
|
||||||
|
* evenly would mean showing staler items from quiet artists in place of
|
||||||
|
* what actually happened today.
|
||||||
|
*
|
||||||
|
* The items crowding the row are genuinely different stories — the
|
||||||
|
* duplicate reports of one story are already collapsed in Rust — so this
|
||||||
|
* is real coverage, not repetition. */
|
||||||
const newsItems = $derived(
|
const newsItems = $derived(
|
||||||
homeStore.news
|
homeStore.news
|
||||||
.filter((n) => n.date > 0 && (Date.now() / 1000 - n.date) / 86400 <= NEWS_MAX_AGE_DAYS)
|
.filter((n) => n.date > 0 && (Date.now() / 1000 - n.date) / 86400 <= NEWS_MAX_AGE_DAYS)
|
||||||
.filter((n) => !REVIEW_RE.test(n.title))
|
.filter((n) => !REVIEW_RE.test(n.title))
|
||||||
// Eleven follows yield ~35 fresh items; the panel is a sidebar,
|
// Three rows of whatever the band is currently columned at, so a
|
||||||
// not a feed reader. Newest dozen — the 45-day window above is
|
// wide window reads more of the 45 days rather than the same
|
||||||
// the honesty rule, this is just column height.
|
// dozen a sidebar column had room for.
|
||||||
.slice(0, 12),
|
.slice(0, Math.max(12, bandCols * 3)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const empty = $derived(
|
const empty = $derived(
|
||||||
@@ -336,39 +390,11 @@
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rails — Spotify's paging gesture in the app's clothes. The action wires
|
/* The paging rails are gone: rows wrap into the artist page's auto-fill
|
||||||
* the two buttons and keeps the edge-fade attributes honest. */
|
* grid instead. That takes their arrows, their per-rail ResizeObservers
|
||||||
function rail(node) {
|
* and the edge-fade mask with them — the mask ramped a fixed 64px into a
|
||||||
const track = node.querySelector(".rail-track");
|
* 150px card, so whatever was left over at the right edge (18px at 1920,
|
||||||
const left = node.querySelector(".rail-btn.left");
|
* 160px at 2560) was smeared into an unreadable stub. */
|
||||||
const right = node.querySelector(".rail-btn.right");
|
|
||||||
const sync = () => {
|
|
||||||
const max = track.scrollWidth - track.clientWidth - 1;
|
|
||||||
node.dataset.left = track.scrollLeft > 4 ? "1" : "";
|
|
||||||
node.dataset.right = track.scrollLeft < max ? "1" : "";
|
|
||||||
left.disabled = track.scrollLeft <= 4;
|
|
||||||
right.disabled = track.scrollLeft >= max;
|
|
||||||
};
|
|
||||||
// A viewport minus one card, so the last visible column stays as the
|
|
||||||
// next page's first — the reader keeps their place.
|
|
||||||
const page = (dir) => track.scrollBy({ left: dir * (track.clientWidth - 166), behavior: "smooth" });
|
|
||||||
const goL = () => page(-1);
|
|
||||||
const goR = () => page(1);
|
|
||||||
left.addEventListener("click", goL);
|
|
||||||
right.addEventListener("click", goR);
|
|
||||||
track.addEventListener("scroll", sync, { passive: true });
|
|
||||||
const ro = new ResizeObserver(sync);
|
|
||||||
ro.observe(track);
|
|
||||||
sync();
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
left.removeEventListener("click", goL);
|
|
||||||
right.removeEventListener("click", goR);
|
|
||||||
track.removeEventListener("scroll", sync);
|
|
||||||
ro.disconnect();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------- the entrance
|
/* -------------------------------------------------------- the entrance
|
||||||
* CatalogDiscography's dealOne/playEntrance with its four approved W1
|
* CatalogDiscography's dealOne/playEntrance with its four approved W1
|
||||||
@@ -410,7 +436,7 @@
|
|||||||
async function playEntrance() {
|
async function playEntrance() {
|
||||||
if (entrancePlayed || !pageEl || reducedMotion()) return;
|
if (entrancePlayed || !pageEl || reducedMotion()) return;
|
||||||
entrancePlayed = true;
|
entrancePlayed = true;
|
||||||
const firstImg = pageEl.querySelector(".feat img, .rail-track img");
|
const firstImg = pageEl.querySelector(".feat img, .grid img");
|
||||||
if (firstImg && firstImg.decode) {
|
if (firstImg && firstImg.decode) {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
firstImg.decode().catch(() => {}),
|
firstImg.decode().catch(() => {}),
|
||||||
@@ -419,22 +445,47 @@
|
|||||||
}
|
}
|
||||||
await new Promise(requestAnimationFrame);
|
await new Promise(requestAnimationFrame);
|
||||||
if (!pageEl) return;
|
if (!pageEl) return;
|
||||||
const chrome = [
|
|
||||||
["[data-greet]", 0, true],
|
/* Deal the SECTIONS that are on screen, not "the first row plus four
|
||||||
[".col .ax-head", 40, true],
|
* panels". A row is no longer a fixed handful of cards — fill-to-width
|
||||||
[".col .feat", 60, true],
|
* puts 8 to 24 in one — so a constant ENTER_ROWS left everything past
|
||||||
];
|
* the eighth sitting at full opacity while its neighbours faded in.
|
||||||
for (const [sel, delay, lift] of chrome) {
|
* Anything below the fold is still left simply present, the way the
|
||||||
const el = pageEl.querySelector(sel);
|
* discography page leaves it. */
|
||||||
if (el) dealOne(el, delay, lift);
|
const paneBox = pageEl.getBoundingClientRect();
|
||||||
|
const scrollTop = pageEl.closest(".content")?.getBoundingClientRect() ?? paneBox;
|
||||||
|
const onScreen = (el) => {
|
||||||
|
const b = el.getBoundingClientRect();
|
||||||
|
return b.width > 0 && b.height > 0
|
||||||
|
&& b.bottom > scrollTop.top + 1 && b.top < scrollTop.bottom - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const greet = pageEl.querySelector("[data-greet]");
|
||||||
|
if (greet) dealOne(greet, 0, true);
|
||||||
|
|
||||||
|
let t = 40;
|
||||||
|
for (const head of pageEl.querySelectorAll(".ax-head")) {
|
||||||
|
const items = [];
|
||||||
|
let n = head.nextElementSibling;
|
||||||
|
while (n && !n.classList.contains("ax-head")) {
|
||||||
|
if (n.classList.contains("feat")) items.push(n);
|
||||||
|
else if (n.classList.contains("grid")) items.push(...n.querySelectorAll(".release, .ax-card"));
|
||||||
|
else if (n.classList.contains("band")) items.push(...n.querySelectorAll(".band-item"));
|
||||||
|
n = n.nextElementSibling;
|
||||||
}
|
}
|
||||||
const first = pageEl.querySelector(".rail");
|
const shown = items.filter(onScreen);
|
||||||
const cards = first ? first.querySelectorAll(".release") : [];
|
const headShown = onScreen(head);
|
||||||
for (let i = 0; i < Math.min(cards.length, ENTER_ROWS); i++) {
|
if (!shown.length && !headShown) continue;
|
||||||
dealOne(cards[i], 80 + i * ENTER_STEP, true);
|
if (headShown) dealOne(head, t, true);
|
||||||
|
// Hold each section to roughly the 8 x ENTER_STEP the deal was
|
||||||
|
// approved at: more items shorten the step rather than letting the
|
||||||
|
// last card land most of a second late.
|
||||||
|
const step = shown.length <= ENTER_ROWS
|
||||||
|
? ENTER_STEP
|
||||||
|
: Math.max(16, Math.round((ENTER_ROWS * ENTER_STEP) / shown.length));
|
||||||
|
shown.forEach((el, i) => dealOne(el, t + 40 + i * step, true));
|
||||||
|
t += 70;
|
||||||
}
|
}
|
||||||
[...pageEl.querySelectorAll(".panel")].slice(0, 4)
|
|
||||||
.forEach((p, i) => dealOne(p, 100 + i * ENTER_STEP, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ handlers */
|
/* ------------------------------------------------------------ handlers */
|
||||||
@@ -450,10 +501,6 @@
|
|||||||
}
|
}
|
||||||
const openNews = (n) => api.openExternal(n.url);
|
const openNews = (n) => api.openExternal(n.url);
|
||||||
|
|
||||||
/** "Show all" unfolds the rail into a wrapped grid in place — there is no
|
|
||||||
* full-feed page yet, and a button that navigates nowhere would lie. */
|
|
||||||
const expanded = $state({});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void loadHome();
|
void loadHome();
|
||||||
if (!libraryStore.loaded) void refreshLibrary();
|
if (!libraryStore.loaded) void refreshLibrary();
|
||||||
@@ -491,7 +538,7 @@
|
|||||||
<button class="cta" onclick={() => goToView("search")}>Start searching</button>
|
<button class="cta" onclick={() => goToView("search")}>Start searching</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="home-page" bind:this={pageEl}>
|
<div class="home-page" bind:this={pageEl} bind:clientWidth={pageW}>
|
||||||
{#if lead}
|
{#if lead}
|
||||||
<div class="ambience" style="background-image: url('{lead.cover}')"></div>
|
<div class="ambience" style="background-image: url('{lead.cover}')"></div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -524,17 +571,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="desk">
|
|
||||||
<div class="col">
|
|
||||||
<!-- ---- New from your artists -------------------------- -->
|
<!-- ---- New from your artists -------------------------- -->
|
||||||
<div class="ax-head">
|
<div class="ax-head">
|
||||||
<h2 class="ax-title">
|
<h2 class="ax-title">
|
||||||
New from your artists
|
New from your artists
|
||||||
<span class="ax-note">· {homeStore.follows.length} followed</span>
|
<span class="ax-note">· {homeStore.follows.length} followed</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button class="disco-all" onclick={() => (expanded.fresh = !expanded.fresh)}>
|
|
||||||
{expanded.fresh ? "Show less" : "Show all"} <Icon name="chevron-right" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{#if lead}
|
{#if lead}
|
||||||
<div class="feat" use:cardAccent={lead.cover}>
|
<div class="feat" use:cardAccent={lead.cover}>
|
||||||
@@ -606,9 +648,7 @@
|
|||||||
<div class="sub">{sub}</div>
|
<div class="sub">{sub}</div>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<div class="rail" class:open={expanded.fresh} use:rail>
|
<div class="grid">
|
||||||
<button class="rail-btn left" disabled aria-label="Scroll back"><Icon name="chevron-left" size={16} /></button>
|
|
||||||
<div class="rail-track">
|
|
||||||
{#each gridItems as r (r.artistName + r.title)}
|
{#each gridItems as r (r.artistName + r.title)}
|
||||||
{@render releaseCard(
|
{@render releaseCard(
|
||||||
r,
|
r,
|
||||||
@@ -618,17 +658,52 @@
|
|||||||
)}
|
)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="rail-btn right" aria-label="Scroll forward"><Icon name="chevron-right" size={16} /></button>
|
|
||||||
|
<!-- One band row, shared by News, Complete the set and Following.
|
||||||
|
`first` clears the top rule for the leading `bandCols` items —
|
||||||
|
the grid fills row-major, so those are every COLUMN's first. -->
|
||||||
|
{#snippet bandRow(pic, title, meta, first, open)}
|
||||||
|
<button class="band-item" class:first title={title} onclick={open}>
|
||||||
|
<span class="band-dot">
|
||||||
|
{#if pic}<img src={pic} alt="" use:reveal />{/if}
|
||||||
|
</span>
|
||||||
|
<span class="band-b">
|
||||||
|
<span class="band-t">{title}</span>
|
||||||
|
<span class="band-m">{meta}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<!-- ---- News ------------------------------------------- -->
|
||||||
|
<!-- A full-width band whose COLUMN COUNT follows the window,
|
||||||
|
three at 1150 and six at 2400, in place of the 312px
|
||||||
|
sidebar that took 338px off every row at every width. -->
|
||||||
|
{#if newsItems.length}
|
||||||
|
<div class="ax-head">
|
||||||
|
<h2 class="ax-title">
|
||||||
|
News
|
||||||
|
<span class="ax-note">· from your follows · last {NEWS_MAX_AGE_DAYS} days</span>
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="band">
|
||||||
|
{#each newsItems as n, i (n.url)}
|
||||||
|
{@render bandRow(
|
||||||
|
slots.find((s) => s.name === n.artist)?.picture,
|
||||||
|
n.title,
|
||||||
|
n.source + " · " + age(new Date(n.date * 1000).toISOString()),
|
||||||
|
i < bandCols,
|
||||||
|
() => openNews(n),
|
||||||
|
)}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- ---- Jump back in ----------------------------------- -->
|
<!-- ---- Jump back in ----------------------------------- -->
|
||||||
{#if jumpRows.length}
|
{#if jumpRows.length}
|
||||||
<div class="ax-head">
|
<div class="ax-head">
|
||||||
<h2 class="ax-title">Jump back in <span class="ax-note">· picks up where you left off</span></h2>
|
<h2 class="ax-title">Jump back in <span class="ax-note">· picks up where you left off</span></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rail" use:rail>
|
<div class="grid">
|
||||||
<button class="rail-btn left" disabled aria-label="Scroll back"><Icon name="chevron-left" size={16} /></button>
|
|
||||||
<div class="rail-track">
|
|
||||||
{#each jumpRows as r (r.artist + r.title)}
|
{#each jumpRows as r (r.artist + r.title)}
|
||||||
<div
|
<div
|
||||||
class="release"
|
class="release"
|
||||||
@@ -656,8 +731,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="rail-btn right" aria-label="Scroll forward"><Icon name="chevron-right" size={16} /></button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ---- From the back catalogue (idea, approved) -------- -->
|
<!-- ---- From the back catalogue (idea, approved) -------- -->
|
||||||
@@ -665,15 +738,11 @@
|
|||||||
<div class="ax-head">
|
<div class="ax-head">
|
||||||
<h2 class="ax-title">From the back catalogue <span class="ax-note">· albums you may have missed</span></h2>
|
<h2 class="ax-title">From the back catalogue <span class="ax-note">· albums you may have missed</span></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rail" use:rail>
|
<div class="grid">
|
||||||
<button class="rail-btn left" disabled aria-label="Scroll back"><Icon name="chevron-left" size={16} /></button>
|
|
||||||
<div class="rail-track">
|
|
||||||
{#each backCatalogue as r (r.artistName + r.title)}
|
{#each backCatalogue as r (r.artistName + r.title)}
|
||||||
{@render releaseCard(r, r.artistName, age(r.release_date), !r.mbOnly)}
|
{@render releaseCard(r, r.artistName, age(r.release_date), !r.mbOnly)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="rail-btn right" aria-label="Scroll forward"><Icon name="chevron-right" size={16} /></button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ---- Recommended ------------------------------------ -->
|
<!-- ---- Recommended ------------------------------------ -->
|
||||||
@@ -681,9 +750,7 @@
|
|||||||
<div class="ax-head">
|
<div class="ax-head">
|
||||||
<h2 class="ax-title">Recommended for you <span class="ax-note">· from everyone you follow</span></h2>
|
<h2 class="ax-title">Recommended for you <span class="ax-note">· from everyone you follow</span></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rail" use:rail>
|
<div class="grid">
|
||||||
<button class="rail-btn left" disabled aria-label="Scroll back"><Icon name="chevron-left" size={16} /></button>
|
|
||||||
<div class="rail-track">
|
|
||||||
{#each because as r (r.id)}
|
{#each because as r (r.id)}
|
||||||
<button class="ax-card" onclick={() => openCatalogArtist(r.id, r)}>
|
<button class="ax-card" onclick={() => openCatalogArtist(r.id, r)}>
|
||||||
<span class="ax-art">
|
<span class="ax-art">
|
||||||
@@ -694,8 +761,6 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="rail-btn right" aria-label="Scroll forward"><Icon name="chevron-right" size={16} /></button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ---- Your library (idea, approved) ------------------- -->
|
<!-- ---- Your library (idea, approved) ------------------- -->
|
||||||
@@ -703,9 +768,7 @@
|
|||||||
<div class="ax-head">
|
<div class="ax-head">
|
||||||
<h2 class="ax-title">Your library <span class="ax-note">· {libraryStore.albums.length} albums</span></h2>
|
<h2 class="ax-title">Your library <span class="ax-note">· {libraryStore.albums.length} albums</span></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rail" use:rail>
|
<div class="grid">
|
||||||
<button class="rail-btn left" disabled aria-label="Scroll back"><Icon name="chevron-left" size={16} /></button>
|
|
||||||
<div class="rail-track">
|
|
||||||
{#each libraryRows as a (a.id)}
|
{#each libraryRows as a (a.id)}
|
||||||
<div
|
<div
|
||||||
class="release"
|
class="release"
|
||||||
@@ -728,99 +791,43 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="rail-btn right" aria-label="Scroll forward"><Icon name="chevron-right" size={16} /></button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="aside">
|
|
||||||
{#if newsItems.length}
|
|
||||||
<div class="panel">
|
|
||||||
<h3>News <em>· from your follows · last {NEWS_MAX_AGE_DAYS} days</em></h3>
|
|
||||||
<div class="news">
|
|
||||||
{#each newsItems as n (n.url)}
|
|
||||||
<button class="news-item" title={n.title} onclick={() => openNews(n)}>
|
|
||||||
<span class="news-dot">
|
|
||||||
{#if slots.find((s) => s.name === n.artist)?.picture}
|
|
||||||
<img src={slots.find((s) => s.name === n.artist).picture} alt="" use:reveal />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
<span class="news-b">
|
|
||||||
<span class="news-t">{n.title}</span>
|
|
||||||
<span class="news-m">{n.source} · {age(new Date(n.date * 1000).toISOString())}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if outSoon.length}
|
|
||||||
<div class="panel">
|
|
||||||
<h3>Out soon</h3>
|
|
||||||
<div class="ax-owned own-col">
|
|
||||||
{#each outSoon as r (r.artistName + r.title)}
|
|
||||||
<button class="ax-own" onclick={() => openRelease(r)}>
|
|
||||||
<span class="ax-own-art"><img src={r.cover} alt="" use:reveal /></span>
|
|
||||||
<span class="ax-own-copy">
|
|
||||||
<span class="ax-own-t">{r.title}</span>
|
|
||||||
<span class="ax-own-m">{r.artistName} · {inDays(r.release_date)}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- ---- Complete the set (idea, approved) --------------- -->
|
||||||
{#if completeSet.length}
|
{#if completeSet.length}
|
||||||
<div class="panel">
|
<div class="ax-head">
|
||||||
<h3>Complete the set</h3>
|
<h2 class="ax-title">Complete the set <span class="ax-note">· gaps on your shelf</span></h2>
|
||||||
<div class="ax-owned own-col">
|
|
||||||
{#each completeSet as row (row.slot.id)}
|
|
||||||
<button class="ax-own" onclick={() => openCatalogArtist(row.slot.id, { id: row.slot.id, name: row.slot.name })}>
|
|
||||||
<span class="ax-own-art round">
|
|
||||||
{#if row.slot.picture}<img src={row.slot.picture} alt="" use:reveal />{/if}
|
|
||||||
</span>
|
|
||||||
<span class="ax-own-copy">
|
|
||||||
<span class="ax-own-t">{row.slot.name}</span>
|
|
||||||
<span class="ax-own-m">
|
|
||||||
You own {row.own} of {row.albums} albums{row.up
|
|
||||||
? ` · new one ${inDays(row.up.release_date)}`
|
|
||||||
: ""}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="band">
|
||||||
|
{#each completeSet as row, i (row.slot.id)}
|
||||||
|
{@render bandRow(
|
||||||
|
row.slot.picture,
|
||||||
|
row.slot.name,
|
||||||
|
setNote(row),
|
||||||
|
i < bandCols,
|
||||||
|
() => openCatalogArtist(row.slot.id, { id: row.slot.id, name: row.slot.name }),
|
||||||
|
)}
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- ---- Following -------------------------------------- -->
|
||||||
{#if following.length}
|
{#if following.length}
|
||||||
<div class="panel">
|
<div class="ax-head">
|
||||||
<h3>Following <em>· {homeStore.follows.length}</em></h3>
|
<h2 class="ax-title">Following <span class="ax-note">· {homeStore.follows.length} artists</span></h2>
|
||||||
<div class="ax-owned own-col">
|
</div>
|
||||||
{#each following as row (row.slot.id)}
|
<div class="band">
|
||||||
<button class="ax-own" onclick={() => openCatalogArtist(row.slot.id, { id: row.slot.id, name: row.slot.name })}>
|
{#each following as row, i (row.slot.id)}
|
||||||
<span class="ax-own-art round">
|
{@render bandRow(
|
||||||
{#if row.slot.picture}<img src={row.slot.picture} alt="" use:reveal />{/if}
|
row.slot.picture,
|
||||||
</span>
|
row.slot.name,
|
||||||
<span class="ax-own-copy">
|
followNote(row),
|
||||||
<span class="ax-own-t">{row.slot.name}</span>
|
i < bandCols,
|
||||||
<span class="ax-own-m">
|
() => openCatalogArtist(row.slot.id, { id: row.slot.id, name: row.slot.name }),
|
||||||
{row.latest
|
)}
|
||||||
? future(row.latest.release_date)
|
|
||||||
? "next " + inDays(row.latest.release_date)
|
|
||||||
: "latest " + age(row.latest.release_date)
|
|
||||||
: "no releases known"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -857,23 +864,17 @@
|
|||||||
mask-image: linear-gradient(180deg, #000 40%, transparent);
|
mask-image: linear-gradient(180deg, #000 40%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= the desk ================= */
|
/* ================= rows =================
|
||||||
.desk {
|
The artist page's own release grid: fixed 150px tracks, auto-filled, so
|
||||||
|
a row carries as many as the window has room for and wraps rather than
|
||||||
|
scrolling. It replaces the paging rail — and with it the arrows, the
|
||||||
|
per-rail ResizeObservers and the edge-fade mask. */
|
||||||
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 312px;
|
grid-template-columns: repeat(auto-fill, 150px);
|
||||||
gap: 26px;
|
gap: 4px 16px;
|
||||||
align-items: start;
|
justify-content: start;
|
||||||
}
|
min-width: 0;
|
||||||
/* A grid item's min-width is auto — without this the column refuses to
|
|
||||||
shrink below its widest rail, the rail never clips, and the PANE
|
|
||||||
scrolls sideways instead of the track. */
|
|
||||||
.desk > * { min-width: 0; }
|
|
||||||
.aside {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--bar-h) + 16px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ section heads (ArtistExtras, P3 sizes) ============ */
|
/* ============ section heads (ArtistExtras, P3 sizes) ============ */
|
||||||
@@ -894,20 +895,6 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.disco-all {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12.5px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 5px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.disco-all:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-highlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============ the featured card (ArtistExtras .feat) ============ */
|
/* ============ the featured card (ArtistExtras .feat) ============ */
|
||||||
.feat {
|
.feat {
|
||||||
@@ -918,8 +905,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.5);
|
||||||
max-width: 900px;
|
/* 900 left ~1200px of dead page to the right of the greeting and the
|
||||||
/* mock: the banner holds the rail off (P3: 34px under it) */
|
lead card on a 2560 monitor. Full-width is worse — the release card
|
||||||
|
gets a long empty right half — and centring pulls it off the left
|
||||||
|
edge its own section head sits on. 1400 is the middle Kyle picked. */
|
||||||
|
max-width: 1400px;
|
||||||
|
/* mock: the banner holds the row off (P3: 34px under it) */
|
||||||
margin-bottom: 34px;
|
margin-bottom: 34px;
|
||||||
}
|
}
|
||||||
.feat-bg {
|
.feat-bg {
|
||||||
@@ -1069,63 +1060,6 @@
|
|||||||
}
|
}
|
||||||
.avatars img:first-child { margin-left: 0; }
|
.avatars img:first-child { margin-left: 0; }
|
||||||
|
|
||||||
/* ============ rails (the mock's Spotify gesture) ============ */
|
|
||||||
.rail { position: relative; min-width: 0; }
|
|
||||||
.rail-track {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
overflow-x: auto;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
padding: 2px 2px 4px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.rail-track::-webkit-scrollbar { display: none; }
|
|
||||||
.rail-track > :global(*) {
|
|
||||||
flex: 0 0 150px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
/* "Show all" unfolds the rail in place. */
|
|
||||||
.rail.open .rail-track { flex-wrap: wrap; overflow: visible; }
|
|
||||||
.rail.open .rail-btn { display: none; }
|
|
||||||
.rail:global([data-right="1"]) .rail-track {
|
|
||||||
-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 64px), transparent);
|
|
||||||
mask-image: linear-gradient(90deg, #000 calc(100% - 64px), transparent);
|
|
||||||
}
|
|
||||||
.rail:global([data-left="1"][data-right="1"]) .rail-track {
|
|
||||||
-webkit-mask-image: linear-gradient(90deg, transparent, #000 64px, #000 calc(100% - 64px), transparent);
|
|
||||||
mask-image: linear-gradient(90deg, transparent, #000 64px, #000 calc(100% - 64px), transparent);
|
|
||||||
}
|
|
||||||
.rail:global([data-left="1"]:not([data-right="1"])) .rail-track {
|
|
||||||
-webkit-mask-image: linear-gradient(90deg, transparent, #000 64px);
|
|
||||||
mask-image: linear-gradient(90deg, transparent, #000 64px);
|
|
||||||
}
|
|
||||||
.rail-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 52px;
|
|
||||||
z-index: 5;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: rgba(10, 10, 14, 0.72);
|
|
||||||
backdrop-filter: blur(10px) saturate(1.4);
|
|
||||||
-webkit-backdrop-filter: blur(10px) saturate(1.4);
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.09);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
transition: opacity 0.16s ease, transform 0.16s ease, background 0.16s ease;
|
|
||||||
}
|
|
||||||
.rail:hover .rail-btn { opacity: 1; transform: none; }
|
|
||||||
.rail-btn:hover { background: rgba(30, 30, 36, 0.9); }
|
|
||||||
.rail-btn[disabled] { opacity: 0 !important; pointer-events: none; }
|
|
||||||
.rail-btn.left { left: -15px; }
|
|
||||||
.rail-btn.right { right: -15px; }
|
|
||||||
|
|
||||||
/* ============ release cards (CatalogArtist, verbatim) ============ */
|
/* ============ release cards (CatalogArtist, verbatim) ============ */
|
||||||
.release {
|
.release {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1240,8 +1174,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============ artist cards (ArtistExtras) ============ */
|
/* ============ artist cards (ArtistExtras) ============ */
|
||||||
|
/* flex column, NOT block. These are <button>s, and a button lays its
|
||||||
|
content out in an anonymous box the UA centres VERTICALLY. In
|
||||||
|
ArtistExtras' rail that never showed, because a flex item is sized by
|
||||||
|
its content; as a grid item it stretches to the tallest card in the
|
||||||
|
row, and the leftover height was splitting above and below the
|
||||||
|
artwork. "Because you follow King Gizzard & The Lizard Wizard" wraps to
|
||||||
|
three lines where "Because you follow toe" takes one, so the artwork
|
||||||
|
sat 16px lower on the short ones. Column flow puts the slack under the
|
||||||
|
text, where it belongs, and keeps the stretch so a row's hover targets
|
||||||
|
stay the same size. */
|
||||||
.ax-card {
|
.ax-card {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1292,25 +1238,17 @@
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ aside panels (the mock's, verbatim) ============ */
|
/* ============ the news band ============
|
||||||
.panel {
|
Was a 312px sidebar column; now a full-width block whose column count
|
||||||
background: var(--bg-elevated);
|
follows the window — three at 1150, six at 2400. BAND_COL/BAND_GAP in
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
the script mirror these two numbers to know which items open a column. */
|
||||||
border-radius: 10px;
|
.band {
|
||||||
padding: 15px 16px;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 0 30px;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
.panel h3 {
|
.band-item {
|
||||||
font-size: 12.5px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin: 0 0 12px;
|
|
||||||
}
|
|
||||||
.panel h3 em {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.news { display: flex; flex-direction: column; }
|
|
||||||
.news-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 11px;
|
gap: 11px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
@@ -1319,9 +1257,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.news-item:first-child { border-top: 0; padding-top: 0; }
|
/* Every COLUMN's first row loses the rule, not just the first column's —
|
||||||
.news-item:hover .news-t { color: #fff; }
|
the grid fills row-major, so those are the leading `bandCols` items. */
|
||||||
.news-dot {
|
.band-item.first { border-top: 0; padding-top: 0; }
|
||||||
|
.band-item:hover .band-t { color: #fff; }
|
||||||
|
.band-dot {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -1329,10 +1269,10 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
background: var(--bg-highlight);
|
background: var(--bg-highlight);
|
||||||
}
|
}
|
||||||
.news-dot img { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 300ms ease; }
|
.band-dot img { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 300ms ease; }
|
||||||
.news-dot img:global(.shown) { opacity: 1; }
|
.band-dot img:global(.shown) { opacity: 1; }
|
||||||
.news-b { min-width: 0; }
|
.band-b { min-width: 0; }
|
||||||
.news-t {
|
.band-t {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
@@ -1340,50 +1280,9 @@
|
|||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.news-m { font-size: 11px; color: var(--text-muted); }
|
.band-m { font-size: 11px; color: var(--text-muted); }
|
||||||
|
|
||||||
/* ArtistExtras' rows, stacked in a column inside panels. */
|
/* ArtistExtras' rows, stacked in a column inside panels. */
|
||||||
.ax-owned { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
||||||
.ax-owned.own-col { flex-direction: column; gap: 8px; flex-wrap: nowrap; }
|
|
||||||
.ax-own {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 14px 8px 8px;
|
|
||||||
background: var(--bg-elevated);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: left;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
.ax-own:hover { background: var(--bg-highlight); }
|
|
||||||
.ax-own-art {
|
|
||||||
display: block;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: none;
|
|
||||||
background: var(--bg-highlight);
|
|
||||||
}
|
|
||||||
.ax-own-art.round { border-radius: 50%; }
|
|
||||||
.ax-own-art img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: 0; transition: opacity 300ms ease; }
|
|
||||||
.ax-own-art img:global(.shown) { opacity: 1; }
|
|
||||||
.ax-own-copy { min-width: 0; }
|
|
||||||
.ax-own-t {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.ax-own-m {
|
|
||||||
display: block;
|
|
||||||
font-size: 11.5px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============ empty state (the old Home's, kept) ============ */
|
/* ============ empty state (the old Home's, kept) ============ */
|
||||||
.empty {
|
.empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in new issue
Block a user