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

121 lines
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:
```ts
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.