From 52e559a1596fbde85e7199ed1aeaee55d496a302 Mon Sep 17 00:00:00 2001 From: Jaume Garriga Maestre Date: Sun, 21 Jun 2026 10:48:57 +0200 Subject: [PATCH] feat: app completa recordaLexia (fases 1-5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App web familiar de rutinas visuales para niños con TDAH: muestra cada día el material del cole y las rutinas de tarde, con gamificación por monedas y tienda de recompensas. Multi-niño y bilingüe ES/CA. Uso doméstico/homelab. Backend (Spring Boot 3.5 / Java 21 / Gradle): - Dominio por capas, PostgreSQL + Liquibase, datos semilla. - API REST con DTOs: /today, toggle con monedas y bonos de bloque/día, monedero, tienda/canje, ajustes y CRUD del panel de padres. - Seguridad ligera por PIN (BCrypt + sesion en memoria), sin Keycloak. - Tests JUnit: generacion del dia, monedas/bonos con reversion, canje, seguridad. Frontend (Angular 19, standalone + signals): - Perfiles, Home (Tablero y Foco), Tienda y panel de padres (5 pestañas). - Tipografia OpenDyslexic conmutable (accesibilidad), i18n ES/CA, TTS y sonido. - Tokens de diseño fieles al handoff (paleta, animaciones, monedas voladoras). Empaquetado: - Docker multi-stage + docker-compose (PostgreSQL + backend + Nginx). - Decisiones de arquitectura documentadas en docs/adr. --- .env.example | 13 + .gitignore | 25 + CLAUDE.md | 66 + README.md | 140 + app-de-rutinas-visuales-para-tdah/README.md | 22 + .../project/.thumbnail | Bin 0 -> 2314 bytes .../project/Rutinas TDAH.dc.html | 630 + .../project/support.js | 1513 ++ .../.thumbnail | Bin 0 -> 2314 bytes .../Rutinas TDAH.dc.html | 630 + .../support.js | 1513 ++ backend/.dockerignore | 7 + backend/.gitattributes | 3 + backend/.gitignore | 37 + backend/Dockerfile | 33 + backend/build.gradle | 38 + backend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + backend/gradlew | 251 + backend/gradlew.bat | 94 + backend/settings.gradle | 1 + .../recordalexia/RecordalexiaApplication.java | 13 + .../recordalexia/bootstrap/DataSeeder.java | 209 + .../recordalexia/config/TimeConfig.java | 22 + .../asepeyo/recordalexia/domain/Activity.java | 98 + .../recordalexia/domain/AfternoonRoutine.java | 130 + .../es/asepeyo/recordalexia/domain/Child.java | 210 + .../recordalexia/domain/CoinTransaction.java | 74 + .../recordalexia/domain/DailyTask.java | 148 + .../recordalexia/domain/EventType.java | 7 + .../asepeyo/recordalexia/domain/Language.java | 7 + .../recordalexia/domain/MaterialItem.java | 83 + .../recordalexia/domain/ParentUser.java | 53 + .../asepeyo/recordalexia/domain/Reward.java | 92 + .../recordalexia/domain/RewardRedemption.java | 74 + .../es/asepeyo/recordalexia/domain/Slot.java | 7 + .../recordalexia/domain/SpecialEvent.java | 117 + .../recordalexia/domain/TaskOrigin.java | 8 + .../recordalexia/domain/TaskStatus.java | 7 + .../asepeyo/recordalexia/domain/ViewMode.java | 7 + .../domain/WeeklyTemplateEntry.java | 100 + .../exception/GlobalExceptionHandler.java | 32 + .../exception/InsufficientCoinsException.java | 19 + .../exception/NotFoundException.java | 9 + .../repository/ActivityRepository.java | 7 + .../AfternoonRoutineRepository.java | 14 + .../repository/ChildRepository.java | 7 + .../repository/CoinTransactionRepository.java | 28 + .../repository/DailyTaskRepository.java | 22 + .../repository/MaterialItemRepository.java | 7 + .../repository/ParentUserRepository.java | 11 + .../RewardRedemptionRepository.java | 10 + .../repository/RewardRepository.java | 11 + .../repository/SpecialEventRepository.java | 14 + .../WeeklyTemplateEntryRepository.java | 14 + .../security/ParentAuthFilter.java | 42 + .../security/ParentAuthService.java | 54 + .../security/ParentSessionStore.java | 52 + .../recordalexia/security/SecurityConfig.java | 63 + .../recordalexia/service/ChildService.java | 121 + .../recordalexia/service/CoinReason.java | 25 + .../service/DayGenerationService.java | 88 + .../service/ProgressCalculator.java | 38 + .../recordalexia/service/StoreService.java | 86 + .../recordalexia/service/TaskService.java | 124 + .../recordalexia/service/TodayService.java | 101 + .../recordalexia/service/WalletService.java | 39 + .../recordalexia/web/ChildController.java | 65 + .../web/ParentAuthController.java | 40 + .../web/ParentCatalogController.java | 106 + .../web/ParentChildController.java | 74 + .../web/ParentEventController.java | 72 + .../web/ParentRewardController.java | 87 + .../web/ParentScheduleController.java | 135 + .../recordalexia/web/StoreController.java | 27 + .../recordalexia/web/TaskController.java | 25 + .../recordalexia/web/dto/ChildDtos.java | 42 + .../recordalexia/web/dto/ParentDtos.java | 53 + .../recordalexia/web/dto/ParentViews.java | 42 + .../recordalexia/web/dto/StoreDtos.java | 34 + .../recordalexia/web/dto/TodayResponse.java | 65 + .../recordalexia/web/dto/ToggleResult.java | 15 + .../recordalexia/web/dto/WalletResponse.java | 17 + backend/src/main/resources/application.yml | 54 + .../resources/db/changelog/changes/.gitkeep | 1 + .../db/changelog/changes/001-schema.yaml | 261 + .../db/changelog/db.changelog-master.yaml | 9 + .../RecordalexiaApplicationTests.java | 13 + .../recordalexia/bootstrap/DataSeederIT.java | 41 + .../service/DayGenerationServiceTest.java | 66 + .../service/StoreServiceTest.java | 63 + .../recordalexia/service/TaskServiceTest.java | 82 + .../recordalexia/web/ParentAuthIT.java | 68 + .../asepeyo/recordalexia/web/TodayFlowIT.java | 92 + backend/src/test/resources/application.yml | 18 + docker-compose.yml | 55 + docs/adr/0001-spring-boot-3x.md | 31 + docs/adr/0002-tipografia-opendyslexic.md | 39 + docs/adr/0003-dominio-y-seguridad-fase2.md | 51 + ...prompt-claude-code-backend-rutinas-tdah.md | 114 + ...rompt-claude-code-recordalexia-director.md | 129 + docs/prompt-claude-design-rutinas-tdah.md | 100 + frontend/.dockerignore | 6 + frontend/.editorconfig | 17 + frontend/.gitignore | 42 + frontend/.vscode/extensions.json | 4 + frontend/.vscode/launch.json | 20 + frontend/.vscode/tasks.json | 42 + frontend/Dockerfile | 27 + frontend/README.md | 59 + frontend/angular.json | 119 + frontend/nginx.conf | 28 + frontend/package-lock.json | 15484 ++++++++++++++++ frontend/package.json | 40 + frontend/public/favicon.ico | Bin 0 -> 15086 bytes frontend/src/app/app.component.spec.ts | 25 + frontend/src/app/app.component.ts | 15 + frontend/src/app/app.config.ts | 15 + frontend/src/app/app.routes.ts | 20 + frontend/src/app/core/api.service.ts | 60 + .../src/app/core/font-preference.service.ts | 80 + frontend/src/app/core/i18n.service.ts | 74 + frontend/src/app/core/kiosk.service.ts | 33 + frontend/src/app/core/models.ts | 256 + frontend/src/app/core/parent-api.service.ts | 125 + .../app/core/parent-session.interceptor.ts | 16 + .../src/app/core/parent-session.service.ts | 57 + frontend/src/app/core/parent.guard.ts | 10 + frontend/src/app/core/sound.service.ts | 50 + frontend/src/app/core/tts.service.ts | 32 + .../app/features/home/board-view.component.ts | 68 + .../home/celebration-overlay.component.ts | 104 + .../features/home/event-banner.component.ts | 54 + .../app/features/home/focus-view.component.ts | 145 + .../src/app/features/home/home.component.html | 73 + .../src/app/features/home/home.component.scss | 100 + .../src/app/features/home/home.component.ts | 190 + .../features/home/morning-timer.component.ts | 41 + .../features/home/progress-bar.component.ts | 49 + .../app/features/home/task-card.component.ts | 131 + .../src/app/features/home/wallet.component.ts | 49 + .../features/parents/events-tab.component.ts | 85 + .../app/features/parents/keypad.component.ts | 137 + .../parents/materials-tab.component.ts | 151 + .../app/features/parents/parents.component.ts | 114 + .../features/parents/rewards-tab.component.ts | 150 + .../parents/routines-tab.component.ts | 122 + .../parents/schedule-tab.component.ts | 99 + .../profiles/profile-select.component.html | 24 + .../profiles/profile-select.component.scss | 92 + .../profiles/profile-select.component.ts | 55 + .../src/app/features/store/store.component.ts | 132 + frontend/src/index.html | 16 + frontend/src/main.ts | 6 + frontend/src/styles.scss | 147 + frontend/src/styles/_animations.scss | 73 + frontend/src/styles/_theme.scss | 89 + frontend/tsconfig.app.json | 15 + frontend/tsconfig.json | 27 + frontend/tsconfig.spec.json | 15 + 160 files changed, 29022 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 app-de-rutinas-visuales-para-tdah/README.md create mode 100644 app-de-rutinas-visuales-para-tdah/project/.thumbnail create mode 100644 app-de-rutinas-visuales-para-tdah/project/Rutinas TDAH.dc.html create mode 100644 app-de-rutinas-visuales-para-tdah/project/support.js create mode 100644 artifacts/App de rutinas visuales para TDAH/.thumbnail create mode 100644 artifacts/App de rutinas visuales para TDAH/Rutinas TDAH.dc.html create mode 100644 artifacts/App de rutinas visuales para TDAH/support.js create mode 100644 backend/.dockerignore create mode 100644 backend/.gitattributes create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/build.gradle create mode 100644 backend/gradle/wrapper/gradle-wrapper.jar create mode 100644 backend/gradle/wrapper/gradle-wrapper.properties create mode 100755 backend/gradlew create mode 100644 backend/gradlew.bat create mode 100644 backend/settings.gradle create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/RecordalexiaApplication.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/bootstrap/DataSeeder.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/config/TimeConfig.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/Activity.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/AfternoonRoutine.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/Child.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/CoinTransaction.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/DailyTask.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/EventType.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/Language.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/MaterialItem.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/ParentUser.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/Reward.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/RewardRedemption.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/Slot.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/SpecialEvent.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/TaskOrigin.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/TaskStatus.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/ViewMode.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/domain/WeeklyTemplateEntry.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/exception/InsufficientCoinsException.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/exception/NotFoundException.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/ActivityRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/AfternoonRoutineRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/ChildRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/CoinTransactionRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/DailyTaskRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/MaterialItemRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/ParentUserRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/RewardRedemptionRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/RewardRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/SpecialEventRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/repository/WeeklyTemplateEntryRepository.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/security/ParentAuthFilter.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/security/ParentAuthService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/security/ParentSessionStore.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/security/SecurityConfig.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/ChildService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/CoinReason.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/DayGenerationService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/ProgressCalculator.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/StoreService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/TaskService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/TodayService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/WalletService.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ChildController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentAuthController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentCatalogController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentChildController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentEventController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentRewardController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/ParentScheduleController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/StoreController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/TaskController.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/ChildDtos.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/ParentDtos.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/ParentViews.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/StoreDtos.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/TodayResponse.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/ToggleResult.java create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/web/dto/WalletResponse.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/db/changelog/changes/.gitkeep create mode 100644 backend/src/main/resources/db/changelog/changes/001-schema.yaml create mode 100644 backend/src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/RecordalexiaApplicationTests.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/bootstrap/DataSeederIT.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/service/DayGenerationServiceTest.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/service/StoreServiceTest.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/service/TaskServiceTest.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/web/ParentAuthIT.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/web/TodayFlowIT.java create mode 100644 backend/src/test/resources/application.yml create mode 100644 docker-compose.yml create mode 100644 docs/adr/0001-spring-boot-3x.md create mode 100644 docs/adr/0002-tipografia-opendyslexic.md create mode 100644 docs/adr/0003-dominio-y-seguridad-fase2.md create mode 100644 docs/prompt-claude-code-backend-rutinas-tdah.md create mode 100644 docs/prompt-claude-code-recordalexia-director.md create mode 100644 docs/prompt-claude-design-rutinas-tdah.md create mode 100644 frontend/.dockerignore create mode 100644 frontend/.editorconfig create mode 100644 frontend/.gitignore create mode 100644 frontend/.vscode/extensions.json create mode 100644 frontend/.vscode/launch.json create mode 100644 frontend/.vscode/tasks.json create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/angular.json create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/src/app/app.component.spec.ts create mode 100644 frontend/src/app/app.component.ts create mode 100644 frontend/src/app/app.config.ts create mode 100644 frontend/src/app/app.routes.ts create mode 100644 frontend/src/app/core/api.service.ts create mode 100644 frontend/src/app/core/font-preference.service.ts create mode 100644 frontend/src/app/core/i18n.service.ts create mode 100644 frontend/src/app/core/kiosk.service.ts create mode 100644 frontend/src/app/core/models.ts create mode 100644 frontend/src/app/core/parent-api.service.ts create mode 100644 frontend/src/app/core/parent-session.interceptor.ts create mode 100644 frontend/src/app/core/parent-session.service.ts create mode 100644 frontend/src/app/core/parent.guard.ts create mode 100644 frontend/src/app/core/sound.service.ts create mode 100644 frontend/src/app/core/tts.service.ts create mode 100644 frontend/src/app/features/home/board-view.component.ts create mode 100644 frontend/src/app/features/home/celebration-overlay.component.ts create mode 100644 frontend/src/app/features/home/event-banner.component.ts create mode 100644 frontend/src/app/features/home/focus-view.component.ts create mode 100644 frontend/src/app/features/home/home.component.html create mode 100644 frontend/src/app/features/home/home.component.scss create mode 100644 frontend/src/app/features/home/home.component.ts create mode 100644 frontend/src/app/features/home/morning-timer.component.ts create mode 100644 frontend/src/app/features/home/progress-bar.component.ts create mode 100644 frontend/src/app/features/home/task-card.component.ts create mode 100644 frontend/src/app/features/home/wallet.component.ts create mode 100644 frontend/src/app/features/parents/events-tab.component.ts create mode 100644 frontend/src/app/features/parents/keypad.component.ts create mode 100644 frontend/src/app/features/parents/materials-tab.component.ts create mode 100644 frontend/src/app/features/parents/parents.component.ts create mode 100644 frontend/src/app/features/parents/rewards-tab.component.ts create mode 100644 frontend/src/app/features/parents/routines-tab.component.ts create mode 100644 frontend/src/app/features/parents/schedule-tab.component.ts create mode 100644 frontend/src/app/features/profiles/profile-select.component.html create mode 100644 frontend/src/app/features/profiles/profile-select.component.scss create mode 100644 frontend/src/app/features/profiles/profile-select.component.ts create mode 100644 frontend/src/app/features/store/store.component.ts create mode 100644 frontend/src/index.html create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/styles.scss create mode 100644 frontend/src/styles/_animations.scss create mode 100644 frontend/src/styles/_theme.scss create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.spec.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78b930f --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Plantilla de variables de entorno de recordaLexia. +# Copia este fichero a .env y rellena los valores reales. El .env NO se versiona. +# +# cp .env.example .env +# +# Base de datos PostgreSQL (usada por el contenedor postgres y por el backend). +DB_NAME=recordalexia +DB_USER=recordalexia +# Pon aquí una contraseña propia. NO uses esta de ejemplo en producción. +DB_PASSWORD=cambia-esta-clave + +# Puerto en el host donde se publica el frontend (Nginx). La tablet apunta aquí. +WEB_PORT=8088 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ced353d --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# --- Entorno / secretos --- +# El .env real (con credenciales) NUNCA se versiona. Solo .env.example. +.env + +# --- Sistema operativo --- +.DS_Store + +# --- Handoff comprimido (la fuente viva está descomprimida en su carpeta) --- +*.zip + +# --- Backend (Gradle) --- +backend/.gradle/ +backend/build/ + +# --- Frontend (Angular / Node) --- +frontend/node_modules/ +frontend/dist/ +frontend/.angular/ + +# --- IDE --- +.idea/ +*.iml + +# Artefactos de Playwright (capturas de verificación) +.playwright-mcp/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c9083c7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# recordaLexia + +App web familiar para niños con TDAH: muestra cada mañana, en una tablet en modo +kiosko, qué material llevar al cole y qué rutinas hacer por la tarde, con +gamificación por monedas y tienda de recompensas. Multi-niño y bilingüe ES/CA. + +## Stack + +- Frontend: Angular 19 (standalone components + signals), TypeScript estricto, i18n ES/CA. +- Backend: Java 21, Spring Boot 3.x, Gradle 8, Spring Web + Data JPA + Security. +- Datos: PostgreSQL con Liquibase. +- Empaquetado: Docker (multi-stage) + docker-compose (postgres + backend + Nginx). +- Contexto doméstico/homelab. Ejecución LOCAL. Sin nube ni infraestructura corporativa. + +## Estructura + +Monorepo: +- `frontend/` — Angular 19. +- `backend/` — Spring Boot 3 + Gradle. +- `docs/` — specs de referencia (prompts de Design, backend y director). Son contrato. +- `artifacts/` — diseño de referencia exportado de Claude Design (HTML). +- `app-de-rutinas-visuales-para-tdah/` — handoff original de Claude Design (solo lectura). +- `docker-compose.yml` en la raíz. + +## Comandos principales + +- Backend: `./gradlew bootRun` (local), `./gradlew test`, `./gradlew build`. +- Frontend: `npm start` (ng serve), `npm test`, `npm run build`. +- Stack completo: `docker-compose up --build`. + +## Convenciones + +- Idioma de trabajo y de los comentarios de código: español de España. Comenta + siempre la lógica de negocio. +- Toda la UI es bilingüe ES/CA. Los textos visibles (materiales, actividades, + rutinas, premios, eventos) se almacenan con `labelEs` y `labelCa`. +- Iconografía = emojis (decisión del diseño). El campo `icon` es un string emoji. + No introducir librerías de iconos en la v1. +- Design tokens fijos (del handoff): tipografías Fredoka (títulos/labels) y Nunito + (texto); paleta de acento `#F2A65A`, `#5B8DEF`, `#A78BD0`, `#7FBF6B`, `#5BC0BE`, + `#F4C95D`, `#EC8FA4`; texto `#2A3142`; monedas pill `#FFF6E0`/`#C7912B`. + Centralizar en un único fichero de tokens y no esparcir colores por el código. +- Backend por capas (controller / service / repository / domain), dominio aislado + de JPA donde sea razonable. Exponer DTOs, nunca entidades. +- Frontend: componentes pequeños y reutilizables; estado con signals; HTTP tipado + contra DTOs alineados con el backend (en especial `GET /api/children/{id}/today`). +- Zona horaria fija Europe/Madrid para decidir qué es "hoy". +- Tests: lógica de generación del día, marcado/monedas (incl. bonos) y canje. + +## Restricciones + +- NO usar Keycloak, Gravitee, Camunda, Kubernetes ni Jenkins en esta fase. La auth + es ligera (kiosko sin fricción para el niño + PIN configurable para padres). +- NO portar el "dock" inferior de demo del prototipo (botones A/B, idioma, + reiniciar, vacío) a producción: esas funciones son preferencias reales del niño. +- NO borrar histórico de tareas ni de canjes al pasar el día (se conserva). +- NO hardcodear el PIN ni los parámetros de gamificación: son configurables. +- NO incrustar el HTML del handoff; reconstruir los componentes en Angular. +- Conservar el modo Foco (una tarea a la vez): es clave para TDAH. + +## Referencias + +El detalle completo de UX, dominio y plan por fases está en `docs/`: +- `docs/prompt-claude-code-recordalexia-director.md` (director, conduce el trabajo). +- `docs/prompt-claude-code-backend-rutinas-tdah.md` (dominio y API). +- `docs/prompt-claude-design-rutinas-tdah.md` (UX y pantallas). diff --git a/README.md b/README.md new file mode 100644 index 0000000..c540d9b --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# recordaLexia + +App web familiar para niños con **TDAH**. Cada mañana, en una tablet en modo kiosko +junto a la puerta, muestra qué material llevar al cole y qué rutinas hacer por la +tarde. El niño marca cada tarea, gana monedas y las canjea en una tienda de premios. +Multi-niño y bilingüe (español / catalán). + +## Características + +- Pantalla "HOY" con dos modos: **Tablero** (cole + tarde a la vista) y **Foco** + (una tarea a la vez), pensado para reducir la carga cognitiva. +- Avisos de exámenes y deberes, temporizador de salida y lectura en voz alta (TTS). +- Gamificación: monedas por tarea / bloque / día y tienda de recompensas. +- Panel de padres protegido por PIN: horario semanal, materiales, eventos, rutinas + de tarde y recompensas. + +## Stack + +- **Frontend:** Angular 19 (standalone + signals), TypeScript, i18n ES/CA. +- **Backend:** Java 21, Spring Boot 3.x, Gradle 8. +- **Datos:** PostgreSQL + Liquibase. +- **Empaquetado:** Docker + docker-compose (postgres + backend + Nginx). + +## Estructura del repositorio + +``` +recordaLexia/ +├── CLAUDE.md # Convenciones para Claude Code +├── README.md # Este fichero +├── docker-compose.yml # Stack: postgres + backend + frontend +├── .env.example # Plantilla de variables (copiar a .env) +├── frontend/ # Angular 19 (standalone + signals) +├── backend/ # Spring Boot 3.5 + Gradle (wrapper) +├── docs/ # Specs de referencia (contrato) +│ ├── adr/ # Decisiones de arquitectura (ADR) +│ ├── prompt-claude-code-recordalexia-director.md +│ ├── prompt-claude-code-backend-rutinas-tdah.md +│ └── prompt-claude-design-rutinas-tdah.md +├── artifacts/ # Diseño de referencia (HTML autónomo) +└── app-de-rutinas-visuales-para-tdah/ # Handoff de Claude Design (solo lectura) + ├── project/ + │ ├── Rutinas TDAH.dc.html + │ └── support.js + └── README.md +``` + +> El handoff de `app-de-rutinas-visuales-para-tdah/` es la fuente de verdad visual; no +> se edita, se usa como referencia para reconstruir la UI en Angular. + +## Requisitos + +- Java 21 (JDK). No hace falta instalar Gradle: el backend trae el *wrapper*. +- Node 20 LTS recomendado (Angular 19 soporta 18.19+/20/22; **Node 24 funciona + pero Angular lo marca como no soportado** — fija 20 LTS en el homelab). +- Docker y Docker Compose. +- No hace falta instalar PostgreSQL: lo levanta docker-compose. + +## Configuración previa + +Copia la plantilla de variables y pon tus valores (credenciales de BD, puerto web): + +```bash +cp .env.example .env +``` + +El `.env` real **no se versiona**. Las credenciales nunca van en el código. + +## Cómo arrancar + +### Todo con Docker (recomendado para probar) + +```bash +docker-compose up --build +``` + +Levanta PostgreSQL, el backend y el frontend tras Nginx. La app queda accesible en +el puerto que defina `docker-compose.yml` (ver su salida). + +### Desarrollo (servicios por separado) + +```bash +# Base de datos +docker-compose up -d postgres + +# Backend (necesita las credenciales de BD en el entorno) +export $(grep -v '^#' .env | xargs) +export SPRING_DATASOURCE_USERNAME=$DB_USER SPRING_DATASOURCE_PASSWORD=$DB_PASSWORD +cd backend && ./gradlew bootRun + +# Frontend +cd frontend && npm install && npm start +``` + +## Uso en la tablet (modo kiosko) + +Monta la tablet en horizontal y abre el frontend en el navegador a pantalla +completa (Chrome admite modo kiosko). Apunta a la URL del frontend en la red local +del homelab. El niño selecciona su perfil y ve directamente las tareas del día. + +## Tipografía accesible (OpenDyslexic) + +La app usa **OpenDyslexic** como tipografía por defecto en todo el texto, pensada +para mejorar la legibilidad. Es una **preferencia conmutable por niño** (activada +de serie); al desactivarla, la UI cae a las tipografías de marca del handoff +(Fredoka/Nunito). Las tres familias se empaquetan en local (sin CDN), así que el +kiosko funciona sin internet. Detalle de la decisión en +[`docs/adr/0002-tipografia-opendyslexic.md`](docs/adr/0002-tipografia-opendyslexic.md). + +## Backend: API y datos de ejemplo + +Al arrancar con la base de datos vacía se siembran los datos del prototipo (niños +Nora 🦊, Leo 🐢 y Mía 🦉, su horario, rutinas, premios y eventos). El **PIN de +padres por defecto es `1234`** (configurable; no se puede cambiar el resto del +panel sin él). + +Endpoints principales (kiosko del niño, acceso libre): + +- `GET /api/children` — perfiles. +- `GET /api/children/{id}/today` — material de mañana, rutinas de tarde, eventos, + progreso, monedero y temporizador. +- `POST /api/tasks/{taskId}/toggle` — marca/desmarca y ajusta monedas (con bonos). +- `GET /api/children/{id}/wallet` — saldo e historial. +- `GET /api/children/{id}/rewards` — tienda. +- `POST /api/rewards/{rewardId}/redeem?childId=` — canje. +- `PUT /api/children/{id}/settings` — modo de vista, sonido, TTS, idioma, hora salida. + +Panel de padres (requiere sesión; `POST /api/parents/login` con el PIN devuelve un +identificador que se envía en la cabecera `X-Parent-Session`): CRUD de niños, +catálogo, horario, rutinas, eventos, premios y ajuste de gamificación. + +## Despliegue en el homelab + +Construye las imágenes y levanta el `docker-compose` en el VPS o la Raspberry Pi. +Recuerda persistir el volumen de PostgreSQL y exponer el frontend tras tu proxy +(Nginx Proxy Manager / Cloudflare tunnel). + +## Documentación + +El detalle de UX, dominio, API y plan por fases está en `docs/`. El fichero +director (`docs/prompt-claude-code-recordalexia-director.md`) conduce el desarrollo. diff --git a/app-de-rutinas-visuales-para-tdah/README.md b/app-de-rutinas-visuales-para-tdah/README.md new file mode 100644 index 0000000..f172393 --- /dev/null +++ b/app-de-rutinas-visuales-para-tdah/README.md @@ -0,0 +1,22 @@ +# CODING AGENTS: READ THIS FIRST + +This is a **handoff bundle** from Claude Design (claude.ai/design). + +A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real. + +## What you should do — IMPORTANT + +**Read `app-de-rutinas-visuales-para-tdah/project/Rutinas TDAH.dc.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing. + +**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing. + +## About the design files + +The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit. + +**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't. + +## Bundle contents + +- `app-de-rutinas-visuales-para-tdah/README.md` — this file +- `app-de-rutinas-visuales-para-tdah/project/` — the `App de rutinas visuales para TDAH` project files (HTML prototypes, assets, components) diff --git a/app-de-rutinas-visuales-para-tdah/project/.thumbnail b/app-de-rutinas-visuales-para-tdah/project/.thumbnail new file mode 100644 index 0000000000000000000000000000000000000000..452cb6a03ad6e0b16b3c6487fe7586be6c34b338 GIT binary patch literal 2314 zcmaKuc|6qX7ssE$wPYpACqzyEwOXgxg!HUKPjQKlBA>Q)B; z0Ai3MV4xTPw6?Zs2MpQ*SjT>3QUt)uI{=5#M~PWk+laBy01R3`%n|GF`;-1NLaOzl zpW1%8|5W_{B%8B~KNgZKLoS7bibFl|LCkmMC(G=!(@#FL&p`pc0g%RYpK<0GZHS#A zmbvmTcKR1%eR2ExTaf0AmnVL|*1qje$Km3A(F}UBLJkN100Z;^YJYxc51CIU0P0@> zU`+azb4mlCJQ@JO(O)^KCjf9q0Z`WaEBEW2_&VYoe_Y1|MFv+_09Ggfa9RVv+Xuh_ z+aG68_#eHAK}T??FCS=K0iM7GhygV423Q~ukrGe@CxI&Tg~j+-0N_yo88BHSj0(+k(betb`Qww10=q6#oHSm31>jBV=+ z@kX|97zmGsET`8MrCF9fB>GA9-}99*plj`4UI|ZOJY16aJ^i>c`x!$ii4&Ha;%)+j zx{NAJtby{Kuwo}`8|ms)HICZ1*N^`3cSh6AQWaEwMUBy){jd8Y5`84zJTBx4o zthsroO9MjCgD7U7;iO!t)KgO5Trgu`k{j4snl&F;mApTIgG93F`!1vv>sZK@@1m*b zM$Tr1fDGTi<$5*BKSm0nwD3zYJ-qk5QvArT&K{RMX!Gp6v!`bA!#%}#loN(-*^s20 z%D%ECy#~PU?lfhi3A zPBW{u)H!RGvyr)&9U|W;-K`z;N8rI#9*K>!S+3N)z+^coV8z`%Ij(z3uDNcaZ9~~O zyLX+wRI2*k=yO56hDWR9g|rmCMi&{Lkv{m6Pr5!iW6e3fObA|~JUqbJw+#D32(ibN zDup3BB@wx`d&o@+FI+N7cCp4J)Tt2EF-DO~oCJ*>a;W;;G5n}v0# znpV_l9m~#&7mSK4{z7RJgvaddDktqaHjQ|+6H520lP^CAy|pfKv_wIYkVxI!WOJGB zD3$A7-E@ytQ)-#pbZ%jxz&d+n1H(>uH8W&=eT^L5eM~Ec-LKsf>9#lPP&r5K*$H!h zIbCOqyZ$opMXz=^3Ldh{^Qr&b%p) zC%GGzomPi#Q}zmHiA%%c^5v)EN<>gEk0qQVsPK0JSzKkOgzHgT4#wrJMOl<-GaWk z&DoQ<8}&zMj^DOL9xBnAz9f%&if^W%+g%G)w<=iW9Q)`+q$RwE(7d{ku`JO|ELNqU zk$w%qnu)G29jAN7RtfezH2Ec+!l)=w8jmNt4l&AGqc$PAtAy6ZUi16~YROIi(4ib4izP zRug762IKN8V1&JVSd63WhpBCcZt212qQ6-k*}J*rpT%TU+JNK%MUVWK%=8F%D1yud z4sHGEh76NC7mat|a-G5IlNQWjHtR9!Ce z&sALYr-&2~lrt2p^!S?ifkFA3QLYUz?Kra#bv0SM0OQsR1JNhtr_tUz-y??<`5g`~ z9QM_V^S6#3vs=064wyW&ESM>iy@$1^r%~@g1+I{+%QEfl(ybLL;PKet^9)~nM^k%l zaki$l#V|hM?P#BGvtpjLX#_wo++7{bOd9wL2{&RMli+NC1tUd2Dh%}nU3(c0d#No- zw!KQ2>tfo|*<3y1y^{sF#h)e`^! literal 0 HcmV?d00001 diff --git a/app-de-rutinas-visuales-para-tdah/project/Rutinas TDAH.dc.html b/app-de-rutinas-visuales-para-tdah/project/Rutinas TDAH.dc.html new file mode 100644 index 0000000..9929944 --- /dev/null +++ b/app-de-rutinas-visuales-para-tdah/project/Rutinas TDAH.dc.html @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ + + +
+
+
🌳
+
{{ L.who }}
+
+
+ +
+ +
+ + {{ p.ageLabel }} + +
+
+
+
+ +
+
+ + + +
+ + +
+
+
{{ L.hola }}, {{ profile.name }}! 👋
+
{{ dayName }} {{ dayDate }}
+
+ + +
+
+
🐦
+
+
+
{{ L.salimos }}
+
{{ timerMin }} {{ L.min }}
+
+
+ + +
+ 🪙 + {{ coins }} +
+ +
+ + +
+
+
+
+
{{ globalDone }}/{{ globalTotal }} {{ L.listo }} ✨
+
+ + + +
+ +
+ {{ e.icon }} +
+
{{ e.kind }}
+
{{ e.title }}
+
+
+
+
+
+ + + +
+
🏖️
+
{{ L.vacioT }}
+
{{ L.vacioS }}
+
+
+ + + +
+ +
+
+ 🎒 + {{ L.cole }} + {{ coleDone }}/{{ coleTotal }} +
+
+ +
+
{{ item.icon }}
+
{{ item.label }}
+ +
{{ item.check }}
+
+
+
+
+ +
+
+ 🌙 + {{ L.tarde }} + {{ tardeDone }}/{{ tardeTotal }} +
+
+ +
+
{{ item.icon }}
+
{{ item.label }}
+ +
{{ item.check }}
+
+
+
+
+
+
+ + + +
+
{{ focus.blockIcon }} {{ focus.blockLabel }}
+
+ +
+
{{ focus.icon }}
+
{{ focus.label }}
+
+ + +
+
+ +
+
+ +
+
+
+
{{ L.quedan }} {{ remaining }} · {{ L.despues }}: {{ nextLabel }}
+
+
+ +
+
+ + + +
+
+ +
🎁 {{ L.tienda }}
+
🪙 {{ coins }}
+
+
+ +
+
{{ r.icon }}
+
{{ r.name }}
+
🪙 {{ r.cost }}
+ +
+
+
+ +
{{ toast.msg }}
+
+
+
+ + + +
+ +
🔒
+
{{ L.pin }}
+
+ +
+
+
+
+ + + +
+
PIN demo: 1 2 3 4
+
+
+ + + +
+
+
👪 {{ L.padres }}
+
+ + + +
+ +
+ +
+ + +
Horario semanal · material de cada día
+
+ +
+
{{ day.name }}
+
+ +
+ {{ a.icon }} + {{ a.name }} +
+
+ +
+
+
+
+
+ + + +
Actividades y su material
+
+ +
+
+ {{ act.icon }} + {{ act.name }} +
+
+ +
{{ m.i }} {{ m.n }}
+
+ +
+
+
+ +
+
+ + + +
Exámenes y deberes
+
+ +
+
{{ e.icon }}
+
+
{{ e.kind }}
+
{{ e.title }}
+
+
📅 {{ e.date }}
+
+
+ +
+
+ + + +
Rutinas de la tarde (por día)
+
+ + + +
+
+ +
+ {{ r.icon }} + {{ r.name }} + +
+
+ +
+
+ + + +
Recompensas y ajustes
+
+ +
+ {{ s.icon }} +
{{ s.label }}
{{ s.hint }}
+
+ + 🪙 {{ s.value }} + +
+
+
+ +
+ {{ tg.icon }} +
{{ tg.label }}
+ +
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + + +
+
+ + +
{{ flyingCoinEls }}
+ + + +
+
{{ confettiEls }}
+
+
🦊🎉
+
{{ L.todoListo }}
+
{{ L.biengrande }}
+
🪙 +{{ coinsPerDay }}
+
+ +
+
+
+
+ +
+
+ + + diff --git a/app-de-rutinas-visuales-para-tdah/project/support.js b/app-de-rutinas-visuales-para-tdah/project/support.js new file mode 100644 index 0000000..9b71e07 --- /dev/null +++ b/app-de-rutinas-visuales-para-tdah/project/support.js @@ -0,0 +1,1513 @@ +// 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:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + 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:rgba(0,0,0,.7);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:rgba(0,0,0,.5);background:rgba(0,0,0,.05);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; } + section, article, figure, table { break-inside: avoid; } + *, *::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.adoptParsed(rootName, parsed); + 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); + }; + }, []); + return h(Root, entry.propOverrides || null); + } + 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 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" + }; + 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 encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + 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, isComponent, 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 (isComponent) { + if (key.includes("-")) 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); + } + 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, true, 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) props[k] = g(vals); + 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 url = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const kind = /\.(jsx|tsx)(\?|#|$)/i.test(url) ? "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, true, host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = url.includes("{{"); + if (url && !urlBindable) host.loadExternal(kind, url); + 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 === "name" || k === "from" || k === "src" || k === "import") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + 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 = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const { propGetters, pseudoClasses } = collectProps(el, false, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { key, "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, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // 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" }, + 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" }, 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/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 BABEL_URL = "https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"; + 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; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + function load(kind, url) { + if (cache.has(url)) return; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = kind === "jsx" ? ensureBabel() : Promise.resolve(); + ready.then(() => 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(); + }); + } + 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/helmet.ts + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + 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); + 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 }; + } + + // src/pseudo.ts + 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 sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + 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) => external.load(kind, url), + 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"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.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 + ) + ); + } + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + 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: + + + + + + + + + + +
+ + +
+
+ + + +
+
+
🌳
+
{{ L.who }}
+
+
+ +
+ +
+ + {{ p.ageLabel }} + +
+
+
+
+ +
+
+ + + +
+ + +
+
+
{{ L.hola }}, {{ profile.name }}! 👋
+
{{ dayName }} {{ dayDate }}
+
+ + +
+
+
🐦
+
+
+
{{ L.salimos }}
+
{{ timerMin }} {{ L.min }}
+
+
+ + +
+ 🪙 + {{ coins }} +
+ +
+ + +
+
+
+
+
{{ globalDone }}/{{ globalTotal }} {{ L.listo }} ✨
+
+ + + +
+ +
+ {{ e.icon }} +
+
{{ e.kind }}
+
{{ e.title }}
+
+
+
+
+
+ + + +
+
🏖️
+
{{ L.vacioT }}
+
{{ L.vacioS }}
+
+
+ + + +
+ +
+
+ 🎒 + {{ L.cole }} + {{ coleDone }}/{{ coleTotal }} +
+
+ +
+
{{ item.icon }}
+
{{ item.label }}
+ +
{{ item.check }}
+
+
+
+
+ +
+
+ 🌙 + {{ L.tarde }} + {{ tardeDone }}/{{ tardeTotal }} +
+
+ +
+
{{ item.icon }}
+
{{ item.label }}
+ +
{{ item.check }}
+
+
+
+
+
+
+ + + +
+
{{ focus.blockIcon }} {{ focus.blockLabel }}
+
+ +
+
{{ focus.icon }}
+
{{ focus.label }}
+
+ + +
+
+ +
+
+ +
+
+
+
{{ L.quedan }} {{ remaining }} · {{ L.despues }}: {{ nextLabel }}
+
+
+ +
+
+ + + +
+
+ +
🎁 {{ L.tienda }}
+
🪙 {{ coins }}
+
+
+ +
+
{{ r.icon }}
+
{{ r.name }}
+
🪙 {{ r.cost }}
+ +
+
+
+ +
{{ toast.msg }}
+
+
+
+ + + +
+ +
🔒
+
{{ L.pin }}
+
+ +
+
+
+
+ + + +
+
PIN demo: 1 2 3 4
+
+
+ + + +
+
+
👪 {{ L.padres }}
+
+ + + +
+ +
+ +
+ + +
Horario semanal · material de cada día
+
+ +
+
{{ day.name }}
+
+ +
+ {{ a.icon }} + {{ a.name }} +
+
+ +
+
+
+
+
+ + + +
Actividades y su material
+
+ +
+
+ {{ act.icon }} + {{ act.name }} +
+
+ +
{{ m.i }} {{ m.n }}
+
+ +
+
+
+ +
+
+ + + +
Exámenes y deberes
+
+ +
+
{{ e.icon }}
+
+
{{ e.kind }}
+
{{ e.title }}
+
+
📅 {{ e.date }}
+
+
+ +
+
+ + + +
Rutinas de la tarde (por día)
+
+ + + +
+
+ +
+ {{ r.icon }} + {{ r.name }} + +
+
+ +
+
+ + + +
Recompensas y ajustes
+
+ +
+ {{ s.icon }} +
{{ s.label }}
{{ s.hint }}
+
+ + 🪙 {{ s.value }} + +
+
+
+ +
+ {{ tg.icon }} +
{{ tg.label }}
+ +
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + + +
+
+ + +
{{ flyingCoinEls }}
+ + + +
+
{{ confettiEls }}
+
+
🦊🎉
+
{{ L.todoListo }}
+
{{ L.biengrande }}
+
🪙 +{{ coinsPerDay }}
+
+ +
+
+
+
+ +
+
+ + + diff --git a/artifacts/App de rutinas visuales para TDAH/support.js b/artifacts/App de rutinas visuales para TDAH/support.js new file mode 100644 index 0000000..9b71e07 --- /dev/null +++ b/artifacts/App de rutinas visuales para TDAH/support.js @@ -0,0 +1,1513 @@ +// 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:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + 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:rgba(0,0,0,.7);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:rgba(0,0,0,.5);background:rgba(0,0,0,.05);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; } + section, article, figure, table { break-inside: avoid; } + *, *::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.adoptParsed(rootName, parsed); + 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); + }; + }, []); + return h(Root, entry.propOverrides || null); + } + 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 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" + }; + 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 encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + 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, isComponent, 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 (isComponent) { + if (key.includes("-")) 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); + } + 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, true, 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) props[k] = g(vals); + 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 url = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const kind = /\.(jsx|tsx)(\?|#|$)/i.test(url) ? "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, true, host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = url.includes("{{"); + if (url && !urlBindable) host.loadExternal(kind, url); + 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 === "name" || k === "from" || k === "src" || k === "import") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + 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 = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const { propGetters, pseudoClasses } = collectProps(el, false, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { key, "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, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // 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" }, + 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" }, 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/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 BABEL_URL = "https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"; + 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; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + function load(kind, url) { + if (cache.has(url)) return; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = kind === "jsx" ? ensureBabel() : Promise.resolve(); + ready.then(() => 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(); + }); + } + 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/helmet.ts + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + 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); + 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 }; + } + + // src/pseudo.ts + 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 sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + 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) => external.load(kind, url), + 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"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.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 + ) + ); + } + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + 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: