|
|
|
|
# `eidos/components/`
|
|
|
|
|
|
|
|
|
|
Recipes + wrappers visuales por componente. Cada componente vive en su
|
|
|
|
|
**subdirectorio** con la forma canónica documentada abajo. Los `.css`
|
|
|
|
|
sueltos al nivel raíz son legacy de la fase "eidos = solo CSS" y se
|
|
|
|
|
migran progresivamente.
|
|
|
|
|
|
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
|
|
|
> **⚠️ Contrato de construcción — leer antes de escribir un recipe/wrapper.** La
|
|
|
|
|
> **fuente de verdad** de qué tokens/arquetipos consume un componente visual es la
|
|
|
|
|
> tabla **«Build contract»** de
|
|
|
|
|
> [`docs/guides/component-guide.md`](../../../../docs/guides/component-guide.md)
|
|
|
|
|
> (migrada allí el 2026-07-11, DOC-1; el audit de 2026-06-19 que la sembró queda
|
|
|
|
|
> como historia): elevación vía `data-depth` (bundle completo) · estado vía
|
|
|
|
|
> `--state-*` · radio vía factor global + `[data-shape-nest]` · foco outline
|
|
|
|
|
> `--focus-ring-*` (§32) · label de campo canónico · bundle `--size-*` ·
|
|
|
|
|
> touch-target §37 · color por token (cero hex) · densidad
|
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo
El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en
`CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source
of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`.
Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es
normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon;
por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que
`:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook
incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las
trampas que el guard no ve; que espeja y que no; y la mitad global de prefs.
EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal:
- `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones,
mandando al wrapper a leer prefs directamente. Eso excluye la prop.
- `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()`
dentro del provider) como LA forma de obtener la direccion — justo lo que el eje
retiro del catalogo.
- `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la
regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una
clave HTML) es correcto y sobrevive; solo cambia el ejemplo.
- `active-architecture.md:416` no listaba `lang` en la proyeccion, contra
`contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`.
Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`,
`data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER
guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`,
`building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la
matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura,
que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`.
El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en
la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en
E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de
estampar `data-dir`, que `:dir()` no puede ver.
DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y
resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del
catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse.
Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos:
RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca
lee al padre» era absoluto y borraba la composicion sancionada en el punto de
llamada · el estampado se afirmaba incondicional en un sitio y condicional en
otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni
`component-audit.ts` conocen.
`docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
> `--space-*`/`--control-height-*` · RTL lógico + `:dir(rtl)` (nunca
|
|
|
|
|
> `[dir='rtl']`), y el provider estampa el `dir` crudo si el recipe ramifica con
|
|
|
|
|
> `:dir()` — sin estampar, `:dir()` sólo ve la dirección heredada
|
|
|
|
|
> ([contrato de dirección](../../../../docs/canon/direction-contract.md)) · i18n
|
|
|
|
|
> `langs.ts` · portal-safe · composición. **Todos los ejes están LIVE** con su
|
|
|
|
|
> guard (R-\*, contracts, elevation-plane).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Forma canónica (disciplined option C — vigente desde 2026-05-10)
|
|
|
|
|
|
|
|
|
|
Convención unificada para TODOS los componentes multi-part. Sigue la
|
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
|
|
|
estructura de `air` (la rama legacy, ya retirada del árbol) y la ergonomía moderna de bits-ui /
|
|
|
|
|
shadcn-svelte: una sola entidad mental — `<Drawer>` — con hijos
|
|
|
|
|
accesibles como propiedades — `<Drawer.Trigger>`, `<Drawer.Content>`, etc.
|
|
|
|
|
|
|
|
|
|
### Reglas duras
|
|
|
|
|
|
|
|
|
|
1. **Un solo punto de entrada por componente**: la default export es el
|
|
|
|
|
componente root visual. Se llama igual que el componente
|
|
|
|
|
(`<Drawer>`, `<Tabs>`, `<Checkbox>`, …) — **no** `<Drawer.Provider>`,
|
|
|
|
|
**no** `<Drawer.Root>`.
|
|
|
|
|
2. **El root vive en `{name}.svelte`** — NO en `{name}-provider.svelte`.
|
|
|
|
|
El nombre del fichero coincide con el nombre del componente.
|
|
|
|
|
3. **No exportar `Provider` públicamente**. La separación
|
|
|
|
|
"Provider compound vs flat" es una invención previa que se ha
|
|
|
|
|
retirado: hay UNA forma compound — el root + sus hijos atados.
|
|
|
|
|
4. **Los hijos siguen el naming de air / headless**: `Trigger`, `Content`,
|
|
|
|
|
`Overlay`, `Title`, `Description`, `Close`, `Portal`, `Header`,
|
|
|
|
|
`Footer`, `Item`, `Indicator`, `HiddenInput`, `Group`, `Label`, etc.
|
|
|
|
|
No inventar nombres nuevos.
|
|
|
|
|
5. **`Portal` se incluye donde air lo tenía** (Dialog, Drawer, Popover,
|
|
|
|
|
Tooltip — overlays portaled). Importado de
|
|
|
|
|
`$soma/components/internal`.
|
|
|
|
|
6. **No flat con snippet slots como API principal**. La invención
|
|
|
|
|
`<Drawer trigger={...} title={...} actions={...}>` está retirada —
|
|
|
|
|
esconde decisiones de composición que deberían ser explícitas en
|
|
|
|
|
componentes con portal/overlay/content/close.
|
|
|
|
|
7. **Los hijos se atan al root con asignación explícita**, no
|
|
|
|
|
`Object.assign` (que en Svelte 5 puede causar issues sutiles de
|
|
|
|
|
hidratación):
|
|
|
|
|
```ts
|
|
|
|
|
const Drawer = DrawerComponent as DrawerNamespace;
|
|
|
|
|
Drawer.Trigger = Trigger;
|
|
|
|
|
Drawer.Content = Content;
|
|
|
|
|
// ...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Estructura de directorio
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
drawer/
|
|
|
|
|
drawer.svelte ← root (lo que era air's Provider)
|
|
|
|
|
drawer-trigger.svelte
|
|
|
|
|
drawer-overlay.svelte
|
|
|
|
|
drawer-content.svelte
|
|
|
|
|
drawer-handle.svelte
|
|
|
|
|
drawer-title.svelte
|
|
|
|
|
drawer-description.svelte
|
|
|
|
|
drawer-close.svelte
|
|
|
|
|
drawer-header.svelte ← eidos-only layout shell (cuando aplique)
|
|
|
|
|
drawer-footer.svelte ← idem
|
|
|
|
|
drawer.css ← recipe
|
|
|
|
|
types.ts ← extiende las props públicas de Soma
|
|
|
|
|
index.ts ← compone Drawer + hijos
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `{name}.svelte` — el root
|
|
|
|
|
|
|
|
|
|
Wrapper directo sobre el provider público de Soma. Setea el
|
|
|
|
|
contexto headless, acepta los bindables del estado (`open`, `value`,
|
|
|
|
|
`pressed`, etc.), añade los data-attrs visuales propios de eidos
|
|
|
|
|
(`data-size`, `data-variant`, `data-color`, …), y renderiza
|
|
|
|
|
`{@render children?.()}` para que los hijos se compongan dentro.
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Eidos `<Drawer>` — root component. Wraps the Soma
|
|
|
|
|
* to set up the component context.
|
|
|
|
|
*/
|
|
|
|
|
import * as Drawer from '$soma/components/drawer';
|
|
|
|
|
import type { DrawerProps } from './types';
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
|
open = $bindable(false),
|
|
|
|
|
activeSnapPoint = $bindable(null),
|
|
|
|
|
isDragging = $bindable(false),
|
|
|
|
|
children,
|
|
|
|
|
...rest
|
|
|
|
|
}: DrawerProps = $props();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Drawer.Provider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
|
|
|
|
|
{@render children?.()}
|
|
|
|
|
</Drawer.Provider>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> **Patrón de import interno**: `import * as Drawer from '$soma/components/drawer'`.
|
|
|
|
|
> Eidos no crea una fachada headless propia por componente; envuelve las
|
|
|
|
|
> partes públicas de Soma directamente como `<Drawer.Provider>`,
|
|
|
|
|
> `<Drawer.Trigger>`, etc.
|
|
|
|
|
|
|
|
|
|
#### Prohibiciones de naming interno
|
|
|
|
|
|
|
|
|
|
El namespace interno que apunta a Soma se llama igual que el componente. No se
|
|
|
|
|
prefija ni se renombra:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- Correcto -->
|
|
|
|
|
import * as Collapsible from '$soma/components/collapsible';
|
|
|
|
|
|
|
|
|
|
<Collapsible.Provider>
|
|
|
|
|
<Collapsible.Trigger />
|
|
|
|
|
</Collapsible.Provider>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Formas prohibidas:
|
|
|
|
|
|
|
|
|
|
- `import * as Parts from '$soma/components/collapsible'`
|
|
|
|
|
- `import * as CollapsibleBase from '$soma/components/collapsible'`
|
|
|
|
|
- `import { Provider as SomaCollapsibleProvider } from '$soma/components/collapsible'`
|
|
|
|
|
- etiquetas sueltas `<Provider>`, `<Trigger>`, `<Content>` dentro de Eidos
|
|
|
|
|
- tipos o aliases internos `SomaXxxProvider`, `XxxBase`, `XxxRoot`
|
|
|
|
|
|
|
|
|
|
Motivo: Eidos ya declara la capa en el path. El import interno debe expresar la
|
|
|
|
|
entidad headless concreta de Soma, no una abstraccion inventada. Si un wrapper
|
|
|
|
|
Eidos necesita el provider de Soma, escribe `<Collapsible.Provider>`; si
|
|
|
|
|
necesita una parte, escribe `<Collapsible.Trigger>`, `<Collapsible.Content>`,
|
|
|
|
|
etc.
|
|
|
|
|
|
uix: date-picker + date-range-picker components + component audit infra
Two new Eidos components shipped end-to-end (wrapper + recipe + demo +
Soma provider hardening) plus a checklist-driven audit pipeline that
scores all 67 morfo components against doctrinal completion criteria.
New components:
- date-picker: full popover-anchored picker over date-field + calendar,
with calendar/content/trigger parts and demo route.
- date-range-picker: standalone wrapper with own calendar/grid/segment
surface, demo route, and recipe CSS.
- Both wrappers follow Option C disciplined (root + parts attached via
explicit assignment, no Object.assign).
Supporting Soma changes:
- range-calendar provider tightened (211 LOC of behavior, 167 LOC of
tests), README brought up to component doctrine.
- date-field, date-picker, date-range-field, date-range-picker Soma
providers + READMEs updated for new wrappers.
- popover provider/close gain props needed by the picker wrappers.
Morfo updates:
- date-picker / date-range-picker / range-calendar morfos refined for
the new APIs (parts, events, ARIA).
Audit infrastructure (new):
- src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across
morfo / eidos wrapper / recipe CSS / demo / README / cross-layer
scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION
and DEMO_AUTHORING_GUIDE.
- scripts/component-audit.ts + `npm run component:audit` — regex parser
over all 67 components, emits tmp/component-audit.md with summary
scoreboard + per-component findings. Validates against canonical
SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS.
- Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip,
date-range-picker). Top systemic gaps: translations.label (49),
README Gaps/Comparativa/Baseline sections (87 combined), keyboard
/event ratio under-declaration (15), apg URL absent (19).
Misc:
- src/uix/kimi-audit-eidos.md — supplementary audit notes.
- .gitignore: ignore .codex-* agent scratch artifacts at repo root.
- continue.md + READMEs updated through the migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
### Guardia de componentes compuestos de fecha
|
|
|
|
|
|
|
|
|
|
Incidencia 2026-05-20: DateField, DatePicker y DateRangePicker no pueden
|
|
|
|
|
cerrarse con wrappers Eidos si Morfo/Soma y la demo no estan cerrados primero.
|
|
|
|
|
Para componentes compuestos de fecha:
|
|
|
|
|
|
|
|
|
|
- Comparar antes con `morfo-runtime` (`air` / `terra`) y referencias React Aria,
|
|
|
|
|
Ark UI, Bits UI y shadcn-svelte.
|
|
|
|
|
- Completar Morfo con partes, `data-*`, ARIA, estados, keyboard y eventos
|
|
|
|
|
observables de la superficie compuesta.
|
|
|
|
|
- La demo no puede inventar `data-calendar-*` o `data-range-calendar-*`; si la
|
|
|
|
|
receta los necesita, pertenecen a Morfo/Soma.
|
|
|
|
|
- Cada control visible (`granularity`, `hourCycle`, segmentos, min/max,
|
|
|
|
|
`pagedNavigation`, modal/no-modal, clear) debe cambiar algo visible en el
|
|
|
|
|
stage y estar reflejado en los snippets.
|
|
|
|
|
- Verificar visualmente popup, uno/dos meses, seleccion, deseleccion parcial,
|
|
|
|
|
limites y accion de borrado antes de documentarlo como listo.
|
|
|
|
|
|
|
|
|
|
### `{name}-{part}.svelte` — hijos passthrough
|
|
|
|
|
|
|
|
|
|
Cada hijo es un wrapper delgado sobre la part del Soma. Si
|
|
|
|
|
añade visual-only data-attrs, los stamp aquí (ej. `data-size` en
|
|
|
|
|
Content). Si es passthrough puro (Trigger, Close), trivial.
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- drawer-trigger.svelte -->
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import * as Drawer from '$soma/components/drawer';
|
|
|
|
|
import type { DrawerTriggerProps } from './types';
|
|
|
|
|
|
|
|
|
|
let { children, ...rest }: DrawerTriggerProps = $props();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Drawer.Trigger {...rest}>
|
|
|
|
|
{@render children?.()}
|
|
|
|
|
</Drawer.Trigger>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `index.ts` — compone el namespace
|
|
|
|
|
|
|
|
|
|
Asignación explícita per-property sobre el root component. **NO** usar
|
|
|
|
|
`Object.assign(DrawerComponent, { Trigger, ... })` — Svelte 5 puede manejar
|
|
|
|
|
mal la mutación bulk del component constructor durante hidratación,
|
|
|
|
|
causando que los hijos aparezcan y desaparezcan después de mount.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import DrawerComponent from './drawer.svelte';
|
|
|
|
|
import Trigger from './drawer-trigger.svelte';
|
|
|
|
|
import Overlay from './drawer-overlay.svelte';
|
|
|
|
|
import Content from './drawer-content.svelte';
|
|
|
|
|
import Title from './drawer-title.svelte';
|
|
|
|
|
import Description from './drawer-description.svelte';
|
|
|
|
|
import Close from './drawer-close.svelte';
|
|
|
|
|
import Header from './drawer-header.svelte';
|
|
|
|
|
import Footer from './drawer-footer.svelte';
|
|
|
|
|
import Handle from './drawer-handle.svelte';
|
|
|
|
|
import { Portal } from '$soma/components/internal';
|
|
|
|
|
|
|
|
|
|
type DrawerNamespace = typeof DrawerComponent & {
|
|
|
|
|
Trigger: typeof Trigger;
|
|
|
|
|
Portal: typeof Portal;
|
|
|
|
|
Overlay: typeof Overlay;
|
|
|
|
|
Content: typeof Content;
|
|
|
|
|
Handle: typeof Handle;
|
|
|
|
|
Title: typeof Title;
|
|
|
|
|
Description: typeof Description;
|
|
|
|
|
Close: typeof Close;
|
|
|
|
|
Header: typeof Header;
|
|
|
|
|
Footer: typeof Footer;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Drawer = DrawerComponent as DrawerNamespace;
|
|
|
|
|
Drawer.Trigger = Trigger;
|
|
|
|
|
Drawer.Portal = Portal;
|
|
|
|
|
Drawer.Overlay = Overlay;
|
|
|
|
|
Drawer.Content = Content;
|
|
|
|
|
Drawer.Handle = Handle;
|
|
|
|
|
Drawer.Title = Title;
|
|
|
|
|
Drawer.Description = Description;
|
|
|
|
|
Drawer.Close = Close;
|
|
|
|
|
Drawer.Header = Header;
|
|
|
|
|
Drawer.Footer = Footer;
|
|
|
|
|
|
|
|
|
|
export { Drawer };
|
|
|
|
|
export default Drawer;
|
|
|
|
|
|
|
|
|
|
export type {
|
|
|
|
|
DrawerProps,
|
|
|
|
|
DrawerTriggerProps as TriggerProps,
|
|
|
|
|
DrawerOverlayProps as OverlayProps,
|
|
|
|
|
DrawerContentProps as ContentProps
|
|
|
|
|
// … etc
|
|
|
|
|
} from './types';
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `types.ts` — passthrough + adiciones eidos
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import type {
|
|
|
|
|
ProviderProps,
|
|
|
|
|
TriggerProps,
|
|
|
|
|
ContentProps
|
|
|
|
|
// …
|
|
|
|
|
} from '$soma/components/drawer';
|
|
|
|
|
|
|
|
|
|
export type DrawerProps = ProviderProps;
|
|
|
|
|
export type DrawerTriggerProps = TriggerProps;
|
|
|
|
|
|
|
|
|
|
// Eidos añade props visuales que el recipe consume vía data-attrs
|
|
|
|
|
export type DrawerContentProps = ContentProps & {
|
|
|
|
|
size?: ResponsiveProp<DrawerSize>;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> **Sin `XxxFlatProps`, sin `XxxProviderProps as ProviderProps` aliases.**
|
|
|
|
|
> Hay un solo `XxxProps` (el del root) y los `XxxPartProps` per hijo.
|
|
|
|
|
|
|
|
|
|
Cuando el tipo root de Soma se importa como `ProviderProps`, se usa solo como
|
|
|
|
|
tipo local dentro de `types.ts`. No se exporta al consumidor con nombre
|
|
|
|
|
`ProviderProps` desde Eidos ni se crea un alias `SomaXxxProviderProps`.
|
|
|
|
|
|
|
|
|
|
### Tipos visuales canonicos
|
|
|
|
|
|
|
|
|
|
No se redeclaran variantes, colores ni tamanos a mano en cada componente.
|
|
|
|
|
La unica fuente para las props visuales compartidas es
|
|
|
|
|
`src/uix/eidos/lib/types.ts`:
|
|
|
|
|
|
|
|
|
|
- `Size` y `ResponsiveProp<T>` para escalas responsivas.
|
|
|
|
|
- `ControlVariant`, `SelectionVariant`, `ChipVariant`, `MarkerVariant`,
|
|
|
|
|
`SurfaceVariant`, etc. para variantes canonicas por familia visual.
|
|
|
|
|
- `ColorRole` para la paleta de intents UIX (`primary`, `secondary`,
|
|
|
|
|
`neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`).
|
|
|
|
|
|
|
|
|
|
Cuando un componente necesita restringir una familia compartida, usa
|
|
|
|
|
`Extract<>` sobre el tipo canonico. No se crean unions locales con literales
|
|
|
|
|
duplicados:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import type { ColorRole, ControlVariant, Size } from '$uix/eidos/lib/types';
|
|
|
|
|
|
|
|
|
|
export type DateFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>;
|
|
|
|
|
export type DateFieldVariant = ControlVariant;
|
|
|
|
|
export type DateFieldColor = ColorRole;
|
|
|
|
|
```
|
|
|
|
|
|
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
### Capas compartidas (shared layers)
|
|
|
|
|
|
|
|
|
|
Cuando VARIOS componentes necesitan la misma geometría, no se copia: se declara
|
|
|
|
|
una vez y se engancha por un **attr de capa** que cada consumidor estampa sobre
|
|
|
|
|
el elemento que ya tiene. Sin nodo envoltorio, sin anidar componentes.
|
|
|
|
|
|
|
|
|
|
Ejemplares vivos: `lib/list-surface.css` (ritmo de menú/listbox, consumido por
|
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado
Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el
encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya
existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y
sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias
prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno
que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el
artefacto de `listbox` (§13), pero total.
**`lib/calendar-surface.css`** (hook `data-calendar-surface`):
- cuatro coordenadas por talla — `padding`, `control-size`, `day-size`,
`font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos
por debajo del bundle de control. Esa desviación estaba escondida en cuatro
bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un
privado con otro nombre; ahora se lee en un sitio.
- la FORMA del anillo de evento y de la marca de festivo.
**Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y
`viewport-placement` componen primitivos del sistema, así que declaran sus
públicos en el fichero y no tienen entrada de receta. Ésta no puede: su
vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config,
así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA
y la capa posee sólo lo que una entrada de receta no sabe expresar.
**El defecto que la justificaba, medido**: `--calendar-event-shadow` y
`--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]`
(audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`,
`month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo:
variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de
`commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token
prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y
ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por
`--_calendar-surface-accent`, que cada superficie alimenta con su propio
forward de paleta THM-2.
**Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker`
y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers
(`date-picker-calendar`, `-month-view`, `-year-view`,
`date-range-picker-calendar`) y un panel portalado no hereda nada del root del
picker. Con sólo los cuatro standalone sellando, ambos quedaban con
`--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era
enganchar la capa a las cuatro identidades de componente: eso viola la regla 1
de capas compartidas, y la respuesta correcta es un sello por wrapper.
computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) ·
year-grid (3.451) · date-picker (464) · date-range-picker (406).
`calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO
código dan 24 en los mismos nodos y las mismas dos propiedades.
Los «missing node» son el propio sello entrando en la clave.
**Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los
selectores month/year es una moneda al aire —
`[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la
MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la
hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel
en un lado ⇒ decisión.
De paso, `calendar-select.css` deja de puentear un privado que sólo
`[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el
fallback, clavado a md fuera cual fuera la talla.
**El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en
`theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global
43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus
consumidores puentean por privados, otra forma, y mueve diez componentes de
golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les
quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO
el catálogo— así que siguen leyendo 0 %.
`recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo
miraba la receta, y una capa que comparte prefijo con un componente la hacía
fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo.
eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) ·
audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) ·
rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el
reformateo en masa que se coló en cuatro README, el test y el censo (churn
ajeno, no mío)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
dropdown-menu · context-menu · menubar · select · combobox · command),
|
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix
Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en
`components/affix/affix.css`, sus consumidores la importaban con
`'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que
es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial`
quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es
geometria.
Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los
tres consumidores importan de ahi. Ninguno depende de ningun componente.
LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo
intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que
el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si
puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de
receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas
que ya son temeables.
Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el
propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave
en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa
exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un
tema mueve la escala de espacio y la escalera de z, no un alias por componente.
Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un
componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus
ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo
de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del
`:root` generado.
Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`,
z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles
haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas
exactas con z 150; y su elemento lleva solo `data-affix`, la identidad.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 ·
smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
`eidos/lib/viewport-placement.css` (colocación contra el viewport, consumida por
|
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado
Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el
encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya
existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y
sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias
prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno
que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el
artefacto de `listbox` (§13), pero total.
**`lib/calendar-surface.css`** (hook `data-calendar-surface`):
- cuatro coordenadas por talla — `padding`, `control-size`, `day-size`,
`font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos
por debajo del bundle de control. Esa desviación estaba escondida en cuatro
bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un
privado con otro nombre; ahora se lee en un sitio.
- la FORMA del anillo de evento y de la marca de festivo.
**Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y
`viewport-placement` componen primitivos del sistema, así que declaran sus
públicos en el fichero y no tienen entrada de receta. Ésta no puede: su
vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config,
así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA
y la capa posee sólo lo que una entrada de receta no sabe expresar.
**El defecto que la justificaba, medido**: `--calendar-event-shadow` y
`--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]`
(audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`,
`month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo:
variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de
`commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token
prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y
ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por
`--_calendar-surface-accent`, que cada superficie alimenta con su propio
forward de paleta THM-2.
**Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker`
y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers
(`date-picker-calendar`, `-month-view`, `-year-view`,
`date-range-picker-calendar`) y un panel portalado no hereda nada del root del
picker. Con sólo los cuatro standalone sellando, ambos quedaban con
`--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era
enganchar la capa a las cuatro identidades de componente: eso viola la regla 1
de capas compartidas, y la respuesta correcta es un sello por wrapper.
computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) ·
year-grid (3.451) · date-picker (464) · date-range-picker (406).
`calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO
código dan 24 en los mismos nodos y las mismas dos propiedades.
Los «missing node» son el propio sello entrando en la clave.
**Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los
selectores month/year es una moneda al aire —
`[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la
MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la
hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel
en un lado ⇒ decisión.
De paso, `calendar-select.css` deja de puentear un privado que sólo
`[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el
fallback, clavado a md fuera cual fuera la talla.
**El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en
`theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global
43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus
consumidores puentean por privados, otra forma, y mueve diez componentes de
golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les
quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO
el catálogo— así que siguen leyendo 0 %.
`recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo
miraba la receta, y una capa que comparte prefijo con un componente la hacía
fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo.
eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) ·
audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) ·
rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el
reformateo en masa que se coló en cuatro README, el test y el censo (churn
ajeno, no mío)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
`Affix` · `Fab` · `MenuDial`) y `lib/calendar-surface.css` (ritmo de la rejilla
|
|
|
|
|
de fechas y forma del anillo de evento, consumida por `Calendar` ·
|
|
|
|
|
`RangeCalendar` · `MonthGrid` · `YearGrid` y los wrappers de `DatePicker` /
|
|
|
|
|
`DateRangePicker`).
|
|
|
|
|
|
|
|
|
|
`calendar-surface` es el ejemplar HÍBRIDO y conviene leerlo antes de escribir
|
|
|
|
|
otra capa: las dos primeras componen primitivos del sistema, así que declaran
|
|
|
|
|
sus públicos en el propio fichero y no tienen entrada en `recipes/base.ts`. La
|
|
|
|
|
familia calendar no puede — su vocabulario son 76 claves SEMÁNTICAS que un tema
|
|
|
|
|
alcanza una a una por config —, así que la entrada `calendar` de la receta es la
|
|
|
|
|
de la FAMILIA y la capa posee sólo lo que una entrada de receta no sabe
|
|
|
|
|
expresar: la resolución por talla sobre un hook compartido, y la forma del
|
|
|
|
|
anillo.
|
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
|
|
|
|
|
Las reglas, y las tres primeras se aprendieron rompiéndose:
|
|
|
|
|
|
|
|
|
|
1. **La geometría engancha en el attr de CAPA, nunca en la identidad del
|
|
|
|
|
componente.** `morfo-check` selecciona `[data-{kebab}]` en toda la página y
|
|
|
|
|
valida cada coincidencia contra ese morfo — si un consumidor estampara la
|
|
|
|
|
identidad para heredar la geometría, quedaría soldado a un contrato ajeno.
|
|
|
|
|
2. **Un eje = un token público + una ranura privada**, consumido como
|
|
|
|
|
`var(--_x, var(--x))`. La capa posee el default; el consumidor escribe la
|
|
|
|
|
ranura si ha evaluado el eje y aterriza en otro sitio. Un consumidor **no
|
|
|
|
|
acuña `--{componente}-{eje}`**: sería un vocabulario paralelo para valores que
|
|
|
|
|
la capa ya posee. Y si el prop escribiera el mismo nombre que la capa lee, un
|
|
|
|
|
valor que referencie el token se vuelve una custom property cíclica —
|
|
|
|
|
_guaranteed-invalid_, `calc()` muerto, insets a `auto`.
|
|
|
|
|
3. **El puente reafirma `position` si el primitivo compuesto declara uno.** La
|
|
|
|
|
regla base de la capa tiene especificidad (0,1,0) y los ficheros van
|
|
|
|
|
code-split, así que un `position` del primitivo al mismo peso decide por orden
|
|
|
|
|
de carga. `Fab` lo debe (compone `<Button>`); `MenuDial` no (no compone nada
|
|
|
|
|
en el marco).
|
|
|
|
|
4. **Se guarda con dos redes**, porque ninguna basta sola: el texto
|
|
|
|
|
(`shared-layer-contract.test.ts`) ve lo que el navegador no puede — un
|
|
|
|
|
`env()` ya sustituido —, y `npm run layer:check` ve lo que el texto no puede
|
|
|
|
|
— quién gana la cascada de verdad.
|
|
|
|
|
|
|
|
|
|
### Partes Eidos-only
|
|
|
|
|
|
|
|
|
|
Algunas partes existen solo para componer la superficie visual: `Header`,
|
|
|
|
|
`Footer`, `Status`, `Main`, `Handle`, filas/labels planas, indicadores
|
|
|
|
|
decorativos o primitives `svg`. No necesitan morfo propio mientras cumplan
|
|
|
|
|
las cuatro reglas:
|
|
|
|
|
|
|
|
|
|
1. No crean comportamiento ni estado.
|
|
|
|
|
2. No son target de eventos perceptivos.
|
|
|
|
|
3. No poseen ARIA obligatoria ni relaciones accesibles propias.
|
|
|
|
|
4. Solo emiten estructura, clase/estilo passthrough o `data-*` visuales que
|
|
|
|
|
el recipe consume.
|
|
|
|
|
|
|
|
|
|
Si cualquiera de esas reglas deja de cumplirse, la parte deja de ser
|
|
|
|
|
Eidos-only y debe subir al contrato correspondiente: primero morfo, luego Soma
|
|
|
|
|
si necesita runtime.
|
|
|
|
|
|
|
|
|
|
El agregador `scripts/eidos-lint-all.ts` mantiene una allowlist explícita de
|
|
|
|
|
estos attrs/parts visuales. El lint base sigue siendo estricto con valores
|
|
|
|
|
inválidos de enums morfo; la allowlist solo evita que el reporte de drift se
|
|
|
|
|
llene de partes visuales intencionales.
|
|
|
|
|
|
|
|
|
|
`src/uix/eidos/component-api-contract.test.ts` protege la forma pública del
|
|
|
|
|
barrel: sin `Object.assign`, sin `Provider` público, root en
|
|
|
|
|
`{component}.svelte` y miembros del `XxxNamespace` sincronizados con sus
|
|
|
|
|
asignaciones explícitas (`Drawer.Trigger = Trigger`, etc.).
|
|
|
|
|
|
|
|
|
|
La guardia `src/uix/eidos/component-visual-attrs.test.ts` fija el cableado
|
|
|
|
|
mínimo entre wrapper y recipe: si un wrapper visual declara una prop que se
|
|
|
|
|
consume como `data-*` (`size`, `variant`, `color`, `position`, `columns`,
|
|
|
|
|
etc.), el fichero debe seguir estampando ese atributo. Esto no añade
|
|
|
|
|
comportamiento a Eidos; solo evita que la capa visual trague props en silencio.
|
|
|
|
|
|
|
|
|
|
### Uso del consumidor
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { Drawer } from '$uix/eidos/components/drawer';
|
|
|
|
|
let open = $state(false);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Drawer bind:open variant="overlay" direction="right">
|
|
|
|
|
<Drawer.Trigger>Open</Drawer.Trigger>
|
|
|
|
|
<Drawer.Portal>
|
|
|
|
|
<Drawer.Overlay />
|
|
|
|
|
<Drawer.Content>
|
|
|
|
|
<Drawer.Header>
|
|
|
|
|
<Drawer.Title>Title</Drawer.Title>
|
|
|
|
|
<Drawer.Description>Subtitle</Drawer.Description>
|
|
|
|
|
</Drawer.Header>
|
|
|
|
|
<p>body</p>
|
|
|
|
|
<Drawer.Footer>
|
|
|
|
|
<Drawer.Close>Close</Drawer.Close>
|
|
|
|
|
</Drawer.Footer>
|
|
|
|
|
</Drawer.Content>
|
|
|
|
|
</Drawer.Portal>
|
|
|
|
|
</Drawer>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Componentes single-part (toggle, icon)
|
|
|
|
|
|
|
|
|
|
Sin hijos compound. El default export ES el componente entero. Sin
|
|
|
|
|
namespace, sin asignación de propiedades, sin `Provider` alias.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// toggle/index.ts
|
|
|
|
|
export { default } from './toggle.svelte';
|
|
|
|
|
export type { ToggleProps, ToggleVariant, ToggleSize } from './types';
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
import Toggle from '$uix/eidos/components/toggle';
|
|
|
|
|
<Toggle bind:pressed variant="solid">Bold</Toggle>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Partes visuales por defecto
|
|
|
|
|
|
|
|
|
|
Algunos componentes multi-part necesitan una parte visual minima para no
|
|
|
|
|
renderizar una superficie rota. `Switch` es el caso canonico: el root sigue
|
|
|
|
|
exponiendo `Switch.Thumb`, pero si el consumidor no aporta children, `<Switch />`
|
|
|
|
|
monta un thumb visual por defecto. Esto no crea una API flat ni esconde el
|
|
|
|
|
contrato de Soma; solo evita que el uso minimo renderice un track sin indicador.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Toast — caso especial (dos roots independientes)
|
|
|
|
|
|
|
|
|
|
`Toast` tiene dos roots públicos que NO se anidan:
|
|
|
|
|
|
|
|
|
|
- `<Toast>` — manual compound (Provider+Viewport+Item iteración del
|
|
|
|
|
consumer). Children attached: Viewport, Item, Status, Main, Title,
|
|
|
|
|
Description, Action, Close.
|
|
|
|
|
- `<Toaster />` — imperative auto-mount. Renderiza Provider+Viewport+
|
|
|
|
|
Item-loop con un template default. Pasas `toaster` de
|
|
|
|
|
`createToaster()`. Export separado, no `Toast.Toaster` porque es un
|
|
|
|
|
root competidor, no un hijo.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { Toast, Toaster, createToaster } from '$uix/eidos/components/toast';
|
|
|
|
|
|
|
|
|
|
const t = createToaster();
|
|
|
|
|
|
|
|
|
|
// Imperative
|
|
|
|
|
<Toaster toaster={t} />
|
|
|
|
|
|
|
|
|
|
// Manual
|
|
|
|
|
<Toast toaster={t}>
|
|
|
|
|
<Toast.Viewport>
|
|
|
|
|
{#each t.toasts as toast}
|
|
|
|
|
<Toast.Item {toast}>
|
|
|
|
|
<Toast.Title>{toast.title}</Toast.Title>
|
|
|
|
|
</Toast.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</Toast.Viewport>
|
|
|
|
|
</Toast>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Convenciones de naming de tokens CSS
|
|
|
|
|
|
|
|
|
|
Los tokens CSS llevan el nombre del **componente**, no de la **capa**.
|
|
|
|
|
Sin prefijos de capa — ni `--eidos-`, ni `--air-`, ni `--terra-`,
|
|
|
|
|
ni `--soma-`.
|
|
|
|
|
|
|
|
|
|
| Forma | Uso | Ejemplo |
|
|
|
|
|
| ------------------ | ----------------------------------------------------- | ------------------------------------------- |
|
|
|
|
|
| `--{component}-…` | tokens públicos (sobreescribibles por el consumer) | `--dialog-content-bg`, `--toggle-height-md` |
|
|
|
|
|
| `--_{component}-…` | tokens internos del recipe (no parte del API público) | `--_tabs-trigger-height`, `--_toggle-bg` |
|
|
|
|
|
|
|
|
|
|
Los `[data-{component}]` y `[data-{component}-{part}]` selectors son la
|
|
|
|
|
única vía pública para que el recipe se acople al runtime — toda
|
|
|
|
|
información cross-layer pasa por data-attrs declarados en el morfo.
|
|
|
|
|
Cada `--{component}-*` consumido por CSS debe salir de `EidosConfig.recipes`;
|
|
|
|
|
si un alias publico queda sin consumidor real, el test
|
|
|
|
|
`src/uix/eidos/recipe-css-contract.test.ts` falla.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Comparativa obligatoria por componente
|
|
|
|
|
|
|
|
|
|
Ningun componente Eidos se declara cerrado solo por envolver Soma. Antes de
|
|
|
|
|
implementar o revisar un componente:
|
|
|
|
|
|
|
|
|
|
1. Leer Air en la rama anterior (`morfo-runtime:src/uix/air/components/{name}`)
|
|
|
|
|
cuando exista. Air es la primera baseline visual.
|
|
|
|
|
2. Leer Soma/Morfo actuales para separar comportamiento, ARIA, estado,
|
|
|
|
|
traducciones y data-attrs de la superficie visual Eidos.
|
|
|
|
|
3. Auditar Morfo/Sema. El morfo no se considera correcto solo porque compile:
|
|
|
|
|
- Clasificar el componente como pasivo, interactivo o mixto.
|
|
|
|
|
- Justificar cualquier `0 events` de forma explicita.
|
|
|
|
|
- Para cada accion real de usuario revisar `family`, `verb`, `sequence`,
|
|
|
|
|
`intent`, `target`, `prewrite` y `commit`.
|
|
|
|
|
- Verificar que Soma dispara esas ocurrencias con `runtime.trigger(...)`.
|
|
|
|
|
- Evitar eventos de alta frecuencia para cambios continuos; normalmente se
|
|
|
|
|
modelan inicio/drag confirmado/commit, no cada frame.
|
|
|
|
|
4. Comparar contra referentes externos relevantes: Radix/Radix Themes, Ark UI,
|
|
|
|
|
Bits UI, shadcn-svelte y React Aria cuando aplique.
|
|
|
|
|
5. Crear/actualizar `components/{name}/README.md` con tabla de funcionalidades,
|
|
|
|
|
tabla Morfo/Sema, gaps y decisiones. Cada `⚠️` / `❌` debe acabar en una
|
|
|
|
|
decision explicita: implementar ahora, diferir a Morfo/Soma, diferir a v2 o
|
|
|
|
|
descartar por no pertenecer a Eidos.
|
|
|
|
|
6. Si hay demo en `web/routes/uix/components/{name}`, sus snippets forman parte
|
|
|
|
|
de la arquitectura del componente: deben reproducir el mismo contrato visible
|
|
|
|
|
que el preview. En componentes schema-driven (`form`, `auto-fields`,
|
|
|
|
|
date/time con formatos, etc.) no se permite un schema reducido que omita
|
|
|
|
|
campos visibles, validators o defaults reales.
|
|
|
|
|
7. Solo despues tocar wrapper, recipe o tokens.
|
|
|
|
|
|
|
|
|
|
El objetivo no es copiar APIs, sino que Eidos no quede por debajo de Air ni de
|
|
|
|
|
los referentes en funcionalidades reales. Si una capacidad pertenece a Soma, la
|
|
|
|
|
tabla debe decirlo; si es visual, Eidos debe cubrirla o justificar el gap.
|
|
|
|
|
|
|
|
|
|
### Reglas especificas para demos de formularios
|
|
|
|
|
|
|
|
|
|
- La validacion en tiempo real se modela como
|
|
|
|
|
`validationBehaviour: 'onChange'`.
|
|
|
|
|
- Si una demo arranca en `onChange`, los defaults iniciales deben ser validos
|
|
|
|
|
salvo que el README y la UI digan explicitamente que se esta mostrando un
|
|
|
|
|
formulario inicialmente invalido.
|
|
|
|
|
- El snippet de Soma y el snippet de Eidos deben declarar los mismos campos
|
|
|
|
|
visibles que el preview: imports SIUM, schema, defaults, `Form + Field` y
|
|
|
|
|
`Form.AutoFields`. No se admiten snippets que omiten `age`, `role`,
|
|
|
|
|
campos de fecha, arrays u otros datos que el preview valida.
|
|
|
|
|
- `Form.AutoFields` es renderer reflectivo de SIUM. Si necesita comportamiento
|
|
|
|
|
nuevo, se cambia Soma / `$libs/forms` / Morfo antes que Eidos.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Forma legacy (CSS-only, retired)
|
|
|
|
|
|
|
|
|
|
Hubo una fase anterior cuando eidos sólo emitía CSS (`accordion.css`,
|
|
|
|
|
`dialog.css`, etc., al nivel raíz de `components/`). Todos los
|
|
|
|
|
componentes han sido migrados a la forma canónica del subdirectorio.
|
|
|
|
|
Si encuentras un `.css` suelto, es un descuido — debe vivir dentro de
|
|
|
|
|
su subdirectorio.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Inventario
|
|
|
|
|
|
|
|
|
|
El inventario vivo de wrappers es el árbol de subdirectorios de
|
|
|
|
|
`components/` + `npm run component:audit`. La tabla de estado de la
|
|
|
|
|
migración 2026-05 (completada) se conserva en
|
|
|
|
|
[`docs/process/handoffs-2026-05.md`](../../../../docs/process/handoffs-2026-05.md).
|