15 KiB
PLAN — Efectos de texto (F5 del plan Scene/Ambient, desgajado por D9)
Tipo: plan de ejecución por fases (process — efímero, no fuente de verdad). Fecha: 2026-07-12 · Estado: PLAN COMPLETO — D-T1…D-T4 resueltas · FT0–FT7 EJECUTADAS (2026-07-12). FT7: 6 demos en
web/routes/uix/components/{kebab}/(frame de 6 pestañas de los pasivos shipped — patrón box/format-number; cada prop pública = control vivo; snippet con paridad; sema empty-state justificado; CountUp condemoLocalepropio por chips — convención service-demos). Matriz 141/0/0 con Demo ✓ y 0 warns en los 6 · check 0 propios · docs:check 0/0 · contracts 38/38. Verificado en navegador: 6 rutas SSR 200 + hidratadas; CountUp formatea por locale del ecosistema EN VIVO (0,00es-ES → chip en-US →0.00); TextGradient resuelve tokens→OKLCH del tema víavar()conbackground-clip:text+ animación activa; TextCircularrole=img+33 chars posicionados; TextFocus SIN botones falsos dentro y el auto-avance detimers.intervalCORRE (palabra activa cambia — los timers no son rAF); TextBlur segmentosaria-hidden+ SR completo + estado inicial sin flash; TextScramble SR surface correcta. Cero errores de consola. Los caminos rAF/IO-dependientes (count visual, flicker, marco) quedan bajo el pendiente visual de pestaña visible ya conocido (pane oculto suspende rAF/IO/measure). Ajuste de tipos post-demo: los 5 visuales aceptanstyle(fijar tokens por instancia); text-gradient fusiona su style interno con el del consumidor. FT0–FT6: los 6 componentes construidos y verdes en toda la maquinaria — matriz component-audit 141/0/0 (los 6 PASS con README F-1 completo: Baseline/Comparativa/Gaps/Passive justification) · contracts 38/38 (enumeración morfo + scope) · check 0 errores propios.CountUp(servicio patrón FormatNumber: morfo 1-part,uix.format.numbersreactivo cada frame con fallback Intl, spring DHO analítico — el de$motiones element/CSS-bound, anotado —, IO/rAF víaeidos.dom, timers víaeidos.timers;separatordel seed NO portado: el locale posee los separadores) ·TextGradient(receta CSS + tokens públicos, stopsvar()-interpolados +preset→--gradient-{name}S9, borde interior#000→--color-surface-defaulttoken, keyframes R-4.5 anotados,margin:0 autodel seed fuera) ·TextCircular(rAF vía dom, reduce-motion AÑADIDO — el seed no paraba nunca —,currentColordefault, nota Aura en morfo/README) ·TextBlur(SR hardening: texto real visually-hidden + segmentos aria-hidden; WAAPI directa = primitivo del driver waapi de $motion, migrará al dominio content cuando el RFC aterrice; IO vía dom) ·TextFocus(a11y fix: fuera losrole="button"+tabindex falsos del seed;timers.interval+dom.measure+observeResize; tokens border/glow; reduce = marco quieto; 1!importantanotado R-4.7) ·TextScramble(rediseño de medición: cache de centros por carácter víadom.measureinvalidado por RO — el seed medía N rects POR pointermove; 1 lectura por move; SR hardening;text: stringen vez de children — el split solo es honesto con texto plano, declarado). Extensión de vocabulario:MorfoElement+'p'(primer párrafo del catálogo). Origen:docs/process/PLAN-scene-ambient-pack.mdD9 — los 6 efectos de texto deweb/routes/demos/animations/text/NO van al pack Ambient: envuelven contenido real → superficie a11y = superficie de contrato → canon, con destinos heterogéneos. La colección seed queda intacta como referencia comparativa (D5/D7).
Los 6 seeds, analizados
| Seed | Sustrato | Qué hace | Interacción | A11y del seed | Ciudadanía a corregir |
|---|---|---|---|---|---|
count (Count Up) |
rAF spring analítico | Cuenta from→to al entrar en viewport | — | ✓ texto real (números) | Intl.NumberFormat('en-US') hardcoded → debe componer uix.format.numbers (locale-aware); setTimeout→uix.timers; IO→$adom; reduce ✓ ya salta al final |
gradient (Gradient Text) |
CSS puro (background-clip:text + keyframes) |
Gradiente animado sobre el texto (+ borde opcional) | pause-on-hover | ✓ texto real intacto | Colores crudos→tokens; #000 del borde interior = phantom→token de superficie; @keyframes→R-4.5 (anotación functional / registro motion); reduce ✓ ya |
blur (Blur Text) |
WAAPI por segmento | Entrada staggered blur→nítido por palabras/letras al entrar en viewport | — | ⚠️ trocea en spans (por letras fragmenta lectura SR) | SR hardening: aria-label en contenedor + segmentos aria-hidden; WAAPI/stagger vía dominio content de $motion; IO→$adom; reduce ✓ ya |
circular (Circular Text) |
rAF + transforms | Caracteres en círculo girando; hover cambia velocidad | hover (slowDown/speedUp/pause/goBonkers) | ✓ role="img" + aria-label (buena base) |
rAF→dom.requestFrame; escrituras transform→dom.apply; colores/tamaños crudos→tokens. Nota Aura: texto orbital alrededor de un shape redondo = pieza candidata de la periferia del indicador de agente |
focus (True Focus) |
CSS transitions + JS | Palabras difuminadas salvo la activa; marco de esquinas viaja (auto o hover) | hover en modo manual | ⚠️ role="button" + tabindex=0 en CADA palabra sin acción real = anti-patrón |
Quitar role=button (o darle semántica real); setInterval→uix.timers; getBoundingClientRect→**dom.measure** (regla dura de layout reads); RO→dom.observe; border/glow crudos→tokens; reduce ✓ ya |
scrambled (Scrambled Text) |
rAF + DOM textContent | Scramble de caracteres cerca del puntero | pointermove | ⚠️ SR puede leer los caracteres scrambled; split destruye el HTML original | SR hardening (aria-label + chars aria-hidden); reflow storm: mide getBoundingClientRect POR CARÁCTER en CADA pointermove → rediseño obligado: cache de rects vía dom.measure invalidado por RO; rAF→dom.requestFrame; pointer→dom.listen |
Precedente real del canon para servicios: formatNumberMorfo — morfo de 1 part,
scope: ['eidos'], sin CSS/eventos («grep-friendly typed wrapper»,
src/uix/morfo/components/format-number.ts). Confirma que morfo-first aplica
incluso a componentes de servicio, con coste mínimo. Los efectos con interacción
(circular/focus/scrambled) añaden data-attrs de estado y, si procede, eventos sema.
Decisiones (gate del usuario)
| ID | Cuestión | Opciones analizadas | Recomendación |
|---|---|---|---|
| D-T1 | Arquitectura por efecto | (a) canon completo ×6 (ruta 9 fases con soma+sema cada uno) · (b) familia "text effects" = wrappers eidos con morfo mínimo · (c) mixto: cada uno a su hogar natural — count=service component (patrón FormatNumber); gradient=eidos CSS con morfo mínimo; blur/circular/focus/scrambled=eidos decorativos con morfo (parts+data-attrs de estado) sin soma provider (no hay máquina de estados accesible que justifique soma; la interacción es decorativa) | (c) — (a) infla 4 componentes decorativos con capas vacías; (b) niega a count su hogar de servicio |
| D-T2 | count: ¿componente nuevo o prop de FormatNumber? | (a) <CountUp> hermano de FormatNumber que compone el MISMO formatter (uix.format.numbers) · (b) prop animate en FormatNumber |
(a) — FormatNumber es puro/inmediato; animar es otra responsabilidad. Ambos comparten formatter (sin duplicar lógica de locale) |
| D-T3 | gradient: ¿API de colores cruda o sistema de gradientes del theming (S9)? | (a) colors: string[] con tokens resolubles (API del seed, theming vía tokens) · (b) integrar el vocabulario de gradientes S9 (presets canónicos) además de colores libres |
(b) con (a) dentro: acepta stops libres (tokens o hex) Y presets del sistema de gradientes — canon presentado como opción, sin recortar la API del seed |
| D-T4 | Naming de la familia | (a) prefijo Text*: TextBlur / TextCircular / TextFocus / TextGradient / TextScramble + CountUp aparte (servicio) · (b) nombres del seed (BlurText, TrueFocus…) · (c) compound Text.Blur |
(a) — agrupa en el catálogo junto al primitivo Text, evita el genérico "True/Up" del seed; count no es de la familia visual |
| D-T5 | Dónde vive la animación de cada uno | blur = dominio content de motion (entrada de contenido: consume uix.motion / WAAPI vía runtime, no keyframes CSS) · gradient = keyframes CSS de receta (loop ambiental, R-4.5) · circular/scrambled = rAF vía dom.requestFrame (estado continuo JS) · focus = CSS transitions + uix.timers · count = spring JS (¿reutilizar el spring de $motion si su API lo expone para valores numéricos? — se verifica en FT1; si no, spring local como el seed) |
Se deriva de la doctrina motion (perceptual anchoring; animation = channel); no exige gate salvo desacuerdo |
Fases (tras el gate)
-
FT0 — Doctrina: asentar D-T* en este plan; verificar precedentes (spring de
$motion, sistema de gradientes S9, wrappers eidos-only) leyendo el código real; actualizardocs/README.mdsi la familia merece fila propia. -
FT1 —
CountUp(servicio): morfo 1-part patrón FormatNumber · componeuix.format.numbers(locale reactivo del ecosistema, NO en-US fijo; elseparatordel seed se reevalúa contra la API real del formatter) · IO por$adom·uix.timers· spring (D-T5) · reduce = salto al final. -
FT2 —
TextGradient(eidos): receta CSS con tokens + R-4.5 · stops libres + presets S9 (D-T3) · borde con token de superficie · reduce ya en CSS. -
FT3 —
TextBlur(eidos + motion content): split words/letters con SR hardening (contenedoraria-label, segmentosaria-hidden) · stagger/WAAPI vía runtime motion · IO por$adom. -
FT4 —
TextCircular(eidos): rAF víadom.requestFrame· hover states como data-attrs del morfo · tokens de tipografía/tamaño · conservarole="img". Anotar la conexión Aura (texto orbital del indicador). -
FT5 —
TextFocus(eidos):uix.timers+dom.measure+dom.observe· fix a11y del role=button · border/glow por tokens. -
FT6 —
TextScramble(eidos): cache de rects (dom.measure + invalidación por RO — mata el reflow storm) · SR hardening · pointer pordom.listen· charset/duración/radio como props. -
FT7 — Cierre: demos con profundidad DEMO_AUTHORING (cada prop = control vivo) · checklist de componente por cada uno ·
npm run check+ vitest + eidos-lint + navegador · memoria + este plan con bloques EJECUTADA. -
Verify por fase: check 0 errores propios · component:audit verde para los nuevos · demo montada en navegador · SR hardening revisado (árbol de accesibilidad en devtools).
Fuera de alcance
La fase Aura (el plan madre §F6.3) sigue sin construirse aquí; TextCircular
solo deja anotada la pieza orbital. Los 4 efectos de imagen (D8) siguen fuera.