Baseline: full tree snapshot after git repository loss

The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
This commit is contained in:
KeelrLeek committed 2026-07-22 17:57:00 -05:00
commit a667b4b28e
161 files changed
+53170

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
<script>
import { player, saveStream } from "../lib/state.svelte.js";
import Icon from "./Icon.svelte";
// align hint: "down" (panel) or "up" (player bar). Auto-flips if needed.
let { align = "down" } = $props();
const currentItem = $derived(player.queue[player.queueIndex] ?? null);
const isStream = $derived(!!currentItem?.stream);
const streamCount = $derived(player.queue.filter((it) => it.stream).length);
const isAlbum = $derived(streamCount > 1);
let triggerEl = $state(null);
let open = $state(false);
let pos = $state({ top: 0, left: 0 });
let toast = $state("");
let toastPos = $state({ top: 0, left: 0 });
let toastTimer;
const MENU_W = 190;
function place() {
const r = triggerEl.getBoundingClientRect();
const h = 92; // two items
let left = Math.max(8, Math.min(r.right - MENU_W, window.innerWidth - MENU_W - 8));
let top = align === "up" ? r.top - 6 - h : r.bottom + 6;
if (align !== "up" && top + h > window.innerHeight - 8) top = r.top - 6 - h;
if (align === "up" && top < 8) top = r.bottom + 6;
pos = { top, left };
}
async function save(scope) {
open = false;
const res = await saveStream(scope);
if (!res.msg) return;
const r = triggerEl.getBoundingClientRect();
toastPos = {
top: align === "up" ? r.top - 34 : r.bottom + 6,
left: Math.max(8, r.right - 190),
};
toast = res.msg;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => (toast = ""), 2600);
}
function onClick(e) {
e.stopPropagation();
// Single track: one destination, save straight away. Album: choose scope.
if (!isAlbum) {
save("track");
return;
}
if (open) {
open = false;
return;
}
place();
open = true;
}
$effect(() => {
if (!open) return;
const onDoc = (e) => {
if (!e.target.closest(".save-wrap") && !e.target.closest(".save-pop")) open = false;
};
const close = () => (open = false);
document.addEventListener("click", onDoc, true);
document.addEventListener("scroll", close, true);
window.addEventListener("resize", close);
return () => {
document.removeEventListener("click", onDoc, true);
document.removeEventListener("scroll", close, true);
window.removeEventListener("resize", close);
};
});
</script>
{#if isStream}
<span class="save-wrap">
<button
bind:this={triggerEl}
class="save-icon"
class:on={open}
title="Save to library"
onclick={onClick}
>
<Icon name="download" size={18} />
</button>
</span>
{#if open}
<div class="save-pop menu" style="top:{pos.top}px; left:{pos.left}px;">
<button class="mi" onclick={() => save("track")}>
<Icon name="music" size={16} /> Save this track
</button>
<button class="mi" onclick={() => save("album")}>
<Icon name="library" size={16} /> Save whole album
</button>
</div>
{/if}
{#if toast}
<div class="save-pop toast" style="top:{toastPos.top}px; left:{toastPos.left}px;">
{toast}
</div>
{/if}
{/if}
<style>
.save-wrap { display: inline-flex; }
.save-icon {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 8px;
color: var(--text-secondary);
}
.save-icon:hover,
.save-icon.on {
color: var(--text-primary);
background: var(--bg-highlight);
}
/* Fixed positioning escapes the panel's overflow clipping and any
stacking context, so the popover always sits on top. */
.save-pop {
position: fixed;
z-index: 1000;
}
.menu {
width: 190px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 10px;
padding: 5px;
box-shadow: 0 16px 38px -12px rgba(0, 0, 0, 0.75);
}
.mi {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 9px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--text-primary);
text-align: left;
}
.mi:hover { background: var(--bg-highlight); }
.mi :global(.icon) { color: var(--text-secondary); flex: 0 0 auto; }
.toast {
white-space: nowrap;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
padding: 7px 12px;
font-size: 12px;
color: var(--text-primary);
box-shadow: 0 12px 26px -10px rgba(0, 0, 0, 0.75);
}
</style>