Files
Musify/web-player/design_handoff_musify_player/Musify.dc.html
T
Admin 7861600081 Redesign explore search and inline top-bar search box
- Explore: replace per-kind colored badges/tints in search results
  with the app's neutral accent, add an always-visible Usuarios chip,
  and keep the top-result type label as plain text
- Move SearchBox into TopBar/MobileHeader as an inline debounced
  search form, dropping the standalone SearchBox component
- Sync design handoff artboards with the current search UI
2026-09-15 03:35:05 +02:00

1128 lines
82 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,.07);border-radius:8px}
::-webkit-scrollbar-track{background:transparent}
@keyframes eq{0%,100%{transform:scaleY(.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,.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:-.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,.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:.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,.035);border:1px solid rgba(255,255,255,.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,.035);border:1px solid rgba(255,255,255,.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,.035);border:1px solid rgba(255,255,255,.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,.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:.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.05);margin:14px 0 8px"></div>
<div style="width:42%;height:9px;border-radius:5px;background:rgba(255,255,255,.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 .3s ease">
<div style="width:52px;height:52px;margin:0 auto 22px;border-radius:15px;background:rgba(226,130,130,.08);border:1px solid rgba(226,130,130,.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:-.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,.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:-.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,.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,.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:-.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:-.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,.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:.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:-.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,.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,.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:-.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,.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:-.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:-.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:-.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:-.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:.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 .3s ease">
<h1 style="font-family:'Sora',sans-serif;font-size:28px;font-weight:600;letter-spacing:-.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:-.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,.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:-.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 .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:.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:-.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,.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,.05);font-size:10.5px;letter-spacing:.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 .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:-.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,.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,.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 .3s ease">
<div style="width:70px;height:70px;margin:0 auto 24px;border-radius:19px;background:linear-gradient(150deg,rgba(156,134,240,.10),rgba(79,191,174,.05));border:1px solid rgba(255,255,255,.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:-.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,.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>
<!-- SEARCH RESULTS -->
<sc-if value="{{ showSearch }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="Search results" style="animation:rise .3s ease;padding-bottom:10px">
<div style="margin:12px 0 18px">
<div style="font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:#8a8a8e;font-weight:600;margin-bottom:11px">Resultados de búsqueda</div>
<h1 style="font-family:'Sora',sans-serif;font-size:26px;font-weight:600;letter-spacing:-.03em;margin:0 0 7px;color:#d9d8dc;text-wrap:pretty">{{ searchHeading }}</h1>
<div style="font-size:12.5px;color:#8a8a8e">{{ searchMeta }}</div>
</div>
<div style="display:flex;gap:7px;flex-wrap:wrap;margin-bottom:26px">
<sc-for list="{{ searchChips }}" as="c" hint-placeholder-count="5">
<div onClick="{{ c.pick }}" style="display:flex;align-items:center;gap:7px;padding:7px 14px;border-radius:999px;font-size:12px;font-weight:500;cursor:pointer;background:{{ c.bg }};color:{{ c.fg }};border:1px solid {{ c.bd }};transition:background .16s ease,color .16s ease">
<span>{{ c.label }}</span>
<span style="font-size:10.5px;font-variant-numeric:tabular-nums;color:{{ c.numFg }}">{{ c.count }}</span>
</div>
</sc-for>
</div>
<sc-if value="{{ hasTop }}" hint-placeholder-val="{{ true }}">
<div onMouseEnter="{{ top.enter }}" onMouseLeave="{{ top.leave }}" onClick="{{ top.open }}" style="display:flex;align-items:center;gap:20px;padding:18px;border-radius:18px;cursor:pointer;margin-bottom:34px;border:1px solid rgba(255,255,255,.06);background:{{ top.wrapBg }};transition:background .2s ease">
<div style="width:88px;height:88px;flex:0 0 auto;overflow:hidden;border-radius:{{ top.radius }};background:{{ top.fallback }};box-shadow:0 14px 34px rgba(0,0,0,.5)">
<div style="width:100%;height:100%;background-image:url({{ top.img }});background-size:cover;background-position:center"></div>
</div>
<div style="min-width:0;flex:1">
<div style="font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:{{ top.tint }};margin-bottom:8px">Mejor resultado · {{ top.kind }}</div>
<div style="font-family:'Sora',sans-serif;font-size:22px;font-weight:600;letter-spacing:-.03em;color:#dcdbe0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ top.title }}</div>
<div style="font-size:12.5px;color:#84848a;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ top.sub }}</div>
</div>
<div style="flex:0 0 auto;width:46px;height:46px;border-radius:50%;display:grid;place-items:center;background:#c6c4cb;color:#0a0a0d;opacity:{{ top.btnOpacity }};transition:opacity .18s ease">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>
</div>
</div>
</sc-if>
<sc-for list="{{ searchGroups }}" as="g" hint-placeholder-count="3">
<div style="margin-bottom:30px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
<div style="font-family:'Sora',sans-serif;font-size:14.5px;font-weight:600;letter-spacing:-.015em;color:#cfccd9">{{ g.label }}</div>
<div style="height:1px;flex:1;background:rgba(255,255,255,.06)"></div>
<div style="font-size:11px;color:#6e6e74;font-variant-numeric:tabular-nums">{{ g.count }}</div>
</div>
<sc-for list="{{ g.items }}" as="r" hint-placeholder-count="4">
<div onMouseEnter="{{ r.enter }}" onMouseLeave="{{ r.leave }}" onClick="{{ r.open }}" style="display:grid;grid-template-columns:52px 1fr auto;gap:15px;align-items:center;padding:9px 12px;border-radius:12px;cursor:pointer;background:{{ r.rowBg }};border-bottom:1px solid rgba(255,255,255,.035);transition:background .15s ease">
<div style="position:relative;width:52px;height:52px;overflow:hidden;border-radius:{{ r.radius }};background:{{ r.fallback }}">
<div style="width:100%;height:100%;background-image:url({{ r.img }});background-size:cover;background-position:center;opacity:.92"></div>
<div style="position:absolute;inset:0;display:grid;place-items:center;background:rgba(6,6,9,.55);color:#e2e1e6;opacity:{{ r.btnOpacity }};transition:opacity .15s ease">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>
</div>
</div>
<div style="min-width:0">
<div style="display:flex;align-items:center;gap:9px;min-width:0">
<div style="font-size:13.5px;font-weight:500;color:{{ r.titleColor }};white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.title }}</div>
<div style="flex:0 0 auto;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;padding:3px 7px;border-radius:5px;color:{{ r.tint }};background:{{ r.tintSoft }}">{{ r.badge }}</div>
</div>
<div style="font-size:11.5px;color:#7f7f83;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.sub }}</div>
</div>
<div style="font-size:11.5px;color:#76767c;font-variant-numeric:tabular-nums;text-align:right">{{ r.meta }}</div>
</div>
</sc-for>
</div>
</sc-for>
</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 .3s ease">
<div style="font-family:'Sora',sans-serif;font-size:20px;font-weight:600;letter-spacing:-.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,.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:-.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:.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:.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 .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:-.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 .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,.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,.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="{&quot;demoState&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;normal&quot;,&quot;loading&quot;,&quot;error&quot;,&quot;empty&quot;],&quot;default&quot;:&quot;normal&quot;,&quot;tsType&quot;:&quot;string&quot;,&quot;section&quot;:&quot;Estados&quot;}}">
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, sfilter: 'Todo'
};
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 albumHits = ALBUMS.filter(a => (a[0] + ' ' + a[1]).toLowerCase().includes(q));
const artistHits = ARTISTS.filter(a => a[0].toLowerCase().includes(q));
const plHits = PLAYLISTS.filter(p => (p[0] + ' ' + p[2]).toLowerCase().includes(q));
const totalHits = searching ? hits.length + albumHits.length + artistHits.length + plHits.length : 0;
const showSearch = !loading && !error && searching && totalHits > 0;
const tint = o => 'oklch(0.80 0.10 ' + ((hue + o + 360) % 360) + ')';
const tintSoft = o => 'oklch(0.80 0.10 ' + ((hue + o + 360) % 360) + ' / .11)';
const sRow = (kind, badge, off, radius, key, title, sub, meta, seed, h, open) => {
const hv = this.hov(key);
return Object.assign({
title, sub, meta, badge, kind, radius,
img: img(seed), fallback: grad(h),
tint: tint(off), tintSoft: tintSoft(off),
titleColor: hv ? '#e0dfe4' : '#cbcacf',
rowBg: hv ? 'rgba(255,255,255,.035)' : 'transparent',
btnOpacity: hv ? 1 : 0,
open
}, this.hoverProps(key));
};
const songRows = hits.map(t => sRow('Canción', 'Canción', 0, '9px', 'sr-t' + t.id, t.title, t.artist + (t.album ? ' · ' + t.album : ''), t.dur, t.seed, t.h,
() => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })));
const albumRows = albumHits.map((a, i) => sRow('Álbum', 'Álbum', 46, '5px', 'sr-a' + i, a[0], a[1], 'Álbum', a[3], a[2],
() => this.setState({ playing: true })));
const artistRows = artistHits.map((a, i) => sRow('Artista', 'Artista', -52, '50%', 'sr-r' + i, a[0], a[1], 'Artista', a[3], a[2],
() => this.setState({ playing: true })));
const plRows = plHits.map((p, i) => sRow('Playlist', 'Playlist', 92, '13px', 'sr-p' + i, p[0], p[2], p[1] + ' canciones', p[4], p[3],
() => this.setState({ view: 'playlist', listCtx: p[0], query: '' })));
const allGroups = [
['Canciones', songRows], ['Álbumes', albumRows],
['Artistas', artistRows], ['Playlists', plRows]
];
const sf = s.sfilter;
const chipMap = { 'Canciones': songRows, 'Álbumes': albumRows, 'Artistas': artistRows, 'Playlists': plRows };
const searchGroups = allGroups
.filter(g => g[1].length && (sf === 'Todo' || sf === g[0]))
.map(g => ({ label: g[0], count: g[1].length + (g[1].length === 1 ? ' resultado' : ' resultados'), items: sf === 'Todo' ? g[1].slice(0, 4) : g[1] }));
const topSrc = [['Artista', artistRows], ['Canción', songRows], ['Álbum', albumRows], ['Playlist', plRows]];
let topItem = null;
for (const [, rows] of topSrc) { const m = rows.find(r => r.title.toLowerCase().startsWith(q)); if (m) { topItem = m; break; } }
if (!topItem) { const g = topSrc.find(x => x[1].length); topItem = g ? g[1][0] : null; }
const topVal = topItem ? Object.assign({}, topItem, {
wrapBg: this.hov(topItem.kind === 'x' ? '' : 'top') ? 'rgba(255,255,255,.05)' : 'rgba(255,255,255,.025)',
btnOpacity: this.hov('top') ? 1 : 0.55
}, this.hoverProps('top')) : {};
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 = (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,
showSearch,
searchHeading: 'Resultados para «' + s.query.trim() + '»',
searchMeta: totalHits + (totalHits === 1 ? ' coincidencia' : ' coincidencias') + ' en canciones, álbumes, artistas y playlists',
searchChips: ['Todo', 'Canciones', 'Álbumes', 'Artistas', 'Playlists'].map(label => {
const on = sf === label;
return {
label,
count: label === 'Todo' ? totalHits : (chipMap[label] || []).length,
bg: on ? '#cfccd9' : 'rgba(255,255,255,.03)',
fg: on ? '#0a0a0d' : '#9b9b9f',
numFg: on ? 'rgba(10,10,13,.55)' : '#65656a',
bd: on ? 'transparent' : 'rgba(255,255,255,.07)',
pick: () => this.setState({ sfilter: label })
};
}),
searchGroups,
hasTop: showSearch && !!topItem && sf === 'Todo',
top: topVal,
showPlaylistIndex: !loading && !error && !searching && view === 'playlists' && !isEmptyView,
showEmpty: !loading && !error && !searching && isEmptyView,
showNoResults: !loading && !error && searching && totalHits === 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: view === 'liked' ? 'Canciones que te gustan' : view === 'recent' ? 'Escuchado recientemente' : listName,
listKind: view === 'recent' ? 'Historial' : view === 'liked' ? 'Colección' : 'Playlist',
listMeta: view === 'liked' ? '312 canciones · 21 h 40 min'
: view === 'recent' ? 'Últimos 7 días · 148 reproducciones'
: listPl[1] + ' canciones · ' + listPl[2],
listImg: 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>