From 23e30d96fc6d39443215122e26a4fab862b737d5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 21 Sep 2026 18:41:04 +0200 Subject: [PATCH] Translate docs to English, rewrite README, drop design handoff folder Translate all of docs/ from Spanish to English and rewrite the root README as a short project overview instead of a technical run guide. Remove web-player/design_handoff_musify_player, an old design-tool export that was no longer needed. --- README.md | 93 +- docs/README.md | 60 +- docs/architecture.md | 82 +- docs/authentication.md | 52 +- docs/development.md | 163 +- docs/media-processing.md | 50 +- docs/projects.md | 74 +- docs/storage.md | 60 +- docs/streaming.md | 62 +- .../Crear playlist.dc.html | 183 -- .../Musify.dc.html | 1127 ---------- .../design_handoff_musify_player/README.md | 399 ---- .../screenshots/01-home-hero.png | Bin 32701 -> 0 bytes .../02-home-continuar-spotlight.png | Bin 32712 -> 0 bytes .../screenshots/03-home-mixes-artistas.png | Bin 32697 -> 0 bytes .../04-home-populares-playlists.png | Bin 32691 -> 0 bytes .../screenshots/05-descubrir.png | Bin 34053 -> 0 bytes .../screenshots/06-playlists.png | Bin 37419 -> 0 bytes .../screenshots/07-tracklist-me-gusta.png | Bin 32766 -> 0 bytes .../screenshots/08-sin-resultados.png | Bin 28546 -> 0 bytes .../design_handoff_musify_player/support.js | 1911 ----------------- 21 files changed, 347 insertions(+), 3969 deletions(-) delete mode 100644 web-player/design_handoff_musify_player/Crear playlist.dc.html delete mode 100644 web-player/design_handoff_musify_player/Musify.dc.html delete mode 100644 web-player/design_handoff_musify_player/README.md delete mode 100644 web-player/design_handoff_musify_player/screenshots/01-home-hero.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/02-home-continuar-spotlight.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/05-descubrir.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/06-playlists.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png delete mode 100644 web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png delete mode 100644 web-player/design_handoff_musify_player/support.js diff --git a/README.md b/README.md index 9a319a7..60fb8fc 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,50 @@ # Musify -Music streaming platform: upload your own tracks, process them asynchronously -(audio transcoded to `.m4a`, thumbnails generated) and stream them without the -bytes ever going through the API. +A music streaming app. Upload your own tracks, organize them into playlists +and albums, and stream them back with a web player built around your +listening habits: auto-generated mixes, a home feed, search, likes, and an +Explore page for finding something new. + +## What it does + +- **Upload and organize your own music**: tracks, albums, cover art, artist credits. +- **Playlists**, likes, and a "recently played" history. +- **Mixes**, playlists generated automatically from what you actually listen to. +- **Explore**, genre browsing and search across tracks, albums, playlists and people. +- **Real streaming**, not just file downloads: seek, queue, shuffle, repeat. +- **Accounts via OIDC login**, with an optional anonymous listening mode. +- Light and dark themes, and a layout that works on both desktop and mobile. + +## How it's built + +**Backend**: .NET 10, split into a REST API, a background worker, and a +dedicated streaming gateway. +- Clean architecture (Domain / Application / Infrastructure) with CQRS (Mediator) and `ErrorOr` results. +- PostgreSQL via EF Core for metadata. RabbitMQ and MassTransit handle async processing (audio transcoding, thumbnails) with saga-based rollback. +- SeaweedFS (S3-compatible) for file storage. Uploads and downloads go straight between the client and storage, never through the API. +- ffmpeg for audio transcoding, ImageSharp for thumbnails. +- A YARP reverse proxy that streams the audio and checks a signed, short-lived ticket on every request. +- Zitadel for login (OIDC/OAuth2). + +**Web client**: SvelteKit 2 on Svelte 5 (runes), TypeScript, Tailwind 4. +The session lives on the server, so the backend access token never reaches the browser. + +**Infrastructure**: the whole stack runs from Docker Compose (dev and +production overlays), with Postgres, RabbitMQ, SeaweedFS, Zitadel and Jaeger +alongside the apps. + +## Project layout ``` -backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests -web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client -deploy/ Docker Compose stacks — self-contained, no wrapper scripts -docs/ What each piece does and why (in Spanish) +backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure +web-player/ the SvelteKit web client +deploy/ Docker Compose stacks for dev and production +docs/ architecture notes, one file per area ``` -```sh -dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS) -``` +## Digging deeper -## Run it - -Development (Docker Desktop): - -```sh -docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d -``` - -Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC -provisioning — all as one-shot jobs baked into the compose file. Apps run from -the IDE by default; add `--profile apps up -d --build` as a second command to -run them in Docker instead. - -Production, on a server with a domain: - -```sh -cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets -docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d -docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build -``` - -Idempotent — run the same commands again to deploy an update. See -[deploy/README.md](deploy/README.md) for the details, the port map and the -production prerequisites. - -## Components - -| Service | Port (dev) | Role | -|---|---|---| -| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets | -| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio | -| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails | -| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) | -| Zitadel | 8080 | Identity (OIDC/OAuth2) | -| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces | - -More in [docs/](docs/README.md): [architecture](docs/architecture.md), -[authentication](docs/authentication.md), [storage](docs/storage.md), -[streaming](docs/streaming.md), [media processing](docs/media-processing.md), -[development](docs/development.md). +[docs/](docs/README.md) covers how each piece works and why: architecture, +authentication, storage, streaming, media processing, and the local dev +setup. [deploy/README.md](deploy/README.md) has the full instructions for +running the stack, in development or on a production server. diff --git a/docs/README.md b/docs/README.md index 316215f..80547bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,47 +1,51 @@ -# Musify — Documentación +# Musify documentation -Musify es un backend de streaming de música: subir pistas, procesarlas (transcodificar el audio a `.m4a` y generar miniaturas), organizarlas en playlists y reproducirlas en streaming sin que los bytes pasen por la API. +Musify is a music streaming backend: upload tracks, process them (transcode +the audio to `.m4a`, generate thumbnails), organize them into playlists, and +stream them without the bytes ever passing through the API. -Esta carpeta explica **qué hace cada pieza, su propósito y por qué está diseñada así**. +This folder explains **what each piece does, its purpose, and why it's built +this way**. -## Índice +## Index -- [architecture.md](architecture.md) — Visión global: componentes, cómo encajan y los flujos principales. -- [projects.md](projects.md) — Qué es y para qué sirve cada proyecto/solución. -- [authentication.md](authentication.md) — Zitadel (OIDC), validación de JWT, provisioning/sync de usuarios. -- [storage.md](storage.md) — SeaweedFS (S3), estructura de claves, URLs prefirmadas, qué es público y qué privado. -- [streaming.md](streaming.md) — Streaming del audio (`.m4a` por HTTP range) con stream-ticket (RS256) y el reverse proxy (StreamingGateway). -- [media-processing.md](media-processing.md) — El worker y los workflows de MassTransit (transcode de audio, miniaturas). -- [development.md](development.md) — Cómo levantar todo en local: contenedores, puertos y comandos. -- [../deploy/README.md](../deploy/README.md) — El deploy en detalle: compose, scripts y producción. +- [architecture.md](architecture.md): overview, components, how they fit together, and the main flows. +- [projects.md](projects.md): what each project/solution is and what it's for. +- [authentication.md](authentication.md): Zitadel (OIDC), JWT validation, user provisioning/sync. +- [storage.md](storage.md): SeaweedFS (S3), key layout, presigned URLs, what's public vs. private. +- [streaming.md](streaming.md): audio streaming (`.m4a` over HTTP range) with stream tickets (RS256) and the reverse proxy (StreamingGateway). +- [media-processing.md](media-processing.md): the worker and its MassTransit workflows (audio transcoding, thumbnails). +- [development.md](development.md): running everything locally, containers, ports, commands. +- [../deploy/README.md](../deploy/README.md): deployment in detail, compose, scripts, and production. -## Mapa de componentes (alto nivel) +## Component map (high level) ``` ┌─────────────────────────────────────────────┐ - Cliente ───► │ Musify.Api (:5111) API REST + emite tickets │ + Client ───► │ Musify.Api (:5111) REST API + issues tickets│ └─────────────────────────────────────────────┘ - │ EF Core │ presigned (subidas) │ MassTransit (eventos) + │ EF Core │ presigned (uploads) │ MassTransit (events) ▼ ▼ ▼ - PostgreSQL SeaweedFS S3 (:8333) RabbitMQ ──► Worker (procesa) - (metadatos) (objetos) audio→.m4a, imágenes + PostgreSQL SeaweedFS S3 (:8333) RabbitMQ ──► Worker (processes) + (metadata) (objects) audio→.m4a, images ▲ - Cliente ───► StreamingGateway (:8081) ┘ sirve audio .m4a (valida ticket, proxy al filer) + Client ───► StreamingGateway (:8081) ┘ serves .m4a audio (validates ticket, proxies to the filer) - Login: Cliente ◄──► Zitadel (:8080, OIDC) → Musify.Api valida el JWT + Login: Client ◄──► Zitadel (:8080, OIDC) → Musify.Api validates the JWT ``` ## Stack - **.NET 10**, C#. Minimal APIs (`Musify.Api`), Worker Service (`Musify.Worker`), ASP.NET + YARP (`Musify.StreamingGateway`). -- **CQRS** con [Mediator] (source-generator) y resultados con **ErrorOr**. -- **EF Core 10** + **PostgreSQL** (metadatos). -- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas). -- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3). -- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas). -- **Zitadel** (OIDC/OAuth2) para identidad. +- **CQRS** with [Mediator] (source generator) and results via **ErrorOr**. +- **EF Core 10** + **PostgreSQL** (metadata). +- **MassTransit 8** + **RabbitMQ** (async processing with routing slips/sagas). +- **SeaweedFS** (S3-compatible object storage, via AWSSDK.S3). +- **ffmpeg** (audio transcoding to AAC/`.m4a`), **SixLabors.ImageSharp** (thumbnails). +- **Zitadel** (OIDC/OAuth2) for identity. -## Estructura del repositorio +## Repository layout -Un solo repo: `backend/` (los seis proyectos .NET), `web-player/` (cliente web -SvelteKit), `deploy/` (stacks de Docker Compose y scripts) y `docs/`. +A single repo: `backend/` (the six .NET projects), `web-player/` (the +SvelteKit web client), `deploy/` (Docker Compose stacks and scripts), and +`docs/`. diff --git a/docs/architecture.md b/docs/architecture.md index bff0579..5f04818 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,55 +1,55 @@ -# Arquitectura +# Architecture -## Principios +## Principles -- **Clean architecture / capas** en `backend/`: el dominio y la aplicación no dependen de infraestructura; la infraestructura implementa las interfaces (`Application/Contracts`). -- **CQRS**: cada operación es un `Command` o `Query` con su handler (Mediator). Los handlers devuelven `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), que la API traduce a códigos HTTP. -- **Los bytes pesados no pasan por la API**: subidas con URLs prefirmadas (cliente → S3) y reproducción vía un reverse proxy dedicado (gateway → S3). La API solo mueve metadatos y emite permisos. -- **Procesado asíncrono**: crear una pista no transcodifica en caliente; publica un evento y un worker hace el trabajo pesado con compensación (sagas/routing slips de MassTransit). +- **Clean architecture / layers** in `backend/`: the domain and application layers don't depend on infrastructure; infrastructure implements their interfaces (`Application/Contracts`). +- **CQRS**: every operation is a `Command` or `Query` with its own handler (Mediator). Handlers return `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), which the API translates into HTTP status codes. +- **Heavy bytes never touch the API**: uploads use presigned URLs (client → S3) and playback goes through a dedicated reverse proxy (gateway → S3). The API only moves metadata and issues permissions. +- **Async processing**: creating a track doesn't transcode it on the spot. It publishes an event, and a worker does the heavy lifting, with compensation (MassTransit sagas/routing slips). -## Componentes +## Components -| Componente | Responsabilidad | Por qué separado | +| Component | Responsibility | Why it's separate | |---|---|---| -| **Musify.Api** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes | -| **Domain/Application/Infrastructure** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por la API y el Worker | -| **StreamingGateway** | Reverse proxy (YARP) que valida el ticket y sirve el audio (`.m4a`) desde SeaweedFS | Escala con el tráfico de bytes, independiente de la API; SeaweedFS queda privado detrás | -| **Worker** | Consume eventos de MassTransit: transcodifica audio a `.m4a` y genera miniaturas | Trabajo CPU/IO intensivo fuera del request HTTP | -| **PostgreSQL** | Metadatos (usuarios, pistas, playlists, upload intents) | — | -| **RabbitMQ** | Cola de eventos + outbox transaccional | Desacopla creación de procesado | -| **SeaweedFS** | Almacenamiento de objetos (S3 + filer) | Guarda originales y derivados (audio `.m4a`, miniaturas) | -| **Zitadel** | Identidad (OIDC/OAuth2) | Login y emisión de JWT | +| **Musify.Api** | REST API (users, tracks, playlists), authorizes requests, issues presigned URLs and stream tickets | A thin entry point, it doesn't process media or serve bytes | +| **Domain/Application/Infrastructure** | Domain + use cases + shared infrastructure (EF, S3, MassTransit, ffmpeg) | Logic shared between the API and the Worker | +| **StreamingGateway** | Reverse proxy (YARP) that validates the ticket and serves the audio (`.m4a`) from SeaweedFS | Scales with byte traffic independently of the API, and keeps SeaweedFS private behind it | +| **Worker** | Consumes MassTransit events: transcodes audio to `.m4a` and generates thumbnails | Keeps CPU/IO-heavy work off the HTTP request path | +| **PostgreSQL** | Metadata (users, tracks, playlists, upload intents) | n/a | +| **RabbitMQ** | Event queue + transactional outbox | Decouples creation from processing | +| **SeaweedFS** | Object storage (S3 + filer) | Stores originals and derivatives (`.m4a` audio, thumbnails) | +| **Zitadel** | Identity (OIDC/OAuth2) | Login and JWT issuance | -## Flujos principales +## Main flows -### 1. Subida de una pista -1. Cliente pide `POST /tracks/upload-urls` → la API crea **UploadIntents** (reservas) y devuelve **URLs prefirmadas PUT** (imagen + audio). -2. Cliente sube los ficheros **directo a SeaweedFS** con esas URLs (no pasan por la API). -3. Cliente llama `POST /tracks` con los `intentId`s → la API valida los intents, crea el `Track` (+ `UserHasTrack`) y publica `CreateTrackResourcesEvent`. -4. El **Worker** consume el evento: mueve los originales a su sitio, genera miniaturas y transcodifica el audio a `.m4a`, y actualiza el estado del track. +### 1. Uploading a track +1. The client requests `POST /tracks/upload-urls`. The API creates **UploadIntents** (reservations) and returns **presigned PUT URLs** (image + audio). +2. The client uploads the files **directly to SeaweedFS** using those URLs (never through the API). +3. The client calls `POST /tracks` with the `intentId`s. The API validates the intents, creates the `Track` (+ `UserHasTrack`), and publishes `CreateTrackResourcesEvent`. +4. The **Worker** consumes the event: moves the originals into place, generates thumbnails, transcodes the audio to `.m4a`, and updates the track's status. -Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md). +See [media-processing.md](media-processing.md) and [storage.md](storage.md) for details. -### 2. Reproducción (streaming) -1. Cliente pide `GET /tracks/{id}/stream` (autenticado) → la API autoriza y devuelve `{ manifestUrl, ticket }` (URL al `.m4a` + ticket RS256 acotado a la carpeta de esa pista). -2. El reproductor abre la URL del audio añadiendo `?t=` (o cabecera `X-Stream-Ticket`) y descarga por HTTP range. -3. El **StreamingGateway** valida el ticket y reenvía al filer de SeaweedFS; los bytes van storage → cliente. +### 2. Playback (streaming) +1. The client requests `GET /tracks/{id}/stream` (authenticated). The API authorizes it and returns `{ manifestUrl, ticket }` (a URL to the `.m4a` plus an RS256 ticket scoped to that track's folder). +2. The player opens the audio URL with `?t=` appended (or an `X-Stream-Ticket` header) and downloads it via HTTP range requests. +3. The **StreamingGateway** validates the ticket and forwards the request to the SeaweedFS filer. The bytes flow storage → client. -Ver [streaming.md](streaming.md). +See [streaming.md](streaming.md). -### 3. Autenticación -1. Cliente hace login OIDC contra **Zitadel** y obtiene un access token (JWT). -2. La Musify.Api valida la firma/iss/aud del JWT. -3. En cada token validado, se **sincroniza** el usuario local (crea/actualiza nombre y foto) — provisioning just-in-time. +### 3. Authentication +1. The client logs in via OIDC against **Zitadel** and gets an access token (JWT). +2. Musify.Api validates the JWT's signature, issuer and audience. +3. On every validated token, the local user is **synced** (name and picture created or updated). This is just-in-time provisioning. -Ver [authentication.md](authentication.md). +See [authentication.md](authentication.md). -## Modelo de datos (entidades) +## Data model (entities) -- **User** — `Id` es el `sub` numérico de Zitadel (`long`), más nombre y `ProfilePictureUrl`. -- **Track** — pista: nombres de originales/derivados, `AudioFolderName` (carpeta del `.m4a` transcodificado), estados de procesado (`ProcessingStatus`), `LifeCycleStatus`. -- **PlayList** — lista del usuario (`UserId`). -- **PlayListHasTrack** — pistas dentro de una playlist (con `Position`). -- **UserHasTrack** — propiedad: qué usuario posee qué pista. -- **UploadIntent** — reserva de subida (clave, bucket, tamaño esperado, estado, expiración) para validar y aplicar cuota. -- **Upload** — registro de objetos subidos/transferidos al storage. +- **User**: `Id` is Zitadel's numeric `sub` (`long`), plus a name and `ProfilePictureUrl`. +- **Track**: original/derivative file names, `AudioFolderName` (the transcoded `.m4a`'s folder), processing state (`ProcessingStatus`), `LifeCycleStatus`. +- **PlayList**: a user's playlist (`UserId`). +- **PlayListHasTrack**: the tracks inside a playlist (with `Position`). +- **UserHasTrack**: ownership, which user owns which track. +- **UploadIntent**: an upload reservation (key, bucket, expected size, status, expiration) used for validation and quota. +- **Upload**: a record of objects uploaded or transferred to storage. diff --git a/docs/authentication.md b/docs/authentication.md index 372d043..c0376b8 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,40 +1,40 @@ -# Autenticación e identidad +# Authentication and identity -## Proveedor: Zitadel (OIDC/OAuth2) +## Provider: Zitadel (OIDC/OAuth2) -El login y la emisión de tokens los hace **Zitadel** (no la API). La API es un **resource server**: solo **valida** el access token (JWT) que le llega en `Authorization: Bearer`. +Login and token issuance are handled by **Zitadel**, not the API. The API is a **resource server**: it only **validates** the access token (JWT) it receives in `Authorization: Bearer`. -- Config en la sección `Authentication` (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, endpoints de authorize/token, scopes. -- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC). -- En dev, `RequireHttpsMetadata = false`. +- Configured under the `Authentication` section (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, authorize/token endpoints, scopes. +- `JwtBearerOptionsSetup` sets up validation (issuer, audience, keys via OIDC metadata). +- In dev, `RequireHttpsMetadata = false`. -### Requisito clave en la app de Zitadel -- **Auth Token Type = JWT** (si no, Zitadel emite un token **opaco** sin puntos y `JwtBearer` no lo puede validar → 401 en todo lo protegido). -- **User Info inside Token** activado si quieres que el access token traiga claims de perfil (`name`, `email`, `picture`). Por defecto el access token es mínimo (`sub`, `aud`, `iss`, `exp`…), sin perfil. +### Key requirement in the Zitadel app +- **Auth Token Type = JWT** (otherwise Zitadel issues an **opaque** token with no dots, which `JwtBearer` can't validate → 401 on everything protected). +- **User Info inside Token** enabled if you want the access token to carry profile claims (`name`, `email`, `picture`). By default the access token is minimal (`sub`, `aud`, `iss`, `exp`…), with no profile info. -## El `sub` es numérico → `User.Id` es `long` +## `sub` is numeric → `User.Id` is `long` -El `sub` de Zitadel es un entero de 64 bits en string (p. ej. `371953080444977155`), **no un GUID**. Por eso: -- `User.Id` y todas las FKs de usuario (`PlayList.UserId`, `UserHasTrack.UserId`, `UploadIntent.UserId`) son **`long`**. -- `User.Id` lleva `[DatabaseGenerated(None)]` para que EF **no** lo autogenere: se asigna el `sub`. -- `CurrentUser.Id` es `long?` (parsea con `long.TryParse`); `RequiredId` lanza si falta. +Zitadel's `sub` is a 64-bit integer encoded as a string (e.g. `371953080444977155`), **not a GUID**. Because of that: +- `User.Id` and every user-related FK (`PlayList.UserId`, `UserHasTrack.UserId`, `UploadIntent.UserId`) are **`long`**. +- `User.Id` has `[DatabaseGenerated(None)]` so EF doesn't autogenerate it: it's assigned from the `sub`. +- `CurrentUser.Id` is `long?` (parsed with `long.TryParse`); `RequiredId` throws if it's missing. -**Por qué**: el código antiguo hacía `Guid.Parse(sub)` y reventaba (FormatException) → 401 en cada endpoint protegido. Se decidió guardar el `sub` tal cual como número. +**Why**: the original code did `Guid.Parse(sub)`, which threw a `FormatException` → 401 on every protected endpoint. The fix was to store the `sub` as-is, as a number. -## Provisioning / sincronización de usuarios +## User provisioning / sync -En cada token validado, `JwtBearerEventsHandler.TokenValidated`: -1. Lee `sub` (id), `name`/`preferred_username`/`email` (con fallback) y el claim OIDC `picture`. -2. Envía un `SyncUserCommand`. +On every validated token, `JwtBearerEventsHandler.TokenValidated`: +1. Reads `sub` (id), `name`/`preferred_username`/`email` (with fallback), and the OIDC `picture` claim. +2. Sends a `SyncUserCommand`. -`SyncUserCommandHandler` hace un **upsert idempotente**: -- Si el usuario no existe → lo crea. -- Si existe → actualiza nombre/apellidos/`ProfilePictureUrl` **solo si cambiaron** (escribe en DB únicamente cuando hay cambios). +`SyncUserCommandHandler` does an **idempotent upsert**: +- If the user doesn't exist yet → creates it. +- If it exists → updates the name and `ProfilePictureUrl` **only if they changed** (a DB write only happens when something is different). -**Por qué upsert con "escribe solo si cambió"**: `TokenValidated` se ejecuta en **cada request**; sin ese guardado condicional estaríamos escribiendo en cada petición. Y debe ser resiliente: si el provisioning falla, se loguea pero **no** tumba la autenticación. +**Why "write only if changed"**: `TokenValidated` runs on **every request**; without that guard we'd be writing to the DB on every single call. It also needs to be resilient: if provisioning fails, it's logged but authentication **doesn't** fail because of it. -> Nota: `CreateUserCommand` (endpoint manual `POST /users`) sigue siendo create-only; el sync del login usa `SyncUserCommand`. +> Note: `CreateUserCommand` (the manual `POST /users` endpoint) is still create-only; login sync uses `SyncUserCommand`. -## Foto de perfil +## Profile picture -`User.ProfilePictureUrl` se rellena desde el claim `picture` del token. Si Zitadel no mete la info de perfil en el access token (ver arriba), llega `null` — el código es correcto, pero no hay nada que sincronizar hasta activar esa opción. +`User.ProfilePictureUrl` is filled from the token's `picture` claim. If Zitadel doesn't include profile info in the access token (see above), it arrives as `null`. The code is correct, there's just nothing to sync until that option is turned on. diff --git a/docs/development.md b/docs/development.md index 1acbc55..3a15444 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,21 +1,21 @@ -# Entorno de desarrollo +# Development environment -Cómo levantar todo en local y probarlo. El detalle completo del deploy (y el de -producción) está en [deploy/README.md](../deploy/README.md). +How to run everything locally and test it. The full deployment details (dev +and prod) live in [deploy/README.md](../deploy/README.md). -## Arranque +## Startup ```sh docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d ``` -Ese único comando levanta la infraestructura y corre los jobs *one-shot* de -bootstrap: genera las claves RS256 de stream-ticket en `deploy/keys/`, aplica -las migraciones EF Core y provisiona Zitadel (proyecto + apps OIDC), dejando -los client ids en `deploy/.env`. Detalle completo, incluido cómo arrancar las -apps en Docker con `--profile apps`, en [deploy/README.md](../deploy/README.md). +That one command brings up the infrastructure and runs the one-shot bootstrap +jobs: generates the RS256 stream-ticket keys in `deploy/keys/`, applies the +EF Core migrations, and provisions Zitadel (project + OIDC apps), writing the +client ids into `deploy/.env`. Full details, including how to run the apps in +Docker with `--profile apps`, are in [deploy/README.md](../deploy/README.md). -## Aplicaciones +## Applications ```powershell dotnet run --project backend/Hosts/Musify.Api # :5111 @@ -24,45 +24,46 @@ dotnet run --project backend/Hosts/Musify.Worker # background npm --prefix web-player run dev # :5173 ``` -## Mapa de puertos +## Port map -| Servicio | Puerto | +| Service | Port | |---|---| | Musify.Api | 5111 (`/scalar/v1`, `/openapi/v1.json`) | | Musify.StreamingGateway | 8081 (`/health`, `/media/...`) | -| web-player | 5173 (`npm run dev`) o 3000 (contenedor) | +| web-player | 5173 (`npm run dev`) or 3000 (container) | | SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 | | Zitadel | 8080 | -| PostgreSQL | 59000 (→ 5432 en el contenedor) | +| PostgreSQL | 59000 (→ 5432 in the container) | | RabbitMQ AMQP / management | 5672 / 15672 | | Jaeger UI / OTLP | 16686 / 4317 | | pgAdmin (`-Tools`) | 5050 | -DB: `musify_db`, usuario `postgres`/`postgres`. Bucket S3: `webapi-storage` -(credenciales `admin_access_key`/`admin_secret_key`). Todos los valores salen de +DB: `musify_db`, user `postgres`/`postgres`. S3 bucket: `webapi-storage` +(credentials `admin_access_key`/`admin_secret_key`). All of these come from `deploy/.env`. -## Migraciones +## Migrations -El servicio `migrate` de `deploy/compose.yml` las aplica en cada `docker -compose up -d` (no-op si el esquema ya está al día). A mano, tras cambios de -esquema, contra la base local: +The `migrate` service in `deploy/compose.yml` applies them on every `docker +compose up -d` (a no-op if the schema is already current). To run them by +hand after a schema change, against the local database: ```powershell dotnet ef database update --project backend/Core/Musify.Infrastructure --startup-project backend/Core/Musify.Infrastructure --context Database ``` -`Musify.Infrastructure` es a la vez el proyecto de migraciones y el de arranque: -tiene el `IDesignTimeDbContextFactory` y el paquete `EFCore.Design`. La cadena -de conexión se lee de sus user-secrets, o de la variable de entorno -`Database__ConnectionString` si no hay user-secrets (así es como la usa el -contenedor `migrate`, ver `backend/Musify.Migrator.Dockerfile`). +`Musify.Infrastructure` is both the migrations project and the startup +project: it has the `IDesignTimeDbContextFactory` and the `EFCore.Design` +package. The connection string is read from its user-secrets, or from the +`Database__ConnectionString` environment variable if there are no +user-secrets (which is how the `migrate` container uses it, see +`backend/Musify.Migrator.Dockerfile`). -## Stream-tickets (claves RS256) +## Stream tickets (RS256 keys) -La API firma los stream-tickets con la clave privada y el gateway los valida con -la pública. El servicio `keys-init` de `deploy/compose.yml` las genera en -`deploy/keys/` si no existen; a mano: +The API signs stream tickets with the private key, and the gateway validates +them with the public one. The `keys-init` service in `deploy/compose.yml` +generates them in `deploy/keys/` if they don't exist yet; to do it by hand: ```powershell openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out deploy/keys/stream_private.pem @@ -78,74 +79,74 @@ openssl rsa -in deploy/keys/stream_private.pem -pubout -out deploy/keys/stream_p dotnet test backend/Musify.slnx ``` -`backend/Tests/` (carpeta `/Tests/` en el `.slnx`) tiene cinco proyectos xUnit: +`backend/Tests/` (the `/Tests/` folder in the `.slnx`) has five xUnit projects: -| Proyecto | Cubre | Docker | +| Project | Covers | Docker | |---|---|---| -| `Musify.Domain.Tests` | Lógica de los value objects (`TrackAudio`/`TrackPictures`: `IsProcessed`, `IsFailed`, `IsInProgress`). Las entidades son en su mayoría anémicas — sin comportamiento propio, nada más que testear ahí. | No | -| `Musify.Application.Tests` | Los ~45 handlers y servicios de `Musify.Application` (Albums, Tracks, PlayLists, Users, Mixes). | No | -| `Musify.Infrastructure.Tests` | `Database`/migraciones, `StorageService`, `StreamTicketService`, `AuditableEntityInterceptor`, `PictureService`, `SingleFlightCache`, `PlayListPresetSeeder`. | **Sí** | -| `Musify.Api.Tests` | Endpoints HTTP reales (routing, auth, `ValidationFilter`, mapeo `ErrorOr`→HTTP) vía `WebApplicationFactory`, más `ErrorOrHttpExtensions`/`CurrentUser`/validators FluentValidation en aislado. | **Sí** | -| `Musify.StreamingGateway.Tests` | `TicketValidator` (RS256) y `TicketValidationMiddleware` (traversal, límites de prefijo, extracción de ticket). | No | +| `Musify.Domain.Tests` | Value object logic (`TrackAudio`/`TrackPictures`: `IsProcessed`, `IsFailed`, `IsInProgress`). Entities are mostly anemic, with no behavior of their own beyond that. | No | +| `Musify.Application.Tests` | The ~45 handlers and services in `Musify.Application` (Albums, Tracks, PlayLists, Users, Mixes). | No | +| `Musify.Infrastructure.Tests` | `Database`/migrations, `StorageService`, `StreamTicketService`, `AuditableEntityInterceptor`, `PictureService`, `SingleFlightCache`, `PlayListPresetSeeder`. | **Yes** | +| `Musify.Api.Tests` | Real HTTP endpoints (routing, auth, `ValidationFilter`, `ErrorOr`→HTTP mapping) via `WebApplicationFactory`, plus `ErrorOrHttpExtensions`/`CurrentUser`/FluentValidation validators in isolation. | **Yes** | +| `Musify.StreamingGateway.Tests` | `TicketValidator` (RS256) and `TicketValidationMiddleware` (traversal, prefix limits, ticket extraction). | No | -`Musify.Worker` no tiene proyecto de test: su `Program.cs` es puro *wiring* de -DI/host (Hangfire, MassTransit, jobs) — la lógica real que ejecuta vive en -`Musify.Infrastructure`, ya cubierta ahí. +`Musify.Worker` has no test project: its `Program.cs` is pure DI/host wiring +(Hangfire, MassTransit, jobs). The actual logic it runs lives in +`Musify.Infrastructure`, already covered there. -### Application.Tests: por qué SQLite en memoria y no mocks +### Application.Tests: why in-memory SQLite instead of mocks -`IDatabase` expone `DbSet` directamente, así que los handlers arman LINQ -real (`Where`, `Include`, `Select`...) contra él — no se puede mockear eso con -NSubstitute, no hay `IQueryable` detrás de un mock. En su lugar, -`TestSupport/TestDatabase.cs` es un `DbContext` real sobre SQLite en memoria -(una conexión `:memory:` nueva por test, con foreign keys reales — a diferencia -del proveedor InMemory de EF Core, SQLite además soporta transacciones, que dos -handlers usan). `TestSupport/TestEntities.cs` y `TestConfigurations.cs` -construyen entidades y configuración con valores por defecto sensatos. -`IStorageService`, `IEventBus`, etc. sí se mockean con NSubstitute, al ser -interfaces normales. +`IDatabase` exposes `DbSet` directly, so handlers build real LINQ +(`Where`, `Include`, `Select`...) against it. That can't be mocked with +NSubstitute, since there's no `IQueryable` behind a mock. Instead, +`TestSupport/TestDatabase.cs` is a real `DbContext` on top of in-memory +SQLite (a fresh `:memory:` connection per test, with real foreign keys, +unlike EF Core's InMemory provider, SQLite also supports transactions, +which two handlers rely on). `TestSupport/TestEntities.cs` and +`TestConfigurations.cs` build entities and configuration with sensible +defaults. `IStorageService`, `IEventBus`, etc. are mocked with NSubstitute, +since those are plain interfaces. -### Infrastructure.Tests y Api.Tests: Testcontainers +### Infrastructure.Tests and Api.Tests: Testcontainers -Estos dos sí necesitan Docker corriendo. Levantan un Postgres real -(`Testcontainers.PostgreSql`) y, en `Infrastructure.Tests`, también un -SeaweedFS real (mismo `chrislusf/seaweedfs:latest` e imagen/comando que -`deploy/compose.yml`, no MinIO) — así `StorageService` se prueba contra el -mismo backend S3-compatible que usa producción, no contra AWS S3. Los -contenedores se levantan una vez por corrida (`ICollectionFixture`), con -migraciones EF Core reales aplicadas contra el Postgres efímero. +These two need Docker running. They spin up a real Postgres +(`Testcontainers.PostgreSql`) and, in `Infrastructure.Tests`, a real +SeaweedFS too (the same `chrislusf/seaweedfs:latest` image/command as +`deploy/compose.yml`, not MinIO), so `StorageService` is tested against the +same S3-compatible backend production uses, not against AWS S3. Containers +are started once per run (`ICollectionFixture`), with real EF Core +migrations applied against the ephemeral Postgres. -`Api.Tests` usa `WebApplicationFactory` contra ese mismo Postgres — -routing, auth y validación reales — pero sustituye por fakes los servicios que -hablan por red (`IStorageService`, `IEventBus`, `IStreamTicketService`) y el -esquema de autenticación JWT real por un handler -de prueba (ver `TestSupport/FakeAuthenticationHandler.cs`) que autentica según -un header, sin necesitar un Zitadel real. +`Api.Tests` uses `WebApplicationFactory` against that same +Postgres, with real routing, auth, and validation, but replaces the services +that talk over the network with fakes (`IStorageService`, `IEventBus`, +`IStreamTicketService`), and swaps the real JWT auth scheme for a test +handler (see `TestSupport/FakeAuthenticationHandler.cs`) that authenticates +based on a header, with no real Zitadel needed. -Escribir estos tests contra el backend real encontró y arregló dos bugs reales -(no simulados): `StorageService.RemoveFolderAsync` reventaba con -`NullReferenceException` contra SeaweedFS (su respuesta `DeleteObjects` omite -la lista `` cuando no hay errores, a diferencia de AWS S3), y -`AuditableEntityInterceptor` nunca actualizaba `UpdatedAt` en una actualización -normal (carga → modifica → guarda) porque corría antes de que EF detectara los -cambios pendientes. +Writing these tests against the real backend found and fixed two actual +bugs (not staged ones): `StorageService.RemoveFolderAsync` threw a +`NullReferenceException` against SeaweedFS (its `DeleteObjects` response +omits the `` list when there are no errors, unlike AWS S3), and +`AuditableEntityInterceptor` never updated `UpdatedAt` on a normal update +(load → modify → save) because it ran before EF had detected the pending +changes. ## Config (AppSettings) -Convención: `AppSettings.json` es la plantilla con valores vacíos o neutros; -`AppSettings.Development.json` tiene los valores reales de dev; en Docker todo -se sobreescribe con variables de entorno (`Seccion__Clave`) desde -`deploy/compose.yml`. Para correr `Musify.Api` desde el IDE, los valores de -`Authentication` van en user-secrets (no en el repo) — cópialos de -`deploy/.env` una vez que `zitadel-init` los haya generado (ver +Convention: `AppSettings.json` is the template, with empty or neutral +values; `AppSettings.Development.json` has the real dev values; in Docker +everything is overridden with environment variables (`Section__Key`) from +`deploy/compose.yml`. To run `Musify.Api` from the IDE, the `Authentication` +values go into user-secrets (not the repo). Copy them from `deploy/.env` +once `zitadel-init` has generated them (see [deploy/README.md](../deploy/README.md)). -Secciones clave: `Authentication` (Zitadel), `Database`, `MassTransit`, +Key sections: `Authentication` (Zitadel), `Database`, `MassTransit`, `InfrastructureStorage` (S3), `ApplicationStorage` (bucket), `Track`, `PlayList`, `UploadIntent`, `AudioTranscoder`, `Workers`, `StreamGateway` / `StreamTicket`. -## Requisitos del host +## Host requirements -- .NET 10 SDK y Node 22. -- **ffmpeg** en el PATH (transcode de audio del Worker). +- .NET 10 SDK and Node 22. +- **ffmpeg** on the PATH (for the Worker's audio transcoding). - Docker Desktop. diff --git a/docs/media-processing.md b/docs/media-processing.md index 717ba11..8de9e30 100644 --- a/docs/media-processing.md +++ b/docs/media-processing.md @@ -1,46 +1,46 @@ -# Procesado de medios (Worker + MassTransit) +# Media processing (Worker + MassTransit) -El procesado pesado (transcode de audio, miniaturas, transferencias en el bucket) ocurre **fuera del request HTTP**, en el **Worker**, orquestado con **MassTransit** sobre RabbitMQ. +Heavy processing (audio transcoding, thumbnails, bucket transfers) happens **outside the HTTP request**, in the **Worker**, orchestrated with **MassTransit** on top of RabbitMQ. -## Por qué asíncrono y con routing slips +## Why async, and why routing slips -- Transcodificar audio y redimensionar imágenes es CPU/IO intensivo: no debe bloquear una llamada a la API. -- Es un proceso **multi-paso con efectos secundarios** (descargar de S3 → transcodificar → subir resultados → actualizar DB). Si un paso falla, hay que **deshacer** los anteriores. Para eso se usan **routing slips** (patrón Saga de MassTransit): cada paso es una *activity* con su **compensación**. -- **Outbox transaccional** (EF + MassTransit): el evento se publica de forma consistente con el cambio en la DB (no se pierde ni se duplica). +- Transcoding audio and resizing images is CPU/IO-intensive: it shouldn't block an API call. +- It's a **multi-step process with side effects** (download from S3 → transcode → upload results → update the DB). If a step fails, the previous ones need to be **undone**. That's what **routing slips** are for (MassTransit's Saga pattern): each step is an *activity* with its own **compensation**. +- **Transactional outbox** (EF + MassTransit): the event is published consistently with the DB change (never lost, never duplicated). -## Disparo +## Trigger -Al crear/actualizar una pista o playlist, el handler publica un evento (p. ej. `CreateTrackResourcesEvent`). Un **consumer** (`CreateTrackConsumer`, `UpdateTrackPictureConsumer`, …) construye un **routing slip** con la secuencia de activities y lo lanza. +When a track or playlist is created or updated, the handler publishes an event (e.g. `CreateTrackResourcesEvent`). A **consumer** (`CreateTrackConsumer`, `UpdateTrackPictureConsumer`, …) builds a **routing slip** with the sequence of activities and executes it. -## Builders de routing slip +## Routing slip builders -- `CreateTrackRoutingSlipBuilder` — alta de pista (consume intents, mueve ficheros, dispara procesado). -- `PictureWorkflowRoutingSlipBuilder` — pipeline de imágenes (descargar → redimensionar → subir → actualizar). -- `AudioWorkflowRoutingSlipBuilder` — pipeline de audio (descargar → transcodificar a `.m4a` → subir carpeta → actualizar). +- `CreateTrackRoutingSlipBuilder`: creating a track (consumes intents, moves files, kicks off processing). +- `PictureWorkflowRoutingSlipBuilder`: image pipeline (download → resize → upload → update). +- `AudioWorkflowRoutingSlipBuilder`: audio pipeline (download → transcode to `.m4a` → upload folder → update). - `DeleteTrackRoutingSlipBuilder`, `DeletePlayListRoutingSlipBuilder`, `PlayListPictureSourceRoutingSlipBuilder`. -## Activities (pasos) +## Activities (steps) -Agrupadas por área: +Grouped by area: - **Files**: `DownloadFileFromBucketActivity`, `UploadFileToBucketActivity`, `TransferFilesToBucketActivity`, `CopyFileInBucketActivity`, `RemoveFileFromBucketActivity`. -- **Audio**: `GenerateAudioWorkflowPathsActivity`, `TranscodeAudioActivity` (ffmpeg → `.m4a`), `UpdateTrackAudioActivity` (guarda `AudioFolderName` y marca `Completed`). +- **Audio**: `GenerateAudioWorkflowPathsActivity`, `TranscodeAudioActivity` (ffmpeg → `.m4a`), `UpdateTrackAudioActivity` (saves `AudioFolderName` and marks it `Completed`). - **Pictures**: `GeneratePictureWorkflowPathsActivity`, `ResizePictureActivity` (ImageSharp), `UpdateTrackPictureActivity`, `UpdatePlayListPictureActivity`. -- **Tracks/PlayLists**: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`, `PublishTrackProcessingEventsActivity`, y equivalentes de playlist. +- **Tracks/Playlists**: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`, `PublishTrackProcessingEventsActivity`, and their playlist equivalents. - **UploadIntents**: `ConsumeUploadIntentsActivity`. -`RoutingSlipCleanUpConsumer` reacciona a `Completed`/`Faulted` para limpiar (p. ej. borrar el directorio temporal de trabajo). +`RoutingSlipCleanUpConsumer` reacts to `Completed`/`Faulted` to clean up (e.g. deleting the temporary working directory). -## Transcode de audio +## Audio transcoding -`AudioTranscoderService` invoca **ffmpeg** leyendo el original por stdin y produciendo en un directorio de trabajo un único `audio.m4a`: AAC con codec/bitrate/sample-rate/profile configurables y `-movflags +faststart` (moov al inicio → seek por range). Luego `TransferFilesToBucketActivity` sube esa carpeta a `Tracks/ProcessedAudios/{folder}/` y `UpdateTrackAudioActivity` guarda el `AudioFolderName` (la carpeta **de destino en S3**, no la local) y pone el estado en `Completed`. +`AudioTranscoderService` calls **ffmpeg**, reading the original from stdin and producing a single `audio.m4a` in a working directory: AAC with a configurable codec/bitrate/sample-rate/profile, and `-movflags +faststart` (moov at the start, so range-based seeking works). `TransferFilesToBucketActivity` then uploads that folder to `Tracks/ProcessedAudios/{folder}/`, and `UpdateTrackAudioActivity` saves the `AudioFolderName` (the **destination folder in S3**, not the local one) and sets the status to `Completed`. -## Configuración que necesita el Worker +## What the Worker needs configured -El Worker registra: storage (S3), transcoder (ffmpeg), pictures, DB, los consumers de MassTransit y los jobs de upload intents. Su `AppSettings` debe tener las secciones correctas: `Bucket` (= `webapi-storage`, el mismo que la API), `PlayList`, `Track`, `MassTransit`, `AudioTranscoder`, `UploadIntent`, `Workers` (directorio temporal), `InfrastructureStorage`, `Database`. +The Worker registers: storage (S3), transcoder (ffmpeg), pictures, the DB, the MassTransit consumers, and the upload-intent jobs. Its `AppSettings` needs the right sections: `Bucket` (= `webapi-storage`, same as the API), `PlayList`, `Track`, `MassTransit`, `AudioTranscoder`, `UploadIntent`, `Workers` (temp directory), `InfrastructureStorage`, `Database`. -## Jobs de fondo +## Background jobs -- `UploadIntentExpirationJob` — marca intents caducados como expirados (libera cuota) cada `ExpirationJobIntervalSeconds`. -- `TemporalUploadsCleanUpJob` — limpia subidas temporales antiguas. +- `UploadIntentExpirationJob`: marks expired intents as such (freeing quota) every `ExpirationJobIntervalSeconds`. +- `TemporalUploadsCleanUpJob`: cleans up old temporary uploads. -Corren en el host del Worker (`AddUploadIntentJobs`). Si no se registran en ningún proceso, los intents colgados no se limpian solos. +Both run on the Worker host (`AddUploadIntentJobs`). If they aren't registered anywhere, stale intents never get cleaned up on their own. diff --git a/docs/projects.md b/docs/projects.md index 10c196b..0891c33 100644 --- a/docs/projects.md +++ b/docs/projects.md @@ -1,74 +1,74 @@ -# Proyectos +# Projects -Los proyectos .NET viven en `backend/` (solución `Musify.slnx`, carpetas -`Core/`, `Hosts/` y `Tests/`); el cliente web en `web-player/`. +The .NET projects live under `backend/` (solution `Musify.slnx`, folders +`Core/`, `Hosts/` and `Tests/`); the web client lives under `web-player/`. ## Musify.Api (`:5111`) -API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema. +The public REST API (Minimal APIs, .NET 10). The thin public face of the system. - **Endpoints**: `UserEndpoints`, `TrackEndpoints`, `PlayListEndpoints`. -- **Autenticación**: JWT Bearer contra Zitadel (`Authentication/`), con `JwtBearerEventsHandler` que sincroniza el usuario en cada token validado. -- **`CurrentUser`**: abstracción que se enlaza como parámetro de endpoint (`IBindableFromHttpContext`) y expone los claims básicos (`Id` como `long?`, `RequiredId`, nombre, etc.) sin tener que leer `ClaimsPrincipal` a mano. -- **Emite**: presigned URLs de subida (vía `IStorageService`) y **stream-tickets** (vía `IStreamTicketService`, RS256). -- **OpenAPI + Scalar** para documentación/prueba; OAuth2 (PKCE) configurado para login desde Scalar. -- **CORS** totalmente abierto **solo en Development** (para el front local). -- Traduce `ErrorOr` → HTTP en `ResultHttpExtensions`. +- **Authentication**: JWT Bearer against Zitadel (`Authentication/`), with a `JwtBearerEventsHandler` that syncs the user on every validated token. +- **`CurrentUser`**: an abstraction bound as an endpoint parameter (`IBindableFromHttpContext`) that exposes the basic claims (`Id` as `long?`, `RequiredId`, name, etc.) without reading `ClaimsPrincipal` by hand. +- **Issues**: presigned upload URLs (via `IStorageService`) and **stream tickets** (via `IStreamTicketService`, RS256). +- **OpenAPI + Scalar** for docs/testing; OAuth2 (PKCE) set up for logging in from Scalar. +- **CORS** wide open, but **only in Development** (for the local frontend). +- Translates `ErrorOr` into HTTP in `ResultHttpExtensions`. -**Propósito/porqué**: separar la cara pública (autorización + metadatos) del procesado y del servido de bytes. +**Purpose**: keep the public surface (authorization + metadata) separate from processing and byte-serving. ## Musify.Domain + Musify.Application + Musify.Infrastructure -El núcleo, en tres proyectos (clean architecture): +The core, split into three projects (clean architecture): ### Domain -Entidades (`Entities/`) y value objects/enums (`ValueObjects/`: `ProcessingStatus`, `LifeCycleStatus`, `UploadIntentStatus`, …). Sin dependencias de infraestructura. +Entities (`Entities/`) and value objects/enums (`ValueObjects/`: `ProcessingStatus`, `LifeCycleStatus`, `UploadIntentStatus`, …). No infrastructure dependencies. ### Application -Casos de uso (CQRS con Mediator): +Use cases (CQRS with Mediator): - `Tracks/`, `PlayLists/`, `Users/` → `Commands`, `Queries`, `Handlers`, `Responses`. -- `Contracts/` → interfaces que implementa la infraestructura (`IDatabase`, `IStorageService`, `IAudioTranscoderService`, `IPictureService`, `IEventBus`, `IStreamTicketService`). -- `Configuration/` → opciones tipadas (Track, PlayList, ApplicationStorage, UploadIntent…) bindeadas y validadas al arranque. -- `Shared/` → helpers transversales (`StorageKey.Combine`, `ImageSize`). +- `Contracts/` → interfaces that infrastructure implements (`IDatabase`, `IStorageService`, `IAudioTranscoderService`, `IPictureService`, `IEventBus`, `IStreamTicketService`). +- `Configuration/` → typed options (Track, PlayList, ApplicationStorage, UploadIntent…) bound and validated at startup. +- `Shared/` → cross-cutting helpers (`StorageKey.Combine`, `ImageSize`). - `Pagination/` → `PaginatedResponse`. -**Porqué**: la lógica de negocio no conoce EF, S3 ni RabbitMQ; solo interfaces. Reutilizable por la API y el Worker. +**Why**: business logic doesn't know about EF, S3, or RabbitMQ, only interfaces. Shared by both the API and the Worker. ### Infrastructure -Implementaciones concretas: -- `Persistence/` → `Database` (EF Core/Npgsql), migraciones. -- `Services/` → `StorageService` (AWSSDK.S3 contra SeaweedFS), `AudioTranscoderService` (ffmpeg → `.m4a`), `PictureService` (ImageSharp), `StreamTicketService` (firma RS256). -- `MassTransit/` → consumers, activities y routing slips (ver [media-processing.md](media-processing.md)). +Concrete implementations: +- `Persistence/` → `Database` (EF Core/Npgsql), migrations. +- `Services/` → `StorageService` (AWSSDK.S3 against SeaweedFS), `AudioTranscoderService` (ffmpeg → `.m4a`), `PictureService` (ImageSharp), `StreamTicketService` (RS256 signing). +- `MassTransit/` → consumers, activities, and routing slips (see [media-processing.md](media-processing.md)). - `Jobs/` → `UploadIntentExpirationJob`, `TemporalUploadsCleanUpJob`. ## Musify.StreamingGateway (`:8081`) -Reverse proxy (ASP.NET + **YARP**) que sirve el audio (`.m4a`) desde SeaweedFS. Valida el stream-ticket (RS256, clave pública) y, si el objeto pedido cae bajo el prefijo autorizado, reenvía al filer. +A reverse proxy (ASP.NET + **YARP**) that serves audio (`.m4a`) from SeaweedFS. Validates the stream ticket (RS256, public key) and, if the requested object falls under the authorized prefix, forwards it to the filer. -**Porqué un proyecto aparte**: escala con el tráfico de bytes independientemente de la API, tiene superficie mínima (solo sabe validar un JWT y proxiar) y permite que SeaweedFS quede en red privada. Ver [streaming.md](streaming.md). +**Why a separate project**: it scales with byte traffic independently of the API, has a minimal surface (it only knows how to validate a JWT and proxy), and lets SeaweedFS stay on a private network. See [streaming.md](streaming.md). ## Musify.Worker -Worker Service (host de fondo) que consume los eventos de MassTransit y ejecuta el procesado pesado: transcode de audio a `.m4a` (ffmpeg), generación de miniaturas (ImageSharp), transferencias de ficheros en el bucket, y los jobs de expiración/limpieza de upload intents. +A background Worker Service that consumes MassTransit events and runs the heavy processing: audio transcoding to `.m4a` (ffmpeg), thumbnail generation (ImageSharp), file transfers within the bucket, and the upload-intent expiration/cleanup jobs. -**Porqué aparte**: el trabajo CPU/IO no debe ocurrir dentro de un request HTTP; se escala y reinicia por separado. (Antes se llamaba `Musify.PictureWorker`; se renombró porque hace más que imágenes.) +**Why separate**: CPU/IO-heavy work shouldn't happen inside an HTTP request; it scales and restarts independently. (Previously named `Musify.PictureWorker`; renamed once it started doing more than just images.) -## web-player (`:5173` en dev, `:3000` en contenedor) +## web-player (`:5173` in dev, `:3000` in a container) -Cliente web (SvelteKit 2 + Svelte 5 + Tailwind 4). La sesión OIDC vive en el -servidor (cookie cifrada); las llamadas al backend salen del servidor con el -access token, y las subidas se hacen contra las URLs prefirmadas de S3. +The web client (SvelteKit 2 + Svelte 5 + Tailwind 4). The OIDC session lives +on the server (an encrypted cookie); backend calls go out from the server +with the access token, and uploads go straight to the presigned S3 URLs. ## Tests -`backend/Tests/`, carpeta `/Tests/` en el `.slnx`: cinco proyectos xUnit — -`Musify.Domain.Tests`, `Musify.Application.Tests` (SQLite en memoria, sin -Docker), `Musify.Infrastructure.Tests` y `Musify.Api.Tests` (Testcontainers: -Postgres + SeaweedFS reales) y `Musify.StreamingGateway.Tests`. Detalle en +`backend/Tests/` (the `/Tests/` folder in the `.slnx`) has five xUnit projects: +`Musify.Domain.Tests`, `Musify.Application.Tests` (in-memory SQLite, no +Docker), `Musify.Infrastructure.Tests` and `Musify.Api.Tests` (Testcontainers: +real Postgres + SeaweedFS), and `Musify.StreamingGateway.Tests`. Details in [development.md](development.md#tests). -## Otras carpetas +## Other folders -- **`deploy/`** — stacks de Docker Compose (dev y prod), autocontenidos. Ver +- **`deploy/`**: self-contained Docker Compose stacks (dev and prod). See [../deploy/README.md](../deploy/README.md). -- **`docs/`** — esta documentación. +- **`docs/`**: this documentation. diff --git a/docs/storage.md b/docs/storage.md index 5d72b83..7f80bbf 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -1,48 +1,48 @@ -# Almacenamiento (SeaweedFS / S3) +# Storage (SeaweedFS / S3) -## Qué es +## What it is -**SeaweedFS** es el almacén de objetos, compatible con la API S3. Guarda los originales subidos y los derivados (audio `.m4a`, miniaturas). Se accede vía `AWSSDK.S3` desde `StorageService` (`IStorageService`). +**SeaweedFS** is the object store, S3-API compatible. It holds the uploaded originals and their derivatives (`.m4a` audio, thumbnails). Accessed via `AWSSDK.S3` from `StorageService` (`IStorageService`). -Expone: -- **S3 API** (`:8333`) — operaciones S3 (presigned URLs, put/get/list…). -- **filer HTTP** (`:8888`) — sirve los ficheros por ruta; lo usa el **StreamingGateway**. -- master (`:9333`) y volume server — internos. +It exposes: +- **S3 API** (`:8333`): S3 operations (presigned URLs, put/get/list…). +- **filer HTTP** (`:8888`): serves files by path, used by the **StreamingGateway**. +- master (`:9333`) and volume server, internal only. -Bucket de la app: **`webapi-storage`** (`ApplicationStorage:Bucket`). Importante: la API y el Worker usan **el mismo bucket** (el worker procesa lo que sube la API). +App bucket: **`webapi-storage`** (`ApplicationStorage:Bucket`). Important: the API and the Worker use **the same bucket** (the worker processes whatever the API uploads). -## Estructura de claves (layout) +## Key layout -- `temp/{userId}/Tracks/{guid}.{ext}` — subidas temporales (objetivo de las presigned PUT, antes de consumir el intent). -- `uploads/{userId}/Tracks/OriginalPictures|OriginalAudios/...` — originales ya consumidos. -- `Tracks/SmallPictures|MediumPictures|LargePictures/...` — miniaturas generadas. -- `Tracks/ProcessedAudios/{AudioFolderName}/` — audio transcodificado (`audio.m4a`, AAC con faststart). +- `temp/{userId}/Tracks/{guid}.{ext}`: temporary uploads, the target of presigned PUTs, before the intent is consumed. +- `uploads/{userId}/Tracks/OriginalPictures|OriginalAudios/...`: originals that have already been consumed. +- `Tracks/SmallPictures|MediumPictures|LargePictures/...`: generated thumbnails. +- `Tracks/ProcessedAudios/{AudioFolderName}/`: transcoded audio (`audio.m4a`, AAC with faststart). -Las claves se construyen **siempre** con el helper `StorageKey.Combine(...)` (en `Application/Shared`), que une segmentos con `/` limpiando espacios y barras. Está centralizado para estandarizar y no repetir lógica. +Keys are always built with the `StorageKey.Combine(...)` helper (in `Application/Shared`), which joins segments with `/` and trims stray spaces and slashes. It's centralized to keep the logic consistent and avoid repeating it. -> En el filer, los objetos S3 viven bajo `/buckets/{bucket}/{key}` — de ahí que el gateway reescriba `/media/{key}` → `/buckets/webapi-storage/{key}`. +> In the filer, S3 objects live under `/buckets/{bucket}/{key}`, which is why the gateway rewrites `/media/{key}` → `/buckets/webapi-storage/{key}`. -## URLs prefirmadas (presigned) +## Presigned URLs -Para **no** hacer pasar ficheros grandes por la API, el cliente habla directo con S3 mediante URLs firmadas de corta vida: +To **avoid** routing large files through the API, the client talks to S3 directly using short-lived signed URLs: -- **Subidas (PUT)**: `RequestTrackUploadUrls` / `RequestPlayListPictureUpload` generan presigned PUT (con `Content-Type` y `If-None-Match: *` firmados, expiración ~120s). El cliente sube directo a S3. -- **Imágenes (GET)**: las miniaturas se sirven con presigned GET (un objeto = una URL). +- **Uploads (PUT)**: `RequestTrackUploadUrls` / `RequestPlayListPictureUpload` generate presigned PUTs (with signed `Content-Type` and `If-None-Match: *`, ~120s expiry). The client uploads straight to S3. +- **Images (GET)**: thumbnails are served via presigned GETs (one object = one URL). -**Por qué presigned y no proxiar**: subir un audio de ~100 MB a través de la API consumiría memoria/ancho de banda de los servidores de aplicación y escalaría mal. La presigned PUT directa a S3 es el patrón estándar y correcto. +**Why presigned instead of proxying**: routing a ~100 MB audio file through the API would burn memory and bandwidth on the application servers and wouldn't scale well. A direct presigned PUT to S3 is the standard, correct pattern here. -### Upload intents y cuota -Cada `upload-urls` crea **UploadIntents** (reservas en estado `Issued`) y devuelve las URLs. Al crear la pista/playlist se **consumen**. Hay cuota por usuario (`MaxActiveUploadIntentsPerUser`, bytes…) para evitar abuso. El `UploadIntentExpirationJob` (en el Worker) marca como expirados los intents caducados y libera la cuota; si ese job no corre, los intents colgados se acumulan y bloquean nuevas subidas. +### Upload intents and quota +Every `upload-urls` call creates **UploadIntents** (reservations in the `Issued` state) and returns the URLs. They're **consumed** when the track/playlist is created. There's a per-user quota (`MaxActiveUploadIntentsPerUser`, byte limits…) to prevent abuse. The `UploadIntentExpirationJob` (in the Worker) marks expired intents and frees up quota. If that job isn't running, stale intents pile up and block new uploads. -## Qué es público y qué privado (despliegue) +## What's public and what's private (deployment) -| Exponer a internet | Mantener privado | +| Expose to the internet | Keep private | |---|---| -| S3 API `:8333` (tras TLS) — necesario para presigned PUT/GET del cliente | filer `:8888` (solo el gateway, en red interna) | -| StreamingGateway `:8081` | master `:9333`, volume server, UI de admin | +| S3 API `:8333` (behind TLS), needed for the client's presigned PUT/GET | filer `:8888` (internal network, gateway only) | +| StreamingGateway `:8081` | master `:9333`, volume server, admin UI | -- **El audio** se sirve por el gateway → SeaweedFS puede quedar privado para ese flujo. -- **Subidas e imágenes** usan presigned → el cliente toca el endpoint S3 directamente, así que `:8333` debe ser alcanzable (pero inútil sin firma válida; es el modelo de cualquier bucket S3/MinIO). -- En **producción**: `InfrastructureStorage:Address` debe ser el **endpoint S3 público** (la firma se calcula para ese host); si los servidores no lo alcanzan internamente, se usa config dividida (host público para firmar / interno para operaciones server-side). +- **Audio** is served through the gateway, so SeaweedFS can stay private for that flow. +- **Uploads and images** use presigned URLs, so the client talks to the S3 endpoint directly and `:8333` has to be reachable (though useless without a valid signature, this is the standard model for any S3/MinIO bucket). +- In **production**: `InfrastructureStorage:Address` must be the **public S3 endpoint** (the signature is computed for that host). If the servers can't reach it internally, use split config: public host for signing, internal host for server-side operations. -**Regla**: el gateway y, como mucho, el `:8333` firmado son la única superficie pública; master/volume/filer/admin **nunca** se exponen. +**Rule of thumb**: the gateway, and at most the signed `:8333`, are the only public surface. Master, volume, filer and admin are **never** exposed. diff --git a/docs/streaming.md b/docs/streaming.md index 5f28c27..1f6ba1a 100644 --- a/docs/streaming.md +++ b/docs/streaming.md @@ -1,55 +1,55 @@ -# Streaming de audio +# Audio streaming -## Objetivo +## Goal -Reproducir el audio **sin que los bytes pasen por la API**, pero con control de acceso. La API solo emite un permiso corto; el reverse proxy sirve los bytes desde SeaweedFS. +Play audio **without the bytes passing through the API**, while still controlling access. The API only issues a short-lived permission; the reverse proxy serves the bytes from SeaweedFS. -## Formato: fichero único AAC (.m4a) por HTTP range +## Format: a single AAC file (.m4a), served via HTTP range -El audio se transcodifica a **un único `audio.m4a`** (AAC, contenedor MP4 con `+faststart` para que el átomo `moov` quede al inicio y el seek por range funcione), dentro de la carpeta `Tracks/ProcessedAudios/{AudioFolderName}/`. No hay manifest ni segmentos: el reproductor descarga el fichero por **HTTP range requests** (modelo tipo Spotify para VOD musical). +Audio is transcoded into **one `audio.m4a`** (AAC, MP4 container with `+faststart` so the `moov` atom sits at the start and range-based seeking works), stored under `Tracks/ProcessedAudios/{AudioFolderName}/`. There's no manifest or segments: the player downloads the file via **HTTP range requests** (the same model Spotify uses for on-demand music). -Multi-bitrate no está implementado hoy; el camino para añadirlo es transcodificar varias calidades (`audio_96.m4a`, …) y que el cliente elija una al iniciar (sin ABR fluido a mitad de pista, igual que Spotify). +Multi-bitrate isn't implemented yet; the path to add it is transcoding several qualities (`audio_96.m4a`, …) and letting the client pick one at playback start (no seamless mid-track ABR, same as Spotify). -## El acceso: stream-ticket de prefijo +## Access control: a prefix-scoped stream ticket -Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de corta vida, y el proxy lo valida en cada petición. Aunque hoy sea un único objeto, el ticket sigue acotado al prefijo `Tracks/ProcessedAudios//` (cubre el fichero actual y cualquier calidad futura). +Access is authorized for **the track's folder prefix** via a short-lived *stream ticket*, validated by the proxy on every request. Even though there's only one object today, the ticket is still scoped to the prefix `Tracks/ProcessedAudios//` (covering the current file and any future quality variants). -## Flujo +## Flow -1. `GET /tracks/{id}/stream` (autenticado) → la API autoriza y devuelve: +1. `GET /tracks/{id}/stream` (authenticated) → the API authorizes the request and returns: ```json { "manifestUrl": "http:///media/Tracks/ProcessedAudios//audio.m4a", "ticket": "", "expiresInSeconds": 3600 } ``` - El ticket lleva el claim `prefix = "Tracks/ProcessedAudios//"`. -2. El reproductor abre la URL del audio añadiéndole `?t=` (o la cabecera `X-Stream-Ticket`). -3. El **StreamingGateway** (YARP) valida el ticket y reenvía al filer; los bytes van SeaweedFS → cliente (con range/seek). + The ticket carries the claim `prefix = "Tracks/ProcessedAudios//"`. +2. The player opens the audio URL with `?t=` appended (or the `X-Stream-Ticket` header). +3. The **StreamingGateway** (YARP) validates the ticket and forwards the request to the filer; the bytes flow SeaweedFS → client (with range/seek support). -## El ticket (RS256) +## The ticket (RS256) -- Lo firma la API con clave **privada** (`StreamTicketService`); el gateway valida con la **pública** (`TicketValidator`). Así el gateway **no puede emitir** tickets, solo verificarlos. -- Claims: `sub`, `prefix`, `aud = media-gateway`, `iss = musify-webapi`, `exp` (TTL ~1h). +- Signed by the API with a **private** key (`StreamTicketService`); validated by the gateway with the **public** one (`TicketValidator`). That way the gateway **can't issue** tickets, only verify them. +- Claims: `sub`, `prefix`, `aud = media-gateway`, `iss = musify-webapi`, `exp` (~1h TTL). -**Por qué RS256 (asimétrica)**: separa responsabilidades — emisor (la API) y verificador (gateway) no comparten secreto; el gateway, aunque se comprometa, no puede crear permisos. +**Why RS256 (asymmetric)**: it separates responsibilities. The issuer (API) and the verifier (gateway) don't share a secret, so even if the gateway is compromised, it can't mint permissions. -## El gateway (validación + proxy) +## The gateway (validation + proxy) -`TicketValidationMiddleware` en cada petición a `/media/**`: -1. Lee `?t=`, valida firma/aud/iss/exp. -2. Comprueba que la clave pedida **empieza por** el `prefix` del ticket (`StartsWith`). Si no → 403. -3. Quita el `?t=` y deja pasar a YARP, que reescribe `/media/{key}` → `/buckets/webapi-storage/{key}` del filer. +`TicketValidationMiddleware`, on every request to `/media/**`: +1. Reads `?t=`, validates its signature/aud/iss/exp. +2. Checks that the requested key **starts with** the ticket's `prefix`. If not → 403. +3. Strips the `?t=` and passes the request on to YARP, which rewrites `/media/{key}` → `/buckets/webapi-storage/{key}` for the filer. -- Sin ticket → **401**; fuera de prefijo → **403**; ok → **200** (y **206** en range requests, así que el seek funciona). -- Pasa `Range`/`Accept-Ranges` y expone esos headers por CORS. +- No ticket → **401**; outside the prefix → **403**; ok → **200** (and **206** for range requests, so seeking works). +- Passes through `Range`/`Accept-Ranges` and exposes those headers via CORS. -## ¿Un ticket por canción? +## One ticket per track? -Sí. Cada ticket está acotado a **una** carpeta (una pista). Para reproducir otra, se pide otro ticket. Es barato (la API solo firma un JWT) y es lo más seguro: cada permiso abre solo lo que vas a reproducir. +Yes. Each ticket is scoped to **one** folder (one track). Playing a different one means requesting a new ticket. It's cheap (the API just signs a JWT) and it's the safest option: every permission only unlocks what you're about to play. -- Si algún día molesta (muchísimos cambios de pista), el mismo mecanismo del claim `prefix` permite ampliar el alcance: a nivel de playlist o de toda la biblioteca (`Tracks/ProcessedAudios/`). Trade-off seguridad ↔ comodidad. Por defecto, **por canción**. +- If that ever becomes a problem (very frequent track switching), the same `prefix` claim mechanism can widen the scope to a whole playlist or the whole library (`Tracks/ProcessedAudios/`). It's a security/convenience trade-off. The default is **per track**. -## Pendiente / notas de seguridad +## Open items / security notes -- La política de quién puede pedir el ticket es hoy "cualquier autenticado" (TODO: restringir con `UserHasTrack`/visibilidad). -- El check de prefijo es `StartsWith` literal; conviene endurecer rechazando claves con `..` (path traversal), aunque navegadores y ASP.NET normalizan `..`. -- El ticket viaja en la query (`?t=`); el gateway lo elimina antes de reenviar (no llega a los logs del filer). +- Today, the policy for who can request a ticket is "any authenticated user" (TODO: restrict it with `UserHasTrack`/visibility). +- The prefix check is a literal `StartsWith`; it would be worth hardening it against `..` path traversal, even though browsers and ASP.NET normalize `..`. +- The ticket travels in the query string (`?t=`); the gateway strips it before forwarding, so it never reaches the filer's logs. diff --git a/web-player/design_handoff_musify_player/Crear playlist.dc.html b/web-player/design_handoff_musify_player/Crear playlist.dc.html deleted file mode 100644 index 43fa0e7..0000000 --- a/web-player/design_handoff_musify_player/Crear playlist.dc.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - -
- -
- -
- -
- - -
-
-
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 deleted file mode 100644 index e7bf466..0000000 --- a/web-player/design_handoff_musify_player/Musify.dc.html +++ /dev/null @@ -1,1127 +0,0 @@ - - - - - - - - - - - - - - -
- -
-
- -
- - - - - -
- -
-
-
- -
-
- -
-
- -
- - -
- -
- -
- - 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 }}
-
-
-
-
-
- - -
-
-

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 }}
-
-
-
-
- - -
-

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 }}
-
-
-
-
- - -
-
-
-

Tus playlists

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

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 -
-
- -
-
-
-
- -
-
#
Título
Álbum
Duración
-
- -
-
- -
-
-
-
-
-
- - {{ t.n }} - -
-
-
-
-
-
-
{{ t.title }}
-
{{ t.artist }}
-
-
-
{{ t.album }}
-
-
- -
-
{{ t.dur }}
-
-
-
-
-
- - - -
-
-
-

Tus playlists

-
7 playlists · 564 canciones · 38 h 12 min
-
-
Nueva playlist
-
-
- -
-
-
-
-
- -
-
-
{{ p.name }}
-
{{ p.meta }}
-
{{ p.artistsLine }}
-
-
-
-
-
- - - -
-
- -
-
{{ emptyTitle }}
-
{{ emptyBody }}
-
-
{{ emptyCta }}
-
Volver al inicio
-
-
-
- - - -
-
-
Resultados de búsqueda
-

{{ searchHeading }}

-
{{ searchMeta }}
-
- -
- -
- {{ c.label }} - {{ c.count }} -
-
-
- - -
-
-
-
-
-
Mejor resultado · {{ top.kind }}
-
{{ top.title }}
-
{{ top.sub }}
-
-
- -
-
-
- - -
-
-
{{ g.label }}
-
-
{{ g.count }}
-
- -
-
-
-
- -
-
-
-
-
{{ r.title }}
-
{{ r.badge }}
-
-
{{ r.sub }}
-
-
{{ r.meta }}
-
-
-
-
-
-
- - - -
-
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 }}
-
-
-
-
- -
-
- - - - - -
- - -
-
- -
-
-
-
-
-
{{ curTitle }}
-
{{ curArtist }}
-
-
- -
-
- -
-
-
- -
-
- -
-
- - - - - - -
-
- -
-
- -
-
-
- {{ elapsed }} -
-
-
-
-
- {{ total }} -
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
-
- - -
- -
- - {{ n.label }} -
-
-
-
-
-
- - - diff --git a/web-player/design_handoff_musify_player/README.md b/web-player/design_handoff_musify_player/README.md deleted file mode 100644 index 2e3de53..0000000 --- a/web-player/design_handoff_musify_player/README.md +++ /dev/null @@ -1,399 +0,0 @@ -# Handoff: Musify — Web Music Player - -## Overview - -Musify es un reproductor de música web: un player permanente, navegación lateral, y vistas de Home / Descubrir / Playlists / Me gusta / Reciente, más estados de carga, error, vacío y sin resultados. Su rasgo de identidad es que **toda la paleta de acento y los fondos derivan de la canción en reproducción**: al cambiar de pista, el tinte del hero, el glow ambiental, el sidebar, la barra del player, el logo, la barra de progreso y los acentos viran en bloque con una transición de 0.6s. - -## About the Design Files - -Los archivos de este paquete son **referencias de diseño hechas en HTML** — prototipos que muestran aspecto y comportamiento previstos, **no código de producción para copiar tal cual**. La tarea es **recrear estos diseños en el entorno del codebase destino** (React, Vue, SwiftUI, nativo…) siguiendo sus patrones y librerías ya establecidos. Si todavía no hay entorno, elige el framework más apropiado para el proyecto e implementa allí. - -El prototipo está escrito como un componente único con estado local y estilos inline. En producción conviene separarlo en componentes (Sidebar, TopBar, Player, Queue, TrackRow, MediaCard, sections de Home) y mover los tokens a la solución de estilos del proyecto. - -## Fidelity - -**Alta fidelidad (hifi).** Colores, tipografía, espaciado, radios, estados y microinteracciones son definitivos. Se espera una recreación fiel usando las librerías del codebase. Las carátulas son fotografías genéricas de marcador de posición; el contenido musical (artistas, álbumes, canciones, playlists, duraciones, reproducciones) es **mock data** y debe sustituirse por datos reales. - -**Regla de producto:** la UI nunca menciona la procedencia de la música. Sin etiquetas de fuente, proveedor ni "powered by". Para el usuario esto es solo Musify. - ---- - -## Layout global - -Altura fija de viewport, sin scroll de página; solo el área de contenido y la cola scrollean. - -``` -┌──────────┬──────────────────────────────┬──────────┐ -│ Sidebar │ TopBar │ Queue │ -│ 254px ├──────────────────────────────┤ 304px │ -│ │ Main (scroll) │ (258px │ -│ │ │ <1180) │ -└──────────┴──────────────────────────────┴──────────┘ - Player flotante (absolute, left/right 0, bottom 0, padding 0 16px 16px) -``` - -- Raíz: `position:relative; height:100vh; overflow:hidden; background: pageBg; transition: background .6s ease`. -- Dos capas decorativas absolutas y no interactivas sobre el fondo: el **ambient glow** y un scrim inferior `radial-gradient(130% 85% at 50% 118%, rgba(0,0,0,.8), transparent 62%)`. -- Fila principal `display:flex; height:100%`. -- El main tiene `padding: 6px 26px` y `padding-bottom` dinámico: `126px` en desktop, `206px` cuando hay barra de pestañas móvil. - -### 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` | - ---- - -## Sistema de color dinámico - -Cada pista lleva un `hue` (0–360) asignado. Todo el color de la app se deriva de ese hue en oklch. Los valores están repartidos por la rueda para que el cambio entre pistas consecutivas sea evidente (paso mínimo ~50°). - -```js -// hue = hue de la pista en reproducción -accent = oklch(0.74 0.145 H) // acción, barra de progreso, corazón activo, EQ -accentTitle = oklch(0.87 0.07 H) // títulos sobre superficie tintada -accentMuted = oklch(0.79 0.035 H) // título de la pista activa en tarjetas -accentSoft = oklch(0.74 0.145 H / .08) // fondo de fila/tarjeta activa -accentLine = oklch(0.74 0.145 H / .2) // borde de superficie acentuada -accentHair = oklch(0.74 0.145 H / .09) // borde de tarjeta activa -accentBtnBg = oklch(0.74 0.145 H / .15) -accentBtnBgHover = oklch(0.74 0.145 H / .24) - -// superficies -pageBg = oklch(0.068 0.008 H) -sidebarBg = linear-gradient(180deg, oklch(0.095 0.012 H), oklch(0.068 0.007 H)) -panelBg = oklch(0.09 0.010 H / .28) // panel de cola -barBg = oklch(0.105 0.012 H / .9) // player y tabs (con backdrop-filter: blur(22px)) -hairline = oklch(0.62 0.030 H / .08) // todos los separadores - -// gradientes: tc(a, l) = oklch(0.34 - l/380 , 0.11 - l/1600 , H / a) -ambient = radial-gradient(52% 38% at 14% 0%, tc(.26,30), transparent 54%), - radial-gradient(46% 34% at 78% 0%, tc(.20,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%) -spotlightBg = linear-gradient(100deg, tc(.28,46) 0%, tc(.10,74) 30%, tc(.03,88) 52%, rgba(6,6,9,0) 72%) -logoGrad = linear-gradient(140deg, oklch(0.78 0.15 H), oklch(0.62 0.13 H+40) 92%) -logoGlow = 0 0 20px oklch(0.74 0.145 H / .3) -``` - -**Importante:** hero y spotlight terminan en `alpha 0` antes del borde derecho (70–72%) y **no llevan borde**, para fundirse con el fondo sin corte visible. - -Los elementos que cambian de color reciben `transition: background .6s ease` (y `box-shadow .6s ease` en el logo): raíz, sidebar, panel de cola, barra del player, tabs. - -### 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` | - -Los grises son **neutros a propósito** (sin tinte), para que solo el acento lleve color. - ---- - -## Tipografía - -- Display / títulos: **Sora** 400/500/600/700. -- 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` | - -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`. - ---- - -## Escalas - -**Radios:** 8px (miniaturas pequeñas, chips) · 9–11px (carátulas 36–48px, botones) · 12–13px (filas activas, carátulas de tarjeta) · 14–16px (tarjetas, portadas grandes) · 18px (player, tabs, portada de cabecera) · 22–24px (hero, spotlight) · 50% (avatares, botón de play, artistas) · 999px (chips de sugerencia). - -**Sombras:** carátula de tarjeta `0 14px 32px rgba(0,0,0,.45)` · artista `0 12px 28px rgba(0,0,0,.4)` · portada de cabecera `0 24px 56px rgba(0,0,0,.55)` · player `0 20px 56px rgba(0,0,0,.6)` · botón primario hero `0 10px 28px rgba(0,0,0,.45)`. - -**Gaps de grid:** tarjetas 20px · artistas 18px · playlists 14–22px · filas compactas 6–14px. - -**Anchos mínimos de columna (auto-fill):** mixes/álbumes/descubrir 164px · artistas 132px · playlists (tarjeta ancha) 258px · playlists (índice) 196px · continuar escuchando 268px · populares 330px · géneros 228px. - ---- - -## Pantallas - -### 1. Sidebar (`>= 1180px`) - -`width:254px; padding:22px 12px 0 20px; border-right:1px solid hairline; background: sidebarBg`. - -- **Wordmark**: cuadrado 24px con `logoGrad` + `logoGlow`, gap 11px, texto "Musify". Padding inferior 26px. -- **Nav** (columna, gap 2px). Ítem: `padding:9px 12px; border-radius:10px; font-size:13.5px; weight:500`, icono SVG 17px stroke 1.6, etiqueta flexible, contador a la derecha (11px, `#5c5c60`, tabular). - - Activo: color `#d5d4d8`, fondo `rgba(255,255,255,.06)`. - - Inactivo: color `#8b8b8f`, fondo transparente. Hover: fondo `rgba(255,255,255,.04)`, color `#d5d4d8`. Transición `.16s`. - - Ítems: Inicio · Descubrir · Playlists (7) · Me gusta (312) · Reciente. -- **Separador**: `margin:24px 12px 14px; height:1px; background:rgba(255,255,255,.05)`. -- **Cabecera "TUS PLAYLISTS"** (10.5px uppercase, 0.13em, `#616165`) con un "+" a la derecha que navega al índice de playlists. -- **Lista de playlists** (scroll, gap 1px). Fila: miniatura 34px radio 8px (imagen `opacity:.88`), nombre 12.5px, meta 11px `#6c6c70`. Si esa playlist está sonando, un ecualizador de 3 barras de 2px a la derecha. - -### 2. TopBar - -`padding:16px 26px 12px; gap:14px`. - -- Atrás / adelante: círculos de 30px, fondo `rgba(255,255,255,.035)`, borde `rgba(255,255,255,.05)`, icono 15px. Atrás activo (`#8b8b8f`, hover `#dbdadf`), adelante deshabilitado (`#555559`). -- Buscador: `flex:1; max-width:480px; height:38px; radius:11px`, fondo `rgba(255,255,255,.035)`, borde `rgba(255,255,255,.055)`, lupa 15px `#737377`, input 13px transparente. Placeholder: "Canciones, artistas, álbumes o playlists". -- Spacer. -- Botón "Cola": alto 33px, radio 10px, 12.5px/500, icono 15px. Activo: color `#d6d5da`, fondo `rgba(255,255,255,.06)`. Inactivo: `#8b8b8f`, fondo `rgba(255,255,255,.028)`. -- Avatar 33px circular con borde `rgba(255,255,255,.1)`. - -### 3. Home - -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*). -- 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". - -**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. - -**d) Mixes** — H2 "Mixes" + sub "Generados a partir de lo que más repites". Grid `minmax(164px,1fr)` gap 20px. Tarjeta cuadrada radio 13px con scrim `linear-gradient(180deg,transparent 45%,rgba(0,0,0,.42))` y botón de play circular 38px abajo a la derecha (`rgba(10,10,13,.76)`, blur 8px, borde `rgba(255,255,255,.12)`) que aparece en hover. - -**e) Artistas que sigues** — grid `minmax(132px,1fr)` gap 18px, retratos circulares, texto centrado, nombre 12.5px/600, oyentes 11px. - -**f) Populares esta semana** — grid de dos columnas `auto-fit minmax(330px,1fr)`, `gap:6px 34px`. Fila: índice/EQ 18px, carátula 38px, título + artista, reproducciones 11px y duración 11.5px. - -**g) Tus playlists** — tarjetas anchas `minmax(258px,1fr)` gap 14px: portada 62px radio 11px, nombre 13.5px/600, nº de canciones 11.5px, artistas principales 11px `#696a6d`, play en hover. Enlace "VER TODAS" en la cabecera. - -### 4. Descubrir - -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) - -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. - -### 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`. -- Lista scrollable: miniatura 36px, título 12px, artista 11px, duración. - -### 8. Player (siempre visible) - -Barra flotante: `radius:18px; padding:11px 16px; background: barBg; backdrop-filter: blur(22px); border:1px solid hairline; box-shadow:0 20px 56px rgba(0,0,0,.6)`; `pointer-events:none` en el contenedor y `auto` en la barra. - -- **Izquierda (256px):** carátula 48px radio 11px, título 13px/600, artista · álbum 11.5px, corazón 16px (activo en `accent`). -- **Centro (flex:1):** shuffle · anterior · play/pausa · siguiente · repetir, gap 14px. Play: círculo 40px `#c6c4cb` con icono `#0a0a0d`, hover `#d6d5da` (**sin scale**). Shuffle/repeat activos en `accent`, inactivos `#747478`. - Progreso: tiempo transcurrido (10.5px tabular, ancho mín. 32px) · pista `rgba(255,255,255,.09)` de 3.5px con relleno en `accent` · duración. Clic en la pista = seek proporcional. -- **Derecha (256px):** letra, volumen (icono + pista de 84px con relleno `#8e8c98`), y botón de cola. Oculto por completo en móvil. - -### 9. Barra de pestañas (`< 1180px`) - -Bajo el player, `margin-top:10px; padding:7px; radius:18px`, mismo `barBg` y blur. Icono 17px + etiqueta 10px, distribuidos con `space-around`. Muestra los 5 ítems de navegación. - ---- - -## Estados - -- **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:** 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 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`. -- Entrada de vista: `rise` (`opacity 0 → 1`, `translateY(12px) → 0`) en `.3s ease`. Cola: `.22s`. -- EQ de pista activa: `eq` `.9s ease-in-out infinite` alternando `scaleY(.25)` y `scaleY(1)`, con desfases de 0.25/0.5s (0.3/0.6s en el sidebar). - ---- - -## Estado necesario - -``` -view 'home' | 'explore' | 'playlists' | 'playlist' | 'liked' | 'recent' -listCtx nombre de la playlist abierta -idx índice de la pista en reproducción -playing boolean -pos segundos transcurridos (intervalo de 1s, vuelve a 0 al llegar a la duración) -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) -w ancho de ventana (listener de resize) -demoState 'normal' | 'loading' | 'error' | 'empty' (solo para demostrar estados) -``` - -### Datos esperados - -`Track { id, title, artist, album?, duration, cover?, hue, plays? }` · -`Album { title, artist, year, cover? }` · -`Artist { name, listeners, photo? }` · -`Playlist { name, trackCount, topArtists, cover?, isPlaying? }` - -Todos los campos opcionales deben tener degradado o texto de respaldo. El `hue` debería calcularse en backend desde la carátula (color dominante, con saturación mínima garantizada) y venir en el payload; en el prototipo está precalculado por pista. - ---- - -## 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: - -- `< 5h` → "¿Sesión de madrugada, {nombre}?" -- `< 13h` → "Buenos días, {nombre}" -- `< 20h` → "Buenas tardes, {nombre}" (miércoles → "Feliz miércoles"; viernes → "Feliz viernes") -- `>= 20h` → "Buenas noches, {nombre}" -- Sábado/domingo → "Que tengas buen finde, {nombre}" (o "¿Madrugada de finde, {nombre}?" antes de las 5h) - ---- - -## Assets - -- **Carátulas y retratos:** marcadores de posición fotográficos servidos por `picsum.photos` con una semilla por elemento, pintados como `background-image` (no ``) para evitar peticiones fallidas mientras se resuelven los datos. Sustituir por las imágenes reales del catálogo. -- **Iconos:** SVG inline, `stroke-width` 1.6–1.8, `stroke-linecap:round`. Reemplazables por el icon set del codebase. -- **Fuentes:** Sora y Plus Jakarta Sans (Google Fonts). -- Sin logotipos de terceros. El wordmark de Musify es tipográfico + el cuadrado con degradado. - -## 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` - 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/screenshots/01-home-hero.png b/web-player/design_handoff_musify_player/screenshots/01-home-hero.png deleted file mode 100644 index deed3a9d70baf915b960c47e06bc24e558e6cf9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32701 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 G<6i(Is}hL- diff --git a/web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png b/web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png deleted file mode 100644 index f7a6ecfd07017b5d1e03d56b613028a913161691..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32697 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 BMyLP) diff --git a/web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png b/web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png deleted file mode 100644 index 8ad339932f1aeae51a73bfdbad3e871bde3f218f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32691 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 GKK~o0cS(i- diff --git a/web-player/design_handoff_musify_player/screenshots/05-descubrir.png b/web-player/design_handoff_musify_player/screenshots/05-descubrir.png deleted file mode 100644 index a5f9424270690cf63217c07f1c9efee21b3ec239..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34053 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 CEJWS_ diff --git a/web-player/design_handoff_musify_player/screenshots/06-playlists.png b/web-player/design_handoff_musify_player/screenshots/06-playlists.png deleted file mode 100644 index 37e8b8c804db26bbf41c4ab815829b0204eece16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37419 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 diff --git a/web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png b/web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png deleted file mode 100644 index 125a45888f26633c2fb20ab3bbf3d412224d3c30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32766 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 diff --git a/web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png b/web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png deleted file mode 100644 index 98ebf7f40f98f4b6128c31e948b795d472e7af53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28546 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 diff --git a/web-player/design_handoff_musify_player/support.js b/web-player/design_handoff_musify_player/support.js deleted file mode 100644 index cb009b6..0000000 --- a/web-player/design_handoff_musify_player/support.js +++ /dev/null @@ -1,1911 +0,0 @@ -// 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: