Files
Musify/web-player/design_handoff_musify_player/Musify.dc.html
T
Admin dc1ba89a6d Add likes, follows and playlist visibility; redesign web-player UI
Backend:
- New Likes feature (toggle/get liked tracks) and endpoints
- User follow/unfollow, listening stats, public user profile
- Playlist visibility (public/private) support
- Various auth and DTO/response adjustments to support the above

Frontend (web-player):
- New design system pass: theme tokens/mode (light/dark), glass menu,
  tabs bar, player dock/extras/queue components, mix tiles, artist link
- New routes: Liked, public user profile (/u/[id]), likes API proxy
- Search refactor: shared SearchBox component used by TopBar and
  MobileHeader (search now reachable on mobile), live results as you
  type instead of requiring Enter, dropped the "Buscando…" quick-result
  preview
- Removed now-unused components (MobileNav, CoverBadge, MixBentoTile,
  PlayPauseOverlay, SuggestionsTable, userDirectory)
- Design handoff reference docs added under web-player/design_handoff_musify_player
  and web-player/docs/design-system-refactor-plan.md
2026-09-14 23:42:45 +02:00

2802 lines
95 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: #050507;
}
a {
color: #b4a4ea;
text-decoration: none;
}
a:hover {
color: #cdc0f5;
}
img {
display: block;
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.07);
border-radius: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
@keyframes eq {
0%,
100% {
transform: scaleY(0.25);
}
50% {
transform: scaleY(1);
}
}
@keyframes shim {
0% {
background-position: -420px 0;
}
100% {
background-position: 420px 0;
}
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
</style>
</helmet>
<div
style="position:relative;height:100vh;overflow:hidden;background:{{ pageBg }};transition:background .6s ease;color:#c9c8cc;font-family:'Plus Jakarta Sans',system-ui,sans-serif;-webkit-font-smoothing:antialiased"
>
<div style="position:absolute;inset:0;pointer-events:none;background:{{ ambient }}"></div>
<div
style="
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(130% 85% at 50% 118%, rgba(0, 0, 0, 0.8), transparent 62%);
"
></div>
<div style="position: relative; display: flex; height: 100%">
<!-- SIDEBAR -->
<aside
data-screen-label="Sidebar"
style="display:{{ sidebarDisplay }};width:254px;flex:0 0 auto;flex-direction:column;padding:22px 12px 0 20px;border-right:1px solid {{ hairline }};background:{{ sidebarBg }};transition:background .6s ease"
>
<div style="display: flex; align-items: center; gap: 11px; padding: 0 6px 26px">
<div
style="width:24px;height:24px;border-radius:8px;background:{{ logoGrad }};box-shadow:{{ logoGlow }};transition:background .6s ease,box-shadow .6s ease"
></div>
<div
style="
font-family: 'Sora', sans-serif;
font-weight: 600;
font-size: 17.5px;
letter-spacing: -0.02em;
color: #d8d7db;
"
>
Musify
</div>
</div>
<nav style="display: flex; flex-direction: column; gap: 2px">
<sc-for list="{{ nav }}" as="n" hint-placeholder-count="5">
<div
onClick="{{ n.go }}"
style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:10px;cursor:pointer;font-size:13.5px;font-weight:500;color:{{ n.color }};background:{{ n.bg }};transition:background .16s ease,color .16s ease"
style-hover="background:rgba(255,255,255,.04);color:#d4d3d7"
>
<svg
viewBox="0 0 24 24"
width="17"
height="17"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="{{ n.icon }}"></path>
</svg>
<span style="flex: 1">{{ n.label }}</span>
<span style="font-size: 11px; font-variant-numeric: tabular-nums; color: #5c5c60"
>{{ n.count }}</span
>
</div>
</sc-for>
</nav>
<div
style="margin: 24px 12px 14px; height: 1px; background: rgba(255, 255, 255, 0.05)"
></div>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px 10px;
"
>
<div
style="
font-size: 10.5px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: #646468;
font-weight: 600;
"
>
Tus playlists
</div>
<div
onClick="{{ goPlaylists }}"
style="font-size: 15px; color: #646468; cursor: pointer; line-height: 1"
style-hover="color:#cfccd8"
>
+
</div>
</div>
<div
style="
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1px;
padding-bottom: 18px;
"
>
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="6">
<div
onClick="{{ p.open }}"
style="display:flex;align-items:center;gap:11px;padding:7px 12px;border-radius:10px;cursor:pointer;background:{{ p.bg }};transition:background .16s ease"
style-hover="background:rgba(255,255,255,.04)"
>
<div
style="position:relative;width:34px;height:34px;border-radius:8px;flex:0 0 auto;overflow:hidden;background:{{ p.fallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.88"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
>
{{ p.name }}
</div>
<div
style="
font-size: 11px;
color: #6c6a75;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
"
>
{{ p.meta }}
</div>
</div>
<sc-if value="{{ p.isPlaying }}" hint-placeholder-val="{{ false }}">
<div
style="
display: flex;
align-items: flex-end;
gap: 2px;
height: 11px;
flex: 0 0 auto;
"
>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
></div>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .3s infinite"
></div>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .6s infinite"
></div>
</div>
</sc-if>
</div>
</sc-for>
</div>
</aside>
<!-- MAIN -->
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column">
<header
style="
display: flex;
align-items: center;
gap: 14px;
padding: 16px 26px 12px;
flex: 0 0 auto;
"
>
<div style="display: flex; gap: 6px">
<div
onClick="{{ back }}"
style="
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.035);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
color: #8f8f93;
"
style-hover="color:#e4e2ea;background:rgba(255,255,255,.07)"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 18l-6-6 6-6"></path>
</svg>
</div>
<div
style="
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.035);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
color: #555559;
"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18l6-6-6-6"></path>
</svg>
</div>
</div>
<div
style="
flex: 1;
max-width: 480px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 14px;
height: 38px;
border-radius: 11px;
background: rgba(255, 255, 255, 0.035);
border: 1px solid rgba(255, 255, 255, 0.055);
"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="#737377"
stroke-width="1.8"
stroke-linecap="round"
>
<circle cx="11" cy="11" r="7"></circle>
<path d="M20 20l-3.6-3.6"></path>
</svg>
<input
value="{{ query }}"
onChange="{{ onQuery }}"
placeholder="Canciones, artistas, álbumes o playlists"
style="
flex: 1;
background: transparent;
border: 0;
outline: none;
color: #c9c8cc;
font-family: inherit;
font-size: 13px;
"
/>
</div>
<div style="flex: 1"></div>
<div
onClick="{{ toggleQueue }}"
style="height:33px;display:flex;align-items:center;gap:8px;padding:0 13px;border-radius:10px;cursor:pointer;font-size:12.5px;font-weight:500;color:{{ queueBtnColor }};background:{{ queueBtnBg }};border:1px solid rgba(255,255,255,.055)"
style-hover="color:#dbdadf"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
>
<path d="M4 7h11M4 12h11M4 17h7M19 10v8"></path>
</svg>
Cola
</div>
<div
style="
width: 33px;
height: 33px;
border-radius: 50%;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
flex: 0 0 auto;
"
>
<img
src="https://picsum.photos/seed/musify-user-ari/120/120"
alt=""
style="width: 100%; height: 100%; object-fit: cover; opacity: 0.9"
/>
</div>
</header>
<main style="flex:1;overflow-y:auto;padding:6px 26px;padding-bottom:{{ bottomPad }}">
<!-- LOADING -->
<sc-if value="{{ isLoading }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="Loading">
<div
style="
height: 260px;
border-radius: 22px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.025),
rgba(255, 255, 255, 0.055),
rgba(255, 255, 255, 0.025)
);
background-size: 420px 100%;
animation: shim 1.25s linear infinite;
margin-bottom: 38px;
"
></div>
<sc-for list="{{ skeletonRows }}" as="s" hint-placeholder-count="2">
<div style="margin-bottom: 34px">
<div
style="
width: 170px;
height: 14px;
border-radius: 6px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.025),
rgba(255, 255, 255, 0.055),
rgba(255, 255, 255, 0.025)
);
background-size: 420px 100%;
animation: shim 1.25s linear infinite;
margin-bottom: 18px;
"
></div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
gap: 20px;
"
>
<sc-for list="{{ skeletonCards }}" as="c" hint-placeholder-count="6">
<div>
<div
style="
aspect-ratio: 1;
border-radius: 13px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.025),
rgba(255, 255, 255, 0.055),
rgba(255, 255, 255, 0.025)
);
background-size: 420px 100%;
animation: shim 1.25s linear infinite;
"
></div>
<div
style="
width: 72%;
height: 10px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.05);
margin: 14px 0 8px;
"
></div>
<div
style="
width: 42%;
height: 9px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.032);
"
></div>
</div>
</sc-for>
</div>
</div>
</sc-for>
</div>
</sc-if>
<!-- ERROR -->
<sc-if value="{{ isError }}" hint-placeholder-val="{{ false }}">
<div
data-screen-label="Error"
style="
max-width: 420px;
margin: 14vh auto;
text-align: center;
animation: rise 0.3s ease;
"
>
<div
style="
width: 52px;
height: 52px;
margin: 0 auto 22px;
border-radius: 15px;
background: rgba(226, 130, 130, 0.08);
border: 1px solid rgba(226, 130, 130, 0.18);
display: grid;
place-items: center;
color: #e09292;
font-family: 'Sora', sans-serif;
font-size: 21px;
"
>
!
</div>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 10px;
color: #d5d4d8;
"
>
No se pudo cargar tu música
</div>
<div
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 26px"
>
La conexión se interrumpió. Lo que tengas descargado sigue disponible sin
conexión.
</div>
<div style="display: flex; gap: 10px; justify-content: center">
<div
onClick="{{ retry }}"
style="
padding: 11px 20px;
border-radius: 11px;
background: #c6c4cb;
color: #0a0a0d;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
"
style-hover="background:#d6d5da"
>
Reintentar
</div>
<div
onClick="{{ goPlaylists }}"
style="
padding: 11px 20px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
color: #a9a7b2;
"
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
>
Ver descargas
</div>
</div>
</div>
</sc-if>
<!-- HOME -->
<sc-if value="{{ showHome }}" hint-placeholder-val="{{ true }}">
<div data-screen-label="Home">
<!-- HERO -->
<section
style="position:relative;overflow:hidden;border-radius:24px;margin-bottom:42px;background:{{ heroBg }}"
>
<div
style="
position: relative;
padding: 36px 36px 32px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 30px;
flex-wrap: wrap;
"
>
<div style="max-width: 560px; min-width: 260px">
<h1
style="
font-family: 'Sora', sans-serif;
font-size: 36px;
line-height: 1.1;
font-weight: 600;
letter-spacing: -0.035em;
margin: 0 0 14px;
color: #d9d8dc;
text-wrap: pretty;
"
>
{{ heroGreeting }}
</h1>
<div
style="
font-size: 13.5px;
color: #97979b;
line-height: 1.65;
max-width: 430px;
"
>
{{ heroBlurb }}
</div>
<div
style="
display: flex;
gap: 10px;
margin-top: 26px;
flex-wrap: wrap;
align-items: center;
"
>
<div
onClick="{{ playHero }}"
style="
display: flex;
align-items: center;
gap: 9px;
padding: 12px 21px;
border-radius: 12px;
background: #c6c4cb;
color: #0a0a0d;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
"
style-hover="background:#d8d7dc"
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
Reanudar {{ heroResume }}
</div>
<div
style="
display: flex;
align-items: center;
gap: 9px;
padding: 12px 19px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.11);
font-size: 13px;
font-weight: 500;
cursor: pointer;
color: #aeaeb2;
"
style-hover="background:rgba(255,255,255,.05);color:#d9d8dd"
>
Ver resumen semanal
</div>
</div>
</div>
<div style="display: flex; gap: 26px; padding: 4px 2px">
<sc-for list="{{ heroStats }}" as="s" hint-placeholder-count="3">
<div>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 25px;
font-weight: 600;
letter-spacing: -0.03em;
color: #d4d3d7;
"
>
{{ s.v }}
</div>
<div style="font-size: 11.5px; color: #818185; margin-top: 4px">
{{ s.k }}
</div>
</div>
</sc-for>
</div>
</div>
</section>
<!-- CONTINUE -->
<section style="margin-bottom: 46px">
<div style="margin-bottom: 16px">
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0;
color: #d3d2d6;
"
>
Continuar escuchando
</h2>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
gap: 10px;
"
>
<sc-for list="{{ continueRow }}" as="c" hint-placeholder-count="6">
<div
onMouseEnter="{{ c.enter }}"
onMouseLeave="{{ c.leave }}"
onClick="{{ c.play }}"
style="display:flex;align-items:center;gap:13px;padding:9px 14px 9px 9px;border-radius:13px;cursor:pointer;background:{{ c.bg }};border:1px solid {{ c.border }};transition:background .16s ease"
>
<div
style="width:46px;height:46px;border-radius:9px;overflow:hidden;flex:0 0 auto;background:{{ c.fallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ c.img }});background-size:cover;background-position:center;opacity:.92"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ c.titleColor }}"
>
{{ c.title }}
</div>
<div
style="
font-size: 11.5px;
color: #848488;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 3px;
"
>
{{ c.sub }}
</div>
</div>
<div
style="flex:0 0 auto;color:{{ accent }};opacity:{{ c.btnOpacity }};transition:opacity .16s ease"
>
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
</div>
</sc-for>
</div>
</section>
<!-- SPOTLIGHT PLAYLIST -->
<section style="margin-bottom: 48px">
<div
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:0;border-radius:22px;overflow:hidden;background:{{ spotlightBg }}"
>
<div
style="
padding: 32px 30px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
"
>
<div style="display: flex; align-items: center; gap: 16px">
<div
style="
width: 88px;
height: 88px;
border-radius: 14px;
overflow: hidden;
flex: 0 0 auto;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
"
>
<div
style="width:100%;height:100%;background-image:url({{ spotlight.img }});background-size:cover;background-position:center"
></div>
</div>
<div style="min-width: 0">
<div
style="
font-size: 10.5px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #8f8f93;
font-weight: 600;
margin-bottom: 9px;
"
>
Playlist destacada
</div>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 23px;
font-weight: 600;
letter-spacing: -0.03em;
color: #d8d7db;
line-height: 1.15;
"
>
{{ spotlight.name }}
</div>
<div style="font-size: 12px; color: #8a8a8e; margin-top: 7px">
{{ spotlight.meta }}
</div>
</div>
</div>
<div
style="
font-size: 13px;
line-height: 1.7;
color: #919195;
max-width: 400px;
"
>
{{ spotlight.blurb }}
</div>
<div style="display: flex; gap: 9px; margin-top: 4px">
<div
onClick="{{ spotlight.play }}"
style="display:flex;align-items:center;gap:8px;padding:10px 18px;border-radius:11px;background:{{ accentBtnBg }};border:1px solid {{ accentLine }};color:{{ accentTitle }};font-size:12.5px;font-weight:600;cursor:pointer"
style-hover="background:{{ accentBtnBgHover }}"
>
<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor">
<path d="M8 5v14l11-7z"></path></svg
>Reproducir
</div>
<div
style="
padding: 10px 16px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.09);
font-size: 12.5px;
font-weight: 500;
color: #a8a8ac;
cursor: pointer;
"
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
>
Mezclar
</div>
</div>
</div>
<div
style="
padding: 22px 24px;
display: flex;
flex-direction: column;
gap: 2px;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.26), rgba(0, 0, 0, 0));
"
>
<sc-for list="{{ spotlightTracks }}" as="t" hint-placeholder-count="5">
<div
onMouseEnter="{{ t.enter }}"
onMouseLeave="{{ t.leave }}"
onClick="{{ t.play }}"
style="display:flex;align-items:center;gap:12px;padding:8px 10px;border-radius:10px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
>
<div
style="
width: 20px;
font-size: 11.5px;
font-variant-numeric: tabular-nums;
color: #5f5d68;
text-align: center;
"
>
{{ t.n }}
</div>
<div style="min-width: 0; flex: 1">
<div
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
>
{{ t.title }}
</div>
<div
style="
font-size: 11px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
"
>
{{ t.artist }}
</div>
</div>
<div
style="
font-size: 11.5px;
font-variant-numeric: tabular-nums;
color: #6c6c70;
"
>
{{ t.dur }}
</div>
</div>
</sc-for>
</div>
</div>
</section>
<!-- ALBUM GRID -->
<section style="margin-bottom: 48px">
<div
style="
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 18px;
"
>
<div>
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0;
color: #d3d2d6;
"
>
Mixes
</h2>
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
Generados a partir de lo que más repites
</div>
</div>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
gap: 20px;
"
>
<sc-for list="{{ albums }}" as="it" hint-placeholder-count="6">
<div
onMouseEnter="{{ it.enter }}"
onMouseLeave="{{ it.leave }}"
onClick="{{ it.play }}"
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ it.cardBg }};transition:background .18s ease;transform:{{ it.lift }}"
>
<div
style="position:relative;aspect-ratio:1;border-radius:13px;overflow:hidden;background:{{ it.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
>
<div
style="width:100%;height:100%;background-image:url({{ it.img }});background-size:cover;background-position:center;opacity:.94"
></div>
<div
style="
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
transparent 45%,
rgba(0, 0, 0, 0.42)
);
"
></div>
<div
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.76);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;transition:opacity .18s ease,transform .18s ease;opacity:{{ it.btnOpacity }};transform:{{ it.btnLift }}"
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
</div>
<div
style="
margin-top: 13px;
font-size: 13px;
font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #cbcacf;
"
>
{{ it.title }}
</div>
<div
style="
margin-top: 4px;
font-size: 11.5px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ it.sub }}
</div>
</div>
</sc-for>
</div>
</section>
<!-- ARTISTS -->
<section style="margin-bottom: 48px">
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0 0 18px;
color: #d3d2d6;
"
>
Artistas que sigues
</h2>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
gap: 18px;
"
>
<sc-for list="{{ artists }}" as="a" hint-placeholder-count="7">
<div
onMouseEnter="{{ a.enter }}"
onMouseLeave="{{ a.leave }}"
onClick="{{ a.play }}"
style="cursor:pointer;text-align:center;padding:10px;margin:-10px;border-radius:16px;background:{{ a.cardBg }};transition:background .18s ease;transform:{{ a.lift }}"
>
<div
style="aspect-ratio:1;border-radius:50%;overflow:hidden;background:{{ a.fallback }};box-shadow:0 12px 28px rgba(0,0,0,.4)"
>
<div
style="width:100%;height:100%;background-image:url({{ a.img }});background-size:cover;background-position:center;opacity:.92"
></div>
</div>
<div
style="
margin-top: 13px;
font-size: 12.5px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #cbcacf;
"
>
{{ a.title }}
</div>
<div
style="
margin-top: 4px;
font-size: 11px;
color: #7d7d81;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ a.sub }}
</div>
</div>
</sc-for>
</div>
</section>
<!-- POPULAR -->
<section style="margin-bottom: 48px">
<div
style="
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16px;
"
>
<div>
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0;
color: #d3d2d6;
"
>
Populares esta semana
</h2>
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
Lo más escuchado entre quienes comparten tu gusto
</div>
</div>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
gap: 6px 34px;
"
>
<sc-for list="{{ popular }}" as="t" hint-placeholder-count="8">
<div
onMouseEnter="{{ t.enter }}"
onMouseLeave="{{ t.leave }}"
onClick="{{ t.play }}"
style="display:flex;align-items:center;gap:14px;padding:8px 12px;border-radius:11px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
>
<div
style="
width: 18px;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: #646468;
text-align: center;
display: flex;
justify-content: center;
height: 14px;
align-items: flex-end;
"
>
<sc-if value="{{ t.isCurrent }}" hint-placeholder-val="{{ false }}">
<div
style="display: flex; align-items: flex-end; gap: 2px; height: 13px"
>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
></div>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .3s infinite"
></div>
<div
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .6s infinite"
></div>
</div>
</sc-if>
<sc-if value="{{ t.showIndex }}" hint-placeholder-val="{{ true }}">
<span>{{ t.n }}</span>
</sc-if>
</div>
<div
style="width:38px;height:38px;border-radius:8px;overflow:hidden;flex:0 0 auto;background:{{ t.fallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ t.img }});background-size:cover;background-position:center;opacity:.9"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
>
{{ t.title }}
</div>
<div
style="
font-size: 11.5px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
"
>
{{ t.artist }}
</div>
</div>
<div
style="
font-size: 11px;
color: #6c6c70;
font-variant-numeric: tabular-nums;
"
>
{{ t.plays }}
</div>
<div
style="
font-size: 11.5px;
color: #6c6c70;
font-variant-numeric: tabular-nums;
"
>
{{ t.dur }}
</div>
</div>
</sc-for>
</div>
</section>
<!-- USER PLAYLISTS -->
<section style="margin-bottom: 20px">
<div
style="
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 18px;
"
>
<div>
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0;
color: #d3d2d6;
"
>
Tus playlists
</h2>
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
Creadas y guardadas por ti
</div>
</div>
<div
onClick="{{ goPlaylists }}"
style="
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: #747478;
cursor: pointer;
"
style-hover="color:#c9c8cd"
>
Ver todas
</div>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
gap: 14px;
"
>
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="6">
<div
onMouseEnter="{{ p.enter }}"
onMouseLeave="{{ p.leave }}"
onClick="{{ p.open }}"
style="display:flex;align-items:center;gap:14px;padding:12px;border-radius:15px;cursor:pointer;background:{{ p.cardBg }};border:1px solid {{ p.border }};transition:background .18s ease;transform:{{ p.lift }}"
>
<div
style="width:62px;height:62px;border-radius:11px;overflow:hidden;flex:0 0 auto;background:{{ p.fallback }};box-shadow:0 10px 24px rgba(0,0,0,.42)"
>
<div
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.92"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="font-size:13.5px;font-weight:600;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
>
{{ p.name }}
</div>
<div
style="
font-size: 11.5px;
color: #818185;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ p.meta }}
</div>
<div
style="
font-size: 11px;
color: #696a6d;
margin-top: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ p.artistsLine }}
</div>
</div>
<div
style="flex:0 0 auto;color:{{ accent }};opacity:{{ p.btnOpacity }};transition:opacity .16s ease"
>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
</div>
</sc-for>
</div>
</section>
</div>
</sc-if>
<!-- EXPLORE -->
<sc-if value="{{ showExplore }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="Explore" style="animation: rise 0.3s ease">
<h1
style="
font-family: 'Sora', sans-serif;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.03em;
margin: 12px 0 6px;
color: #eeecf3;
"
>
Descubrir
</h1>
<div style="font-size: 13px; color: #8a8a8e; margin-bottom: 30px">
Rutas de escucha fuera de tu historial. Se renuevan cada 12 horas.
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
gap: 14px;
margin-bottom: 46px;
"
>
<sc-for list="{{ genres }}" as="g" hint-placeholder-count="8">
<div
style="position:relative;overflow:hidden;height:112px;border-radius:16px;padding:17px;cursor:pointer;border:1px solid rgba(255,255,255,.05);background:{{ g.bg }};transition:background .18s ease"
style-hover="background:rgba(255,255,255,.03)"
>
<div
style="
position: relative;
font-family: 'Sora', sans-serif;
font-size: 15.5px;
font-weight: 600;
letter-spacing: -0.02em;
color: #d4d3d7;
"
>
{{ g.name }}
</div>
<div
style="
position: relative;
font-size: 11.5px;
color: #919195;
margin-top: 6px;
"
>
{{ g.meta }}
</div>
<div
style="
position: absolute;
right: -16px;
bottom: -22px;
width: 80px;
height: 80px;
border-radius: 18px;
overflow: hidden;
transform: rotate(20deg);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
"
>
<div
style="width:100%;height:100%;background-image:url({{ g.img }});background-size:cover;background-position:center;opacity:.82"
></div>
</div>
</div>
</sc-for>
</div>
<section style="margin-bottom: 44px">
<h2
style="
font-family: 'Sora', sans-serif;
font-size: 17.5px;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0 0 5px;
color: #d3d2d6;
"
>
Fuera de tu burbuja
</h2>
<div style="font-size: 12px; color: #7d7d81; margin-bottom: 18px">
Álbumes que tu historial nunca te habría servido
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
gap: 20px;
"
>
<sc-for list="{{ discover }}" as="it" hint-placeholder-count="6">
<div
onMouseEnter="{{ it.enter }}"
onMouseLeave="{{ it.leave }}"
onClick="{{ it.play }}"
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ it.cardBg }};transition:background .18s ease;transform:{{ it.lift }}"
>
<div
style="position:relative;aspect-ratio:1;border-radius:13px;overflow:hidden;background:{{ it.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
>
<div
style="width:100%;height:100%;background-image:url({{ it.img }});background-size:cover;background-position:center;opacity:.94"
></div>
<div
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.76);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;opacity:{{ it.btnOpacity }};transition:opacity .18s ease"
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
</div>
<div
style="
margin-top: 13px;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #cbcacf;
"
>
{{ it.title }}
</div>
<div
style="
margin-top: 4px;
font-size: 11.5px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ it.sub }}
</div>
</div>
</sc-for>
</div>
</section>
</div>
</sc-if>
<!-- TRACK LIST -->
<sc-if value="{{ showList }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="Tracklist" style="animation: rise 0.3s ease">
<div
style="
display: flex;
align-items: flex-end;
gap: 26px;
flex-wrap: wrap;
padding: 14px 0 32px;
"
>
<div
style="width:164px;height:164px;border-radius:18px;flex:0 0 auto;overflow:hidden;background:{{ listFallback }};box-shadow:0 24px 56px rgba(0,0,0,.55)"
>
<div
style="width:100%;height:100%;background-image:url({{ listImg }});background-size:cover;background-position:center"
></div>
</div>
<div style="min-width: 240px">
<div
style="
font-size: 10.5px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #8a8a8e;
font-weight: 600;
margin-bottom: 12px;
"
>
{{ listKind }}
</div>
<h1
style="
font-family: 'Sora', sans-serif;
font-size: 33px;
font-weight: 600;
letter-spacing: -0.035em;
margin: 0 0 13px;
color: #d9d8dc;
text-wrap: pretty;
"
>
{{ listTitle }}
</h1>
<div style="font-size: 12.5px; color: #8a8a8e">{{ listMeta }}</div>
<div style="display: flex; gap: 10px; margin-top: 22px">
<div
onClick="{{ playHero }}"
style="
display: flex;
align-items: center;
gap: 9px;
padding: 11px 21px;
border-radius: 11px;
background: #c6c4cb;
color: #0a0a0d;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
"
style-hover="background:#e3e1ea"
>
<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor">
<path d="M8 5v14l11-7z"></path></svg
>Reproducir
</div>
<div
style="
width: 40px;
height: 40px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.09);
display: grid;
place-items: center;
cursor: pointer;
color: #919195;
"
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
>
<path
d="M16 3l5 5-5 5M21 8H9a4 4 0 00-4 4M8 21l-5-5 5-5M3 16h12a4 4 0 004-4"
></path>
</svg>
</div>
</div>
</div>
</div>
<div
style="
display: grid;
grid-template-columns: 32px 1fr 1fr 92px;
gap: 16px;
padding: 0 14px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 10.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #616165;
font-weight: 600;
"
>
<div>#</div>
<div>Título</div>
<div>Álbum</div>
<div style="text-align: right">Duración</div>
</div>
<sc-for list="{{ listTracks }}" as="t" hint-placeholder-count="8">
<div
onMouseEnter="{{ t.enter }}"
onMouseLeave="{{ t.leave }}"
onClick="{{ t.play }}"
style="display:grid;grid-template-columns:32px 1fr 1fr 92px;gap:16px;align-items:center;padding:10px 14px;border-radius:12px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
>
<div
style="
font-size: 12px;
font-variant-numeric: tabular-nums;
color: #646468;
display: flex;
align-items: flex-end;
gap: 3px;
height: 14px;
"
>
<sc-if value="{{ t.isCurrent }}" hint-placeholder-val="{{ false }}">
<div style="display: flex; align-items: flex-end; gap: 2px; height: 13px">
<div
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
></div>
<div
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .25s infinite"
></div>
<div
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .5s infinite"
></div>
</div>
</sc-if>
<sc-if value="{{ t.showIndex }}" hint-placeholder-val="{{ true }}">
<span>{{ t.n }}</span>
</sc-if>
</div>
<div style="display: flex; align-items: center; gap: 13px; min-width: 0">
<div
style="width:40px;height:40px;border-radius:9px;flex:0 0 auto;overflow:hidden;background:{{ t.fallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ t.img }});background-size:cover;background-position:center;opacity:.9"
></div>
</div>
<div style="min-width: 0">
<div
style="font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
>
{{ t.title }}
</div>
<div
style="
font-size: 11.5px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 3px;
"
>
{{ t.artist }}
</div>
</div>
</div>
<div
style="
font-size: 12px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ t.album }}
</div>
<div
style="
display: flex;
align-items: center;
justify-content: flex-end;
gap: 14px;
"
>
<div
onClick="{{ t.like }}"
style="color:{{ t.likeColor }};display:grid;place-items:center;opacity:{{ t.likeOpacity }};transition:opacity .15s ease"
>
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="{{ t.likeFill }}"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
>
<path
d="M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z"
></path>
</svg>
</div>
<div
style="
font-size: 12px;
font-variant-numeric: tabular-nums;
color: #7f7f83;
min-width: 34px;
text-align: right;
"
>
{{ t.dur }}
</div>
</div>
</div>
</sc-for>
</div>
</sc-if>
<!-- PLAYLISTS INDEX -->
<sc-if value="{{ showPlaylistIndex }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="Playlists" style="animation: rise 0.3s ease">
<div
style="
display: flex;
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
gap: 14px;
margin: 12px 0 28px;
"
>
<div>
<h1
style="
font-family: 'Sora', sans-serif;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.03em;
margin: 0 0 6px;
color: #eeecf3;
"
>
Tus playlists
</h1>
<div style="font-size: 13px; color: #8a8a8e">
7 playlists · 564 canciones · 38 h 12 min
</div>
</div>
<div
style="
padding: 11px 18px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 12.5px;
font-weight: 500;
color: #aeaeb2;
cursor: pointer;
"
style-hover="color:#d9d8dd;background:rgba(255,255,255,.04)"
>
Nueva playlist
</div>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
gap: 22px;
"
>
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="7">
<div
onMouseEnter="{{ p.enter }}"
onMouseLeave="{{ p.leave }}"
onClick="{{ p.open }}"
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ p.cardBg }};transition:background .18s ease;transform:{{ p.lift }}"
>
<div
style="position:relative;aspect-ratio:1;border-radius:14px;overflow:hidden;background:{{ p.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
>
<div
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.93"
></div>
<div
style="
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
transparent 50%,
rgba(0, 0, 0, 0.5)
);
"
></div>
<div
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.78);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;opacity:{{ p.btnOpacity }};transition:opacity .18s ease"
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
</div>
<div
style="margin-top:13px;font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
>
{{ p.name }}
</div>
<div
style="
margin-top: 4px;
font-size: 11.5px;
color: #7f7f83;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ p.meta }}
</div>
<div
style="
margin-top: 3px;
font-size: 11px;
color: #696a6d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ p.artistsLine }}
</div>
</div>
</sc-for>
</div>
</div>
</sc-if>
<!-- EMPTY -->
<sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}">
<div
data-screen-label="Empty"
style="
max-width: 420px;
margin: 13vh auto;
text-align: center;
animation: rise 0.3s ease;
"
>
<div
style="
width: 70px;
height: 70px;
margin: 0 auto 24px;
border-radius: 19px;
background: linear-gradient(
150deg,
rgba(156, 134, 240, 0.1),
rgba(79, 191, 174, 0.05)
);
border: 1px solid rgba(255, 255, 255, 0.07);
display: grid;
place-items: center;
"
>
<svg
viewBox="0 0 24 24"
width="25"
height="25"
fill="none"
stroke="#919195"
stroke-width="1.5"
stroke-linecap="round"
>
<path d="M9 18V6l10-2v12"></path>
<circle cx="6.5" cy="18" r="2.5"></circle>
<circle cx="16.5" cy="16" r="2.5"></circle>
</svg>
</div>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.025em;
margin-bottom: 10px;
color: #d5d4d8;
"
>
{{ emptyTitle }}
</div>
<div
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 24px"
>
{{ emptyBody }}
</div>
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap">
<div
onClick="{{ goExplore }}"
style="
padding: 11px 20px;
border-radius: 11px;
background: #c6c4cb;
color: #0a0a0d;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
"
style-hover="background:#d6d5da"
>
{{ emptyCta }}
</div>
<div
onClick="{{ goHome }}"
style="
padding: 11px 20px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
color: #aeaeb2;
"
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
>
Volver al inicio
</div>
</div>
</div>
</sc-if>
<!-- NO RESULTS -->
<sc-if value="{{ showNoResults }}" hint-placeholder-val="{{ false }}">
<div
data-screen-label="No results"
style="
max-width: 450px;
margin: 13vh auto;
text-align: center;
animation: rise 0.3s ease;
"
>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.025em;
margin-bottom: 10px;
color: #d5d4d8;
"
>
Sin resultados para “{{ query }}”
</div>
<div
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 22px"
>
Revisa la ortografía o prueba con un término más corto. También puedes buscar
por álbum o por playlist.
</div>
<div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap">
<sc-for list="{{ suggestions }}" as="s" hint-placeholder-count="4">
<div
onClick="{{ s.go }}"
style="
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.08);
font-size: 12px;
color: #9b9b9f;
cursor: pointer;
"
style-hover="color:#dbdadf;background:rgba(255,255,255,.05)"
>
{{ s.label }}
</div>
</sc-for>
</div>
</div>
</sc-if>
</main>
</div>
<!-- QUEUE -->
<sc-if value="{{ queueOpen }}" hint-placeholder-val="{{ false }}">
<aside
data-screen-label="Queue"
style="width:{{ queueW }};flex:0 0 auto;display:flex;flex-direction:column;border-left:1px solid {{ hairline }};padding:20px 16px;padding-bottom:{{ bottomPad }};animation:rise .22s ease;background:{{ panelBg }};transition:background .6s ease"
>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
"
>
<div
style="
font-family: 'Sora', sans-serif;
font-size: 14.5px;
font-weight: 600;
letter-spacing: -0.02em;
color: #d3d2d6;
"
>
En cola
</div>
<div
onClick="{{ toggleQueue }}"
style="font-size: 16px; color: #747478; cursor: pointer; line-height: 1"
style-hover="color:#d0cfd4"
>
×
</div>
</div>
<div
style="
font-size: 10.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #616165;
font-weight: 600;
margin-bottom: 11px;
"
>
Reproduciendo
</div>
<div
style="display:flex;align-items:center;gap:12px;padding:10px;border-radius:12px;background:{{ accentSoft }};border:1px solid {{ accentLine }};margin-bottom:24px"
>
<div
style="width:40px;height:40px;border-radius:9px;flex:0 0 auto;overflow:hidden;background:{{ curFallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ curImg }});background-size:cover;background-position:center"
></div>
</div>
<div style="min-width: 0">
<div
style="font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ accentTitle }}"
>
{{ curTitle }}
</div>
<div
style="
font-size: 11px;
color: #848488;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 3px;
"
>
{{ curArtist }}
</div>
</div>
</div>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 11px;
"
>
<div
style="
min-width: 0;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 10.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #616165;
font-weight: 600;
"
>
A continuación · {{ queueSource }}
</div>
<div
style="flex: 0 0 auto; font-size: 11px; color: #646468; cursor: pointer"
style-hover="color:#c9c8cd"
>
Vaciar
</div>
</div>
<div
style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px"
>
<sc-for list="{{ queue }}" as="q" hint-placeholder-count="6">
<div
onClick="{{ q.play }}"
style="
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: 11px;
cursor: pointer;
transition: background 0.15s ease;
"
style-hover="background:rgba(255,255,255,.04)"
>
<div
style="width:36px;height:36px;border-radius:8px;flex:0 0 auto;overflow:hidden;background:{{ q.fallback }}"
>
<div
style="width:100%;height:100%;background-image:url({{ q.img }});background-size:cover;background-position:center;opacity:.88"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="
font-size: 12px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #c2c1c6;
"
>
{{ q.title }}
</div>
<div
style="
font-size: 11px;
color: #7d7d81;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
"
>
{{ q.artist }}
</div>
</div>
<div
style="font-size: 11px; font-variant-numeric: tabular-nums; color: #696a6d"
>
{{ q.dur }}
</div>
</div>
</sc-for>
</div>
</aside>
</sc-if>
</div>
<!-- PLAYER -->
<div
data-screen-label="Player"
style="
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0 16px 16px;
pointer-events: none;
"
>
<div
style="pointer-events:auto;display:flex;align-items:center;gap:18px;padding:11px 16px;border-radius:18px;border:1px solid {{ hairline }};background:{{ barBg }};backdrop-filter:blur(22px);box-shadow:0 20px 56px rgba(0,0,0,.6);transition:background .6s ease"
>
<div
style="display:flex;align-items:center;gap:13px;min-width:0;width:{{ playerInfoW }}"
>
<div
style="width:48px;height:48px;border-radius:11px;flex:0 0 auto;overflow:hidden;background:{{ curFallback }};box-shadow:0 8px 20px rgba(0,0,0,.5)"
>
<div
style="width:100%;height:100%;background-image:url({{ curImg }});background-size:cover;background-position:center"
></div>
</div>
<div style="min-width: 0; flex: 1">
<div
style="
font-size: 13px;
font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #d4d3d7;
"
>
{{ curTitle }}
</div>
<div
style="
font-size: 11.5px;
color: #848488;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 3px;
"
>
{{ curArtist }}
</div>
</div>
<div
onClick="{{ likeCurrent }}"
style="flex:0 0 auto;cursor:pointer;color:{{ curLikeColor }}"
style-hover="opacity:.8"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="{{ curLikeFill }}"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
>
<path
d="M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z"
></path>
</svg>
</div>
</div>
<div
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
"
>
<div style="display: flex; align-items: center; gap: 14px">
<div
onClick="{{ toggleShuffle }}"
style="cursor:pointer;color:{{ shuffleColor }};display:{{ extrasDisplay }}"
style-hover="color:#d0cfd4"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
>
<path d="M16 3l4 4-4 4M4 7h16M8 21l-4-4 4-4M20 17H4"></path>
</svg>
</div>
<div
onClick="{{ prev }}"
style="cursor: pointer; color: #aaaaae"
style-hover="color:#dddce1"
>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M7 6h2v12H7zm3 6l9-6v12z"></path>
</svg>
</div>
<div
onClick="{{ toggle }}"
style="
width: 40px;
height: 40px;
border-radius: 50%;
background: #c6c4cb;
color: #0a0a0d;
display: grid;
place-items: center;
cursor: pointer;
transition: background 0.14s ease;
"
style-hover="background:#d6d5da"
>
<sc-if value="{{ playing }}" hint-placeholder-val="{{ true }}">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"></path>
</svg>
</sc-if>
<sc-if value="{{ paused }}" hint-placeholder-val="{{ false }}">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</sc-if>
</div>
<div
onClick="{{ next }}"
style="cursor: pointer; color: #aaaaae"
style-hover="color:#dddce1"
>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M15 6h2v12h-2zM5 6l9 6-9 6z"></path>
</svg>
</div>
<div
onClick="{{ toggleRepeat }}"
style="cursor:pointer;color:{{ repeatColor }};display:{{ extrasDisplay }}"
style-hover="color:#d0cfd4"
>
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
>
<path
d="M17 2l3 3-3 3M20 5H8a4 4 0 00-4 4v1M7 22l-3-3 3-3M4 19h12a4 4 0 004-4v-1"
></path>
</svg>
</div>
</div>
<div
style="display: flex; align-items: center; gap: 11px; width: 100%; max-width: 540px"
>
<span
style="
font-size: 10.5px;
font-variant-numeric: tabular-nums;
color: #747478;
min-width: 32px;
text-align: right;
"
>{{ elapsed }}</span
>
<div
onClick="{{ seek }}"
style="flex: 1; height: 14px; display: flex; align-items: center; cursor: pointer"
>
<div
style="
width: 100%;
height: 3.5px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.09);
overflow: hidden;
"
>
<div
style="height:100%;border-radius:4px;background:{{ accent }};width:{{ progressPct }}"
></div>
</div>
</div>
<span
style="
font-size: 10.5px;
font-variant-numeric: tabular-nums;
color: #747478;
min-width: 32px;
"
>{{ total }}</span
>
</div>
</div>
<div
style="display:{{ extrasDisplay }};align-items:center;gap:14px;width:{{ playerInfoW }};justify-content:flex-end"
>
<div style="cursor: pointer; color: #747478" style-hover="color:#d0cfd4">
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
>
<path d="M4 6h9M4 12h9M4 18h5M17 10v8"></path>
</svg>
</div>
<div style="display: flex; align-items: center; gap: 9px">
<svg
viewBox="0 0 24 24"
width="15"
height="15"
fill="none"
stroke="#747478"
stroke-width="1.8"
stroke-linecap="round"
>
<path d="M4 9h3l4-3v12l-4-3H4z"></path>
<path d="M16 9a4 4 0 010 6"></path>
</svg>
<div
onClick="{{ setVol }}"
style="
width: 84px;
height: 14px;
display: flex;
align-items: center;
cursor: pointer;
"
>
<div
style="
width: 100%;
height: 3.5px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.09);
overflow: hidden;
"
>
<div
style="height:100%;border-radius:4px;background:#8e8c98;width:{{ volPct }}"
></div>
</div>
</div>
</div>
<div
onClick="{{ toggleQueue }}"
style="cursor:pointer;color:{{ queueBtnColor }}"
style-hover="color:#d0cfd4"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
>
<path d="M3 7h12M3 12h12M3 17h8M19 9v9"></path>
<circle cx="17" cy="18" r="2"></circle>
</svg>
</div>
</div>
</div>
<!-- MOBILE TABS -->
<div
style="display:{{ tabsDisplay }};pointer-events:auto;margin-top:10px;padding:7px;border-radius:18px;border:1px solid {{ hairline }};background:{{ barBg }};backdrop-filter:blur(22px);justify-content:space-around;transition:background .6s ease"
>
<sc-for list="{{ nav }}" as="n" hint-placeholder-count="4">
<div
onClick="{{ n.go }}"
style="display:flex;flex-direction:column;align-items:center;gap:5px;padding:6px 10px;border-radius:12px;cursor:pointer;color:{{ n.color }}"
>
<svg
viewBox="0 0 24 24"
width="17"
height="17"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="{{ n.icon }}"></path>
</svg>
<span style="font-size: 10px; font-weight: 500">{{ n.label }}</span>
</div>
</sc-for>
</div>
</div>
</div>
</x-dc>
<script
type="text/x-dc"
data-dc-script
data-props='{"demoState":{"editor":"enum","options":["normal","loading","error","empty"],"default":"normal","tsType":"string","section":"Estados"}}'
>
const img = (seed, n) => 'https://picsum.photos/seed/' + seed + '/' + (n || 400) + '/' + (n || 400);
const grad = h => 'linear-gradient(150deg,oklch(0.40 0.09 ' + h + '),oklch(0.18 0.04 ' + ((h + 50) % 360) + '))';
const TR = (id, title, artist, album, dur, h, seed, plays) => ({ id, title, artist, album, dur, h, seed, plays });
const TRACKS = [
TR(1, 'Yankee', 'Quevedo', 'DONDE QUIERO ESTAR', '3:11', 42, 'yankee', '96 M'),
TR(2, 'Nights', 'Frank Ocean', 'Blonde', '5:07', 225, 'blonde', '2,4 M'),
TR(3, 'Redbone', 'Childish Gambino', 'Awaken, My Love!', '5:27', 320, 'awaken', '3,1 M'),
TR(4, 'Kyoto', 'Phoebe Bridgers', 'Punisher', '3:04', 140, 'punisher', '742 K'),
TR(5, 'Glue', 'Bicep', 'Bicep', '5:40', 265, 'bicep', '980 K'),
TR(6, 'Alright', 'Kendrick Lamar', 'To Pimp a Butterfly', '3:39', 15, 'tpab', '4,2 M'),
TR(7, 'Time Alone With You', 'Jacob Collier', 'Djesse Vol. 3', '4:46', 180, 'djesse', '412 K'),
TR(8, 'Weird Fishes / Arpeggi', 'Radiohead', 'In Rainbows', '5:18', 95, 'rainbows', '1,3 M'),
TR(9, 'Instant Crush', 'Daft Punk', 'Random Access Memories', '5:37', 290, 'ram', '2,9 M'),
TR(10, 'Supercut', 'Lorde', 'Melodrama', '4:38', 55, 'melodrama', '1,1 M'),
TR(11, 'Sun Bleached Flies', 'Big Thief', null, '5:00', 200, 'bigthief', '298 K'),
TR(12, 'August 10', 'Khruangbin', 'Mordechai', '3:58', 345, 'mordechai', '536 K')
];
const ALBUMS = [
['Currents', 'Tame Impala · 2015', 288, 'currents'],
['Blonde', 'Frank Ocean · 2016', 32, 'blonde'],
['Punisher', 'Phoebe Bridgers · 2020', 205, 'punisher'],
['In Rainbows', 'Radiohead · 2007', 18, 'rainbows'],
['Melodrama', 'Lorde · 2017', 340, 'melodrama'],
['Isles', 'Bicep · 2021', 232, 'isles'],
['Mordechai', 'Khruangbin · 2020', 168, 'mordechai'],
['Random Access Memories', 'Daft Punk · 2013', 260, 'ram']
];
const MIXES = [
['Mix Quevedo', 'Bad Gyal, Saiko, Mora', 42, 'yankee'],
['Mix Frank Ocean', 'Blood Orange, Steve Lacy', 32, 'blonde'],
['Indie de tarde', 'Big Thief, Alvvays, Adrianne Lenker', 142, 'punisher'],
['Mix electrónico', 'Bicep, Four Tet, Jamie xx', 232, 'isles'],
['Mix Khruangbin', 'Men I Trust, Mild High Club', 168, 'mordechai'],
['Clásicos que repites', 'Radiohead, Daft Punk, Lorde', 18, 'rainbows'],
['Concentración', 'Nala Sinephro, Floating Points', 262, 'd-space'],
['Soul de medianoche', 'Sault, Michael Kiwanuka', 58, 'awaken']
];
const ARTISTS = [
['Tame Impala', '18,2 M oyentes', 288, 'a-tame'],
['Frank Ocean', '22,6 M oyentes', 32, 'a-frank'],
['Phoebe Bridgers', '9,4 M oyentes', 205, 'a-phoebe'],
['Bicep', '4,1 M oyentes', 232, 'a-bicep'],
['Khruangbin', '6,8 M oyentes', 168, 'a-khruangbin'],
['Little Dragon', '2,3 M oyentes', 120, 'a-littledragon'],
['Jamie xx', '5,5 M oyentes', 60, 'a-jamiexx']
];
const PLAYLISTS = [
['Favorites', 214, 'Frank Ocean, Tame Impala, Lorde', 32, 'pl-fav'],
['Workout', 48, 'Kendrick Lamar, Bicep, Justice', 96, 'pl-workout'],
['Chill', 96, 'Khruangbin, Men I Trust, Homeshake', 168, 'pl-chill'],
['Late Night', 72, 'Burial, Bicep, Four Tet', 250, 'pl-latenight'],
['Driving', 63, 'The War on Drugs, Kavinsky', 42, 'pl-driving'],
['My Playlist #1', 31, 'Radiohead, Big Thief, Alvvays', 200, 'pl-mine'],
['Discover', 40, 'Sudan Archives, Yaeji, Nala Sinephro', 300, 'pl-discover']
];
const DISCOVER = [
['Natural Wonder Beauty Concept', 'DJ Python & Ana Roxanne', 190, 'd-nwbc'],
['Sundial', 'Noname · 2023', 72, 'd-sundial'],
['Space 1.8', 'Nala Sinephro', 262, 'd-space'],
['Natural Brown Prom Queen', 'Sudan Archives', 20, 'd-sudan'],
['Promises', 'Floating Points & Pharoah Sanders', 130, 'd-promises'],
['Cavalcade', 'Black Country, New Road', 340, 'd-cavalcade']
];
const GENRES = [
['Electrónica de madrugada', '412 playlists', 250, 'g-elec'],
['Jazz contemporáneo', '98 playlists', 44, 'g-jazz'],
['Indie latino', '1,1 K pistas', 18, 'g-latino'],
['Shoegaze y dream pop', '330 pistas', 300, 'g-shoegaze'],
['Instrumental para concentrarse', '86 mixes', 172, 'g-focus'],
['Soul y neo-soul', '520 pistas', 58, 'g-soul'],
['Piano moderno', '64 playlists', 214, 'g-piano'],
['Bass y club', '740 pistas', 280, 'g-club']
];
class Component extends DCLogic {
state = {
view: 'home', playing: true, idx: 0, pos: 78, vol: 0.72,
queueOpen: true, hover: null, query: '', liked: { 2: true, 4: true },
shuffle: false, repeat: false, w: 1400, listCtx: null, loadingOnce: true,
tone: null
};
sampleCover(seed) {
const im = new Image();
im.crossOrigin = 'anonymous';
im.onload = () => {
try {
const c = document.createElement('canvas');
c.width = 12; c.height = 12;
const x = c.getContext('2d');
x.drawImage(im, 0, 0, 12, 12);
const d = x.getImageData(0, 0, 12, 12).data;
let r = 0, g = 0, b = 0, n = 0;
for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2]; n++; }
this.setState({ tone: [Math.round(r / n), Math.round(g / n), Math.round(b / n)], toneSeed: seed });
} catch (e) { this.setState({ tone: null }); }
};
im.onerror = () => this.setState({ tone: null });
im.src = img(seed, 48) + '?cors=1';
}
componentDidMount() {
this.onResize = () => this.setState({ w: window.innerWidth });
this.onResize();
window.addEventListener('resize', this.onResize);
this.tick = setInterval(() => {
if (!this.state.playing) return;
const len = this.durSec(TRACKS[this.state.idx].dur);
this.setState(s => ({ pos: s.pos >= len ? 0 : s.pos + 1 }));
}, 1000);
this.boot = setTimeout(() => this.setState({ loadingOnce: false }), 900);
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
clearInterval(this.tick); clearTimeout(this.boot);
}
greeting() {
const name = 'Ariadna';
const d = new Date(), hh = d.getHours(), wd = d.getDay();
const days = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const day = days[wd];
const clock = d.getHours() + ':' + (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
let line;
if (hh < 5) line = '¿Sesión de madrugada, ' + name + '?';
else if (hh < 13) line = 'Buenos días, ' + name;
else if (hh < 20) line = wd === 5 ? 'Feliz viernes, ' + name : 'Buenas tardes, ' + name;
else line = 'Buenas noches, ' + name;
if (wd === 6 || wd === 0) line = hh < 5 ? '¿Madrugada de finde, ' + name + '?' : 'Que tengas buen finde, ' + name;
else if (wd === 3 && hh >= 5 && hh < 20) line = 'Feliz miércoles, ' + name;
return { line, eyebrow: day.charAt(0).toUpperCase() + day.slice(1) + ' · ' + clock };
}
durSec(d) { const p = String(d).split(':'); return (+p[0]) * 60 + (+p[1] || 0); }
mmss(s) { const m = Math.floor(s / 60), r = Math.floor(s % 60); return m + ':' + (r < 10 ? '0' : '') + r; }
hov(k) { return this.state.hover === k; }
hoverProps(k) {
return { enter: () => this.setState({ hover: k }), leave: () => this.setState({ hover: null }) };
}
card(title, sub, h, seed, round) {
const hv = this.hov('c' + seed);
return Object.assign({
title, sub: sub || 'Sin información', img: img(seed), fallback: grad(h),
cardBg: hv ? 'rgba(255,255,255,.035)' : 'transparent',
lift: 'none',
btnOpacity: hv ? 1 : 0, btnLift: hv ? 'translateY(0)' : 'translateY(6px)',
play: () => this.setState({ playing: true })
}, this.hoverProps('c' + seed));
}
trackRow(t, i, keyPrefix, withPlays) {
const cur = TRACKS[this.state.idx];
const isCur = t.id === cur.id;
const k = keyPrefix + t.id;
const hv = this.hov(k);
return Object.assign({
n: i + 1, title: t.title, artist: t.artist, album: t.album || '—', dur: t.dur,
plays: withPlays ? t.plays : '', img: img(t.seed), fallback: grad(t.h),
isCurrent: isCur, showIndex: !isCur,
titleColor: isCur ? (this.accTitle || '#bfaef5') : '#cbcacf',
rowBg: isCur ? (this.accSoft || 'rgba(156,134,240,.07)') : hv ? 'rgba(255,255,255,.032)' : 'transparent',
likeOpacity: this.state.liked[t.id] || hv ? 1 : 0,
likeColor: this.state.liked[t.id] ? (this.acc || '#9c86f0') : '#919195',
likeFill: this.state.liked[t.id] ? (this.acc || '#9c86f0') : 'none',
like: e => { if (e) e.stopPropagation(); this.setState(st => ({ liked: Object.assign({}, st.liked, { [t.id]: !st.liked[t.id] }) })); },
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
}, this.hoverProps(k));
}
renderVals() {
const s = this.state;
const mob = s.w < 760, tab = s.w < 1180;
const cur = TRACKS[s.idx];
const demo = this.props.demoState || 'normal';
const loading = demo === 'loading' || s.loadingOnce;
const error = demo === 'error' && !loading;
const emptyForced = demo === 'empty';
const q = s.query.trim().toLowerCase();
const searching = q.length > 0;
const hits = TRACKS.filter(t => (t.title + ' ' + t.artist + ' ' + (t.album || '')).toLowerCase().includes(q));
const view = s.view;
const t = null;
let hue = cur.h;
if (t) {
const r = t[0] / 255, g = t[1] / 255, b = t[2] / 255;
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d0 = mx - mn;
if (d0 > 0.09) {
let hh;
if (mx === r) hh = ((g - b) / d0) % 6;
else if (mx === g) hh = (b - r) / d0 + 2;
else hh = (r - g) / d0 + 4;
hue = (hh * 60 + 360) % 360;
}
}
this.acc = 'oklch(0.74 0.145 ' + hue + ')';
this.accSoft = 'oklch(0.74 0.145 ' + hue + ' / .08)';
this.accLine = 'oklch(0.74 0.145 ' + hue + ' / .2)';
this.accTitle = 'oklch(0.87 0.07 ' + hue + ')';
this.accMuted = 'oklch(0.79 0.035 ' + hue + ')';
this.accHair = 'oklch(0.74 0.145 ' + hue + ' / .09)';
const surf = (l, c, a) => 'oklch(' + l + ' ' + c + ' ' + hue + (a === undefined ? '' : ' / ' + a) + ')';
const isEmptyView = emptyForced && (view === 'playlists' || view === 'liked');
const showList = !loading && !error && !searching && !isEmptyView && (view === 'liked' || view === 'recent' || view === 'playlist');
const showSearchList = !loading && !error && searching && hits.length > 0;
const navDef = [
['home', 'Inicio', 'M3 11l9-7 9 7v8a2 2 0 01-2 2h-4v-6H9v6H5a2 2 0 01-2-2z', ''],
['explore', 'Descubrir', 'M12 3a9 9 0 100 18 9 9 0 000-18zm3.5 5.5l-2 5-5 2 2-5z', ''],
['playlists', 'Playlists', 'M4 6h10M4 11h10M4 16h6M18 9v8', '7'],
['liked', 'Me gusta', 'M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z', '312'],
['recent', 'Reciente', 'M12 7v5l3 2M12 3a9 9 0 100 18 9 9 0 000-18z', '']
];
const nav = navDef.map(([id, label, icon, count]) => ({
label, icon, count,
color: view === id ? '#eae8f1' : '#8b8b8f',
bg: view === id ? 'rgba(255,255,255,.06)' : 'transparent',
go: () => this.setState({ view: id, query: '' })
}));
const playingPl = 'Late Night';
const myPlaylists = PLAYLISTS.map(([name, n, artistsLine, h, seed]) => {
const k = 'pl' + seed, hv = this.hov(k);
const active = view === 'playlist' && s.listCtx === name;
return Object.assign({
name, meta: n + ' canciones', artistsLine, img: img(seed), fallback: grad(h),
isPlaying: name === playingPl && s.playing,
bg: active ? 'rgba(255,255,255,.055)' : 'transparent',
titleColor: active || name === playingPl ? this.accTitle : '#cbcacf',
cardBg: hv ? 'rgba(255,255,255,.04)' : 'rgba(255,255,255,.02)',
border: hv ? 'rgba(255,255,255,.08)' : 'rgba(255,255,255,.045)',
lift: 'none',
btnOpacity: hv ? 1 : 0,
open: () => this.setState({ view: 'playlist', listCtx: name, query: '' })
}, this.hoverProps(k));
});
const continueRow = TRACKS.slice(0, 6).map(t => {
const k = 'cont' + t.id, hv = this.hov(k);
const isCur = t.id === cur.id;
return Object.assign({
title: t.title, sub: t.album ? t.artist + ' · ' + t.album : t.artist,
img: img(t.seed), fallback: grad(t.h),
titleColor: isCur ? this.accMuted : '#cdccd1',
bg: isCur ? this.accSoft : hv ? 'rgba(255,255,255,.045)' : 'rgba(255,255,255,.022)',
border: isCur ? this.accHair : hv ? 'rgba(255,255,255,.075)' : 'rgba(255,255,255,.04)',
btnOpacity: hv ? 1 : 0,
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
}, this.hoverProps(k));
});
const spotTracks = [TRACKS[4], TRACKS[8], TRACKS[11], TRACKS[6], TRACKS[9]];
const listName = s.listCtx || 'Favorites';
const listPl = PLAYLISTS.find(p => p[0] === listName) || PLAYLISTS[0];
const listSrc = showSearchList ? hits : (view === 'liked' ? TRACKS.slice(0, 9) : view === 'recent' ? TRACKS.slice(2, 11) : TRACKS.slice(0, 8));
const emptyCopy = view === 'playlists'
? ['Aún no has creado ninguna playlist', 'Crea una desde cero o deja que Musify la construya a partir de un artista, un año o un estado de ánimo.', 'Crear playlist']
: ['Nada con me gusta todavía', 'Pulsa el corazón en cualquier canción y aparecerá aquí, disponible sin conexión.', 'Descubrir música'];
const tc = (a, l) => 'oklch(' + (0.34 - l / 380).toFixed(3) + ' ' + (0.11 - l / 1600).toFixed(3) + ' ' + hue + ' / ' + a + ')';
return {
ambient: 'radial-gradient(52% 38% at 14% 0%,' + tc(.26, 30) + ',transparent 54%),radial-gradient(46% 34% at 78% 0%,' + tc(.2, 40) + ',transparent 56%)',
heroBg: 'linear-gradient(105deg,' + tc(.34, 42) + ' 0%,' + tc(.11, 70) + ' 28%,' + tc(.03, 88) + ' 50%,rgba(6,6,9,0) 70%)',
heroEyebrow: this.greeting().eyebrow,
heroGreeting: this.greeting().line,
heroBlurb: 'Cinco selecciones nuevas construidas a partir de las 214 canciones que escuchaste esta semana.',
heroResume: 'Late Night',
heroStats: [
{ v: '214', k: 'Canciones esta semana' },
{ v: '18 h', k: 'Tiempo de escucha' },
{ v: '9', k: 'Artistas nuevos' }
],
sidebarDisplay: tab ? 'none' : 'flex',
queueW: tab ? '258px' : '304px',
queueOpen: s.queueOpen && !mob,
queueSource: playingPl,
queueBtnColor: s.queueOpen ? '#d6d5da' : '#8b8b8f',
queueBtnBg: s.queueOpen ? 'rgba(255,255,255,.06)' : 'rgba(255,255,255,.028)',
extrasDisplay: mob ? 'none' : 'flex',
tabsDisplay: tab ? 'flex' : 'none',
playerInfoW: mob ? 'auto' : '256px',
bottomPad: tab ? '206px' : '126px',
nav, myPlaylists, continueRow,
albums: MIXES.map(([t, sub, h, seed]) => this.card(t, sub, h, seed)),
artists: ARTISTS.map(([t, sub, h, seed]) => this.card(t, sub, h, seed, true)),
discover: DISCOVER.map(([t, sub, h, seed]) => this.card(t, sub, h, seed)),
popular: TRACKS.slice(0, 8).map((t, i) => this.trackRow(t, i, 'pop', true)),
genres: GENRES.map(([name, meta, h, seed]) => ({
name, meta, img: img(seed, 260),
bg: 'linear-gradient(140deg,oklch(0.24 0.07 ' + h + '),rgba(9,9,12,.95))'
})),
spotlightBg: 'linear-gradient(100deg,' + tc(.28, 46) + ' 0%,' + tc(.1, 74) + ' 30%,' + tc(.03, 88) + ' 52%,rgba(6,6,9,0) 72%)',
spotlight: {
name: 'Late Night', meta: '72 canciones · 4 h 51 min · privada',
img: img('pl-latenight'),
blurb: 'Tu playlist más escuchada de madrugada. Añadiste 6 canciones esta semana, casi todas después de la una.',
play: () => this.setState({ view: 'playlist', listCtx: 'Late Night', playing: true })
},
spotlightTracks: spotTracks.map((t, i) => this.trackRow(t, i, 'spot', false)),
isLoading: loading, isError: error,
showHome: !loading && !error && !searching && view === 'home',
showExplore: !loading && !error && !searching && view === 'explore',
showList: showList || showSearchList,
showPlaylistIndex: !loading && !error && !searching && view === 'playlists' && !isEmptyView,
showEmpty: !loading && !error && !searching && isEmptyView,
showNoResults: !loading && !error && searching && hits.length === 0,
skeletonRows: [1, 2], skeletonCards: [1, 2, 3, 4, 5, 6],
emptyTitle: emptyCopy[0], emptyBody: emptyCopy[1], emptyCta: emptyCopy[2],
suggestions: ['Frank Ocean', 'Currents', 'Late Night', 'Bicep'].map(label => ({ label, go: () => this.setState({ query: label }) })),
listTracks: listSrc.map((t, i) => this.trackRow(t, i, 'list', false)),
listTitle: showSearchList ? 'Resultados para “' + s.query + '”' : view === 'liked' ? 'Canciones que te gustan' : view === 'recent' ? 'Escuchado recientemente' : listName,
listKind: showSearchList ? 'Búsqueda' : view === 'recent' ? 'Historial' : view === 'liked' ? 'Colección' : 'Playlist',
listMeta: showSearchList
? hits.length + ' coincidencias en canciones, álbumes y artistas'
: view === 'liked' ? '312 canciones · 21 h 40 min'
: view === 'recent' ? 'Últimos 7 días · 148 reproducciones'
: listPl[1] + ' canciones · ' + listPl[2],
listImg: showSearchList || view === 'recent' ? img(cur.seed) : view === 'liked' ? img('pl-fav') : img(listPl[4]),
listFallback: grad(view === 'liked' ? 32 : listPl[3]),
query: s.query,
onQuery: e => this.setState({ query: e.target.value }),
curTitle: cur.title,
curArtist: cur.album ? cur.artist + ' · ' + cur.album : cur.artist,
curImg: img(cur.seed), curFallback: grad(cur.h),
pageBg: surf(0.068, 0.008),
sidebarBg: 'linear-gradient(180deg,' + surf(0.095, 0.012) + ',' + surf(0.068, 0.007) + ')',
panelBg: surf(0.09, 0.01, .28),
barBg: surf(0.105, 0.012, .9),
hairline: surf(0.62, 0.03, .08),
accent: this.acc, accentSoft: this.accSoft, accentLine: this.accLine, accentTitle: this.accTitle,
accentGrad: 'linear-gradient(90deg,' + this.accTitle + ',' + this.acc + ')',
logoGrad: 'linear-gradient(140deg,oklch(0.78 0.15 ' + hue + '),oklch(0.62 0.13 ' + ((hue + 40) % 360) + ') 92%)',
logoGlow: '0 0 20px oklch(0.74 0.145 ' + hue + ' / .3)',
accentBtnBg: 'oklch(0.74 0.145 ' + hue + ' / .15)',
accentBtnBgHover: 'oklch(0.74 0.145 ' + hue + ' / .24)',
curLikeColor: s.liked[cur.id] ? this.acc : '#818185',
curLikeFill: s.liked[cur.id] ? this.acc : 'none',
playing: s.playing, paused: !s.playing,
elapsed: this.mmss(s.pos), total: cur.dur,
progressPct: Math.min(100, (s.pos / this.durSec(cur.dur)) * 100) + '%',
volPct: (s.vol * 100) + '%',
shuffleColor: s.shuffle ? this.acc : '#747478',
repeatColor: s.repeat ? this.acc : '#747478',
queue: TRACKS.slice(1, 9).map(t => ({
title: t.title, artist: t.artist, dur: t.dur, img: img(t.seed), fallback: grad(t.h),
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
})),
toggle: () => this.setState(st => ({ playing: !st.playing })),
next: () => this.setState(st => ({ idx: (st.idx + 1) % TRACKS.length, pos: 0, playing: true })),
prev: () => this.setState(st => ({ idx: (st.idx + TRACKS.length - 1) % TRACKS.length, pos: 0, playing: true })),
toggleShuffle: () => this.setState(st => ({ shuffle: !st.shuffle })),
toggleRepeat: () => this.setState(st => ({ repeat: !st.repeat })),
toggleQueue: () => this.setState(st => ({ queueOpen: !st.queueOpen })),
likeCurrent: () => this.setState(st => ({ liked: Object.assign({}, st.liked, { [cur.id]: !st.liked[cur.id] }) })),
seek: e => { const r = e.currentTarget.getBoundingClientRect(); this.setState({ pos: Math.round(((e.clientX - r.left) / r.width) * this.durSec(cur.dur)) }); },
setVol: e => { const r = e.currentTarget.getBoundingClientRect(); this.setState({ vol: Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) }); },
playHero: () => this.setState({ playing: true }),
retry: () => this.setState({ loadingOnce: true }, () => setTimeout(() => this.setState({ loadingOnce: false }), 800)),
back: () => this.setState({ view: 'home', query: '' }),
goHome: () => this.setState({ view: 'home', query: '' }),
goExplore: () => this.setState({ view: 'explore', query: '' }),
goRecent: () => this.setState({ view: 'recent', query: '' }),
goPlaylists: () => this.setState({ view: 'playlists', query: '' })
};
}
}
</script>
</body>
</html>