6.7 KiB
PLAN — eje focus-first (firmado 2026-08-26)
Firma del autor: «lo mejor para la arquitectura y diseño del framework
independientemente de lo que cueste» + «ejecuta el eje focus-first completo».
La amputación de Morfo.focus queda descartada; la dirección es la inversa:
completar el contrato y darle ejecutor y censo.
Origen: informe de auditoría (hallazgo verificado nº8) + contraste del
corpus (eidos.md:472 falso; regla 2-de-3 a cero) + la inversión del autor:
«el error quizás sean los componentes que deberían declararlo». Censo que
funda el eje: ~21+ componentes con comportamiento de foco (7 con
FocusScope: dialog, drawer, popover, context-menu, dropdown-menu,
float-panel, menubar · 14 de familia roving con tab-stop único
reimplementado a mano: toolbar, tabs, listbox, radio-group, stepper,
tag-group, tags-input, toggle-group, carousel, drag-drop, tree-view,
tree-grid, grid-list, chronos) contra 9 declaraciones históricas — y la
forma vieja {initial, trap, return, restore} no podía expresar roving.
La tesis del eje
- La declaración es el DEFAULT de especie; la prop por-instancia manda
(patrón
dir).<Dialog modal={false}>sigue decidiendo en el consumidor. - Dos familias, una unión discriminada — la forma vieja era un fragmento
para overlays; la nueva nombra el mecanismo:
Validación en schema:type MorfoFocus = | { kind: 'trap'; trap: boolean; // default de especie initial?: 'first-focusable' | 'trigger' | { partRef: string }; return?: 'trigger' | 'previous' | { partRef: string }; restore?: boolean; } | { kind: 'roving'; // ENSANCHADO en F2: toolbar rueda sobre VARIAS partes // (button, link, group-item) — un partRef singular mentiría. parts: readonly string[]; orientation: 'horizontal' | 'vertical' | 'both' | 'grid'; loop?: boolean; executor?: { custom: string }; }; // excepción FIRMADA (censo)partRefcruzado contra las partes;customexige razón no vacía;gridsinexecutor.customes inválido mientras el primitivo sea 1D (la mentira estructural se hace imposible, no prohibida). - Dos ejecutores, uno por familia:
trap→FocusScopegana una entradapolicy(elcompiled.focus) y la usa como fallback real:trap = trapFocus ?? modal ?? policy.trap.roving→ renaceRovingFocusGroup($adom, hoy muerto): se corrige su defecto medido (tab-stop por id → por elemento) y pasa a ser EL ejecutor del patrón «exactamente un tab-stop»; las reimplementaciones a mano colapsan por olas (abajo).
- El censo hace imposible mentir (patrón
direction-census.test.ts,focus-census.test.ts):- morfo
trap⇒ su provider usaFocusScopeY el default de especie del wrapper (modal/trapFocus) coincide condeclaration.trap— la clase Popover (declaraba true, embarcaba false) muere aquí; - morfo
roving⇒ el provider consumeRovingFocusGroupO llevaexecutor.customfirmado; - la inversa del autor: componente que usa FocusScope/roving SIN declaración ⇒ FALLO. Excepciones, solo firmadas en el propio censo.
- morfo
- Segundo consumidor = docs-site: la pestaña A11y de cada componente
renderiza la política de foco desde la declaración (como ya hace con
keyboard[]) → la regla 2-de-3 se cumple con dos consumidores REALES (ejecutores + docs), no por gracia.
Fases (cada una = commit(s) + verificación)
- F0 · árbol limpio — vicen-42 revierte su amputación sin commitear
(types/schema/compile + 9 morfos). Nadie más toca su delta.
→ verificar:
git statuslimpio en ese frente;MorfoFocuspresente. - F1 · el contrato — la unión discriminada en
types.ts, validación enschema.ts(partRef cruzado, custom con razón, grid⇒custom), paso porcompile.ts(CompiledMorfo.focus), ySomaRuntimeexponeruntime.focus. → verificar:schema.test+compile.testcon casos nuevos (inválidos incluidos);npm run checksrc/ a 0. - F2 · las declaraciones (~21+) — los 9 históricos se reescriben a
kind: 'trap'con la verdad embarcada (popover:trap: false); los 14 roving declarankind: 'roving'con su orientación real; barrido del resto del catálogo por si hay foco sin censar. → verificar: sweep devalidateMorfo(161+) verde; el censo F5 en rojo CONTROLADO (aún sin ejecutores) documenta el estado intermedio. - F3 · ejecutor trap —
FocusScope.useaceptapolicy; los 7 providers pasanruntime.focus; ningún default de wrapper contradice la declaración. → verificar: suites de los 7 overlays; test nuevo: policy como fallback (sin props → manda la declaración; conmodal→ manda la prop). - F4 · ejecutor roving — (a)
RovingFocusGroupcorregido (tab-stop por elemento,''inválido, membresía viva) + su suite; (b) ola 1 de migración (los 1D limpios: toolbar, tabs, radio-group, toggle-group, tag-group, menubar); (c) ola 2 (listbox, stepper, carousel, tags-input — cada uno con su matiz: typeahead, linear-gate…); (d) los 2D/complejos (tree-view, tree-grid, grid-list, chronos, drag-drop) declaranexecutor: { custom: <razón> }— firmados, no migrados a la fuerza a un primitivo 1D. → verificar por ola: suite del componente +perm:checkde sus rutas + interacción de flechas/Home/End en navegador. - F5 · el censo —
focus-census.test.tscon las tres direcciones y las excepciones firmadas en el fichero. → verificar: censo verde; plantar una mentira (declarar trap:true en popover) y ver el censo en ROJO — un guard que no sabe fallar no vale. - F6 · doctrina y docs-site — morfo.md (fila de
focus= ejecutada POR FIN, con sus dos ejecutores), eidos.md:472 corregida, soma-architecture, glossary; la pestaña A11y del docs-site renderiza la política. → verificar:docs:check0; página de un trap y un roving en navegador. - F7 · cierre —
npm run gateentero +morfo:check+ actualización de CONTINUE-audit-p0.md y memoria.
Leyes que este eje debe respetar
- Árbol compartido: add por lista explícita, verificar+commitear en la misma invocación; los deltas de otros ejes no se tocan ni se stagean.
- La declaración NUNCA contradice lo embarcado: donde difieran, la verdad embarcada manda y la declaración se corrige (Popover es el precedente).
- Un componente 2D no se fuerza al primitivo 1D: excepción firmada > mentira estructural.
runed/tabbableport:continue-runed-tabbable-port.mdexiste en process/ — leerlo antes de F4a por si el port pactó decisiones sobre el tab-stop.