android: the whole discography, on a page of its own
The artist page already filed every release the desktop's way, but it only ever showed one category at a time and it showed all of it: no slice anywhere in that grid, so an artist with 140 releases drew 140 cards. There is a page now — All in front of the categories, a sort, a grouping by year, and the editions opened in place under their own record, which is the part a flat list loses — and the grid behind it stops at twelve with the way down under it. It is the same classifier twice over the same cached albums, which the desktop cannot do because its filing waits on a background description harvest; the three things its own page has and this one does not — a tracklist per record, an accent sampled per cover, the scroll-led banner — are 140 calls and 140 decodes over mobile data for rows that are one tap away, and ANDROID.md says so rather than leaving them looking forgotten. The list caps at sixty with the rest a button away, before the grouping, so the last year on screen can be a part of itself; under a name order By year is not offered, because the years would repeat out of sequence. The mock gains a 140-record artist so both caps are walked rather than argued about.
This commit is contained in:
1 parent
1258b2b78d
commit
f2cde4ec7c
9 files changed
+491
-12
No files matched your search
@@ -62,6 +62,7 @@
|
||||
if (!h) return null;
|
||||
if (h.kind !== "more") return h.kind;
|
||||
const t = top();
|
||||
if (t.n === "disco") return "discosort";
|
||||
if (t.n === "album") return "album-more";
|
||||
if (t.n === "library") return "library-more";
|
||||
if (t.n === "playlist") return "playlist-more";
|
||||
@@ -722,6 +723,30 @@
|
||||
{@render row("refresh", "Forget and look again", "", act(() => { void forgetLyrics(); }))}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if kind === "discosort"}
|
||||
{@const c = top().c}
|
||||
{@const ord = c.order || "new"}
|
||||
{@const byName = ord === "az" || ord === "za"}
|
||||
{@const grp = byName ? "none" : (c.group ?? "year")}
|
||||
<!-- the four orders are written out here rather than imported
|
||||
from disco.js: this file is edited by every round and a new
|
||||
import line is a collision waiting to happen. ORDER_LABEL
|
||||
there carries the same four, and the walk asserts both. -->
|
||||
<div class="sheet-title">Sort & group</div>
|
||||
<div class="sheet-sec">Order</div>
|
||||
<div class="chips" style="padding-bottom:12px">
|
||||
{#each [["new", "Newest first"], ["old", "Oldest first"], ["az", "A–Z"], ["za", "Z–A"]] as [k, l] (k)}
|
||||
<button class="chip" class:on={ord === k} class:quiet={ord !== k} aria-pressed={ord === k} onclick={() => patch({ order: k })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="sheet-sec">Group</div>
|
||||
<div class="chips" style="padding-bottom:12px">
|
||||
{#each [["year", "By year"], ["none", "None"]] as [k, l] (k)}
|
||||
<button class="chip" class:on={grp === k} class:quiet={grp !== k} aria-pressed={grp === k} disabled={byName && k === "year"} onclick={() => patch({ group: k })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if byName}<div class="sec-note">By year needs a date order — under A–Z the years would repeat out of sequence.</div>{/if}
|
||||
<div style="padding: 4px var(--pad) 16px"><button class="btn btn-fill wide" onclick={closeHalf}>Done</button></div>
|
||||
{:else if kind === "librarysort"}
|
||||
<div class="sheet-title">Sort & view</div>
|
||||
<div class="sheet-sec">Order</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import Library from "./screens/Library.svelte";
|
||||
import Album from "./screens/Album.svelte";
|
||||
import Artist from "./screens/Artist.svelte";
|
||||
import Discography from "./screens/Discography.svelte";
|
||||
import CatalogAlbum from "./screens/CatalogAlbum.svelte";
|
||||
import Playlist from "./screens/Playlist.svelte";
|
||||
import Import from "./screens/Import.svelte";
|
||||
@@ -86,6 +87,16 @@
|
||||
const a = albumById(e.c.id);
|
||||
return { title: a?.title ?? "", kicker: a?.artist ?? "", amb: a?.art_path ?? null, flush: true };
|
||||
}
|
||||
/* the whole discography wears the artist's own ground, but no
|
||||
hero of its own: it is a list, and the bar is over it from the
|
||||
first pixel rather than after 210 of them */
|
||||
case "disco": {
|
||||
const as = albumsBy(e.c.name);
|
||||
const cat = catalog.artists[artistKey(e.c.name)]?.page;
|
||||
const p = cat?.banner || cat?.artist?.picture
|
||||
|| as.find((a) => a.artist_art_path)?.artist_art_path || as[0]?.art_path || null;
|
||||
return { title: e.c.name, kicker: "Discography", amb: p };
|
||||
}
|
||||
case "artist": {
|
||||
const as = albumsBy(e.c.name);
|
||||
const cat = catalog.artists[artistKey(e.c.name)]?.page;
|
||||
@@ -339,6 +350,8 @@
|
||||
<Album id={entry.c.id} {screenEl} />
|
||||
{:else if entry.n === "artist"}
|
||||
<Artist name={entry.c.name} seed={entry.c.seed ?? null} {screenEl} />
|
||||
{:else if entry.n === "disco"}
|
||||
<Discography {entry} {screenEl} />
|
||||
{:else if entry.n === "catalogAlbum"}
|
||||
<CatalogAlbum id={entry.c.id} artist={entry.c.artist} title={entry.c.title} {screenEl} />
|
||||
{:else if entry.n === "playlist"}
|
||||
|
||||
@@ -92,3 +92,61 @@ export const SOCIAL_LABEL = {
|
||||
youtube: "YouTube", instagram: "Instagram", twitter: "X", x: "X", facebook: "Facebook", tiktok: "TikTok",
|
||||
spotify: "Spotify", apple: "Apple Music", discogs: "Discogs", lastfm: "Last.fm", musicbrainz: "MusicBrainz",
|
||||
};
|
||||
|
||||
/* ── the whole discography, on a page of its own ─────────────────────────
|
||||
The artist page's tabs are one lane per category; the discography page is
|
||||
the same filing seen whole. Both read ONE classifier, and it is this one.
|
||||
|
||||
The desktop cannot do that: CatalogDiscography.svelte takes the artist
|
||||
page's already-computed tab list as the same array object, because the
|
||||
desktop's filing depends on a background description harvest and on
|
||||
deferred MusicBrainz type hints that a re-run would not read
|
||||
(CatalogDiscography.svelte:28-46). `discoTabs` here is pure and reads only
|
||||
what the catalog page carries, so the phone simply calls it twice over the
|
||||
same cached albums and the two pages can never disagree. */
|
||||
|
||||
/** "All" in front of the categories — built by CONCATENATING the per-category
|
||||
* families, never by re-folding the whole set. A single that shares an
|
||||
* album's base title (Pond released "Terrestrials" in 2026 as an album AND
|
||||
* as a single) would fold into it as a phantom edition, and one of the two
|
||||
* would vanish from its own tab. */
|
||||
export function allTabs(tabs) {
|
||||
const ts = tabs || [];
|
||||
if (!ts.length) return [];
|
||||
return [{ key: "all", label: "All", items: ts.flatMap((t) => t.items) }, ...ts];
|
||||
}
|
||||
|
||||
/** The four orders the page offers, and what each is called. The sort sheet
|
||||
* in HalfSheet.svelte writes the same four out inline — it is edited by
|
||||
* every other round and a new import there is a collision waiting to
|
||||
* happen — so if a label changes it changes in both places. */
|
||||
export const ORDER_LABEL = { new: "Newest first", old: "Oldest first", az: "A–Z", za: "Z–A" };
|
||||
|
||||
/** The desktop's sort (CatalogDiscography.svelte:71-78): dates on
|
||||
* `release_date`, names on the title, either direction. */
|
||||
export function orderFamilies(items, order) {
|
||||
const list = [...(items || [])];
|
||||
const byName = order === "az" || order === "za";
|
||||
list.sort((a, b) => (byName
|
||||
? (a.title ?? "").localeCompare(b.title ?? "")
|
||||
: (a.release_date ?? "").localeCompare(b.release_date ?? "")));
|
||||
return order === "new" || order === "za" ? list.reverse() : list;
|
||||
}
|
||||
|
||||
/** [{ key: "2001", items }] — the year headers, cut out of the list in the
|
||||
* order it is already in rather than re-sorted, which is why a name order
|
||||
* must not be grouped: the years would repeat out of sequence. `on` false
|
||||
* gives the whole list back under one null key, so the caller renders one
|
||||
* loop either way. */
|
||||
export function byYear(items, on) {
|
||||
const list = items || [];
|
||||
if (!on) return list.length ? [{ key: null, items: list }] : [];
|
||||
const out = [];
|
||||
for (const r of list) {
|
||||
const y = String(r.release_date || "").slice(0, 4) || "—";
|
||||
const last = out[out.length - 1];
|
||||
if (last && last.key === y) last.items.push(r);
|
||||
else out.push({ key: y, items: [r] });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -128,7 +128,13 @@
|
||||
else if (!ts.some((t) => t.key === tab)) tab = albumsFirst();
|
||||
});
|
||||
});
|
||||
const families = $derived(tabs.find((t) => t.key === tab)?.items ?? []);
|
||||
/* the grid is a SLICE — the whole tab lives on the discography page,
|
||||
which the head's "All" and the link under the grid both open. Before
|
||||
this an artist with 140 releases rendered 140 cards here. */
|
||||
const GRID_CAP = 12;
|
||||
const activeTab = $derived(tabs.find((t) => t.key === tab) ?? null);
|
||||
const families = $derived((activeTab?.items ?? []).slice(0, GRID_CAP));
|
||||
const tabCount = $derived(activeTab?.items.length ?? 0);
|
||||
const inLib = (a) => albums.find((x) => looseTitle(x.title) === looseTitle(a.title)) || null;
|
||||
const familyLib = (f) => (f.editions || [f]).map(inLib).find(Boolean) || null;
|
||||
const familyKeyOf = (f) => f.familyKey ?? f.key ?? f.id;
|
||||
@@ -284,7 +290,10 @@
|
||||
{/if}
|
||||
|
||||
{#if tabs.length}
|
||||
<div class="sec"><h2>Discography <em>· {releasedCount} on the catalog</em></h2></div>
|
||||
<div class="sec">
|
||||
<h2>Discography <em>· {releasedCount} on the catalog</em></h2>
|
||||
<button class="all" onclick={() => push("disco", { name }, screenEl)} aria-label="All releases">All<Icon name="chevron-right" size={14} /></button>
|
||||
</div>
|
||||
{#if tabs.length > 1}
|
||||
<div class="segtabs disco-tabs" role="tablist" aria-label="Discography">
|
||||
{#each tabs as t (t.key)}
|
||||
@@ -306,6 +315,9 @@
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if tabCount > GRID_CAP}
|
||||
<button class="more-link dg-all" onclick={() => push("disco", { name, tab }, screenEl)}>Show all {tabCount} {activeTab?.label ?? "releases"}</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if timeline}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<script>
|
||||
/* The whole discography — the desktop's CatalogDiscography.svelte, which
|
||||
the artist page reaches with "All" on its Discography head.
|
||||
|
||||
The desktop's page is a stack of album-page heroes — the album page's
|
||||
own hero at `--cover-scale: 0.45`, a 108px cover, with the record's
|
||||
whole tracklist under it, fetched as the section scrolls near and its
|
||||
accent sampled from its own cover (CatalogDiscography.svelte:120-175,
|
||||
1555-1560). On a 411px screen that is one record per screenful and one
|
||||
`catalog_album` per row over mobile data, for rows that are a tap away
|
||||
on the record page — so none of those three comes over. What does is
|
||||
the four things the page has that the artist page's tabs do not: All,
|
||||
a sort, a grouping, and the editions kept whole.
|
||||
|
||||
Filed by the same classifier the artist page uses. The state — tab,
|
||||
order, grouping — lives on the nav entry, so it survives a push and
|
||||
the sort sheet's `patch()` reaches it the way Library's does. */
|
||||
import { untrack } from "svelte";
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Cover from "../Cover.svelte";
|
||||
import { art, wantArt } from "../art.svelte.js";
|
||||
import { albumsBy } from "../lib.svelte.js";
|
||||
import { push, patch, openHalf } from "../nav.svelte.js";
|
||||
import { catalog, lookupArtist, artistKey, yearOf, looseTitle, picture } from "../catalog.svelte.js";
|
||||
import { discoTabs, allTabs, orderFamilies, byYear, ORDER_LABEL } from "../disco.js";
|
||||
import { CAT_ONE } from "../../lib/localReleaseType.js";
|
||||
import { editionLabel } from "../../lib/editions.js";
|
||||
|
||||
let { entry, screenEl = null } = $props();
|
||||
|
||||
const name = $derived(entry.c.name);
|
||||
/* normally warm — the artist page that linked here looked the name up
|
||||
already — but a page that only works warm is a page that breaks the
|
||||
day something else links to it. `lookupArtist` is its own guard. */
|
||||
$effect(() => { const n = name, sd = entry.c.seed ?? null; untrack(() => lookupArtist(n, sd)); });
|
||||
|
||||
const cat = $derived(catalog.artists[artistKey(name)] ?? null);
|
||||
const page = $derived(cat?.page ?? null);
|
||||
const artistId = $derived(page?.artist?.id ?? cat?.id ?? null);
|
||||
const overview = $derived(artistId != null ? catalog.overviews[artistId] ?? null : null);
|
||||
const shownName = $derived(page?.artist?.name || overview?.artist?.name || name);
|
||||
/* the exhaustive page when it is here, the overview's first round until
|
||||
then — never the other way round, the artist page's own rule */
|
||||
const discoAll = $derived(page?.albums ?? overview?.albums ?? []);
|
||||
|
||||
/* ── the tabs, the sort, the grouping ────────────────────────────── */
|
||||
const catTabs = $derived(discoTabs(discoAll));
|
||||
const tabs = $derived(allTabs(catTabs));
|
||||
const tab = $derived(tabs.some((t) => t.key === entry.c.tab) ? entry.c.tab : (tabs[0]?.key ?? "all"));
|
||||
const order = $derived(entry.c.order || "new");
|
||||
/* Under a name sort the years would repeat out of order, so By year is
|
||||
not offered there and the grouping falls to none. The sheet says why. */
|
||||
const byName = $derived(order === "az" || order === "za");
|
||||
const grouped = $derived(!byName && (entry.c.group ?? "year") === "year");
|
||||
|
||||
const active = $derived(tabs.find((t) => t.key === tab) ?? null);
|
||||
const items = $derived(orderFamilies(active?.items ?? [], order));
|
||||
/* the desktop's header line: a record with no track count makes the
|
||||
whole figure a floor, not a number */
|
||||
const known = $derived(items.reduce((n, r) => n + (r.track_count || 0), 0));
|
||||
const someUnknown = $derived(items.some((r) => r.track_count == null));
|
||||
/* which category each family was filed under, read off the per-category
|
||||
tabs rather than by asking the classifier again once per row */
|
||||
const kinds = $derived.by(() => {
|
||||
const m = new Map();
|
||||
for (const t of catTabs) for (const f of t.items) m.set(f.familyKey ?? f.key ?? f.id, CAT_ONE[t.key] ?? t.label);
|
||||
return m;
|
||||
});
|
||||
|
||||
/* the cap. A prolific artist is hundreds of rows and the phone renders
|
||||
every one of them if it is asked to — Browse's rule (Browse.svelte:51):
|
||||
a slice that extends on demand, reset whenever the list underneath it
|
||||
changes. It is taken BEFORE the grouping, so the last year on screen
|
||||
can be a part of itself and the button says how much is left. */
|
||||
const CAP = 60;
|
||||
let cap = $state(CAP);
|
||||
$effect(() => { void tab; void order; void grouped; cap = CAP; });
|
||||
const groups = $derived(byYear(items.slice(0, cap), grouped));
|
||||
|
||||
/* ── what this phone already holds ───────────────────────────────── */
|
||||
const albums = $derived(albumsBy(name));
|
||||
const inLib = (a) => albums.find((x) => looseTitle(x.title) === looseTitle(a.title)) || null;
|
||||
const familyLib = (f) => (f.editions || [f]).map(inLib).find(Boolean) || null;
|
||||
const familyKeyOf = (f) => f.familyKey ?? f.key ?? f.id;
|
||||
const subOf = (f, l) => [
|
||||
yearOf(f.release_date),
|
||||
kinds.get(familyKeyOf(f)) || "",
|
||||
f.track_count ? `${f.track_count} tracks` : "",
|
||||
l ? "in your library" : "",
|
||||
].filter(Boolean).join(" · ");
|
||||
|
||||
const picUrl = $derived(picture(page?.artist?.picture) || picture(cat?.seed?.picture) || null);
|
||||
const seedPath = $derived(albums.find((a) => a.artist_art_path)?.artist_art_path || albums[0]?.art_path || null);
|
||||
$effect(() => { if (seedPath) wantArt(seedPath); });
|
||||
const porSrc = $derived(picUrl || (seedPath ? art[seedPath] : "") || "");
|
||||
|
||||
/* ── opening things ──────────────────────────────────────────────── */
|
||||
function openRow(r) {
|
||||
const l = inLib(r);
|
||||
if (l) push("album", { id: l.id }, screenEl);
|
||||
else push("catalogAlbum", { id: r.id, artist: shownName, title: r.title }, screenEl);
|
||||
}
|
||||
function openFamily(f) {
|
||||
const l = familyLib(f);
|
||||
if (l) push("album", { id: l.id }, screenEl);
|
||||
else push("catalogAlbum", { id: f.id, artist: shownName, title: f.title }, screenEl);
|
||||
}
|
||||
/* a long press is the record sheet the grid's already opens — Play, Save,
|
||||
Add to playlist, Go to record, Go to artist. No new actions here. */
|
||||
const moreFor = (f) => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: shownName } });
|
||||
|
||||
/* the editions opened in place, under their own record — Browse's folder
|
||||
and its files, rather than a rail that scrolls sideways inside a list
|
||||
that scrolls down */
|
||||
let openEds = $state(new Set());
|
||||
function toggleEds(key) {
|
||||
const s = new Set(openEds);
|
||||
if (s.has(key)) s.delete(key); else s.add(key);
|
||||
openEds = s;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dg-head">
|
||||
<span class="dg-por">
|
||||
{#if porSrc}<img src={porSrc} alt="" />{:else}<Icon name="users" size={22} />{/if}
|
||||
</span>
|
||||
<div class="b">
|
||||
<div class="t">{shownName}</div>
|
||||
<div class="m">{items.length} release{items.length === 1 ? "" : "s"} · {someUnknown ? "≥" : ""}{known} tracks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if tabs.length > 1}
|
||||
<div class="segtabs dg-tabs" role="tablist" aria-label="Discography">
|
||||
{#each tabs as t (t.key)}
|
||||
<button class:on={tab === t.key} onclick={() => patch({ tab: t.key })} role="tab" aria-selected={tab === t.key} aria-label={t.label + ", " + t.items.length}>{t.label}<span class="count" aria-hidden="true">{t.items.length}</span></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if items.length}
|
||||
<div class="sec dg-sec">
|
||||
<h2>{active?.label ?? "All"} <em>· {items.length} release{items.length === 1 ? "" : "s"}</em></h2>
|
||||
<button class="all" onclick={() => openHalf("discosort")} aria-label="Sort and group">{ORDER_LABEL[order] ?? ORDER_LABEL.new}<Icon name="chevron-down" size={14} /></button>
|
||||
</div>
|
||||
{#each groups as g (g.key ?? "_all")}
|
||||
{#if g.key}
|
||||
<div class="dg-group">{g.key} <em>· {g.items.length} release{g.items.length === 1 ? "" : "s"}</em></div>
|
||||
{/if}
|
||||
{#each g.items as f (familyKeyOf(f))}
|
||||
{@const l = familyLib(f)}
|
||||
{@const key = familyKeyOf(f)}
|
||||
{@const eds = (f.editions?.length ?? 1) > 1 ? f.editions : null}
|
||||
<div class="dg-item">
|
||||
<button class="row dg-row" onclick={() => openFamily(f)} oncontextmenu={(e) => { e.preventDefault(); moreFor(f); }}>
|
||||
<span class="art">
|
||||
{#if f.cover}<img src={f.cover} alt="" loading="lazy" />
|
||||
{:else if l?.art_path}<Cover path={l.art_path} size={20} />
|
||||
{:else}<Icon name="disc" size={20} />{/if}
|
||||
</span>
|
||||
<span class="b">
|
||||
<span class="t">{f.title}</span>
|
||||
<span class="m">{subOf(f, l)}</span>
|
||||
</span>
|
||||
</button>
|
||||
{#if eds}
|
||||
<button class="dg-eline" onclick={() => toggleEds(key)} aria-expanded={openEds.has(key)}>
|
||||
<Icon name={openEds.has(key) ? "chevron-up" : "chevron-down"} size={13} />{openEds.has(key) ? "Hide editions" : `${eds.length} editions`}
|
||||
</button>
|
||||
{#if openEds.has(key)}
|
||||
<div class="eds dg-eds">
|
||||
{#each eds as e, i (e.id)}
|
||||
<button class="ed" onclick={() => openRow(e)}>
|
||||
<span class="ed-art">{#if e.cover}<img src={e.cover} alt="" loading="lazy" />{:else}<Icon name="disc" size={16} />{/if}</span>
|
||||
<span class="ed-copy">
|
||||
<span class="n">{i === 0 ? "Original" : editionLabel(e, f)}</span>
|
||||
<span class="y">{[yearOf(e.release_date), e.track_count ? `${e.track_count} tracks` : ""].filter(Boolean).join(" · ")}</span>
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{#if items.length > cap}
|
||||
<div style="padding: 10px var(--pad) 4px"><button class="btn btn-ghost wide dg-more" onclick={() => (cap += 120)}>+ {items.length - cap} more releases</button></div>
|
||||
{/if}
|
||||
{:else if !cat || cat.status === "loading"}
|
||||
<div class="sec-note" style="padding-top: 16px">Asking the catalog…</div>
|
||||
{:else}
|
||||
<div class="stub"><b>Nothing to list.</b>The catalog has no releases for {shownName}{cat.status === "error" ? " — it did not answer" : ""}.</div>
|
||||
{/if}
|
||||
|
||||
<div class="endpad"></div>
|
||||
@@ -1613,3 +1613,51 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="
|
||||
}
|
||||
/* why the rows will not move while the filter has something in it */
|
||||
.pl-note { padding: 2px var(--pad) 8px; font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
/* ── the whole discography ───────────────────────────────────────────────
|
||||
The artist page's "All". Nearly all of it is the app's own parts — .sec,
|
||||
.segtabs, .row, .eds/.ed, .more-link, .btn — so what is here is the head,
|
||||
the year written into the list where the desktop has a sticky rail, the
|
||||
editions line under a record, and the editions themselves widened out of
|
||||
the record page's 210px rail into the width this list already has. */
|
||||
.dg-head { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 6px; }
|
||||
.dg-head .b { flex: 1; min-width: 0; }
|
||||
.dg-head .t { font-size: 18px; font-weight: 800; letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.dg-head .m { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
|
||||
.dg-por {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--ring);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.dg-por img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.dg-sec { padding-top: 12px; }
|
||||
/* the year. A phone list is read top to bottom, so the span the desktop's
|
||||
rail jumps to is simply written into it. */
|
||||
.dg-group {
|
||||
padding: 16px var(--pad) 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.dg-group em { font-style: normal; font-weight: 650; letter-spacing: 0; color: var(--text-muted); }
|
||||
/* both of these line up under the row's TITLE, not under its cover: the
|
||||
pad, the 46px cover and the row's 12px gap */
|
||||
.dg-eline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 var(--pad) 9px calc(var(--pad) + 58px);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
.dg-eds { display: flex; flex-direction: column; gap: 6px; padding: 0 var(--pad) 12px calc(var(--pad) + 58px); }
|
||||
.dg-eds .ed { width: 100%; }
|
||||
Reference in new issue
Block a user