132 lines
6.0 KiB
Svelte
132 lines
6.0 KiB
Svelte
<script>
|
||
// Ask shape A: the sheet off a card. One for the whole app, portalled to
|
||
// body (the pane is a containing block and would clip a fixed child),
|
||
// anchored beside whichever card asked, closed by Escape, the ×, or a
|
||
// click anywhere else. Shape B — the section inside an open album — is
|
||
// WhoHas.svelte placed inline; this is the same rows in a dark glass
|
||
// sheet, the drawer's material.
|
||
import Icon from "./Icon.svelte";
|
||
import WhoHas from "./WhoHas.svelte";
|
||
import { whoHas, closeWhoHas } from "../lib/whoHas.svelte.js";
|
||
|
||
const W = 440;
|
||
const MARGIN = 12;
|
||
let el = $state(null);
|
||
let people = $state(null);
|
||
|
||
function portal(node) {
|
||
document.body.appendChild(node);
|
||
return { destroy: () => node.remove() };
|
||
}
|
||
|
||
/** Beside the card when there is room to its right, under it otherwise,
|
||
* and never off the viewport. */
|
||
const place = $derived.by(() => {
|
||
const a = whoHas.anchor;
|
||
const vw = window.innerWidth;
|
||
const vh = window.innerHeight;
|
||
if (!a) return { left: Math.max(MARGIN, (vw - W) / 2), top: Math.max(MARGIN, vh * 0.2) };
|
||
let left = a.x + a.w + 10;
|
||
let top = a.y;
|
||
if (left + W + MARGIN > vw) left = Math.max(MARGIN, a.x - W - 10);
|
||
if (left < MARGIN) {
|
||
left = Math.min(Math.max(MARGIN, a.x), vw - W - MARGIN);
|
||
top = a.y + a.h + 8;
|
||
}
|
||
const h = el?.offsetHeight ?? 320;
|
||
if (top + h + MARGIN > vh) top = Math.max(MARGIN, vh - h - MARGIN);
|
||
return { left, top };
|
||
});
|
||
|
||
$effect(() => {
|
||
if (!whoHas.open) return;
|
||
people = null;
|
||
const onKey = (e) => {
|
||
if (e.key === "Escape") closeWhoHas();
|
||
};
|
||
const onDown = (e) => {
|
||
if (el && !el.contains(e.target)) closeWhoHas();
|
||
};
|
||
// The sheet is placed ONCE from the card's box and is position:fixed,
|
||
// so a scroll anywhere under it slides the card away and leaves the
|
||
// sheet floating beside nothing (measured 2026-09-09: the pane moved
|
||
// 120px, the sheet stayed). Scroll events do not bubble, so this is
|
||
// a capture listener on window; the sheet's own overflow scroll is
|
||
// the one scroller it must ignore.
|
||
const onScroll = (e) => {
|
||
if (el && e.target instanceof Node && el.contains(e.target)) return;
|
||
closeWhoHas();
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
window.addEventListener("scroll", onScroll, true);
|
||
window.addEventListener("resize", onScroll);
|
||
// Deferred a tick so the click that opened the sheet does not close it.
|
||
const t = setTimeout(() => window.addEventListener("pointerdown", onDown, true), 0);
|
||
return () => {
|
||
clearTimeout(t);
|
||
window.removeEventListener("keydown", onKey);
|
||
window.removeEventListener("scroll", onScroll, true);
|
||
window.removeEventListener("resize", onScroll);
|
||
window.removeEventListener("pointerdown", onDown, true);
|
||
};
|
||
});
|
||
</script>
|
||
|
||
{#if whoHas.open}
|
||
<div class="sheet" use:portal bind:this={el} style:left="{place.left}px" style:top="{place.top}px" role="dialog" aria-label="Who has {whoHas.album}">
|
||
<div class="hd">
|
||
{#if whoHas.cover}
|
||
<img class="cv" src={whoHas.cover} alt="" draggable="false" />
|
||
{:else}
|
||
<span class="cv ph"><Icon name="disc" size={20} /></span>
|
||
{/if}
|
||
<div class="copy">
|
||
<div class="t" title={whoHas.album}>{whoHas.album}</div>
|
||
<div class="a">{whoHas.artist}</div>
|
||
</div>
|
||
<div class="q">{#if people != null}<b>{people} {people === 1 ? "person" : "people"}</b>{people === 1 ? "has it" : people ? "have it" : "in your rooms"}{/if}</div>
|
||
<button class="x" onclick={closeWhoHas} title="Close" aria-label="Close"><Icon name="x" size={14} /></button>
|
||
</div>
|
||
{#key whoHas.artist + " |