mini player + dark glass: eight shapes run by the hidden main as the one brain, dissolving covers and accents, drag from anywhere, and the space between the panes turned to real acrylic - with the webview flags that keep rest frames honest
This commit is contained in:
1 parent
9b84b30aef
commit
dc7ea16b22
33 files changed
+2432
-5
No files matched your search
@@ -0,0 +1,96 @@
|
||||
<script>
|
||||
// V6 · The Deck — Kyle's horizontal reference: art, transport cluster,
|
||||
// centered name over a slim lane, modes and volume on the right.
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="side" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="lay">
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="cluster">
|
||||
<button class="m-skip" title="Previous" onclick={() => cmd("prev")}><Icon name="skip-back" size={18} /></button>
|
||||
<button class="m-disc" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={16} /></button>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={18} /></button>
|
||||
</div>
|
||||
<div class="mid">
|
||||
<div class="name"><b>{mini.artist ?? "Nothing playing"}</b>{mini.title ? ` - ${mini.title}` : ""}</div>
|
||||
<Lane slim />
|
||||
</div>
|
||||
<div class="modes">
|
||||
<button class="m-mode" class:on={mini.shuffle} title="Shuffle" onclick={() => cmd("shuffle")}><Icon name="shuffle" size={16} /></button>
|
||||
<button class="m-mode" class:on={mini.repeat !== "off"} title="Repeat" onclick={() => cmd("repeat")}><Icon name={mini.repeat === "one" ? "repeat-one" : "repeat"} size={16} /></button>
|
||||
<VolumeFly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
padding: 0 14px 0 8px;
|
||||
}
|
||||
.art {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: none;
|
||||
}
|
||||
.mid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
}
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
.name b {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
.modes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script>
|
||||
// V5 · The Focus card — Kyle's vertical reference: transport riding the
|
||||
// art, centered three-line meta (with the album), the lane, and one
|
||||
// modes row (shuffle · repeat · volume together, per his centring note).
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="deep" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="lay">
|
||||
<div class="artrow">
|
||||
<button class="m-skip" title="Previous" onclick={() => cmd("prev")}><Icon name="skip-back" size={18} /></button>
|
||||
<div class="artbox" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
<button class="m-disc onart" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={19} /></button>
|
||||
</div>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={18} /></button>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="tt">{mini.title ?? "Nothing playing"}</div>
|
||||
<div class="ar">{mini.artist ?? ""}</div>
|
||||
<div class="al">{mini.album ?? ""}</div>
|
||||
</div>
|
||||
<Lane />
|
||||
<div class="modes">
|
||||
<button class="m-mode" class:on={mini.shuffle} title="Shuffle" onclick={() => cmd("shuffle")}><Icon name="shuffle" size={16} /></button>
|
||||
<button class="m-mode" class:on={mini.repeat !== "off"} title="Repeat" onclick={() => cmd("repeat")}><Icon name={mini.repeat === "one" ? "repeat-one" : "repeat"} size={16} /></button>
|
||||
<VolumeFly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px 14px 12px;
|
||||
gap: 9px;
|
||||
}
|
||||
.artrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.artrow .m-skip {
|
||||
width: 26px;
|
||||
}
|
||||
.artbox {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
/* The disc rides the art but only shows itself under the pointer — the
|
||||
cover stays a cover until you reach for it. Hidden = click-through,
|
||||
so a press on the resting art moves the window like anywhere else. */
|
||||
:global(.m-disc.onart) {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms, background 120ms, transform 120ms;
|
||||
}
|
||||
.artbox:hover :global(.m-disc.onart) {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
:global(.m-disc.onart:hover) {
|
||||
transform: translate(-50%, -50%) scale(1.06);
|
||||
}
|
||||
.meta {
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.tt {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.al {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.modes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script>
|
||||
// V7 · On Deck — the queue made visible: the NEXT record leans against
|
||||
// the strip's edge; clicking it plays it now. SoulStream is a gapless
|
||||
// queue player — this is the shape only it can wear.
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="side" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="lay">
|
||||
<div class="row">
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="tt">{mini.title ?? "Nothing playing"}</div>
|
||||
<div class="ar">{mini.artist ?? ""}</div>
|
||||
</div>
|
||||
<VolumeFly />
|
||||
<button class="m-disc" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={16} /></button>
|
||||
{#if mini.next}
|
||||
<button class="ondeck" title="Play the next track now" onclick={() => cmd("next")}>
|
||||
{#if mini.next.artUrl}<img src={mini.next.artUrl} alt="" draggable="false" />{/if}
|
||||
<span class="nx"><span class="lbl">Up next</span><span class="nt">{mini.next.title}</span></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<Lane />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 9px 12px 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.art {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tt {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ar {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ondeck {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex: none;
|
||||
max-width: 128px;
|
||||
padding: 4px 8px 4px 4px;
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
transition: background 120ms;
|
||||
text-align: left;
|
||||
}
|
||||
.ondeck:hover {
|
||||
background: rgba(255, 255, 255, 0.11);
|
||||
}
|
||||
.ondeck img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
flex: none;
|
||||
}
|
||||
.nx {
|
||||
min-width: 0;
|
||||
display: block;
|
||||
}
|
||||
.lbl {
|
||||
font-size: 8.5px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
display: block;
|
||||
}
|
||||
.nt {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
.lay :global(.lane) {
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<script>
|
||||
// V8 · The Queue card — the Rdio shape in our clothes: a header player
|
||||
// over the live queue, the current row wearing the app's stepped
|
||||
// equalizer (height keyframes, never transform — the corner saga).
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd, fmt } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="head" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="head">
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="hr">
|
||||
<div class="toprow">
|
||||
<div class="name">
|
||||
<b>{mini.title ?? "Nothing playing"}</b>{mini.artist ? ` — ${mini.artist}` : ""}
|
||||
</div>
|
||||
<button class="m-mode" class:on={mini.shuffle} title="Shuffle" onclick={() => cmd("shuffle")}><Icon name="shuffle" size={15} /></button>
|
||||
<button class="m-mode" class:on={mini.repeat !== "off"} title="Repeat" onclick={() => cmd("repeat")}><Icon name={mini.repeat === "one" ? "repeat-one" : "repeat"} size={15} /></button>
|
||||
<VolumeFly />
|
||||
</div>
|
||||
<div class="ctlrow">
|
||||
<button class="m-skip" title="Previous" onclick={() => cmd("prev")}><Icon name="skip-back" size={16} /></button>
|
||||
<button class="m-disc small" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={13} /></button>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={16} /></button>
|
||||
<Lane slim />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue">
|
||||
<div class="qhead"><span>Queue</span><span>{mini.rows.length} tracks</span></div>
|
||||
<div class="qlist">
|
||||
{#each mini.rows as row, i (i)}
|
||||
<button class="qrow" class:now={i === mini.index} onclick={() => cmd("jump", i)}>
|
||||
{#if i === mini.index}
|
||||
<span class="eq" class:active={playing()}><b></b><b></b><b></b></span>
|
||||
{:else}
|
||||
<span class="n">{i + 1}</span>
|
||||
{/if}
|
||||
<span class="qt">{row.title}</span>
|
||||
<span class="qd">{row.dur != null ? fmt(row.dur) : ""}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.head {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
padding: 11px 12px 9px;
|
||||
flex: none;
|
||||
}
|
||||
.art {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.hr {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.toprow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
}
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.name b {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.ctlrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.ctlrow :global(.lane) {
|
||||
flex: 1;
|
||||
}
|
||||
.queue {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.qhead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px 4px;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
flex: none;
|
||||
}
|
||||
.qlist {
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||||
}
|
||||
.qrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 0 12px;
|
||||
height: 30px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-align: left;
|
||||
}
|
||||
.qrow:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.n {
|
||||
width: 16px;
|
||||
flex: none;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.qt {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.qd {
|
||||
flex: none;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.qrow.now {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.qrow.now .qd {
|
||||
color: var(--accent);
|
||||
}
|
||||
/* the stepped equalizer — HEIGHT keyframes, never transform */
|
||||
.eq {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
gap: 1.5px;
|
||||
width: 16px;
|
||||
height: 11px;
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
}
|
||||
.eq b {
|
||||
width: 2.5px;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.eq.active b {
|
||||
animation: eq 1.2s steps(1, end) infinite;
|
||||
}
|
||||
.eq b:nth-child(1) {
|
||||
height: 40%;
|
||||
--eq-hi: 40%;
|
||||
--eq-lo: 16%;
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.eq b:nth-child(2) {
|
||||
height: 90%;
|
||||
--eq-hi: 90%;
|
||||
--eq-lo: 36%;
|
||||
}
|
||||
.eq b:nth-child(3) {
|
||||
height: 60%;
|
||||
--eq-hi: 60%;
|
||||
--eq-lo: 24%;
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
@keyframes eq {
|
||||
0%,
|
||||
100% {
|
||||
height: var(--eq-lo);
|
||||
}
|
||||
50% {
|
||||
height: var(--eq-hi);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script>
|
||||
// V2 · The Sleeve — the vertical record card: full art, glass foot,
|
||||
// centered meta, the lane, and a three-column transport so the disc sits
|
||||
// on the card's axis no matter what flanks it (Kyle's centring note).
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="down" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div class="titlerow">
|
||||
<span class="check" title="In your library"><Icon name="check" size={15} strokeWidth={2.6} /></span>
|
||||
<div class="meta">
|
||||
<div class="tt">{mini.title ?? "Nothing playing"}</div>
|
||||
<div class="ar">{mini.artist ?? ""}</div>
|
||||
</div>
|
||||
<span class="gap"></span>
|
||||
</div>
|
||||
<Lane />
|
||||
<div class="transport">
|
||||
<span class="side l">
|
||||
<button class="m-mode" class:on={mini.shuffle} title="Shuffle" onclick={() => cmd("shuffle")}><Icon name="shuffle" size={16} /></button>
|
||||
</span>
|
||||
<span class="mid">
|
||||
<button class="m-skip" title="Previous" onclick={() => cmd("prev")}><Icon name="skip-back" size={18} /></button>
|
||||
<button class="m-disc big" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={19} /></button>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={18} /></button>
|
||||
</span>
|
||||
<span class="side r">
|
||||
<button class="m-mode" class:on={mini.repeat !== "off"} title="Repeat" onclick={() => cmd("repeat")}><Icon name={mini.repeat === "one" ? "repeat-one" : "repeat"} size={16} /></button>
|
||||
<VolumeFly />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.art {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.foot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 11px 14px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.foot::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: rgba(20, 20, 24, 0.3);
|
||||
backdrop-filter: blur(14px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.6);
|
||||
}
|
||||
.titlerow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.check {
|
||||
color: var(--accent);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
.gap {
|
||||
width: 26px;
|
||||
flex: none;
|
||||
}
|
||||
.meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.tt {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.transport {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
.mid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
.side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.side.l {
|
||||
justify-content: flex-start;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.side.r {
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
// V3 · The Sliver — one row and a slim lane; the smallest footprint.
|
||||
// Long names glide like the playbar's (transform animation only while
|
||||
// it actually runs — never will-change, per the corner saga).
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
let box = $state(null);
|
||||
let glider = $state(null);
|
||||
let over = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
void mini.title;
|
||||
void mini.artist;
|
||||
requestAnimationFrame(() => {
|
||||
if (box && glider) over = Math.max(0, glider.scrollWidth - box.clientWidth);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="side" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="lay">
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="mid">
|
||||
<div class="namebox" class:masked={over > 0} class:gliding={over > 0} style="--glide: {over + 12}px" bind:this={box}>
|
||||
<div class="glider" bind:this={glider}>
|
||||
<b>{mini.title ?? "Nothing playing"}</b><span>{mini.artist ?? ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Lane slim />
|
||||
</div>
|
||||
<button class="m-disc" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={16} /></button>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={18} /></button>
|
||||
<VolumeFly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
padding: 0 10px 0 8px;
|
||||
}
|
||||
.art {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.mid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
}
|
||||
.namebox {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.glider {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
font-size: 12.5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.glider b {
|
||||
font-weight: 700;
|
||||
}
|
||||
.glider span {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.namebox.masked {
|
||||
-webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 20px), transparent);
|
||||
mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 20px), transparent);
|
||||
}
|
||||
.namebox.gliding .glider {
|
||||
animation: glide 9s linear infinite;
|
||||
}
|
||||
@keyframes glide {
|
||||
0%,
|
||||
12% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
50%,
|
||||
62% {
|
||||
transform: translateX(calc(-1 * var(--glide, 80px)));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
// V1 · The Strip — the playbar distilled: cover, meta, transport, lane.
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let artEl = $state(null);
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="side" />
|
||||
<Chrome anchor={artEl} />
|
||||
<div class="lay">
|
||||
<div class="art" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row">
|
||||
<div class="meta">
|
||||
<div class="tt">{mini.title ?? "Nothing playing"}</div>
|
||||
<div class="ar">{mini.artist ?? ""}</div>
|
||||
</div>
|
||||
<button class="m-skip" title="Previous" onclick={() => cmd("prev")}><Icon name="skip-back" size={18} /></button>
|
||||
<button class="m-disc" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={16} /></button>
|
||||
<button class="m-skip" title="Next" onclick={() => cmd("next")}><Icon name="skip-forward" size={18} /></button>
|
||||
<VolumeFly />
|
||||
</div>
|
||||
<Lane />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.art {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 12px 8px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tt {
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ar {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.row :global(.lane) {
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script>
|
||||
// V4 · The Wave — the flowing-line visualizer. Unlike the mockup's BPM
|
||||
// guess, the envelope here is the RECORD's own dynamics: the waveform
|
||||
// peaks the backend already computed, sampled around the playhead — the
|
||||
// line pumps when the song does, because it IS the song.
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import Wash from "../bits/Wash.svelte";
|
||||
import Art from "../bits/Art.svelte";
|
||||
import Lane from "../bits/Lane.svelte";
|
||||
import VolumeFly from "../bits/VolumeFly.svelte";
|
||||
import Chrome from "../bits/Chrome.svelte";
|
||||
import { waveLevels } from "../../lib/waveshape.js";
|
||||
import { mini, playing, cmd } from "../mini.svelte.js";
|
||||
|
||||
let flowEl = $state(null);
|
||||
let artEl = $state(null);
|
||||
let phase = 0;
|
||||
let env = 0.3; // smoothed energy
|
||||
|
||||
// A finer fold than the lane's, for reading level at the playhead.
|
||||
let fine = [];
|
||||
let fineFor = null;
|
||||
function levelNow() {
|
||||
if (fineFor !== mini.waveform) {
|
||||
fine = waveLevels(mini.waveform, 512);
|
||||
fineFor = mini.waveform;
|
||||
}
|
||||
if (!fine.length || !mini.duration) return 0.35;
|
||||
const i = Math.min(fine.length - 1, Math.floor((mini.position / mini.duration) * fine.length));
|
||||
// A touch of look-ahead so hits land ON the beat, not after it.
|
||||
const j = Math.min(fine.length - 1, i + 1);
|
||||
return Math.max(fine[i], fine[j] * 0.8);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
let alive = true;
|
||||
let last = performance.now();
|
||||
const loop = (now) => {
|
||||
if (!alive) return;
|
||||
const dt = Math.min(0.05, (now - last) / 1000);
|
||||
last = now;
|
||||
draw(dt);
|
||||
requestAnimationFrame(loop);
|
||||
};
|
||||
requestAnimationFrame(loop);
|
||||
return () => (alive = false);
|
||||
});
|
||||
|
||||
function draw(dt) {
|
||||
const c = flowEl;
|
||||
if (!c || !c.clientWidth) return;
|
||||
const w = c.clientWidth;
|
||||
const h = c.clientHeight;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
if (c.width !== Math.round(w * dpr)) {
|
||||
c.width = Math.round(w * dpr);
|
||||
c.height = Math.round(h * dpr);
|
||||
}
|
||||
const x = c.getContext("2d");
|
||||
x.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
x.clearRect(0, 0, w, h);
|
||||
const target = playing() ? levelNow() : 0.12;
|
||||
// fast attack, slow release — how meters (and eyes) like it
|
||||
env += (target - env) * (target > env ? 0.5 : 0.06);
|
||||
phase += dt * 1.7 * (playing() ? 1 : 0.1);
|
||||
const mid = h / 2;
|
||||
const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim();
|
||||
const layer = (amp, freq, ph, color, width, alpha) => {
|
||||
x.beginPath();
|
||||
for (let px = 0; px <= w; px += 2) {
|
||||
const t = px / w;
|
||||
const e = Math.sin(t * Math.PI);
|
||||
const y =
|
||||
mid +
|
||||
Math.sin(t * freq * Math.PI * 2 + ph) * amp * e +
|
||||
Math.sin(t * freq * 0.53 * Math.PI * 2 - ph * 0.7) * amp * 0.45 * e;
|
||||
px === 0 ? x.moveTo(px, y) : x.lineTo(px, y);
|
||||
}
|
||||
x.strokeStyle = color;
|
||||
x.globalAlpha = alpha;
|
||||
x.lineWidth = width;
|
||||
x.lineCap = "round";
|
||||
x.stroke();
|
||||
x.globalAlpha = 1;
|
||||
};
|
||||
layer(h * 0.42 * env, 2.6, phase * 0.62 + 1.7, "#ffffff", 2, 0.1);
|
||||
layer(h * 0.5 * env, 2.1, phase * 0.8, accent, 2.5, 0.32);
|
||||
layer(h * 0.4 * env, 1.6, phase, accent, 2.5, 0.9);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="shape">
|
||||
<Wash artUrl={mini.artUrl} fade="side" />
|
||||
<Chrome anchor={flowEl} />
|
||||
<div class="lay">
|
||||
<canvas class="flow" bind:this={flowEl}></canvas>
|
||||
<div class="namerow">
|
||||
<div class="nart" bind:this={artEl}>
|
||||
<Art url={mini.artUrl} />
|
||||
</div>
|
||||
<div class="name">
|
||||
<b>{mini.artist ?? "Nothing playing"}</b><span> - {mini.title ?? ""}</span>
|
||||
</div>
|
||||
<VolumeFly />
|
||||
<button class="m-disc" title="Play/Pause" onclick={() => cmd("toggle")}><Icon name={playing() ? "pause" : "play"} size={16} /></button>
|
||||
</div>
|
||||
<Lane />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-player);
|
||||
}
|
||||
.lay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 10px;
|
||||
}
|
||||
.flow {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.namerow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
.nart {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13.5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.name b {
|
||||
font-weight: 700;
|
||||
}
|
||||
.name span {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user