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/CONTINUE-direction-runtime.md

21 KiB

CONTINUE — la dirección deja de ser una convención

Handoff para una sesión nueva. Leer entero antes de tocar nada: el refactor es pequeño en líneas y grande en alcance — toca un mecanismo del que cuelga todo el catálogo.

Contexto previo: docs/process/CONTINUE-direction.md (el eje entero) y docs/canon/direction-contract.md (el contrato, que es lo que este refactor automatiza). No hay que releerlos para empezar; lo que hace falta está aquí.


1. Por qué — los números

El contrato es correcto. El mecanismo para cumplirlo es teclear la misma línea en cada componente, y por eso falla:

veces
Estampado del dir escrito a mano en un provider 54
Enlace al padre compuesto a mano en un envoltorio 7
Content portalizados que re-arrancan la cadena por su cuenta 9
Componentes que aceptaban dir y no lo estampaban 20 de 55

Los 20 se arreglaron uno a uno (21055bd3c), leyendo receta y envoltorio de cada uno. Encontrarlos costó una auditoría. El siguiente componente que alguien escriba volverá a tenerlo, porque nada lo impide: la regla vive en la cabeza del que lo escribe.

Una convención que hay que repetir 54 veces y que 20 de 55 incumplían no es una convención — es una abstracción que falta.


2. Las dos comprobaciones, HECHAS

Las dos condiciones de viabilidad están verificadas. No hay que repetirlas.

2.1 · ¿Puede el runtime emitir un atributo que a veces debe estar AUSENTE?

Sí, ya lo hace. src/uix/soma/runtime.svelte.ts:519-522, en renderProps():

for (const plan of compiledPart.dynamicAttrs) {
	const value = evalAttrPlan(plan, bindings);
	if (value !== undefined) props[plan.attr] = value;
}

undefined ⇒ el atributo no se escribe. Es exactamente la semántica del estampado crudo: nadie afirmó ⇒ no hay atributo ⇒ el elemento hereda. resolveProps() (:496-503) hace lo mismo. No hay que construir nada para esto.

2.2 · ¿Y sin declararlo en el morfo?

También, y hay precedente literal. partPropsForRegistration() (runtime.svelte.ts:538-551) ya inyecta id, el marcador del componente, data-archetype y reg.attachment sin ninguna declaración en el morfo, y el comentario de data-archetype justifica por qué:

data-archetype is part of static identity (cross-component classification, never mutates), so it ships through partProps — not through dom.apply.

Así que el movimiento 1 NO exige tocar 55 ficheros de morfo. Hay dos vehículos posibles y la elección es de quien lo haga (§4.1).

2.3 · ¿Tiene la capa flotante acceso al dueño?

Sí. Los sub-proveedores llevan this.provider (verificado en select y popover).

⚠️ El reparto que decía este apartado era FALSO, y la fuga no estaba donde decía. Censo real (§9.1): de los 11 FloatingContent.create, 10 pasan dir: opts.dir y 1 (sidebar) pasa this.provider.opts.dir — no 8 y 3. Y ese opts.dir no era una fuga: los 7 envoltorios Content que lo alimentan ya componían el enlace al padre a mano, así que la afirmación de la raíz SÍ cruzaba. Lo que no cruzaba era otra cosa (§9.1).


3. Qué está mal hoy, en una frase

El estampado y la propagación son dos convenciones manuales, y el eje entero —cuatro auditorías, tres barridos y unos veinte defectos— es lo que cuesta mantenerlas a mano.


4. Los dos movimientos

4.1 · El estampado pertenece al runtime

Objetivo: que las 54 líneas escritas a mano se vuelvan cero, y que un componente que acepte dir no pueda dejar de estamparlo.

Vehículo A — inyección en partPropsForRegistration. El provider registra su parte pasando la dirección cruda; el runtime la emite junto a id y data-archetype. Precedente exacto (§2.2). Mínimo código. Coste: el runtime pasa a conocer un concepto que hoy no conoce.

Vehículo B — un dynamicAttr universal en el compilador de morfo. El compilador añade a la parte provider un AttrPlan para dir con fuente fromProp: 'dir'. Encaja con «morfo declara, soma ejecuta», y renderProps() ya lo omitiría solo cuando resuelve undefined (§2.1). Coste: toca el compilador y su caché; hay que mirar contracts.cssSelectors, porque el conjunto cerrado de selectores que eidos puede usar se calcula ahí.

⚠️ Decidir A o B es la primera tarea, y no está decidida. Mi inclinación es A por tamaño y por precedente, pero B es más fiel a la doctrina. Léete los dos sitios antes de elegir.

⚠️ Lo que NO debe pasar: que el atributo aparezca en componentes que hoy NO deben llevarlo. Son cuatro y están verificados: popover, tooltip, link-preview y context-menu — su raíz no renderiza elemento o no pinta nada direccional, y su pintura vive en el panel portalizado. Si el runtime lo emite universalmente, esos cuatro ganan un atributo inútil. Es la parte delicada del movimiento 1: la emisión tiene que colgar de que el componente ACEPTE dir, no de que exista una parte provider.

⚠️ Y la excepción del §2 del contrato: avatar e image NO aceptan dir a propósito (su :dir() sólo traduce a físico una prop lógica que ya eligió el consumidor). Si acaban con la prop por efecto colateral, el refactor se ha llevado por delante una decisión firmada por el usuario.

4.2 · La propagación pertenece a la capa flotante

Objetivo: que la afirmación de la raíz cruce el portal sin que nadie escriba el enlace.

Hoy cada Content portalizado vuelve a arrancar activeDir desde cero, y por eso la afirmación no cruza. La forma: FloatingContent toma la dirección del proveedor DUEÑO, y el dir propio del Content la pisa si existe. Una composición dentro de la capa en vez de once fuera.

Los 7 enlaces compuestos a mano (dropdown-menu ×2, menubar, select, combobox, context-menu ×2) y el de color-picker se borran.

Y cierra sin trabajo extra el pendiente que quedó reportado: ningún picker de eidos reenvía dir a su PopoverContent, así que el cromo del picker-shell dentro del portal (picker-shell.css:78, el split del pie) está sin cubrir en los siete. Con 4.2 deja de ser una tarea; sin 4.2 son seis parches idénticos.


✅ 4.2 EJECUTADA — ver §9. Lo de abajo es el enunciado original, conservado porque el diagnóstico cambió al verificarlo.

5. Orden, y por qué

4.2 primero. Es más pequeña, está más acotada (una capa, once consumidores) y su verificación es directa: abrir un Select y comprobar que trigger y panel coinciden. Además borra código, así que 4.1 llega a un árbol más limpio.

4.1 después, y sólo cuando 4.2 esté verde y commiteada.


6. Verificación — la línea base exacta

Antes de empezar, tomar estas medidas y compararlas al final. Cualquier desviación hay que explicarla, no absorberla:

valor esperado
npx svelte-check --threshold error 77 (la base; sube si otra sesión toca media-player)
npm run rtl:check 1 error — palabras-chrome.css:446, preexistente y excluido
npm run docs:check 0 errores sobre 564 docs
Tests de los componentes tocados verdes; eidos/lint.test.ts falla por audio-player sin morfo, ajeno

⚠️ En navegador, y no es opcional: el censo del estampado NO se puede hacer por grep. Dos componentes (listbox, navigation-menu) estampan a través de un alias assertedDir y un grep del patrón literal los da como huecos. La comprobación buena es leer el DOM:

// con la demo abierta, por cada componente
document.querySelector('[data-select]').getAttribute('dir');

⚠️ El panel del navegador tiene que estar VISIBLE. Oculto no compone frames: no hay capturas y las transiciones CSS se congelan, así que getComputedStyle(...).transform devuelve la matriz identidad y parece un defecto que no lo es. Me pasó dos veces.


7. Trampas ya pagadas — no volver a pisarlas

  • El estampado es el CRUDO (opts.dir.current), nunca resolvedDir. Un valor resuelto escribe dir="ltr" cuando nadie afirmó nada, forzando el subárbol a LTR dentro de una página RTL. Pasó en sidebar y lo escribí yo.
  • dir en un <svg> no hace nada — la hoja UA de HTML no alcanza a los elementos SVG. Va en el envoltorio HTML. Medido en Chrome.
  • OptsFromProps quita el undefined del prop público y destruye la distinción entre «nadie afirmó» y 'ltr'. Quien lo use declara dir aparte.
  • Clasificar esto por grep se equivoca. Yo di avatar por defectuoso leyendo el patrón en vez del componente, y el usuario lo cazó. Abre la receta, abre el provider, mira dónde renderiza cada parte.
  • Una migración mecánica hereda los defectos que traduce. El barrido de §6.3 cambió doce selectores sin mirar los cuerpos y arrastró dos dobles volteos hasta que los cazó el guard RTL-2.

8. Estado del árbol al escribir esto

Rama alpha-0.1-dir-prefs, con remoto gita (no hay origin). Último commit del eje: 21055bd3c. Sin tocar y no commitear: .claude/settings.local.json, src/arts/adom/__scratch-verify.ts, web/routes/alpha/. media-player lo edita otra sesión — excluirlo de todo staging.


9. Sesión 2026-08-04 — §4.2 ejecutada

Sin commitear. Rama alpha-0.1-dir-prefs, HEAD se movió a 4755d97d1 mientras trabajaba (otra sesión, media-player + audio-player): esos ficheros están fuera de este cambio y fuera de cualquier staging.

9.1 · El censo del §2.3 estaba mal, y la fuga era otra

Rehecho leyendo los ficheros, no el patrón:

FloatingContent.create 11 sitios en 9 ficheros
…pasando dir: opts.dir 10
…pasando this.provider.opts.dir 1 (sidebar)
Envoltorios Content que componían el enlace al padre A MANO 7
Envoltorios Content SIN enlace 3 — popover, tooltip, link-preview

Y esos 3 no eran un defecto: sus raíces no aceptaban dir en absoluto, así que no había nada de la raíz que heredar. O sea entre los 11 consumidores del motor flotante no había ninguna fuga.

La fuga estaba un piso más arriba: en quien COMPONE un popover. Nueve componentes crean un PopoverProvider internamente (color-picker, date-picker, date-range-picker, emoji-picker, gradient-picker, natural-time-picker, picker, time-picker, time-range-picker) y siete de ellos aceptan dir — pero PopoverProvider no tenía dónde recibirlo, así que la afirmación moría en el borde del portal.

Medido en Chrome ANTES, con <DatePicker dir="rtl"> y la página en LTR:

[data-floating-wrapper]  dir="ltr"      ← el panel entero forzado a LTR
[data-picker-shell]      direction ltr
[data-picker-footer]     direction ltr   Clear x=15 · Cancel x=102 · Close x=209
[data-calendar]          direction RTL   ← lo ÚNICO que espejaba

El calendario mirroreaba porque es un componente soma propio que recibe el dir por el árbol de proveedores; el cromo que lo rodea —el picker-shell, cuyo pie reparte con margin-inline-end: auto (picker-shell.css:78)— no. Panel partido por la mitad, que es exactamente el fallo del §2 del contrato.

9.2 · La forma implementada

el dir propio de la superficie  →  el dir afirmado por el proveedor DUEÑO  →  omitir
  • FloatingProviderOpts.dir — el dueño baja su afirmación (ya pasada por activeDir, o sea prop → prefs, así que la cola de prefs queda intacta).
  • FloatingContent.assertedDir — opts.dir.current ?? provider.opts.dir.current. Los dos estampados del wrapper (rama nativa y rama JS) leen de ahí.
  • createFloatingShellRoot lo reenvía; es la puerta de 8 de los 10 FloatingProvider. Los otros 2 (submenús de dropdown-menu y context-menu) pasan el del menú padre.

⚠️ El punto que decide si esto funciona: FloatingContentOpts.dir pasa a ser la prop cruda del Content, no el resultado de activeDir. Si el envoltorio resolviera ahí, el valor sería casi siempre concreto (prefs) y el dueño nunca ganaría el fallback. Los 10 envoltorios pasan ahora readableActive(() => dir).

Consecuencias:

  • Los 7 enlaces compuestos a mano se borran, y con ellos el soma, el activeDir y el parentRoot/parentMenu de cada envoltorio.
  • popover, tooltip y link-preview aceptan dir en su raíz (prop pública nueva + opt). No estampan nada —no renderizan elemento—; el valor existe para cruzar el portal, y es lo que permite que un picker empuje su dirección al panel. Sin esa prop habría que escribir activeDir(() => undefined, soma) en la raíz, que es peor y no da nada a cambio.
  • Los 9 composers pasan su dir al PopoverProvider. gradient-picker y picker no aceptan dir (§9.17 del eje), así que corren la cadena con el primer eslabón ausente: activeDir(() => undefined, soma).

Queda UN activeDir con enlace al padre, y a propósito: dropdown-menu-sub-content.svelte lo necesita para effectiveSide (el lado por defecto del submenú es un left/right concreto, o sea MATEMÁTICA, que sí quiere valor resuelto). El ATRIBUTO ya no sale de ahí.

9.3 · Verificación

Medido en Chrome real, con la página en LTR y el dir movido por el control de la demo (el camino real, nunca setAttribute):

antes después
date-picker · wrapper ltr rtl
date-picker · shell / footer ltr rtl
date-picker · Clear/Cancel/Close x 15 / 102 / 209 200 / 92 / 15
date-range-picker · Clear/Cancel/Close x — 448 / 217 / 17

Y a ojo: el panel entero espeja —cabecera, rejilla de días y pie—, no sólo el calendario. Sin regresión en los que ya funcionaban, comprobados uno a uno: dropdown-menu (raíz y submenú, con data-side=left en RTL), context-menu, menubar, select (por prefs, las dos direcciones), y tooltip en auto, que sigue estampando ltr — la cola de prefs no se perdió.

guard resultado
npx svelte-check --threshold error 78 = línea base (A/B con stash: 78 sin mis cambios)
Tests de los 20 ámbitos tocados 158/158
npm run rtl:check 1 — palabras-chrome.css:446, preexistente
npm run docs:check 0 sobre 565
Pasada completa de vitest 9 fallos, ninguno mío (A/B con el árbol limpio: los 6 deterministas de contracts/eidos-lint salen igual; soma-attr-audit, orca y cookie-adapter pasan aislados — flaky bajo carga)

⚠️ La línea base de check es 78, no 77 como decía §6 — con el __scratch-verify.ts sin commitear en el árbol.

9.4 · Trampas de esta sesión

  • Lancé prettier --write sobre los 43 ficheros tocados y fue un error. Reformateó líneas PREEXISTENTES (imports colapsados, prosa de dos READMEs reflowada: popover/README.md pasó de +7/−4 a +49/−46). Deshecho a mano hasta devolver la diff a su forma quirúrgica. La regla del §5 del eje sigue viva: formatea sólo lo que ensuciaste, y mira el tamaño de la diff después. Lo único que se conserva del formateo son dos líneas MÍAS que superaban las 100 columnas.
  • Un click JS justo después de navegar no engancha: hasta que Svelte hidrata, .click() no dispara el handler. Y leer el atributo en la MISMA llamada que el click devuelve el valor viejo — hay que separar en dos llamadas.
  • El control de dirección de algunas demos vive bajo la pestaña «eidos System», no en la vista Live. Sin cambiar de pestaña no hay chips que pulsar.

9.5 · Lo que queda apuntado, sin tocar

  1. 9 campos resolvedDir MUERTOS en los proveedores Content (select 495, combobox 577, dropdown-menu 314 y 1230, context-menu 252 y 995, menubar 563, tooltip 409, link-preview 268). Verificado que nadie los lee — toda la matemática direccional consume this.provider.resolvedDir, o sea el de la RAÍZ. Ya eran código muerto antes; ahora además su docblock («Direction for this part's OWN math») describe algo que opts.dir ya no es. Borrarlos requiere tu palabra.
  2. Agujero de observabilidad en 5 demos de picker: time-picker, time-range-picker, color-picker, emoji-picker y gradient-picker tienen el control dir del arnés pero no se lo pasan al componente, así que la mitad que ejercita el portal no se puede ver. Sólo date-picker y date-range-picker lo pasan — y por eso son los dos que pude medir. Mismo agujero que §8.2 del eje, misma clase de arreglo.
  3. context-menu-sub-content clava side = 'right' (físico) mientras dropdown-menu-sub-content lo deriva de la dirección. Incoherencia entre dos componentes hermanos, preexistente y ajena a este cambio.
  4. §4.1 sigue entera y sin empezar — el estampado del dir en el runtime, y la elección Vehículo A vs B sigue sin tomar.

10. Auditoría de la sesión — lo que salió al re-verificarlo todo

10.1 · Código muerto: BORRADO

Los 9 resolvedDir de §9.5.1 están eliminados. Antes de borrar, verificado que ninguno se lee: las 5 lecturas vivas de resolvedDir en esos ficheros son todas this.provider.resolvedDir —o sea el de la RAÍZ— desde SelectTriggerProvider, MenuContentProvider, ContextMenuContentProvider, MenubarMenuProvider y MenubarTriggerProvider. Cero lecturas desde eidos, blocks, packs, web/routes o tests. Quedan 5 declaraciones, las cinco de raíces vivas.

10.2 · Las demos: las 5 cableadas

time-picker, time-range-picker y color-picker no tenían control de dirección en absoluto; emoji-picker y natural-time-picker lo tenían en el arnés pero no se lo pasaban al componente. Ahora las siete demos de picker que aceptan dir lo pasan a la instancia, así que el camino de la PROP —el único que ejercita el portal— es observable en todas.

⚠️ Corrección a §9.5.2: dije «5 demos con el control pero sin pasarlo». Falso en dos sentidos: tres no tenían control, y gradient-picker/picker no aceptan dir (§9.17 del eje), así que no entran.

10.3 · Censo de ISLAS — un defecto nuevo, PREEXISTENTE y de otra familia

Con las demos cableadas se puede al fin escanear cada panel en RTL buscando elementos que estampen una dirección DISTINTA a la del envoltorio flotante:

panel islas
date-picker · date-range-picker · time-picker · time-range-picker 0
natural-time-picker 1 — [data-slider]
emoji-picker 3 — [data-command] + [data-toggle-group] ×2
color-picker 1 — [data-color-field-format-select] (un Select)

La forma es la misma que arregló §4.2, pero para composición EN SITIO en vez de por portal: un componente eidos monta otro componente del canon (natural-time-picker-panel.svelte → Slider.Provider, emoji-picker-content.svelte → Command + ToggleGroup, color-field-format-select.svelte → Select) sin reenviarle el dir. El hijo corre su propia cadena, cae en prefs, estampa ltr — y ese atributo CORTA la herencia del panel, que sí es rtl.

Es preexistente: mientras el panel entero era LTR la isla no se veía. Lo que hizo §4.2 fue volverla visible.

⚠️ No lo he arreglado, y es deliberado: no hay UN solo precedente en eidos de reenviar dir a un componente compuesto (grep "dir={" src/uix/eidos da un único resultado, y es data-dir de otra cosa). Arreglarlo es inventar el patrón —doctrina nueva, no un parche— y toca 3 ficheros que este movimiento no toca. Es un movimiento propio.

La misma clase de defecto vivía en la DEMO del color-picker, que monta <Tabs> y un <ColorField> a mano dentro del panel: ahí sí lo arreglé, porque es demo y porque si no el control de dirección que acabo de añadir enseñaría un panel medio volteado y parecería defecto del framework.

10.4 · Verificación tras la auditoría

npx svelte-check --threshold error 78 = línea base (A/B con stash: 78 sin mis cambios)
Tests de los 20 ámbitos tocados 158/158
npm run rtl:check 1, el de palabras
npm run docs:check 0 sobre 565
Islas en los 7 paneles 4 paneles a 0; las 5 restantes documentadas arriba

⚠️ Trampa medida dos veces: svelte-check dio 79 en una pasada y 78 en la siguiente sin tocar nada. Era una CARRERA con la otra sesión guardando ficheros de media-player. En una rama compartida, un número que no reproduce no es un hallazgo — repite la medida antes de perseguirlo.

⚠️ Las 5 demos tocadas ya fallaban prettier --check en HEAD, así que no se formatearon (§9.4).

Powered by TurnKey Linux.