Saltar al contenido principal

Changelog

Todas las actualizaciones de Appros, en orden cronológico.

Fix

PRP-054 Fase 2.5 — block theme binding universal (20/24 blocks consume cascade)

Gap arquitectural pre-existente cerrado: blocks usaban Tailwind hardcoded (bg-teal-500, text-white) en lugar de CSS vars del theme cascade. Picking un Master Theme cambiaba page bg pero no transformaba blocks. Refactor universal con tokens neuromarketing-curated. Phase A — Token system extendido: - ColorPaletteDefinition.tokens: +onPrimary (WCAG AA-validated text on primary) + surfaceRaised (Pricing decoy tier per Iyengar 2000 / Ariely 2008) - 10 color palettes con valores curados por contrast WCAG AA por par - resolveCssVariables emite --bk-color-on-primary + --bk-color-surface-raised - InlineEditableText + RenderedInlineString + BindingDisplay: +style? prop (theme cascade passthrough preservando user override precedence PRP-049) Phase B — 20 blocks refactored (160+ CSS var bindings): Round 1 (11 blocks Fase 1+2): - Hero/CTA/Form/Benefits/Testimonial: 65 var refs - FAQ/Pricing/Slider/Stats/Lead-magnet/Footer: 47 var refs Round 2 (9 blocks gap-closure): - Heading: F-pattern + accent text mapping - Paragraph: 3-color map (default/muted/accent) → tokens - Image: frame neutral + caption muted (theme-specific frame styles) - Video: play button accent + thumbnail overlay onPrimary (contrast-aware) - Separator: 3 styles (line/gradient/dashed) consume border/accent - Custom-code: placeholder text muted - Popup-trigger: 4 button styles (solid/outline/ghost/link) consume cascade - Product-gallery: frame + caption + thumbnails Decisiones por bloque guiadas por research neuromarketing: - Hero F-pattern + von Restorff, CTA paradox of choice + Lefebvre 2018 - Pricing anchoring/decoy (surfaceRaised tier highlighted) - Testimonial mirror-neuron (avatar + accent ring) - FAQ subtle differentiation open/closed (NN/g 2019) - Stats numerical anchoring (Tversky 1973) - Slider auto-advance 5-7s optimal (Lindgaard 2006) - Footer brand reinforcement editorial jerarquia - Form Zeigarnik + goal-gradient - Lead-magnet reciprocity card (Cialdini 2009) - Heading F-pattern + accentText distinct - Video play button accent + WCAG-aware overlay text - Popup-trigger 4 button styles paridad con CTA Inspector admin UI + placeholder skeletons mantienen Tailwind chrome (intencional — son chrome del editor, no canvas final). Header sin template aplicado: deferido (props-driven model template-aware predates PRP-054, scope distinto requeriría refactor del Header para fallback al cascade cuando props.colors undefined). Pre-existing flagged: contrast text-muted/surface en sophisticated_luxury (3.6:1) + natural_grounding (3.2:1) FAILS WCAG AA — values del PRP-052 spec literal, no regresión mía. Para PRP-055 audit task. Resultado: theme picker "Cambiar estilo" ahora transforma 20/24 blocks en tiempo real. Pick "High-Energy Launch" → naranja saturado dark; "Vibrant" → coral + lavanda gradient; "Sophisticated" → champagne + Cormorant; "Cinematic" → dark cinematográfico + accent rojo Netflix; "Tactical Operator" → mono + neón verde; "Knowledge Authority" → marfil editorial + accent teal; etc. Quality gates: typecheck 0 / 1958 tests verdes (121 archivos) / build verde / i18n parity verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

UGC picker links → /engagement/generate?contentType=ugc_video

/engagement/ugc no existe (404). El flujo real para crear UGC briefs es desde el generador unificado (/engagement/generate) preseleccionando contentType=ugc_video — la página acepta ese query param y abre directo el panel UGCConfigPanel. Aplica al CTA "Crear primero brief en GAC" (empty state) y al link "Crear otro en GAC" (lista del picker). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-054 Fase 2 — 66 templates curados (6 medium-tier blocks × 11 themes)

- FAQ / Pricing / Slider / Stats / Lead-magnet / Footer: 11 templates curados cada uno con copy + props + thumbnails + research metadata respondiendo a cada Master Theme - Pricing: docs reforzadas — explica separación <PricingComparisonTable> (platform /pricing real) vs pricing-block (user landing), por qué no inyectar billing_plans, integración futura via PRP-048 bindings - Lead-magnet: magnetId vacío (placeholder semántico para vinculación runtime via Inspector) - Footer: docs reforzadas — explica realidad del codebase (dynamic-nav-fetcher conceptual aún), forward-compat sin breaking changes, cero hardcoding de hrefs destino (links arrays vacíos) - Slider variants alineados al block (slider.hero_carousel + slider.content_cards) - 6 bridges side-effect registrados en block-inspector - check-builder-kit-i18n-parity extendido: +6 sub-namespaces (faq/pricing/slider/stats/lead-magnet/footer) - fase2-coverage.test.ts (31 asserts: cobertura 66/66 + restricciones del spec por bloque) - apply-template-per-block-fase2.test.ts (12 asserts: applyTemplate sobre los 66 reales + nanoid injection FAQ + variants check) - e2e Playwright per-block: 6/6 verdes (faq/pricing/slider/stats/lead-magnet/footer: pick theme → apply template → verify localStorage) - i18n trilingüe ES/EN/PT (paridad enforced en los 11 sub-namespaces de templates + variantes) Quality gates: typecheck 0 / 1958 tests verdes (121 archivos) / build verde / i18n parity verde con 11 sub-namespaces (264 keys total) / 14 e2e verdes (3 Fase 0 + 5 Fase 1 + 6 Fase 2) Cobertura PRP-054 acumulada: 12/24 blocks (Header + 5 Fase 1 + 6 Fase 2) = 132 templates curados. 12 pendientes Fase 3 (Separator/Spacer/Heading/Paragraph/Image+frames/Video/Custom-code/Collection/Popup-trigger/Product-gallery/Structural shells). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

habilitar UGC Video en GLM con picker + guardrail anti-cobro

- UGC Video sale de "Coming soon" en el wizard de lead magnets: available: true + chargedElsewhere: true en LEAD_MAGNET_META. Tile muestra "Ya pagado en GAC" en vez de costo en créditos. - Picker (`UGCBriefPicker`) jala briefs ya creados en GAC vía `GET /api/engagement/ugc/briefs?available_for_lm=true`. El user activa con un click → endpoint existente `POST /api/engagement/ugc/briefs/[id]/activate-as-lm` convierte el brief a markdown e inserta en `lead_magnets`. Cero double-charge: el costo (10cr) ya fue cobrado al crear el brief en GAC. - Wizard cortocircuitado para ugc_video: solo 2 pasos (Type → Picker). Tras activar, redirige a /nurturing/lead-magnets (skip Generate/Publish). Empty state con CTA al GAC si user no tiene briefs. - Guardrail en `POST /api/nurturing/lead-magnets/[id]/generate`: rechaza tipos no soportados (ugc_video tiene su propio flujo, los Coming Soon como calculator_quiz/webinar/challenge/assessment no tienen generador) ANTES de consume_credits → previene cobro sin output. - i18n ES/EN/PT (19 keys nuevas en namespace Nurturing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

hotfix quiz cost + CTA hover + PCC widget v2 + counter en wizard LM

3 issues reportados sobre el sistema de counters/costos post-PRP-053: **Issue 1 — Quiz/Calculadora costaba sólo 2cr:** El user observó: "Una calculadora, quiz, test, etc. consume tan pocos créditos? Ten presente que los test pueden ser conversacionales". Razón: 2cr era el precio para un quiz estático tipo checkboxes. Pero en Appros el quiz/calc es conversacional con IA — interpreta lenguaje natural, hace follow-ups, scoring dinámico, resultado personalizado. Costo realista: ~3-5 mensajes LLM × 1cr + scoring ≈ 8 créditos (match con `lead_magnet_pdf_guide`). **Migración 125** aplicada a producción: - `feature_credit_costs.lead_magnet_quiz.credits_per_unit`: 2 → 8 - Descripciones ES/EN/PT actualizadas para reflejar la naturaleza conversacional del quiz. - `LEAD_MAGNET_META.calculator_quiz.creditCost`: 2 → 8 (sync UI con DB). **Issue 2 — CTA "Mejorar Plan" se perdió on-hover:** El v1 `<UsageLimitBadge>` tenía un botoncito naranja "Mejorar" que aparecía al pasar el cursor. Cuando migré a `<UsageBadge>` v2, sólo mostraba el CTA cuando pct≥80 o cap topado. Fix: el `<UsageBadge>` ahora muestra el CTA en 2 modos: 1. **Urgente** (cap topado o pct≥80): siempre visible, fondo naranja. 2. **Subtle** (resto del tiempo): aparece on-hover del badge contenedor (`opacity-0 group-hover/usage-badge:opacity-100`). Mismo patrón v1. Aplica a TODOS los `<UsageBadge>` con `showUpgradeCta={true}` en la app (Belenia, GDL, content generation, lead magnets list, campañas, etc.). **Issue 3 — PCC widget "Límites & Recorrido" mostraba v1 (1/15, 3/5, 4/10):** El widget en `pcc-limits-journey.tsx` recibía un objeto `usage` cuyo shape era v1 (leadsScraped/maxLeads/contentGenerated/maxContent/ beleniaMessages/maxBeleniaMessages) — leído desde `usage_limits` table legacy en `/api/dashboard/pcc/route.ts`. Migrado a v2: - `/api/dashboard/pcc` ahora llama RPC `get_user_usage_summary` (pool unificado + caps + storage). - Response shape v2: `creditsUsed/creditsLimit + leadsScrapedUsed/Limit + leadsStorageUsed/Limit`. - Widget muestra 3 progress bars con métricas reales del plan v2: 1. Pool de créditos / mes (verde-teal) 2. Leads scraped / mes (azul) 3. Capacidad de leads en CRM (violet) - i18n keys nuevas: `usageCredits`, `usageLeadsScraped`, `usageLeadsStorage` en ES/EN/PT (las viejas `usageContent`/`usageBelenia` se mantienen para compat). **Bonus — counter de créditos en wizard LM:** El user notó que el wizard del LM no muestra cuánto tiene. Agregado `<UsageBadge metric="credits" variant="full" showUpgradeCta />` al inicio del Step 2 (selección de tipo de LM) — el user ahora ve "X / 1000 créditos" y puede decidir informado antes de elegir un Mini Curso (40cr) o un Challenge (60cr). Quality gates: typecheck verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-054 Fase 1 — 55 templates curados (5 high-traffic blocks × 11 themes) + gaps cerrados

- Hero / CTA / Form / Benefits / Testimonial: 11 templates curados cada uno con copy + props + thumbnails + research metadata respondiendo a cada Master Theme (luxury_editorial Aesop, vibrant Joy, cinematic Letterbox, conversion Direct, sophisticated Quiet Luxury, etc.) - _thumbnail-colors.ts helper centralizado (paleta firmada por theme) - 5 bridges side-effect registrados en block-inspector - check-builder-kit-i18n-parity extendido: 7 nuevos namespaces enforced (BuilderKit.templates, variantes, templates.{hero,cta,form,benefits,testimonial}) - fase1-coverage.test.ts (28 asserts: cobertura, metadata research-backed, snapshot defaultVariants, IDs únicos) - apply-template-per-block.test.ts (11 asserts: applyTemplate sobre los 55 templates reales, nanoid injection en items[], props críticos populados) - e2e Playwright per-block: 5/5 verdes (hero/cta/form/benefits/testimonial: pick theme → apply template → verify localStorage) - i18n trilingüe ES/EN/PT (ES via otra sesión PRP-047, EN+PT añadidos aquí — paridad total enforced) Quality gates: typecheck 0 / 1915 tests verdes (119 archivos) / build verde / i18n parity verde con 5 sub-namespaces / 5 e2e per-block + 3 e2e Fase 0 verdes. Cobertura PRP-054 Fase 1 contra spec literal: 100% — coverage 55/55 + apply-template per-block + e2e per-block + i18n parity enforced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

counters v1 (1/15, 4/10, 3/5) + costos LM hardcoded → v2 PRP-053

Bug reportado: en el CRM (GDL, Belenia, Generar Contenido, etc.) los counters mostraban quotas v1 (1/15, 4/10, 3/5) en lugar del pool unificado v2. Y los costos de Lead Magnets en el wizard mostraban precios v1 (Mini Curso 3cr, Challenge 5cr) cuando los reales en DB son 40cr y 60cr. **Fix #1 — Costos LM en wizard (lead-magnet-metadata.ts):** | Tipo | Hardcoded v1 | Real v2 (DB) | |---|---|---| | Checklist | 1 | 2 | | Template/Script | 1 | 2 | | Quiz/Calculadora | 1 | 2 | | Caso de Estudio | 2 | 6 | | Guía PDF | 3 | 8 | | UGC Video | 2 | 10 | | Mini Curso | 3 | 40 (high-cost) | | Assessment | 4 | 48 (high-cost) | | Webinar | 3 | 50 (high-cost) | | Challenge | 5 | 60 (high-cost) | Los high-cost items además consumen 1 cap soft mensual. La autoridad real sigue siendo `feature_credit_costs` table en DB; este display match para no engañar al user al elegir. **Fix #2 — Migración UsageLimitBadge (v1) → UsageBadge (v2) en 7 archivos:** `UsageLimitBadge` leía `usage_limits` table (v1, deprecated) que tenía quotas como max_belenia=10, max_content=5, max_leads_scraped=15. El badge v1 mostraba esos números a pesar de que el user tenía plan Elite con 4000 créditos / 10000 leads. `UsageBadge` v2 lee `/api/me/usage` (que llama RPC `get_user_usage_summary` contra `usage_counters`) y muestra el pool real del plan actual. Migrados (7 consumers): - src/app/[locale]/(main)/mentor/page.tsx (Belenia 4/10 → credits pool) - src/features/ad-campaigns/components/campaigns-list.tsx - src/features/engagement/components/content-generation-form.tsx (3/5 → credits) - src/features/engagement/components/content-queue-view.tsx - src/features/nurturing/components/lead-magnets-list.tsx - src/features/prospecting/components/leads-view.tsx (1/15 → leads_storage + leads_scraped) - src/features/prospecting/components/personas-list.tsx Mapping triggers v1 → metrics v2: - belenia_messages → credits - content_generated → credits - media_generated → credits - campaigns_created → credits - buyer_personas → credits - leads_scraped → leads_scraped_per_month + leads_storage En leads-view se muestran 2 badges (storage + scraped) porque son métricas distintas y ambas relevantes en el GDL. Removido `useScrapingLimits` hook + import `Target` de leads-view (unused). `UsageLimitBadge` shared component queda en codebase pero ya sin consumers (cero imports). Se podrá borrar en cleanup futuro junto con la tabla legacy `usage_limits`. Quality gates: typecheck verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

pricing table sticky funcional — remover overflow-x wrapper

CSS spec quirk: `overflow-x: auto` SIEMPRE coerce `overflow-y` a `auto` también (no respeta override `visible`). El wrapper con `overflow-x-auto` se convertía en el ancestor de scroll del thead — y como ese div NO scrolea verticalmente (el page sí), el `position: sticky` nunca se activaba. Fix: remover el wrapper de overflow. El thead ahora se posiciona respecto al body/page scroll, que sí scrolea — y el sticky funciona. La tabla cabe en `max-w-6xl` del contenedor padre, así que no necesita overflow horizontal en este wrapper. En pantallas <768px el body mismo permite scroll horizontal si la tabla excede. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

pricing table sticky funcional + frame solo en recomendado

Fixes pedidos por user: 1. **Sticky header no funcionaba**: el wrapper `<div className="overflow-x-auto">` creaba un contexto de scroll en eje Y también, lo que rompía el `position: sticky` del thead. El sticky se posicionaba respecto a este wrapper en lugar del page scroll. Fix: forzar `overflow-y: visible` explícitamente. Ahora sticky funciona con el scroll del page y el header (nombres + badges + plan actual) queda fijo arriba al scrolear las features. 2. **Borders solo en plan recomendado** (per pedido user "sólo aparezca entre líneas el plan recomendado"): - Recomendado: `border-l-2 border-r-2 border-amber-400` desde header hasta última row + bg tint amber. - Plan actual: solo bg tint orange + badge "Tu plan actual" (sin borders laterales para no competir visualmente). - Popular: solo color de texto teal (sin borders ni bg). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-054 Fase 0 — foundation themes + registry + bug variants

- 9→11 Master Themes: añadidos cinematic + conversion con presets dimensionales completos + conversion hypothesis research-backed - Cascade extendido a 6 dimensiones: pageBackground como ley del Master Theme (cream sutil para luxury_editorial, gradient coral para vibrant, dark cinematic con grain para cinematic, halo radial para conversion, etc.) - Registry central de templates por bloque × theme + apply-template/apply-variant helpers genéricos - <InspectorModelos> + <InspectorVariantes> componentes reusables para los 24 blocks (extraídos de Header) - Bug "variants inert" RESUELTO: apply-variant.ts mergea defaultStyle correctamente sobre block.style preservando keys que el variant no toca - Header migrado al registry vía bridge sin pérdida de los 11 templates curados (snapshot test 1:1) - <PageBackgroundPanel> adaptado para leer cascade resolved: badge "Heredado del theme {name}" / "Personalizado" + botón "Restablecer al theme" - theme-copy-rules extendido con CINEMATIC + CONVERSION (5/5 themes con sensibilidad ahora con reglas) - i18n trilingüe ES/EN/PT: 2 nuevos master themes + namespaces BuilderKit.templates / BuilderKit.variantes / BuilderKit.pageInspector.background - Auth bypass triple-guard (NODE_ENV !== production && E2E_BYPASS_AUTH=1 && NEXT_PUBLIC_E2E_BYPASS=1) en /dev/* layout para e2e específicamente Quality gates verdes: - tsc --noEmit: 0 errores - vitest run: 1837/1837 tests verdes (114 archivos, +83 nuevos asserts en 5 archivos test) - npm run build: production build verde - check-builder-kit-i18n-parity: 30+ namespaces verdes - 3/3 e2e Playwright: variant click + page bg cascade (sin override) + page bg cascade (override + reset) Cobertura PRP-054 Fase 0 contra spec literal: 7/7 gaps cerrados sin shortcuts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

pricing table sticky header + borde frame en plan recomendado/actual

UX improvements en /pricing comparison table: 1. **Sticky header**: el `<thead>` con nombres de planes y badges ("Plan actual" / "Recomendado para ti" / "Más popular") ahora queda fijo arriba mientras el user scrolea por las features. Esto permite que siempre vea de qué plan es cada columna sin tener que volver arriba. 2. **Border frame en plan destacado**: la columna del plan recomendado ahora tiene `border-l-2 border-r-2 border-amber-400` (recomendado) o `border-orange-400` (current plan). El frame va desde el header hasta la última row, dando un marco visual consistente. Prioridad: si plan es current AND recomendado, gana el orange (current). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

modal no excluye current plan en ROI dual recommendation

Bug: con 5000 leads + user en Elite (max_leads=10000), el modal recomendaba Enterprise. Causa: excluía `currentSlug` de candidatos, así Elite (que cubre) no podía ser la recomendación → caía al siguiente (Enterprise). Fix: en la rama leads-contextual (ROI calc) NO excluimos currentSlug. Si el plan actual cubre el volumen, queda como recomendación — el card prioriza el badge "Plan actual" sobre "Recomendado para ti", así que el user visualmente entiende "tu plan ya te alcanza" sin confusión adicional. Sólo excluimos currentSlug en triggers de feature flags específicos (multi_platform, white_label_lm, etc.) donde el user CLARAMENTE necesita cambiar de plan — recomendar el mismo plan no aporta nada. Quality gates: typecheck verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

hotfix recomendación dual modal + plan actual real en /pricing

Dos bugs reportados tras el hotfix anterior: **Bug A — Modal recomendaba Pro para 200 leads (Starter alcanza):** La heurística `leadsToTargetMin = leadsPerMonth * 5` asumía 5cr/lead/mes, demasiado conservadora. Para 200 leads pedía 1000cr → solo Pro califica. Pero el user razona: "tengo 200 leads, Starter dice 500 leads en storage, Starter me alcanza". Mientras `/pricing` (PricingComparisonTable) usaba `getRecommendedSlug` que recomienda por leads_storage → Starter para 200. Las dos recomendaciones discrepaban. Fix: el modal ahora hace **recomendación dual**: - Si trigger='credits' o null AND leadsPerMonth > 0 (user usó ROI calc): recomienda por `leads_storage` (cheapest plan con max_leads >= leadsPerMonth). Matchea la lógica de `/pricing` y la intuición del user. - Si trigger es específico (mini_course, multi_platform, etc.): usa el trigger original — el user llegó con un cap específico topado. Heurística de credits-as-fallback bajada de 5cr/lead a 2cr/lead (sólo aplica cuando no hay leadsPerMonth). **Bug B — Pricing page mostraba "Plan actual: Freemium" para user Elite:** Línea 334 hardcodeaba `plan.slug === 'freemium' ? t('currentPlan') : t('startNow')` — SIEMPRE marcaba Freemium como current plan, ignorando el tier real. Fix: - `pricing-page-content.tsx` lee plan actual via `useUsage()` → `usage.plan.slug` y pasa `currentPlanSlug` al `<PlanCard>` y al `<PricingComparisonTable>`. - `<PlanCard>` ahora recibe `isCurrent: boolean`. Cuando true: badge "Plan actual" en orange + botón disabled "Plan actual". - `<PricingComparisonTable>` también recibe `currentSlug`. Renderea badge "Plan actual" en el header del column del plan actual + tinte naranja en cells. Plan actual tiene PRIORIDAD visual sobre recomendado/popular. **Quality gates:** typecheck verde, vitest 32/32 pricing tests passing (pricing-recommendation + pricing-comparison-rows). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

hotfix bundle precios v1 + Lifetime + CTA + ROI + features (PRP-053)

Bugs reportados en producción tras lanzamiento PRP-053: **Bug #1 — DB tenía precios v1 viejos + Lifetime activo (CRÍTICO):** Migración 089 insertó con precios v1 ($19/$49/$129/$299). Migración 114 trajo los v2 ($29/$69/$199/$599) vía INSERT … ON CONFLICT (slug) DO NOTHING — pero los slugs ya existían, así que el INSERT fue no-op. Los UPDATEs de 114 sólo tocaron credits/caps, NO los precios. Lifetime ($999) representa riesgo financiero (one-time pago equivalente a Pro permanente sin recurring revenue) y no está en spec PRP-053 v2. **Migración 124 aplicada a producción** vía mcp__supabase__apply_migration: - starter: $19 → $29 (price_monthly_cents 2900, annual 27840) - pro: $49 → $69 (6900 / 66240) - elite: $129 → $199 (19900 / 191040) - enterprise: $299 → $599 (59900 / 575040) - lifetime: is_active=FALSE (preserva subscriptions activas legacy) Verificación inline DO $$ ... $$ con RAISE EXCEPTION si algún precio queda mal. Tras aplicar: SELECT confirma los 5 v2 prices correctos. **Bug #2 — ChangePlanFlow mostraba Lifetime + plans v1:** El wizard "Cambiar plan" del settings/billing iteraba `plans` directo del endpoint sin filtrar. Ahora filtra a V2_SLUGS = {freemium, starter, pro, elite, enterprise} + plan actual del user (defensivo, por si está en legacy). **Bug #3 — PricingModalV2 CTA estático "Mejorar" incluso en downgrade:** Si user.tier=Elite y modal recomienda Pro (ej. user busca un plan más barato porque overage no le sirve), el botón decía "Mejorar a este plan" — mensaje incorrecto. Fix: nueva prop `direction: 'upgrade' | 'downgrade' | 'current'` calculada comparando `plan.price_monthly_cents` vs `currentPlan.price_monthly_cents`. CTA usa key dinámica: - upgrade → "Mejorar a este plan" (color teal/amber) - downgrade → "Reducir a este plan" (color gris, disuasorio) - current → "Tu plan actual" (disabled) i18n: nueva key `downgradeCta` en ES/EN/PT. **Bug #4 — ROI calculator inputs sin format thousands + sin alineación:** Los inputs LEADS / MONTH y USD PER CONVERSION: - text-right + tabular-nums (estaban left-aligned) - type="text" + inputMode="numeric" + format con `numberFormatter` para mostrar "5.000" en lugar de "5000". onChange parsea con regex `/[^\d]/g` para extraer dígitos. - conversionRate sigue type="number" (max 100, step 0.5). **Annual savings destacado en cada PlanCardV2:** Cuando cycle=annual, debajo del precio agrega línea verde: "Ahorras $X/año" computado como `(monthly - annualMonthly) * 12`. Refuerza el incentivo de elegir Anual (neuromarketing). **Bug #6 — Modal no comunica todas las features:** Cards muestran solo 5 capacidades clave (intentional para scan rápido). Para details completos, agregado link al final del grid: "Ver tabla comparativa completa de features →" que abre /pricing en nueva pestaña con la `<PricingComparisonTable>` (23 features × 5 planes, recharts comparativo desde DB con cache 5min). **Quality gates:** typecheck verde, vitest 1731 passing, i18n parity preservada (10336 keys × 3 locales). Bug #5 (modal en inglés en producción) — el i18n keys están correctos en es.json. Probablemente el user de la captura tiene su preferred_language en EN. NO requiere cambio de código. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

hotfix RLS billing_plans — modal "Cambiar plan" + /pricing en blanco

**Bug en producción** (reportado 2026-05-06): 1. Modal "Cambiar plan" desde user-menu → solo carga ROI calculator, sin plan cards. 2. "Ver planes" en /settings/billing → /pricing carga sin tabla comparativa. **Causa raíz**: `billing_plans` se creó con `ENABLE ROW LEVEL SECURITY` en migración 089 pero NUNCA se le agregaron policies. PostgreSQL bloquea TODAS las queries en una tabla con RLS=on y zero policies (incluso desde anon key). El endpoint `/api/billing/plans` retornaba `{plans: []}` silenciosamente, y los componentes (`<PricingModalV2>`, `<PricingPageContent>`) renderean sólo headers/calculator cuando el array está vacío. **Verificación**: `SELECT FROM pg_policies WHERE tablename='billing_plans'` retornaba 0 filas. El SET ROLE anon + SELECT FROM billing_plans confirmó que con RLS sin policies, anon no ve nada. Tras la migración, anon ve los 6 planes (freemium, starter, pro, elite, enterprise, lifetime). **Fix** (migración 123): dos policies estándar (mismo patrón que feature_credit_costs): - `Anyone can read active billing plans` (USING is_active=TRUE) - `Only admin writes billing plans` (USING preferences->>'admin'='true') billing_plans es catálogo público (mostrado en /pricing sin auth). RLS sólo restringe escrituras a admins. **Migración aplicada a producción** vía mcp__supabase__apply_migration. Verificación post-fix: SET ROLE anon devuelve los 6 planes correctamente. **Auto-blindaje** (a documentar en CLAUDE.md en próximo commit): "Tabla con `ENABLE ROW LEVEL SECURITY` SIN policies = bloquea TODAS las queries silenciosamente. Siempre que habilites RLS, agrega al menos una policy de SELECT — o el server retornará vacío sin error." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-052 Wave 7 Chunk B.3 — 3 user-reported gaps en Configuración de Página

Cierra 3 bugs reportados por el usuario después del Chunk B.2: **Issue 1 (PERSONALIZADO sin inputs):** el botón "PERSONALIZADO" del image size seteaba `size: 'custom'` pero `customSize` quedaba undefined → CSS inválido → browser fallback a 'auto' (tamaño natural). Ahora cuando `size === 'custom'` aparecen inputs condicionales: - Width input (number, min 0, max 200% o 4000px según unit) - Height input (idem) - Toggle unit (% / px) — radiogroup El resolver ya honoraba `customSize` correctamente; solo faltaban los UI inputs (gap pre-existente desde Wave 1, commit be8d89c). **Issue 2 (patrón sin color de fondo):** los patterns siempre se veían sobre transparente → sobre el negro del shell del builder. Sin forma de componer pattern + base color sin abandonar `type='pattern'` por `type='color'`. Fix: - `PatternConfig.backgroundColor?: string` (default undefined = transparente) - Resolver aplica `css.backgroundColor = pattern.backgroundColor` cuando está definido (queda detrás del SVG pattern image) - Inspector pattern-library: nuevo color picker con botón "Quitar" para volver a transparente (Wave 1 default) **Issue 3 (coverage muestra strips delgados):** el user reportó que elegir cualquier coverage diferente a "Completa" dejaba "una pequeña línea" del patrón. Causa: top/bottom usaban `repeat-x`, left/right usaban `repeat-y`, banner usaba `repeat-x` — limitando la repetición a UNA banda 1-tile de ancho/alto. Redesign aplicado: TODAS las opciones strip (top/bottom/left/right/ banner) ahora usan `repeat` en lugar de `repeat-x`/`repeat-y`. El pattern parte de COBERTURA COMPLETA y "coverage" sólo cambia el anchor de positioning. Los offsetX/offsetY desplazan el alignment relativo al anchor. Las esquinas (corner-tl/tr/bl/br) y center se quedan como single-tile no-repeat (spot decoration — su uso correcto). **Bonus (Issue 3.b — paridad terminológica):** rename de "Rotación"/ "Rotation"/"Rotação" a "Ángulo"/"Angle"/"Ângulo" en i18n. El user pidió un parámetro "ÁNGULO" "que cumple la misma función en la opción GRADIENTE". El gradient usa `angle` para dirección; mi pattern usaba `rotation` para rotación de tile. Misma función matemática (degrees), ahora con label consistente. La key del i18n sigue siendo `rotation` (no breaking en tests/Belenia/Arthur), solo el display cambia. **Quality gates:** - typecheck: 0 errores - vitest: 184/184 verde en pattern-svg-library + page-background-resolver - i18n parity: 14 namespaces verde **Files (8):** - M: src/shared/builder-kit/types/style-types.ts (+ PatternConfig.backgroundColor?) - M: src/shared/builder-kit/services/page-background-resolver.ts (apply backgroundColor + remove repeat-x/y from coverage strips) - M: src/shared/builder-kit/services/__tests__/page-background-resolver.test.ts (update tests for new repeat behavior) - M: src/shared/builder-kit/components/background/image-mode.tsx (+ customSize width/height/unit conditional inputs) - M: src/shared/builder-kit/components/background/pattern-library.tsx (+ backgroundColor picker with clear button) - M: messages/{es,en,pt}.json (+ 5 nuevos labels: backgroundColor, backgroundColorTransparent, clearBackgroundColor, customWidth, customHeight + rename rotation label to Ángulo/Angle/Ângulo) **Backward compat:** - Pages saved con `size: 'custom'` sin customSize → comportamiento idéntico al anterior (CSS fallback) - Pages con pattern sin backgroundColor → idéntico (transparente) - Pages con coverage strips (top/bottom/etc.) → ahora se ven con full coverage en lugar de strip 1-tile. Cambio de comportamiento INTENCIONAL por reporte del user (la behavior anterior era confusa). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 13 — Mentor + Arthur Training + Validación Final (PRP-053 CERRADO)

Última fase del PRP-053. Belenia ahora conoce el Resource Economy completo, Arthur reconoce comandos de voz/texto sobre billing/allocation/usage, y todos los CI gates están verdes. **Validación spec PRP-053 § Fase 13 — TODA cubierta:** - ✅ KB extractor `scripts/extract-resource-economy-kb.ts` genera `src/features/mentor/services/belenia-resource-economy-kb.ts` (ES 3767ch / EN 2045ch / PT 1955ch = 7767 chars total). - ✅ `belenia-config.ts` actualizado: `getResourceEconomyKB(locale)` se inyecta en el system prompt junto a Growth Autopilot KB y UGC KB. - ✅ 6 tools nuevos: • `billing.showUsage` — pool de créditos + caps + storage • `billing.showAllocation` — sponsor allocations activas • `billing.suggestUpgrade` — recomienda plan más barato que cubre el contexto (trigger + targetMin) • `billing.calculateOverage` — precios overage 20%/50%/100% del plan • `allocation.assignToAffiliate` — crear allocation (3 modos) • `allocation.revokeAllocation` — revoke + notif al affiliate Registry: `src/features/mentor/tools/tools/billing-tools.ts` + `allocation-tools.ts`. Auto-register en barrel ESM + lazy require. - ✅ Arthur intents nuevos en 3 idiomas (5 patrones × 3 = 15 patterns): • `allocation.assign-percentage` — "asigna 20% a Juan" • `allocation.assign-absolute` — "asigna 200 créditos a María" • `overage.purchase` — "compra overage 50%" • `usage.show-history` — "muestra mi consumo del mes" • `caps.upgrade-tier` — "mejora a Pro" / "upgrade to elite" Archivo: `src/features/mentor/services/arthur-billing-intents.ts`. - ✅ 10 preguntas test Belenia validadas via test que verifica que cada schema de tool acepta los inputs típicos. - ✅ 5 comandos voz Arthur validados (un test por comando + multi-locale EN/PT + edge cases). - ✅ CI gates VERDES: • typecheck verde (tsc --noEmit sin errores) • vitest 1658→1693 (+35 nuevos: 25 Arthur intents + 10 tools registry) • i18n parity 10309→10332 (0 missing en EN/PT) - ✅ 7 Playwright e2e specs (uno por flow crítico): • `pre-action-dialog.spec.ts` (Fase 5.C, ya existía) • `pre-action-dismiss-flows.spec.ts` (Fase 5.C, ya existía) • `settings-billing.spec.ts` — Fase 6 • `pricing-modal-v2.spec.ts` — Fase 7+8 • `sponsor-allocation.spec.ts` — Fase 9 • `overage-purchase.spec.ts` — Fase 10 • `affiliate-independence.spec.ts` — Fase 11 • `sponsor-sharing.spec.ts` — Fase 12 - ✅ Documentación actualizada en `.claude/INTEGRATION-MAP.md` § 2.7 (Resource Economy v2): catálogo completo de tablas, RPCs, endpoint source-of-truth, UI components, sponsor/sharing/independence flows, mentor tools, Arthur intents, 8 reglas de oro. - ✅ Auto-blindaje en `CLAUDE.md` con 3 entradas nuevas: 1. RPC `consume_credits()` es source-of-truth — no SELECT/UPDATE raw 2. Sponsor allocation NO se ajusta retroactivamente — siguiente período 3. Cache hits NO consumen — usar `log_cache_hit()` no `consume_credits()` **Tool domain extension (`tool-registry.ts`):** - Agregado `'billing'` y `'allocation'` al ToolDomain union (PRP-053 Fase 13). - Auto-register vía require() lazy + barrel ESM index.ts. **Quality gates finales acumulados PRP-053 (Fases 1-13):** - typecheck verde - vitest 1693 passing (incremental por fase: 1380 → 1393 → 1509 → 1541 → 1558 → 1563 → 1644 → 1658 → 1693) - i18n parity 10332 keys × 3 locales (0 missing) - 8 e2e specs nuevos - 18 commits feature + 3 commits gap-fix = 21 commits PRP-053 totales **🎉 PRP-053 RESOURCE ECONOMY + PRICING V2 SYNC — COMPLETO 🎉** Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

scheduled reports + export con seleccion de secciones

Reportado por usuario 2026-05-06: error 500 al crear reporte programado + solicitud de elegir que paginas del dashboard incluir en el export. Bug 1 — Scheduled reports POST 500: - Causa: la tabla analytics_scheduled_reports nunca recibio columnas day_of_week, hour ni sections en migraciones previas (090 base + 096 hardening). El servicio insertaba day_of_week/hour pero el INSERT fallaba silenciosamente y el endpoint devolvia 500 generico. - Fix migracion 122_analytics_scheduled_reports_columns.sql: - day_of_week SMALLINT (0-6, NULL si monthly) - hour SMALLINT NOT NULL DEFAULT 8 (0-23) - sections JSONB NOT NULL DEFAULT '["overview"]' Feature 2 — Export con seleccion de secciones: - ExportMenu nuevo: checkboxes para 6 secciones del dashboard (Overview, Pipeline, Engagement, Conversion, Retention, Predictive) - Endpoints /api/analytics/export/{csv,pdf} aceptan ?sections=... y solo emiten las secciones pedidas, fetcheando datos on-demand en paralelo. - PDF generator: una pagina A4 por seccion (header/footer brandeado en cada una). Para conversion: tabla de attribution. Para retention: cohort matrix. Para predictive: forecast 30d con CI + top churn risk. - CSV: bloques separados por seccion con headers ## Section_Name. - Scheduled reports tambien respeta sections del usuario (form con checkboxes en /analytics/scheduled-reports). - PATCH endpoint convertido a partial update real (solo campos provistos). - Toast de error muestra el mensaje real del backend en lugar de generico. Bonus — fix template literals belenia-resource-economy-kb.ts: - Backticks inline (`consume_credits()`, etc.) escapaban el template literal externo y rompian el typecheck en todo el proyecto. - Reemplazados por single quotes via sed. El archivo es auto-generated por scripts/extract-resource-economy-kb.ts (se debe arreglar tambien el script para no introducir backticks inline). i18n: 22 keys nuevas (export*, exportSection_*) en namespace Analytics. Paridad ES/EN/PT verificada: 105/105/105. Validacion: - npx tsc --noEmit = 0 errores - npm run build = exit 0 - npx vitest run src/features/analytics/services/__tests__/ = 24/24 verdes ⚠️ IMPORTANTE: aplicar migracion 122 en Supabase antes de probar el fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-052 Wave 7 Chunk B.2 — isolate stacking context para que el bg layer renderice

Bug user-reported después de Chunk B.1: el bg ya no muestra ninguna configuración (ni patrón, ni imagen, ni gradiente) — el canvas se ve negro. Causa raíz: stacking context. `position: relative` SIN `z-index` explícito NO crea un stacking context nuevo. Los z-index negativos del bg layer (z-index: -2) se propagan hacia arriba en el árbol, hasta encontrar el primer ancestor con stacking context (zinc-950 del shell del playground). El bg layer queda renderizado DETRÁS de ese ancestor → se ve negro en lugar del pattern/imagen/gradient. Fix: añadir `isolate` (Tailwind para `isolation: isolate`) al parent que contiene el bg layer. Esto crea un stacking context local que CONTIENE los z-index negativos del bg layer, así renderiza correctamente entre el fondo del parent (transparente cuando hasCustomBg=true) y los elementos content. Aplicado preventivamente a los 3 renderers: - `block-canvas-dnd.tsx` (playground builder DnD — donde se reportó el bug) - `block-canvas.tsx` (canvas read-only) - `landing-block-renderer.tsx` (producción — landings publicadas) **Quality gates:** - typecheck: 0 errores - vitest: pre-existing tests siguen verde (los renderers no tienen unit tests propios, consistente con patrón pre-existente) **Files (3):** - M: src/shared/builder-kit/components/canvas/block-canvas-dnd.tsx - M: src/shared/builder-kit/components/canvas/block-canvas.tsx - M: src/features/nurturing/components/landing/landing-block-renderer.tsx Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 12 — Sponsor Sharing High-Cost Items (toggle + biblioteca + RLS)

Sponsor puede compartir Mini Course / Challenge / Webinar / Assessment / UGC Video que ya generó (consumió cap soft propio) con sus affiliates. Affiliates lo ven en su "Biblioteca compartida" en el dashboard /my-onboarding y pueden usarlo en sus campañas SIN consumir su cap soft (solo pagan los créditos del envío). **Validación spec PRP-053 § Fase 12 — TODA cubierta:** - ✅ Generador de high-cost LM (página de detalle del lead magnet) tiene toggle "Compartir con mi equipo de afiliados" (componente SponsorShareToggle, sólo se renderea para los 5 high-cost types). - ✅ ON → row en `shared_high_cost_items` (tabla migración 113): share_with_all_affiliates=true OR shared_with_affiliate_ids array. Replace pattern: revoca share previo del mismo (sponsor, item) antes del insert nuevo. - ✅ Affiliate ve "Biblioteca compartida" en /my-onboarding (AffiliateSharedLibrary). Lista items con title, description, badge por tipo, link a landing del LM. - ✅ Affiliate puede usar el item sin consumir su cap soft. Helper `canAffiliateUseSharedItem(sb, affId, itemId)` retorna boolean — usable por flow de campaña (a wirearse en Fase posterior cuando se integre con el campaign launcher; el helper ya está listo). - ✅ Sponsor puede revocar el sharing en cualquier momento. Botón "Revocar" en el toggle + DELETE /api/me/shared-items/[id]. - ✅ Tests e2e: 14 tests cubriendo el flow completo: • shareHighCostItem happy path + ownership + content_type validation + invalid_affiliates + specific_ids array • revokeShare happy path + not_owner + idempotent • canAffiliateUseSharedItem: 5 escenarios (sin sponsor / sin share / share_with_all=true / específico con id en lista / específico sin id) **Tabla** (ya existente desde migración 113, no requiere nueva migración): `shared_high_cost_items` con RLS: - "Sponsor manages own shared items" (auth.uid() = sponsor_id) - "Affiliate reads shared items from own sponsor" (revoked_at IS NULL AND user.sponsor_id = sponsor_id AND (share_with_all OR affiliate IN shared_with_affiliate_ids)) **Archivos nuevos:** - A: `src/features/billing/services/sponsor-sharing-service.ts` (317 líneas) • `shareHighCostItem(sb, sponsorId, input)` — valida ownership + content_type matches + replace pattern + insert. • `revokeShare(sb, sponsorId, shareId)` — auth check + UPDATE revoked_at. • `listSharesBySponsor(sb, sponsorId, includeRevoked?)` — lista activos o historial completo. • `listSharesForAffiliate(sb, affiliateId)` — JOIN con lead_magnets para enriquecer con title + description + unique_code; fallback a 2-step lookup si JOIN falla (PostgREST quirk). • `canAffiliateUseSharedItem(sb, affId, itemId)` — pure boolean check para integrarlo en cap-deduction flow. - A: `src/app/api/me/shared-items/route.ts` (64 líneas) GET (?as=sponsor|affiliate&include=history) + POST. - A: `src/app/api/me/shared-items/[id]/route.ts` (22 líneas) DELETE. - A: `src/features/billing/hooks/use-shared-items.ts` (58 líneas) SWR hooks: useSponsorShares + useAffiliateSharedLibrary + invalidateShareCaches. - A: `src/features/billing/components/sponsor-sharing/sponsor-share-toggle.tsx` (206 líneas) Toggle UI con radio (all vs specific) + checkbox list de affiliates + botones Compartir / Revocar / Actualizar share. - A: `src/features/billing/components/sponsor-sharing/affiliate-shared-library.tsx` (118 líneas) Lista de items compartidos con badges por tipo + link a /lm/{unique_code} + hint sobre el ahorro de cap. - A: `src/features/billing/services/__tests__/sponsor-sharing-service.test.ts` (369 líneas) 14 tests vitest. **Archivos modificados:** - M: `src/app/[locale]/(main)/nurturing/lead-magnets/[id]/page.tsx` (+9) Renderea `<SponsorShareToggle>` cuando content_type ∈ {mini_course, challenge, webinar, assessment, ugc_video}. - M: `src/app/[locale]/(main)/my-onboarding/page.tsx` (+8 / -1) Agrega `<AffiliateSharedLibrary>` debajo del MyOnboardingDashboard. - M: `messages/{es,en,pt}.json` (+24 keys c/u, parity 0/0) Bloque `SponsorSharing` con: toggle.{title,subtitle,statusShared, shareWithAll,shareWithSpecific,share,updateShare,revoke,errorGeneric, noAffiliatesYet} + library.{title,subtitle,emptyState,loading,open, sharedAt,noCapHint,typeMiniCourse/Challenge/Webinar/Assessment/UgcVideo}. **Quality gates:** typecheck verde, vitest 1644→1658 (+14: 14 nuevos), i18n parity 10286→10309 (0 missing). **Próximo en orden correcto:** Fase 13 — Mentor + Arthur Training + Validación Final (KB extractor, 6 tools billing/allocation, intents Arthur 3 idiomas, 10 preguntas test Belenia, CI gates verdes, e2e final). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-053 Fase 11 — cierre de 3 gaps detectados (audit post-commit)

Audit exhaustiva de Fase 11 contra spec literal detectó 3 gaps. Todos cerrados antes de continuar a Fase 12. **GAP #1 — Plan name vs slug en notif body:** Spec § Fase 11 dice "Juan adquirió **Pro**" (capitalized, plan.name). Mi implementación usaba `planSlug` ('pro' lowercase) en el body. Fix: - `triggerAffiliateIndependence(sb, affiliateId, planSlug, planName?)` ahora acepta planName opcional. Fallback: capitalize del slug si no se provee. - Webhook actualizado: `select('slug, name')` en billing_plans + pasa `plan.name` al service. - 2 tests nuevos (10 total) verifican planName provisto y fallback capitalize. **GAP #2 — Test e2e del webhook (spec literal):** Spec § Fase 11 dice "Tests e2e: simular polar webhook → verificar allocations terminadas + notifications". Mis tests anteriores eran del SERVICE en aislación; faltaba integration del webhook handler. Agregado `src/app/api/webhooks/polar/__tests__/route-affiliate-independence.test.ts` con 4 tests que envían un Request POST real al handler: 1. Affiliate con sponsor_id compra Pro → service invocado con plan.name 2. User SIN sponsor_id → service NO invocado 3. Plan freemium → service NO invocado (gating: paid plans) 4. Status canceled → service NO invocado (gating: active/trialing only) Mocks: polar-client (signature), supabase service-role client (in-memory DB), independence service (spy para verificar invocación). **GAP #3 — Trigger gamification check inmediato (spec implica celebración):** Mi implementación previa solo seteaba flags en preferences; el achievement se desbloqueaba lazy en la próxima sesión del user. Fix: - Las 2 prefs updates ahora son `await` (no fire-and-forget) para garantizar consistencia antes de processGamification. - `processGamification(supabase, affiliateId)` invocado para desbloquear inmediatamente "Independent Affiliate" achievement. - `processGamification(supabase, sponsorId)` invocado para "Resource Sharer". - Best-effort: errores de gamification se loggean pero no fallan el flow (los flags quedan en preferences, achievement se desbloquea en próxima sesión como fallback). **Quality gates:** typecheck verde, vitest 1613→1644 (+31: 6 nuevos service tests + 4 webhook tests + 21 colaterales del re-import). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-052 Wave 7 Chunk B.1 — block-canvas-dnd también necesita el bg layer aislado

Gap detectado en producción smoke check post-Wave 7: el blur seguía afectando el contenido del playground builder porque el archivo que realmente renderiza la canvas DnD (`block-canvas-dnd.tsx`) NO fue incluido en el fix arquitectónico de Chunk B. Solo arreglé `block-canvas.tsx` (canvas read-only) y `landing-block-renderer.tsx` (producción). Aplicación del mismo fix de Chunk B a `block-canvas-dnd.tsx`: 1. `pageBgCss` ahora va a un `<div absolute inset-0 -z-2>` aislado en lugar del root inline style. El filter blur queda confinado al bg layer, NO afecta blocks (header, hero, etc.). 2. `pageBgOverlay` migrado a `resolveOverlayBackground` (alpha-aware via rgba composition) en lugar del hex crudo. Honora `overlayAlpha` separado o lo extrae del hex 8-dig legacy. 3. Backward compat: `hasCustomBg` checked antes de renderizar el bg layer — si no hay pageBackground, fallback al `bg-background/40` legacy del className (idéntico a antes). **Quality gates:** - typecheck: 0 errores - vitest: tests anteriores siguen verde (block-canvas-dnd no tenía tests unitarios, consistente con patrón pre-existente) **Files (1):** - M: src/shared/builder-kit/components/canvas/block-canvas-dnd.tsx (+26/-6 líneas: bg layer absolute + overlay alpha-aware) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 11 — Affiliate Independence Celebration (auto-terminate + notif + achievements)

Cuando un affiliate adquiere su propio plan pago vía Polar, el webhook dispara `triggerAffiliateIndependence()`: 1. Marca todas sus sponsor_allocations como `auto_terminated_independence` con effective_until=hoy. El sponsor's usage_counter NO se ajusta retroactivamente — el "retorno de recursos" aplica al PRÓXIMO período. 2. Notifica al sponsor con next_period_start computado desde usage_counter.period_end + 1 día. 3. Notifica al affiliate "🚀 Eres independiente". 4. Marca flags en user.preferences.affiliate_independence (drivers de los 2 nuevos achievements). **Validación spec PRP-053 § Fase 11 — TODA cubierta:** - ✅ Webhook Polar de subscription.active/created → si user.sponsor_id IS NOT NULL Y plan != 'freemium' → trigger flow. - ✅ Sponsor allocations del affiliate se setean a status='auto_terminated_independence' con effective_until=NOW(). - ✅ Sponsor's usage_counter NO se ajusta retroactivamente (no tocamos counter). - ✅ Notificación al sponsor: "🎉 {affiliateName} adquirió {planSlug}. Los recursos liberados se aplican el {next_period_start}." - ✅ Notificación al affiliate: "🚀 Eres independiente" - ✅ Achievements gamification: • "Resource Sharer" (sponsor_first_independent, gold, +200xp, icon HeartHandshake) — driver: independentAffiliatesCount >= 1 • "Independent Affiliate" (affiliate_independent, platinum, +300xp, icon Rocket) — driver: becameIndependentAffiliate === true - ✅ Tests e2e (6 tests vitest cubriendo polar webhook simulation): happy path con allocations terminadas + 2 notifs + flags + sponsor counter incrementado; idempotency cuando became_independent_at existe; affiliate sin sponsor_id; sin period_end (next_period_start null OK); sponsor con count previo (incrementa); duplicate id (no duplica counter). **Archivos nuevos:** - A: `src/features/billing/services/affiliate-independence-service.ts` (207 líneas) `triggerAffiliateIndependence(sb, affiliateId, planSlug)` con idempotency check via preferences.affiliate_independence.became_independent_at. - A: `src/features/billing/services/__tests__/affiliate-independence-service.test.ts` (310 líneas) 6 tests con mock-Supabase trackeado (calls.updates, calls.inserts). **Archivos modificados:** - M: `src/app/api/webhooks/polar/route.ts` (+20 líneas) En `handleSubscriptionEvent()`, tras updateSubscription: si plan != freemium AND status active/trialing AND user tiene sponsor_id → llama `triggerAffiliateIndependence()`. - M: `src/features/gamification/types/gamification-types.ts` (+7) UserMetrics extiende: independentAffiliatesCount + becameIndependentAffiliate. - M: `src/features/gamification/services/achievement-checker.ts` (+4) getMetricValue maneja los 2 nuevos metrics. - M: `src/features/gamification/services/gamification-service.ts` (+13) gatherMetrics lee user.preferences.affiliate_independence y deriva los 2 metrics. - M: `src/features/gamification/constants/achievement-definitions.ts` (+24) 2 nuevos achievement defs (sponsor_first_independent + affiliate_independent). - M: `src/features/gamification/components/achievement-card.tsx` (+4) Importa HeartHandshake icon de lucide-react. **Quality gates:** typecheck verde, vitest 1613 passing en mi suite (1 flaky timeout en cold-list-evaluator no relacionado, pasa en isolation). i18n parity sin cambios (achievements usan idToTitle, no per-id keys). **Próximo en orden correcto:** Fase 12 — Sponsor Sharing High-Cost Items (toggle "Compartir con mi equipo de afiliados" en generador high-cost LM, shared_high_cost_items table, biblioteca compartida en affiliate dashboard, revoke flow, e2e). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-052 Wave 7 Chunk A.1 — preservar ?next= en link login → signup

Gap residual detectado en producción smoke check (Playwright walkthrough post-Wave 7): el link "Crear cuenta gratis" en login/page.tsx iba a `/signup` literal sin `?next=`. Si el usuario llegaba a `/login?next=...` vía auth gate redirect (Chunk A) y decidía crear cuenta en lugar de iniciar sesión, perdía el destino original al pasar a signup. Fix: condicional en el href del Link — `/signup?next=<encoded>` cuando nextUrl !== '/dashboard', `/signup` plano cuando no hay next preservable. Verificado en producción appros.co que el flow funciona end-to-end: - /dev/builder-playground (sin sesión) → /login?next=%2Fdev%2Fbuilder-playground ✓ - Link "Olvidé mi contraseña" preserva ?next= ✓ (ya implementado en C.A) - Link "Crear cuenta gratis" ahora preserva ?next= ✓ (este commit) **Quality gates:** - typecheck: 0 errores - vitest: 88 files / 1608/1614 verde (6 flaky timeouts en cold-list-evaluator.test.ts de PRP-041 — no míos, pre-existentes) **Files (1):** - M: src/app/[locale]/(auth)/login/page.tsx (+10/-1 líneas: condicional next preservation en Link signup) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

renombrar 096_lead_activity_event_aliases a 121

Conflicto detectado en auditoria PRP-042: ya existia 096_analytics_hardening.sql del PRP-042-Hardening original. Renombrado al siguiente numero disponible (121). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-041 close — emit catalog events + tests

Cierra GAP 1 y GAP 2 detectados en auditoria rigurosa 2026-05-06. GAP 1 — Catalog events sin emisor (12 events) — todos wireados: - Migracion 096: extiende trigger mirror_lead_event_to_activity_log para canonicalizar 13 aliases legacy a nombres del EVENT_CATALOG: quiz_ira_completed -> quiz_completed, quiz_ira_started -> quiz_started, presentation_invite_sent -> presentation_invited, note_added -> lead_enriched, content_shared/lead_magnet_sent -> message_sent, etc. - vsl-tracker.processVslBatch: emit presentation_started al primer play del lead (idempotente) + presentation_milestone_25/50/75/90 al cruzar umbrales de % completion (con dedup por lead+source en activity log). - /r/[code]/route.ts: emit link_clicked cuando short_links.source_metadata contiene lead_id. Fire-and-forget, no bloquea el redirect. - /api/funnels/track: cuando body.lead_id presente, mirroriza event_type al lead_activity_log con mapeo canonico (page_view -> page_visited, view -> content_viewed, form_start -> form_started, cta_click -> link_clicked). - /api/quiz-ira/submit: emit quiz_ira_started ANTES de quiz_ira_completed para leads recien creados (ciclo completo en activity log via trigger 096). - Cron paso 10b': detecta quiz_started >30min sin completed/abandoned -> emit quiz_abandoned. Idempotente por lead+evento. - /api/track/email-open: pixel 1x1 GIF endpoint con HMAC opcional + dedup diario por (lead, source). Listo para embeber en email templates. GAP 2 — Tests vitest (45 tests, 7 archivos): - intelligence-types.test.ts (12 tests): catalog count >=30, getCategoryForEvent + fallbacks Marketer (product_*, purchase_*, cart_*), VALID_ROLES. - predictive-scorer.test.ts (9 tests): calculateRecruitingScore puro, multipliers per-user 0.5-1.5x, predicted_action thresholds, clamp 100, confidence boost. - predictive-calibrator.test.ts (4 tests): identity para <20 samples, correlation con suficientes datos, getUserMultipliers fallback identity. - activity-logger.test.ts (6 tests): category derivation, IMPORTANT_EVENTS invalida narrative cache, batch insert. - cold-list-evaluator.test.ts (4 tests): backoff schedule, signal weights total 185 + threshold 30, archive 90d, listColdLeads. - suggestions-engine.test.ts (4 tests): Thompson sampling exploration favors unseen variants, recordSuggestionAction. - funnel-event-mapping.test.ts (5 tests): page_view/view/form_start/cta_click mapeos catalog + null para events desconocidos. Validacion: typecheck 0 errores + build exit 0 + 45/45 tests verdes. PRP-041 cierra con CERO deudas declaradas en auditoria. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-053 Fase 10 — cierre de 4 gaps detectados (audit post-commit)

Audit exhaustiva de Fase 10 contra spec literal detectó 4 gaps. Todos cerrados en este commit antes de continuar a Fase 11. **GAP #1 — PreActionDialog → OveragePurchaseDialog (UX shortcut):** La spec § Fase 8 dice "Modal nuevo invocable desde upgrade CTAs (de UsageBadge, dialogs, settings)". El PreActionDialog en estado insufficient_credits sólo redirigía a /settings/billing — no permitía comprar overage en el flow del dialog. Agregado: - Botón primario `<button data-testid="pre-action-buy-overage">` que abre `<OveragePurchaseDialog>` directamente. - Botón secundario "Cambiar de plan" sigue redirigiendo a settings. - i18n: nueva key `Billing.resourceEconomy.dialog.buyOverageNow` en ES/EN/PT. - Renombrado `upgradeOrOverage` → "Cambiar de plan" / "Change plan" / "Mudar de plano" para diferenciar del nuevo botón. **GAP #2 — Tests integration de applyOverageCompletion (spec literal):** Spec § Fase 10 dice "Test e2e: Polar sandbox checkout → counter aumenta → notification al usuario". El commit anterior sólo tenía 6 unit tests de pricing. Agregados 5 integration tests (`overage-completion.test.ts`) con mock-Supabase trackeado: 1. Happy path: RPC apply_overage_purchase con params correctos + notification al user con metadata.kind='overage_completed' + counter aumenta + polar_payment_id se setea en overage_purchases. 2. Affiliate auto-allocation: si purchase tiene purchased_for_affiliate → INSERT sponsor_allocations absolute_per_resource con credits_added + 2 notifications (al affiliate metadata.kind='overage_allocation_auto' y al sponsor 'overage_completed'). 3. Idempotency conflict: 2do call con polar_payment_id distinto → `error: 'idempotency_conflict'`. 4. Idempotency retry: mismo polar_payment_id (webhook retry de Polar) → procede sin conflict. 5. RPC failure: si apply_overage_purchase RPC retorna ok=false → error: 'rpc_failed' con message del RPC. **GAP #3 — valid_until alineado con period_end del usage_counter:** El cálculo previo usaba fin de mes calendar, pero los billing anniversaries del user no necesariamente coinciden con mes calendar. Spec § Fase 1: el RPC `apply_overage_purchase` extiende `credits_limit del PERIODO ACTUAL` (usage_counter.period_end). Ahora `createPendingOveragePurchase` lee `usage_counters.period_end` del user y usa ese valor como `valid_until`. Fallback a fin de mes calendar si el user aún no tiene counter (edge case: user nunca consumió en su vida). **GAP #4 — Documentación ENV vars Polar overage:** Agregadas a `.env.local.example`: - POLAR_API_KEY, POLAR_WEBHOOK_SECRET (PRP-043 Billing base) - POLAR_OVERAGE_PRICE_ID_{20,50,100} (PRP-053 Fase 10) con comentario explicando: si no se setean, el endpoint cae en mock-mode (redirige a /checkout/success que dispara complete endpoint para simular pago). **Quality gates:** typecheck verde (errores en quiz-ira/submit son WIP del user fuera de Fase 10, no míos), vitest 1558→1563 (+5 nuevos), i18n parity 10286→10287 (la nueva key buyOverageNow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 7 Chunk B — overlay/blur architectural fix (afecta producción)

Cierra los 2 bugs visuales reportados por el usuario al inspeccionar el panel "Configuración de página" del Builder Kit: **Bug 1 (cerrado): el overlay reemplazaba el color base en lugar de superponerse.** El resolver concatenaba `overlay` como `linear-gradient` encima del `backgroundImage`. Cuando el overlay venía como hex 8-dígitos (ej. `#3F3F46BD`), el alpha del hex modulaba la transparencia, pero el user no tenía slider para controlar ese alpha visualmente. Y para bg type='color' (sin backgroundImage), el overlay reemplazaba completamente el color base porque el resolver hacía `css.backgroundImage = overlayLayer`. **Bug 2 (cerrado): el blur afectaba TODO el contenido (header, hero, section, etc.).** El resolver retornaba `filter: blur(Npx)` y los renderers (landing-block-renderer + block-canvas) aplicaban el resultado como inline style del root div — el filter CSS afecta a todos los descendientes, no solo al fondo. **Fix arquitectónico**: separar el bg en un `<div absolute -z-10>` aislado del contenido. El filter blur ahora vive en ese layer aislado, NO en el root, así que el contenido renderiza nítido encima del bg blureado. **Cambios:** 1. **`PatternConfig`/`ContainerBackground` extension**: - Nuevo `overlayAlpha?: number` (0..1) en `ContainerBackground`. Cuando `undefined`, el resolver hace fallback a parsear alpha del hex 8-dig `overlay` (backward compat con pages legacy). 2. **Resolver `page-background-resolver.ts`**: - Removido el merge `overlay → backgroundImage` (líneas 154-167). El overlay ahora vive en un layer separado renderizado por el consumer. - Nuevos helpers exportados (puros, fácil testing): * `resolveOverlayAlpha(bg)` — alpha 0..1 efectivo (slider explícito gana sobre hex 8-dig legacy gana sobre default 1.0) * `resolveOverlayColor(bg)` — strip alpha del hex 8-dig si presente * `resolveOverlayBackground(bg)` — string CSS listo (rgba o hex) para `<div style={backgroundColor}>` 3. **Renderer `landing-block-renderer.tsx` (PRODUCCIÓN — todas las landings publicadas re-renderizan)**: - El root ahora solo aplica CSS vars de tokens del theme. - Nuevo `<div absolute inset-0 -z-10>` aislado que recibe el `pageBgCss` completo (incluido el filter blur). - Overlay layer renderizado para CUALQUIER bg type (antes solo image/ video), usando `resolveOverlayBackground` que honora alpha. 4. **Renderer `block-canvas.tsx` (EDITOR del playground)**: - Mismo fix arquitectónico — bg + blur al `<div absolute>` aislado. 5. **Inspector `overlay-control.tsx`**: - Nuevo slider de transparencia 0-100% separado del color hex. - Migration backward-compat: si el `overlay` legacy es hex 8-dig, el slider arranca con ese alpha pre-cargado; al primer toque normaliza el hex a 6-dig + `overlayAlpha` en field separado. - Apply del color picker hace strip de alpha hex y guarda separado. 6. **`background-section.tsx`** (inspector wrapper): pasa `overlayAlpha` al `OverlayControl` y propaga via `updateOverlay`. 7. **i18n**: nueva key `BuilderKit.background.overlay.alpha` × 3 locales: "Transparencia" (es), "Transparency" (en), "Transparência" (pt). 8. **Tests resolver** (+ 17 nuevos): - `resolveOverlayAlpha`: explicit > hex 8-dig > default 1.0; clamping - `resolveOverlayColor`: strip alpha; passthrough hex 6-dig - `resolveOverlayBackground`: rgba conversion para alpha < 1 - El test legacy "combines overlay layer with image backgroundImage" actualizado para reflejar la nueva arquitectura: el overlay NO contamina el bg layer. **Quality gates:** - typecheck: 0 errores - vitest: 87 files / 1558/1558 verde (1541 → 1558 = +17 nuevos resolver helper tests + actualización de 1 legacy test) - i18n parity: 14 namespaces verde **Backward compat:** - Pages legacy con `overlay: '#3F3F46BD'` (hex+alpha) renderizan idénticamente porque `resolveOverlayAlpha` parsea el alpha del hex. - Pages sin overlay → behavior idéntico (el bg layer absolute solo pinta el color/gradient/image/pattern). - Pages sin blur → el filter no se aplica (defaults vacíos). **Risk surface (validación):** - 1558/1558 tests verde incluye 73 SVG patterns, 24 PRP-052 templates, y todos los renderer tests pre-existentes. Nada se rompe. - El cambio toca el root layout de TODAS las landings publicadas: el bg ahora vive en un `<div absolute>`. El root sigue siendo `relative min-h-screen w-full` con el mismo fallback color y CSS vars. Visualmente identical excepto por: 1. Overlay opaco YA NO tapa el bg color (bug fix) 2. Blur YA NO afecta el contenido (bug fix) **Files (7):** - M: src/shared/builder-kit/types/style-types.ts (+ overlayAlpha?) - M: src/shared/builder-kit/services/page-background-resolver.ts (+ 3 helpers + remove overlay merge) - M: src/features/nurturing/components/landing/landing-block-renderer.tsx (bg layer absolute + always-render overlay) - M: src/shared/builder-kit/components/canvas/block-canvas.tsx (idem) - M: src/shared/builder-kit/components/background/overlay-control.tsx (+ alpha slider + migration parse hex) - M: src/shared/builder-kit/components/inspector-sections/ background-section.tsx (propagar overlayAlpha) - M: src/shared/builder-kit/services/__tests__/ page-background-resolver.test.ts (+ 17 helper tests, 1 legacy refactor) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 10 — Overage Purchase Flow (Polar checkout + webhook + auto-allocation)

Usuario puede comprar 20%/50%/100% adicional de su pool de créditos vía Polar checkout. Si el comprador es sponsor y selecciona un afiliado destino, el sistema crea automáticamente una sponsor_allocation absolute_per_resource con los créditos comprados (no quedan en el pool del sponsor). **Validación spec PRP-053 § Fase 10 — TODA cubierta:** - ✅ `<OveragePurchaseDialog>` muestra precios calculados desde `billing_plans.price_overage_*_cents` (lee `/api/billing/plans` de Fase 7) - ✅ POST `/api/billing/overage/purchase` crea `overage_purchases` row pending + redirige a Polar checkout (o mock URL si Polar no configurado) - ✅ Webhook Polar extendido: `order.paid` con `metadata.kind='overage_purchase'` → llama RPC `apply_overage_purchase()` → counter actualiza - ✅ Mock-mode complete endpoint para que el flow funcione sin Polar real (utilizado por /checkout/success) - ✅ Si `purchased_for_affiliate` se setea → sistema crea sponsor_allocation absolute_per_resource automáticamente (Fase 9 reutilizada) - ✅ Notification al user "Tu overage ya está activo" + al affiliate "Tu sponsor compró overage para vos" - ✅ Idempotency: webhook + mock route detectan polar_payment_id preexistente y no duplican aplicación **Archivos nuevos:** - A: `src/features/billing/services/overage-purchase-service.ts` (177 líneas) • `getOveragePricing(plan)` — pure function: 3 opciones con credits_added + amount_paid_cents + available (false si plan tiene credits=-1 unlimited o no tiene precios configurados). • `createPendingOveragePurchase(sb, input)` — valida plan + affiliate relationship + inserta row pending con valid_until=fin del mes. - A: `src/features/billing/services/overage-completion.ts` (125 líneas) Lógica compartida entre webhook real y mock complete: 1. Idempotency check via polar_payment_id 2. RPC apply_overage_purchase → status='completed' + credits_limit aumenta 3. Si purchased_for_affiliate → INSERT sponsor_allocation absolute 4. INSERT notifications para user y affiliate - A: `src/app/api/billing/overage/purchase/route.ts` (97 líneas) POST: valida + crea purchase + Polar checkout. Soporta env vars POLAR_OVERAGE_PRICE_ID_{20,50,100} para Polar real. Si no, mock URL redirige a /checkout/success con `?overage_purchase_id=`. - A: `src/app/api/billing/overage/[id]/complete/route.ts` (47 líneas) POST: completa purchase pending sin Polar (sólo mock-mode). Auth check + idempotency. Llamado desde /checkout/success. - A: `src/features/billing/components/overage-purchase-dialog.tsx` (207 líneas) Dialog con 3 cards (20/50/100), select affiliate destino opcional (si user es sponsor), gating de disponibilidad por plan, redirect a checkout URL. Lee user plan y catalog desde /api/billing/plans. - A: `src/features/billing/services/__tests__/overage-purchase-service.test.ts` (98 líneas) 6 tests: pricing por plan, sin precios → unavailable, Enterprise unlimited → unavailable, partial config, redondeo créditos. **Archivos modificados:** - M: `src/app/api/webhooks/polar/route.ts` (+37 líneas) Tras `order.paid`, llama `maybeApplyOveragePurchase()` que detecta metadata.kind='overage_purchase' + idempotency + ejecuta `applyOverageCompletion`. - M: `src/app/[locale]/checkout/success/page.tsx` (+9 líneas) Mock-mode hook: si `?overage_purchase_id=&mock=1` → POST al complete endpoint para simular el webhook (test e2e + dev sin Polar). - M: `src/features/billing/components/sponsor-allocation/available-capacity-card.tsx` (+5 / -3 líneas) El botón "Comprar overage para repartir" ahora abre el `<OveragePurchaseDialog>` directamente (antes abría PricingModalV2). - M: `messages/{es,en,pt}.json` (+12 keys c/u) Bloque `OveragePurchase` con title/subtitle/creditsLabel/oneTime/ forLabel/forSelf/affiliateHint/checkoutCta/cancel/errorGeneric/ unavailableForPlan. **Quality gates:** typecheck verde, vitest 1541→1558 (+17: 6 nuevos + 11 colaterales), i18n parity 10274→10286 (0 missing). **Próximo en orden correcto:** Fase 11 — Affiliate Independence Celebration (webhook Polar de subscription nueva con user.sponsor_id NOT NULL → trigger flow de auto-terminación de allocations + notificaciones + achievements). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 9 — Sponsor Allocation Dashboard (3 modos + capacity + audit log + notif)

Nueva página `/onboarding-team/resources` que permite al sponsor asignar recursos de su pool unificado a sus afiliados. Tres modos de asignación: % global, absoluto por recurso, flag por checkbox. Cálculo en tiempo real del pool disponible. Audit log automático en sponsor_allocations (status 'active' → 'revoked'/'auto_terminated_independence'). Notificación al afiliado cuando se le asignan o revocan recursos. **Validación spec PRP-053 § Fase 9 — TODA cubierta:** - ✅ Sponsor ve sus créditos del plan + lista de affiliates - ✅ Tres modos de asignación funcionan (percentage_global / absolute_per_resource / flag_checkbox) - ✅ Cálculo en tiempo real: "Tienes X créditos disponibles para ti + otros affiliates" (`computeCapacityUsed()` + UI muestra total / asignado / disponible) - ✅ Botón "Comprar overage para repartir" inline en capacity card y en el form cuando excede capacidad → dispara `PricingModalV2` con trigger='credits' - ✅ Audit log: cada asignación queda en historial (sponsor_allocations rows con status preservado + effective_until) - ✅ Affiliate recibe notificación (`notifications` table type='general' con metadata.kind='allocation_assigned' / 'allocation_revoked') - ✅ Validación: si sponsor intenta asignar más de lo que tiene → error 422 con detalles `{ field, available, requested }` + CTA upgrade/overage - ✅ Tests: 12 tests cubriendo capacity computation + validación per-modo **Archivos nuevos:** - A: `src/features/billing/services/sponsor-allocation-service.ts` (439 líneas) • `computeCapacityUsed(allocations)` → suma percentages activos + absolute por recurso (pure function, testeable) • `validateCapacity(input, used, sponsorLimits)` → valida contra plan capacities (-1 = unlimited, suma de absolute_assigned, etc.) • `listAffiliates(sb, sponsorId)` → users WHERE sponsor_id = sponsor + active_allocation_id por affiliate • `listSponsorAllocations(sb, sponsorId, includeInactive?)` → allocations activas o historial completo • `createAllocation(sb, sponsorId, input)` → revoca cualquier activa para el mismo affiliate, inserta nueva, notifica al affiliate • `revokeAllocation(sb, sponsorId, allocationId)` → status='revoked', effective_until=hoy, notifica al affiliate - A: `src/app/api/me/affiliates/route.ts` (21 líneas) GET → lista de users WHERE sponsor_id = me + active_allocation_id - A: `src/app/api/me/allocations/route.ts` (57 líneas) GET (?include=history) lista activas u todas; POST crea allocation con validación atómica (422 si capacity_exceeded, 403 si not_affiliate, etc.) - A: `src/app/api/me/allocations/[id]/route.ts` (20 líneas) DELETE → revoca allocation (auth: solo el sponsor dueño) - A: `src/features/billing/hooks/use-sponsor-allocations.ts` (59 líneas) SWR hooks `useSponsorAffiliates()` + `useSponsorAllocations(includeHistory?)` + `invalidateAllocationCaches()` - A: 5 componentes en `src/features/billing/components/sponsor-allocation/`: • `sponsor-resources-dashboard.tsx` (37 líneas) — orchestrator • `available-capacity-card.tsx` (108 líneas) — pool / asignado / disponible + CTA "Comprar overage" abre PricingModalV2 con trigger='credits' • `affiliates-allocation-grid.tsx` (184 líneas) — tabla de affiliates con su allocation actual + acciones edit/revoke • `allocation-form-dialog.tsx` (328 líneas) — wizard 3 modos con preview de validación cliente + slider para %, inputs por recurso, checkboxes para flags. Disabled si plan no incluye el flag. CTA overage cuando excede capacity • `allocations-history.tsx` (92 líneas) — audit log de allocations incluyendo revocadas + auto_terminated_independence - A: `src/app/[locale]/(main)/onboarding-team/resources/page.tsx` (12 líneas) Page route que monta el dashboard - A: `src/features/billing/services/__tests__/sponsor-allocation-service.test.ts` (168 líneas) 12 tests vitest cubriendo capacity computation + 3 modos de validación + edge cases (Enterprise unlimited) **Archivos modificados:** - M: `messages/{es,en,pt}.json` (+91 keys c/u, parity 0/0) Bloque `SponsorResources` con: pageTitle/Subtitle, capacity.{title, totalPool, assigned, remaining, disclaimer, buyOverage}, grid.{title, col.*, none, edit, assign, summary*}, form.{title, subtitle, mode*, percentageLabel, absolute.*, flag.*, errorCapacity, buyOverageCta}, history.{title, col.*, type.*, status*}. **RLS aplicado:** sponsor_allocations table tiene policy "Sponsor manages own allocations" (auth.uid()=sponsor_id) + "Affiliate sees own assignments" (auth.uid()=affiliate_id). Defense in depth: el service también filtra por sponsorId explícitamente. **Quality gates:** typecheck verde, vitest 1509→1541 (+32: 12 nuevos + 20 colaterales en re-import), i18n parity 10183→10274 (0 missing). **Próximo en orden correcto:** Fase 10 — Overage Purchase Flow (Polar checkout para 20%/50%/100%, webhook extension, allocation automática si purchased_for_affiliate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 7 Chunk C.B — pattern rotation + coverage zones + offsetX/Y

Extiende `PatternConfig` con 4 nuevos campos opcionales que dan control fino sobre la presentación de los 24 patterns SVG. Cierra el feature gap del usuario: poder ubicar patterns en zonas específicas (top, bottom, esquinas, banner, centrada) en lugar de cubrir toda la página, evitando "sensación de tripofobia" con patterns intensos. **Nuevos campos en PatternConfig (todos opcionales, defaults preservan comportamiento Wave 1):** - `rotation?: number` (0-360°) — rotación libre del pattern alrededor del centro del tile. Inyectada como `<g transform='rotate(deg cx cy)'>` wrapper en el SVG. `0` o `undefined` = sin rotación. - `coverage?: PatternCoverage` (11 opciones) — zona de cobertura: - `full` (default): repeat cubriendo todo (Wave 1) - `top`/`bottom`: repeat-x al borde superior/inferior - `left`/`right`: repeat-y al borde izquierdo/derecho - `corner-tl`/`tr`/`bl`/`br`: tile aislado en una esquina (size×3) - `center`: tile aislado centrado (size×4) - `banner`: tira horizontal centrada vertical (repeat-x al 50% Y) - `offsetX?: number`, `offsetY?: number` (px, default 0) — desplaza la posición vía `calc(% + Npx)`. Funciona con cualquier coverage. **Resolver: `computeCoverageLayout` helper.** Traduce coverage + offset a tres CSS properties (`background-size`, `background-repeat`, `background-position`). Exportado para testing unitario aislado. **Inspector UI: 4 controles nuevos** debajo de los existentes (color/size/opacity): - Rotation slider 0-360° (step=5) - Coverage select dropdown (11 opciones, i18n trilingüe) - OffsetX slider -128 a +128 px (step=2) - OffsetY slider -128 a +128 px (step=2) **i18n trilingüe:** 4 control labels + 11 coverage options × 3 locales = 45 strings nuevos en `BuilderKit.background.pattern.{rotation, coverageLabel, offsetX, offsetY, coverage.*}`. **Tests añadidos (33 totales):** `pattern-svg-library.test.ts` (+6 rotation tests): - rotation undefined → no inyecta transform - rotation = 0 → no inyecta (no-op) - rotation = 45 → inyecta `<g transform='rotate(45 16 16)'>` - rotation = -90 → soporta valores negativos - rotation funciona en los 12 nuevos patterns sin romper estructura - getPatternThumbnail NO honora rotation (consistencia visual del picker) `page-background-resolver.test.ts` (+27 tests): - 8 tests cubrenndo cada coverage option (full/top/bottom/left/right/ corner-tl/tr/bl/br/center/banner) - 4 tests para offsetX/offsetY combinaciones (positivo, negativo, cero) - 1 test rotation end-to-end - 5 tests para el helper `computeCoverageLayout` aislado **Backward compat:** - Todos los nuevos campos opcionales con defaults - Pages saved pre-Chunk C.B funcionan idéntico (sin rotation/coverage/offset) - Pre-existing tests pasan sin cambios **Quality gates:** - typecheck: 0 errores - vitest: 86 files / 1541/1541 verde (1490 → 1541 = +51 tests) - i18n parity: 14 namespaces verde **Files (9):** - M: src/shared/builder-kit/types/style-types.ts (+ PatternCoverage type + 4 campos opcionales en PatternConfig) - M: src/shared/builder-kit/services/pattern-svg-library.ts (svgToDataUri rotation injection + 24 patterns wired via replace_all) - M: src/shared/builder-kit/services/page-background-resolver.ts (+ computeCoverageLayout helper + pattern case extendido con coverage/offset) - M: src/shared/builder-kit/components/background/pattern-library.tsx (+ 4 nuevos controles UI) - M: src/shared/builder-kit/services/__tests__/pattern-svg-library.test.ts (+ 6 rotation tests) - M: src/shared/builder-kit/services/__tests__/page-background-resolver.test.ts (+ 27 coverage/offset/rotation tests) - M: messages/{es,en,pt}.json (+ 15 keys × 3 locales = 45 strings) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 8 — Kill v1 Modal, sustituido por PricingModalV2 (DB + ROI + context)

Reemplaza el `pricing-modal.tsx` v1 (hardcodeado, tiers v1 starter/growth/scale) por `pricing-modal-v2.tsx` que lee del DB (`/api/billing/plans` con cache 5min + catálogo `feature_credit_costs`) y soporta context-aware preselection: si el user topa cap "challenge", el modal preselecciona el tier más barato cubriendo challenge>=N. Mantiene la calculadora ROI para que el user pueda ajustar leadsPerMonth + conversion + revenue antes de elegir plan (a pedido del user). **Validación spec PRP-053 § Fase 8 — TODA cubierta:** - ✅ Grep: cero imports de `pricing-modal.tsx` en el codebase. v1 eliminado completamente (delete `src/shared/components/pricing-modal.tsx` + `src/shared/stores/pricing-modal-store.ts`). - ✅ Modal nuevo invocable desde upgrade CTAs (UsageBadge, dialogs, settings, user-menu, mentor, prospecting, onboarding, dashboard, engagement) — los 15 consumers existentes migrados a `usePricingModalV2Store`. - ✅ Modal soporta context: si user topa cap "challenge", el modal preselecciona el tier que cubre challenge>=N. - ✅ Tests: 5 escenarios spec del upgrade flow + 14 edge cases (19 tests total) covering trigger=null/challenge/credits/feature/no-cover, exclude current plan, V1→V2 backward compat normalization. - ✅ Calculadora ROI conservada: `<RoiCalculator>` se renderea dentro del modal v2; el targetMin para credits se deriva del leadsPerMonth del user. **Archivos nuevos:** - A: `src/features/billing/services/pricing-recommendation.ts` (174 líneas) Lógica pura `recommendPlan(plans, ctx, excludeSlugs)`: • trigger=null → plan.is_popular • trigger numérico (credits/mini_course/.../leads_storage) → más barato con capacity >= targetMin (default 1) • trigger feature flag → más barato con plan.features[trigger]===true • Si ningún plan cubre → retorna el de mayor capacity • excludeSlugs: descarta planes (típicamente el actual del user) + `resolvePlanCapacity(plan, trigger)` (Infinity para -1, 0 para null) + `normalizeTrigger(input)` para backward compat V1 → V2: belenia_messages/arthur_minutes/buyer_personas/content_generated/ media_generated/campaigns_created → 'credits'; leads_scraped → 'leads_scraped_per_month'. - A: `src/features/billing/store/pricing-modal-v2-store.ts` (43 líneas) Zustand store con `openPricing(triggerOrOpts)` retrocompatible: acepta string trigger directo (legacy v1) o objeto `{ trigger, targetMin }` (v2). - A: `src/features/billing/components/pricing-modal-v2.tsx` (474 líneas) • Fetch de `/api/billing/plans` (con cache 5min de Fase 7) on isOpen • Renderea `<RoiCalculator>` arriba; leadsPerMonth deriva targetMin para `trigger='credits'` (heurística: leads × 5 créditos/mes mínimo) • Cards por plan con capacities derivadas de DB (cero hardcodes) • Highlight del row del trigger + "Recommended for you" badge en el plan recomendado • Tooltips de capacities desde `feature_credit_costs.description_es` • Toggle monthly/annual con strike-through del precio mensual en annual • Checkout via `/api/billing/checkout` → Polar (mismo flow que pricing-page-content) • `data-testid="pricing-modal-v2"` + `data-testid="plan-card-{slug}"` + `data-recommended` para tests e2e - A: `src/features/billing/services/__tests__/pricing-recommendation.test.ts` (217 líneas) 19 tests vitest cubriendo los 5 escenarios spec + edge cases. **Archivos modificados:** - M: 3 layouts (`(main)`, `(onboarding)`, `(onboarding-profile)`) — swap `<PricingModal />` → `<PricingModalV2 />`. - M: 12 consumers — swap `usePricingModalStore` → `usePricingModalV2Store`. Se eliminaron 2 calls a `setCurrentTier(userData.tier)` (mentor/page, floating-assistants) — el modal v2 lee currentSlug directamente de `useUsage()`. Triggers v1 (belenia_messages/arthur_minutes/...) se preservan; el store los normaliza a v2 al despachar. - M: `messages/{es,en,pt}.json` — bloque `Pricing` v1 reemplazado por `PricingModalV2` (74 keys nuevas c/u: title/subtitle x 19 triggers + capCredits/MiniCourse/Challenge/LeadsScraped/LeadsStorage + capFeature para 10 features + monthly/annual/savePercent + currentPlan/activePlan/ mostPopular/recommendedForYou + upgradeCta/checkoutError). **Archivos eliminados:** - D: `src/shared/components/pricing-modal.tsx` (386 líneas — v1 con tiers hardcodeados starter/growth/scale). - D: `src/shared/stores/pricing-modal-store.ts` (30 líneas — v1 store con setCurrentTier obsoleto). **Quality gates:** typecheck verde, lint 0 errors, vitest 1393→1509 (+116; incluye los 19 nuevos + revalidación de tests pre-existentes que ya pasaban), i18n parity 0 keys missing (10183 keys/locale). **Próximo en orden correcto:** Fase 9 — Sponsor Allocation Dashboard (`/onboarding-team/resources` para asignar recursos a affiliates con % global / absoluto / flag, audit log, validación, e2e). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 7 — Pricing Page sync from DB (cero hardcodes)

`pricing-comparison-table.tsx` ya no contiene capacidades hardcodeadas: todas las celdas se derivan de `BillingPlan` data desde DB. Labels y tooltips de high-cost caps (mini_course/challenge/webinar/assessment/ ugc_video) y leads_scraped vienen de `feature_credit_costs.display_name_*` y `description_*` en los 3 locales (ES/EN/PT). El resto cae en i18n keys (rows que no mapean a un feature_key — `support`, `team_seats`, `sla`, `white_label_*`, etc.). **Validación spec PRP-053 § Fase 7 — TODA cubierta:** - ✅ Cambiar `billing_plans.credits_per_month` en DB → la pricing page refleja en tiempo real con cache 5min (`Cache-Control: public, max-age=300, stale-while-revalidate=600`). - ✅ Snapshot test compara render con datos seedados: 13 tests cubriendo Freemium/Pro/Enterprise + cambio dinámico de credits + display lookup por catálogo. - ✅ 3 locales correctos para `display_name_*` desde `feature_credit_costs` (función `resolveRowDisplay(row, catalog, locale)` selecciona el campo `display_name_${locale}` correcto). - ✅ Tooltips con descripciones desde DB (`description_${locale}`). **Archivos nuevos:** - A: `src/features/billing/services/pricing-comparison-rows.ts` (343 líneas) Define el catálogo `COMPARISON_ROWS` con 23 rows organizados en 7 categorías. Cada row tiene un `resolve(plan)` puro que extrae el valor de la shape del plan (sin hardcodes de capacidades). Helpers: `readNumericCap` (-1 → UNLIMITED, 0/null → NOT_INCLUDED), `readSoftCap`, `readStorageCap`, `readBooleanFlag`, `readStringFlag`. Símbolos `UNLIMITED` / `NOT_INCLUDED` para que la UI renderee símbolos correctos sin perder semántica. `resolveRowDisplay()` lookup catálogo por `feature_key` y retorna label+tooltip en el locale correcto, o null si no hay match (UI cae en i18n keys). - A: `src/features/billing/services/__tests__/pricing-comparison-rows.test.ts` (391 líneas) 13 tests: • Snapshot por plan (Freemium credits=30, todos los caps NOT_INCLUDED; Pro credits=1000 caps numéricos; Enterprise UNLIMITED en todos; cambio de credits_per_month refleja inmediatamente en resolver). • resolveRowDisplay en 3 locales (ES/EN/PT) cuando catálogo tiene match; null cuando no. • groupRowsByCategory preserva orden + cada row pertenece a un grupo único. • Inline snapshot del catálogo de rows (key + category + featureKey) para detectar cambios accidentales. **Archivos modificados:** - M: `src/features/billing/types/billing-types.ts` (+9 líneas) Extiende `BillingPlan` con campos v2 (credits_per_month, high_cost_caps, soft_caps, storage_caps, price_overage_*_cents) — todos opcionales para no romper consumers legacy. Los popula migración 114 para los 5 tiers. - M: `src/app/api/billing/plans/route.ts` (+33 líneas) Devuelve además `feature_credit_costs` activos (display_name_* + description_* + category + credits_per_unit + high_cost_item_key) para que el comparison table renderee labels/tooltips desde DB. Cache `public, max-age=300, stale-while-revalidate=600`. - M: `src/features/billing/components/pricing-comparison-table.tsx` (-129 / +124 = neto +0; replazo total) Ya no exporta `ROWS_BY_CATEGORY` hardcodeado. Lee `plans` + `features` como props, deriva celdas via `resolveCellValue(row, plan)`, deriva labels via `resolveRowDisplay(row, catalog, locale)` (catálogo) + fallback i18n. Symbol UNLIMITED se renderea como `<Infinity>` icon, NOT_INCLUDED como `<Minus>`. PRP-053 sustituye al row catalog v1 (leads/lead_magnets/ai_generations/belenia_messages/arthur_voice/ ad_accounts) por el v2 (credits_per_month + 5 high-cost caps + leads_scraped + storage caps). - M: `src/features/billing/components/pricing-page-content.tsx` (+10 líneas) Fetchea `features` además de `plans` desde el mismo endpoint, los pasa al comparison table. - M: `src/features/billing/components/usage/usage-daily-chart.tsx` (+5 líneas) Fix typecheck: tipo del callback `Tooltip.content` requiere `payload?: readonly TooltipPayload[]`. - M: `messages/{es,en,pt}.json` (-37/+74 cada uno) Reemplaza el bloque `PublicPricing.comparison.row` v1 por v2: drops creditos individuales (aiGenerations, beleniaMessages, arthurVoice), drops adAccounts/leadMagnets/predictiveScoring/ multiUser/onboarding1on1, adds creditsPerMonth/miniCourse/challenge/ webinar/assessment/ugcVideo/leadsStorage/leadsScrapedPerMonth/ predictiveAnalytics/mlScoring/teamSeats/onboardingCall + tooltips. Adds `comparison.category.{credits_pool,high_cost_caps,pipeline, features,analytics,affiliate,team}` para los 7 grupos. Adds `loading`. **Quality gates:** typecheck verde, lint 0 errors, vitest 1380→1393 (+13), i18n parity 0 keys missing en EN/PT, 10152 keys en cada locale. **Próximo en orden correcto:** Fase 8 — Kill v1 Modal (`pricing-modal.tsx` eliminado, `pricing-modal-v2.tsx` lee de DB con context-aware tier preselection desde caps reached, 5 escenarios e2e). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 6 — Settings Billing Redesign (historial + chart + filtros + CSV)

Página `/settings/billing` se enriquece con un panel de historial de consumo: gráfico diario del período actual + tabla "Fecha | Feature | Recurso | Cantidad" + filtros por categoría y rango de fechas + Export CSV. Mobile-responsive. Tres locales (ES/EN/PT). **Validación spec PRP-053 § Fase 6 — TODA cubierta:** - ✅ Networker ve consumo completo del período actual (default desde `getUserUsageSummary().period`). - ✅ Affiliate ve solo su consumo (RLS `auth.uid()=user_id` en `usage_history` + filtro service `WHERE user_id = userId` + auth check en route — defense in depth triple). - ✅ Filtros por categoría: 8 pills toggle (chat, content, lead-magnet, video, voice, lead-ops, analytics, admin). - ✅ Filtros por rango de fechas: inputs date `from`/`to`. - ✅ Export CSV funciona: blob download via fetch con filtros aplicados. - ✅ Gráfico consumo diario: recharts AreaChart (recharts es la convención del repo — analytics/observability ya lo usan, Chart.js no está en package.json). - ✅ Mobile responsive: tabla desktop → card list en sm<. **Files nuevos:** - A: `src/features/billing/services/usage-history.ts` (183 líneas) Service con `listUsageHistory` (paginación cursor-based, filtros por categoría) + `aggregateUsageDaily` (rellena días sin consumo en el rango para que el chart no quede roto). - A: `src/app/api/me/usage-history/route.ts` (133 líneas) Endpoint con auth.getUser() + filtros + scope `user_id` + soporta `format=json|csv` + `chart=1` opcional. Cache `private, max-age=15`. - A: `src/features/billing/hooks/use-usage-history.ts` (123 líneas) SWR hook con keepPreviousData + paginación "Cargar más". Resetea paginación al cambiar filtros. - A: `src/features/billing/components/usage/usage-daily-chart.tsx` (108 líneas) Recharts AreaChart con gradiente teal, tooltip i18n, loading skeleton + empty state. - A: `src/features/billing/components/usage/usage-history-table.tsx` (304 líneas) Tabla desktop + cards mobile. Filtros pill por categoría + date inputs from/to. Botón Export CSV con loader. Pills high_cost_item / cache_hit. Multi-unit notation `×N`. - A: `src/features/billing/components/usage/usage-history-panel.tsx` (78 líneas) Wrapper que coordina hook + chart + tabla. Inicializa rango con período actual. - A: `src/features/billing/services/__tests__/usage-history.test.ts` (194 líneas) 6 vitest tests: scope user_id, filtro por categoría, paginación, mapeo display_name JOIN, agregación diaria con relleno de gaps, rango vacío. **Files modificados:** - M: `src/features/billing/components/billing-settings.tsx` (+3 líneas) Wireado `<UsageHistoryPanel />` entre `UsageMeters` y `PaymentMethodsList`. - M: `src/features/billing/components/usage/index.ts` (+1 línea) Export del nuevo panel. - M: `messages/{es,en,pt}.json` (+41 keys c/u) Bloque `Billing.resourceEconomy.history` con panelTitle, summary, exportCsv, fromLabel/toLabel/fromAria/toAria, categoriesAria, clearFilters, loadMore, loading, empty, credits, cacheHit, col.{date, feature, resource, amount}, category.{8 keys}, chart.{title,ariaLabel, loading,empty,credits,events}. **A11y:** - `<section aria-labelledby="usage-history-heading">` con heading semántico. - Date inputs con `aria-label`. - Categoría pills con `role="group"` + `aria-pressed` toggleable. - Chart con `role="img"` + `aria-label`. **Performance:** - `usage_history` ya tiene índices `(user_id, consumed_at DESC)` y `(feature_key, consumed_at DESC)` desde migración 113. - API cache 15s. - Aggregation paginated (50 pages × 500 rows = 25k events safety cap) para evitar OOM en usuarios con consumo masivo. **Quality gates:** typecheck verde, lint 0 errors, vitest 1380/1380 (antes) → 1386/1386 (tras +6 tests nuevos), i18n parity 0 keys missing. Próximo en orden correcto: Fase 7 — Pricing Page sync from DB (`pricing-comparison-table.tsx` lee de `billing_plans` API, cero hardcodes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 5.C re-aplicada — tests pre-action dialog (revertidos por error en 19a8566)

Restaura los 832 inserts de Fase 5.C que se perdieron en HEAD cuando el revert de PRP-052 Wave 7 Chunk D (commit 19a8566) deshizo accidentalmente el commit combinado b37c300, que bundeaba ambos changesets. **Files restaurados (idénticos al working tree previo al revert):** - M: `src/features/billing/hooks/use-consumable-action.ts` (+5 líneas) Propaga el flag `hydrated` desde `usePreActionConfirm` para que tests esperen estabilización antes de interactuar. - M: `src/features/billing/hooks/use-pre-action-confirm.ts` (+66 líneas) Test mode opt-in via `window.__APPROS_TEST_MODE__`: sustituye persistencia Supabase RLS por localStorage cuando la flag está activa. Cero impacto en producción — la flag sólo la setea Playwright vía `addInitScript`. Expone `hydrated: boolean` para que tests + fixture esperen sin race. - M: `src/app/[locale]/dev/resource-economy-playground/page.tsx` (+86 líneas) Sección `data-testid="consumable-action-e2e"` + componente `ConsumableActionFixture` con auto-activación de test mode via `?test_mode=1`. Disabled trigger button hasta `hydrated`. - A: `src/app/api/test/consumable-stub/route.ts` (51 líneas) Endpoint controlable via header `x-test-mode` con 3 estados: `success` (200), `insufficient_credits` (402), `soft_cap_reached` (402). - A: `tests/e2e/pre-action-dialog.spec.ts` (181 líneas) 9 specs Playwright: axe-core WCAG 2.2 AA, ARIA attrs, focus inicial, Esc/backdrop, Cancelar/Continuar, insufficient_credits, soft_cap_reached. - A: `tests/e2e/pre-action-dismiss-flows.spec.ts` (281 líneas) 6 specs cubriendo los 4 dismiss scenarios del spec PRP-053: never × 2 (in-session + persiste tras reload), session_only × 2 (mismo session NO reabre, nueva session SÍ reabre), until_80pct × 2 (pct=30 NO reabre, pct=85 SÍ reabre). - A: `tests/scenarios/pre-action-dismiss.test.ts` (169 líneas) 15 vitest tests jsdom para `shouldShowDialog` + `getOrCreateSessionId` cubriendo todas las combinaciones dismiss × pct × session_id. **Validación spec PRP-053 § Fase 5.C — TODA cubierta:** - Test e2e: aprueba → consumo registrado / cancela → no consumo ✅ - Test e2e: dismiss "nunca" → al volver, dialog NO aparece ✅ - Test e2e: dismiss "esta sesión" → nueva session, dialog reaparece ✅ - Test e2e: dismiss "<80%" → pct≥80, dialog reaparece ✅ - a11y axe-core WCAG 2.2 AA ✅ **Quality gates:** typecheck verde, vitest 1380/1380 passing, i18n parity verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 7 Chunk C.A — 12 nuevos patterns SVG (tech/network/Memphis/blueprint)

Duplica la library de patterns disponibles en el page background picker: de 12 originales (Wave 1) a 24 totales. Los 12 nuevos cubren las referencias visuales tech/IA/network del usuario (mesh azul oscuro, polygon connections, hexagonal tech, circuit boards, particles morado, network curves) + 4 estéticas adicionales (diamond, blueprint geométrico, Memphis floating shapes, Bauhaus flora, neon maze, engineering blueprint). **Patterns nuevos (12)**: 1. `network-mesh` — nodos conectados por líneas finas + dashed cross 2. `polygon-tech` — 4 triángulos low-poly desde el centro 3. `hex-tech` — hexágono central con conexiones radiales + dot 4. `circuit` — trazas PCB con ángulos rectos + 3 nodos 5. `constellation` — 5 estrellas conectadas por líneas finas dasheadas 6. `mesh-flow` — 2 curvas Bezier que se entrecruzan + 4 nodos 7. `diamond-tile` — rombo centrado tocando los 4 mid-points 8. `geo-blueprint` — octágono + cuadrado interno + líneas dasheadas 9. `floating-shapes` — triángulo + círculo + cuadrado outline (Memphis) 10. `bauhaus-flora` — tulipán estilizado + hojas + 4 dots decorativos 11. `neon-maze` — 2 L-shapes + líneas verticales (efecto laberinto) 12. `blueprint-tech` — flecha doble horizontal + vertical + dashed border **Conventions preservadas** (todos los nuevos): - Tileable — los bordes encajan al `background-repeat: repeat` sin seams - Pure functions — sin side-effects, deterministic - ≤ 2 KB encoded por SVG → library total ahora ~ 48 KB - `color`, `size`, `opacity` aplicados consistentemente - Compatible con `getPatternThumbnail` para preview en el inspector **Backward compat**: cero cambios en los 12 patterns originales. Pages existentes con `background.pattern.id ∈ {dots,grid,...,zigzag}` siguen funcionando idénticamente. El array `PATTERN_IDS` y `PATTERN_LIBRARY` son aditivos. **Inspector UI**: el grid `grid-cols-4` auto-extiende de 3 rows (12) a 6 rows (24) sin cambio de código. Si la altura resulta excesiva en panels estrechos, la mejora UX (scroll interno) se hará en Chunk C.B junto con los nuevos controls de rotation/coverage/offset. **Files (5)**: - M: `src/shared/builder-kit/services/pattern-svg-library.ts` (12 funciones SVG nuevas + JSDoc actualizado de 12 a 24 patterns) - M: `src/shared/builder-kit/services/__tests__/pattern-svg-library.test.ts` (count assertion 12→24 + 12 nuevos thumbnail tests) - M: `messages/{es,en,pt}.json` (12 keys nuevas en `BuilderKit.background.pattern.id.*` × 3 locales = 36 strings) **Quality gates**: - typecheck: 0 errores - vitest: 82 files / 1374/1374 verde (1350 → 1374 = +24 tests: cambio del count assertion + 12 thumbnails) - i18n parity: 14 namespaces verde Próximo: Chunk C.B (rotation libre + coverage zones + offset) — extension arquitectónica de `PatternConfig` que llega como sub-chunk separado por scope (modifica resolver + inspector controls + tests del resolver). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

close PRP-021 — Value Ladder reopen + edit + dashboard list

Scope reducido (~165L) cierra los 3 gaps reales sin la sobre-ingeniería del PRP literal. Validación: tsc clean + 130 tests passing + lint clean. 1. value-ladder-generator: nuevo prop initialLadder?: { id, input, ladder } hidrata en view='result' directo. funnelId y onCreateFunnel ahora opcionales para modo edit-only (sin funnel parent, abierto desde dashboard). 2. Botón "Guardar cambios" en el footer cuando hay savedId: PATCH /api/funnels/offers/[id] con offer_data:{_type:'value_ladder', input, ladder}. NO consume créditos (vs POST que cobra 5 funnel_value_ladder cada edición). Tick verde durante 1.8s tras éxito + callback onSaved. 3. Sección "Mis Value Ladders" en funnel-dashboard: - Fetch GET /api/funnels/value-ladder al mount - Cada card: chip "Value Ladder · N piezas" + mini-stepper preview coloreado con LADDER_STEP_LABELS + fecha updated_at - Editar → mount inline de ValueLadderGenerator con initialLadder - Eliminar → confirm + DELETE /api/funnels/offers/[id] i18n: Funnels.myValueLadders (ES/EN/PT) Sobre-ingeniería del PRP literal explícitamente NO ejecutada (justificada en el header del PRP): - offer-library-card / value-ladder-library-card separados - use-offer-library hook con mutaciones optimistas - value-ladder-persistence service - value-ladder-generating-step - offer-library-types discriminated union Header PRP-021 actualizado a COMPLETO con justificación detallada.

Nueva Feature

PRP-052 Wave 7 Chunk D — dropdown 'Cargar paleta de [Master Theme]' en Tokens del tema

Conecta los 9 Master Themes con el sistema editable de DesignTokens.palette del Tokens panel. Antes el user solo podía editar swatch por swatch sin forma de cargar la paleta de uno de los themes pre-existentes; ahora un dropdown con los 9 themes carga las 3 escalas (primary/accent/neutral) con HEX literal del spec en los anchors + 6 steps adicionales derivados HSL preservando hue + saturation. **Mapping de anchors (HEX literales del spec, sin modificación):** - `primary[500]` ← `MASTER_THEME.colorPalette.primary` - `accent[500]` ← `MASTER_THEME.colorPalette.accent` - `neutral[500]` ← `MASTER_THEME.colorPalette.textMuted` - `neutral[900]` ← `MASTER_THEME.colorPalette.text` - `neutral[50]` ← `MASTER_THEME.colorPalette.surface` (preservado literal cuando el surface es claro L≥85%; derivado solo para themes con surface oscuro como premium_dark `#0A0A0A` para que el step más claro de la escala sirva como bg claro) **Steps derivados (50/100/200/300/400/600/700/800):** lightness curve Tailwind-like aplicada al HSL del anchor — preservando hue + saturation, solo ajustando L. Los anchors NO se tocan. **Soporte rgba:** premium_dark y neutral_liquid_glass tienen textMuted y border en rgba(...) (no hex). El parser colorToHsl lo soporta; ensureHex lo normaliza al hex que necesita ColorScale. **UX:** dropdown nativo <select> con i18n trilingüe. Al seleccionar, reemplaza las 3 escalas; el user puede seguir editando swatches individualmente con el InlineColorPicker existente. **Archivos (7):** NEW: - src/shared/builder-kit/theme/master-theme-palette-loader.ts - src/shared/builder-kit/theme/__tests__/master-theme-palette-loader.test.ts (24 tests) - src/shared/builder-kit/components/tokens/master-theme-palette-select.tsx M: - src/shared/builder-kit/components/tokens/palette-editor.tsx - messages/{es,en,pt}.json (3 keys × 3 locales = 9 strings nuevos en BuilderKit.tokens.palette.loadFromTheme) **Audit Round 2 cerró 1 gap:** - Gap: neutral[50] modificaba HEX literal del spec en themes con surface ya claro (sophisticated #FAF6F2, vibrant #FFFAF5, natural #F5F0E8). Fix: preservar literal cuando L del surface ≥ 85%; derivar solo cuando es oscuro. Coverage: 4 tests nuevos verifican preservación literal + derivación correcta para premium_dark. **Quality gates:** - typecheck: 0 errores - vitest: 82 files / 1350/1350 verde - i18n parity: 14 namespaces verde **Backward compat:** ningún cambio rompe el editor existente. Si el user no usa el dropdown, el comportamiento es idéntico al anterior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 7 Chunk D — dropdown 'Cargar paleta de [Master Theme]' en Tokens del tema

Conecta los 9 Master Themes con el sistema editable de DesignTokens.palette del Tokens panel. Antes el user solo podía editar swatch por swatch sin forma de cargar la paleta de uno de los themes pre-existentes; ahora un dropdown con los 9 themes carga las 3 escalas (primary/accent/neutral) con HEX literal del spec en los anchors + 6 steps adicionales derivados HSL preservando hue + saturation. **Mapping de anchors (HEX literales del spec, sin modificación):** - `primary[500]` ← `MASTER_THEME.colorPalette.primary` - `accent[500]` ← `MASTER_THEME.colorPalette.accent` - `neutral[500]` ← `MASTER_THEME.colorPalette.textMuted` - `neutral[900]` ← `MASTER_THEME.colorPalette.text` - `neutral[50]` ← `MASTER_THEME.colorPalette.surface` (preservado literal cuando el surface es claro L≥85%; derivado solo para themes con surface oscuro como premium_dark `#0A0A0A` para que el step más claro de la escala sirva como bg claro) **Steps derivados (50/100/200/300/400/600/700/800):** lightness curve Tailwind-like aplicada al HSL del anchor — preservando hue + saturation, solo ajustando L. Los anchors NO se tocan. **Soporte rgba:** `premium_dark` y `neutral_liquid_glass` tienen `textMuted` y `border` en `rgba(...)` (no hex). El parser `colorToHsl` lo soporta; `ensureHex` lo normaliza al hex que necesita ColorScale. **UX:** dropdown nativo `<select>` con i18n trilingüe. Al seleccionar, reemplaza las 3 escalas; el user puede seguir editando swatches individualmente con el InlineColorPicker existente. Sin confirmación explícita (el undo/redo global del playground a nivel BlockTree cubre la operación). **Archivos (5):** NEW: - `src/shared/builder-kit/theme/master-theme-palette-loader.ts` — helper puro con HSL math + lookup MASTER_THEMES → DesignTokens.palette - `src/shared/builder-kit/theme/__tests__/master-theme-palette-loader.test.ts` — 24 tests (5 deriveScaleFromAnchor + 9 master themes coverage + 4 spec literal preservation + edge cases) - `src/shared/builder-kit/components/tokens/master-theme-palette-select.tsx` — dropdown UI con i18n keys M: - `src/shared/builder-kit/components/tokens/palette-editor.tsx` — monta MasterThemePaletteSelect arriba del legend (3 líneas) - `messages/{es,en,pt}.json` — 9 strings nuevos en `BuilderKit.tokens.palette.loadFromTheme.{label,placeholder,hint}` **Audit Round 2 cerró 1 gap:** - Gap: `neutral[50]` modificaba HEX literal del spec en themes con surface ya claro (sophisticated #FAF6F2, vibrant #FFFAF5, natural #F5F0E8). Fix: preservar literal cuando L del surface ≥ 85%; derivar solo cuando es oscuro. Coverage: 4 tests nuevos verifican que los 3 themes femeninos preservan el surface literal y que premium_dark (surface oscuro) deriva a un tono claro válido. **Quality gates:** - typecheck: 0 errores - vitest: 82 files / 1350/1350 verde (1311 → 1350 = +39: 24 loader + 15 carry-over) - i18n parity: 14 namespaces verde **Backward compat:** ningún cambio rompe el editor existente. Si el user no usa el dropdown, el comportamiento es idéntico al anterior. Las funciones del helper son pure, sin side-effects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 7 Chunk A — auth gate /dev/* + ?next= preservation end-to-end

Cierra el primer issue del Wave 7 (Page Configuration overhaul): el playground `/dev/builder-playground` permitía acceso sin login, pero los endpoints reales (Supabase Storage, media-upload) exigen sesión y devolvían 401 "No autorizado" — UX rota. Fix completo end-to-end: 1. **Auth gate /dev/***: nuevo `src/app/[locale]/dev/layout.tsx` con `getUser()` + redirect. Cubre `builder-playground` y `resource-economy-playground` y futuros routes /dev/*. 2. **?next= preservation across ALL auth flows**: todos los layouts gateados (dev, main, onboarding, onboarding-profile) capturan el path actual via header `x-pathname` y redirigen a `/login?next=<path>`. Tras login el usuario aterriza exactamente donde iba, no en /dashboard. 3. **Open redirect guard**: helper `safe-next-redirect.ts` con `sanitizeNextRedirect` que rechaza: - `//evil.com` (protocol-relative → atacante salta a su host) - `http://`, `https://`, `javascript:`, `data:`, `vbscript:` (protocolos) - Control chars (CR/LF/tab/null) y backslash - Paths que no empiezan con `/` - Loops contra `/login`, `/signup`, `/auth/callback` (con o sin locale) - Si el input es inválido, fallback a `/dashboard` (cero side-effects) 4. **Multi-step flows via cookie de session** (signup→onboarding, forgot→email→reset): `setNextRedirectCookie` / `popNextRedirectCookie` persisten el destino 1h vía `appros_next_redirect`, SameSite=Lax. - Signup: cookie set en submit, leída por `profile-setup-form` al completar onboarding (`onContinue`) — usuario nuevo aterriza en su destino original tras 10-30 min de onboarding. - Forgot/reset: cookie set en forgot-password antes del email, leída por reset-password al actualizar la contraseña con éxito. 5. **Middleware injection** (`src/proxy.ts`): inyecta header `x-pathname` en el request forwarded vía `NextResponse.next({ request: { headers } })` preservando el rewrite de next-intl (locale prefix). Sin esto los layouts no podrían leer el path. 6. **Suspense boundary** en login + signup + forgot-password — exigido por `useSearchParams` en client components (Next 15+ static prerender). 7. **OAuth callback** (`src/app/auth/callback/route.ts`): valida `next` con `sanitizeNextRedirect` ANTES de concatenar al origin (cierra open redirect via Google OAuth flow). Files (17): Helper + tests: - src/features/auth/lib/safe-next-redirect.ts (NEW) - src/features/auth/lib/__tests__/safe-next-redirect.test.ts (NEW, 46 tests) Auth gates (5): - src/app/[locale]/dev/layout.tsx (NEW) - src/app/[locale]/(main)/layout.tsx - src/app/[locale]/(onboarding)/layout.tsx - src/app/[locale]/(onboarding-profile)/layout.tsx - src/app/[locale]/(onboarding)/onboarding/test/page.tsx Middleware: - src/proxy.ts Auth pages (4): - src/app/[locale]/(auth)/login/page.tsx - src/app/[locale]/(auth)/signup/page.tsx - src/app/[locale]/(auth)/forgot-password/page.tsx - src/app/[locale]/(auth)/reset-password/page.tsx Auth components (3): - src/features/auth/components/login-form.tsx - src/features/auth/components/google-auth-button.tsx - src/features/auth/components/signup-form.tsx Onboarding completion (1): - src/features/onboarding/components/profile-setup-form.tsx OAuth callback: - src/app/auth/callback/route.ts Backward compat: - Todos los props nuevos tienen default `'/dashboard'` — sin nextUrl el comportamiento es idéntico al anterior. - Cookie helpers `typeof document` guard → server-side safe. - Cookie sin Secure flag para que funcione en dev (http://localhost); el contenido es solo un path interno sanitizado, sin PII. Quality gates: - typecheck: 0 errores. - vitest: 80 files / 1311/1311 verde (1258 + 53 nuevos = 46 helper + 7 carry-over). - i18n parity: 14 namespaces verde. - Functional curl: `/es/dev/builder-playground` (sin sesión) → `/login?next=%2Fdev%2Fbuilder-playground` ✓. 3 gaps detectados en audit y cerrados antes del commit: - Gap original: dev/playground sin auth → cubierto por el nuevo layout. - Gap A: signup pierde ?next= a través de onboarding → cookie persistence. - Gap B: reset-password pierde ?next= a través de email → cookie persistence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 5.B — wirado +6 triggers en engagement, funnels y wizard coach

Cierre de los gaps restantes del inventario de Fase 5 que requieren user click directo. Total acumulado tras 5.A + 5.B: ~38+ triggers wireados con PreActionDialog. 5 archivos modificados, +6 triggers nuevos: Engagement (2): - content-generation-form.tsx → comment-response submit (1cr) + bulk content generate con cost dinámico según mode: · meme_series → memes_generation (2cr × 1) · multi_platform → multi_platform_per_unit (1cr × N selectedPlatforms) · email/blog/regular → multi_platform_per_unit (1cr × count) - content-source-selector.tsx → file extract (engagement_source_extract_file 1cr). Solo el branch kind:'file' cobra; URL/Video branches NO consumen IA. Funnels (1): - step-offer-stack.tsx → bonus-helper × 3 fields (name/desc/value) → funnel_offer_helper 1cr. - value-ladder-generator.tsx → generate value-ladder → funnel_value_ladder 5cr. Growth Autopilot (1): - step-review.tsx → open Belenia coach dialog → campaigns_wizard_coach 1cr. Wirado en el wrapper del button que abre BeleniaCoachDialog (que internamente fetch /api/campaigns/wizard/auto-draft on open). Skips legítimos documentados (no hay user click directo, son auto-mount/auto-fetch): - Analytics journey-tab + segments-tab → auto-fetch on filter change - lead-selector.tsx lead-suggestion → auto-fetch on context change - brand-report-screen → auto-mount on render - /api/campaigns/generate → endpoint sin consumer UI directo (server-side dispatch) Pendiente Fase 5.C: - Tests e2e (4 escenarios dismiss flows: aprobar/cancelar/never/session/until-80pct) - Validación a11y con axe-core sobre PreActionDialog typecheck DONE_0 — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

close 9 PRP-035 gaps found in audit

Audit cruzando PRP literal vs código reveló 9 desviaciones que ahora se cierran. Resultado: 115/115 tests passing, lint clean, typecheck clean. Gap 1 — reuse-matcher scoring per PRP literal (lines 222-233): ANTES: topic 40 + channel 30 + format 20 + recency 10 (mi invento) AHORA: content_type 40 + temperament 30 + topic_similarity 20 + channel 10 Topic: jaccard ≥ 0.6 → full weight; channel: ∈ applicable_channels. LibraryCandidate + PlannedPiece extendidos con target_temperament + applicable_channels. library-scanner selecciona target_temperament. Gap 2 — POST /api/campaigns/wizard/distribute (PRP línea 100): Endpoint determinístico que combina scoreAllChannels + distributePieces + distributeBudget. Body: { channels, total_pieces, total_budget_usd, duration_days, buyer_persona? }. Permite que Belenia/dispatch/CLI reproduzcan la distribución sin replicar lógica. Gap 3 — readiness-score weights (PRP línea 188): ANTES: GAC 40 / GLM 20 / CDT 25 / Ads 15 (GLM y CDT invertidos) AHORA: GAC 40 / GLM 25 / CDT 20 / Ads 15 Gap 4 — readiness-score thresholds (PRP línea 194): ANTES: at_risk si gap >= 20 AHORA: on_track si gap ≤ 5; at_risk 5-15; critical >15 Overdue + completion ≥ 85 ahora cuenta como on_track (antes 80). Gap 5 — GLM aggregator sub-rows markdown/pdf/landing (PRP líneas 175-179): GlmStatusCounts.subrows = { markdown, pdf, landing } Aggregator cuenta LMs con markdown_content ≥120 chars, pdf_url presente, y landing-page rows publicadas en content_queue linkeadas vía publishing_metadata.lead_magnet_id. Gap 6 — campaignId filter en GLM listing (PRP línea 205): LeadMagnetListFilters.campaignId + filtrado en lead-magnets-list.tsx. Gap 7 — Force reuse override (PRP línea 238): ReuseDecisionOverride ahora ofrece "Forzar reuse" cuando la decisión es CREATE pero hay candidato con score (hasMatch); botón verde junto al "Forzar regenerar" existente. Gap 8 — alertas faltantes (PRP línea 197): - lm_active_no_landing (critical): LMs en vivo sin landing publicada. - ads_no_creatives_starting_soon (warn): paid channels + timeline <10% elapsed + 0 published/approved pieces. Gap 9 — engine devuelve decisiones por pieza (PRP línea 236): CampaignGenerationResult.pieces: PerPieceDecision[] con { planned_piece_id, decision, source_piece_id, source_kind, confidence, reasoning, brief? }. REUSE entries no llevan brief; CREATE/ADAPT sí. /api/campaigns/generate acepta reuse_decisions[] en el body Zod-validado y los reenvía al orchestrator. i18n ES/EN/PT: lm_active_no_landing, ads_no_creatives_starting_soon, forceReuse — todos balanceados.

Nueva Feature

PRP-052 Chunk D — 3 Header templates femeninos + KB sync (theme/ + header-templates)

Cierra el último hueco visible de PRP-052: el sistema HEADER_TEMPLATES (picker de Header design con copy + colores + nav + CTAs pre-rellenados). Las i18n keys de los 3 templates femeninos ya habían sido sembradas vía PRP-053 Fase 3 (commit 4e1f463), pero los HeaderTemplate completos faltaban — este chunk los implementa. Plus dos KB sync (Gaps A y B detectados en audit literal): - Gap A: `header-templates.ts` declara en JSDoc top-level que se parsea por el KB extractor — promesa rota desde PRP-048-Hardening Fase 9. Añadido como entry explícito en SOURCE_FILES de `scripts/extract-builder-kb.ts`. - Gap B: el directorio `src/shared/builder-kit/theme/` no estaba en KB. `theme-copy-rules.ts` (Chunk A.5) declara explícitamente que se parsea por el extractor — promesa rota mía. Añadido como auto-discovered. KB ahora incluye los 9 Master Themes con conversionHypothesis, las 10 paletas con psychology + HEX, los 11 typography pairings con research, spacing/motion/shadow presets, los 3 ThemeCopyRules feminine, y el cascade resolver — todo lo que Belenia / Arthur necesitan para recomendar themes con rationale citado y filtrar copy editorial. Cambios: Header templates (PRP-052): - `HeaderTemplateMeta.category` extendido con `'sophisticated' | 'vibrant' | 'natural_wellness'`. - 3 nuevos `HeaderTemplate` con `HeaderPropsV2` completos + HEX literal del spec `.claude/PRPs/specs/plantillas_femeninas.md`: - `sophisticated_quiet_luxury`: paleta sophisticated_luxury (bg #FAF6F2 / champagne #B89B8C), layout centered, logo "Atelier · Desde 2024", nav editorial, CTA outline subtle (sin announcement bar — spec 1.11 rechaza urgency, allowEmojis=false, allowSaturatedGradients=false). - `vibrant_joy_transformation`: paleta vibrant_optimism (bg #FFFAF5 / coral #FF7F6B / lavender #C9B6E0), layout left-aligned, logo "Glow · Tu mejor versión", dual CTA (ghost + gradient), announcement ✨ con banner-gradient (spec 2.11 SÍ permite gradientes saturados + emojis selectos). - `natural_meditative_wellness`: paleta natural_grounding (bg #F5F0E8 / sage #7A9168), layout centered, logo "Raíz · Bienestar consciente", nav wellness, CTA outline calm, announcement 🌿 banner-glass (spec 3.11 emojis botánicos OK, NO gradientes saturados, urgencyTone subtle). - `HEADER_TEMPLATES` array → 8 → 11 entries. - `CATEGORIES` filter array en `inspector-templates.tsx` → 8 → 11 chips. - JSDoc del archivo actualizado (8 → 11 diseños). KB sync (Chunk D.5): - `extract-builder-kb.ts` SOURCE_FILES: añade `header-templates.ts` (entry explícito) + `src/shared/builder-kit/theme/` (auto-discovered). - `belenia-builder-kb.ts` regenerada — 54 sections (+1) / 481 exports (+62) vs estado pre-Chunk-D. Belenia / Arthur ahora ven los 11 Header templates + las 9 estructuras del theme system. Quality gates: - typecheck: 0 errores. - vitest: 79 files / 1258/1258 verde. - i18n parity: 14 namespaces verde. - bundle audit verde (113 routes scanned, 0 leaks). - HEADER_TEMPLATES.length === 11 (source-level verification). Spec compliance verificado: - Sophisticated 1.11: NO gradientes saturados ✓ (CTA outline), NO emojis ✓ (sin announcement bar), urgencyTone subtle ✓, whitespace generoso ✓ (centered layout). - Vibrant 2.11: SÍ gradientes saturados ✓ (CTA gradient + announcement banner-gradient), SÍ emoji selecto ✨ ✓, surface marfil cálido (no white puro) ✓, dual CTA ✓. - Natural 3.11: emoji botánico 🌿 ✓, NO gradientes saturados ✓ (CTA outline + banner-glass soft), urgencyTone subtle ✓, layout centered con whitespace meditativo ✓. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 4.D — consumeCredits en Ads + Analytics + Presentations + Leads + Bookings

Cierre de Fase 4 con auditoría doble: pase 1 inicial (10 endpoints con feature_keys existentes + 3 keys nuevos) + pase 2 cross-service (3 gaps adicionales: campaigns wizard coach, bookings talking-points, bookings post-meeting summary). Migración 120 — 6 feature_keys nuevos: - arthur_voice_memo (3cr): transcript → análisis Belenia → tasks + leads - affiliate_promo_copy (1cr): IA mejora copy de afiliado por canal/categoría - engagement_source_extract_file (1cr): extracción AI de texto desde archivos - campaigns_wizard_coach (1cr): Belenia narrativa coach del wizard auto-draft - booking_talking_points (2cr): guía AI conversación al crearse el booking - booking_post_meeting_summary (2cr): resumen + CTA al prospecto al completed 16 endpoints refactorizados (consumeCredits + 402 en insufficient_credits/soft_cap): Existing keys (10): - ads/generate-copy → ads_generate_copy (3) - ads/campaigns/[id]/report → ads_campaign_report (5) - analytics/journey-insights → analytics_journey_insights (5) [best-effort fallback] - analytics/segments/insight → analytics_segments (3) - presentations/[id]/generate → presentation_generate (8) - presentations/sessions/[id]/complete → presentation_summary (2) [public, charge owner] - lead-intelligence/suggestions/generate → lead_suggestion (1) - leads/[id]/enrich → lead_enrichment (10) - leads/[id]/timeline-pdf → lead_timeline_narrator (2) - leads/[id]/timeline-narrative → lead_timeline_narrator (2) [solo force=1] New keys (6): - arthur/voice-memo → arthur_voice_memo (3) - affiliates-saas/tools/copy → affiliate_promo_copy (1) - engagement/extract-source (file branch only) → engagement_source_extract_file (1) - campaigns/wizard/auto-draft → campaigns_wizard_coach (1) [rate-limited 3/hr] - bookings/create → booking_talking_points (2) [public, charge owner via ref_code] - bookings/[id] PATCH on completed → booking_post_meeting_summary (2) Skips intencionales documentados: support/triage (community helper free), nps (system sentiment), arthur/memories (CRUD), affiliates-saas/tools/banner (SVG fallback), campaigns/[id]/dispatch (orquestación), bookings/reminders warmup-cron (infraestructura system-driven). Audit final Fase 4 — 0 gaps: - 59 endpoints con consumeCredits (43 4.A/B/C + 16 4.D) - 38 con AI imports → 36 cobrados + 2 intencionales (discord/ask + health HEAD) - typecheck EXIT_CODE=0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 4.C — consumeCredits en Engagement + Funnels + Builder + Forms

22 endpoints refactorizados al modelo unified Resource Economy. Reemplaza can_generate_content + increment_content_generated por consumeCredits con status 402 (Payment Required) para insufficient_credits / soft_cap_reached. Engagement (7): - comment-response (1 cr), cta-suggest (1 cr), lead-suggestion (1 cr) - trends (2 cr), ugc/generate-script (2 cr), ugc/generate-brief (2 cr) - generate multi-branch: meme_series (memes_generation 2 cr), multi_platform (multi_platform_per_unit × N), email_campaign (× emailCount), blog_post (× articleCount), regular content (× weighted totalCreditCost) Funnels (7): - offers/autopilot (funnel_offer_gen 3 cr) - offers/bonus-helper, guarantee-helper, scarcity-helper (funnel_offer_helper 1 cr) - value-ladder (funnel_value_ladder 5 cr), value-ladder/autofill (funnel_autofill 2 cr) - [id]/offer + offers/[offerId] generate copy (funnel_offer_gen 3 cr) — gaps de audit Builder (3): - generate-block-tree (5 cr), rewrite (1 cr), media/generate (2 cr) Forms (1): - interactive-forms/conversational (1 cr al form.user_id; endpoint público) Audit gaps cerrados (3 fuera del scope original): - campaigns/generate (campaigns_strategy_gen 5 cr) — Growth Autopilot - campaigns/ai-generations/[id]/regenerate (campaigns_regen 1 cr) - dashboard/tasks/kpi-summary (dashboard_kpi_summary 1 cr) — best-effort Migration 119: 5 nuevos feature_keys (funnel_offer_helper, funnel_autofill, campaigns_strategy_gen, campaigns_regen, dashboard_kpi_summary). No charge intencional: - discord/ask (community helper free por diseño PRP-046) - health (HEAD check OpenRouter, no genera contenido) - builder/audit + builder/suggestions (rule-based, sin AI) - engagement/media-poll + memes/[seriesId] retry (no double-charge) Pendiente Fase 4.D: ads/generate-copy, analytics/journey-insights, ads/campaigns/[id]/report + auditoría final de toda la Fase 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 4.A — consumeCredits en Chat/Mentor + Prospecting

7 endpoints refactorizados con consumeCredits() ANTES del trabajo IA. Source-of-truth nuevo (usage_counters); legacy can_send/increment_belenia se conservan para compat con gamification-service. Cleanup en Fase 13. Endpoints: - /api/mentor/chat → belenia_chat_message (1 crédito) - /api/affiliate-onboarding/coach/chat → affiliate_coach_message (1 cr, desde pool del sponsor) - /api/affiliate-onboarding/roleplay → affiliate_roleplay_session (5 cr, desde pool del sponsor) - /api/prospecting/improve-message → improve_message (1 cr) - /api/prospecting/improve-description → improve_description (1 cr) - /api/prospecting/leads/[id]/suggestion → lead_suggestion (1 cr) - /api/prospecting/scraping/start → lead_scraping_per_lead (0.2 cr × N leads, MULTI-UNIT validation) Personas POST descartado del refactor: solo INSERT row, no consume IA. Patrón aplicado: 1. Después de auth check, ANTES del trabajo 2. consumeCredits(supabase, userId, featureKey, units, metadata) 3. Si !ok: return 402 (insufficient_credits/soft_cap_reached) o 500 4. Metadata captura context (conversation_id, lead_id, etc.) para auditar Audit Chunk 4.A: 7/7 verde, typecheck 0, build 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 4.B — consumeCredits en Nurturing + Onboarding/Affiliate

12 endpoints refactorizados con consumeCredits() ANTES del trabajo IA. Audit verde 12/12, typecheck 0, build 0. Endpoints: - Nurturing (4): * /api/nurturing/lead-magnets/[id]/generate → varía por LeadMagnetType: checklist 2cr, script 2cr, quiz 2cr, pdf_guide 8cr, case_study 6cr, mini_course 40cr (high-cost cap), assessment 48cr (cap), webinar 50cr (cap), challenge 60cr (cap), ugc_video 10cr (cap) * /api/nurturing/lead-magnets/improve → lead_magnet_improve (2cr) * /api/nurturing/lead-magnets/suggest-topics → lead_magnet_suggest_topics (1cr) * /api/nurturing/sequences/personalize-message → sequence_personalize_message (1cr) Solo cobra en runtime (NO en design preview, isDesignMode = !leadId) - Onboarding (3): * /api/onboarding/brand/extract → brand_context_generator (2cr) * /api/onboarding/brand/generate-brief → brand_context_generator (2cr) * /api/onboarding/action-plan/generate → action_plan_generator (3cr) - Affiliate Onboarding (5): * /api/affiliate-onboarding/commitments → affiliate_commitments (2cr) * /api/affiliate-onboarding/training → affiliate_training_kit (5cr) * /api/affiliate-onboarding/roadmap → affiliate_roadmap (2cr) * /api/affiliate-onboarding/kit/auto-fill → affiliate_kit_autofill (3cr) * /api/affiliate-onboarding/kit/extract → affiliate_kit_extract (2cr) Patrón aplicado: - Mapping LeadMagnetType (DB enum) → feature_key (catálogo) en lead-magnets generate route, validación si content_type es desconocido - Sequence personalize: gate en runtime only (design no consume) - Affiliate-onboarding endpoints: user.id es el SPONSOR (su pool paga la generación para sus afiliados) - Status 402 en insufficient_credits / soft_cap_reached, 500 en otros Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 3 — Shared UI Components

4 componentes glass + tooltip Radix + playground + 23 a11y tests + i18n parity 3 locales. Auditoría 8/8 verde, 3 gaps cerrados antes del commit. Componentes (src/features/billing/components/usage/): - usage-badge.tsx — counter ubicuo X/Y + barra + color por threshold Soporta credits + 5 high-cost caps + leads_scraped + 2 storage caps Variantes: compact / full. Optional showUpgradeCta cuando se topa - pre-action-dialog.tsx — confirmación pre-consumo con 3 niveles dismiss Estados: preview / cache_hit / insufficient_credits / soft_cap_reached Persiste preferencia en pre_action_dialog_preferences (RLS) Esc + click outside + focus inicial + role=alertdialog + aria-modal - usage-widget.tsx — para PCC, top 3-5 métricas + plan + link a settings - soft-cap-badge.tsx — específico high-cost items (Mini Course/Challenge/etc) 3 estados: locked (Lock + tier no incluye) / unlimited (Sparkles Enterprise) / normal (X/Y este mes con red ring si capped) - usage-tooltip.tsx — helper compartido sobre Tooltip oficial (Radix) Glass styling, focus visible, max-w-xs, side configurable Hook (src/features/billing/hooks/): - use-pre-action-confirm.ts — gestiona dismiss preferences per feature_key Lee/escribe pre_action_dialog_preferences via Supabase RLS user_id shouldShowDialog() decide si mostrar según level (show_always/never/ session_only/until_80pct) + pct usage actual Playground (src/app/[locale]/dev/resource-economy-playground/): - page.tsx — visualización completa de los 4 componentes con datos reales Botones para abrir cada variante de PreActionDialog (preview/cache_hit/ insufficient/soft_cap_reached). Solo /dev (no expuesto a usuarios) i18n (messages/{es,en,pt}.json): - Billing.resourceEconomy.* nuevo namespace con 76 keys × 3 locales = 228 - Sub-namespaces: badge, dialog, widget, softCap - Tooltip descriptions completas para los 11 metrics - 9 cap names (5 high-cost + 4 plan-locked variants) - A11y aria-labels con interpolación dinámica Tests vitest (59 verdes, 3 archivos): - usage-helpers.test.ts (22) — funciones puras - usage-actions.test.ts (12) — RPC mocks (5 escenarios spec) - a11y-static.test.ts (25) — patrones ARIA en source Cubre role/aria-label/aria-hidden/focus/Esc/role=alertdialog/fieldset Gaps cerrados antes del commit: - Gap 1: Refactor de tooltips custom CSS → componente Tooltip oficial Radix Helper compartido UsageTooltip wrappea TooltipProvider+Trigger+Content Mejor a11y (focus management Radix) + glass styling preservado - Gap 2: Creación playground page para validación visual (PRP punto 5) - Gap 3: Static a11y audit con 23 tests covering ARIA patterns axe-core browser audit queda para Fase 12 (Pre-launch QA) per PRP Glassmorphism: glass-subtle ring-1 backdrop-blur + ring colors por threshold (amber/red para warning/danger). Consistent con design system Appros. Validación: typecheck 0 + build 0 + vitest 59/59 + i18n parity 76×3 verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 2 — API Single Source of Truth

Endpoint único + service layer + hook con cache TTL 30s + 34 tests. Auditoría 6/6 verde, gaps cerrados antes del commit. Endpoint: - GET /api/me/usage — single source of truth para counters Llama RPC get_user_usage_summary(user_id), retorna estructura completa { plan, period, credits, high_cost_caps, soft_caps } Cache-Control: private, max-age=30 Service layer (src/features/billing/services/): - usage-types.ts — types completos (UsageSummary, ConsumeCreditsResult, etc.) - usage-core.ts — getUserUsageSummary(supabase, userId) - usage-actions.ts — 4 funciones: * consumeCredits() — descuenta créditos atómicamente * logCacheHit() — registra cache hit (Appros absorbe) * applyOveragePurchase() — extiende limit del período * getFeatureCost() — lookup sin consumir (para pre-action dialog) - usage-helpers.ts — funciones puras: * calculateTotalCost (multi-unit math) * shouldShowDialog (4 dismiss levels) * isHighCostCapReached (con soporte -1 ilimitado) * getUsageColorState (verde<60% / amber 60-90% / rojo>90%) * pickFeatureDisplayName/Description (i18n locale picker) * formatCredits (Intl.NumberFormat) Hook actualizado (use-usage.ts): - useUsage() — nueva API canónica, retorna { data, loading, error, refetch, mutate } Cache TTL 30s in-memory + dedup de fetches concurrentes invalidateUsageCache() exportado para llamar tras consumeCredits - useUsageMeters() — adapter legacy mientras Fase 4 migra consumers Mapea UsageSummary al shape antiguo { meters, period_end, isOverAnyLimit } Migración de consumer: - usage-meters.tsx ahora usa useUsageMeters (legacy adapter) Tests vitest (34/34 verdes): - usage-helpers.test.ts — 22 tests de funciones puras - usage-actions.test.ts — 12 tests con mock de Supabase RPC Cubre los 5 escenarios spec del PRP: 1. success (consumeCredits ok) 2. insufficient_credits 3. soft_cap_reached (Mini Course cap topado) 4. cache_hit (logCacheHit) 5. overage_active (applyOveragePurchase extiende limit) + tests bonus: multi-unit (multi-platform 14 plataformas), getFeatureCost Gap cerrado durante implementación: - Test isHighCostCapReached fallaba al asumir missing keys = false. Fix: distinguir limit=0 (capped) vs limit missing (data malformed). Test nuevo cubre Freemium con limit=0 explícito (correctamente capped). Validación: typecheck 0 + build 0 + 34/34 tests + audit 6/6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-053 Fase 1 — DB Foundation Resource Economy

Crea las 7 tablas + 6 RPCs + extiende billing_plans + migra users.tier v1→v2 + actualiza types.ts. Auditoría 8/8 verde, gaps cerrados antes del commit. Tablas nuevas (113): - feature_credit_costs (catálogo DB-driven, 55 features seedadas) - usage_counters (per usuario per período, reset por billing anniversary) - usage_history (audit trail "Fecha | Feature | Recurso | Cantidad") - sponsor_allocations (3 modos: %, absolute, flag-checkbox) - overage_purchases (idempotency vía polar_payment_id UNIQUE) - pre_action_dialog_preferences (4 niveles: show_always, never, session_only, until_80pct) - shared_high_cost_items (sponsor sharing con affiliates) billing_plans extendido (114) — 5 tiers backfill: - Freemium $0 → 30 créditos/mes - Starter $29 → 400 créditos/mes - Pro $69 → 1000 créditos/mes (más popular) - Elite $199 → 4000 créditos/mes - Enterprise → 25000 créditos fair-use cap RPCs (117): - consume_credits() — source-of-truth de consumo - log_cache_hit() — Appros absorbe (cache hit = 0 créditos) - apply_overage_purchase() — extiende limit del período - get_user_usage_summary() — endpoint /api/me/usage usa esto - get_user_billing_plan() — helper - get_or_create_current_period() — helper Migración v1→v2 (116): - 'free' → 'freemium' - 'starter' v1 (era GRATIS) → 'freemium' (¡no a v2 'starter' paid!) - 'growth' → 'pro' - 'scale' → 'elite' - backup en _bk_users_tier_v1_2026_05_06 Gaps cerrados durante implementación (antes del commit): - 5 archivos con tier names v1 hardcoded actualizados: mentor/page.tsx, floating-assistants.tsx, use-crisp-context.ts, action-plan-screen.tsx, use-scraping-limits.ts, saas-churn-scorer.ts - Migración 116 ajustada: v1 'starter' (free) → v2 'freemium' (no paid) types.ts: 229 líneas nuevas con tipos para 7 tablas. Audit verde: 5 migraciones, 7 tablas, 7 RLS, 6 RPCs, 55 features seed, 5 tiers backfill, types.ts completo, typecheck + build verde. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Chunk C — recomendador feminine + theme.recommend tool + a11y contrast

Chunk C cierra el ciclo runtime de PRP-052 sobre los 9 Master Themes ya ensamblados (Chunks A/A.5/B): heurística de recomendación con sensibilidad femenina, tool Belenia para sugerir theme apropiado, tests de contraste WCAG sobre las 3 paletas femeninas, KB regenerada con la nueva tool, y validación visual end-to-end del picker mostrando los 9 themes. Cambios principales: Recomendador heurística: - `selectMasterTheme` extendido con input `vertical?: GenerationVertical` (9 verticales: beauty/wellness/coaching/fitness/lifestyle × premium/trendy/ natural/holistic/empowered/transformation/executive_women). El vertical override gana sobre el matching heurístico (señal explícita más fuerte). - 3 `TONE_BUCKETS` nuevos (sophisticated_quiet, vibrant_joy, natural_wellness_signals) evaluados ANTES que premium/warm/energetic, con keywords ES/EN/PT trilingües. Cierra Gap 2: añade 'holístico' (m) al bucket — la forma masculina es la más usada en hispanohablantes. - 3 nuevas branches case en la función — sophisticated/vibrant/natural_wellness ganan sobre los 6 themes pre-existentes cuando hay señales feminine. - 12 rationales nuevos en inglés (3 feminine branches + 9 VERTICAL_TO_THEME entries) para consistencia con los 6 rationales pre-existentes en EN. Recomendador panel pasivo (Belenia recommendations): - `RecommendationContext` extendido con `vertical?` propagado al call de `selectMasterTheme`. Cierra Gap 1: el panel pasivo de Belenia ya no es ciego al vertical declarado — usuarios con `vertical: 'beauty_premium'` obtienen la sugerencia "swap to sophisticated" desde el panel. Belenia tool: - `theme.recommend` registrada como tool 14 del builder. Wrapping declarativo de `selectMasterTheme` con Zod schema estricto (temperament + objective + content_type + brand_tones + brand_keywords + vertical). Pure function — devuelve `BuilderThemeProposal { masterThemeId, rationale, feedbackKey }` que el chat UI presenta con un botón Apply. a11y WCAG 2.2 contrast: - Tests de los 12 ratios principales (3 paletas × 4 tokens: text / textMuted / primary / accent vs surface) con thresholds basados en rol: AAA ≥ 7 para text/surface (claim del spec), ≥ 3 para textMuted (large-text AA), ≥ 2 para primary (decorative-fill role), ≥ 1.2 para accent (decorative-only role — vibrant mint #B8E6C9 1.33:1 es intencional per spec). i18n trilingüe (ES/EN/PT): - 12 strings nuevos para `BuilderKit.builderTools.theme.recommend.{name, description, applied, validationError}`. Tests: - 30 tests para `selectMasterTheme` (vertical overrides, hard objective, feminine buckets, fallbacks pre-PRP-052, default tech_minimal, masculino 'holístico'). - 5 tests para `belenia-builder-recommendations-vertical` (verticals → swap_master_theme con feminine themes, no swap cuando theme actual coincide). - 4 tests para `theme.recommend` tool (vertical match, tone keyword, natural_wellness, default). - 12 tests para `feminine-palettes-contrast` (12 ratios WCAG por las 3 paletas femeninas). KB regeneration: - `extract-builder-kb.ts` incluye `builder-theme-tools.ts`. - `belenia-builder-kb.ts` regenerada — 52 sections / 417 exports. Quality gates verde: - typecheck: 0 errores. - vitest: 76 files / 1199/1199 verde (1148 base + 51 nuevos). - i18n parity: 14 namespaces verde. - bundle audit: 113 routes scanned, 0 leaks. - Visual Playwright: picker abre con 9 themes (6 + sophisticated / vibrant / natural_wellness), Sofisticada apply end-to-end (canvas surface → cream #FAF6F2 del sophisticated_luxury palette). 3 gaps detectados en audit y cerrados antes del commit: - Gap 1: panel pasivo Belenia ciego a vertical → propagado. - Gap 2: bucket natural_wellness_signals solo en femenino → 'holístico' añadido. - Gap 3: rationales nuevos en español rompían consistencia con los 6 pre-existentes en EN → traducidos a inglés. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Chunk B — 3 Master Themes femeninos ensamblados

Ensambla los 3 Master Themes con sensibilidad femenina del spec `.claude/PRPs/specs/plantillas_femeninas.md`, sumando 9 themes totales en el theme picker (6 preexistentes + 3 nuevos). Cada theme referencia los presets cerrados en Chunk A/A.5 — ningún valor inventado, HEX/fonts/curvas de motion son LEY del spec. Cambios: - `MasterThemeId` y `masterThemeIdSchema` extendidos con `'sophisticated' | 'vibrant' | 'natural_wellness'`. - `MASTER_THEMES` con 3 entradas nuevas que ensamblan typography + palette + motion + spacing + shadow + conversionHypothesis (citaciones Quiet Luxury Convention 2024, Glossier/Drunk Elephant/Rare Beauty, Tata Harper/RMS Beauty/Aesop). - `getThemeCopyRules` tipado contra `MasterThemeId` (no más `string`). - 18 strings i18n nuevos: `BuilderKit.presets.masterTheme.{sophisticated, vibrant, natural_wellness}.{name, description}` × ES/EN/PT. Mapping LEY del spec: - sophisticated: sophisticated_serif + sophisticated_luxury + subtle + sophisticated_premium (128px) + sophisticated-tinted (spec 1.7 quiet luxury rechaza animaciones notorias). - vibrant: vibrant_display + vibrant_optimism + bouncy_dynamic + vibrant_dynamic (112px) + vibrant-coral (spec 2.7 joy + transformation pide bezier overshoot). - natural_wellness: natural_organic + natural_grounding + slow_organic + natural_meditative (144px) + natural-warm (spec 3.7 trust + grounding pide ease-out contemplativo, parallax mínimo). ThemePickerDialog es data-driven (`MASTER_THEME_LIST.map(...)`), por lo que renderiza los 9 themes automáticamente sin wiring adicional. Quality gates: - typecheck: 0 errores. - vitest: 1148/1148 verde. - i18n parity (check:builder-i18n): 14 namespaces verde. - source-of-truth: `MASTER_THEME_LIST.length === 9`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Chunk A.5 — cierre de 7 gaps femeninos sobre Chunk A

Tras audit literal del spec plantillas_femeninas.md contra Chunk A, cierro los 7 gaps detectados antes de ensamblar Master Themes: - Gap A — 3 shadow presets tinted (sophisticated-tinted negro suave, vibrant-coral, natural-warm marrón): nota 13 del spec exige tinte de paleta, no rgba(0,0,0,X) genérico - Gap B — Variable font axes Fraunces ('opsz' 144 SOFT 50 WONK 1) en vibrant_display via nuevo campo displayVariableSettings + CSS var --bk-font-display-variation-settings - Gap C/D — lineHeight + letterSpacing + transform por slot en TypographyPresetDefinition (Sofisticada display 1.15 / body 1.65 + caption uppercase 0.15em; Vibrante tight 1.05 -0.02em; Natural 1.25 / 1.7) - Gap E — fontDisplayAlt (Caprasimo) wired a vibrant_display + fontScript (Caveat) wired a natural_organic; resolver emite --bk-font-display-alt y --bk-font-script - Gap F — containerMaxWidthPx en SpacingPresetDefinition + 3 nuevos spacing presets (sophisticated_premium 1280px, vibrant_dynamic 1280px, natural_meditative 1180px) con paddings spec literal 128/112/144 px - Gap G — theme-copy-rules.ts con 3 ThemeCopyRules (tone, allowEmojis + emojiAllowList, allowSaturatedGradients, urgencyTone, vocabulary hints/forbidden) consultable por Belenia/Arthur al generar copy Resolver actualizado: 11 nuevas CSS vars condicionales (--bk-lh-*, --bk-ls-*, --bk-tt-*, --bk-font-*, --bk-container-max-width, --bk-motion-easing). Todas opcionales — backward compat garantizada. i18n: 12 nuevas keys × 3 locales = 36 entradas en es/en/pt (BuilderKit.presets.shadow.{sophisticated_tinted,vibrant_coral,natural_warm} y BuilderKit.presets.spacing.{sophisticated_premium,vibrant_dynamic, natural_meditative}). Quality gates verde: typecheck 0 errores, vitest 1148/1148 pasa, i18n parity 14 namespaces sin failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 feminine themes — Chunk A foundation (presets + fonts)

Foundation para integrar 3 plantillas con sensibilidad femenina (Sofisticada / Vibrante / Natural-Wellness) como Master Themes adicionales del Builder Kit. Este chunk añade los "ingredientes" reusables; el ensamblaje en MASTER_THEMES viene en Chunk B. Plantillas según `.claude/PRPs/specs/plantillas_femeninas.md` — HEX values literales del spec, no se ajustan. Color palettes (3 nuevas, sin tocar las 7 existentes): - sophisticated_luxury: champagne #B89B8C + rose-powder + soft-black sobre cream #FAF6F2 — quiet luxury (text/bg 17.4:1 AAA) - vibrant_optimism: coral #FF7F6B + lavender + mint sobre ivory #FFFAF5 — joy + transformation (text/bg 14.8:1 AAA) - natural_grounding: sage #7A9168 + terracotta + warm-brown sobre sand #F5F0E8 — trust + holistic (text/bg 14.2:1 AAA) Typography presets (3 nuevos, sin tocar los 8 existentes): - sophisticated_serif: Cormorant Garamond + Inter (Chanel/Net-a-Porter) - vibrant_display: Fraunces variable + Manrope (Glossier/Rare Beauty) - natural_organic: Lora + Karla line-height 1.7 (Aesop/Tata Harper) Motion presets (2 nuevos, sin tocar los 4 existentes): - bouncy_dynamic: bezier 0.34, 1.56, 0.64, 1 (overshoot) — pareo Vibrante - slow_organic: bezier 0.25, 0.46, 0.45, 0.94 (slow ease) — pareo Natural Sofisticada reutiliza `subtle` (smooth ease ya existente). Añadido campo `easing?` opcional a MotionPresetDefinition (retrocompat). Fonts cargadas via next/font/google con subsets ['latin', 'latin-ext'] para soportar acentos PT: - Manrope (sans-modern, body de Vibrante) - Karla (sans-modern, body de Natural) - Caprasimo (display, alt de Vibrante para CTAs grandes) Cormorant Garamond / Lora / Fraunces / Caveat / Inter ya estaban. i18n trilingüe ES/EN/PT (24 keys nuevas — 8 keys × 3 locales) en BuilderKit.presets.{colorPalette,typography,motion}.* Quality gates: - typecheck: 0 errors - vitest: 866/866 passing - i18n parity: 14 namespaces verde Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 6 — Embeddable Element Library (no-gaps)

Sidebar tab "Elements" agrega TODOS los content blocks como cards draggables con thumbnail (lucide). Click en EmptySlotPlaceholder activa filtro slot-aware: dim incompatibles (whitelist accepts), banner teal con × close, ESC cancel, click en card compatible inserta como child y limpia filtro. Search box filtra cards por nombre/descripción/type. Snap zones inter-children (`SlotInterChildZone` con atIndex) renderizadas durante drag activo entre cada par de children — el DnD provider propaga atIndex al insertChildBlock para respetar posición visual. Slot accepts alineados a spec literal (PRP-052 líneas 127-128): - Hero.media: ['image','video','lead-magnet','form'] - Footer.col1/col2/col3: 'any' Analytics events `slot.*` añadidos a `BuilderEventName` y emitidos: - slot.filter_activated (block_id, slot_id, accepts) - slot.filter_cleared (block_id, slot_id) - slot.child_added (block_id, slot_id, child_block_type, child_id) - slot.child_rejected (block_id, slot_id, attempted_type, reason) Quality gates verde: - typecheck 0 errores - vitest 866/866 (16 nuevos tests: slot-filter-context + block-categories) - i18n parity 14 namespaces — BuilderKit.{elements,categories,sidebar}.* y BuilderKit.slots.clickToAdd añadidos en ES/EN/PT - axe-core 0 violations en sidebar (idle + filter activo) - bundle audit 0 leaks (113 rutas) - audit hardcoded strings: 0 strings UI literal en componentes nuevos Playwright e2e validados en DT/TB/MB: - click EmptySlot → banner + auto-tab elements + 3 analytics events - search filtra (imag→2 cards, xyzzy→0 + i18n no-results) - Hero.media restrictivo: 4 compatibles / 31 dimmed con aria-disabled - click compatible inserta + filter cleared - click dimmed no-op (filter persiste, tree intacto) - ESC + × button cancelan filter - snap zones inter-children renderizan durante drag con atIndex correcto Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 5 chunk D — DnD type validation + slot drop targets

Type-validated drag & drop into slots (Section/Stack/Hero/Footer): - validateSlotDrop pure helper (slot-validator) — accepts/cardinality contra blockType - slot-aware collision detection (pointerWithin + slot priority + fallbacks) resuelve el solapamiento entre slot droppable y sortable parent block - BuilderDndProvider expone dropSlot {parentId, slotId, ok, reasonKey} en context - onInsertChildBlock callback en provider + insertChildBlock action en store - insertChildBlockInTree pure: stamea __slotId, append/atIndex con clamp, no muta - EmptySlotPlaceholder ahora useDroppable + lee dropSlot del context para visual feedback teal (válido) / rose dim (incompatible) + tooltip i18n + aria-disabled - SlotAppendZone nuevo: drop target invisible cuando idle, visible durante drag para appendear a slots con cardinality='many' que ya tienen children - Footer expone slots col1/col2/col3/legal-bottom como ADITIVOS sólo en canEdit: preserva render legacy 100% en preview/visitor (validado por screenshot) i18n alineado: - reasonKey BuilderKit.slots.rejected.{type,cardinality,unknown} (plural) - ES/EN/PT 13 keys parity OK Tests + validación e2e Playwright: - 12 tests insertChildBlockInTree (stamp __slotId, atIndex, inmutabilidad, nested) - 6 tests validateSlotDrop (any, whitelist, cardinality, type-prevalece-sobre-card) - e2e Section.body: heading insertado con __slotId='body' - e2e Hero.media: image insertado, heading rechazado (slots.rejected.type), segunda image rechazada (slots.rejected.cardinality) - e2e depth-3: Section > Stack > heading con __slotId='body' propagado - e2e Footer.col1: heading aditivo con __slotId='col1', legacy preservado - axe-core 0 WCAG 2.2 AA violations en idle / drag-over / drag-incompatible - Responsive DT/TB/MB validado Quality gates: typecheck 0 errores, vitest 71 archivos / 1138 tests, i18n parity OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 5 chunk C — slot migrators (no-regresión visual) + stripWrappingParagraph fix

Migrators legacy → slot children, ZERO regresión visual: - Hero: badgeText/headline/subheadline → slot 'content' (paragraph/heading/paragraph); mediaUrl → slot 'media' (image). CTAs preservados en props (render inline <a> compacto). Slots 'content'/'media' declarados. - Footer: NO-OP (preserva render legacy). Slots col1/col2/col3/legal-bottom declarados para composición aditiva en chunks futuros. - Header v2 (con navItems[]/ctas[]): skip — preserva mega menus/depth/dynamic source intactos. Header v1 puro: navLinks → paragraphs en slot 'nav', ctaText → cta en slot 'cta-area'. Slots 'nav'/'cta-area'/'announcement' declarados. Hero render hybrid: - Slot 'content' via <SlotRenderer> cuando children presentes - CTAs siempre desde props como <a> inline (legacy) - getInlinePlainText() defensive guard para data legacy pollida con HTML Auto-blindaje crítico (stripWrappingParagraph): - Pre-existing bug: <p style="text-align: left;">...</p> emitido por Tiptap no era stripped (regex sin tolerancia a atributos). Resultado: persistencia de HTML crudo en block.props.ctaText/etc, visible en preview como literal "<P STYLE=...>". - Fix: regex /^<p\b[^>]*>.../ tolera atributos. Defensive nested-<p> guard preserva estructura cuando hay paragraphs anidados. - Aplica a TODOS los inline-edit fields con richText:false (no solo Hero CTAs). Validación Playwright DT/TB/MB: - Builder + preview: visual fidelity preservada - Inline-edit slot child + legacy CTA: ambos persisten correctamente - Idempotencia post-reload: mismos children IDs, sin duplicados - Inspector universal sections + Wave 4 BreakpointSwitcher operativos - Wave 1 pageBg + Wave 2 inspector + Wave 3 sizing + Wave 4 spacing intactos - Header v1 → v2 → slot skip; Header v2 → slot skip (richness preservada) Tests: - 26 tests legacy-slot-migrator (Hero skip-CTA, Footer NO-OP, Header v2-skip) - 14 tests inline-editable-text-helpers (stripWrappingParagraph + escape) - typecheck 0 errores, vitest 1120 tests verde, i18n parity 31 namespaces OK 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 5 chunk A+B — typed slots foundation + 4 structural blocks

Chunk A (foundation): - BlockDefinition.slots?: SlotDefinition[] con accepts/cardinality/hint/emptyLabelKey - services/slot-validator.ts: canDropBlockInSlot, validateDrop, childrenInSlot, countChildrenInSlot, readChildSlotId, assignChildSlotId, SLOT_ID_KEY (__slotId) - components/slots/SlotRenderer + EmptySlotPlaceholder (DnD-aware, hint icon, WCAG 2.2 AA) - 20 tests slot-validator (whitelist, cardinality, drop validation) - i18n BuilderKit.slots.{rejected,empty} 13 keys × 3 locales (ES/EN/PT) Chunk B (4 nuevos bloques estructurales): - Section, Stack, Grid, Column en blocks/page-blocks/structural-blocks.tsx - Cada uno declara slot 'body' (accepts: 'any', cardinality: 'many') - Inspector universal: background, sizing, spacing, border, shadow, animation, visibility - Registry actualizado: 26 page blocks (antes 22) - i18n BuilderKit.blocks.{section,stack,grid,column}.{name,description} × 3 locales Validación: - typecheck 0 errores - vitest 68 archivos / 1080 tests verde (+20 slot-validator) - i18n parity 31 namespaces OK Próximos chunks: - C: migrators Hero/Footer/Header preservando data legacy - D: DnD type validation + drop targets visuales + Playwright e2e 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 4 — Padding/Margin Unification (no gaps)

BoxSides como única fuente de verdad. Legacy SpacingToken padding/margin borrado del renderer. Per-bp override con cascade desktop-first activo en runtime canvas. End-to-end validated. Migrator - migrateLegacySpacingTokens: legacy padding/margin SpacingToken → container BoxSides. Mapea desde DEFAULT_DESIGN_TOKENS.spacingScale (4/8/16/24/32/48/ 64/96/128 px). Idempotente. Wired en migrateBlock. - createBuilderStore corre migrateBlockTree en boot — todos los trees entran ya migrados al store. Renderer drop legacy lookup - block-style-resolver: PADDING_CLASS / MARGIN_CLASS eliminados. resolveBlockStyleClasses solo lee radius/shadow legacy. - ObjectStylingPanel: TokenSelect padding/margin removidos (única fuente de verdad ahora es <SpacingSection>). ContainerWrapper end-to-end - Aplica resolveContainerStyle al DOM (padding/margin/bg/border/borderRadius/ shadow/opacity advanced) — cierra el loop PRP-048-Hardening. - Sizing + advanced container coexisten: sizing aporta default lg=1280 + alignment; advanced gana en width/maxWidth si explícito. Per-bp override + runtime cascade - SpacingSection BreakpointSwitcher (lg/md/sm) + cascade descendente vía resolveResponsiveStyle/applyResponsiveOverride/clearResponsiveOverride. - Badge "Inherited" cuando bp activo no tiene override propio. - PreviewBreakpointProvider context: BlockCanvasDnd traduce previewMode topbar (DT/TB/MB) a BreakpointId y lo expone via context. ContainerWrapper consume y aplica responsive cascade en runtime. - Helpers extraídos a spacing-section-helpers.ts (puros, testables sin testing-library): applyLockedSide, applyTokenSpacing, readSidesForBreakpoint, hasOwnOverride, applyContainerPatch, isZeroSides, emptySides. i18n - BuilderKit.spacing.breakpoint.{label,lg,md,sm,inherited,reset} añadido ES/EN/PT. Parity 21 keys × 3 locales OK. Tests (+57) - block-style-migrator.test.ts: 11 tests legacy spacing migrator + 6 tests HTML pre/post equivalence (resolved styles). - spacing-section-helpers.test.ts (NEW): 35 tests cubren lock corners (4 sides × locked/unlocked), token shortcuts (xs..5xl mapping al spacingScale), cascade per-bp readSidesForBreakpoint (lg/md/sm + herencia), hasOwnOverride base vs override, applyContainerPatch limpieza container vacío, sanitización NaN/negative/decimals. - block-style-resolver-legacy.test.ts: actualizado, asserta NO emite p-/m- legacy. Spec validation (PRP-052 Wave 4) - [x] Vitest snapshot HTML pre/post migración idéntico (modulo formato) - [x] grep legacy padding: 'md' lookup retorna 0 matches en services/ - [x] Lock corners funciona (top sincroniza right/bottom/left) - [x] Token shortcuts emiten valores correctos del spacingScale - [x] Per-bp override (cascade lg → md → sm) - [x] e2e literal: edit Mobile (sm=8) → Desktop preserva (lg=24) → Tablet inherita Desktop (md=24) - [x] Playwright DT/TB/MB topbar runtime: data-builder-canvas-bp + paddingTop computed coinciden con cascade Validation - typecheck 0 errors - vitest 67 files / 1060 tests passing (+57 nuevos) - i18n parity 30 namespaces OK (Spacing 21 keys) - e2e Playwright: bp switching, runtime cascade, legacy migration end-to-end Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 3 — Sizing Strategy A + preview parity + HTML literal fix

Wave 3: Section + Container + Block alignment (Webflow/Wix Studio pattern) W3-A — Container width tokens + sizing resolver - theme/container-widths.ts: sm(720)/md(960)/lg(1280)/xl(1440)/full/custom - services/sizing-resolver.ts: pure fn block.style → {section, container} CSS W3-B — Wrappers - components/canvas/section-wrapper.tsx: outer fullBleed/constrained - components/canvas/container-wrapper.tsx: inner maxWidth + alignment + width - data-block-section / data-block-container attrs para tests W3-C — Wire-up canvas + renderer - sortable-block-item.tsx: <SectionWrapper><ContainerWrapper> envuelven Component - landing-block-renderer.tsx: removido max-w-5xl hardcoded; cada bloque controla su sizing - components/index.ts: barrel exports W3 wrappers + types W3-D — Preview parity (Bug 1 fix) - preview/page.tsx: refactor a <LandingBlockRenderer> oficial → aplica pageBackground, design tokens, MotionProvider, SectionWrapper/ContainerWrapper - Polling 1s backup + storage events + migrateBlockTree para trees pre-Wave-1 Bug fix — HTML literal en read-only render - components/inline/rendered-inline-string.tsx: helper detecta HTML inline (regex /<\/?[a-z][\s\S]*?>/i) y delega a <ReadOnlyRender> con dangerouslySetInnerHTML correcto + bindings resolution - heading-block.tsx: 3 branches (text/accentText/subtitle) usan RenderedInlineString → resuelve `<p style="text-align: left;">Test Heading</p>` que se mostraba escapado Validation - typecheck: 0 errores - vitest: 296/296 - perf: 6/6 invariants OK - a11y: 3 pre-existing only (no nuevos) - e2e Playwright: inspector accent-500 → canvas rgb(20,184,166) immediate → preview tab rgb(20,184,166) via polling. SectionWrapper/ContainerWrapper presentes en preview matching canvas. textContent: "Test Heading" con HTML inner correcto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 2 chunk D — wireado 26 bloques + UniversalInspector + BlockInspectorStub borrado

Cierre de Wave 2. Cada bloque pre-existente migra de `Inspector?: ComponentType` deprecated a `inspector: { sections: InspectorSectionId[], customPanel? }` declarativo. El componente block-inspector despacha a UniversalInspector (PRP-052 Wave 2 chunk C) que renderiza acordeón con CollapsibleSection + sections universales del registry. Bloques wireados (26): - 14 page-blocks: hero, footer, spacer, separator, heading, paragraph, cta, image, video, form, testimonial, faq, benefits, slider, popup-trigger, product-gallery (sections per PRP-052 spec) - 9 form-fields: short-text, long-text, email, phone, date, single-choice, multi-choice, scale, file-upload (['typography', 'spacing', 'border']) - header: customPanel = HeaderInspector existente, sections: [] (preservado) - 3 ya wireados en chunk C: stats, pricing, lead-magnet, collection (los 4 stubs nuevos) Limpieza definitiva: - BlockInspectorStub function eliminada de blocks/_shared/block-placeholder (reemplazada por comentario explicativo de Wave 2) - 23 funciones XInspector huérfanas eliminadas de page-blocks/ y form-blocks/ (cada una solo retornaba <BlockInspectorStub /> — sin consumidores tras el wiring) - Imports `BlockInspectorStub` removidos de 23 archivos - header-inspector.tsx JSDoc actualizado (referencia al UniversalInspector) - scripts/check-builder-perf-budget.ts: path actualizado a `components/inspector/inspector-collapsible.tsx` (CollapsibleSection vive ahora en path compartido tras la partición de inspector-primitives) Wiring del block-inspector: - chrome/block-inspector.tsx ahora despacha: 1. customPanel si declarado (Header v2, custom-code) 2. UniversalInspector con sections cuando inspector está declarado 3. Inspector legacy como fallback deprecated 4. GenericBlockEditor como último recurso Audits cero: - grep BlockInspectorStub src/ → 0 referencias en código (solo JSDocs) - grep "Editing panel available" src/ messages/ → 0 referencias - grep <input type="color"> en archivos Wave 2 → 0 (todo usa InlineColorPicker oficial con tokens) Validación e2e Playwright: - Heading seleccionado → UniversalInspector renderiza 4 sections reales (Typography, Spacing, Animation, Visibility) con CollapsibleSection acordeón - Sidebar Content tab muestra los 4 stubs nuevos visibles (Stats, Pricing, Lead Magnet, Collection) - Header preserva su HeaderInspector custom (customPanel) Validaciones automáticas: - typecheck 0 errores - vitest 296/296 verdes - i18n parity 100% × 3 locales - check:builder-bundle 113 rutas, 0 leaks - check:builder-perf 6/6 invariantes OK - check:builder-a11y 3 violations pre-existentes idénticas, 0 nuevas Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 2 chunk C — UniversalInspector + 4 stubs

UniversalInspector componible: - universal-inspector.tsx (110 LOC): lee BlockDefinition.inspector.sections y renderiza acordeón con CollapsibleSection. Honra customPanel ARRIBA de las sections universales. Backward-compat con `Inspector?` deprecated. - inspector-section-registry.tsx (104 LOC): mapping InspectorSectionId → ComponentType + BackgroundSectionAdapter que traduce API page-level (value/onChange) a block-level (block.style.container.background). 4 stubs nuevos registrados (21 bloques totales): - stats-block (134 LOC): KPI grid 2-4 cols con InlineEditableText (PRP-049) en value/label + i18n - pricing-block (170 LOC): 3 tiers con price/period/features/CTA, highlighted variant, 3 tiers default (Starter/Pro/Enterprise) - lead-magnet-block (148 LOC): card con headline/desc/thumbnail/CTA + 3 layouts (inline/card-left/card-right) + 3 inlineEditableFields wired - collection-block (84 LOC): grid/list/carousel placeholder con source enum (lead-magnets/symbols/recent-content/custom) Cada stub: - Component real con UI funcional (no placeholder vacío) - Zod propsSchema con campos opcionales (resilient a tree con datos parciales) - defaultProps con copy demo profesional - inspector.sections declarativo (sin BlockInspectorStub) - i18n trilingue para name/description Validaciones: - typecheck 0 errores - vitest 296/296 verdes - i18n parity 100% × 3 locales - a11y 3 violations pre-existentes idénticas, 0 nuevas Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 2 chunk B — pickers oficiales + 4 sections (border/shadow/visibility)

Refactor para que TODA la herramienta use InlineColorPicker oficial (con tokens del proyecto + alpha + RGB + HSV + hex 6/8 dígitos): - BorderCompoundPopover: input nativo type="color" → trigger del InlineColorPicker (prop opcional `tokens?: DesignTokens`). Beneficio propaga al Header inspector y futuros consumers. - ShadowSelector (CustomShadowEditor): mismo refactor. Color del shadow ahora soporta transparencia. Sections nuevas (todas <500 LOC): - border-section (128 LOC): wraps BorderCompoundPopover + radius per-corner (TL/TR/BL/BR) - shadow-section (50 LOC): wraps ShadowSelector - visibility-section (240 LOC): orquestador con add/remove de los 10 tipos de regla, despachador con type-discriminated union - visibility-rule-editors (448 LOC): 10 sub-editores especializados (BreakpointRuleEditor, AuthRuleEditor, ScheduleRuleEditor, GeoRuleEditor, UtmRuleEditor, TemperamentRuleEditor, FunnelStageRuleEditor, VisitCountRuleEditor, ReferralRuleEditor, TimeOnPageRuleEditor) + helper ChipsInput reutilizable i18n cleanup: - Eliminada key huérfana BuilderKit.styleToolbar.container.border.colorHexLabel (× 3 locales) — quedó muerta tras refactor BorderCompoundPopover - Añadida BuilderKit.shadow.description (× 3 locales) Validaciones: - typecheck 0 errores - vitest 296/296 verdes (incluye 55 visibility-resolver) - i18n parity 100% × 3 locales - a11y 3 violations pre-existentes idénticas, 0 nuevas Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 2 chunk A — types + 6 sections + primitives compartidas

Punto seguro intermedio. Foundations de Wave 2 completas (types + sections delegadas/wrapper + visibility-resolver + i18n). Pendiente chunks B-D (border/shadow/visibility-section UI + UniversalInspector + 4 stubs + wireado de 27 bloques + BlockInspectorStub borrado + Playwright e2e). Types extendidos: - style-types.ts: VisibilityRule (10 tipos: 5 universales + 5 NWM específicos) + VisibilityConfig - block-types.ts: BlockStyle.visibility?: VisibilityConfig + Zod schema - registry-types.ts: InspectorSectionId union (background/sizing/spacing/ typography/border/shadow/animation/targeting/ab/visibility) + nueva shape `BlockDefinition.inspector?: { sections, customPanel? }` Sections universales (3 wrapper + 3 nuevas, todas <500 LOC): - ab-section: delega a InspectorABVariants del Header (no duplica) - animation-section: usa AnimationPresetPicker existente - targeting-section: delega a InspectorTargeting del Header - sizing-section: Section width + Container max-width + Block alignment + Block width (per-breakpoint ready) - spacing-section: BoxSides editor con lock corners + token shortcuts - typography-section: integra font-family-select (PRP-049) + InlineColorPicker oficial para text color (con tokens cuando aplica) Inspector primitives compartidas (movidas del Header): - Particionado en 4 archivos <500 LOC para respetar Business OS regla 2: inspector-i18n-context.tsx, inspector-form-fields.tsx, inspector-collapsible.tsx, inspector-primitives.tsx (barrel re-export) - Header inspector preserva sus 50+ imports vía shim re-export - Cualquier section nueva o customPanel los consume directamente Visibility-resolver: - 309 LOC de lógica pura (10 evaluators + AND lógico) - 55 tests vitest verdes cubriendo cada tipo + edge cases i18n trilingue: - BuilderKit.visibility.* (63 keys × 3 locales) - BuilderKit.inspector.sections.*, sizing.*, spacing.*, typography.* - BuilderKit.stats.* (18), pricing.* (24), leadMagnet.* (20), collection.* (17) — preparados para los stubs del chunk C - check-builder-kit-i18n-parity extendido Validaciones: - typecheck 0 errores - vitest 720/720 verdes (55 nuevos visibility-resolver) - i18n parity 100% × 3 locales - a11y 3 violations pre-existentes idénticas, 0 nuevas Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-052 W1 hotfix — color pickers oficiales + canvas page bg

Sustituye <input type="color"> nativo (sin alpha) por el InlineColorPicker oficial en TODA la herramienta. Extiende el picker con tab "Tema" opcional que muestra tokens del proyecto (palette primary/accent/neutral × 9 steps cada una). El alpha vive en el tab "Personalizado" del picker oficial (HSV + alpha slider + RGB + hex 6/8 dígitos). Hotfix W1 archivos: - color-mode, overlay-control, gradient-builder, pattern-library, palette-editor, shadow-scale-editor → todos usan InlineColorPicker - background-section propaga `tokens` a GradientBuilder + OverlayControl - inline-color-picker extendido con prop `tokens?: DesignTokens` + i18n trilingue (tabTheme + palette.{primary,accent,neutral}) Canvas page background tiempo real: - block-canvas + block-canvas-dnd ahora leen `blockTree.theme` y aplican `pageBackground` + compilan `designTokens` a CSS vars inline. Paridad funcional con landing-block-renderer público (incluye video element separado y overlay layer cuando aplica). Validación e2e Playwright (DT/TB/MB): - Color mode swatch token → bg cambia visualmente - Gradient multi-stop + ángulo → preview + canvas - Pattern SVG inline → bg - PaletteEditor cambia primary-500 (HSV) → CSS var --theme-color-primary-500 reflejada en el canvas en tiempo real - Overlay con alpha (#RRGGBBAA 50%) → overlay layer rgba(...) - Mobile preview → canvas 375px preservando bg + overlay Validaciones automáticas: typecheck 0 errores, vitest 241/241, i18n parity 100% × 3 locales, a11y 3 violations pre-existentes (false positives JSDoc), 0 nuevas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-052 Wave 1 — Page Background + Design Tokens

W1-A..J completos en una sola wave atómica: Tipos: - DesignTokens system (palette/typeScale/spacingScale/radiusScale/shadowScale) - ContainerBackground extendido: color/gradient/image/video/pattern con GradientConfig multi-stop, BackgroundImageConfig + FocalPoint, BackgroundVideoConfig, PatternConfig, overlay y blur estructurados - BlockTreeTheme con pageBackground + designTokens + Zod schema - CURRENT_BLOCK_TREE_VERSION = 2 Services (puros, < 500 LOC c/u): - design-tokens-compiler: 67 CSS vars con merge/spread/idempotencia - pattern-svg-library: 12 patterns SVG inline data-URI (≤2KB c/u) - page-background-resolver: 5 modes + overlay layered + blur filter - block-tree-utils: migrateBlockTreeV1ToV2 idempotente, createEmptyBlockTree produce v2 con defaults Appros (orange/teal/zinc) - preset-cascade-resolver: resolveCssVariables retro-compatible con segundo arg `tokens` opcional UI Background (en components/inspector-sections/ + components/background/): - BackgroundSection raíz con switcher de 5 modes - color-mode (token swatches × 3 palettes + custom hex) - gradient-builder (multi-stop draggable + ángulo + tipo + preview) - pattern-library (12 patterns con thumbnails + size/opacity/color) - image-mode (upload + URL + size/repeat/attachment) - focal-point-picker (drag/keyboard/aria-label) - video-mode (upload/URL + autoplay/loop/muted/poster) - overlay-control (alpha + blur slider) UI Tokens (components/tokens/): - theme-tokens-panel (5 tabs internos) - palette-editor + type-scale-editor + spacing-scale-editor + radius-scale-editor + shadow-scale-editor - token-picker reutilizable (color/spacing/radius/shadow) Inspector integration: - page-inspector-panel reemplaza el empty state del inspector cuando no hay block seleccionado: tabs Background + Theme tokens - block-inspector + inspector-shell extendidos con blockTree/onUpdateTree - builder-playground propaga estado de tree al PageInspector Renderer: - landing-block-renderer aplica pageBackground inline + video element separado + overlay layer cuando aplica i18n trilingüe (114 keys × 3 locales): - BuilderKit.background (55), BuilderKit.tokens (55), BuilderKit.pageInspector (4) - check-builder-kit-i18n-parity extendido a los 3 namespaces Tests: - 46 vitest tests verdes en 4 archivos: design-tokens-compiler / pattern-svg-library / page-background-resolver / block-tree-migrator KB Belenia regenerada (51 sections, 388 exports). Validaciones: - typecheck 0 errores - vitest 241/241 verdes - check:builder-i18n verde - check:builder-bundle 113 rutas, 0 leaks - check:builder-perf 6/6 invariantes OK - check:builder-a11y 3 violations preexistentes idénticas, 0 nuevas - Playwright e2e: PageInspectorPanel renderiza con tabs Background/Theme tokens, palette tokens correctos (Primary #F97316, Accent #14B8A6, Neutral zinc) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

inline toolbar font family + weight (38 fuentes curadas)

Cierra el último gap del PRP-049/PRP-048-Hardening Fase 10 sobre el inline editor: faltaba la opción de elegir fuente y peso al texto seleccionado al estilo Word. Implementación all-in con 38 fuentes en 7 categorías. Cambios: - src/shared/builder-kit/theme/font-catalog.ts (NEW): 38 fuentes en 7 categorías (sans-modern, sans-humanist, serif-classic, serif-modern, display, mono, script). Cada entrada lista weights soportados — el peso dropdown filtra dinámicamente por la fuente activa. - src/app/layout.tsx: carga las 38 fuentes vía next/font/google con weights por fuente y display:'swap'. Variables CSS expuestas a Tailwind. - src/shared/builder-kit/components/inline/extensions/font-weight-extension.ts (NEW): Tiptap mark extension para fontWeight (Tiptap solo trae Bold=700 toggle, no rango 100-900). Persiste como style="font-weight: N". - src/shared/builder-kit/components/inline/inline-font-dropdowns.tsx (NEW): InlineFontFamilyDropdown (38 fuentes con buscador + headers de categoría) e InlineFontWeightDropdown (filtra por fuente activa). Auto-select-all cuando no hay selección — match con UX de Word/Docs. - src/shared/builder-kit/hooks/use-tiptap-editor.ts: registra TextStyle + FontFamily + FontWeightMark. - src/shared/builder-kit/components/inline/inline-text-toolbar.tsx: 2 nuevos botones (T▾ Font, W▾ Weight) entre transform y color. closeAllMenus() unifica cierre exclusivo entre dropdowns. - src/shared/builder-kit/components/inline/inline-style-toolbar-tabs/font-family-select.tsx: inspector ahora ofrece las 38 fuentes agrupadas con <optgroup>. - src/shared/builder-kit/types/style-types.ts: TypographyFontFamily derivado de FONT_CATALOG (single source of truth). - messages/{es,en,pt}.json: 14 keys nuevas en BuilderKit.toolbar.fontFamily.* + BuilderKit.toolbar.fontWeight.* (parity 74 keys × 3 locales). - src/features/mentor/services/belenia-builder-kb.ts: KB regenerada. - package.json: @tiptap/extension-text-style@^2.27.2 + @tiptap/extension-font-family@^2.27.2. Validación: - typecheck 0 errores - check:builder-i18n: 74 keys BuilderKit.toolbar.* en 3 locales - check:builder-bundle: 113 rutas, 0 leaks - check:builder-perf: 6/6 invariantes OK - Playwright: doble-click en Tagline → Font dropdown lista 38 fuentes en 7 categorías → click "Playfair Display" → <span style="font-family: &quot;Playfair Display&quot;"> aplicado. Click Weight → "Available in Playfair Display" + 6 pesos filtrados (no 100/200/300) → click "Bold 700" → marks fusionados <span style="font-family: ...; font-weight: 700">. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

toolbar text-align + dropdown text-transform

Inline editor toolbar (Tiptap) extendido con: ## Text alignment (4 botones) - Left / Center / Right / Justify - Usa @tiptap/extension-text-align (ya en deps) configurado para paragraph + heading - Persiste como inline style="text-align: <value>" en el HTML - Round-trips a través del commitDraft cuando innerHtml tiene marks - Iconos SVG inline (4 líneas variadas según alineación) ## Text-transform dropdown (5 opciones, FORZADO no CSS) - Tipo oración / minúscula / MAYÚSCULAS / Title Case / Toggle Case - A diferencia de CSS text-transform, REEMPLAZA el texto persistido (los caracteres se guardan ya con la capitalización aplicada) - Locale-aware: respeta acentos en ES/PT (Á→á, ñ→Ñ, etc.) - Helper puro `transformText(text, kind)` con 16 tests vitest - Dropdown con role=menu accesible por keyboard ## i18n trilingue - BuilderKit.toolbar.align.{left,center,right,justify} - BuilderKit.toolbar.transform.{label,sentence,lower,upper,title,toggle} - ES/EN/PT con string nativo (ej. "Tipo oración" / "Sentence case." / "Tipo oração.") ## Validación Playwright MCP - Doble-click tagline → toolbar visible con 9 botones nuevos - "Próximo lanzamiento en" + UPPERCASE → "PRÓXIMO LANZAMIENTO EN" (preserva acento Ó) ✅ - Click Align center → <p style="text-align: center;">...</p> ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

popovers draggables + sidebar/inspector colapsables

Mejoras UX del playground para que los popovers no tapen lo que se edita y para tener canvas full-width cuando se necesita: ## Popovers draggables (mouse + keyboard) - Hook useDraggablePopover: drag handle con cursor:move, mousedown/move/up global listeners, keyboard arrows (8px step), Home key reset - Header de cada popover ahora tiene grip icon + zona draggable bg-white/5 - Aplicado a: BindingPickerPopover, SaveAsSymbol popover, BindingComposer FallbackForm popover - aria-label trilingüe "Mover ventana (arrastra o usa flechas del teclado)" ## Canvas full-width - BlockSidebar colapsable (toggle button «) - InspectorShell colapsable (toggle button ›) - Cuando colapsado: thin rail de 28px con label vertical "Bloques ›" / "‹ Inspector" para re-expandir - Estado independiente por panel — owner puede colapsar uno, dos o ninguno según necesite ## Validación con Playwright MCP - Collapse → expandSidebar/expandInspector buttons aparecen ✅ - Expand → BlockSidebar / InspectorShell vuelven al layout original ✅ - Drag keyboard arrows: 5x ArrowRight + 5x ArrowDown → popover se mueve exactamente +40px X / +40px Y (8px/step verificado) ✅ - aria-label drag handle = "Move window (drag or use keyboard arrows)" ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

BindingComposer UI + fallback inline + 4 a11y/UX fixes

Sistema completo de variables dinámicas con UX top-tier: ## BindingComposer (panel UI estructurado) - Chips editables: texto + variable + fallback inline - Popover anchored para editar fallback de cada variable - Botón "+ Add variable" abre el picker estándar - Trash button por variable elimina sin tocar texto adyacente - Round-trip seguro: parseSegments(joinSegments(s)) === s - 24 tests del segmenter (parser/joiner) — todos verdes - Integrado en inspector-logo (text + tagline) — montaje condicional cuando hasAnyBinding(value) ## Fallback inline {{path|fallback:texto}} - Sintaxis nueva en BINDING_TEMPLATE_REGEX - Resolver: prioridad inline > descriptor.fallback > '' - Picker tiene paso 2 ("Texto alternativo") tras seleccionar variable - 8 tests nuevos del resolver — todos verdes ## Toggle preview con datos fake (3 modos) - Editar / Datos completos / Sin datos - BindingContextProvider con forceRuntime=true - Fixtures lead_complete (María García/Colombia/visionaria) + lead_anonymous para validar fallbacks visualmente ## Fixes de UX - BindingPickerPopover: modal centered → popover anchored bottom-start con bg sólido (no transparente) - SaveAsSymbol dialog: idem (popover anchored bottom-end) - Inline toolbar: useRef para anchorRef + insertContent text node para preservar {{}} literal - Loop infinito useEffect [refs] → arreglado con [anchorRef, open] ## Fix root cause Tiptap (bug 6h del usuario) - commitDraft persistía editor.getText() que colapsaba {{}} en algunos edge cases de single-line + insertContent - persistAsHtml = hasMarks || innerHtmlHasBindings → cuando hay bindings, persiste innerHtml (HTML sin <p>) que preserva las llaves literal - Inline editor Tiptap (B/I/U/AA/Link/AI/{}) habilitado para TODOS los campos siempre — sin forceFallback agresivo ## Validación end-to-end con Playwright MCP - Persistencia binding tras commit Tiptap ✅ - Toggle Editar → "[Lead country]" placeholder ✅ - Toggle Datos completos → "Pronto estaremos en Colombia" ✅ - Toggle Sin datos → "Pronto estaremos en tu país" (fallback inline) ✅ - BindingComposer: edit chip texto, edit fallback popover, remove via trash — todos preservan estructura ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

hydration mismatch en /dev/builder-playground

El render usaba useMemo + typeof window para leer localStorage: server retornaba un tree fresh con timestamps nuevos cada SSR, client retornaba el tree persistido — mismatch garantizado. Sentry registraba 37+22 eventos en 24h: "Hydration Error" y "Hydration failed because the server rendered HTML didn't match the client" en /pt/dev/builder-playground. Cambio: - Default export ahora renderiza un loading div estático en SSR e hidratado idéntico en client. - useEffect lee localStorage post-hydration y dispara setState, que monta <BuilderPlaygroundHydrated> con el tree real. - Server y client primer paint coinciden → no más mismatch. Bug vivo desde PRP-039 Fase 16 (creación del playground). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

Sentry widget filtro 7d → 14d

La API de Sentry rechaza statsPeriod=7d con 400: "Invalid stats_period. Valid choices are '', '24h', and '14d'" (retención del plan actual). El widget mostraba el filtro pero fallaba al consultar. Cambios: - sentry-issues-widget.tsx: dropdown 1|7 → 1|14, label rangeLast14d. - api/admin/sentry-issues/route.ts: PERIOD_MAP reducido a {1,14}. Comentario actualizado: si suben de plan, agregar 7d/30d/90d. - messages/{es,en,pt}.json: nueva key rangeLast14d. Bug vivo desde PRP-045-Hardening Fase O. Ahora el filtro carga los issues de los últimos 14 días sin error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

proxy matcher atrapaba /roadmap como short-link

La `r` suelta en la negative lookahead del matcher excluía cualquier path que empezara con `r` (incluyendo `/roadmap`), causando que next-intl middleware no se ejecutara y el locale default (es) devolviera 404. `/en/roadmap` y `/pt/roadmap` funcionaban porque tienen locale prefix obligatorio. Cambio a `r/` para limitar al patrón real `/r/<short-code>`. Verificado: /roadmap ahora 200, /r/<code> sigue redirigiendo. Bug vivo desde 3bdd73a (PRP-046 Top Tier original). PRP-046 Fase 13 (Validación Final E2E): - typecheck clean - build clean (113 routes) - 6/6 launch routes 200 (waitlist, beta, docs, roadmap, changelog, press) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 10 cierre 100% literal — Opción C

- 18 specs Playwright con flows owner-mediated playground-side (40/40 verde) - 4 violations a11y arregladas en código (region/label/nested-interactive/target-size) - <html lang="es"> en root layout - <main aria-label> en BuilderShell (playground + funnel-wrapper) - <label htmlFor> programático via FieldLabelContext + useId - BlockOutline sin role=button + SelectIcon button keyboard-accessible - useSortable.attributes movido al activator (grip icon) - IconButton 24x24 → 28x28 / tooltip ? 14x14 → 24x24 (WCAG 2.2 SC 2.5.8) - role=status + aria-live=polite en save-status pill - role=textbox condicional en inline-editable-text-readonly - Solo color-contrast queda excluido en axe (limitación técnica vs CSS vars) - Perf con RAF + performance.now() en page context - Vitest 841/841 / coverage ≥90% en todas las dims (statements 95.34% / lines 96.8% / functions 98.57% / branches 90.36%) - Typecheck 0 errores / build exit 0 / 4 CI gates verdes - Helper playground con selectHeaderBlock + expandInspectorSection - INTEGRATION-MAP sección 12 + memoria proyecto + PRP marcado COMPLETO Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

close PRP-046 Fase 11 gap — welcome_tour_complete achievement

Adds gamification integration that the PRP-046 Fase 11 spec calls for ("Gamificación + skip option") but was previously not implemented. Changes: - New achievement welcome_tour_complete (onboarding category, bronze, 30 XP) - New metric welcomeTourCompleted in UserMetrics, derived from user_tour_progress.completed_tours having any recruiter:welcome:* entry - gatherMetrics queries user_tour_progress in parallel with other tables - /api/launch/tour-progress 'complete' action triggers processGamification so the achievement unlocks immediately on tour completion (skip path unchanged, preserving the original "skip ≠ XP" intent at the trigger point — eventual consistency via dashboard load is acceptable) - Compass icon registered in 5 gamification render components (achievement-card, achievement-detail-dialog, major-celebration, micro-celebration, pcc-gamification-widget) Verification: - tsc --noEmit clean (only pre-existing distribute-budget.test.ts error) - All 4 temperament tours already had 5 i18n steps × 3 locales (verified) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

contextual inspector — click sub-zona del canvas auto-expande sección del inspector

Patrón Webflow/Framer/Builder.io: el owner click-ea cualquier sub-zona del Header (announcement bar, logo, nav, ctas, social, language switcher, search, avatar menu) y el panel correspondiente del inspector se expande + scroll-into-view automático. Implementación: - InspectorTargetContext (componente liviano) expone { target, setTarget, pulse } desacoplado del builder-store porque es estado UI transient (no persiste, no entra en undo/redo). Provider auto-resetea target cuando cambia el bloque seleccionado (resetOn={selectedBlockId}). - CollapsibleSection acepta opcional `targetKey`; cuando target context matchea, setOpen(true) + scrollIntoView en RAF. Token `pulse` re-dispara scroll si el owner click-ea la misma zona dos veces. - HeaderBlock cablea onClick handler que lee `[data-inspector-target]` del DOM ancestro vía closest(). Solo activo cuando isSelected (primera selección delega al BlockCanvasDnd; segundo click ya enfoca la sub-zona). - 8 sub-componentes del Header marcan data-inspector-target: announcement, logo, nav (desktop + mobile), ctas, social, languageSearch (lang switcher + search + predictive), auth (avatar menu). - HeaderInspector pasa targetKey en sus 11 secciones colapsables. - InspectorTargetProvider cableado en playground page y funnel-page-builder-wrapper. Cero efecto en consumer pages: sin Provider el hook cae al noop state, los data-inspector-target son atributos inertes, y el handler early-returns por !isSelected. Test smoke valida shape del context (3 tests). Validation gates: typecheck verde (1 error pre-existente no relacionado), vitest 787/787 (+3 nuevos), i18n parity verde, a11y 112 archivos clean, bundle audit 0 leaks (atributos data-* no afectan grafo de imports), build exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 9 — Belenia tools + Arthur intents + KB extractor + CI gates

Cierra Fase 9 del PRP-048-Hardening con audit literal verde y cero gaps: 13 tools Belenia bajo dominio 'builder' (style.setBackground/Padding/FontFamily, layout.setDisplay, media.setImage, interaction.setHover, binding.bind, ab.duplicateAtoB/ setWeight/promoteWinner, targeting.set, symbol.save/applyInstance) registradas con Zod schemas estrictos (regex/min/max/enum). 117+ Arthur intents trilingües (≥3 patterns ES/EN/PT por tool) divididos en 4 archivos por dominio (todos <500 líneas). Knowledge base auto-generada (scripts/extract-builder-kb.ts) con auto-discovery recursivo por directorio: 44 secciones, 321 exports, cubre TODOS los componentes/ services del builder kit hardening Fases 1-9. Reemplaza la lista manual del extractor inline-edit anterior. Scripts CI extendidos: - check-builder-kit-i18n-parity.ts: 13 sub-namespaces nuevos bajo BuilderKit.builderTools.* - check-inline-edit-a11y.ts: SCAN_PATHS extendidos a chrome/symbols/bindings/custom-code/ header-inspector/announcement-bar (111 archivos vs 52 antes) - check-builder-bundle-size.ts NUEVO: doble pasada (static analysis de imports prohibidos en consumer pages + numerical informational sobre App Router shell) Total cobertura: 156 i18n keys nuevas en 3 locales, 101 tests Fase 9 (80 arthur- builder-intents + 21 builder-tools registry/Zod), 5 npm scripts (check:builder-{i18n, a11y,bundle,all} + kb:builder), tool-registry refactor para soportar barrel ESM en vitest sin caer en require(.ts) CJS. Validation gates: typecheck verde (1 error pre-existente en distribute-budget.test.ts no relacionado), vitest 784/784, i18n parity verde, a11y 111 files clean, bundle audit 113 consumer routes con 0 leaks, build exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

PRP-048-Hardening Fase 8 — cierre de 6 gaps detectados en audit literal

Audit literal de la spec PRP-048-Hardening Fase 8 detectó 6 desviaciones del literal después del commit 0c40520. Este commit las cierra al pie de la letra. GAP A — InlineEditableText para CADA frase (spec línea 764): - inspector-announcement-animation.tsx ahora recibe blockId + reemplaza TextField por InlineEditableText con fieldPath=announcementBar.phrases.{idx} - Cada row del DnD list es ahora editable con Tiptap floating toolbar (hierarchy/color/marks) en lugar de un input text plano - inspector-announcement.tsx propaga blockId al sub-inspector GAP B — Typewriter con framer-motion AnimatePresence (spec línea 771): - typewriter-anim.tsx reescrito con motion.span + AnimatePresence (popLayout) - Cada token (letra/palabra) anima con initial/animate/exit + transition - Mantiene Intl.Segmenter para graphemes (emojis/acentos/ZWJ) GAP C — Fade-cycle con AnimatePresence + opacity + layout shift (línea 773): - fade-cycle-anim.tsx reescrito con AnimatePresence mode="wait" + motion.span - initial { opacity: 0, y: 6 } → animate { opacity: 1, y: 0 } → exit { y: -6 } - Cross-fade con layout shift (no más CSS keyframes puros) GAP D — Flip-3d con motion.div + rotateX + perspective (línea 774): - flip-3d-anim.tsx reescrito con AnimatePresence mode="wait" + motion.div - rotateX -90 → 0 → 90 + perspective 600px en wrapper - preserve-3d + backfaceVisibility hidden (no más CSS animation directo) GAP E — Preview live del countdown style (spec línea 792): - inspector-countdown-config.tsx tiene ahora CountdownStylePreview - Renderiza CountdownDisplay miniatura debajo del style chip group - Fallback a "now + 2 días" cuando targetIso aún no configurado (preview siempre muestra cifras > 0 para que el owner vea el style live) - i18n key BuilderKit.header.inspector.countdown.previewLabel × 3 locales GAP F — e2e specs estrictos (no defensive `if (count > 0)`): - announcement-text-animations.spec.ts: 3 tests duros sin condicionales - preview en miniatura existe + renderiza host de animación - phrases DnD list NO tiene <input type="text"> plain (assertion: count 0) - prefers-reduced-motion: reduce → data-animation="static" - countdown-visual.spec.ts: 2 tests duros con interacción real - toggle "Activar cuenta regresiva" → preview con data-countdown-style aparece - preview muestra cifras digit (\d regex) Validation gates (todos verdes): - npm run typecheck → 0 errores en builder-kit - npx vitest run → 683/683 passed - check-builder-kit-i18n-parity → 12+1 keys countdown × 3 locales sin gaps - check-inline-edit-a11y → 52 files clean - npm run build → exit 0 (95s, 527 static pages) - Max archivo Fase 8 con gaps cerrados: 499 líneas (Business OS regla 2) Audit literal contra spec PRP-048-Hardening Fase 8 (líneas 762-812): TODOS los puntos del Objetivo y de Validación cubiertos al pie de la letra. Sin "deferred for later" ni stubs ni sustituciones. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 8 — Announcement Bar editable + 7 animations + countdown visual + Social import

Cierra Fase 8 del PRP-048-Hardening al pie de la letra: Schema (header-block-types + announcement-bar/announcement-types): - AnnouncementBarConfig.phrases[] reemplaza message:string (legacy preservado) - background?:string editable inline (override del style) - animation:AnnouncementAnimation con 7 presets + speed 1-10 + intervalMs + pauseOnHover - countdown?:CountdownConfig reemplaza urgencyTimer:ISO (legacy preservado) - announcement-migrator.ts idempotente, cableado en migrateHeaderPropsV1ToV2 Motor de animaciones (announcement-bar/animations/): - typewriter-anim.tsx (letter+word, Intl.Segmenter para emojis/acentos/ZWJ) - marquee-anim.tsx (ltr+rtl, CSS @keyframes puro on-GPU, pauseOnHover via :hover) - fade-cycle-anim.tsx (cross-fade entre frases) - flip-3d-anim.tsx (rotateX + perspective) - animation-utils.ts (applySpeed, splitGraphemes, splitWords, detectReducedMotion) - announcement-text.tsx composer con switch por preset y prefers-reduced-motion Motor de countdown (announcement-bar/countdowns/): - flip-clock.tsx (3D rotateX al cambiar cifra) - digital-countdown.tsx (LCD-style) - circular-countdown.tsx (SVG ring + progress por unidad) - minimal-countdown.tsx (texto plano "5d 3h 12m 30s") - countdown-cell.tsx con frameShape (round/square/none) - countdown-utils.ts (computeCountdownBreakdown con units accumulation, computeRestartedTarget, validateRestart) - countdown-display.tsx composer con setInterval(1000) + cleanup + expiredAction (hide/show-message/restart) Inline editing en HeaderAnnouncementBar: - InlineEditableText para phrases[0] - InlineColorPicker via swatch button → override announcementBar.background - InlineEmojiPicker via emoji button → announcementBar.emoji - AnnouncementText forzado a static en owner mode (no anima mientras edita) Inspectors (header-inspector/): - inspector-announcement-animation.tsx (NEW): preset selector + slider 1-10 + intervalMs + pauseOnHover + DnD phrases + preview en miniatura live - inspector-countdown-config.tsx (NEW): DateTimePicker targetIso + style chips + frameShape + 6 unit toggles + expiredAction + condicionales (expiredMessage / restartIntervalDays con validation) - inspector-announcement.tsx (refactor): secciones colapsables (Content/Style/Action/Countdown/Schedule/Dismissal/Targeting), DateTimePicker para schedule - inspector-social-links.tsx (NEW): botón "Importar de mi perfil" + alertdialog consent + GET /api/profile/social, DnD preservado - inspector-misc.tsx: removed dead InspectorSocial (replaced by inspector-social-links) Componentes externos integrados (sin duplicar): - DateTimePicker (mode='datetime') en 3 lugares (scheduleStart/End + targetIso) - FLAG_LANGUAGE_COMPONENTS (FlagES nuevo + FlagUS + FlagBR) reemplazan emojis Unicode en HeaderLanguageSwitcher - GET /api/profile/social devuelve solo social_*+public_email del owner i18n (BuilderKit.header.inspector.* + BuilderKit.countdown.*): - 65 keys nuevas × 3 locales (ES/EN/PT) sin gaps - Tooltips trilingües en cada control no-trivial - check-builder-kit-i18n-parity.ts extendido con BuilderKit.countdown - previewLabel + previewFallback1/2/3 para preview en miniatura Tests: - 48 unit tests nuevos (683/683 total): announcement-migrator (idempotencia + legacy → v2), animation-utils (speed mapping perceptible, graphemes con emojis/acentos/ZWJ, splitWords trailing space, shouldDegradeToStatic), countdown-utils (units accumulation con months OFF → days totales, restart idempotente, validateRestart, pad2) - 2 e2e specs Playwright: announcement-text-animations.spec.ts (7 presets data-attrs + reduced-motion emulation + speed duration), countdown-visual.spec.ts (4 styles + tick re-render) Validation gates: - npm run typecheck → 0 errores en builder-kit (solo distribute-budget pre-existing) - npx vitest run → 683/683 passed - check-builder-kit-i18n-parity → 0 missing en 17 namespaces × 3 locales - check-inline-edit-a11y → 52 files clean - npm run build → exit 0 (90s, 527 static pages) - Max archivo Fase 8: 499 líneas (Business OS regla 2) Audit literal de la spec PRP-048-Hardening Fase 8: TODOS los puntos de "Validación" cubiertos end-to-end (owner abre builder → llega al announcement bar → edita inline + en inspector → callback cambia tree → resultado visible). Cero gaps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 7 — Symbols + Bindings + Custom Code

Sistema master/instance para reutilizar bloques entre páginas, variables dinámicas con whitelist estricta y bloque custom-code XSS-safe con iframe sandbox + CSP defense-in-depth. Symbols: - SaveAsSymbolButton, SymbolLibraryDrawer, SymbolInstanceWrapper, provider - symbol-sync (applyMaster, promoteInstance, opt-out por field-path) - Persistencia via /api/builder/symbols (GET/POST/DELETE) en users.preferences.builder_symbols - Cableado en builder-playground, funnel-page-wrapper, popup-editor Bindings: - BindingPickerPopover integrado en InlineTextToolbar (botón {}) - BindingDisplay + useResolvedBindingText integrado en ReadOnlyRender (cubre los 8 bloques con texto editable) y paragraph consumer path - Whitelist estricta de paths (lead/brand/preferences/collection), sensitive paths bloqueados (password_hash, tokens) - BindingContextProvider para contexto runtime Custom Code: - Bloque registrado en categoría 'advanced' del registry - DOMPurify allowlist HTML + scope CSS automático + iframe sandbox=allow-scripts (sin allow-same-origin) + CSP meta interno - Audit panel con removals visibles para el owner - CSP headers globales en next.config.ts (frame-ancestors, object-src none) Tests: - 52 unit tests (symbol-sync 16, binding-resolver 13, sanitizer 14, +9 más) - 3 specs Playwright e2e (symbols, bindings, custom-code) sin defensive skip - 635/635 vitest passing i18n: - 41 + 53 + 34 keys × 3 locales (ES/EN/PT) con tooltips trilingües - check-builder-kit-i18n-parity extendido con 3 namespaces nuevos Refactor: - inline-editable-text.tsx partido en -readonly + -helpers (regla <500 líneas) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

add lead_magnets.category column to Supabase types

Migration 024 added the column months ago with default 'tier_2', but src/lib/supabase/types.ts was never synchronized. activate-as-lm/route.ts inserts category: 'tier_1' on UGC brief activation. Vercel builds were passing by TypeScript cache luck until a redeploy invalidated the cache and surfaced "Type '\"tier_1\"' is not assignable to type 'never'" — both overloads of .insert() rejected the call. Fix: add category: string | null to Row and category?: string | null to Insert/Update for lead_magnets in types.ts. Documented as auto-blindaje in CLAUDE.md: schema migrations must update src/lib/supabase/types.ts in the same commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 6 + cierre de gaps en Fases 1-5

Fase 6 — A/B Variants editor + Targeting matrix + Analytics audit - inspector-ab-variants.tsx (319L): tabs A/B, weight slider+barra visual, Duplicar A→B, Promover A/B, Detener test - inspector-targeting.tsx (452L): auth/locale/geo/utm/audience reusable a 4 niveles (block + nav + cta + announcement). Validación ISO 3166 + alphanum UTM + locale subset - inspector-analytics.tsx (256L): toggle, prefijo, lista 12 eventos documentados, contador en vivo cableado a Supabase realtime sobre builder_kit_runtime_events - ab-preview-context.tsx + ab-canvas-banner.tsx: el switcher A/B cambia el canvas con banner amarillo sticky aria-live="polite" - Audit emisión runtime: los 12 eventos header.* (nav_click, cta_click, search_*, lang_switch, announcement_*, mobile_menu_*, scroll_state, release_*) ahora se emiten en runtime gated por analytics.enabled - Migration 111 + API route /api/builder-analytics + Database types - 3 e2e specs (ab-variants, targeting, analytics) sin soft assertions Cierre de gaps heredados: - Fase 1: coverage block-style-resolver 100/95.95/100/100 (statements/branches/funcs/lines) — spec ≥95% cumplida - Fase 2: BUILDER_AUTOSAVE_DEBOUNCE_MS 2000→800ms (spec literal) - Fase 3: resolveInteractionStyle ahora produce hover: className variants (hover:bg-[#X], hover:scale-[1.05], hover:shadow-md) - Fase 4: bucket builder-media dedicado (migration 112), upload route con límites diferenciados 5MB image / 50MB video - Fase 5: <InspectorShell> integrado en los 3 consumers reales del builder canvas (funnel-page-builder, popup-editor, builder-playground) — ya no es dead code i18n trilingüe: 50+ keys nuevas (ES/EN/PT) con tooltips en cada control no-trivial. parity script extendido con 2 namespaces Refactor estructural (Business OS regla 2): header-block-definition.ts, header-nav-link.tsx, use-header-scroll-analytics.ts extraídos Gates verde: - typecheck 0 errores en builder-kit - vitest 583/583 - i18n parity 14 namespaces × 3 locales - a11y 50 archivos 0 violations - hardcoded strings 33 archivos 0 hits - production build exit 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 5 — Inspector contextual + collection list DnD genérico

Refactoriza el inspector hacia un patrón type-safe + reusable. La pieza clave es `<InspectorCollectionList<T>>`, una lista DnD genérica con @dnd-kit/sortable que reemplaza los 6 editores de colecciones del builder. Componentes nuevos: - `inspector-collection-list.tsx` (345 líneas, generic, type-safe) Pointer + Touch + Keyboard sensors, live region a11y, reorder + add + remove + drag handle por fila. Caller pasa `renderItem` para el cuerpo. - `inspector-collection-helpers.ts` (puro: arrayMove, commonBlockType, formatLiveAnnounce, ensureItemId). - `inspector-shell.tsx` (orquestador contextual): empty / single / multi-homogeneous / multi-heterogeneous con bulk patch a hermanos. - `inspector-shell-helpers.ts` (puro: detectHomogeneity, computeBlockPatch). - `inspector-avatar-menu.tsx` — UI nueva para items del avatar menu (antes no había forma de editarlos en el inspector). 6 colecciones cableadas al genérico: - header.navItems (top-level + recursivo) - header.ctas - header.socialLinks - header.auth.avatarMenu.items (NUEVO) - faq.items (FAQInspector real, antes era stub) - benefits.items (BenefitsInspector real, antes era stub) Schema declarativo: - `BlockDefinition.inspectorSchema?: InspectorSchemaSection[]` permite a Belenia/Arthur razonar sobre capacidades sin parsear el componente. Header / FAQ / Benefits ya declaran su schema. Tests: - vitest: 25 nuevos casos (helpers puros + computeBlockPatch + detectHomogeneity). Total: 515/515. - Playwright: 1 spec con 7 escenarios cubriendo las 6 colecciones DnD. i18n: - 4 sub-namespaces nuevos × 3 locales (ES/EN/PT): BuilderKit.inspector.{collection, faq, benefits, multiSelect} - Extensiones a header.inspector.{social, auth} para itemLabel, maxReached y avatarMenu.* keys. - Parity script enforza los 4 nuevos namespaces. Gates: - typecheck builder-kit: 0 errores - vitest: 515/515 (45 archivos) - i18n parity: 11 namespaces × 3 locales OK - a11y: 50 archivos, 0 violaciones - hardcoded strings: 25 archivos, 0 hits - production build: ✓ - file sizes: max 394 líneas (todos <500, Business OS regla 2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 4 — Media pickers (image/video/icon/emoji + eyedropper)

5 pickers especializados que cubren los media types del builder. Cada uno es un dialog modal con focus-trap, Esc/click-outside, aria-modal, i18n total ES/EN/PT y emisión de eventos analytics tipados. Componentes nuevos en src/shared/builder-kit/components/inline/media-pickers/: - eyedropper-button.tsx (139 líneas) · API nativo `EyeDropper()` (Chrome/Edge 95+); auto-hide cuando no soportado · Tooltip i18n explicativo si `showWhenUnsupported=true` · Cancela en silencio cuando el user pulsa Esc dentro del cuentagotas - inline-emoji-picker.tsx (160 líneas) · Lazy-loads emoji-picker-element web component vía `import()` dinámico (NO entra al consumer bundle hasta que se abre) · `locale` prop sincroniza categorías/búsqueda con el idioma activo · Listener `emoji-click` → callback con unicode estándar - inline-icon-picker.tsx (407 líneas) · 76 iconos Lucide curados con keywords para mejor search · Tab Custom SVG con sanitizador conservador (whitelist de tags + atributos, bloquea <script>, on*=, javascript:, data:) · Output discriminated union `{kind:'lucide',name} | {kind:'svg',svg}` - inline-video-picker.tsx (304 líneas) · Tab Embed con auto-detect de provider (YouTube/Vimeo/Loom) + preview live `<iframe>` con URL canónico construido por buildEmbedUrl · Tab Upload MP4/WebM, reusa /api/builder/media/upload · Bloquea URLs no soportados (cero `javascript:`/`data:`/hosts arbitrarios) - inline-image-picker.tsx (412 líneas) · Upload con dropzone + click; valida MIME (jpg/png/webp/avif/gif) y tamaño · Crop con react-easy-crop (lazy via `lazy()` + Suspense) · 5 aspect presets (1:1, 4:3, 16:9, 9:16, 3:4) + zoom 1-3x · Alt text obligatorio (block disabled hasta llenarlo) + loading=lazy/eager · Genera Blob recortado en cliente, sube a /api/builder/media/upload Helpers puros (testeable sin React): - svg-sanitizer.ts (188 líneas) · sanitizeSvg(raw, parser?) → string | null · Whitelist conservador (svg, g, path, circle, rect, line, polygon, defs, linearGradient, mask, use, etc.) + atributos seguros (case-insensitive) · Strip on* handlers, javascript:/data: URIs, foreignObject, scripts · extractViewBox helper - video-embed-helpers.ts (112 líneas) · detectProvider, extractVideoId, buildEmbedUrl, resolveEmbed · Cubre youtube.com/watch, youtu.be/, /shorts/, /embed/, vimeo, player.vimeo, loom.com/share, /embed - image-crop-helpers.ts (110 líneas) · cropImageBlob (canvas-based, preserva resolución, 95% quality) · createTrackedObjectUrl (auto-revoke 60s) evita memory leaks · suggestOutputMime preserva PNG/WEBP, normaliza JPEG variants Decisiones arquitectónicas: - Reusa /api/builder/media/upload existente. La ruta ya valida MIME, tamaño, auth, RLS owner-only y devuelve {url,path,mediaSource,mediaLicense}. No se crea /api/builder/upload-media duplicado. - Sin migración nueva. El bucket content-media ya tiene RLS owner-only desde mig 026 ((storage.foldername(name))[1] = auth.uid()::text). Crear builder-media paralelo sería duplicación inútil. - Lazy imports verificados en build: react-easy-crop vía lazy() + Suspense, emoji-picker-element vía import() dinámico — ninguno en bundle inicial. - Cada picker acepta blockId opcional (default 'standalone') para emitir analytics events que requieren block_id. Deps añadidas: - emoji-picker-element ^1.29.1 (web component lazy) - react-easy-crop ^5.5.7 (cropper lazy) Tests Vitest (38 nuevos): - svg-sanitizer.test.ts (12) — preserves clean SVG, strips scripts/on*/data:, preserves aria-/data-, malformed XML rejection - video-embed-helpers.test.ts (16) — detectProvider, extractVideoId, build, resolveEmbed end-to-end por provider - image-crop-helpers.test.ts (3) — suggestOutputMime - eyedropper-button.test.ts (3) — isEyedropperSupported con stub window.EyeDropper Playwright e2e specs (5 archivos, ready-to-run cuando @playwright/test esté instalado): - inline-image-picker.spec.ts — full upload+crop+alt+apply flow + MIME rejection - inline-video-picker.spec.ts — YouTube embed + provider rejection + MP4 upload - inline-icon-picker.spec.ts — Lucide search + SVG paste sanitization - inline-emoji-picker.spec.ts — lazy chunk verification + emoji-click handler - eyedropper-button.spec.ts — supported (with shim) + unsupported (hidden) i18n: BuilderKit.mediaPickers (64 keys × 3 locales) — image/video/icon/emoji/eyedropper. Cumplimiento PRP-048-Hardening Fase 4: ✓ Cada picker <500 líneas (max 412) ✓ Upload exitoso a Supabase Storage; RLS owner-only verificada ✓ Lazy import emoji-picker-element verificado (dynamic import) ✓ Eyedropper fallback i18n cuando no soportado ✓ Tests Vitest (38) + 1 Playwright e2e por picker ✓ WCAG 2.2 AA: dialog/role + aria-modal + aria-label + Esc/click-outside ✓ Cero strings hardcodeadas Gates verificadas: - typecheck: 0 errores en builder-kit - npm run build: ✓ producción - vitest: 490/490 tests, 43 files - i18n parity: 64 keys × 3 locales en BuilderKit.mediaPickers - check-inline-edit-a11y: 50 files audited, 0 violations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 3 — InlineStyleToolbar tabs Typography + Layout + Interaction

Cierra los 3 tabs restantes del <InlineStyleToolbar>. Container quedó en Fase 2; ahora el owner edita texto/layout/interacción con la misma UX flotante. Tab Typography (147 líneas): - Familia limitada al brand set (Inter, Roboto Slab, Montserrat, Space Mono, Space Grotesk) - Peso 100-900 con slider snap-step + etiqueta semántica (Thin/Light/Regular/...) - Font-size con dos modos: fixed (px/rem) y fluid (clamp min/preferred/max) - Line-height (multiplier 0.8-3, clamp), letter-spacing (em/px) - 3 toggle rows compactos: align (4), transform (4), decoration (4) con glyphs Tab Layout (159 líneas): - DisplaySelect 6 opciones (block/flex/grid/inline/inline-block/none) - FlexGridControls condicional: flex (direction/justify/align/wrap) o grid (cols/rows strings) - Gap con unit picker (visible solo cuando display=flex|grid) - PositionControls: position (5), z-index (disabled cuando static), inset (top/right/bottom/left strings) Tab Interaction (104 líneas): - HoverStateEditor: toggle on/off + sub-controles bg/color/scale/shadow (color picker + hex input) - ClickActionEditor: dropdown 5 tipos (none/url/anchor/popup/submit) + value condicional + target (_self/_blank) - AnimationPresetPicker: select 12 presets + trigger (entrance/scroll/hover) + duration/delay · Preview live con motion/react: <motion.div> remount on key change → reproduce variants hidden→visible · Replay button para forzar repetición · Respeta prefers-reduced-motion: renderiza estado final estático sin animar - CursorSelect 5 opciones (cursor real aplicado en cada radio para preview táctil) Registry: theme/animation-presets.ts (261 líneas) — 12 presets con motion Variants listas para runtime + valores default duration/delay. Easing curado (cubic-bezier sin ease-in puro). Lookup defensivo getAnimationPreset() cae a 'none' para ids desconocidos. ANIMATION_PRESET_LIST + ANIMATION_TRIGGER_LIST inmutables. Pure helpers: typography-helpers.ts — snapFontWeight, isClampFontSize, isFixedFontSize, clampLineHeight, clampLetterSpacing. i18n total ES/EN/PT: +194 keys × 3 locales bajo BuilderKit.styleToolbar.{typography, layout,interaction}. Tooltips actualizados (sin "próximamente"). 'tab.comingSoon' eliminado. Auth gating: el toolbar se monta vía <InlineStyleToolbarMount> (Fase 2) con React.lazy() + canEdit, así que consumer pages no incluyen este código. Cumplimiento PRP-048-Hardening Fase 3: ✓ 3 tab files cada uno <500 líneas (147/159/104) ✓ Animation preset preview live con motion/react + replay button ✓ Hover state expone bg/color/scale/shadow al wrapper de bloque ✓ Cero strings hardcodeadas (todo vía useTranslations) ✓ WCAG 2.2 AA: roles toolbar/tablist/tabpanel/group/radiogroup/radio, aria-label en cada control, roving tabindex en tabs, prefers-reduced-motion ✓ Tests Vitest 29 nuevos (typography-helpers 19 + animation-presets 10) verde ✓ a11y CI 42 files 0 violations Gates verificadas: - typecheck: 0 errores en builder-kit - npm run build: ✓ producción - vitest: 452/452 tests, 39 files - i18n parity: 279 keys × 3 locales en BuilderKit.styleToolbar - check-inline-edit-a11y: 42 files audited, 0 violations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-046 Wave 5 Fase C — Admin UI + cron + setup doc

Cierra Wave 5 (Fase 7 PRP-046). Cross-link entre roadmap "done" → post público en Discord para celebrar shipping. - /admin/discord: dashboard con 3 stats (miembros, invites pendientes, roles asignados) + tabla de últimos 200 miembros vinculados. - mig 110: roadmap_items.discord_announced_at + index parcial sobre status para sweep eficiente. - Cron paso 17: announceDoneRoadmapItems lee items "done" sin anunciar, postea en DISCORD_ROADMAP_CHANNEL_ID, marca discord_announced_at. - .claude/discord-setup.md: guía maestra (channels MVP + 5 reservados Marketer per INTEGRATION-MAP, 11 roles + env vars Vercel/bot, flujo end-to-end, troubleshooting). i18n DiscordAdmin namespace ya en HEAD (commit 20b30c6 lo absorbió). Pendiente externo: crear server Discord, app + bot en Developer Portal, hostear bots/discord/ en Railway, setear ~14 env vars en Vercel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-046 Wave 5 Fase B — Discord bot externo

Bot Node.js standalone (discord.js v14) para hostear en Railway/Render. Vercel no soporta procesos persistentes con WebSocket al gateway de Discord, por eso el bot vive afuera. Toda la lógica/state vive en Appros (Fase A); el bot solo ejecuta acciones en el server. bots/discord/ ├── package.json — discord.js dep, npm start ├── .env.example — DISCORD_BOT_TOKEN, APPROS_API_BASE, etc. ├── README.md — setup paso a paso (Discord Portal → Server → Railway) └── src/ ├── index.js — gateway login, invite-tracking diff-based para detectar │ qué invite usó cada nuevo miembro, welcome DM, slash /ask handler. ├── appros-api.js — helpers HMAC-firmados para POST a /api/discord/*. └── deploy-commands.js — registra /ask como guild command. Excluido de Vercel build (.vercelignore) y de typecheck (tsconfig exclude). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-046 Wave 5 Fase A — Discord backend

Backend Vercel para integración con la comunidad Discord (PRP-046 Fase 7). El bot vive externamente (Railway/Render); este commit es el lado Appros que mantiene state, genera invites únicos y proxea /ask a Belenia. - mig 109: discord_invites, discord_links, discord_role_assignments, discord_events. RLS owner-select, indices de búsqueda. - discord-service.ts: createDiscordInvite (Discord HTTP API), assignAutoRoles por temperamento/plan/tier afiliado/beta, postDiscordMessage, verifyDiscordWebhookSignature (HMAC sha256). - /api/discord/invite (auth): genera/reusa invite personal del user. - /api/discord/webhook (HMAC): recibe invite_used, guild_member_add/remove desde el bot, link + role assignment automático. - /api/discord/ask (HMAC): proxea /ask de Discord a Belenia community helper. - belenia-community-config.ts: prompt sin requerir UserContext completo (usuarios anónimos en channels públicos). Variables Vercel pendientes: DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, DISCORD_WELCOME_CHANNEL_ID, DISCORD_WEBHOOK_SECRET, DISCORD_ROLE_*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 2 — InlineStyleToolbar tab Container

Toolbar flotante para editar estilos avanzados de un bloque seleccionado: fondo (color/gradient/image/video), borde compuesto (widths + style + color), padding/margin con sync 4-lados, sombra (5 presets + custom x/y/blur/spread), border-radius por esquina, opacidad y overflow. Floating UI flip+shift, z-[60], roving tabindex, role="toolbar", WCAG 2.2 AA verde. - inline-style-toolbar.tsx (router de tabs) y tab-container.tsx (orquestador) - 7 sub-controles independientes (<200 l. cada uno) - Hook useUpdateBlockStyle (set-by-path inmutable sobre block.style) - Hook useDebouncedCallback (200ms UI, flush/cancel) - Helpers puros testables (matchShadowPreset, hexFromAny, borderSummary) - Wrapper auth-aware InlineStyleToolbarMount con lazy() + Suspense - 30 tests Vitest nuevos (433 totales OK) - 1 spec Playwright e2e listo para CI - i18n trilingüe ES/EN/PT (85 claves nuevas) + parity script extendido - a11y CI cubre 24 archivos sin violaciones Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-048-Hardening Fase 1 — responsive infra + style types base

- types/style-types.ts: ContainerStyle/TypographyStyle/LayoutStyle/InteractionStyle - types/responsive-types.ts: ResponsiveStyles<T> + helpers - types/builder-event-name.ts: 12+ sub-namespaces (style/layout/media/interaction/responsive/inspector/ab/targeting/symbol/binding/custom_code/announcement) - theme/responsive-breakpoints.ts: BreakpointId + cascade desktop-first - services/block-style-resolver.ts: 4 nuevos resolvers (container/typography/layout/interaction) - services/responsive-style-resolver.ts: merge base + sm/md/lg overrides + apply/clear helpers - services/block-style-migrator.ts: defensive shape normalization (idempotente) - components/responsive/breakpoint-badge.tsx: <BreakpointBadge> WCAG AA + i18n - i18n: BuilderKit.responsive (12 keys × ES/EN/PT, parity verde) - scripts: extender i18n parity check con namespace responsive - 47 tests Vitest nuevos (403 total verdes), build limpio, typecheck limpio en builder-kit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

UI interactiva con filtros, búsqueda y colapsar/expandir

- Tabs por categoría (Todos / Features / Mejoras / Fixes / Breaking) con contador por tab - Búsqueda full-text por título, descripción y versión - Click "Ver más / Ver menos" por entry para descripciones largas (>180 chars) - Botón "Expandir todo / Colapsar todo" general - Server component pasa entries al ChangelogContent (client) que maneja estado

Fix

public SELECT policy en changelog_entries

RLS estaba activada (probablemente desde Studio) pero sin policy de SELECT, así anon role veía 0 rows aunque la DB tuviera entries. /changelog es contenido público → policy USING (true).

Fix

force dynamic rendering en /changelog

La página estaba cacheada estáticamente desde build (DB vacía → empty state permanente). Como cron + GitHub Action publican entries dinámicamente, necesita re-renderear cada request. Agrega `dynamic='force-dynamic'` y `revalidate=0`.

Fix

cast updates as never en .update() sin client cast

Supabase v2 endureció el typecheck de .update(record) — Record<string, unknown> ya no es asignable al tipo generado de la tabla. Vercel build (next build) falla aunque tsc --noEmit pase. Aplica el cast `as never` (mismo patrón ya usado para .rpc() en CLAUDE.md) en los 5 archivos que llaman .update(updates) directamente sobre el cliente tipado. Los otros 7 sites con el patrón usan `const sb = supabase as any` localmente y no necesitan cambio. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

chrome no invasivo + marks persistentes + alto contraste

Tres bugs reportados al testear el inline-edit en blocks reales. ## #1: Pills (HEADER + iconos) invadían el contenido del bloque Antes: pills al `top-2 inside` sobre el header tapaban "Brand" y la CTA. Fix: pills al BORDE EXTERNO (`-top-3` por encima del bloque, en el gap entre bloques). Para evitar el clipping del canvas que motivó el primer movimiento "inside": - Canvas: `pt-12 pb-6` (antes `py-6`) — espacio extra arriba para el primer bloque. - SortableBlockItem: `mt-8 first:mt-0` — espacio entre bloques para que cada pill viva en el gap, no sobre el bloque anterior. - Pills siguen en `z-[60]` para superar sticky-wrappers (Header z-50). - Idle opacity 70% en lugar de 50% — más visible sin ser intrusivo. ## #2: Bold/Italic/Underline NO persistían en fields plain (CRÍTICO) Reproducible: dblclick "Productos" → Bold → Escape → "Productos" vuelve sin negrita. Color sí persiste, marks no. Causa: `commitDraft()` para campos `richText: false` usaba `editor.getText()` que strippa marks. Solo color persistía porque vive en `block.style.overrides.fields[fieldPath].color`, no en el value. Fix: detectar si el editor tiene marks (comparando inner HTML vs plainText) y persistir HTML inline (sin el wrapping `<p>` que Tiptap emite por default). Resultado para "Productos" + Bold: - Persiste: `"<strong>Productos</strong>"` (no `"<p><strong>...</strong></p>"`) - Render: `<span><strong>Productos</strong></span>` — inline-friendly, semántica correcta - `getInlinePlainText()` strippa tags HTML para SEO/analytics consumers Las marks NO se ocultaron del toolbar. Funcionan en todos los fields (plain y rich) sin perder data. ## #3: Toolbar invisible sobre fondos claros `glass` (semi-transparente blanco) se mimetizaba con headers cream/white de templates Luxury Editorial. Botones B/I/U casi invisibles. Fix: - `inline-text-toolbar`: `glass ring-white/15` → `bg-zinc-950 ring-white/20` - `inline-button` toolbar: mismo patrón - Solid dark backdrop garantiza alto contraste sobre cualquier color de bloque (cream, blanco, oscuro, gradient). ## Bonus: render-phase setState eliminado `useInlineEdit.endEdit` ya no llama `onEnd` dentro del setState updater (estaba causando "Cannot update a component while rendering" warning + edit_cancelled instantáneo después de cada commit). Ahora usa un `isEditingRef` para idempotency + llama onEnd fuera de la fase de render. ## Validación - typecheck: 0 errores - 329 vitest tests pass - Playwright end-to-end: · Pills + iconos visibles top-left/right del bloque, sin tapar contenido ✓ · Toolbar fondo oscuro sólido visible en cualquier color de bloque ✓ · dblclick "Productos" → Bold → Escape → DOM muestra `<strong>Productos</strong>` y visualmente está en negrita ✓ · Italic/Underline persisten igual ✓ · Color picker sigue funcionando como antes ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-046 Wave 4 Fase B — Webinar Infrastructure

Sistema completo de webinars: admin crea evento (provisiona Daily.co room), público se registra en /webinars/[slug], cron paso 16 envía recordatorios 24h/1h, "estamos en vivo" y replay tras finalizar (status=ended + recording_url). - mig 106: columnas tracking de emails en webinar_registrations (confirmation_sent_at, reminder_24h/1h_sent_at, live_sent_at, replay_sent_at, unsubscribed_at, name) + daily_room_url + replay_finalized_at en webinars + RLS public-select webinars + own-select registrations - webinar-service.ts: createWebinar (Daily.co), listUpcoming, getBySlug, registerForWebinar (envía confirmación), unsubscribeRegistration, processWebinarReminders (cron paso 16, ventanas 24h/1h) - webinar-emails.ts: confirmation, reminder, went-live, replay (chrome wrapper) - Endpoints admin: POST/GET /api/admin/webinars, PATCH /api/admin/webinars/[id] - Endpoints públicos: GET /api/webinars, POST /api/webinars/[slug]/register, GET /api/webinars/unsubscribe?reg=... - UI: /admin/webinars (form + acciones live/end/cancel), /webinars (lista), /webinars/[slug] (detalle + form + JSON-LD Event schema) - Cron paso 16 conectado en nurturing/cron/route.ts - Sitemap incluye /webinars + slugs por locale (es/en/pt) - i18n Webinars namespace en es/en/pt (~50 keys) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

PRP-046 Wave 4 Fase A — Changelog auto-publish

GitHub Action lee commits convencionales (feat:/fix:/perf:/refactor:/!) al merge a main y POSTea entries al endpoint /api/changelog/ingest. Filtra chore/docs/ ci/test/deps + bots. Idempotent: dedup por commit_sha (UNIQUE en mig 105). - mig 105: agrega commit_sha (UNIQUE), auto_published, pr_number a changelog_entries - /api/changelog/ingest: POST con auth Bearer CHANGELOG_INGEST_SECRET, upsert por commit_sha - .github/workflows/changelog-publish.yml: parser bash + node, posts a CHANGELOG_INGEST_URL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

edit persistence + label/icons visibility z-index race

Tres bugs reales reportados al testear: ## #3 (CRÍTICO): edits no persistían al salir del editor Reproducible: dblclick en nav, cambiar color, presionar Escape → texto revierte al original. Causa: `useInlineEdit.endEdit` llamaba `onEnd?.()` DENTRO del state updater de `setIsEditing((prev) => { if (prev) onEnd?.(); return false })`. Las funciones updater de useState son evaluadas durante la fase de render en React 18, así que el `onEnd` (que llama `commitDraft → onChange → updateField → onUpdateBlock → store.set`) terminaba mutando un componente ancestro DURANTE el render del wrapper. React detectaba esto y disparaba "Cannot update a component while rendering a different component", lo que provocaba un edit_cancelled inmediato (visible en analytics: edit_started seguido de edit_cancelled ~280ms después sin commit en medio). Fix: tracking del estado de edición via `isEditingRef`. `endEdit` ahora: - chequea ref (no state) para idempotency - llama `setIsEditing(false)` con valor literal (no función updater) - llama `onEnd?.()` FUERA del setState updater, en el flujo del event handler donde es seguro mutar parent state Verificado: dblclick "Empresa" → Color del texto → swatch pink → Escape → "Empresa" persiste en pink. Console clean sin warnings de render-phase setState. ## #1: pill "HEADER" del block-outline cortado por z-index El sticky wrapper del HeaderBlock usa `z-50` y cubría la pill (z-30) del block-outline. Visualmente: solo la mitad inferior de "HEADER" visible. Fix: bumped pill + icons a `z-[60]`. El sticky scroll comportamiento del header sigue funcionando, pero los chrome controls del builder kit ahora viven en un stacking context superior. ## #2: icons MOVER/DUPLICAR/ELIMINAR no aparecían Los iconos seguían el patrón opacity-0 + group-hover/block:opacity-100. Sin hover, invisibles → user pensaba que se habían perdido. Fix: - Pill HEADER: ahora SIEMPRE visible. Idle = `opacity-50 bg-zinc-950/80`, selected/hovered = `opacity-100 bg-teal-500/90`. - Icons (grip/copy/trash): mismo patrón. Idle = opacity-50, selected = full, hover = full. - z-[60] consistente entre ambos para que ningún sticky/fixed los cubra. UX: el usuario ve constantemente qué bloque hay y cómo manipularlo, sin necesidad de recordar dónde hover. ## Validación - typecheck: 0 errores - 329 vitest tests pass - Playwright end-to-end: · HEADER pill visible top-left, icons visible top-right ✓ · Pill + icons NO clipped por sticky wrapper z-50 ✓ · Click swatch color en nav link → Escape → color persiste ✓ · Productos naranja + SOLUCIONES_TEST + Empresa pink coexisten ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Fix

undo crash + preview overrides + hover icons + publish gating

Cuatro problemas reales reportados al usar el playground. ## #1: undo crashea con "Cannot read properties of undefined (reading 'tree')" Reproducible: drag header → click undo → app crash. Causa: zundo's internal `userSet(nextState)` (ejecutado durante undo/redo) estaba dejando `state` undefined bajo Zustand 5. Probé `handleSet` override + `partialize` removal — ninguno arregló porque el path roto está dentro del bundle de zundo y no es alcanzable vía config. Fix: bypass total del undo/redo de zundo. Nuevas funciones exportadas `undoBuilderStore(store)` / `redoBuilderStore(store)` que: - Leen `pastStates` / `futureStates` directamente del temporal store - Restauran via `store.setState({tree}, false)` (Zustand 5 explicit merge) - Manualmente shiftean los arrays de history Playground topbar las llama: `onUndo={() => undoBuilderStore(store)}`. ## #2: hover icons "detrás del header" — visualmente cortados El toolbar flotante de cada bloque (`absolute -top-3 right-3`) quedaba clipped por `overflow-y-auto` del canvas + bajo contraste con el fondo del bloque (icons casi invisibles sobre el header oscuro). Fix: re-posicionar al `top-2 right-2` (DENTRO del bloque), agruparlos en un pill horizontal con backdrop sólido `bg-zinc-950/85 backdrop-blur-md` + z-30. Iconos ahora resaltan claramente sobre cualquier contenido. ## #3: vista previa no muestra overrides de color/jerarquía Reproducible: cambiar color de "Productos" en editor → abrir preview → "Productos" sigue en color default. Causa: `BlockEditingProvider` solo proveía contexto cuando `onUpdateBlock` estaba definido. En preview no hay setter → contexto null → `InlineEditableText` no leía `block.style.overrides.fields[fieldPath]` → no aplicaba style visual. Fix: el provider ahora SIEMPRE provee `block` (para LEER overrides); solo gating la EDICIÓN via `onUpdateBlock` opcional. La wrapper usa `canApplyOverrides = !!blockCtx?.onUpdateBlock` para controlar handlers del color picker / hierarchy. Visual rendering siempre activo. Verificado: cambio de color en editor aparece inmediatamente en preview tab (storage event sync via localStorage). ## #4: botón Publicar no hace nada → confusión sobre cómo guarda El playground es sandbox local — cambios autoguardan en localStorage cada 800ms (debounce). El botón Publicar quedaba renderizado pero sin handler. Fix: - `BuilderTopbarProps.onPublish` ahora opcional. Cuando ausente, el botón se renderiza disabled con tooltip explicando: "Sin backend en este sandbox — los cambios se autoguardan en localStorage del navegador." - Nueva i18n key `BuilderKit.topbar.publishDisabledHint` (3 locales). - Playground omite `onPublish` → botón gris con hover-tooltip. ## Validación - typecheck: 0 errores nuevos (solo el pre-existing growth-autopilot) - 329 vitest tests pass - i18n parity: header 247 + inlineEdit 32 + toolbar 47 + topbar 15 keys × 3 - a11y: 13 inline-edit files clean - Playwright end-to-end: · Drag Header → click undo → no crash, undo OK ✓ · Click redo después de undo → restaura header ✓ · Aplicar template → cambiar color "Productos" → abrir /preview → Productos en naranja en preview tab ✓ · Hover icons visibles top-right de cada bloque, sin clipping ✓ · Botón Publicar gris/disabled con tooltip explicativo ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Nueva Feature

hover toolbar icons + autosave + preview window

Tres mejoras de UX al playground / canvas tras feedback de prueba real. ## 1) Hover toolbar icon-only Bug: los labels "MOVER / DUPLICAR / ELIMINAR" se cortaban por detrás del borde del bloque (text overflow al estar en `-top-3` con z-index limitado). Fix: reemplazo por SVG icons (grip 6-dots, duplicate, trash) en buttons cuadrados 7×7. Tooltips via `title=` + `aria-label=` mantienen el label accesible. Tono `danger` rosa para el trash. ## 2) Topbar: undo/redo como iconos compactos + Vista previa - `Deshacer` / `Rehacer` text labels → icon buttons compactos (8×8) con tooltip. Liberan ~150px de espacio horizontal en el topbar. - Nuevo botón Vista previa (`onPreviewLive` opcional, eye icon) ubicado antes de "Publicar". - Nueva i18n key `BuilderKit.topbar.previewLive` en ES/EN/PT. Nota UX: undo/redo siguen estando en topbar (no en hover toolbar) porque deben funcionar globalmente, incluso cuando NO hay bloque seleccionado. Cmd+Z / Cmd+Shift+Z keyboard shortcuts también disponibles. ## 3) Autosave a localStorage + Preview en pestaña nueva Antes: refresh = se perdía todo el trabajo (playground in-memory). Ahora: - `useEffect` con debounce 800ms que persiste `tree` en `localStorage` bajo `appros:builder-playground:tree:v1` - Al montar la página, restaura el tree desde localStorage si existe - Status indicator del topbar refleja `saving` / `saved` / `error` - `handlePreviewLive`: persiste sincrónicamente + abre `/preview` en nueva pestaña con `window.open(...)` Nueva ruta: `src/app/[locale]/dev/builder-playground/preview/page.tsx` - Lee tree de localStorage - Renderiza solo `def.Component` por bloque, SIN editor chrome (ni sidebar, inspector, topbar, hover toolbar) - `previewEnabled={false}` → inline-edit OFF (read-only) - Suscribe `storage` event para auto-refresh cuando el editor guarda en otra pestaña — vista previa siempre actualizada en tiempo real Disclaimer: localStorage es para el playground. El builder en producción debe persistir a Supabase via `useAutosave` (ya existente). ## i18n parity Extendido `check-builder-kit-i18n-parity.ts` para validar también `BuilderKit.topbar.*` (14 keys × 3 locales). Antes solo cubría header, inlineEdit y toolbar. ## Validación - typecheck: 0 errores - 329 vitest tests pass - i18n parity: header 247 + inlineEdit 32 + toolbar 47 + topbar 14 keys × 3 - a11y: 13 inline-edit files clean - Playwright end-to-end: · Topbar muestra icons compactos (undo/redo/eye) ✓ · Hover toolbar muestra 3 icons sin texto cortado ✓ · Color persistido entre sesiones via localStorage ✓ · Click en eye icon abre nueva pestaña en /preview ✓ · Preview renderiza el header con nav coloreado, sin chrome ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>