Music — Release Upload (tracks · art · metadata)
A dark, album-art-driven release-upload form for distributing music. Pick a generated gradient cover that themes the whole UI, fill in title, artist, label, genre, date and an explicit toggle, then build a reorderable tracklist where each row carries featured artists, an ISRC, a tiny waveform and a simulated audio-uploaded state. A three-step Details → Tracks → Review stepper validates as you go, while a sticky live preview card mirrors every field with a working scrubber, equalizer and simulated playback.
MCP
Код
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
--shadow-sm: 0 6px 20px rgba(0, 0, 0, 0.35);
/* themed from cover art */
--cv-a: #8b5cf6;
--cv-b: #ff3d71;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
background:
radial-gradient(1100px 600px at 80% -10%, color-mix(in srgb, var(--cv-a) 22%, transparent), transparent 60%),
radial-gradient(900px 500px at -10% 110%, color-mix(in srgb, var(--cv-b) 16%, transparent), transparent 55%),
var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background 0.5s ease;
}
button { font-family: inherit; cursor: pointer; }
input, select { font-family: inherit; }
.app {
max-width: 1180px;
margin: 0 auto;
padding: 28px 22px 60px;
}
/* ---------- TOPBAR ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 26px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 38px; height: 38px;
border-radius: 11px;
background: linear-gradient(135deg, var(--cv-a), var(--cv-b));
box-shadow: 0 6px 18px color-mix(in srgb, var(--cv-a) 45%, transparent);
position: relative;
transition: background 0.5s ease;
}
.brand-mark::after {
content: "";
position: absolute; inset: 0;
border-radius: 11px;
background:
repeating-linear-gradient(90deg, rgba(255,255,255,0.5) 0 2px, transparent 2px 6px);
-webkit-mask: radial-gradient(circle at 50% 50%, #000 30%, transparent 31%);
mask: radial-gradient(circle at 50% 50%, #000 30%, transparent 31%);
opacity: 0.7;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong {
font-family: "Space Grotesk", sans-serif;
font-size: 1.02rem;
letter-spacing: -0.01em;
}
.brand-text span { font-size: 0.78rem; color: var(--muted); }
/* ---------- STEPPER ---------- */
.stepper { display: flex; align-items: center; gap: 6px; }
.step {
display: inline-flex; align-items: center; gap: 8px;
background: transparent;
border: 1px solid var(--line);
color: var(--muted);
padding: 9px 14px;
border-radius: var(--r-full);
font-size: 0.82rem;
font-weight: 600;
transition: all 0.2s ease;
}
.step:hover { border-color: var(--line-2); color: var(--text); }
.step-num {
width: 20px; height: 20px;
display: grid; place-items: center;
border-radius: var(--r-full);
background: var(--surface-2);
font-size: 0.72rem;
transition: all 0.2s ease;
}
.step.is-active {
color: var(--text);
border-color: color-mix(in srgb, var(--cv-a) 55%, transparent);
background: color-mix(in srgb, var(--cv-a) 14%, transparent);
}
.step.is-active .step-num { background: var(--cv-a); color: #fff; }
.step.is-done .step-num { background: var(--accent); color: #04210f; }
.step-line { width: 18px; height: 1px; background: var(--line); }
/* ---------- LAYOUT ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 22px;
align-items: start;
}
/* ---------- FORM ---------- */
.form {
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 24px;
box-shadow: var(--shadow);
}
.panel { display: none; animation: fade 0.35s ease; }
.panel.is-active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.panel-title {
font-family: "Space Grotesk", sans-serif;
font-size: 1.32rem;
margin: 0 0 4px;
letter-spacing: -0.01em;
}
.panel-sub { margin: 0 0 18px; color: var(--muted); font-size: 0.86rem; }
.muted { color: var(--muted); }
.small { font-size: 0.8rem; }
/* art row */
.art-row { display: flex; gap: 18px; margin-bottom: 22px; }
.dropzone {
position: relative;
width: 148px; height: 148px;
flex-shrink: 0;
border-radius: var(--r-md);
border: 1.5px dashed var(--line-2);
background: var(--surface-2);
padding: 0; overflow: hidden;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.dropzone:hover { transform: translateY(-2px); border-color: var(--cv-a); }
.dz-cover {
position: absolute; inset: 0;
background: linear-gradient(135deg, var(--cv-a), var(--cv-b));
opacity: 0; transition: opacity 0.4s ease;
}
.dropzone.has-art .dz-cover { opacity: 1; }
.dropzone.has-art { border-style: solid; border-color: transparent; }
.dz-cover::before {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(circle at 28% 30%, rgba(255,255,255,0.35), transparent 42%),
repeating-linear-gradient(45deg, rgba(0,0,0,0.10) 0 8px, transparent 8px 18px);
}
.dz-overlay {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
color: var(--text);
background: rgba(11,11,15,0.34);
opacity: 1; transition: opacity 0.25s ease;
}
.dropzone.has-art .dz-overlay { opacity: 0; }
.dropzone.has-art:hover .dz-overlay { opacity: 1; }
.dz-icon { font-size: 1.6rem; line-height: 1; }
.dz-hint { font-weight: 600; font-size: 0.82rem; }
.dz-sub { font-size: 0.7rem; color: var(--muted); }
.art-side { display: flex; flex-direction: column; gap: 8px; }
.art-side p { margin: 0; }
/* fields */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 16px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: 0.74rem; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.field input[type="text"],
.field input[type="date"],
.field select {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--text);
padding: 11px 12px;
font-size: 0.9rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
color-scheme: dark;
}
.field input:focus, .field select:focus {
outline: none;
border-color: var(--cv-a);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-a) 22%, transparent);
}
/* toggle */
.toggle {
display: inline-flex; align-items: center; gap: 10px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 9px 12px;
color: var(--text);
font-size: 0.88rem; font-weight: 500;
align-self: flex-start;
}
.toggle-track {
width: 38px; height: 22px;
border-radius: var(--r-full);
background: var(--surface);
border: 1px solid var(--line-2);
position: relative; flex-shrink: 0;
transition: background 0.2s ease;
}
.toggle-thumb {
position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px; border-radius: var(--r-full);
background: var(--muted);
transition: transform 0.2s ease, background 0.2s ease;
}
.toggle[aria-checked="true"] .toggle-track { background: var(--accent-3); border-color: var(--accent-3); }
.toggle[aria-checked="true"] .toggle-thumb { transform: translateX(16px); background: #fff; }
.explicit-badge {
margin-left: auto;
display: grid; place-items: center;
width: 20px; height: 20px;
border-radius: 5px;
background: var(--surface);
color: var(--muted);
font-size: 0.7rem; font-weight: 800;
transition: all 0.2s ease;
}
.toggle[aria-checked="true"] .explicit-badge { background: var(--accent-3); color: #fff; }
/* ---------- BUTTONS ---------- */
.btn {
border: none;
background: var(--cv-a);
color: #fff;
font-weight: 700;
font-size: 0.86rem;
padding: 10px 16px;
border-radius: var(--r-full);
transition: transform 0.15s ease, filter 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 6px 18px color-mix(in srgb, var(--cv-a) 35%, transparent);
}
.btn:hover { transform: translateY(-1px); filter: brightness(1.07); }
.btn:active { transform: translateY(0); }
.btn.ghost {
background: transparent;
color: var(--text);
border: 1px solid var(--line-2);
box-shadow: none;
}
.btn.ghost:hover { border-color: var(--text); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
.btn.publish {
width: 100%;
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, var(--cv-a)));
padding: 14px;
font-size: 0.96rem;
box-shadow: 0 10px 28px color-mix(in srgb, var(--accent) 38%, transparent);
}
/* ---------- TRACKLIST ---------- */
.tracklist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.track {
display: grid;
grid-template-columns: 26px 26px 1fr auto;
gap: 12px;
align-items: center;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.18s ease, opacity 0.2s ease;
}
.track.dragging { opacity: 0.45; border-style: dashed; }
.track.drop-target { border-color: var(--cv-a); box-shadow: 0 0 0 2px color-mix(in srgb, var(--cv-a) 30%, transparent); }
.track-handle {
cursor: grab;
color: var(--muted);
display: grid; place-items: center;
font-size: 1rem;
user-select: none;
align-self: stretch;
}
.track-handle:active { cursor: grabbing; }
.track-index {
display: grid; place-items: center;
width: 26px; height: 26px;
border-radius: var(--r-sm);
background: var(--surface);
color: var(--muted);
font-weight: 700; font-size: 0.82rem;
}
.track-main { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.track-fields { display: grid; grid-template-columns: 1.4fr 1fr 0.9fr; gap: 8px; }
.track-fields input {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--text);
padding: 8px 10px;
font-size: 0.84rem;
min-width: 0;
}
.track-fields input:focus { outline: none; border-color: var(--cv-a); }
.track-fields input::placeholder { color: var(--muted); opacity: 0.7; }
.track-audio { display: flex; align-items: center; gap: 10px; }
.wave {
display: flex; align-items: flex-end; gap: 2px; height: 22px; flex: 1;
}
.wave span {
flex: 1;
background: linear-gradient(180deg, var(--cv-a), var(--cv-b));
border-radius: 2px;
opacity: 0.85;
min-width: 2px;
}
.audio-state {
display: inline-flex; align-items: center; gap: 5px;
font-size: 0.74rem; font-weight: 700;
color: var(--accent);
white-space: nowrap;
}
.audio-state::before {
content: "✓";
display: grid; place-items: center;
width: 16px; height: 16px; border-radius: var(--r-full);
background: color-mix(in srgb, var(--accent) 22%, transparent);
}
.track-remove {
background: transparent; border: none;
color: var(--muted); font-size: 1.1rem; line-height: 1;
width: 30px; height: 30px; border-radius: var(--r-sm);
align-self: start;
transition: all 0.15s ease;
}
.track-remove:hover { background: color-mix(in srgb, var(--accent-3) 18%, transparent); color: var(--accent-3); }
.track-count { margin-top: 14px; }
/* ---------- REVIEW ---------- */
.checklist { list-style: none; margin: 0 0 18px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.checklist li {
display: flex; align-items: center; gap: 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
font-size: 0.88rem;
}
.checklist li .ck-key { color: var(--muted); width: 130px; flex-shrink: 0; font-weight: 600; font-size: 0.8rem; }
.checklist li .ck-val { font-weight: 600; }
.checklist li .ck-flag {
margin-left: auto;
font-size: 0.72rem; font-weight: 700;
padding: 3px 9px; border-radius: var(--r-full);
}
.ck-flag.ok { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.ck-flag.warn { background: color-mix(in srgb, var(--accent-3) 18%, transparent); color: var(--accent-3); }
.rights { margin-bottom: 16px; }
.check { display: flex; align-items: center; gap: 10px; font-size: 0.86rem; color: var(--muted); cursor: pointer; }
.check input { width: 17px; height: 17px; accent-color: var(--cv-a); }
/* ---------- FORM NAV ---------- */
.form-nav { display: flex; justify-content: space-between; gap: 12px; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line); }
/* ---------- PREVIEW ---------- */
.preview { position: sticky; top: 20px; }
.preview-tag {
display: inline-block;
font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 10px;
}
.release-card {
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow);
}
.rc-cover {
position: relative;
aspect-ratio: 1 / 1;
background: linear-gradient(135deg, var(--cv-a), var(--cv-b));
transition: background 0.5s ease;
}
.rc-cover::before {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(circle at 30% 26%, rgba(255,255,255,0.30), transparent 45%),
repeating-linear-gradient(45deg, rgba(0,0,0,0.10) 0 10px, transparent 10px 22px);
}
.rc-eq {
position: absolute; left: 16px; bottom: 16px;
display: flex; align-items: flex-end; gap: 4px; height: 26px;
opacity: 0; transition: opacity 0.3s ease;
}
.rc-cover.playing .rc-eq { opacity: 1; }
.rc-eq span {
width: 4px; height: 8px; border-radius: 2px;
background: rgba(255,255,255,0.92);
animation: eq 0.9s ease-in-out infinite;
}
.rc-eq span:nth-child(2) { animation-delay: 0.18s; }
.rc-eq span:nth-child(3) { animation-delay: 0.36s; }
.rc-eq span:nth-child(4) { animation-delay: 0.10s; }
.rc-eq span:nth-child(5) { animation-delay: 0.28s; }
@keyframes eq { 0%, 100% { height: 7px; } 50% { height: 24px; } }
.rc-play {
position: absolute; right: 14px; bottom: 14px;
width: 50px; height: 50px;
border-radius: var(--r-full);
border: none;
background: #fff; color: #0b0b0f;
display: grid; place-items: center;
box-shadow: 0 8px 22px rgba(0,0,0,0.4);
transition: transform 0.18s ease;
}
.rc-play:hover { transform: scale(1.08); }
.rc-play svg { width: 24px; height: 24px; fill: currentColor; }
.rc-play .ic-pause { display: none; }
.rc-play[aria-pressed="true"] .ic-play { display: none; }
.rc-play[aria-pressed="true"] .ic-pause { display: block; }
.rc-body { padding: 18px; }
.rc-head { display: flex; align-items: center; gap: 8px; }
.rc-title {
font-family: "Space Grotesk", sans-serif;
font-size: 1.3rem; margin: 0; letter-spacing: -0.01em;
line-height: 1.2;
}
.rc-explicit {
display: grid; place-items: center;
width: 19px; height: 19px; border-radius: 5px;
background: var(--accent-3); color: #fff;
font-size: 0.66rem; font-weight: 800; flex-shrink: 0;
}
.rc-artist { margin: 4px 0 1px; font-weight: 600; color: var(--text); }
.rc-meta { margin: 0 0 16px; color: var(--muted); font-size: 0.82rem; }
/* scrubber */
.rc-scrub { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.rc-time { font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums; width: 30px; }
.rc-time:last-child { text-align: right; }
.scrubber {
flex: 1; height: 18px; position: relative; cursor: pointer;
display: flex; align-items: center;
}
.scrubber::before {
content: ""; position: absolute; left: 0; right: 0; height: 5px;
background: var(--surface-2); border-radius: var(--r-full);
}
.scrub-fill {
position: absolute; left: 0; height: 5px; width: 0%;
background: linear-gradient(90deg, var(--cv-a), var(--cv-b));
border-radius: var(--r-full);
}
.scrub-knob {
position: absolute; left: 0; width: 13px; height: 13px;
border-radius: var(--r-full);
background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.4);
transform: translateX(-50%);
transition: transform 0.1s ease;
}
.scrubber:focus-visible { outline: none; }
.scrubber:focus-visible .scrub-knob { box-shadow: 0 0 0 4px color-mix(in srgb, var(--cv-a) 35%, transparent); }
.rc-now { margin: 4px 0 14px; }
.rc-list { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.rc-list li {
display: flex; align-items: center; gap: 10px;
padding: 7px 8px; border-radius: var(--r-sm);
font-size: 0.84rem;
transition: background 0.15s ease;
}
.rc-list li:hover { background: var(--surface-2); }
.rc-list li.active { background: color-mix(in srgb, var(--cv-a) 14%, transparent); }
.rc-li-num { color: var(--muted); width: 18px; font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.rc-li-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.rc-li-feat { color: var(--muted); font-weight: 400; }
.rc-li-time { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.rc-li-e { font-size: 0.6rem; font-weight: 800; color: var(--muted); border: 1px solid var(--line-2); border-radius: 3px; padding: 0 3px; }
.rc-foot {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
padding-top: 14px; border-top: 1px solid var(--line);
font-size: 0.76rem; color: var(--muted);
}
/* ---------- TOAST ---------- */
.toast {
position: fixed; left: 50%; bottom: 28px;
transform: translate(-50%, 24px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
padding: 12px 18px;
border-radius: var(--r-full);
font-size: 0.86rem; font-weight: 600;
box-shadow: var(--shadow);
opacity: 0; pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
z-index: 50;
max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast::before { content: "♪ "; color: var(--cv-a); }
/* ---------- RESPONSIVE ---------- */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.preview { position: static; }
.release-card { max-width: 420px; }
}
@media (max-width: 520px) {
.app { padding: 18px 14px 50px; }
.stepper { width: 100%; justify-content: space-between; gap: 2px; }
.step { padding: 8px 10px; font-size: 0.74rem; }
.step-line { width: 8px; }
.form { padding: 18px; }
.grid-2 { grid-template-columns: 1fr; }
.art-row { flex-direction: column; }
.dropzone { width: 100%; height: 200px; }
.track-fields { grid-template-columns: 1fr; }
.track { grid-template-columns: 22px 22px 1fr auto; gap: 8px; padding: 10px; }
.panel-head { flex-direction: column; }
.panel-head .btn { align-self: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.rc-explicit[hidden] {
display: none;
}(function () {
"use strict";
/* ---------- cover palettes (accent pulled from "art") ---------- */
var COVERS = [
{ a: "#8b5cf6", b: "#ff3d71" },
{ a: "#1db954", b: "#0ea5e9" },
{ a: "#f59e0b", b: "#ff3d71" },
{ a: "#06b6d4", b: "#8b5cf6" },
{ a: "#ec4899", b: "#6366f1" },
{ a: "#22c55e", b: "#eab308" }
];
var coverIndex = -1;
var hasArt = false;
/* ---------- state ---------- */
var tracks = [
{ title: "Paper Lanterns", feat: "Velvet Static", isrc: "US-S1Z-26-00001", dur: 222 },
{ title: "Glass Harbor", feat: "", isrc: "US-S1Z-26-00002", dur: 198 },
{ title: "Slow Tide", feat: "Marlowe Hale", isrc: "US-S1Z-26-00003", dur: 246 },
{ title: "Reservoir (Reprise)", feat: "", isrc: "US-S1Z-26-00004", dur: 171 }
];
var step = 0;
var uid = 100;
/* ---------- elements ---------- */
var $ = function (id) { return document.getElementById(id); };
var dropzone = $("dropzone");
var dzCover = $("dzCover");
var steps = Array.prototype.slice.call(document.querySelectorAll(".step"));
var panels = Array.prototype.slice.call(document.querySelectorAll(".panel"));
var prevBtn = $("prevBtn");
var nextBtn = $("nextBtn");
var tracklist = $("tracklist");
/* ---------- toast ---------- */
var toastEl = $("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
function fmt(sec) {
sec = Math.max(0, Math.round(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" + s : s);
}
/* ---------- COVER / DROPZONE ---------- */
function applyCover(idx) {
var c = COVERS[idx];
document.documentElement.style.setProperty("--cv-a", c.a);
document.documentElement.style.setProperty("--cv-b", c.b);
var grad = "linear-gradient(135deg, " + c.a + ", " + c.b + ")";
dzCover.style.background = grad;
}
function pickCover() {
coverIndex = (coverIndex + 1) % COVERS.length;
hasArt = true;
dropzone.classList.add("has-art");
applyCover(coverIndex);
renderPreview();
renderReview();
}
dropzone.addEventListener("click", function () {
pickCover();
toast("Cover artwork attached");
});
$("shuffleCover").addEventListener("click", function () {
if (!hasArt) { pickCover(); } else { pickCover(); }
toast("Artwork shuffled");
});
/* ---------- DETAIL FIELDS ---------- */
["fTitle", "fArtist", "fLabel", "fGenre", "fDate"].forEach(function (id) {
$(id).addEventListener("input", function () { renderPreview(); renderReview(); });
});
var explicitBtn = $("fExplicit");
explicitBtn.addEventListener("click", function () {
var on = explicitBtn.getAttribute("aria-checked") === "true";
explicitBtn.setAttribute("aria-checked", String(!on));
renderPreview();
renderReview();
});
function isExplicit() { return explicitBtn.getAttribute("aria-checked") === "true"; }
/* ---------- WAVEFORM (deterministic from seed) ---------- */
function waveBars(seed) {
var bars = "";
var x = seed * 9301 + 49297;
for (var i = 0; i < 28; i++) {
x = (x * 9301 + 49297) % 233280;
var h = 20 + Math.floor((x / 233280) * 80);
bars += '<span style="height:' + h + '%"></span>';
}
return bars;
}
/* ---------- TRACKLIST RENDER ---------- */
function renderTracks() {
tracklist.innerHTML = "";
tracks.forEach(function (t, i) {
var li = document.createElement("li");
li.className = "track";
li.draggable = true;
li.dataset.index = i;
li.innerHTML =
'<span class="track-handle" title="Drag to reorder" aria-hidden="true">⋮⋮</span>' +
'<span class="track-index">' + (i + 1) + '</span>' +
'<div class="track-main">' +
'<div class="track-fields">' +
'<input data-k="title" type="text" placeholder="Track title" value="' + esc(t.title) + '" aria-label="Track title" />' +
'<input data-k="feat" type="text" placeholder="Featured artists" value="' + esc(t.feat) + '" aria-label="Featured artists" />' +
'<input data-k="isrc" type="text" placeholder="ISRC" value="' + esc(t.isrc) + '" aria-label="ISRC code" />' +
'</div>' +
'<div class="track-audio">' +
'<div class="wave" aria-hidden="true">' + waveBars(i + 3) + '</div>' +
'<span class="audio-state">audio uploaded · ' + fmt(t.dur) + '</span>' +
'</div>' +
'</div>' +
'<button class="track-remove" type="button" title="Remove track" aria-label="Remove track">✕</button>';
// field bindings
li.querySelectorAll(".track-fields input").forEach(function (inp) {
inp.addEventListener("input", function () {
tracks[Number(li.dataset.index)][inp.dataset.k] = inp.value;
renderPreview();
renderReview();
});
});
// remove
li.querySelector(".track-remove").addEventListener("click", function () {
if (tracks.length <= 1) { toast("A release needs at least one track"); return; }
tracks.splice(Number(li.dataset.index), 1);
renderTracks(); renderPreview(); renderReview();
toast("Track removed");
});
bindDrag(li);
tracklist.appendChild(li);
});
$("trackCount").textContent =
tracks.length + " track" + (tracks.length === 1 ? "" : "s") + " · " + fmt(totalDur()) + " total";
}
function esc(s) {
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
}
function totalDur() { return tracks.reduce(function (a, t) { return a + t.dur; }, 0); }
/* ---------- DRAG & DROP REORDER ---------- */
var dragIdx = null;
function bindDrag(li) {
li.addEventListener("dragstart", function (e) {
dragIdx = Number(li.dataset.index);
li.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
});
li.addEventListener("dragend", function () {
li.classList.remove("dragging");
clearTargets();
dragIdx = null;
});
li.addEventListener("dragover", function (e) {
e.preventDefault();
clearTargets();
li.classList.add("drop-target");
});
li.addEventListener("drop", function (e) {
e.preventDefault();
var to = Number(li.dataset.index);
if (dragIdx === null || dragIdx === to) return;
var moved = tracks.splice(dragIdx, 1)[0];
tracks.splice(to, 0, moved);
renderTracks(); renderPreview(); renderReview();
toast("Reordered to #" + (to + 1));
});
}
function clearTargets() {
Array.prototype.forEach.call(tracklist.children, function (c) { c.classList.remove("drop-target"); });
}
$("addTrack").addEventListener("click", function () {
uid++;
tracks.push({
title: "Untitled Track",
feat: "",
isrc: "US-S1Z-26-" + String(uid).padStart(5, "0"),
dur: 150 + Math.floor(Math.random() * 130)
});
renderTracks(); renderPreview(); renderReview();
toast("Track added");
});
/* ---------- PREVIEW (simulated player) ---------- */
var rcCover = $("rcCover");
var rcPlay = $("rcPlay");
var scrubFill = $("scrubFill");
var scrubKnob = $("scrubKnob");
var scrubber = $("scrubber");
var playing = false;
var cur = 0; // seconds into current track
var activeTrack = 0;
var timer = null;
function curDur() { return tracks[activeTrack] ? tracks[activeTrack].dur : 0; }
function renderPreview() {
$("rcTitle").textContent = $("fTitle").value || "Untitled Release";
$("rcArtist").textContent = $("fArtist").value || "Unknown Artist";
var year = ($("fDate").value || "2026").slice(0, 4);
$("rcMeta").textContent = $("fGenre").value + " · " + year;
$("rcLabel").textContent = $("fLabel").value || "Independent";
$("rcExplicit").hidden = !isExplicit();
if (activeTrack >= tracks.length) activeTrack = 0;
$("rcNow").textContent = "Now playing: " + ((tracks[activeTrack] && tracks[activeTrack].title) || "—");
$("rcDur").textContent = fmt(curDur());
// tracklist
var ul = $("rcList");
ul.innerHTML = "";
tracks.forEach(function (t, i) {
var li = document.createElement("li");
if (i === activeTrack) li.className = "active";
var feat = t.feat ? ' <span class="rc-li-feat">(feat. ' + esc(t.feat) + ")</span>" : "";
li.innerHTML =
'<span class="rc-li-num">' + (i + 1) + '</span>' +
'<span class="rc-li-title">' + esc(t.title || "Untitled") + feat + '</span>' +
(isExplicit() ? '<span class="rc-li-e">E</span>' : '') +
'<span class="rc-li-time">' + fmt(t.dur) + '</span>';
li.addEventListener("click", function () { selectTrack(i, true); });
ul.appendChild(li);
});
updateScrub();
}
function updateScrub() {
var pct = curDur() ? (cur / curDur()) * 100 : 0;
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
$("rcCur").textContent = fmt(cur);
scrubber.setAttribute("aria-valuenow", String(Math.round(pct)));
}
function selectTrack(i, autoplay) {
activeTrack = i;
cur = 0;
renderPreview();
if (autoplay && !playing) startPlay();
else updateScrub();
}
function tick() {
cur += 1;
if (cur >= curDur()) {
// advance to next track
if (activeTrack < tracks.length - 1) {
selectTrack(activeTrack + 1, false);
} else {
stopPlay();
cur = 0;
updateScrub();
toast("Reached end of release");
return;
}
}
updateScrub();
}
function startPlay() {
playing = true;
rcPlay.setAttribute("aria-pressed", "true");
rcCover.classList.add("playing");
clearInterval(timer);
timer = setInterval(tick, 1000);
}
function stopPlay() {
playing = false;
rcPlay.setAttribute("aria-pressed", "false");
rcCover.classList.remove("playing");
clearInterval(timer);
}
rcPlay.addEventListener("click", function () {
if (playing) stopPlay(); else startPlay();
});
/* scrubber interaction */
function seekFromEvent(e) {
var rect = scrubber.getBoundingClientRect();
var x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
var pct = Math.min(1, Math.max(0, x / rect.width));
cur = pct * curDur();
updateScrub();
}
var seeking = false;
scrubber.addEventListener("pointerdown", function (e) {
seeking = true; scrubber.setPointerCapture(e.pointerId); seekFromEvent(e);
});
scrubber.addEventListener("pointermove", function (e) { if (seeking) seekFromEvent(e); });
scrubber.addEventListener("pointerup", function () { seeking = false; });
scrubber.addEventListener("keydown", function (e) {
var d = curDur();
if (e.key === "ArrowRight") { cur = Math.min(d, cur + 5); updateScrub(); e.preventDefault(); }
else if (e.key === "ArrowLeft") { cur = Math.max(0, cur - 5); updateScrub(); e.preventDefault(); }
else if (e.key === "Home") { cur = 0; updateScrub(); e.preventDefault(); }
else if (e.key === "End") { cur = d; updateScrub(); e.preventDefault(); }
});
/* ---------- REVIEW ---------- */
function renderReview() {
var list = $("checklist");
var titledTracks = tracks.filter(function (t) { return t.title && t.title.trim() && t.title !== "Untitled Track"; }).length;
var rows = [
{ k: "Release title", v: $("fTitle").value || "—", ok: !!$("fTitle").value.trim() },
{ k: "Primary artist", v: $("fArtist").value || "—", ok: !!$("fArtist").value.trim() },
{ k: "Cover artwork", v: hasArt ? "Attached" : "Missing", ok: hasArt },
{ k: "Genre / date", v: $("fGenre").value + " · " + ($("fDate").value || "—"), ok: !!$("fDate").value },
{ k: "Tracks", v: tracks.length + " (" + titledTracks + " titled)", ok: tracks.length >= 1 && titledTracks === tracks.length },
{ k: "Total runtime", v: fmt(totalDur()), ok: true },
{ k: "Content rating", v: isExplicit() ? "Explicit" : "Clean", ok: true }
];
list.innerHTML = rows.map(function (r) {
return '<li><span class="ck-key">' + r.k + '</span>' +
'<span class="ck-val">' + esc(r.v) + '</span>' +
'<span class="ck-flag ' + (r.ok ? "ok" : "warn") + '">' + (r.ok ? "Ready" : "Check") + '</span></li>';
}).join("");
}
/* ---------- STEPPER ---------- */
function validateStep(s) {
if (s === 0) {
if (!$("fTitle").value.trim()) { toast("Add a release title to continue"); return false; }
if (!$("fArtist").value.trim()) { toast("Add a primary artist to continue"); return false; }
if (!hasArt) { toast("Pick cover artwork to continue"); return false; }
}
if (s === 1) {
var empty = tracks.some(function (t) { return !t.title.trim(); });
if (empty) { toast("Every track needs a title"); return false; }
}
return true;
}
function goStep(s) {
step = s;
panels.forEach(function (p) { p.classList.toggle("is-active", Number(p.dataset.panel) === s); });
steps.forEach(function (b) {
var i = Number(b.dataset.step);
b.classList.toggle("is-active", i === s);
b.classList.toggle("is-done", i < s);
if (i === s) b.setAttribute("aria-current", "step"); else b.removeAttribute("aria-current");
});
prevBtn.disabled = s === 0;
if (s < 2) {
nextBtn.style.display = "";
nextBtn.textContent = s === 0 ? "Next: Tracks" : "Next: Review";
} else {
nextBtn.style.display = "none";
}
if (s === 2) renderReview();
}
steps.forEach(function (b) {
b.addEventListener("click", function () {
var target = Number(b.dataset.step);
// forward navigation must validate intermediate steps
if (target > step) {
for (var i = step; i < target; i++) { if (!validateStep(i)) return; }
}
goStep(target);
});
});
nextBtn.addEventListener("click", function () {
if (!validateStep(step)) return;
goStep(Math.min(2, step + 1));
});
prevBtn.addEventListener("click", function () { goStep(Math.max(0, step - 1)); });
/* ---------- PUBLISH ---------- */
$("publish").addEventListener("click", function () {
if (!validateStep(0) || !validateStep(1)) { goStep(0); return; }
if (!$("fRights").checked) { toast("Confirm you control the rights"); return; }
var btn = $("publish");
btn.disabled = true;
btn.textContent = "Distributing…";
setTimeout(function () {
btn.textContent = "Published ✓";
toast('"' + ($("fTitle").value || "Release") + '" sent to stores');
setTimeout(function () { btn.disabled = false; btn.textContent = "Publish release"; }, 2400);
}, 1100);
});
/* ---------- INIT ---------- */
pickCover(); // start with an attached cover so preview is themed
renderTracks();
renderPreview();
renderReview();
goStep(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Release Upload — Distribution Studio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" id="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<div class="brand-text">
<strong>Distribution Studio</strong>
<span>New release · draft</span>
</div>
</div>
<nav class="stepper" aria-label="Release steps">
<button class="step is-active" data-step="0" type="button" aria-current="step">
<span class="step-num">1</span> Details
</button>
<span class="step-line" aria-hidden="true"></span>
<button class="step" data-step="1" type="button">
<span class="step-num">2</span> Tracks
</button>
<span class="step-line" aria-hidden="true"></span>
<button class="step" data-step="2" type="button">
<span class="step-num">3</span> Review
</button>
</nav>
</header>
<div class="layout">
<!-- ============ FORM SIDE ============ -->
<section class="form" aria-label="Release form">
<!-- STEP 1 — DETAILS -->
<div class="panel is-active" data-panel="0">
<h2 class="panel-title">Release details</h2>
<p class="panel-sub">Tell us about the release. The preview updates as you type.</p>
<div class="art-row">
<button id="dropzone" class="dropzone" type="button" aria-label="Choose cover artwork">
<div class="dz-cover" id="dzCover" aria-hidden="true"></div>
<div class="dz-overlay">
<span class="dz-icon" aria-hidden="true">+</span>
<span class="dz-hint">Click to pick a cover</span>
<span class="dz-sub">3000×3000 · simulated</span>
</div>
</button>
<div class="art-side">
<span class="field-label">Cover artwork</span>
<p class="muted small">Tap the tile to cycle through generated gradient covers. The release accent is pulled from the art.</p>
<button id="shuffleCover" class="btn ghost" type="button">Shuffle artwork</button>
</div>
</div>
<div class="grid-2">
<label class="field">
<span class="field-label">Release title</span>
<input id="fTitle" type="text" value="Midnight Reservoir" autocomplete="off" />
</label>
<label class="field">
<span class="field-label">Primary artist</span>
<input id="fArtist" type="text" value="Neon Tides" autocomplete="off" />
</label>
<label class="field">
<span class="field-label">Label</span>
<input id="fLabel" type="text" value="Velvet Static Records" autocomplete="off" />
</label>
<label class="field">
<span class="field-label">Genre</span>
<select id="fGenre">
<option>Synthwave</option>
<option>Indie Pop</option>
<option>Electronic</option>
<option>Alt R&B</option>
<option>Dream Pop</option>
<option>Lo-fi</option>
</select>
</label>
<label class="field">
<span class="field-label">Release date</span>
<input id="fDate" type="date" value="2026-09-18" />
</label>
<div class="field">
<span class="field-label">Content</span>
<button id="fExplicit" class="toggle" type="button" role="switch" aria-checked="false">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text">Explicit lyrics</span>
<span class="explicit-badge" aria-hidden="true">E</span>
</button>
</div>
</div>
</div>
<!-- STEP 2 — TRACKS -->
<div class="panel" data-panel="1">
<div class="panel-head">
<div>
<h2 class="panel-title">Tracklist</h2>
<p class="panel-sub">Drag the handle to reorder. Each track simulates an uploaded audio file.</p>
</div>
<button id="addTrack" class="btn" type="button">+ Add track</button>
</div>
<ol class="tracklist" id="tracklist" aria-label="Tracks"></ol>
<p class="muted small track-count" id="trackCount"></p>
</div>
<!-- STEP 3 — REVIEW -->
<div class="panel" data-panel="2">
<h2 class="panel-title">Review & publish</h2>
<p class="panel-sub">Confirm everything looks right before you distribute.</p>
<ul class="checklist" id="checklist"></ul>
<div class="rights">
<label class="check">
<input type="checkbox" id="fRights" checked />
<span>I own or control all rights to this audio and artwork.</span>
</label>
</div>
<button id="publish" class="btn publish" type="button">Publish release</button>
</div>
<!-- FOOTER NAV -->
<div class="form-nav">
<button id="prevBtn" class="btn ghost" type="button" disabled>Back</button>
<button id="nextBtn" class="btn" type="button">Next: Tracks</button>
</div>
</section>
<!-- ============ PREVIEW SIDE ============ -->
<aside class="preview" aria-label="Release preview">
<span class="preview-tag">Live preview</span>
<article class="release-card" id="releaseCard">
<div class="rc-cover" id="rcCover">
<div class="rc-eq" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<button class="rc-play" id="rcPlay" type="button" aria-pressed="false" aria-label="Play preview">
<svg class="ic-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
</div>
<div class="rc-body">
<div class="rc-head">
<h3 class="rc-title" id="rcTitle">Midnight Reservoir</h3>
<span class="rc-explicit" id="rcExplicit" hidden aria-label="Explicit">E</span>
</div>
<p class="rc-artist" id="rcArtist">Neon Tides</p>
<p class="rc-meta" id="rcMeta">Synthwave · 2026</p>
<div class="rc-scrub">
<span class="rc-time" id="rcCur">0:00</span>
<div class="scrubber" id="scrubber" role="slider" tabindex="0"
aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="scrub-fill" id="scrubFill"></div>
<div class="scrub-knob" id="scrubKnob"></div>
</div>
<span class="rc-time" id="rcDur">0:00</span>
</div>
<p class="rc-now muted small" id="rcNow">Now playing: track 1</p>
<ul class="rc-list" id="rcList"></ul>
<div class="rc-foot">
<span id="rcLabel">Velvet Static Records</span>
<span id="rcCount">12,480 monthly listeners</span>
</div>
</div>
</article>
</aside>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Release Upload (tracks · art · metadata)
A full distribution form for getting a fictional record onto stores, art-directed dark-first with an album-art-driven accent. The cover tile is a simulated drop-zone — clicking it cycles through CSS-drawn gradient covers, and the chosen art pulls a --cv-a/--cv-b accent pair that re-themes the buttons, focus rings, stepper, and waveforms. The details step gathers release title, primary artist, label, genre, release date, and an explicit-content switch, all wired to a live preview.
The tracks step is a reorderable list: each row has a drag handle, an inline title / featured-artists / ISRC field group, a tiny generated waveform, and an “audio uploaded ✓” state with a duration timestamp. Tracks can be added, removed (with a one-track floor), and dragged into a new order, and the running track count and total runtime update live. A three-step Details → Tracks → Review stepper guards forward navigation with per-step validation and surfaces issues through a small toast() helper, ending in a review checklist and a publish button that simulates distribution.
The sticky preview is a now-playing release card that mirrors the form in real time — cover gradient, title, artist, genre/year, explicit badge, and the full tracklist. Its play/pause button morphs between icons, drives an animated equalizer and a role="slider" scrubber (pointer-draggable and arrow-key seekable), advances through tracks on a timer, and reflects the currently playing row. Everything is keyboard-usable, AA-contrast, responsive down to ~360px, and honors prefers-reduced-motion.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.