diff --git a/web-player/design_handoff_musify_player/Crear playlist.dc.html b/web-player/design_handoff_musify_player/Crear playlist.dc.html new file mode 100644 index 0000000..43fa0e7 --- /dev/null +++ b/web-player/design_handoff_musify_player/Crear playlist.dc.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + +
+
+
Tu biblioteca
+
Crear playlist
+
+
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+
+
+
Subir imagen
+
+ + +
+ + +
{{ counter }}
+ + + +
+
+ + +
+
+
+
Visibilidad
+
{{ visHint }}
+
+
+ +
{{ v.label }}
+
+
+
+ +
+
+
Colaborativa
+
Quien tenga el enlace puede añadir canciones
+
+
+
+
+
+
+ + +
+
Podrás añadir canciones desde cualquier pantalla.
+
+
Cancelar
+
+ + Crear playlist +
+
+
+
+ + +
+
+
Playlist «{{ createdName }}» creada
+
+
+
+ +
+ + + diff --git a/web-player/design_handoff_musify_player/Musify.dc.html b/web-player/design_handoff_musify_player/Musify.dc.html index 6acb0f6..e7bf466 100644 --- a/web-player/design_handoff_musify_player/Musify.dc.html +++ b/web-player/design_handoff_musify_player/Musify.dc.html @@ -1,2801 +1,1127 @@ - + - - - - - - - - - - - + + + + + + + + + + + -
-
-
+
-
- - -
+ +
-
- - - - Cola -
-
- -
- +
+
+
+ +
+
+ +
+
-
- - -
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
+
+ + +
- - -
-
- ! -
-
- No se pudo cargar tu música -
-
- La conexión se interrumpió. Lo que tengas descargado sigue disponible sin - conexión. -
-
-
- Reintentar -
-
- Ver descargas -
-
-
-
+
- - -
- -
-
-
-

- {{ heroGreeting }} -

-
- {{ heroBlurb }} -
-
-
- - - - Reanudar {{ heroResume }} -
-
- Ver resumen semanal -
-
-
-
- -
-
- {{ s.v }} -
-
- {{ s.k }} -
-
-
-
-
-
+
+ + Cola +
+
+ +
+
- -
-
-

- Continuar escuchando -

-
-
- -
-
-
-
-
-
- {{ c.title }} -
-
- {{ c.sub }} -
-
-
- - - -
-
-
-
-
+
- -
-
-
-
-
-
-
-
-
- Playlist destacada -
-
- {{ spotlight.name }} -
-
- {{ spotlight.meta }} -
-
-
-
- {{ spotlight.blurb }} -
-
-
- - Reproducir -
-
- Mezclar -
-
-
-
- -
-
- {{ t.n }} -
-
-
- {{ t.title }} -
-
- {{ t.artist }} -
-
-
- {{ t.dur }} -
-
-
-
-
-
+ + +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
- -
-
-
-

- Mixes -

-
- Generados a partir de lo que más repites -
-
-
-
- -
-
-
-
-
- - - -
-
-
- {{ it.title }} -
-
- {{ it.sub }} -
-
-
-
-
+ + +
+
!
+
No se pudo cargar tu música
+
La conexión se interrumpió. Lo que tengas descargado sigue disponible sin conexión.
+
+
Reintentar
+
Ver descargas
+
+
+
- -
-

- Artistas que sigues -

-
- -
-
-
-
-
- {{ a.title }} -
-
- {{ a.sub }} -
-
-
-
-
+ + +
- -
-
-
-

- Populares esta semana -

-
- Lo más escuchado entre quienes comparten tu gusto -
-
-
-
- -
-
- -
-
-
-
-
-
- - {{ t.n }} - -
-
-
-
-
-
- {{ t.title }} -
-
- {{ t.artist }} -
-
-
- {{ t.plays }} -
-
- {{ t.dur }} -
-
-
-
-
+ +
+
+
+

{{ heroGreeting }}

+
{{ heroBlurb }}
+
+
+ + Reanudar {{ heroResume }} +
+
Ver resumen semanal
+
+
+
+ +
+
{{ s.v }}
+
{{ s.k }}
+
+
+
+
+
- -
-
-
-

- Tus playlists -

-
- Creadas y guardadas por ti -
-
-
- Ver todas -
-
-
- -
-
-
-
-
-
- {{ p.name }} -
-
- {{ p.meta }} -
-
- {{ p.artistsLine }} -
-
-
- - - -
-
-
-
-
-
-
+ +
+
+

Continuar escuchando

+
+
+ +
+
+
+
+
+
{{ c.title }}
+
{{ c.sub }}
+
+
+ +
+
+
+
+
- - -
-

- Descubrir -

-
- Rutas de escucha fuera de tu historial. Se renuevan cada 12 horas. -
-
- -
-
- {{ g.name }} -
-
- {{ g.meta }} -
-
-
-
-
-
-
-
-

- Fuera de tu burbuja -

-
- Álbumes que tu historial nunca te habría servido -
-
- -
-
-
-
- - - -
-
-
- {{ it.title }} -
-
- {{ it.sub }} -
-
-
-
-
-
-
+ +
+
+
+
+
+
+
+
+
Playlist destacada
+
{{ spotlight.name }}
+
{{ spotlight.meta }}
+
+
+
{{ spotlight.blurb }}
+
+
+ Reproducir +
+
Mezclar
+
+
+
+ +
+
{{ t.n }}
+
+
{{ t.title }}
+
{{ t.artist }}
+
+
{{ t.dur }}
+
+
+
+
+
- - -
-
-
-
-
-
-
- {{ listKind }} -
-

- {{ listTitle }} -

-
{{ listMeta }}
-
-
- - Reproducir -
-
- - - -
-
-
-
+ +
+
+
+

Mixes

+
Generados a partir de lo que más repites
+
+
+
+ +
+
+
+
+
+ +
+
+
{{ it.title }}
+
{{ it.sub }}
+
+
+
+
-
-
#
-
Título
-
Álbum
-
Duración
-
- -
-
- -
-
-
-
-
-
- - {{ t.n }} - -
-
-
-
-
-
-
- {{ t.title }} -
-
- {{ t.artist }} -
-
-
-
- {{ t.album }} -
-
-
- - - -
-
- {{ t.dur }} -
-
-
-
-
-
+ +
+

Artistas que sigues

+
+ +
+
+
+
+
{{ a.title }}
+
{{ a.sub }}
+
+
+
+
- - -
-
-
-

- Tus playlists -

-
- 7 playlists · 564 canciones · 38 h 12 min -
-
-
- Nueva playlist -
-
-
- -
-
-
-
-
- - - -
-
-
- {{ p.name }} -
-
- {{ p.meta }} -
-
- {{ p.artistsLine }} -
-
-
-
-
-
+ +
+
+
+

Populares esta semana

+
Lo más escuchado entre quienes comparten tu gusto
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + {{ t.n }} + +
+
+
+
+
+
{{ t.title }}
+
{{ t.artist }}
+
+
{{ t.plays }}
+
{{ t.dur }}
+
+
+
+
- - -
-
- - - - - -
-
- {{ emptyTitle }} -
-
- {{ emptyBody }} -
-
-
- {{ emptyCta }} -
-
- Volver al inicio -
-
-
-
+ +
+
+
+

Tus playlists

+
Creadas y guardadas por ti
+
+
Ver todas
+
+
+ +
+
+
+
+
+
{{ p.name }}
+
{{ p.meta }}
+
{{ p.artistsLine }}
+
+
+ +
+
+
+
+
+
+ - - -
-
- Sin resultados para “{{ query }}” -
-
- Revisa la ortografía o prueba con un término más corto. También puedes buscar - por álbum o por playlist. -
-
- -
- {{ s.label }} -
-
-
-
-
- -
+ + +
+

Descubrir

+
Rutas de escucha fuera de tu historial. Se renuevan cada 12 horas.
+
+ +
+
{{ g.name }}
+
{{ g.meta }}
+
+
+
+
+
+
+
+

Fuera de tu burbuja

+
Álbumes que tu historial nunca te habría servido
+
+ +
+
+
+
+ +
+
+
{{ it.title }}
+
{{ it.sub }}
+
+
+
+
+
+
- - - - -
+ + +
+
+
+
+
+
+
{{ listKind }}
+

{{ listTitle }}

+
{{ listMeta }}
+
+
+ Reproducir +
+
+ +
+
+
+
- -
-
-
-
-
-
-
-
- {{ curTitle }} -
-
- {{ curArtist }} -
-
-
- - - -
-
+
+
#
Título
Álbum
Duración
+
+ +
+
+ +
+
+
+
+
+
+ + {{ t.n }} + +
+
+
+
+
+
+
{{ t.title }}
+
{{ t.artist }}
+
+
+
{{ t.album }}
+
+
+ +
+
{{ t.dur }}
+
+
+
+
+ -
-
-
- - - -
-
- - - -
-
- - - - - - - - - - -
-
- - - -
-
- - - -
-
-
- {{ elapsed }} -
-
-
-
-
- {{ total }} -
-
+ + +
+
+
+

Tus playlists

+
7 playlists · 564 canciones · 38 h 12 min
+
+
Nueva playlist
+
+
+ +
+
+
+
+
+ +
+
+
{{ p.name }}
+
{{ p.meta }}
+
{{ p.artistsLine }}
+
+
+
+
+
-
-
- - - -
-
- - - - -
-
-
-
-
-
-
- - - - -
-
-
+ + +
+
+ +
+
{{ emptyTitle }}
+
{{ emptyBody }}
+
+
{{ emptyCta }}
+
Volver al inicio
+
+
+
- -
- -
- - - - {{ n.label }} -
-
-
-
-
-
- - + 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: '' }) + }; + } +} + + diff --git a/web-player/design_handoff_musify_player/README.md b/web-player/design_handoff_musify_player/README.md index 1591004..2e3de53 100644 --- a/web-player/design_handoff_musify_player/README.md +++ b/web-player/design_handoff_musify_player/README.md @@ -39,11 +39,11 @@ Altura fija de viewport, sin scroll de página; solo el área de contenido y la ### Breakpoints -| Ancho | Comportamiento | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `>= 1180px` | Sidebar visible (254px), cola 304px, player completo | -| `< 1180px` (tablet) | Sidebar oculto → barra de pestañas bajo el player; cola 258px; padding inferior 206px | -| `< 760px` (móvil) | Cola oculta; se ocultan shuffle/repeat/volumen/letra/cola del player (queda carátula + título + transporte + progreso); el bloque de info del player pasa a `width:auto` | +| Ancho | Comportamiento | +|---|---| +| `>= 1180px` | Sidebar visible (254px), cola 304px, player completo | +| `< 1180px` (tablet) | Sidebar oculto → barra de pestañas bajo el player; cola 258px; padding inferior 206px | +| `< 760px` (móvil) | Cola oculta; se ocultan shuffle/repeat/volumen/letra/cola del player (queda carátula + título + transporte + progreso); el bloque de info del player pasa a `width:auto` | --- @@ -84,17 +84,17 @@ Los elementos que cambian de color reciben `transition: background .6s ease` (y ### Grises fijos (no dependen del hue) -| Rol | Hex | -| ------------------------------------------- | ------------------------------------------------------------------------------------ | -| Texto principal (títulos de sección, h1/h2) | `#d3d2d6` – `#d9d8dc` | -| Texto de tarjeta / fila | `#cbcacf` / `#cdccd1` | -| Texto base del documento | `#c9c8cc` | -| Secundario | `#8a8a8e`, `#8f8f93`, `#919195` | -| Terciario | `#7f7f83`, `#7d7d81`, `#848488` | -| Cuaternario / contadores | `#6c6c70`, `#696a6d`, `#646468`, `#616165`, `#5c5c60` | -| Iconos inactivos player | `#747478` | -| Botón primario (fondo) | `#c6c4cb`, hover `#d8d7dc`; texto `#0a0a0d` | -| Superficies neutras | `rgba(255,255,255,.022)` base, `.045` hover; bordes `rgba(255,255,255,.04)` / `.075` | +| Rol | Hex | +|---|---| +| Texto principal (títulos de sección, h1/h2) | `#d3d2d6` – `#d9d8dc` | +| Texto de tarjeta / fila | `#cbcacf` / `#cdccd1` | +| Texto base del documento | `#c9c8cc` | +| Secundario | `#8a8a8e`, `#8f8f93`, `#919195` | +| Terciario | `#7f7f83`, `#7d7d81`, `#848488` | +| Cuaternario / contadores | `#6c6c70`, `#696a6d`, `#646468`, `#616165`, `#5c5c60` | +| Iconos inactivos player | `#747478` | +| Botón primario (fondo) | `#c6c4cb`, hover `#d8d7dc`; texto `#0a0a0d` | +| Superficies neutras | `rgba(255,255,255,.022)` base, `.045` hover; bordes `rgba(255,255,255,.04)` / `.075` | Los grises son **neutros a propósito** (sin tinte), para que solo el acento lleve color. @@ -106,19 +106,19 @@ Los grises son **neutros a propósito** (sin tinte), para que solo el acento lle - UI / texto: **Plus Jakarta Sans** 400/500/600/700. - `-webkit-font-smoothing: antialiased`. -| Uso | Fuente | Tamaño | Peso | Tracking | -| ------------------------------------------ | ------- | ---------------- | ---- | ------------------------------------ | -| H1 hero | Sora | 36px / 1.1 | 600 | -0.035em | -| H1 vista (Descubrir, Playlists, Tracklist) | Sora | 28–33px | 600 | -0.03 / -0.035em | -| H2 sección | Sora | 17.5px | 600 | -0.025em | -| Título spotlight | Sora | 23px | 600 | -0.03em | -| Wordmark | Sora | 17.5px | 600 | -0.02em | -| Subtítulo de sección | Jakarta | 12px | 400 | — | -| Eyebrow / cabecera de columna | Jakarta | 10.5px uppercase | 600 | 0.12–0.17em | -| Título de tarjeta | Jakarta | 13–13.5px | 600 | -0.01em | -| Fila de canción (título) | Jakarta | 12.5–13px | 500 | — | -| Meta / artista | Jakarta | 11–11.5px | 400 | — | -| Números (duración, índice, reproducciones) | Jakarta | 11–12px | 400 | `font-variant-numeric: tabular-nums` | +| Uso | Fuente | Tamaño | Peso | Tracking | +|---|---|---|---|---| +| H1 hero | Sora | 36px / 1.1 | 600 | -0.035em | +| H1 vista (Descubrir, Playlists, Tracklist) | Sora | 28–33px | 600 | -0.03 / -0.035em | +| H2 sección | Sora | 17.5px | 600 | -0.025em | +| Título spotlight | Sora | 23px | 600 | -0.03em | +| Wordmark | Sora | 17.5px | 600 | -0.02em | +| Subtítulo de sección | Jakarta | 12px | 400 | — | +| Eyebrow / cabecera de columna | Jakarta | 10.5px uppercase | 600 | 0.12–0.17em | +| Título de tarjeta | Jakarta | 13–13.5px | 600 | -0.01em | +| Fila de canción (título) | Jakarta | 12.5–13px | 500 | — | +| Meta / artista | Jakarta | 11–11.5px | 400 | — | +| Números (duración, índice, reproducciones) | Jakarta | 11–12px | 400 | `font-variant-numeric: tabular-nums` | Todo texto de una línea en tarjetas y filas usa `white-space:nowrap; overflow:hidden; text-overflow:ellipsis` con `min-width:0` en el contenedor flex. Los titulares llevan `text-wrap: pretty`. @@ -166,8 +166,7 @@ Todo texto de una línea en tarjetas y filas usa `white-space:nowrap; overflow:h Ritmo editorial deliberado — no filas iguales encadenadas: **a) Hero** — `border-radius:24px; margin-bottom:42px; background: heroBg`, sin borde, `padding:38px 36px 34px; max-width:620px`. - -- Saludo H1 dinámico por hora y día (ver _Copy dinámico_). +- Saludo H1 dinámico por hora y día (ver *Copy dinámico*). - Blurb 13.5px `#8a8a8e`, `line-height:1.65`, `max-width:440px`. - Botones: primario "Reproducir" (`#c6c4cb` sobre `#0a0a0d`, 12px 21px, radio 12px, icono play 14px) + secundario "Guardar" (borde `rgba(255,255,255,.11)`), y un meta a la derecha en 12px. - Tres métricas: valor Sora 26px/600 y etiqueta 11.5px — "Canciones esta semana", "Tiempo de escucha", "Artistas nuevos". @@ -175,7 +174,6 @@ Ritmo editorial deliberado — no filas iguales encadenadas: **b) Continuar escuchando** — grid `auto-fill minmax(268px,1fr)`, gap 10px. Fila-tarjeta: `padding:9px 14px 9px 9px; radius:13px`, carátula 46px radio 9px, título 13px/600, sub 11.5px, y un icono play 15px en `accent` que aparece en hover. La pista en curso usa `accentSoft` de fondo, `accentHair` de borde y `accentMuted` en el título. **c) Playlist destacada (spotlight)** — grid de dos columnas `auto-fit minmax(290px,1fr)`, `radius:22px`, `background: spotlightBg`, sin borde ni divisor central. - - Izquierda: portada 88px radio 14px, eyebrow "PLAYLIST DESTACADA", nombre Sora 23px, meta 12px, blurb 13px/1.7, botón "Reproducir" (`accentBtnBg` + `accentLine` + `accentTitle`, hover `accentBtnBgHover`) y "Mezclar" secundario. - Derecha: 5 canciones con índice 20px, título 12.5px, artista 11px, duración tabular; fondo `rgba(0,0,0,.26)` que se desvanece a transparente hacia la derecha. @@ -191,18 +189,54 @@ Ritmo editorial deliberado — no filas iguales encadenadas: H1 28px + sub. Mosaico de géneros `minmax(228px,1fr)` gap 14px: tarjetas de 112px, radio 16px, fondo `linear-gradient(140deg, oklch(0.24 0.07 Hgénero), rgba(9,9,12,.95))`, borde `rgba(255,255,255,.05)`, nombre Sora 15.5px, meta 11.5px, y una miniatura de 80px rotada 20° asomando en la esquina inferior derecha (`opacity:.82`). Debajo, "Fuera de tu burbuja" con la misma tarjeta cuadrada de Mixes. -### 5. Tracklist (Me gusta / Reciente / Playlist / Resultados) +### 5. Tracklist (Me gusta / Reciente / Playlist) -Cabecera: portada 164px radio 18px + eyebrow (`Colección` / `Historial` / `Playlist` / `Búsqueda`), H1 33px, meta 12.5px, botón "Reproducir" y un botón cuadrado 40px de "aleatorio". +Cabecera: portada 164px radio 18px + eyebrow (`Colección` / `Historial` / `Playlist`), H1 33px, meta 12.5px, botón "Reproducir" y un botón cuadrado 40px de "aleatorio". Tabla: `grid-template-columns: 32px 1fr 1fr 92px; gap:16px`. Cabecera 10.5px uppercase `#616165` con borde inferior `rgba(255,255,255,.05)`. Fila `padding:10px 14px; radius:12px`: - - Col 1: número o, si es la pista activa, EQ de 3 barras de 2.5px en `accent`. - Col 2: carátula 40px radio 9px + título 13px/500 + artista 11.5px. - Col 3: álbum (o `—` si falta). - Col 4: corazón (14px; visible siempre si está marcado, si no solo en hover) + duración tabular. - Activa: fondo `accentSoft`, título `accentTitle`. Hover: `rgba(255,255,255,.032)`. +### 5b. Resultados de búsqueda + +**Vista propia, no una tracklist.** La búsqueda devuelve canciones, álbumes, artistas y playlists, así que no lleva portada-hero ni columnas de tabla: es una lista agrupada donde cada tipo se reconoce a simple vista. + +**Cabecera** (`margin:12px 0 18px`): eyebrow `RESULTADOS DE BÚSQUEDA` (10.5px uppercase, tracking .16em, `#8a8a8e`, 600) · H1 Sora 26px/600 tracking -0.03em `#d9d8dc` con el texto `Resultados para «{query}»` · meta 12.5px `#8a8a8e`: `{n} coincidencias en canciones, álbumes, artistas y playlists` (singular "coincidencia" con 1). + +**Chips de filtro** (`display:flex; gap:7px; flex-wrap:wrap; margin-bottom:26px`): `Todo`, `Canciones`, `Álbumes`, `Artistas`, `Playlists`. Cada chip `padding:7px 14px; radius:999px; font-size:12px/500` y lleva a su derecha el contador en 10.5px tabular-nums. +- Activo: fondo `#cfccd9`, texto `#0a0a0d`, contador `rgba(10,10,13,.55)`, sin borde. +- Inactivo: fondo `rgba(255,255,255,.03)`, texto `#9b9b9f`, contador `#65656a`, borde `1px solid rgba(255,255,255,.07)`. +- Transición `background .16s, color .16s`. + +**Mejor resultado** (solo con el filtro `Todo`): fila-tarjeta `display:flex; gap:20px; padding:18px; radius:18px; border:1px solid rgba(255,255,255,.06)`, fondo `rgba(255,255,255,.025)` → `.05` en hover, `margin-bottom:34px`. Miniatura 88px con el radio del tipo y `box-shadow:0 14px 34px rgba(0,0,0,.5)`. Eyebrow `Mejor resultado · {Tipo}` 10.5px uppercase tracking .14em en el **tinte del tipo**; título Sora 22px/600 tracking -0.03em `#dcdbe0`; subtítulo 12.5px `#84848a`. A la derecha, botón circular 46px `#c6c4cb` / icono `#0a0a0d`, `opacity .55` → `1` en hover. +Selección: el primer elemento cuyo nombre **empieza** por la consulta, recorriendo Artista → Canción → Álbum → Playlist; si ninguno empieza por ella, el primer resultado del primer grupo no vacío. + +**Grupos** (`margin-bottom:30px` cada uno). Cabecera de grupo: título Sora 14.5px/600 `#cfccd9` + regla `1px rgba(255,255,255,.06)` que ocupa el espacio libre + contador 11px `#6e6e74` (`{n} resultados`). Con filtro `Todo` cada grupo muestra **máximo 4** elementos; al filtrar por un tipo se muestran todos los de ese tipo. + +**Fila de resultado:** `grid-template-columns: 52px 1fr auto; gap:15px; padding:9px 12px; radius:12px; border-bottom:1px solid rgba(255,255,255,.035)`; fondo transparente → `rgba(255,255,255,.035)` en hover (`.15s ease`), sin elevación. +- Miniatura 52px con degradado de respaldo del hue del elemento y overlay `rgba(6,6,9,.55)` + play 15px `#e2e1e6` que aparece en hover (`opacity 0 → 1`). +- Título 13.5px/500 `#cbcacf` → `#e0dfe4` en hover, seguido del **badge de tipo**: 9.5px uppercase, tracking .1em, 600, `padding:3px 7px; radius:5px`, color del tinte del tipo sobre el mismo tinte al 11%. +- Subtítulo 11.5px `#7f7f83` (una línea, ellipsis). +- Meta a la derecha 11.5px `#76767c` tabular-nums. + +**Diferenciación por tipo** — la forma de la miniatura es la señal primaria; el badge la confirma. El tinte se deriva del hue de la canción en reproducción con un desplazamiento fijo, así que la vista sigue siendo cromáticamente coherente con el resto de la app: + +| Tipo | Radio de miniatura | Tinte | Subtítulo | Meta | +|---|---|---|---|---| +| Canción | `9px` (cuadrado suave) | `oklch(0.80 0.10 H)` | `artista · álbum` | duración | +| Álbum | `5px` (casi recto) | `oklch(0.80 0.10 H+46)` | `artista · año` | `Álbum` | +| Artista | `50%` (círculo) | `oklch(0.80 0.10 H-52)` | oyentes | `Artista` | +| Playlist | `13px` (muy redondeado) | `oklch(0.80 0.10 H+92)` | artistas principales | `{n} canciones` | + +Orden de los grupos: Canciones, Álbumes, Artistas, Playlists. Los grupos vacíos no se renderizan. + +**Coincidencias:** canciones por título + artista + álbum; álbumes por título + `artista · año`; artistas por nombre; playlists por nombre + artistas principales. Todo en minúsculas, `includes`. + +**Acciones:** canción → fija la pista y reproduce; álbum y artista → reproducen; playlist → abre la vista de playlist con su contexto y limpia la búsqueda. + ### 6. Índice de playlists H1 "Tus playlists" + meta agregada, botón "Nueva playlist" secundario, grid `minmax(196px,1fr)` gap 22px de tarjetas cuadradas con nombre, nº de canciones y artistas principales. @@ -210,7 +244,6 @@ H1 "Tus playlists" + meta agregada, botón "Nueva playlist" secundario, grid `mi ### 7. Cola (aside derecho) `border-left:1px solid hairline; background: panelBg; padding:20px 16px` + padding inferior dinámico. - - Cabecera "En cola" (Sora 14.5px) + cerrar "×". - "REPRODUCIENDO": tarjeta con fondo `accentSoft`, borde `accentLine`, carátula 40px, título en `accentTitle`. - Cabecera "A CONTINUACIÓN · {playlist}" con `gap:12px`; la etiqueta trunca con elipsis (`flex:1 1 auto; min-width:0; nowrap`) y "Vaciar" es `flex:0 0 auto`. @@ -236,29 +269,30 @@ Bajo el player, `margin-top:10px; padding:7px; radius:18px`, mismo `barBg` y blu - **Loading (skeleton):** bloque de hero de 260px + 2 secciones de 6 tarjetas. Gradiente `linear-gradient(90deg, rgba(255,255,255,.025), .055, .025)` con `background-size:420px 100%` y `animation: shim 1.25s linear infinite` (`-420px 0` → `420px 0`). Se muestra 900ms al arrancar. - **Error:** cuadro 52px con "!" (`rgba(226,130,130,.08)` / borde `.18` / texto `#e09292`), título 20px, cuerpo 13px, botones "Reintentar" (primario) y "Ver descargas". Copy: "No se pudo cargar tu música" / "La conexión se interrumpió. Lo que tengas descargado sigue disponible sin conexión." Reintentar vuelve al skeleton 800ms. - **Vacío:** icono 70px radio 19px con degradado tenue, título 20px, cuerpo 13px, CTA primario + "Volver al inicio". Dos variantes de copy: playlists sin crear y "me gusta" sin canciones. -- **Sin resultados:** título `Sin resultados para "{query}"`, cuerpo de ayuda y 4 chips de sugerencia (radio 999px, borde `rgba(255,255,255,.08)`, 12px). +- **Sin resultados:** se muestra cuando **ningún** tipo tiene coincidencias (canciones + álbumes + artistas + playlists = 0). Título `Sin resultados para "{query}"`, cuerpo de ayuda y 4 chips de sugerencia (radio 999px, borde `rgba(255,255,255,.08)`, 12px). - **Datos incompletos:** si falta el álbum, la columna muestra `—` y el player omite el `· álbum`; si falta el subtítulo de una tarjeta, "Sin información". Toda carátula tiene un degradado de respaldo detrás de la imagen. --- ## Interacciones -| Elemento | Comportamiento | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| Nav / tabs | Cambia de vista y limpia la búsqueda | -| Playlist (sidebar, tarjeta, índice, spotlight) | Abre la vista de playlist con su contexto | -| Tarjeta / fila de canción | Fija la pista, `pos = 0`, `playing = true` | -| Play/pausa | Alterna reproducción (el reloj avanza 1s por segundo) | -| Siguiente / anterior | Cíclicos sobre el array de pistas, resetean la posición | -| Pista de progreso | Clic → `pos = (x / ancho) * duración` | -| Pista de volumen | Clic → `vol` entre 0 y 1 | -| Corazón (fila o player) | Alterna en el mapa `liked` | -| Shuffle / repeat | Alternan, se tiñen de `accent` | -| Botón "Cola" | Abre/cierra el aside | -| Buscar | Filtra por título, artista y álbum; vacío → vista normal; sin coincidencias → estado "sin resultados" | +| Elemento | Comportamiento | +|---|---| +| Nav / tabs | Cambia de vista y limpia la búsqueda | +| Playlist (sidebar, tarjeta, índice, spotlight) | Abre la vista de playlist con su contexto | +| Tarjeta / fila de canción | Fija la pista, `pos = 0`, `playing = true` | +| Play/pausa | Alterna reproducción (el reloj avanza 1s por segundo) | +| Siguiente / anterior | Cíclicos sobre el array de pistas, resetean la posición | +| Pista de progreso | Clic → `pos = (x / ancho) * duración` | +| Pista de volumen | Clic → `vol` entre 0 y 1 | +| Corazón (fila o player) | Alterna en el mapa `liked` | +| Shuffle / repeat | Alternan, se tiñen de `accent` | +| Botón "Cola" | Abre/cierra el aside | +| Buscar | Filtra canciones, álbumes, artistas y playlists → vista de resultados agrupada; vacío → vuelve a la vista normal; 0 coincidencias en total → estado "sin resultados" | +| Chip de filtro de resultados | Fija `sfilter`; `Todo` recorta cada grupo a 4, un tipo concreto muestra todos | +| Nueva playlist (sidebar / índice) | Abre el modal de crear playlist | **Animación — reglas:** - - **Nada de elevación en hover.** Se eliminó a propósito: el feedback es solo un cambio de fondo y la aparición del botón de play. - Hover de fondo/color: `.15–.18s ease`. Play del player: `.14s` solo de color, sin `scale`. - Cambio de tinte por canción: `.6s ease`. @@ -278,6 +312,7 @@ pos segundos transcurridos (intervalo de 1s, vuelve a 0 al llegar a la d vol 0..1 queueOpen boolean (forzado a false por debajo de 760px) query texto de búsqueda +sfilter 'Todo' | 'Canciones' | 'Álbumes' | 'Artistas' | 'Playlists' (filtro de resultados) liked { [trackId]: true } shuffle / repeat boolean hover clave del elemento con hover (o null) @@ -296,6 +331,43 @@ Todos los campos opcionales deben tener degradado o texto de respaldo. El `hue` --- +## Modal: crear playlist + +Diseño aparte en `Crear playlist.dc.html`. Se invoca desde "Nueva playlist" (sidebar e índice de playlists). + +**Scrim:** `rgba(4,4,6,.55)` + `backdrop-filter: blur(3px)` sobre el fondo `radial-gradient(120% 90% at 50% 0%, #15131c, #08080b 58%, #050507)`. Modal centrado con `display:grid; place-items:center; padding:40px 20px`. + +**Contenedor:** `max-width:560px; radius:24px; border:1px solid rgba(255,255,255,.075)`, fondo `linear-gradient(180deg,#141419,#0d0d11 62%,#0b0b0e)`, `box-shadow:0 32px 90px rgba(0,0,0,.66)` + `inset 0 2px 0 rgba(255,255,255,.035)`. Entrada `modalIn .28s cubic-bezier(.2,.8,.3,1)` (`opacity 0 → 1`, `translateY(14px) scale(.985)` → sin transformar). + +**Aura:** capa absoluta no interactiva `radial-gradient(110% 60% at 18% -10%, oklch(.55 .13 H / .16), transparent 62%)`, con `transition: background .5s` — vira con el acento elegido. + +**Header** (`padding:24px 26px 0`): eyebrow `TU BIBLIOTECA` 10.5px uppercase tracking .16em `#8f8f93`; título Sora 21px/600 tracking -0.03em `#d8d7db` "Crear playlist". Cerrar: 32px, radio 10px, borde `rgba(255,255,255,.06)`, fondo `rgba(255,255,255,.025)` → `.07` y color `#87878c` → `#d0cfd4` en hover. + +**Portada** (columna izquierda, 124px): +- Cuadrado 124px radio 16px, `linear-gradient(150deg, oklch(.66 .16 H), oklch(.34 .11 H) 58%, #0e0e13)`, borde `rgba(255,255,255,.07)`, `box-shadow:0 14px 34px oklch(.5 .13 H / .22)`, con el icono de nota 26px `rgba(255,255,255,.62)` centrado. Transición `.45s` al cambiar de acento. +- **Selector de acento:** 5 franjas `flex:1; height:20px; radius:7px` (gap 6px, `margin-top:11px`), cada una `linear-gradient(145deg, oklch(.66 .16 h), oklch(.38 .11 h))`. Hues: Ámbar 68, Violeta 288 (por defecto), Azul 244, Verde 158, Rojo 22. Seleccionada: `box-shadow:0 0 0 1.5px rgba(255,255,255,.85), 0 0 0 4px rgba(0,0,0,.5)`; el resto `0 0 0 1px rgba(255,255,255,.07)`. +- **Subir imagen:** botón de texto 11px `#8a8a8f`, `padding:7px 0; radius:9px`, borde `1px dashed rgba(255,255,255,.1)`; hover texto `#cfccd9` y borde `rgba(255,255,255,.2)`. + +**Campos** (columna derecha, `gap:20px` respecto a la portada): +- Etiquetas 11px/600 `#87878c`. +- Nombre: `padding:12px 14px; radius:12px`, fondo `rgba(255,255,255,.035)`, borde `rgba(255,255,255,.075)`, texto **Sora 16px/600** tracking -0.02em `#dcdbe0`, placeholder "Playlist sin título" `#5d5d63`, `maxlength 60`. Foco: borde `rgba(255,255,255,.2)`, fondo `.055`. Contador `{n}/60` 10.5px `#65656a` alineado a la derecha. +- Descripción (opcional): `textarea` 3 filas, `resize:none`, 12.5px/1.6 `#c4c3c9`, mismo tratamiento de superficie y foco. Placeholder "Para qué sirve esta playlist, cuándo la escuchas…". + +**Opciones** (`padding:20px 26px 0`, dos bloques `radius:13px; padding:13px 15px`, fondo `rgba(255,255,255,.026)`, borde `rgba(255,255,255,.055)`, gap 9px): +- **Visibilidad** — segmentado de 2 (`Privada` por defecto / `Pública`) en un riel `radius:10px` `rgba(0,0,0,.34)`; opción activa fondo `#cfccd9` texto `#0a0a0d`, inactiva `#8e8e94`. El texto de ayuda cambia: "Solo tú la ves en tu biblioteca" / "Aparece en tu perfil y se puede compartir". +- **Colaborativa** — toggle 40×23px, riel `rgba(255,255,255,.12)` → `oklch(.68 .15 H)` al activar, perilla 17px `#0c0c10` con `translateX(17px)`, `.2s cubic-bezier(.3,.8,.3,1)`. Ayuda: "Quien tenga el enlace puede añadir canciones". Toda la fila es clicable. + +**Footer** (`border-top:1px solid rgba(255,255,255,.055)`, fondo `rgba(0,0,0,.2)`, `padding:17px 26px 20px`): nota 11px `#6e6e74` "Podrás añadir canciones desde cualquier pantalla."; a la derecha "Cancelar" (secundario, borde `rgba(255,255,255,.1)`, `#a9a7b2`) y "Crear playlist" (primario con icono +). + +**Validación:** el CTA solo se habilita con nombre no vacío (`trim`). Deshabilitado: fondo `rgba(255,255,255,.08)`, texto `#6c6c72`, `cursor:not-allowed`. Habilitado: `#c6c4cb` sobre `#0a0a0d`. + +**Confirmación:** al crear, toast fijo abajo-centro (`bottom:34px`, `radius:13px`, fondo `#16161c`, borde `rgba(255,255,255,.09)`, `box-shadow:0 18px 46px rgba(0,0,0,.6)`, `fadeIn .22s`) con un cuadrado 26px del degradado elegido y el texto «{nombre}» creada. Se oculta a los 2.6s. + +**Estado:** `{ name, desc, hue: 0..4, vis: 'Privada'|'Pública', collab: boolean, created: boolean, createdName }`. +**Salida esperada:** `POST /playlists { name, description, coverHue, visibility, collaborative }` → redirigir a la playlist recién creada. + +--- + ## Copy dinámico Saludo del hero según hora y día de la semana: @@ -317,9 +389,11 @@ Saludo del hero según hora y día de la semana: ## Files +- `Crear playlist.dc.html` — modal de creación de playlist (diseño aparte). - `Musify.dc.html` — prototipo completo (plantilla + lógica + datos de ejemplo). Contiene todas las vistas y estados; el selector `demoState` permite ver loading / error / empty. - `support.js` — runtime del prototipo. **No portar**: solo hace funcionar el HTML de referencia. - `screenshots/` — capturas de referencia (viewport ~924px, es decir en modo tablet: sidebar plegado a barra de pestañas inferior): - `01-home-hero.png` … `04-home-populares-playlists.png` — recorrido vertical de Home - `05-descubrir.png`, `06-playlists.png`, `07-tracklist-me-gusta.png`, `08-sin-resultados.png` - Todas con "Yankee" sonando, de ahí el tinte ámbar: con otra pista el mismo layout se ve en otro color. + Nota: las capturas son anteriores al rediseño de la vista de resultados de búsqueda (5b) y al modal de crear playlist; para esos dos, la referencia son los archivos HTML y este documento. + Todas con "Yankee" sonando, de ahí el tinte ámbar: con otra pista el mismo layout se ve en otro color. diff --git a/web-player/design_handoff_musify_player/support.js b/web-player/design_handoff_musify_player/support.js index 712aa3b..cb009b6 100644 --- a/web-player/design_handoff_musify_player/support.js +++ b/web-player/design_handoff_musify_player/support.js @@ -1,86 +1,89 @@ // GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. -'use strict'; +"use strict"; (() => { - var __defProp = Object.defineProperty; - var __defNormalProp = (obj, key, value) => - key in obj - ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) - : (obj[key] = value); - var __publicField = (obj, key, value) => - __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value); + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - // src/react.ts - function getReact() { - const R = window.React; - if (!R) throw new Error('dc-runtime: window.React is not available yet'); - return R; - } - function getReactDOM() { - const RD = window.ReactDOM; - if (!RD) throw new Error('dc-runtime: window.ReactDOM is not available yet'); - return RD; - } - var h = (...args) => getReact().createElement(...args); + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); - // src/parse.ts - function parseDcDocument(doc) { - const dc = doc.querySelector('x-dc'); - if (!dc) return null; - const scriptEl = doc.querySelector('script[data-dc-script]'); - const { props, preview } = parseDataProps(scriptEl?.getAttribute('data-props') ?? null); - return { - template: dc.innerHTML, - js: scriptEl ? scriptEl.textContent || '' : '', - props, - preview - }; - } - function parseDcText(src) { - const openMatch = /]*)?>/.exec(src); - if (!openMatch) return null; - const close = src.lastIndexOf('
'); - if (close === -1 || close < openMatch.index) return null; - const template = src.slice(openMatch.index + openMatch[0].length, close); - const doc = new DOMParser().parseFromString(src, 'text/html'); - const scriptEl = doc.querySelector('script[data-dc-script]'); - const { props, preview } = parseDataProps(scriptEl?.getAttribute('data-props') ?? null); - return { - template, - js: scriptEl ? scriptEl.textContent || '' : '', - props, - preview - }; - } - function parseDataProps(raw) { - if (!raw) return { props: null, preview: null }; - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { props: null, preview: null }; - } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - return { props: null, preview: null }; - } - const obj = parsed; - const preview = obj.$preview && typeof obj.$preview === 'object' ? obj.$preview : null; - const rest = {}; - for (const k of Object.keys(obj)) { - if (k[0] !== '$') rest[k] = obj[k]; - } - return { props: Object.keys(rest).length ? rest : null, preview }; - } - function dcNameFromPath(pathname) { - let p = pathname || ''; - try { - p = decodeURIComponent(p); - } catch {} - const base = p.split('/').pop() || 'Root'; - return base.replace(/\.dc\.html$/, '').replace(/\.html?$/, '') || 'Root'; - } + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } - // src/boot.ts - var BASE_CSS = ` + // src/boot.ts + var BASE_CSS = ` .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); border:1px solid color-mix(in srgb,currentColor 50%,transparent); border-radius:2px;box-sizing:border-box;overflow:hidden} @@ -126,1828 +129,1783 @@ } } `; - var FULL_PAGE_CSS = 'html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}'; - function rootNameForDocument(doc, loc) { - let bootPath = loc.pathname || ''; - if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { - try { - bootPath = new URL(doc.baseURI || '/').pathname; - } catch {} - } - return dcNameFromPath(bootPath); - } - function safeDecode(s) { - try { - return decodeURIComponent(s); - } catch { - return s; - } - } - function boot(runtime, doc = document) { - const parsed = parseDcDocument(doc); - if (!parsed) return null; - const React = getReact(); - const rootName = rootNameForDocument(doc, location); - runtime.markFetched(rootName); - runtime.setRootName(rootName); - runtime.adoptParsed(rootName, parsed); - if (!window.__resources) { - fetch(location.href) - .then((res) => (res.ok ? res.text() : '')) - .then((t) => { - const raw = t ? parseDcText(t) : null; - if (raw?.template) runtime.updateHtml(rootName, raw.template); - }) - .catch(() => {}); - } - const dc = doc.querySelector('x-dc'); - const hostEl = doc.createElement('div'); - hostEl.id = 'dc-root'; - dc.replaceWith(hostEl); - if (!parsed.preview) { - const s = doc.createElement('style'); - s.textContent = FULL_PAGE_CSS; - doc.head.appendChild(s); - } - const Root = runtime.getDC(rootName); - const entry = runtime.registry.get(rootName); - function StandaloneRoot() { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - entry.subs.add(sub); - return () => { - entry.subs.delete(sub); - }; - }, []); - const defaults = React.useMemo(() => { - const d = {}; - for (const k in entry.propsMeta || {}) { - const v = entry.propsMeta?.[k]?.default; - if (v !== void 0) d[k] = v; - } - return d; - }, [entry.propsMeta]); - return h(Root, { ...defaults, ...(entry.propOverrides || {}) }); - } - const ReactDOM = getReactDOM(); - if (ReactDOM.createRoot) ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); - else ReactDOM.render(h(StandaloneRoot), hostEl); - return rootName; - } + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + if (!window.__resources) { + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + } + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } - // src/expr.ts - var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; - var NUMBER_RE = /^-?\d+(\.\d+)?$/; - function resolve(vals, src) { - const expr = String(src).trim(); - if (!expr) return void 0; - if (expr[0] === '(' && expr[expr.length - 1] === ')' && parensWrapWhole(expr)) { - return resolve(vals, expr.slice(1, -1)); - } - const eq = findTopLevelEquality(expr); - if (eq) { - const lv = resolve(vals, expr.slice(0, eq.index)); - const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); - switch (eq.op) { - case '===': - return lv === rv; - case '!==': - return lv !== rv; - case '==': - return lv == rv; - default: - return lv != rv; - } - } - if (expr[0] === '!') return !resolve(vals, expr.slice(1)); - if (expr === 'true') return true; - if (expr === 'false') return false; - if (expr === 'null') return null; - if (expr === 'undefined') return void 0; - if (NUMBER_RE.test(expr)) return Number(expr); - if ( - expr.length >= 2 && - (expr[0] === '"' || expr[0] === "'") && - expr[expr.length - 1] === expr[0] - ) { - return expr.slice(1, -1); - } - return resolvePath(vals, expr); - } - function parensWrapWhole(expr) { - let depth = 0; - for (let i = 0; i < expr.length - 1; i++) { - if (expr[i] === '(') depth++; - else if (expr[i] === ')') { - depth--; - if (depth === 0) return false; - } - } - return true; - } - function findTopLevelEquality(expr) { - let depth = 0; - for (let i = 0; i < expr.length; i++) { - const c = expr[i]; - if (c === '[' || c === '(') depth++; - else if (c === ']' || c === ')') depth--; - else if (depth === 0 && (c === '=' || c === '!') && expr[i + 1] === '=') { - if (i > 0 && (expr[i - 1] === '=' || expr[i - 1] === '!')) continue; - if (!expr.slice(0, i).trim()) continue; - const op = expr[i + 2] === '=' ? c + '==' : c + '='; - return { index: i, op }; - } - } - return null; - } - function resolvePath(vals, expr) { - const head = expr.match(IDENT_RE); - if (!head) return void 0; - let cur = vals == null ? void 0 : vals[head[0]]; - let i = head[0].length; - while (i < expr.length) { - if (expr[i] === '.') { - const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); - if (!m) return void 0; - cur = cur == null ? void 0 : cur[m[0]]; - i += 1 + m[0].length; - } else if (expr[i] === '[') { - let depth = 1; - let j = i + 1; - while (j < expr.length && depth > 0) { - if (expr[j] === '[') depth++; - else if (expr[j] === ']') { - depth--; - if (depth === 0) break; - } - j++; - } - if (depth !== 0) return void 0; - const key = resolve(vals, expr.slice(i + 1, j)); - cur = cur == null ? void 0 : cur[key]; - i = j + 1; - } else { - return void 0; - } - } - return cur; - } + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } - // src/encode.ts - var CAMEL_ATTR = 'sc-camel-'; - var INLINE_TEXT_TAGS = new Set( - 'a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr'.split( - ' ' - ) - ); - var RAW_WRAP = { - select: 'sc-raw-select', - table: 'sc-raw-table', - tbody: 'sc-raw-tbody', - thead: 'sc-raw-thead', - tfoot: 'sc-raw-tfoot', - tr: 'sc-raw-tr', - td: 'sc-raw-td', - th: 'sc-raw-th', - caption: 'sc-raw-caption' - }; - var RAW_UNWRAP = Object.fromEntries(Object.entries(RAW_WRAP).map(([k, v]) => [v, k])); - var EVENT_MAP = { - onclick: 'onClick', - onchange: 'onChange', - oninput: 'onInput', - onsubmit: 'onSubmit', - onkeydown: 'onKeyDown', - onkeyup: 'onKeyUp', - onkeypress: 'onKeyPress', - onmousedown: 'onMouseDown', - onmouseup: 'onMouseUp', - onmouseenter: 'onMouseEnter', - onmouseleave: 'onMouseLeave', - onfocus: 'onFocus', - onblur: 'onBlur', - ondoubleclick: 'onDoubleClick', - oncontextmenu: 'onContextMenu', - onmousemove: 'onMouseMove', - onmouseover: 'onMouseOver', - onmouseout: 'onMouseOut', - onpointerdown: 'onPointerDown', - onpointerup: 'onPointerUp', - onpointermove: 'onPointerMove', - onpointerenter: 'onPointerEnter', - onpointerleave: 'onPointerLeave', - onpointercancel: 'onPointerCancel', - onpointerover: 'onPointerOver', - onpointerout: 'onPointerOut', - ongotpointercapture: 'onGotPointerCapture', - onlostpointercapture: 'onLostPointerCapture', - ontouchstart: 'onTouchStart', - ontouchend: 'onTouchEnd', - ontouchmove: 'onTouchMove', - ontouchcancel: 'onTouchCancel', - ondragstart: 'onDragStart', - ondragend: 'onDragEnd', - ondragenter: 'onDragEnter', - ondragleave: 'onDragLeave', - ondragover: 'onDragOver', - onanimationstart: 'onAnimationStart', - onanimationend: 'onAnimationEnd', - onanimationiteration: 'onAnimationIteration', - ontransitionend: 'onTransitionEnd' - }; - var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; - var IMPORT_SELF_CLOSE_RE = new RegExp('<(x-import|dc-import)(' + ATTRS + ')/>', 'gi'); - var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; - function encodeCamelAttrs(html) { - return html.replace( - CAMEL_ATTR_RE, - (_, sp, name, eq) => - sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()) + eq - ); - } - function encodeCase(html) { - html = html.replace(IMPORT_SELF_CLOSE_RE, (_, t, a) => '<' + t + a + '>'); - html = html.replace(/)/gi, '/gi, ''); - html = encodeCamelAttrs(html); - for (const [real, alias] of Object.entries(RAW_WRAP)) { - html = html.replace(new RegExp('(])', 'gi'), '$1' + alias); - } - return html; - } - function kebabToCamel(s) { - return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); - } - function cssToObj(css) { - const o = {}; - for (const decl of css.split(';')) { - const i = decl.indexOf(':'); - if (i < 0) continue; - const prop = decl.slice(0, i).trim(); - o[prop.startsWith('--') ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); - } - return o; - } - function compileAttr(raw) { - const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); - if (whole) { - const path = whole[1]; - return (vals) => resolve(vals, path); - } - if (raw.includes('{{')) { - const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); - return (vals) => parts.map((s, i) => (i & 1 ? (resolve(vals, s) ?? '') : s)).join(''); - } - return () => raw; - } + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu", + onmousemove: "onMouseMove", + onmouseover: "onMouseOver", + onmouseout: "onMouseOut", + onpointerdown: "onPointerDown", + onpointerup: "onPointerUp", + onpointermove: "onPointerMove", + onpointerenter: "onPointerEnter", + onpointerleave: "onPointerLeave", + onpointercancel: "onPointerCancel", + onpointerover: "onPointerOver", + onpointerout: "onPointerOut", + ongotpointercapture: "onGotPointerCapture", + onlostpointercapture: "onLostPointerCapture", + ontouchstart: "onTouchStart", + ontouchend: "onTouchEnd", + ontouchmove: "onTouchMove", + ontouchcancel: "onTouchCancel", + ondragstart: "onDragStart", + ondragend: "onDragEnd", + ondragenter: "onDragEnter", + ondragleave: "onDragLeave", + ondragover: "onDragOver", + onanimationstart: "onAnimationStart", + onanimationend: "onAnimationEnd", + onanimationiteration: "onAnimationIteration", + ontransitionend: "onTransitionEnd" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCamelAttrs(html) { + return html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + } + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = encodeCamelAttrs(html); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } - // src/compile.ts - function collectProps(node, kind, host) { - const propGetters = []; - const pseudoClasses = []; - let hintSize = null; - for (const { name, value } of [...node.attributes]) { - if (name === 'sc-name' || name === 'data-dc-tpl') continue; - let key = name; - if (key.startsWith(CAMEL_ATTR)) key = kebabToCamel(key.slice(CAMEL_ATTR.length)); - if (key === 'hint-size') { - hintSize = value; - continue; - } - if (key.startsWith('style-')) { - pseudoClasses.push(host.pseudoClass(key.slice(6), value)); - continue; - } - if (kind !== 'dom') { - if ( - key.includes('-') && - !(kind === 'x-import' && (key.startsWith('aria-') || key.startsWith('data-'))) - ) - key = kebabToCamel(key); - } else { - if (key === 'class') key = 'className'; - else if (key === 'for') key = 'htmlFor'; - else if (key.startsWith('on')) - key = EVENT_MAP[key] || 'on' + key[2].toUpperCase() + key.slice(3); - } - propGetters.push([key, compileAttr(value)]); - } - return { propGetters, pseudoClasses, hintSize }; - } - var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ - 'position', - 'left', - 'right', - 'top', - 'bottom', - 'inset', - 'width', - 'height', - 'z-index', - 'transform' - ]); - function hostPositionStyle(style) { - const all = - typeof style === 'string' - ? cssToObj(style) - : style != null && typeof style === 'object' - ? style - : null; - if (!all) return void 0; - const out = {}; - for (const [k, v] of Object.entries(all)) { - const kebab = k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); - if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; - } - return Object.keys(out).length ? out : void 0; - } - function compileTemplate(html, host) { - const tpl = document.createElement('template'); - //! nosemgrep: direct-inner-html-assignment - tpl.innerHTML = encodeCase(html); - let tplN = 0; - (function stamp(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - node.setAttribute('data-dc-tpl', String(tplN++)); - } - for (const c of node.childNodes) stamp(c); - })(tpl.content); - const builders = walkChildren(tpl.content, host); - const render = (vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i)); - render.__annotated = tpl.innerHTML; - return render; - } - function walkChildren(node, host) { - return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); - } - var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; - var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; - var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; - function isDeckMountTag(el) { - if (el.localName === 'deck-stage') return true; - return ( - el.localName === 'x-import' && - (el.getAttribute('component-from-global-scope') || '') === 'deck-stage' - ); - } - function walkDeckChildren(el, host) { - const pairs = [...el.childNodes] - .map((c) => ({ c, b: walk(c, host) })) - .filter((p) => p.b !== null); - const kids = pairs.map((p) => p.b); - const seen = /* @__PURE__ */ new Set(); - const wsSeen = /* @__PURE__ */ new Map(); - const keys = []; - const nextSlideId = new Array(pairs.length); - { - let upcoming = null; - for (let j = pairs.length - 1; j >= 0; j--) { - const n = pairs[j].c; - if (n.nodeType === Node.ELEMENT_NODE) { - const t = n.localName; - upcoming = - !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) - ? n.getAttribute('data-om-slide-id') - : null; - } - nextSlideId[j] = upcoming; - } - } - for (let j = 0; j < pairs.length; j++) { - const { c } = pairs[j]; - if (c.nodeType === Node.TEXT_NODE) { - if ((c.nodeValue ?? '').trim() === '') { - const base = nextSlideId[j] ? 'omid-ws:' + nextSlideId[j] : 'omid-ws:aux'; - const n = wsSeen.get(base) ?? 0; - wsSeen.set(base, n + 1); - keys.push(n === 0 ? base : base + ':' + n); - continue; - } - return { kids, keys: null }; - } - if (c.nodeType !== Node.ELEMENT_NODE) { - keys.push(j); - continue; - } - const child = c; - const tag = child.localName; - if (DECK_AUX_RE.test(tag)) { - keys.push(j); - continue; - } - if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; - const v = child.getAttribute('data-om-slide-id'); - if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { - return { kids, keys: null }; - } - seen.add(v); - keys.push('omid:' + v); - } - return { kids, keys }; - } - function renderDeckKids(kids, kidKeys, vals, ctx) { - return kids.map((b, j) => { - const k = kidKeys ? kidKeys[j] : j; - const out = b(vals, ctx, k); - return kidKeys != null && typeof out === 'string' - ? h(getReact().Fragment, { key: k }, out) - : out; - }); - } - function walk(node, host) { - if (node.nodeType === Node.TEXT_NODE) return walkText(node); - if (node.nodeType !== Node.ELEMENT_NODE) return null; - const el = node; - const tag = el.tagName.toLowerCase(); - if (tag === 'sc-for') return walkFor(el, host); - if (tag === 'sc-if') return walkIf(el, host); - if (tag === 'x-import') return walkXImport(el, host); - if (tag === 'sc-helmet') return host.helmet(el); - if (tag === 'dc-import') return walkComponent(el, host); - return walkElement(el, host); - } - var warnedHoles = /* @__PURE__ */ new Set(); - function warnUnresolved(ctx, what) { - const key = (ctx?.__name || '?') + '\0' + what; - if (warnedHoles.has(key)) return; - warnedHoles.add(key); - console.warn('[dc-runtime] ' + (ctx?.__name || 'template') + ': ' + what); - } - function walkText(node) { - const txt = node.nodeValue ?? ''; - if (!txt.includes('{{')) { - if (!txt.trim() && !txt.includes(' ')) return null; - return () => txt; - } - const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); - return (vals, ctx, key) => - h( - getReact().Fragment, - { key }, - ...parts.map((p, i) => { - if (!(i & 1)) return p; - const v = resolve(vals, p); - if (v === void 0) { - if (!ctx?.__streamingNow) { - if (document.body?.hasAttribute('data-dc-editor-on')) { - return h( - 'span', - { key: i, className: 'sc-interp sc-unresolved' }, - '{{ ' + p.trim() + ' }}' - ); - } - warnUnresolved(ctx, '{{ ' + p.trim() + ' }} never resolved \u2014 rendered as empty'); - return null; - } - return h('span', { key: i, className: 'sc-interp sc-missing' }, p.trim()); - } - if (getReact().isValidElement(v) || Array.isArray(v)) { - return h(getReact().Fragment, { key: i }, v); - } - if (v === null || typeof v === 'boolean') return null; - return h('span', { key: i, className: 'sc-interp' }, String(v)); - }) - ); - } - function walkFor(el, host) { - const listGet = compileAttr(el.getAttribute('list') || ''); - const asName = el.getAttribute('as') || 'item'; - const hintN = parseInt(el.getAttribute('hint-placeholder-count') || '0', 10); - const kids = walkChildren(el, host); - const listSrc = el.getAttribute('list') || ''; - return (vals, ctx, key) => { - let list = listGet(vals); - if (!Array.isArray(list)) { - if (!ctx?.__streamingNow) { - if (list !== void 0 && list !== null) { - warnUnresolved( - ctx, - 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ')' - ); - } - list = []; - } else { - list = hintN > 0 ? Array(hintN).fill(void 0) : []; - } - } - return h( - getReact().Fragment, - { key }, - list.map((item, i) => { - const sub = { ...vals, [asName]: item, $index: i }; - return h( - getReact().Fragment, - { key: i }, - kids.map((b, j) => b(sub, ctx, j)) - ); - }) - ); - }; - } - function walkIf(el, host) { - const valGet = compileAttr(el.getAttribute('value') || ''); - const hintRaw = el.getAttribute('hint-placeholder-val'); - const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - let v = valGet(vals); - if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); - return v - ? h( - getReact().Fragment, - { key }, - kids.map((b, j) => b(vals, ctx, j)) - ) - : null; - }; - } - function walkComponent(el, host) { - const name = el.getAttribute('name') || el.getAttribute('component') || ''; - el.removeAttribute('name'); - el.removeAttribute('component'); - const tplId = el.getAttribute('data-dc-tpl'); - const styleRaw = el.getAttribute('style'); - el.removeAttribute('style'); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const { propGetters, hintSize } = collectProps(el, 'dc-import', host); - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - const props = { - key, - __hintSize: hintSize, - __tplId: tplId, - __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 - }; - for (const [k, g] of propGetters) { - const v = g(vals); - if (k === 'dcProps') { - if (v && typeof v === 'object') Object.assign(props, v); - continue; - } - props[k] = v; - } - if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); - return h(host.component(name), props); - }; - } - function walkXImport(el, host) { - const globalNameGet = compileAttr(el.getAttribute('component-from-global-scope') || ''); - const exportNameGet = compileAttr( - el.getAttribute('component') || el.getAttribute('name') || '' - ); - const fromRaw = - el.getAttribute('from') || - (el.getAttribute('component-from-global-scope') - ? '' - : el.getAttribute('src') || el.getAttribute('import') || ''); - const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; - const url = urls.length ? urls[urls.length - 1] : ''; - const kindOf = (u) => (/\.(jsx|tsx)(\?|#|$)/i.test(u) ? 'jsx' : 'js'); - const tplId = el.getAttribute('data-dc-tpl'); - const styleRaw = el.getAttribute('style'); - el.removeAttribute('style'); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const wrap = tplId != null || styleGet != null; - const { propGetters, hintSize } = collectProps(el, 'x-import', host); - const hasContent = el.children.length > 0 || !!(el.textContent || '').trim(); - const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; - const kidKeys = deckKeyed?.keys ?? null; - const urlBindable = fromRaw.includes('{{'); - if (urls.length && !urlBindable) { - let prev; - for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); - } - const evalName = (g, vals) => { - const v = g(vals); - const s = v == null ? '' : String(v); - return s.includes('{{') ? '' : s; - }; - return (vals, ctx, key) => { - const globalName = evalName(globalNameGet, vals); - const name = globalName || evalName(exportNameGet, vals); - const C = - !name || urlBindable - ? null - : globalName - ? host.resolveExternalGlobal(url, globalName) - : host.resolveExternal(url, name); - const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; - const wrapper = wrap - ? { - key, - className: 'sc-host-x', - 'data-dc-tpl': tplId, - style: hostStyle || { display: 'contents' } - } - : null; - if (!C) { - const error = urlBindable - ? 'x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL' - : host.resolveExternalError(url, name); - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error - }); - return wrapper ? h('div', wrapper, ph) : ph; - } - const props = wrapper ? {} : { key }; - let unresolvedHole = false; - for (const [k, g] of propGetters) { - if (k === 'component' || k === 'componentFromGlobalScope' || k === 'from') { - continue; - } - const v = g(vals); - if (v === void 0) unresolvedHole = true; - if (k === 'dcProps') { - if (v && typeof v === 'object') Object.assign(props, v); - continue; - } - props[k] = v; - } - if (unresolvedHole && ctx?.__htmlStreamingNow) { - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error: null - }); - return wrapper ? h('div', wrapper, ph) : ph; - } - if (kids.length) { - props.children = renderDeckKids(kids, kidKeys, vals, ctx); - } - return wrapper ? h('div', wrapper, h(C, props)) : h(C, props); - }; - } - function contentKey(el) { - const clone = el.cloneNode(true); - for (const d of clone.querySelectorAll('*')) { - while (d.attributes.length) d.removeAttribute(d.attributes[0].name); - } - const s = clone.innerHTML; - let h2 = 5381; - for (let i = 0; i < s.length; i++) h2 = ((h2 << 5) + h2 + s.charCodeAt(i)) | 0; - return s.length + '.' + (h2 >>> 0).toString(36); - } - var NEVER_CONTENT_KEYED = new Set( - 'script style textarea option title select canvas iframe video audio'.split(' ') - ); - var NOT_INLINE_SELECTOR = ':not(' + [...INLINE_TEXT_TAGS].join(',') + ')'; - function walkElement(el, host) { - const realTag = RAW_UNWRAP[el.localName] || el.localName; - const tplId = el.getAttribute('data-dc-tpl'); - const inlineOnly = - el.childNodes.length > 0 && - !NEVER_CONTENT_KEYED.has(realTag) && - el.querySelector(NOT_INLINE_SELECTOR) === null; - const keySuffix = inlineOnly ? '|' + contentKey(el) : ''; - const { propGetters, pseudoClasses } = collectProps(el, 'dom', host); - const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); - const kidKeys = deckKeyed?.keys ?? null; - return (vals, ctx, key) => { - const props = { - key: key + keySuffix, - 'data-dc-tpl': tplId - }; - for (const [k, g] of propGetters) { - let v = g(vals); - if (k === 'style' && typeof v === 'string') v = cssToObj(v); - if ((k === 'value' || k === 'checked') && v === void 0) { - v = k === 'checked' ? false : ''; - } - props[k] = v; - } - if (pseudoClasses.length) { - props.className = [props.className, ...pseudoClasses].filter(Boolean).join(' '); - } - return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); - }; - } + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; + var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; + var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; + function isDeckMountTag(el) { + if (el.localName === "deck-stage") return true; + return el.localName === "x-import" && (el.getAttribute("component-from-global-scope") || "") === "deck-stage"; + } + function walkDeckChildren(el, host) { + const pairs = [...el.childNodes].map((c) => ({ c, b: walk(c, host) })).filter((p) => p.b !== null); + const kids = pairs.map((p) => p.b); + const seen = /* @__PURE__ */ new Set(); + const wsSeen = /* @__PURE__ */ new Map(); + const keys = []; + const nextSlideId = new Array(pairs.length); + { + let upcoming = null; + for (let j = pairs.length - 1; j >= 0; j--) { + const n = pairs[j].c; + if (n.nodeType === Node.ELEMENT_NODE) { + const t = n.localName; + upcoming = !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) ? n.getAttribute("data-om-slide-id") : null; + } + nextSlideId[j] = upcoming; + } + } + for (let j = 0; j < pairs.length; j++) { + const { c } = pairs[j]; + if (c.nodeType === Node.TEXT_NODE) { + if ((c.nodeValue ?? "").trim() === "") { + const base = nextSlideId[j] ? "omid-ws:" + nextSlideId[j] : "omid-ws:aux"; + const n = wsSeen.get(base) ?? 0; + wsSeen.set(base, n + 1); + keys.push(n === 0 ? base : base + ":" + n); + continue; + } + return { kids, keys: null }; + } + if (c.nodeType !== Node.ELEMENT_NODE) { + keys.push(j); + continue; + } + const child = c; + const tag = child.localName; + if (DECK_AUX_RE.test(tag)) { + keys.push(j); + continue; + } + if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; + const v = child.getAttribute("data-om-slide-id"); + if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { + return { kids, keys: null }; + } + seen.add(v); + keys.push("omid:" + v); + } + return { kids, keys }; + } + function renderDeckKids(kids, kidKeys, vals, ctx) { + return kids.map((b, j) => { + const k = kidKeys ? kidKeys[j] : j; + const out = b(vals, ctx, k); + return kidKeys != null && typeof out === "string" ? h(getReact().Fragment, { key: k }, out) : out; + }); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; + const kidKeys = deckKeyed?.keys ?? null; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) { + props.children = renderDeckKids(kids, kidKeys, vals, ctx); + } + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); + const kidKeys = deckKeyed?.keys ?? null; + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); + }; + } - // src/logic.ts - var StreamableLogic = class { - constructor(props) { - __publicField(this, 'props'); - __publicField(this, 'state', {}); - /** Back-pointer to the wrapper component, installed after construction. */ - __publicField(this, '__host'); - this.props = props || {}; - } - setState(update, cb) { - this.__host && this.__host.__setLogicState(update, cb); - } - forceUpdate() { - this.__host && this.__host.forceUpdate(); - } - componentDidMount() {} - componentDidUpdate(_prevProps) {} - componentWillUnmount() {} - /** The flat object the template renders against (merged over props). */ - renderVals() { - return {}; - } - }; - function evalDcLogic(src) { - //! nosemgrep: eval-and-function-constructor - const fn = new Function( - 'DCLogic', - 'StreamableLogic', - 'React', - src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' - ); - return fn(StreamableLogic, StreamableLogic, getReact()); - } + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } - // src/component.ts - function shallowEqual(a, b) { - if (!b) return false; - const ak = Object.keys(a).filter((k) => k !== 'children'); - const bk = Object.keys(b).filter((k) => k !== 'children'); - if (ak.length !== bk.length) return false; - for (const k of ak) if (a[k] !== b[k]) return false; - return true; - } - function Placeholder({ name, hintSize, streaming, error }) { - const [w, hgt] = (hintSize || '100%,60px').split(','); - return h( - 'div', - { - className: 'sc-placeholder' + (streaming ? ' sc-streaming' : ''), - style: { width: w.trim(), height: hgt && hgt.trim() }, - title: name - }, - error - ? h('div', { className: 'sc-placeholder-error' }, (name ? name + ': ' : '') + error) - : null - ); - } - function hintToMin(hint) { - if (!hint) return void 0; - const [w, hgt] = hint.split(','); - return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; - } - function createComponentFactory(registry, ensureFetched) { - const React = getReact(); - const AncestorContext = React.createContext([]); - class StreamableComponent extends React.Component { - constructor(props) { - super(props); - __publicField(this, '__name'); - __publicField(this, '__sub'); - __publicField(this, '__needsDidMount', false); - /** Snapshot of the registry's streaming flags taken at render time — - * builders read it off the RenderCtx (this) to pick placeholder vs - * render-nothing for unresolved values. */ - __publicField(this, '__streamingNow', false); - __publicField(this, '__htmlStreamingNow', false); - /** When a construct throws, remember the (class, registry.ver, props) - * triple so render-time reconcile doesn't re-attempt it on every parent - * re-render. A registry bump (new class, template, external module - * resolving via bumpAll) changes `ver` and breaks the memo so an - * env-dependent constructor can self-heal. */ - __publicField(this, '__failedLogic', null); - __publicField(this, '__failedUserProps', null); - __publicField(this, '__failedVer', -1); - /** Per-instance constructor error — kept here (not on the registry entry) - * so one instance's successful construct can't hide a sibling's failure, - * and a construct can never wipe an eval error `updateJs` recorded on - * `r.logicError`. */ - __publicField(this, '__ctorError', null); - __publicField(this, 'logic'); - this.__name = props.__name; - this.state = { __v: 0, __err: null }; - this.__sub = () => { - if (this.state.__err) this.setState({ __err: null }); - this.forceUpdate(); - }; - this.__makeLogic(registry.get(this.__name).Logic, null); - ensureFetched(this.__name); - } - /** Error-boundary hook: a render crash anywhere in this DC's subtree - * (its own template, an x-import'd component, a child DC without its - * own deeper boundary) lands here instead of unmounting the page. */ - static getDerivedStateFromError(e) { - return { __err: e instanceof Error && e.message ? e.message : String(e) }; - } - componentDidCatch(e, info) { - console.error( - '[dc-runtime] render error in <' + this.__name + '>:', - e, - info?.componentStack || '' - ); - } - /** Instantiate the logic class (or the no-op base) and adopt `prevState` - * over its initial state — used both at mount and on hot-swap. */ - __makeLogic(Logic, prevState) { - const L = Logic || StreamableLogic; - try { - this.logic = new L(this.__userProps()); - this.__failedLogic = null; - this.__failedUserProps = null; - this.__ctorError = null; - } catch (e) { - console.error(e); - this.__failedLogic = Logic; - this.__failedUserProps = this.__userProps(); - this.__failedVer = registry.get(this.__name).ver; - this.__ctorError = - this.__name + ': ' + (e instanceof Error && e.message ? e.message : String(e)); - this.logic = new StreamableLogic(this.__userProps()); - } - this.logic.__host = this; - if (prevState) this.logic.state = { ...(this.logic.state || {}), ...prevState }; - } - /** The props the author's logic + template see — internal __-prefixed - * wiring stripped. */ - __userProps() { - const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; - return rest; - } - __setLogicState(update, cb) { - const prev = this.logic.state; - const patch = typeof update === 'function' ? update(prev) : update; - this.logic.state = { ...prev, ...patch }; - this.setState((s) => ({ __v: s.__v + 1 }), cb); - } - /** Swap the logic instance when the registry's Logic class changed - * (streaming completion, hot reload). State carries over; didMount - * re-fires after the swap commits so refs exist. */ - __reconcileLogic() { - const r = registry.get(this.__name); - const Next = r.Logic; - const Cur = this.logic.constructor; - if ( - Next === Cur || - (!Next && Cur === StreamableLogic) || - (Next === this.__failedLogic && - r.ver === this.__failedVer && - shallowEqual(this.__userProps(), this.__failedUserProps)) - ) { - return; - } - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - this.__makeLogic(Next, this.logic.state); - this.__needsDidMount = true; - } - componentDidMount() { - registry.get(this.__name).subs.add(this.__sub); - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } - componentDidUpdate(prevProps) { - this.logic.props = this.__userProps(); - if (this.__needsDidMount) { - if (this.state.__err || !registry.get(this.__name).tpl) return; - this.__needsDidMount = false; - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } else { - try { - this.logic.componentDidUpdate(prevProps); - } catch (e) { - console.error(e); - } - } - } - componentWillUnmount() { - registry.get(this.__name).subs.delete(this.__sub); - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - } - render() { - const r = registry.get(this.__name); - const cls = - 'sc-host' + - (r.htmlStreaming ? ' sc-streaming-html' : '') + - (r.jsStreaming ? ' sc-streaming-js' : ''); - const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; - const hostStyle = - this.props.__hostStyle || hintStyle - ? { ...(hintStyle || {}), ...(this.props.__hostStyle || {}) } - : void 0; - const hostBase = { - className: cls, - style: hostStyle, - 'data-sc-name': this.__name, - 'data-dc-tpl': this.props.__tplId - }; - const chain = Array.isArray(this.context) ? this.context : []; - if (chain.includes(this.__name)) { - const cycle = [...chain.slice(chain.indexOf(this.__name)), this.__name].join(' \u2192 '); - return h( - 'div', - { ...hostBase, className: cls + ' sc-has-error' }, - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: 'circular import: ' + cycle - }) - ); - } - if (this.state.__err) { - return h( - 'div', - { ...hostBase, className: cls + ' sc-has-error' }, - h( - 'div', - { className: 'sc-logic-error', 'data-omelette-chrome': '' }, - this.__name + ': ' + this.state.__err - ), - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: this.state.__err - }) - ); - } - this.__reconcileLogic(); - if (!r.tpl) { - return h( - 'div', - hostBase, - h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) - ); - } - const userProps = this.__userProps(); - this.logic.props = userProps; - let vals = userProps; - let renderErr = r.logicError || this.__ctorError; - try { - vals = { ...userProps, ...(this.logic.renderVals() || {}) }; - } catch (e) { - console.error(e); - renderErr = - this.__name + - '.renderVals(): ' + - (e instanceof Error && e.message ? e.message : String(e)); - } - this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); - this.__htmlStreamingNow = !!r.htmlStreaming; - return h( - 'div', - { ...hostBase, className: cls + (renderErr ? ' sc-has-error' : '') }, - renderErr && - h('div', { className: 'sc-logic-error', 'data-omelette-chrome': '' }, renderErr), - h(AncestorContext.Provider, { value: [...chain, this.__name] }, r.tpl(vals, this)) - ); - } - } - __publicField(StreamableComponent, 'contextType', AncestorContext); - const named = /* @__PURE__ */ new Map(); - function getDC(name) { - const hit = named.get(name); - if (hit) return hit; - function Dispatcher(p) { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - registry.get(name).subs.add(sub); - return () => { - registry.get(name).subs.delete(sub); - }; - }, []); - ensureFetched(name); - return h(StreamableComponent, { ...p, __name: name }); - } - Dispatcher.displayName = name; - named.set(name, Dispatcher); - return Dispatcher; - } - return { - getDC, - StreamableComponent - }; - } + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } - // src/bundled.ts - function bundledBlob(url) { - const blobs = window.__resourceBlobs; - const b = blobs ? blobs[url.split('#')[0]] : void 0; - return b instanceof Blob ? b : null; - } + // src/bundled.ts + function bundledBlob(url) { + const blobs = window.__resourceBlobs; + const b = blobs ? blobs[url.split("#")[0]] : void 0; + return b instanceof Blob ? b : null; + } - // src/cdn.ts - var REACT_URL = 'https://unpkg.com/react@18.3.1/umd/react.production.min.js'; - var REACT_SRI = 'sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z'; - var REACT_DOM_URL = 'https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js'; - var REACT_DOM_SRI = 'sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1'; - var BABEL_URL = 'https://unpkg.com/@babel/standalone@7.29.0/babel.min.js'; - var BABEL_SRI = 'sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y'; - function cdnScriptFor(url, sri) { - const res = window.__resources; - const v = res ? res[url] : void 0; - return typeof v === 'string' && v ? { src: v } : { src: url, integrity: sri }; - } + // src/cdn.ts + var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; + var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; + var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; + var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; + } - // src/external.ts - var isCustomElementName = (n) => !n.includes('.') && n.includes('-'); - function isRenderableType(g) { - if (typeof g === 'function') return !isElementClass(g); - return typeof g === 'object' && g !== null && typeof g.$$typeof === 'symbol'; - } - function resolveDottedPath(root, name) { - let cur = root; - for (const seg of name.split('.')) { - if (cur == null) return void 0; - cur = cur[seg]; - } - return cur; - } - var GLOBAL_POLL_INTERVAL_MS = 50; - var GLOBAL_POLL_TIMEOUT_MS = 3e4; - function createExternalModules(onResolved) { - const cache = /* @__PURE__ */ new Map(); - let babelLoading = null; - const reportedMissing = /* @__PURE__ */ new Map(); - const polling = /* @__PURE__ */ new Set(); - function ensureBabel() { - if (window.Babel) return Promise.resolve(); - if (babelLoading) return babelLoading; - const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); - babelLoading = new Promise((res, rej) => { - const s = document.createElement('script'); - s.src = babel.src; - if (babel.integrity) { - s.integrity = babel.integrity; - s.crossOrigin = 'anonymous'; - } - s.onload = () => res(); - s.onerror = rej; - document.head.appendChild(s); - }); - return babelLoading; - } - const pending = /* @__PURE__ */ new Map(); - function load(kind, url, after) { - const existing = pending.get(url); - if (existing) return existing; - cache.set(url, null); - console.info('[dc-runtime] x-import: loading', url, '(' + kind + ')'); - const ready = Promise.all([ - kind === 'jsx' ? ensureBabel() : Promise.resolve(), - after ?? Promise.resolve() - ]); - const p = ready - .then(() => { - const pre = bundledBlob(url); - if (pre) return pre.text(); - return fetch(url).then((r) => { - if (!r.ok) throw new Error('HTTP ' + r.status); - return r.text(); - }); - }) - .then((src) => { - const code = - kind === 'jsx' - ? window.Babel.transform(src, { - filename: url, - presets: ['react', 'typescript'] - }).code - : src; - const module = { exports: {} }; - const before = new Set(Object.keys(window)); - //! nosemgrep: eval-and-function-constructor - new Function('React', 'module', 'exports', 'require', code)( - getReact(), - module, - module.exports, - () => ({}) - ); - const globals = {}; - for (const k of Object.keys(window)) { - if (!before.has(k) && typeof window[k] === 'function') { - globals[k] = window[k]; - } - } - cache.set(url, { mod: module.exports, globals }); - console.info( - '[dc-runtime] x-import: loaded', - url, - '\u2014 exports:', - Object.keys(module.exports), - 'window globals:', - Object.keys(globals) - ); - onResolved(); - }) - .catch((e) => { - cache.set(url, { - mod: {}, - globals: {}, - error: 'failed to load: ' + (e instanceof Error && e.message ? e.message : String(e)) - }); - console.error('[dc-runtime] x-import: FAILED to load', url, '(' + kind + ')', e); - onResolved(); - }); - pending.set(url, p); - return p; - } - function resolve2(url, name) { - const entry = cache.get(url); - if (!entry) return null; - const { mod, globals } = entry; - const C = - (mod && mod[name]) || - (globals && globals[name]) || - (typeof window !== 'undefined' && window[name]) || - (mod && mod.default); - if (typeof C === 'function') return C; - const key = url + '\0' + name; - if (!reportedMissing.has(key)) { - reportedMissing.set( - key, - entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(', ') + ')' - ); - console.error( - '[dc-runtime] x-import: module', - url, - 'loaded but has no component named', - JSON.stringify(name), - '\u2014 available exports:', - Object.keys(mod), - 'window globals:', - Object.keys(globals), - '. The module must `module.exports = {' + name + '}` or set `window.' + name + '`.' - ); - } - return null; - } - function waitForGlobal(name) { - if (polling.has(name)) return; - polling.add(name); - const started = Date.now(); - const isCE = isCustomElementName(name); - const tick = () => { - const found = isCE - ? customElements.get(name) - : isRenderableType(resolveDottedPath(window, name)); - if (found) { - polling.delete(name); - onResolved(); - return; - } - if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { - console.warn( - '[dc-runtime] x-import: global', - JSON.stringify(name), - 'never appeared on window after ' + GLOBAL_POLL_TIMEOUT_MS + 'ms' - ); - return; - } - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - }; - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - } - function resolveGlobal(url, name) { - const isCE = isCustomElementName(name); - if (!url) { - if (isCE) { - if (customElements.get(name)) return name; - waitForGlobal(name); - return null; - } - const g2 = resolveDottedPath(window, name); - if (isRenderableType(g2)) return g2; - waitForGlobal(name); - return null; - } - const entry = cache.get(url); - if (!entry) return null; - if (isCE && customElements.get(name)) return name; - const g = entry.globals[name] ?? resolveDottedPath(window, name); - if (isRenderableType(g)) return g; - if (name.includes('.')) return null; - const key = url + '\0global\0' + name; - if (!reportedMissing.has(key)) { - reportedMissing.set(key, null); - if (isCE && !customElements.get(name)) { - console.warn( - '[dc-runtime] x-import:', - url, - 'loaded but no custom element', - JSON.stringify(name), - 'is registered and window.' + - name + - ' is not a function \u2014 rendering <' + - name + - '> as an unknown element.' - ); - } - } - return name; - } - function getError(url, name) { - const entry = cache.get(url); - if (entry?.error) return entry.error; - return reportedMissing.get(url + '\0' + name) || null; - } - return { load, resolve: resolve2, resolveGlobal, getError }; - } - function isElementClass(g) { - try { - return ( - typeof g === 'function' && - typeof HTMLElement !== 'undefined' && - g.prototype instanceof HTMLElement - ); - } catch { - return false; - } - } + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + s.crossOrigin = "anonymous"; + } + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => { + const pre = bundledBlob(url); + if (pre) return pre.text(); + return fetch(url).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } - // src/atomics.ts - var ATOMIC_CSS = - // layout - '.fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}'; + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); - // src/helmet.ts - var DESIGN_DOC_MODE_RE = - /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; - var CANVAS_BG_LIGHT = '#f0eee6'; - var CANVAS_BG_DARK = '#2e2c26'; - function createHelmetManager(doc, isStreaming) { - const mounted = /* @__PURE__ */ new Set(); - const live = /* @__PURE__ */ new Map(); - let designDocMode = null; - let canvasStyleEl = null; - let appTheme = 'light'; - try { - const ds = doc.documentElement.dataset.theme; - appTheme = - ds === 'dark' || ds === 'light' - ? ds - : new URLSearchParams(doc.defaultView?.location.search ?? '').get('theme') === 'dark' - ? 'dark' - : 'light'; - } catch {} - function applyCanvasBg() { - if (!canvasStyleEl) return; - const bg = appTheme === 'dark' ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; - canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; - } - function postDesignMode(mode) { - if (window.parent === window) return; - try { - window.parent.postMessage({ type: '__dc_design_mode', mode }, '*'); - } catch {} - } - function setDesignDocMode(mode) { - if (mode === designDocMode) return; - designDocMode = mode; - postDesignMode(mode); - if (mode === 'canvas') { - doc.documentElement.setAttribute('data-dc-canvas', ''); - canvasStyleEl = doc.createElement('style'); - canvasStyleEl.setAttribute('data-dc-canvas', ''); - applyCanvasBg(); - doc.head.appendChild(canvasStyleEl); - } else { - doc.documentElement.removeAttribute('data-dc-canvas'); - canvasStyleEl?.remove(); - canvasStyleEl = null; - } - } - window.addEventListener('message', (e) => { - const type = e.data && e.data.type; - if (type === '__dc_theme') { - const t = e.data.theme; - if (t === 'light' || t === 'dark') { - appTheme = t; - applyCanvasBg(); - } - return; - } - if (!designDocMode || type !== '__dc_probe') return; - postDesignMode(designDocMode); - }); - function compile(node) { - const raw = [...node.children]; - const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; - if (node.hasAttribute('data-dc-atomics') && !mounted.has('__dc-atomics')) { - mounted.add('__dc-atomics'); - const el = doc.createElement('style'); - el.id = '__dc-atomics'; - el.textContent = ATOMIC_CSS; - doc.head.appendChild(el); - } - return (_vals, ctx) => { - const name = (ctx && ctx.__name) || ''; - const streaming = !!(name && isStreaming(name)); - for (let i = 0; i < raw.length; i++) { - const child = raw[i]; - const tag = child.tagName; - const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; - if (tag === 'SCRIPT') { - if (mayBePartial) continue; - const key = 'SCRIPT|' + (child.getAttribute('src') || child.textContent || ''); - if (mounted.has(key)) continue; - mounted.add(key); - const el = doc.createElement('script'); - for (const { name: an, value } of [...child.attributes]) el.setAttribute(an, value); - if (child.textContent) el.textContent = child.textContent; - doc.head.appendChild(el); - } else if (tag === 'LINK' || tag === 'META') { - if (mayBePartial) continue; - const key = - tag + - '|' + - (child.getAttribute('href') || child.getAttribute('src') || child.outerHTML); - if (mounted.has(key)) continue; - mounted.add(key); - if (tag === 'LINK') { - const rel = (child.getAttribute('rel') || '').toLowerCase().split(/\s+/); - const href = (child.getAttribute('href') || '').trim(); - const res = window.__resources; - const pre = - res && rel.includes('stylesheet') && !rel.includes('alternate') - ? res[href] - : void 0; - const blob = typeof pre === 'string' && pre ? bundledBlob(pre) : null; - if (blob) { - const el = doc.createElement('style'); - if (child.hasAttribute('disabled')) { - el.setAttribute('media', 'not all'); - } else if (child.getAttribute('media')) { - el.setAttribute('media', child.getAttribute('media')); - } - if (child.getAttribute('title')) - el.setAttribute('title', child.getAttribute('title')); - void blob.text().then((css) => { - el.textContent = css; - }); - doc.head.appendChild(el); - continue; - } - } - doc.head.appendChild(child.cloneNode(true)); - } else { - const key = name + '|' + i; - let el = live.get(key); - if (!el || el.tagName !== tag) { - if (el) el.remove(); - el = doc.createElement(tag.toLowerCase()); - live.set(key, el); - doc.head.appendChild(el); - } - for (const { name: an, value } of [...child.attributes]) { - if (el.getAttribute(an) !== value) el.setAttribute(an, value); - } - if (el.textContent !== child.textContent) el.textContent = child.textContent; - } - } - return null; - }; - } - return { compile, setDesignDocMode }; - } + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + if (tag === "LINK") { + const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); + const href = (child.getAttribute("href") || "").trim(); + const res = window.__resources; + const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; + const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; + if (blob) { + const el = doc.createElement("style"); + if (child.hasAttribute("disabled")) { + el.setAttribute("media", "not all"); + } else if (child.getAttribute("media")) { + el.setAttribute("media", child.getAttribute("media")); + } + if (child.getAttribute("title")) + el.setAttribute("title", child.getAttribute("title")); + void blob.text().then((css) => { + el.textContent = css; + }); + doc.head.appendChild(el); + continue; + } + } + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } - // src/pseudo.ts - function scanUnquotedUrl(css, i) { - if ( - (css[i] !== 'u' && css[i] !== 'U') || - css.slice(i, i + 4).toLowerCase() !== 'url(' || - /[a-z0-9_-]/i.test(css[i - 1] ?? '') - ) { - return -1; - } - let j = i + 4; - while (j < css.length && /\s/.test(css[j])) j++; - if (css[j] === '"' || css[j] === "'") return -1; - while (j < css.length && css[j] !== ')') { - if (css[j] === '\\') j++; - j++; - } - return j < css.length ? j + 1 : css.length; - } - function stripComments(css) { - let out = ''; - let quote = ''; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === '\\') { - out += c + (css[i + 1] ?? ''); - i++; - continue; - } - if (c === quote) quote = ''; - out += c; - } else if (c === "'" || c === '"') { - quote = c; - out += c; - } else if (c === '/' && css[i + 1] === '*') { - const end = css.indexOf('*/', i + 2); - i = end === -1 ? css.length : end + 1; - out += ' '; - } else { - const end = scanUnquotedUrl(css, i); - if (end === -1) out += c; - else { - out += css.slice(i, end); - i = end - 1; - } - } - } - return out; - } - function importantify(css) { - css = stripComments(css); - const decls = []; - let start = 0; - let depth = 0; - let quote = ''; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === '\\') i++; - else if (c === quote) quote = ''; - } else if (c === "'" || c === '"') quote = c; - else if (c === '(') depth++; - else if (c === ')') depth = Math.max(0, depth - 1); - else if (c === ';' && depth === 0) { - decls.push(css.slice(start, i)); - start = i + 1; - } else { - const end = scanUnquotedUrl(css, i); - if (end !== -1) i = end - 1; - } - } - decls.push(css.slice(start)); - return decls - .map((d) => d.trim()) - .filter(Boolean) - .map((d) => (/!\s*important$/i.test(d) ? d : d + ' !important')) - .join(';'); - } - function createPseudoSheet(doc) { - let el = null; - const cache = /* @__PURE__ */ new Map(); - let n = 0; - return (pseudo, css) => { - const k = pseudo + '|' + css; - const hit = cache.get(k); - if (hit) return hit; - if (!el) { - el = doc.createElement('style'); - doc.head.appendChild(el); - } - const cls = 'scp' + (n++).toString(36); - const isPseudoElement = pseudo === 'before' || pseudo === 'after'; - const sel = isPseudoElement ? '.' + cls + '::' + pseudo : '.' + cls + ':' + pseudo; - el.sheet.insertRule( - sel + '{' + (isPseudoElement ? css : importantify(css)) + '}', - el.sheet.cssRules.length - ); - cache.set(k, cls); - return cls; - }; - } + // src/pseudo.ts + function scanUnquotedUrl(css, i) { + if (css[i] !== "u" && css[i] !== "U" || css.slice(i, i + 4).toLowerCase() !== "url(" || /[a-z0-9_-]/i.test(css[i - 1] ?? "")) { + return -1; + } + let j = i + 4; + while (j < css.length && /\s/.test(css[j])) j++; + if (css[j] === '"' || css[j] === "'") return -1; + while (j < css.length && css[j] !== ")") { + if (css[j] === "\\") j++; + j++; + } + return j < css.length ? j + 1 : css.length; + } + function stripComments(css) { + let out = ""; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") { + out += c + (css[i + 1] ?? ""); + i++; + continue; + } + if (c === quote) quote = ""; + out += c; + } else if (c === "'" || c === '"') { + quote = c; + out += c; + } else if (c === "/" && css[i + 1] === "*") { + const end = css.indexOf("*/", i + 2); + i = end === -1 ? css.length : end + 1; + out += " "; + } else { + const end = scanUnquotedUrl(css, i); + if (end === -1) out += c; + else { + out += css.slice(i, end); + i = end - 1; + } + } + } + return out; + } + function importantify(css) { + css = stripComments(css); + const decls = []; + let start = 0; + let depth = 0; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") i++; + else if (c === quote) quote = ""; + } else if (c === "'" || c === '"') quote = c; + else if (c === "(") depth++; + else if (c === ")") depth = Math.max(0, depth - 1); + else if (c === ";" && depth === 0) { + decls.push(css.slice(start, i)); + start = i + 1; + } else { + const end = scanUnquotedUrl(css, i); + if (end !== -1) i = end - 1; + } + } + decls.push(css.slice(start)); + return decls.map((d) => d.trim()).filter(Boolean).map((d) => /!\s*important$/i.test(d) ? d : d + " !important").join(";"); + } + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const isPseudoElement = pseudo === "before" || pseudo === "after"; + const sel = isPseudoElement ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule( + sel + "{" + (isPseudoElement ? css : importantify(css)) + "}", + el.sheet.cssRules.length + ); + cache.set(k, cls); + return cls; + }; + } - // src/registry.ts - function createRegistry() { - const entries = /* @__PURE__ */ Object.create(null); - function get(name) { - return ( - entries[name] || - (entries[name] = { - html: '', - tpl: null, - Logic: null, - jsStreaming: false, - htmlStreaming: false, - ver: 0, - subs: /* @__PURE__ */ new Set(), - fetched: false - }) - ); - } - function bump(name) { - const r = get(name); - r.ver++; - for (const fn of r.subs) fn(); - } - return { - entries, - get, - bump, - bumpAll() { - for (const n in entries) bump(n); - } - }; - } + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } - // src/runtime.ts - var COMPONENT_DIR = '.'; - function createRuntime(doc = document) { - const registry = createRegistry(); - const pseudoClass = createPseudoSheet(doc); - const helmet = createHelmetManager(doc, (name) => registry.get(name).htmlStreaming); - const external = createExternalModules(() => registry.bumpAll()); - const factory = createComponentFactory(registry, ensureFetched); - const host = { - component: (name) => factory.getDC(name), - placeholder: (props) => h(Placeholder, props), - helmet: (node) => helmet.compile(node), - loadExternal: (kind, url, after) => external.load(kind, url, after), - resolveExternal: (url, name) => external.resolve(url, name), - resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), - resolveExternalError: (url, name) => external.getError(url, name), - pseudoClass - }; - function ensureFetched(name) { - const r = registry.get(name); - if (r.fetched) return; - r.fetched = true; - const url = COMPONENT_DIR + '/' + encodeURIComponent(name) + '.dc.html'; - const res = window.__resources; - const pre = res ? res[url] : void 0; - const target = typeof pre === 'string' && pre ? pre : url; - const blob = bundledBlob(target); - (blob - ? blob.text() - : fetch(target).then((res2) => { - if (!res2.ok) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '" failed:', - url, - 'returned', - res2.status, - '\u2014 the reference renders as an empty placeholder.' - ); - return ''; - } - return res2.text(); - }) - ) - .then((t) => { - if (!t) return; - const parsed = parseDcText(t); - if (!parsed) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '":', - url, - 'has no block \u2014 not a Design Component.' - ); - return; - } - if (parsed.props) r.propsMeta = parsed.props; - if (parsed.preview) r.preview = parsed.preview; - if (parsed.template && !r.html) updateHtml(name, parsed.template); - if (parsed.js && !r.Logic) updateJs(name, parsed.js); - }) - .catch((e) => - console.error('[dc-runtime] sibling fetch for "' + name + '" threw:', url, e) - ); - } - let rootName = null; - function updateHtml(name, html) { - const r = registry.get(name); - r.html = html; - if (name === rootName) { - const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; - if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); - } - try { - r.tpl = compileTemplate(html, host); - } catch (e) { - console.error('[dc-runtime] template compile FAILED for', name, e); - } - registry.bump(name); - } - function updateJs(name, src) { - const r = registry.get(name); - const seq = (r.jsSeq = (r.jsSeq || 0) + 1); - try { - const Cls = evalDcLogic(src); - if (r.jsSeq !== seq) return; - if (typeof Cls !== 'function') { - r.logicError = - name + - '.dc.html:
-
- - - Musify - + + + Musify + -
- {#if user} - - - {#if menuOpen} - - (menuOpen = false)} - /> - (menuOpen = false)} - /> - (menuOpen = false)} - /> - {#if accountUrl} - (menuOpen = false)} - /> - {/if} - themeMode.toggle()} - /> -
- - -
+
+ {#if user} +
-
+ - + {#if menuOpen} + + (menuOpen = false)} + /> + (menuOpen = false)} + /> + (menuOpen = false)} + /> + {#if accountUrl} + (menuOpen = false)} + /> + {/if} + themeMode.toggle()} + /> +
+ + +
+ {/if} + {:else} + + + + {/if} +
diff --git a/web-player/src/lib/components/SearchBox.svelte b/web-player/src/lib/components/SearchBox.svelte deleted file mode 100644 index 551566e..0000000 --- a/web-player/src/lib/components/SearchBox.svelte +++ /dev/null @@ -1,86 +0,0 @@ - - -
-
- -
-
diff --git a/web-player/src/lib/components/TopBar.svelte b/web-player/src/lib/components/TopBar.svelte index ed66c21..c59632e 100644 --- a/web-player/src/lib/components/TopBar.svelte +++ b/web-player/src/lib/components/TopBar.svelte @@ -1,5 +1,10 @@ + + + +{#snippet content()} + {@render art(artClass)} +
+
+ {#if explicit} + + {/if} + {title} +
+ {#if subtitle} +
{subtitle}
+ {/if} +
+ {#if meta} + {meta} + {/if} +{/snippet} + +{#if href} + + {@render content()} + +{:else} + +{/if} diff --git a/web-player/src/lib/components/ui/SectionHeading.svelte b/web-player/src/lib/components/ui/SectionHeading.svelte index d573cf8..d161890 100644 --- a/web-player/src/lib/components/ui/SectionHeading.svelte +++ b/web-player/src/lib/components/ui/SectionHeading.svelte @@ -12,7 +12,7 @@ {#snippet heading()} -

+

{title}

{#if subtitle} diff --git a/web-player/src/routes/(app)/explore/+page.svelte b/web-player/src/routes/(app)/explore/+page.svelte index 78e98da..ab08ff2 100644 --- a/web-player/src/routes/(app)/explore/+page.svelte +++ b/web-player/src/routes/(app)/explore/+page.svelte @@ -1,20 +1,18 @@