5.9 KiB
Eidos TextFocus
<TextFocus> difumina todas las palabras de una frase salvo la activa, y
un marco de esquinas viaja hasta ella — avance automático con cadencia, o
manualMode para enfocar la palabra bajo el cursor.
Familia text-effects (TextGradient / TextBlur / TextCircular /
TextScramble) — diseño de familia: docs/decisions/design-text-effects.md.
Superficie
<TextFocus sentence="True Focus" />
<TextFocus sentence="Pick a word by hovering" manualMode blurAmount={8} />
<TextFocus sentence="Slow travel" animationDuration={1.2} pauseBetweenAnimations={2} />
<TextFocus sentence="Tinted" borderColor="--color-affirm-solid" />
Props clave
| Prop | Tipo | Default | Notas |
|---|---|---|---|
sentence |
string |
'True Focus' |
La frase. |
separator |
string |
' ' |
Separador de palabras. |
manualMode |
boolean |
false |
Hover enfoca; sin auto-avance. |
blurAmount |
number (px) |
5 |
Blur de las palabras inactivas. |
borderColor / glowColor |
color o token | tokens de la receta | '--color-*' se interpola como var(). |
animationDuration |
number (s) |
0.5 |
Transición de foco/marco. |
pauseBetweenAnimations |
number (s) |
1 |
Reposo del marco (auto). |
Tokens públicos
--text-focus-border-color · --text-focus-glow-color ·
--text-focus-word-size · --text-focus-word-weight · --text-focus-gap.
Baseline
Port limpio del seed web/routes/demos/animations/text/focus/focus.svelte
(colección intacta como referencia — provenance en docs/architecture/packs.md). El seed ya
traía el auto-avance, el modo manual y el RO de re-alineado; sus déficits:
setInterval/RO/getBoundingClientRect crudos, colores green/rgba
fijos y — el peor — role="button" + tabindex="0" en cada palabra sin
acción activable detrás.
Comparativa
| Capacidad | Eidos | react-bits TrueFocus | CSS-only (:hover blur) |
GSAP Flip |
|---|---|---|---|---|
| Marco que viaja con física de layout real | Sí | Sí | No | Sí |
| Palabras sin falsos roles interactivos | Sí | No (role heredado) |
Sí | N/A |
Lecturas de layout coalescidas post-layout (dom.measure) |
Sí | No | N/A | No |
| Timers/RO del ecosistema (cancelables) | Sí | No | N/A | No |
| Reduced motion (marco quieto + sin transiciones) | Sí | No | Manual | Manual |
Referencias externas: react-bits TrueFocus (https://reactbits.dev/text-animations/true-focus) · GSAP Flip (https://gsap.com/docs/v3/Plugins/Flip/) · MDN filter: blur() (https://developer.mozilla.org/docs/Web/CSS/filter-function/blur).
Decisiones
- A11y fix sobre el seed: fuera
role="button"/tabindexde las palabras — eran botones falsos sin acción (trampa de tabulación). La frase se lee como texto normal; el hover del modo manual no necesita rol. getBoundingClientRect→eidos.dom.measure(lectura coalescida post-layout — la regla dura de layout reads del repo); cadencia →eidos.timers.interval; re-alineado →eidos.dom.observeResize.- Reduced motion — upgrade sobre el seed: sin auto-avance (el marco se
queda en la palabra inicial) y las transiciones inline se anulan en la
receta (única regla
!important, anotada — el estilo inline no se puede vencer de otro modo). - Colores por tokens con default
--color-primary-solid(el seed:green+ rgba fijos); los props aceptan tokens (--x→var()). data-activeen la palabra activa — hook de receta/consumidor.- No emite eventos sema: el foco viajero es ambiental; el hover manual elige, no compromete.
Talla y tema
Guard R-5.4 (npm run theming:sentinel -- text-focus <url>): 8/8 tokens
mueven un computed en vivo, sin una sola excepción. Corrido 2026-08-22.
Contrato en lib/recipes/base.ts > text-focus (8 claves, entrada NUEVA).
Alcance del censo: 100 % (era 63 %).
Ya consumía cinco públicos «de facto» con fallback y sin declarar ninguno; los otros tres faltaban, y están todos en la ESQUINA del marco de foco.
La esquina es una escuadra dibujada con bordes: 1rem × 1rem con dos de
sus cuatro lados a 0 según la posición (tl, tr, bl, br). El tamaño es
un knob, el grosor del trazo otro y el radio el tercero. Los desplazamientos
-0.625rem no son knobs: colocan la escuadra justo fuera del marco y se
derivan del tamaño y el trazo.
border-fg y glow-fg son el mismo acento en dos capas —el trazo y su
resplandor vía drop-shadow— y se declaran por separado a propósito: un tema
puede querer el halo de otro tono que el borde.
⚠ --radius-xs no existe, y esta receta lo referenciaba
La escala de radios del sistema es sm | md | lg | xl | xxl, sin xs. El CSS
escribía var(--radius-xs, 3px) y pintaba el fallback, así que nadie lo
notó. Tokenizarlo tal cual dejó la variable vacía, la declaración inválida y
las cuatro esquinas CUADRADAS — 48 diffs, que es exactamente para lo que sirve
el gate. El contrato guarda 3px verbatim, el valor que de verdad pintaba.
Registrado en next-features.md §13 junto con la pregunta que abre: cuántos
var(--fantasma, fallback) más hay en el catálogo.
Gaps
- Navegación por teclado del foco (flechas): diferir — exigiría un contrato interactivo real (morfo con keyboard + roving focus); si un consumidor lo pide, sube de clase (deja de ser pasivo).
- Callback
onFocusChange(index): implementar cuando haya consumidor. - Pausar el auto-avance al hover (sin
manualMode): diferir.
Passive justification
Pasivo por diseño: loop ambiental de lectura sobre texto real. El hover en modo manual solo selecciona qué palabra está nítida — sin commit, sin emerge, sin activación ni teclado (los falsos roles del seed se retiraron precisamente por eso).
Referencias
- Seed portado:
web/routes/demos/animations/text/focus/focus.svelte. - Regla de layout reads:
CLAUDE.md§Key Conventions (dom.measure).