You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-focus-first.md

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

  1. La declaración es el DEFAULT de especie; la prop por-instancia manda (patrón dir). <Dialog modal={false}> sigue decidiendo en el consumidor.
  2. Dos familias, una unión discriminada — la forma vieja era un fragmento para overlays; la nueva nombra el mecanismo:
    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)
    
    Validación en schema: partRef cruzado contra las partes; custom exige razón no vacía; grid sin executor.custom es inválido mientras el primitivo sea 1D (la mentira estructural se hace imposible, no prohibida).
  3. Dos ejecutores, uno por familia:
    • trap → FocusScope gana una entrada policy (el compiled.focus) y la usa como fallback real: trap = trapFocus ?? modal ?? policy.trap.
    • roving → renace RovingFocusGroup ($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).
  4. El censo hace imposible mentir (patrón direction-census.test.ts, focus-census.test.ts):
    • morfo trap ⇒ su provider usa FocusScope Y el default de especie del wrapper (modal/trapFocus) coincide con declaration.trap — la clase Popover (declaraba true, embarcaba false) muere aquí;
    • morfo roving ⇒ el provider consume RovingFocusGroup O lleva executor.custom firmado;
    • la inversa del autor: componente que usa FocusScope/roving SIN declaración ⇒ FALLO. Excepciones, solo firmadas en el propio censo.
  5. 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 status limpio en ese frente; MorfoFocus presente.
  • F1 · el contrato — la unión discriminada en types.ts, validación en schema.ts (partRef cruzado, custom con razón, grid⇒custom), paso por compile.ts (CompiledMorfo.focus), y SomaRuntime expone runtime.focus. → verificar: schema.test + compile.test con casos nuevos (inválidos incluidos); npm run check src/ 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 declaran kind: 'roving' con su orientación real; barrido del resto del catálogo por si hay foco sin censar. → verificar: sweep de validateMorfo (161+) verde; el censo F5 en rojo CONTROLADO (aún sin ejecutores) documenta el estado intermedio.
  • F3 · ejecutor trap — FocusScope.use acepta policy; los 7 providers pasan runtime.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; con modal → manda la prop).
  • F4 · ejecutor roving — (a) RovingFocusGroup corregido (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) declaran executor: { custom: <razón> } — firmados, no migrados a la fuerza a un primitivo 1D. → verificar por ola: suite del componente + perm:check de sus rutas + interacción de flechas/Home/End en navegador.
  • F5 · el censo — focus-census.test.ts con 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:check 0; página de un trap y un roving en navegador.
  • F7 · cierre — npm run gate entero + 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/tabbable port: continue-runed-tabbable-port.md existe en process/ — leerlo antes de F4a por si el port pactó decisiones sobre el tab-stop.

Powered by TurnKey Linux.