who has this: a gap card says who holds it, the sheet and the open album name them, a Space grows a Holdings tab, and Get it is the hub's own grab or a wait for their presence
This commit is contained in:
1 parent
a98c750908
commit
b8e6fb7396
11 files changed
+454
-6
No files matched your search
@@ -30,6 +30,8 @@
|
||||
import { listenerPeak, listenerShare } from "../lib/listenerBars.js";
|
||||
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import WhoHas from "../components/WhoHas.svelte";
|
||||
import { community } from "../lib/community.svelte.js";
|
||||
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
||||
|
||||
const page = $derived(catalog.albumPage); // { album, tracks, album_plays, … }
|
||||
@@ -887,6 +889,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !inLibrary && community.spaces.length}
|
||||
<section class="whohas-sec">
|
||||
<div class="whohas-head"><h2>Who has this</h2><span class="p">from your rooms' holdings · names compared, never files</span></div>
|
||||
{#key album.artist + "\u0000" + album.title}<WhoHas artist={album.artist} album={album.title} />{/key}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="tracklist">
|
||||
{#if tracks.length}
|
||||
<div class="tcols" class:has-count={showCount}>
|
||||
@@ -1976,4 +1985,8 @@
|
||||
:global(:root.glass) .stat-note {
|
||||
border-color: var(--glass-hairline);
|
||||
}
|
||||
.whohas-sec { margin: 8px 0 18px; padding: 12px 14px; border-radius: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.07); }
|
||||
.whohas-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
|
||||
.whohas-head h2 { margin: 0; font-size: 15px; font-weight: 800; }
|
||||
.whohas-head .p { font-size: 11.5px; color: var(--text-muted); }
|
||||
</style>
|
||||
@@ -52,6 +52,8 @@
|
||||
import { openPreview } from "../lib/preview/previewSession.svelte.js";
|
||||
import { traceToSvg, svgDataUrl } from "../lib/trace.js";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import WhoHas from "../components/WhoHas.svelte";
|
||||
import { openWhoHas, holdersFor, pairKey, holderCount } from "../lib/whoHas.svelte.js";
|
||||
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
||||
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
||||
import HeroGpuCanvas from "../components/HeroGpuCanvas.svelte";
|
||||
@@ -2536,6 +2538,17 @@
|
||||
});
|
||||
|
||||
const visibleTabs = $derived(tabs.filter((tab) => tab.key === activeTabKey));
|
||||
// Who in your rooms holds a release you lack: asked for the visible tab's
|
||||
// families at once, filed by pairKey; the count rides the card, and the
|
||||
// open album shows the people.
|
||||
let heldBy = $state({});
|
||||
const heldFor = (title) => (page ? heldBy[pairKey(page.artist.name, title)] : null);
|
||||
$effect(() => {
|
||||
const name = page?.artist?.name;
|
||||
const items = visibleTabs.flatMap((g) => g.items).filter((al) => !inLibrary(al)).map((al) => ({ artist: name, album: al.title }));
|
||||
if (!name || !items.length) return;
|
||||
untrack(() => void holdersFor(items).then((m) => { for (const [k, v] of m) heldBy[k] = v; }));
|
||||
});
|
||||
|
||||
// FLIP for the stage-2 correction. Svelte's `animate:` directive can't be
|
||||
// used here — it requires the animated element to be the ONLY child of the
|
||||
@@ -5399,6 +5412,9 @@
|
||||
onclick={(e) => { e.stopPropagation(); streamRelease(al, al); }}
|
||||
><Icon name="play" size={13} /></button>
|
||||
{/if}
|
||||
{#if heldFor(al.title)?.length}
|
||||
<button class="have" title="In your rooms — who has it?" onclick={(e) => { e.stopPropagation(); openWhoHas({ artist: page.artist.name, album: al.title, cover: al.cover ?? null }, e.currentTarget.closest(".release")); }}>{holderCount(heldFor(al.title))} {holderCount(heldFor(al.title)) === 1 ? "has it" : "have it"}</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="title" title={al.title}>
|
||||
{al.title}
|
||||
@@ -5565,6 +5581,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="xp-tracks">
|
||||
{#if openAlbum && !inLibrary(openAlbum) && heldFor(openAlbum.title)?.length}
|
||||
<div class="xp-whohas">
|
||||
<div class="xp-whohas-head"><span class="t">Who has this</span><span class="p">from your rooms' holdings · names compared, never files</span></div>
|
||||
{#key openAlbum.id}<WhoHas artist={page.artist.name} album={openAlbum.title} compact />{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if openLoading}
|
||||
<!-- Nothing at all until the load
|
||||
has outlasted LOADING_GRACE:
|
||||
@@ -9602,4 +9624,11 @@
|
||||
:global(:root.glass) .notch {
|
||||
background: var(--glass-surface);
|
||||
}
|
||||
/* who in your rooms holds a release you lack — the count rides the card */
|
||||
.have { position: absolute; right: 6px; bottom: 6px; z-index: 2; display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 7px; border-radius: 99px; background: rgba(0, 0, 0, 0.62); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); color: #fff; font-size: 10.5px; font-weight: 900; border: 1px solid rgba(255, 255, 255, 0.14); cursor: pointer; white-space: nowrap; }
|
||||
.have:hover { background: rgba(0, 0, 0, 0.82); border-color: rgba(255, 255, 255, 0.34); }
|
||||
.xp-whohas { margin: 0 0 12px; padding: 10px 12px; border-radius: 10px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.07); }
|
||||
.xp-whohas-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; flex-wrap: wrap; }
|
||||
.xp-whohas-head .t { font-size: 12.5px; font-weight: 800; white-space: nowrap; }
|
||||
.xp-whohas-head .p { font-size: 11px; color: var(--text-muted); }
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
import { onMount, untrack } from "svelte";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import Identicon, { hueOf } from "../components/Identicon.svelte";
|
||||
import WhoHas from "../components/WhoHas.svelte";
|
||||
import { rankPeople, reasonParts } from "../lib/discover.js";
|
||||
import Soulseek from "./Soulseek.svelte";
|
||||
import * as api from "../lib/api.js";
|
||||
@@ -96,6 +97,9 @@
|
||||
pinToSpace,
|
||||
leaveSpace,
|
||||
setShareListens,
|
||||
refreshHoldings,
|
||||
holdingsFor,
|
||||
setShareHoldings,
|
||||
openBoard,
|
||||
proposeToBoard,
|
||||
decideProposal,
|
||||
@@ -548,6 +552,18 @@
|
||||
let spaceCreating = $state(false);
|
||||
const sv = $derived(community.space);
|
||||
const spaceOwner = $derived(sv?.my_role === "owner");
|
||||
// Holdings: the room's lists are read when a Space opens (stale members
|
||||
// in the background; the event brings the result back), and the ask box
|
||||
// answers from the cache.
|
||||
const hv = $derived(sv ? holdingsFor(sv.owner, sv.space_id) : null);
|
||||
let askArtist = $state("");
|
||||
let askAlbum = $state("");
|
||||
let asked = $state(null);
|
||||
$effect(() => {
|
||||
const id = sv?.id;
|
||||
if (!id) return;
|
||||
untrack(() => void refreshHoldings(sv.owner, sv.space_id));
|
||||
});
|
||||
let spaceEl = $state(null);
|
||||
$effect(() => {
|
||||
const n = sv?.messages.length;
|
||||
@@ -1362,6 +1378,7 @@
|
||||
<div class="sptabs">
|
||||
<button class="subtab" class:on={spaceTab === "chat"} onclick={() => (spaceTab = "chat")}>Chat<b>{sv.messages.length}</b></button>
|
||||
<button class="subtab" class:on={spaceTab === "listening"} onclick={() => (spaceTab = "listening")}>Listening{#if sv.listens.length}<b>{sv.listens.length}</b>{/if}</button>
|
||||
<button class="subtab" class:on={spaceTab === "holdings"} onclick={() => (spaceTab = "holdings")}>Holdings{#if hv?.sharing}<b>{hv.sharing}</b>{/if}</button>
|
||||
<button class="subtab" class:on={spaceTab === "members"} onclick={() => (spaceTab = "members")}>Members<b>{sv.members.length}</b></button>
|
||||
<button class="subtab" class:on={spaceTab === "about"} onclick={() => (spaceTab = "about")}>About</button>
|
||||
<button class="subtab" class:on={spaceTab === "proposals"} onclick={() => (spaceTab = "proposals")}>Proposals{#if bv && bv.uuid === sv.pinned_uuid && bv.proposals.length}<b>{bv.proposals.length}</b>{/if}</button>
|
||||
@@ -1446,6 +1463,50 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if spaceTab === "holdings"}
|
||||
<div class="panel sharebar" style="margin-bottom:12px">
|
||||
<button class="toggle" class:on={sv.share_holdings} aria-pressed={sv.share_holdings} title="Share what I hold in this Space"
|
||||
onclick={() => setShareHoldings(sv.owner, sv.space_id, !sv.share_holdings)} disabled={!nodeUp || !sv.can_post || !!community.busy["holdings:share"]}><i></i></button>
|
||||
<div class="grow">
|
||||
<div class="ttl">Share what I hold in this Space</div>
|
||||
<p class="hint">{#if sv.share_holdings && hv?.mine && !hv.mine.withdrawn}<b>{fmtCount(hv.mine.count)} albums</b> · published{#if sv.private} sealed to this room's epoch key{/if} · updated when your library changes · <b>synced as of {fmtClock(hv.mine.published_at * 1000)}</b>{:else}Album names as keys, one quality word each, and your Soulseek name so members can get it from you — never files. Per Space, off until you say so.{/if}</p>
|
||||
</div>
|
||||
{#if community.busy["holdings:share"]}<span class="chip off">working…</span>{:else if sv.share_holdings}<span class="chip acc">sharing</span>{:else}<span class="chip off">not sharing</span>{/if}
|
||||
</div>
|
||||
<div class="disc"><Icon name="alert" size={14} /><span><b>What the room learns:</b> members can tell whether you hold any album they can name, and roughly how many you hold. They never see files, paths, or formats beyond a lossless/lossy word. Turning this off withdraws the list; copies members already read stay with them.</span></div>
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h3><Icon name="search" size={13} /> Ask this room{#if hv}<span class="chip up">{hv.sharing} of {sv.members.length} sharing</span>{/if}</h3>
|
||||
<form class="inl askinl" style="margin-top:8px" onsubmit={(e) => { e.preventDefault(); if (askArtist.trim() && askAlbum.trim()) asked = { artist: askArtist.trim(), album: askAlbum.trim() }; }}>
|
||||
<input placeholder="Artist" bind:value={askArtist} />
|
||||
<input placeholder="Album" bind:value={askAlbum} />
|
||||
<button class="ax-ghost sm" type="submit" disabled={!askArtist.trim() || !askAlbum.trim()}>Ask</button>
|
||||
</form>
|
||||
{#if asked}{#key asked.artist + "\u0000" + asked.album}<div style="margin-top:8px"><WhoHas artist={asked.artist} album={asked.album} /></div>{/key}{/if}
|
||||
</div>
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h3><Icon name="users" size={13} /> Who shares here <button class="ax-ghost sm" style="margin-left:auto" onclick={() => refreshHoldings(sv.owner, sv.space_id, true)} disabled={!nodeUp}><Icon name="refresh" size={12} /> Refresh</button></h3>
|
||||
{#if hv}
|
||||
<div class="hlist">
|
||||
<div class="hrow">
|
||||
<Identicon seed={key ?? "you"} size={30} />
|
||||
<span class="tx"><span class="l1">you</span>
|
||||
<span class="l2">{#if sv.share_holdings && hv.mine && !hv.mine.withdrawn}<b>{fmtCount(hv.mine.count)}</b> albums · list {hv.mine.sequence} · published {fmtClock(hv.mine.published_at * 1000)}{:else}not sharing{/if}</span></span>
|
||||
<span class="chip" class:acc={sv.share_holdings && hv.mine && !hv.mine.withdrawn} class:off={!(sv.share_holdings && hv.mine && !hv.mine.withdrawn)}>{sv.share_holdings && hv.mine && !hv.mine.withdrawn ? "shared" : "none"}</span>
|
||||
</div>
|
||||
{#each hv.members.filter((m) => !m.is_me) as m (m.key)}
|
||||
<div class="hrow">
|
||||
<Identicon seed={m.username ?? m.key} size={30} />
|
||||
<span class="tx"><span class="l1">{m.is_me ? "you" : m.username ? display(m.username) : fpShort(m.key)}</span>
|
||||
<span class="l2">{#if m.state === "shared"}<b>{fmtCount(m.count)}</b> albums · list {m.sequence}{#if m.read_at} · read {fmtClock(m.read_at * 1000)}{/if}{:else if m.state === "none"}not sharing{:else if m.state === "withdrawn"}withdrew their list{:else if m.state === "sealed"}sealed to an epoch you have no key for{:else}not read yet{/if}</span></span>
|
||||
<span class="chip" class:acc={m.state === "shared"} class:off={m.state !== "shared"}>{m.state}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="hint" style="margin-top:8px">Reading the room's lists…</p>
|
||||
{/if}
|
||||
<p class="hint" style="margin-top:8px">A member's list is read when the Space opens and again after ten minutes; a member who never published costs a slow search, so they are asked again only hourly.</p>
|
||||
</div>
|
||||
{:else if spaceTab === "members"}
|
||||
<div class="panel">
|
||||
<div class="members">
|
||||
@@ -2390,6 +2451,18 @@
|
||||
.mem .nm { font-size: 11.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 96px; }
|
||||
.mem .role { font-size: 9.5px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
|
||||
.mem .role.own { color: #d9a6ff; }
|
||||
/* holdings: the disclosure and the who-shares list */
|
||||
.disc { display: flex; gap: 8px; align-items: flex-start; padding: 9px 11px; border-radius: 9px; background: color-mix(in srgb, var(--warn) 7%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent); font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; }
|
||||
.disc b { color: #f0c070; }
|
||||
.askinl input { height: 30px; padding: 0 10px; border-radius: 8px; background: var(--surf-hi); border: none; color: var(--text-primary); font-size: 12px; outline: none; }
|
||||
.askinl input::placeholder { color: var(--text-muted); }
|
||||
.hlist { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
|
||||
.hrow { display: flex; align-items: center; gap: 10px; padding: 5px 8px; border-radius: 8px; min-height: 40px; }
|
||||
.hrow:hover { background: var(--surf-hi); }
|
||||
.hrow .tx { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.hrow .l1 { font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hrow .l2 { font-size: 11.5px; color: var(--text-muted); }
|
||||
.hrow .l2 b { color: var(--text-secondary); }
|
||||
/* ── you ── */
|
||||
/* The You section sized by the PANE (container query), never the window:
|
||||
three columns from ~1240px of pane, two down to ~820, then one. */
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// is CatalogArtist's class for class with its animation numbers unchanged.
|
||||
// Styles are copied because Svelte scopes them per component — the same
|
||||
// trade CatalogDiscography made, with provenance comments at each block.
|
||||
import { tick } from "svelte";
|
||||
import { tick, untrack } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
||||
@@ -41,6 +41,7 @@
|
||||
importResume,
|
||||
goToView,
|
||||
} from "../lib/state.svelte.js";
|
||||
import { openWhoHas, holdersFor, pairKey, holderCount } from "../lib/whoHas.svelte.js";
|
||||
import {
|
||||
fileRelease,
|
||||
CAT_ORDER,
|
||||
@@ -121,6 +122,17 @@
|
||||
} catch { return {}; }
|
||||
})());
|
||||
let gapRows = $state({}); // foldKey(artist) -> catalog rows not owned
|
||||
let gapNames = $state({}); // foldKey(artist) -> the artist's display name
|
||||
// Who in your rooms holds a gap: asked for every open shelf's rows at
|
||||
// once, filed by pairKey. A marked card opens the ask sheet.
|
||||
let heldBy = $state({});
|
||||
$effect(() => {
|
||||
const items = Object.keys(gapOpen)
|
||||
.filter((k) => gapOpen[k] && gapRows[k]?.length && gapNames[k])
|
||||
.flatMap((k) => gapRows[k].map((r) => ({ artist: gapNames[k], album: r.title })));
|
||||
if (!items.length) return;
|
||||
untrack(() => void holdersFor(items).then((m) => { for (const [k, v] of m) heldBy[k] = v; }));
|
||||
});
|
||||
let gapOpen = $state({}); // foldKey(artist) -> line expanded
|
||||
let gapLoading = $state({});
|
||||
|
||||
@@ -182,6 +194,7 @@
|
||||
...gapRows,
|
||||
[key]: released.filter((r) => !ownedTitles.has(normKey(r.title))),
|
||||
};
|
||||
gapNames = { ...gapNames, [key]: artistName };
|
||||
gapTotals = { ...gapTotals, [key]: { total: released.length } };
|
||||
try { localStorage.setItem(GAPS_KEY, JSON.stringify(gapTotals)); } catch { /* ignore */ }
|
||||
} catch { /* the line stays collapsed; nothing is invented */ }
|
||||
@@ -1544,7 +1557,7 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet gapCard(r)}
|
||||
{#snippet gapCard(r, artist)}
|
||||
<!-- A record you do NOT own, at full size and dimmed. It is filed by the
|
||||
same classifier as everything else, off the CatalogAlbum payload's own
|
||||
fields (`record_type`/`track_count`/`release_date`, plus the is_live and
|
||||
@@ -1558,7 +1571,8 @@
|
||||
is_live: r.is_live,
|
||||
is_soundtrack: r.is_soundtrack,
|
||||
})}
|
||||
<div class="release gap" role="listitem" title={`${r.title} — not in your library`}>
|
||||
{@const held = heldBy[pairKey(artist, r.title)]}
|
||||
<div class="release gap" class:held={!!held?.length} role="listitem" title={`${r.title} — not in your library`}>
|
||||
<div class="cover-wrap">
|
||||
{#if r.release_date?.slice(0, 4)}
|
||||
<span class="ed-badge">{r.release_date.slice(0, 4)}</span>
|
||||
@@ -1568,6 +1582,9 @@
|
||||
{:else}
|
||||
<div class="cover placeholder"><Icon name="disc" size={30} /></div>
|
||||
{/if}
|
||||
{#if held?.length}
|
||||
<button class="have" title="{holderCount(held)} in your rooms {holderCount(held) === 1 ? "has" : "have"} this — who?" onclick={(e) => { e.stopPropagation(); openWhoHas({ artist, album: r.title, cover: r.cover ?? null }, e.currentTarget.closest(".release")); }}>{holderCount(held)} {holderCount(held) === 1 ? "has it" : "have it"}</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="title" title={r.title}>{r.title}</div>
|
||||
<div class="sub">{cat === "other" ? "" : CAT_ONE[cat]}</div>
|
||||
@@ -1826,7 +1843,7 @@
|
||||
<div class="lib-gaps" role="list">
|
||||
<div class="grid">
|
||||
{#each missing as r (r.id)}
|
||||
{@render gapCard(r)}
|
||||
{@render gapCard(r, name)}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2942,6 +2959,10 @@
|
||||
.lib-gaps { margin-top: 12px; }
|
||||
.lib-gaps .release.gap { opacity: 0.44; transition: opacity 150ms ease; cursor: default; }
|
||||
.lib-gaps .release.gap:hover { opacity: 1; background: none; }
|
||||
.lib-gaps .release.gap.held { opacity: 0.72; }
|
||||
/* who in your rooms holds a release you lack — the count rides the card */
|
||||
.have { position: absolute; right: 6px; bottom: 6px; z-index: 2; display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 7px; border-radius: 99px; background: rgba(0, 0, 0, 0.62); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); color: #fff; font-size: 10.5px; font-weight: 900; border: 1px solid rgba(255, 255, 255, 0.14); cursor: pointer; white-space: nowrap; }
|
||||
.have:hover { background: rgba(0, 0, 0, 0.82); border-color: rgba(255, 255, 255, 0.34); }
|
||||
|
||||
/* ============================================================ the table
|
||||
The power-user surface: every column is something the files carry. The
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
} from "../lib/state.svelte.js";
|
||||
import { untrack } from "svelte";
|
||||
import * as api from "../lib/api.js";
|
||||
import { community, forgetWait } from "../lib/community.svelte.js";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
||||
|
||||
@@ -408,6 +409,18 @@
|
||||
</div>
|
||||
|
||||
{#if sub === "downloads"}
|
||||
{#if community.waits.length}
|
||||
<div class="waits">
|
||||
<div class="waits-head">Waiting for <b>{community.waits.length}</b> · grabs that start the moment the person comes online</div>
|
||||
{#each community.waits as w (w.username + "\u0000" + w.artist + "\u0000" + w.album)}
|
||||
<div class="wait">
|
||||
<span class="tx"><span class="t">{w.album}</span><span class="a">{w.artist} · from <b>{w.username}</b>{#if w.via} · via {w.via}{/if}</span></span>
|
||||
<span class="chip">waiting</span>
|
||||
<button class="toolbtn" onclick={() => forgetWait(w.username, w.artist, w.album)}>Forget</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if dlItems.length === 0}
|
||||
<div class="empty">
|
||||
<p class="big">No downloads yet</p>
|
||||
@@ -1622,4 +1635,14 @@
|
||||
:global(:root.glass) .bub.me {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
/* holdings: albums waiting on a person's presence */
|
||||
.waits { margin: 0 0 12px; padding: 8px 10px; border: 1px dashed rgba(255, 255, 255, 0.16); border-radius: 10px; }
|
||||
.waits-head { font-size: 11.5px; color: var(--text-muted); margin-bottom: 4px; }
|
||||
.waits-head b { color: var(--text-secondary); }
|
||||
.wait { display: flex; align-items: center; gap: 10px; padding: 6px 4px; }
|
||||
.wait .tx { min-width: 0; flex: 1; display: flex; flex-direction: column; }
|
||||
.wait .t { font-size: 12.5px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wait .a { font-size: 11px; color: var(--text-muted); }
|
||||
.wait .a b { color: var(--text-secondary); }
|
||||
.wait .chip { font-size: 10px; font-weight: 900; letter-spacing: 0.05em; padding: 2px 8px; border-radius: 99px; background: var(--bg-highlight); color: var(--text-secondary); }
|
||||
</style>
|
||||
Reference in new issue
Block a user