diff --git a/rediseno-funcional-de-aplicacion/README.md b/rediseno-funcional-de-aplicacion/README.md new file mode 100644 index 0000000..5f73df2 --- /dev/null +++ b/rediseno-funcional-de-aplicacion/README.md @@ -0,0 +1,22 @@ +# CODING AGENTS: READ THIS FIRST + +This is a **handoff bundle** from Claude Design (claude.ai/design). + +A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real. + +## What you should do — IMPORTANT + +**Read `redise-o-funcional-de-aplicaci-n/project/PluriWave Rediseno.dc.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing. + +**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing. + +## About the design files + +The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit. + +**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't. + +## Bundle contents + +- `redise-o-funcional-de-aplicaci-n/README.md` — this file +- `redise-o-funcional-de-aplicaci-n/project/` — the `Rediseño funcional de aplicación` project files (HTML prototypes, assets, components) diff --git a/rediseno-funcional-de-aplicacion/project/.thumbnail b/rediseno-funcional-de-aplicacion/project/.thumbnail new file mode 100644 index 0000000..9a4cd85 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/.thumbnail differ diff --git a/rediseno-funcional-de-aplicacion/project/PluriWave Rediseno.dc.html b/rediseno-funcional-de-aplicacion/project/PluriWave Rediseno.dc.html new file mode 100644 index 0000000..025ca4f --- /dev/null +++ b/rediseno-funcional-de-aplicacion/project/PluriWave Rediseno.dc.html @@ -0,0 +1,2233 @@ + + + + + + + + + + + + + + + +
+
4Todas las pantallas en el formato de 3a · móvil + Android Auto
+ +
+
Reglas del sistema, aplicadas a todo. Cuatro raíces con barra globo (Escuchar · Explorar · Alarmas · Ajustes); las pantallas de segundo nivel entran empujadas con cabecera de 56 px, flecha atrás y sin barra de pestañas; lo modal sube como hoja sobre un velo. Superficie de lista opaca #102532, cristal solo en el cromo y en la tarjeta activa. Miniatura de emisora 44–68 px, arte grande solo en reproductor y coche. Android Auto reproduce el árbol real de ConstructorArbolAuto: Favoritos · Todas las emisoras · Mis emisoras · Música Local, sin carpeta de ecualizador. La barra de 3a sigue siendo la navegable; aquí va estática en su pestaña. Ecualización en dos niveles: el base vive en Ajustes y cubre todas las emisoras; el de una emisora concreta se define desde su propia reproducción (bandeja de herramientas) y manda sobre el base, con «Volver al base» siempre a mano.
+
+ +
+
+
4aMóvil · raíces y reproductor
+
+ +
+
1 · Escuchar (raíz)
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
ESCUCHANDO AHORA
cast
more_horiz
+
+
+
+
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
bedtime
+
+
+
expand_lessEcualizador, grabar, calidad
+
+
Tus emisoras8Ver todas
+
+
+ +
+
+
{{ s.n }}
{{ s.g }}
+
+
+
+
+
+
+
+
+
+
graphic_eqEscuchar
+
travel_explore
+
alarm
+
settings
+
+
+
+
+ +
+
2 · Reproductor completo (empujado)
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
keyboard_arrow_down
EN DIRECTO
cast
+
+
+
+
Radio Horizonte
+
+
Noticias · España · Español
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
volume_up
+
+
+
+
equalizerEQ propio
+
micGrabar
+
bedtime30 min
+
ios_shareCompartir
+
+
+
hdMP3 · 128 kbps · conexión estableCambiar
+
+
+
+ +
+
3 · Explorar (raíz)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
searchEmisora, país, género…mic
+
+
Cerca de ti · España
+
+
+ +
+
play_arrow
+
{{ s.n }}
{{ s.g }} · {{ s.kbps }} kbps
+
+
+
+
+
Explorar por
+
+
+
public
Países
238
+
library_music
Géneros
64
+
trending_up
Tendencias
Hoy
+
fiber_new
Novedades
Esta semana
+
+
+
Populares ahora
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.p }}
+
favorite
+
+
+
+
+
+
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+
+
+
+
graphic_eq
+
travel_exploreExplorar
+
alarm
+
settings
+
+
+
+
+ +
+
+
+
4bMóvil · segundo nivel de Explorar y toda la rama de alarmas
+
+ +
+
4 · Tus emisoras · grupos (empujada)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
arrow_back
Tus emisoras
create_new_folder
swap_vert
+
+
+
Todas · 8
+
Cada mañana · 4
+
Coche · 3
+
+
+
+ +
+ drag_indicator +
+
{{ s.n }}
{{ s.g }} · {{ s.p }}
+
play_arrow
+
+
+
+
+
addAñadir emisora personalizada
+
+
+
+
Radio Horizonte
En directo
+
pause_circle
+
keyboard_arrow_up
+
+
+
+ +
+
5 · Explorar › Países (empujada)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
arrow_back
Países
search
+
+
TUS IDIOMAS
+
+
ES
España
2.184 emisoras
chevron_right
+
MX
México
1.902 emisoras
chevron_right
+
AR
Argentina
1.140 emisoras
chevron_right
+
+
+
TODOS · 238
+
+
DE
Alemania
3.421 emisoras
chevron_right
+
BR
Brasil
2.760 emisoras
chevron_right
+
US
Estados Unidos
8.045 emisoras
chevron_right
+
FR
Francia
1.688 emisoras
chevron_right
+
IT
Italia
1.412 emisoras
chevron_right
+
JP
Japón
604 emisoras
chevron_right
+
GB
Reino Unido
2.058 emisoras
chevron_right
+
+
+
+
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+ +
+
6 · Búsqueda con resultados y filtros activos
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
arrow_back
+
jazzclose
+
+
+
+
Españaclose
+
≥128 kbpsclose
+
Idioma
+
Ordenar
+
+
+
37 RESULTADOS
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.p }} · {{ s.kbps }} kbps
+
favorite
+
play_arrow
+
+
+
+
+
+
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+ +
+
7 · Alarmas (raíz)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
Alarmas
addNueva
+
+ alarm_on +
Suena en 9 h 12 min
Cada mañana · mañana a las 07:30
+
Saltar
+
+
+
beach_access
Vacaciones
2 rangos · el próximo empieza en 1 día
4–18 AGOchevron_right
+
+
+ +
+
+
+
{{ a.hora }}{{ a.dias }}
+
+
{{ a.nombre }}
+
+
+
+
+
+
+
+
+
+
+
+
+
graphic_eq
+
travel_explore
+
alarmAlarmas
+
settings
+
+
+
+
+ +
+
8 · Editor de alarma (hoja)
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
Alarmas
+
+
+
+
+
+
+
Nueva alarmaGuardar
+
+
+ 07:30 +
+
+
REPETIR
+
+
L
+
M
+
X
+
J
+
V
+
S
+
D
+
+
+
+
Radio Horizonte
Suena al despertar
chevron_right
+
+
volume_upVolumen
80%
+
+
trending_upSubida progresiva30 schevron_right
+
+
beach_accessSonar en vacaciones
+
+
+
+
+ +
+
9 · Alarma sonando (pantalla completa)
+
+
+
+
+
7:30
signal_cellular_altwifibattery_full
+
+
+
alarmCADA MAÑANA
+
+
7:30
+
+
Lunes, 3 de agosto
+
+
+
+
Radio Horizonte
+
+
Subiendo volumen · 18 s
+
+
+
snoozePOSPONER
+
+
3min
+
5min
+
10min · habitual
+
+
stop_circleParar
+
+
+
+
+ +
+
9 bis · Alarmas › Vacaciones (gestor de rangos)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
arrow_back
Vacaciones
addAñadir
+
+
info
Durante estos rangos no suenan las alarmas marcadas como «pausar en vacaciones». Las marcadas como «sonar siempre» no se ven afectadas.
+
+
EN CURSO
+
+
+ + ACTIVO AHORA · QUEDAN 3 DÍAS + more_vert +
+
+
+
28 jul
lunes
+
+
31 jul
jueves
+
+
+
alarm_offPausa 07:30 y 13:45 · 08:15 sigue
+
+
+
PROGRAMADOS
+
+
+
EMPIEZA EN 1 DÍA · 15 DÍASmore_vert
+
+
+
4 ago
lunes
+
+
18 ago
lunes
+
+
+
labelVerano
+
+
+
EN 4 MESES · 3 DÍASmore_vert
+
+
+
6 dic
sábado
+
+
8 dic
lunes
+
+
+
labelPuente
+
+
+
+
date_rangeAñadir rango de vacaciones
+
+
historyRangos pasados5chevron_right
+
+
+
+ +
+
+
+
4cMóvil · ajustes, detalles y estados
+
+ +
+
10 · Ajustes (raíz) · 12 paneles → 4 grupos
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
AjustesPRO · 11 meses
+
AUDIO
+
+
equalizer
Ecualizador base
6 emisoras con ajuste propio
Voz clarachevron_right
+
+
hdCalidad de streamingAltachevron_right
+
+
bluetooth_audioPreset por dispositivo3 guardadoschevron_right
+
+
all_inclusiveReproducción sin interrupciones
+
+
+
EMISORAS
+
+
folderGrupos de favoritos3chevron_right
+
+
sortOrden de las listasAlfabéticochevron_right
+
+
starEmisora preferidaRadio Horizontechevron_right
+
+
radioEmisoras personalizadas2chevron_right
+
+
+
GRABACIONES Y MÚSICA
+
+
folder_openGrabaciones7 · 84 MBchevron_right
+
+
library_musicMúsica local (Auto)Music/chevron_right
+
+
+
APLICACIÓN
+
+
languageIdiomaEspañolchevron_right
+
+
cloud_doneCopia de seguridadHoy, 08:12chevron_right
+
+
+
+
+
+
+
+
graphic_eq
+
travel_explore
+
alarm
+
settingsAjustes
+
+
+
+
+ +
+
11 · Ajustes › Ecualizador (empujada)
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
arrow_back
Ecualizador base
+
+
info
Este es el ecualizador general: se aplica a todas las emisoras que no tengan ajuste propio. El de una emisora concreta se define desde su reproducción y manda sobre este.
+
+
+
Voz clara
+
Plano
+
Graves +
+
Rock
+
+
+
+
+ +
+ {{ b.db }} +
+ {{ b.hz }} +
+
+
+
+
+
+
bluetooth_audioSalida activaCoche · Bluetooth
+
+
saveGuardar como presetchevron_right
+
+
restart_altRestablecer a plano
+
+
+
+
tune
Emisoras con ajuste propio
Ignoran este ecualizador base
6chevron_right
+
+
+
+
+ +
+
12 · Ajustes › Grabaciones (empujada)
+
+
+
9:41
signal_cellular_altwifibattery_full
+
arrow_back
Grabaciones
folder_open
+
+
+
84 MB de 200 MB
Music/PluriWave · se borran las más antiguas al llegar al límite
+
+
+
+
Radio Horizonte
Hoy 08:04 · 14:32 · 18,2 MB
play_circle
more_vert
+
Los 40 Classic
Ayer 19:15 · 05:00 · 6,4 MB
play_circle
more_vert
+
Onda Clásica
1 ago · 30:00 · 41,0 MB
play_circle
more_vert
+
Ibiza Global
28 jul · 01:00 · 2,3 MB
play_circle
more_vert
+
+
+
+
drive_folder_uploadCambiar carpetachevron_right
+
+
saveTamaño máximo200 MBchevron_right
+
+
+
+
+ +
+
13 · Estados: sin conexión, cargando, reconectando
+
+
+
9:41
signal_cellular_offwifi_offbattery_full
+
+
Explorar
+
wifi_off
Sin conexión
Mostrando tus favoritas guardadas
Reintentar
+
+
+
radio
+
Radio Horizonte
Reconectando · intento 2 de 5
+
stop_circle
+
+
+
BUSCANDO EMISORAS…
+
+
+ +
+
+
+
+
+
+
+
+ search_off +
+
Sin resultados para «jazzz»
+
+
Prueba con menos filtros o revisa la ortografía.
+
+
filter_alt_offQuitar los 2 filtros
+
+
+
+
+
+
+
+
graphic_eq
+
travel_exploreExplorar
+
alarm
+
settings
+
+
+
+
+ +
+
14 · Bienvenida y PRO (14 días)
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
+ +
+
Tu mundo,
en directo
+
+
53.412 emisoras de 238 países. Guarda tus favoritas, ponlas en el coche y despiértate con ellas.
+
+
+
equalizer
Ecualizador por emisora
Y un preset por dispositivo de salida
+
directions_car
Android Auto
Tus favoritas y tu música local en el coche
+
alarm
Alarmas musicales
Con subida progresiva y modo vacaciones
+
+
+
14 días PRO gratisluego 2,99 €/año
Sin anuncios, ecualizador y reconocimiento ilimitado. Se puede cancelar cuando quieras.
+
+
Empezar a escuchar
+
+
Seguir con la versión gratuita
+
+
+
+ +
+
+
+
4dAndroid Auto · 1360×720 · el árbol real de ConstructorArbolAuto: Favoritos · Todas las emisoras · Mis emisoras · Música Local. Filas de 96 px, texto desde 22 px, dos niveles como máximo y ninguna carpeta de ecualizador (el preset por dispositivo se aplica solo al conectar el coche).
+
+ +
+
A1 · Raíz
+
+
+
+ + PluriWave +
search
+
+
+
favorite
Favoritos
8 emisoras · 4 grupos
chevron_right
+
public
Todas las emisoras
Las 50 más escuchadas cerca de ti
chevron_right
+
radio
Mis emisoras
2 emisoras personalizadas
chevron_right
+
library_music
Música Local
Music/ · 12 carpetas
chevron_right
+
+
+
+
Radio Horizonte
En directo · 128 kbps
+
favorite
+
pause
+
+
+
+ +
+
A2 · Favoritos › grupos
+
+
+ +
+
favorite
+
public
+
radio
+
library_music
+
+
+
arrow_back
Favoritos
+
+
+ +
+
{{ f.ico }}
+
{{ f.n }}
{{ f.c }}
+ chevron_right +
+
+
+
+
+
+
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
+
+
+
+ +
+
A3 · Todas las emisoras · rejilla (CONTENT_STYLE_PLAYABLE_HINT = 2)
+
+
+
arrow_back
+ Todas las emisoras + 50 máx. · España +
+
+
+ +
+
+
{{ s.n }}
+
{{ s.kbps }} kbps
+
+
+
+
+
+
+
Radio Horizonte
En directo · 128 kbps
+
favorite
+
pause
+
+
+
+ +
+
A4 · Ahora suena (plantilla de reproducción)
+
+
+
+
+
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · MP3 128 kbps
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
queue_music
+
+
+
+
+ +
+
A5 · Música Local › carpeta · acciones de reproducción y paginación
+
+
+
arrow_back
+ Música Local › Podcasts + 1–50 de 214 +
+
+
+
play_arrow
Reproducir carpeta
214 pistas en orden
+
shuffle
Reproducir aleatorio
Mezcla toda la carpeta
+
sort_by_alpha
+
+
+
+
01
Amanecer en el puerto · capítulo 12
42:18 · 192 kbps · 48 kHz
play_arrow
+
02
Ondas cortas · especial verano
28:04 · 160 kbps · 44,1 kHz
play_arrow
+
03
Voces del norte · entrevista
55:41 · 128 kbps · 44,1 kHz
play_arrow
+
expand_moreCargar 50 más
+
+
+
+
audio_file
+
Amanecer en el puerto · capítulo 12
14:22 / 42:18
+
skip_previous
+
pause
+
skip_next
+
+
+
+ +
+
+
+ +

Set completo: 15 pantallas de móvil (4a · 4b · 4c) y 5 de Android Auto (4d). Siguientes pasos: "haz navegable también 4b" · "añade Géneros y Tendencias" · "diseña el widget y la notificación" · "prepara el handoff para desarrollo".

+ +
+ +
+
3Comprado: 1c + barra globo de 2a · prototipo navegable
+ +
+
Navega de verdad: pincha las cuatro pestañas y la burbuja se desplaza cambiando de pantalla; el chip «Ecualizador, grabar, calidad» abre la bandeja de herramientas y el fondo la cierra. Con cuatro pestañas la celda pasa de 74 a 92 px, así que la burbuja respira más y las etiquetas caben sin recortarse. Ajustes era el pendiente: sus 12 paneles apilados (_AjustesContent) pasan a cuatro grupos de filas con el valor actual a la derecha, que es lo que se viene a comprobar.
+
+ +
+
+
3aEscuchar · Explorar · Alarmas · Ajustes, con la barra globo de 4 celdas y la bandeja de herramientas.
+
+
+
+ +
9:41
signal_cellular_altwifibattery_full
+ +
+ +
+
+
+
+
ESCUCHANDO AHORA
cast
more_horiz
+
+
+
+
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
bedtime
+
+
+
expand_lessEcualizador, grabar, calidad
+
+
+
Tus emisoras8swap_vert
+
+
+ +
+
+
{{ s.n }}
{{ s.g }}
+
+
+
+
+
+ +
+
searchEmisora, país, género…mic
+
+
Cerca de ti · España
+
+
+ +
+
play_arrow
+
{{ s.n }}
{{ s.g }} · {{ s.kbps }} kbps
+
+
+
+
+
Explorar por
+
+
+
public
Países
238
+
library_music
Géneros
64
+
trending_up
Tendencias
Hoy
+
fiber_new
Novedades
Esta semana
+
+
+
Populares ahora
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.p }}
+
favorite
+
+
+
+
+ +
+
Alarmas
addNueva
+
+ alarm_on +
Suena en 9 h 12 min
Cada mañana · mañana a las 07:30
+
Saltar
+
+
+
+ +
+
+
+
{{ a.hora }}{{ a.dias }}
+
+
{{ a.nombre }}
+
+
+
+
+
+
+
+ +
+
AjustesPRO · 11 meses
+ +
AUDIO
+
+
equalizer
Ecualizador base
6 emisoras con ajuste propio
Voz clarachevron_right
+
+
hdCalidad de streamingAltachevron_right
+
+
all_inclusiveReproducción sin interrupciones
+
+ +
+
EMISORAS
+
+
folderGrupos de favoritos3chevron_right
+
+
sortOrden de las listasAlfabéticochevron_right
+
+
radioEmisoras personalizadas2chevron_right
+
+ +
+
GRABACIONES
+
+
folder_openCarpetaMusic/PluriWavechevron_right
+
+
saveTamaño máximo200 MBchevron_right
+
+ +
+
APLICACIÓN
+
+
languageIdiomaEspañolchevron_right
+
+
cloud_doneCopia de seguridadHoy, 08:12chevron_right
+
+
+
+ +
+ +
+
+
+
+
HerramientasPRO activo
+
+
+
Ecualizador de esta emisoraPROPIO
Solo para Radio Horizonte · el base vive en Ajustes
+
+
+ +
+
+ {{ b.hz }} +
+
+
+
+
undoVolver al base
tunePresets
+
+
+
+
mic
Grabar
Stream original
+
bedtime
28:44
Apagado
+
hd
128 kbps
Calidad
+
alarm_add
Alarma
Con esta emisora
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ {{ it.icon }} + {{ it.label }} +
+
+
+
+
+
+
+
+ +

Pincha las pestañas y el chip de herramientas. Siguientes pasos posibles: "añade la pantalla de país/género de Explorar" · "haz la versión de Android Auto con esta jerarquía" · "monta el editor de alarma".

+
+ +
+
2Barra de pestañas «globo» · sobre 1a
+ +
+
Interactivo: pincha las pestañas para ver moverse la burbuja. La barra plana y la burbuja comparten un mismo color, así que la silueta se lee como un único globo alargado que se infla justo donde está el foco; el contorno sale de una sombra proyectada sobre la unión de las dos formas, no de un borde, para que no aparezca costura. Al cambiar de pestaña la burbuja se desliza con una curva ligeramente elástica: la etiqueta anterior se cierra en vertical y la nueva se abre. Altura de la barra: 74 px → 52 px (el abultamiento se sale por encima y flota sobre el contenido, así que no ocupa maquetación).
+
+ +
+ +
+
2aGlobo vertical — la burbuja crece hacia arriba y saca la etiqueta. Iconos legibles a 24 px en vez de las ilustraciones de 512 px.
+
+
+
+
+ +
9:41
signal_cellular_altwifibattery_full
+
Inicio
search
bedtime
+ +
+
+ +
+
+ +
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+ +
+
+
pause
+
+
+ +
+
Tus favoritasVer todas
+
+
+ +
+
play_arrow
+
{{ s.n }}
+
+
+
+ +
+
+
Todos
+
{{ g }}
+
+ +
+
Populares en España53.412
+
+
+ +
+
+
{{ s.n }}
+
{{ s.kbps }} kbps
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ {{ it.icon }} + {{ it.label }} +
+
+
+
+
+
+ +
+
Detalle a 2× · misma barra, mismo estado
+
+
+
+
+
+
+
+
+
+ +
+
+ {{ it.icon }} + {{ it.label }} +
+
+
+
+
+
+
La burbuja mide 1,19 veces el ancho de una celda, así que se ensancha un poco más allá de su pestaña: es lo que hace que la deformación parezca elástica y no un simple rectángulo que se mueve. En Flutter esto es un AnimatedPositioned para la burbuja + AnimatedSlide y AnimatedSize por pestaña, con Curves.easeOutBack.
+
+
+
+ +
+ +

Pruébala pinchando las pestañas. Luego puedes pedirme: "aplica la barra globo a 1b y 1c" · "hazla aún más baja" · "que la burbuja rebote un poco más".

+
+ +
+
1Diagnóstico y primera ronda
+ +
+
+
Supuestos de partida
+
+
Leo el código de pluriwave/lib, no de memoria: paleta y radios salen de pluriwave_tokens.dart, la tipografía es Plus Jakarta Sans, y el arte de emisora son los cuatro PNG reales de assets/images. Mantengo teal #21D4D9, coral #F4B860, menta #7EE4C2 y el fondo aurora. Prioridad de uso que asumo, según tu respuesta: volver a lo que ya escuchas > descubrir > alarmas. Y todo lo que decida tiene que sobrevivir a Android Auto: objetivos grandes, favoritos primero, jerarquía de dos niveles.
+
+ +
+
Qué mide mal hoy · medido en el código, no a ojo
+
+
+
1 · La cabecera cuesta 171 px, cinco veces
PluriScreenHeader repite orbe de 56 px, banner, marca de agua, dos orbes decorativos y dos pastillas de estado en Inicio, Buscar, Favoritos y Alarmas. Ninguno de esos píxeles es accionable.
+
2 · En Inicio se ven 0 emisoras completas
Cabecera + Cerca de ti + Radar + Géneros = 583 px. Con bottomChromeInset: 146 y childAspectRatio: 0.78, del grid solo asoma una franja.
+
3 · Favoritos es el 80% del uso y la 3.ª pestaña
Cada fila mide ~100 px: anillo SweepGradient de 58 px, dos IconButton apilados y un contador. Entran 5 favoritas por pantalla.
+
4 · Buscar pide 330 px antes del primer resultado
Tres paneles de filtros (país, idioma, calidad) siempre desplegados, cada uno en su propia tarjeta de cristal, encima de los resultados.
+
5 · El reproductor: 62% de adorno, 3 controles
El círculo ocupa width*0.62+40. Ecualizador y favorito viven escondidos en la AppBar; el temporizador es un TextButton al fondo; grabar ocupa una fila entera.
+
6 · Iconos de 512 px dibujados a 34 px
Las pestañas usan ilustraciones detalladas (pluri_home.png y compañía). A tamaño de pestaña son manchas azules indistinguibles, y solo la activa lleva etiqueta.
+
+
+ +
+
Sistema que propongo
+
+
+
Cristal con presupuesto
Un solo elemento translúcido por pantalla, y siempre el cromo (nav + mini-reproductor). Las listas pasan a superficie opaca #102532: mismo color, cero coste de lectura.
+
La imagen manda, el adorno no
El arte se queda grande donde se mira (reproductor, tarjeta de la emisora activa) y se convierte en miniatura nítida de 56–64 px en listas. Lo que quito son orbes, marcas de agua y anillos de degradado.
+
Cabecera de una línea
171 px → 56 px. Título + la única acción real de la pantalla. El nombre de la app no se repite en cada pestaña.
+
Movimiento con función
La animación se reserva para estado: la onda solo late si suena, el anillo de conexión gira solo al conectar. Nada se mueve por decorar.
+
+
+
+ +
+ +
+
1aActual · recreación fiel del código para comparar. Los recortes son reales: el contenido se corta donde se corta hoy.
+
+ +
+
Inicio
+
+
+
+
+
+ +
9:41
signal_cellular_altwifibattery_full
+
PluriWave
bedtime
+ +
+
+
+
+ +
+
+
+
+
+
PluriWave
+
+
Tu mundo en directo, emisora a emisora.
+
+
auto_awesomeExplorar emisoras
+
+
+
public53.412
+
hdCalidad HD
+
+
+
+ +
+
Cerca de ti · España
my_locationDetectar
+
+ +
+
Radar en directo
+
+
+
graphic_eqCadena SER
+
graphic_eqLos 40
+
graphic_eqRock FM
+
+
+ +
+
Géneros
+
+
+
{{ g }}
+
+
+ +
+ +
+
+
EN DIRECTO
+
favorite
+
+
{{ s.n }}
{{ s.p }}
+
+
+
+
+ +
+
+
graphic_eq
+
Radio Horizonte
En directo
+ +
pause_circle
+
+
+
Inicio
+
+
+
+
+
+
+
+
+ +
+
Buscar
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
PluriWave
bedtime
+
+
+
+
+ +
+
+
+
+
Buscar
+
+
Filtra por país, idioma y calidad de emisión.
+
+
tuneFiltros
+
+
+
+
Buscar emisoras, géneros, países…
+
+
+
País
+
España
EE. UU.
México
+
+
+
Idioma
+
Español
Inglés
Francés
+
+
+
Calidad mínima
+
64 kbps
96 kbps
128 kbps
+
+
+ +
+
+
{{ s.n }}
{{ s.sub }}
+ +
favorite
+
+
+
+
+
+
+
graphic_eq
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+
Buscar
+
+
+
+
+
+
+
+ +
+
Favoritos
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
PluriWave
bedtime
+
+
+
+
+ +
+
+
+
+
Favoritos
+
+
Tu colección, agrupada como quieras.
+
+
library_music8
+
+
+
+
folder
Cada mañana
4 emisoras
+
+
+ +
+
+
+
{{ s.n }}
{{ s.sub }}
+
+
+
drive_file_move
delete
+
+
+
+
+
+
+
+
graphic_eq
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+
+
Favoritos
+
+
+
+
+
+
+ +
+
Reproductor
+
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
keyboard_arrow_down
equalizer
favorite
+
+
+
+
+
+
+
+
Radio Horizonte
+
+
+
España
+
Español
+
128 kbps
+
MP3
+
+
+
Calidad original · MP3 · 128 kbps
+
+
+ +
+
+
+
stop
+
pause
+
+
+
+
+ radio_button_checked +
Grabar en directo
Guarda el stream original
+
micGrabar
+
folder_open
+
+
+
bedtimeTemporizador de apagado
+
+
+
+ +
+
Alarmas
+
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
PluriWave
bedtime
+
+
+
+
+ +
+
+
+
+
Alarmas
+
+
Despierta con tu emisora favorita.
+
+
auto_awesomeCrear alarma
+
+
alarm_on3
+
+
+
+ +
Próxima alarma
Cada mañana · mañana, 07:30
+
+
+
+
+ +
07:30
Cada mañana
+
+
+
+
+
repeatL–V
+
beach_accessPausa en vacaciones
+
volume_up80%
+
trending_upFade 30 s
+
+
+
event_availablePróxima ejecución: mañana, 07:30
+
+
editEditar
skip_nextSaltar
+
+
+
+
08:15
Fin de semana
+
+
+
+
+
graphic_eq
+
Radio Horizonte
En directo
+
pause_circle
+
+
+
+
+
+
Alarmas
+
+
+
+
+
+ +
+
+ +
+
1bEvolución — misma piel, la mitad del peso. Cabecera de 56 px, lo que suena arriba y accionable, favoritas a un toque, grid de 3, filtros en una sola barra. El cristal se reserva para el cromo y la tarjeta activa; las listas pasan a superficie opaca. Iconos de pestaña legibles a 24 px. Mini-reproductor y barra de pestañas fusionan sus márgenes: 146 px → 66 px.
+
+ +
+
Inicio · 6 emisoras a la vista + favoritas + géneros
+
+
+
+ +
9:41
signal_cellular_altwifibattery_full
+
Inicio
search
bedtime
+ +
+
+ +
+
+ +
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+ +
+
+
pause
+
+
+ +
+
Tus favoritasVer todas
+
+
+ +
+
play_arrow
+
{{ s.n }}
+
+
+
+ +
+
+
Todos
+
{{ g }}
+
+ +
+
Populares en España53.412
+
+
+ +
+
+
{{ s.n }}
+
{{ s.kbps }} kbps
+
+
+
+
+ +
+
homeInicio
+
travel_exploreBuscar
+
favoriteFavoritos
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
Buscar · resultados desde el píxel 160
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
Buscar
tuneFiltros · 2
+ +
+
searchjazzclose
+
+
+
Españaclose
+
≥128 kbpsclose
+
Idioma
+
Ordenar
+
+
+
37 RESULTADOS
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.p }} · {{ s.kbps }} kbps
+
favorite
+
play_arrow
+
+
+
+
+ +
+ +
Radio Horizonte
En directo
+
pause_circle
+
keyboard_arrow_up
+
+
+
homeInicio
+
travel_exploreBuscar
+
favoriteFavoritos
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
Favoritos · 8 filas visibles, grupos como pestañas
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
Favoritos
create_new_folder
swap_vert
+ +
+
+
Todas · 8
+
Cada mañana · 4
+
Coche · 3
+
+
+
+ +
+ drag_indicator +
+
{{ s.n }}
{{ s.g }} · {{ s.p }}
+
play_arrow
+
+
+
+
+ +
+ +
Radio Horizonte
En directo
+
pause_circle
+
keyboard_arrow_up
+
+
+
homeInicio
+
travel_exploreBuscar
+
favoriteFavoritos
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
Reproductor · 9 acciones visibles en lugar de 3
+
+ +
+
+ +
9:41
signal_cellular_altwifibattery_full
+
keyboard_arrow_down
EN DIRECTO
cast
+ +
+
+
+
Radio Horizonte
+
+
Noticias · España · Español
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
volume_up
+
+
+
+
equalizerEQ · Rock
+
micGrabar
+
bedtime30 min
+
ios_shareCompartir
+
+
+
hdMP3 · 128 kbps · conexión estableCambiar
+
+
+
+ +
+
Alarmas · la hora es lo primero que se lee
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
Alarmas
addNueva
+ +
+
+ alarm_on +
Suena en 9 h 12 min
Cada mañana · mañana a las 07:30
+
Saltar
+
+
+
+ +
+
+
+
{{ a.hora }}{{ a.dias }}
+
+
{{ a.nombre }}
+
+
+
+
+
+
+
+ +
+ +
Radio Horizonte
En directo
+
pause_circle
+
keyboard_arrow_up
+
+
+
homeInicio
+
travel_exploreBuscar
+
favoriteFavoritos
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
+ +
+
1cReforma — "Ahora" como pantalla raíz y una sola jerarquía para móvil y coche. Cuatro pestañas (Escuchar · Explorar · Alarmas · Ajustes): favoritos deja de ser un destino y se convierte en el cuerpo de la pantalla de inicio. El reproductor completo pasa a ser opcional — pausar, saltar y cambiar de emisora se hacen sin abrirlo. La misma jerarquía de dos niveles (Favoritas → emisora) es la que Android Auto necesita, así que móvil y coche dejan de divergir.
+
+ +
+
Escuchar · reproductor + tus emisoras en una pantalla
+
+ +
+
+ +
9:41
signal_cellular_altwifibattery_full
+
ESCUCHANDO AHORA
cast
more_horiz
+ +
+
+
+ +
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+
+
+ +
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
bedtime
+
+
+
expand_lessEcualizador, grabar, calidad
+
+ +
+
Tus emisoras8swap_vert
+
+
+ +
+
+
{{ s.n }}
{{ s.g }}
+
+
+
+
+ +
+
graphic_eqEscuchar
+
travel_exploreExplorar
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
Explorar · una superficie para buscar y descubrir
+
+
+
+
9:41
signal_cellular_altwifibattery_full
+
+
searchEmisora, país, género…mic
+
+
Cerca de ti · España
+
+
+ +
+
play_arrow
+
{{ s.n }}
{{ s.g }} · {{ s.kbps }} kbps
+
+
+
+
+
Explorar por
+
+
+
public
Países
238
+
library_music
Géneros
64
+
trending_up
Tendencias
Hoy
+
fiber_new
Novedades
Esta semana
+
+
+
Populares ahora
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.p }}
+
favorite
+
+
+
+
+
+ +
Radio Horizonte
En directo
+
pause_circle
+
+
+
graphic_eqEscuchar
+
travel_exploreExplorar
+
alarmAlarmas
+
settingsAjustes
+
+
+
+ +
+
Bandeja de herramientas · sube desde el reproductor, no lo tapa
+
+ +
+
+
9:41
signal_cellular_altwifibattery_full
+
+
+
keyboard_arrow_down
EN DIRECTO
cast
+
+
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
favoriteskip_previouspauseskip_nextvolume_up
+
+
+
+
+
+
HerramientasPRO activo
+
+
+
Ecualizador de esta emisoraPROPIO
Solo para Radio Horizonte · el base vive en Ajustes
+
+
+ +
+
+ {{ b.hz }} +
+
+
+
+
undoVolver al base
tunePresets
+
+
+
+
mic
Grabar
Stream original
+
bedtime
28:44
Apagado
+
hd
128 kbps
Calidad
+
alarm_add
Alarma
Con esta emisora
+
+
+
+
+
+ +
+
Android Auto · 1360×720 · misma jerarquía, objetivos de 96 px
+
+
+ +
+ +
+
favorite
+
history
+
travel_explore
+
folder
+
+ +
+
Favoritas8
+
+
+ +
+
+
{{ s.n }}
{{ s.g }} · {{ s.kbps }} kbps
+ {{ s.ico }} +
+
+
+
+ +
+ +
+
EN DIRECTO
+
+
Radio Horizonte
+
+
Noticias · España · 128 kbps
+
+
+
favorite
+
skip_previous
+
pause
+
skip_next
+
+
+
+
+ +
+
+ +
+ +

Esto es el punto de partida y el diagnóstico. Dime si te cuadra y sigo con las propuestas, o pídeme ya: "enséñame la evolución" · "quiero ver la versión más radical" · "añade la pantalla de Android Auto".

+
+ +
+ + + diff --git a/rediseno-funcional-de-aplicacion/project/assets/art_aurora.jpg b/rediseno-funcional-de-aplicacion/project/assets/art_aurora.jpg new file mode 100644 index 0000000..5ac68d8 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/art_aurora.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/art_cosmic.jpg b/rediseno-funcional-de-aplicacion/project/assets/art_cosmic.jpg new file mode 100644 index 0000000..4ab93b2 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/art_cosmic.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/art_nova.jpg b/rediseno-funcional-de-aplicacion/project/assets/art_nova.jpg new file mode 100644 index 0000000..f2cbd8f Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/art_nova.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/art_pulse.jpg b/rediseno-funcional-de-aplicacion/project/assets/art_pulse.jpg new file mode 100644 index 0000000..d592c14 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/art_pulse.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/banner.jpg b/rediseno-funcional-de-aplicacion/project/assets/banner.jpg new file mode 100644 index 0000000..2f249cb Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/banner.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_alarm.png b/rediseno-funcional-de-aplicacion/project/assets/i_alarm.png new file mode 100644 index 0000000..6582386 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_alarm.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_favorites.png b/rediseno-funcional-de-aplicacion/project/assets/i_favorites.png new file mode 100644 index 0000000..8c022af Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_favorites.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_home.png b/rediseno-funcional-de-aplicacion/project/assets/i_home.png new file mode 100644 index 0000000..6de97df Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_home.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_player.png b/rediseno-funcional-de-aplicacion/project/assets/i_player.png new file mode 100644 index 0000000..2f75b75 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_player.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_search.png b/rediseno-funcional-de-aplicacion/project/assets/i_search.png new file mode 100644 index 0000000..a08b5a3 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_search.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/i_settings.png b/rediseno-funcional-de-aplicacion/project/assets/i_settings.png new file mode 100644 index 0000000..e2b2512 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/i_settings.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/mark.png b/rediseno-funcional-de-aplicacion/project/assets/mark.png new file mode 100644 index 0000000..b5347f7 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/mark.png differ diff --git a/rediseno-funcional-de-aplicacion/project/assets/noise.jpg b/rediseno-funcional-de-aplicacion/project/assets/noise.jpg new file mode 100644 index 0000000..2d86b84 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/assets/noise.jpg differ diff --git a/rediseno-funcional-de-aplicacion/project/ref/mockup-aaa.png b/rediseno-funcional-de-aplicacion/project/ref/mockup-aaa.png new file mode 100644 index 0000000..54b5bc8 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/ref/mockup-aaa.png differ diff --git a/rediseno-funcional-de-aplicacion/project/ref/mockup-night-ocean.png b/rediseno-funcional-de-aplicacion/project/ref/mockup-night-ocean.png new file mode 100644 index 0000000..417fb58 Binary files /dev/null and b/rediseno-funcional-de-aplicacion/project/ref/mockup-night-ocean.png differ diff --git a/rediseno-funcional-de-aplicacion/project/support.js b/rediseno-funcional-de-aplicacion/project/support.js new file mode 100644 index 0000000..cb009b6 --- /dev/null +++ b/rediseno-funcional-de-aplicacion/project/support.js @@ -0,0 +1,1911 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"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); + + // 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/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} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + 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/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/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/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/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/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/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: