From dd188f1255ce566c9823219e30c8d09842e2c468 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 21 Aug 2026 21:11:18 +0200 Subject: [PATCH] =?UTF-8?q?uix(tree-view):=20temable=20=E2=80=94=200=20%?= =?UTF-8?q?=20=E2=86=92=2092=20%,=20y=20el=20velo=20que=20ti=C3=B1e=20la?= =?UTF-8?q?=20carpeta=20entera?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 46 claves en el bloque `tree-view` de `recipes/base.ts` (fusionadas con el forward de paleta). Censo 0 % → 92 %, cero globales. Global 46 %, sin contrato 47 → 46. Molde del hermano `tree-grid` allí donde el problema es el mismo. LA PROPUESTA GENERADA FALLABA EN EL NOMBRE DE CASI TODO LO QUE IMPORTA: el clasificador toma el PRIMER selector de cada regla como si fuera la parte pintada, y aquí ese primero es el root o el branch-control cuando el nodo real es otro. 1. `root-width: 1px` / `root-bg` NO son del root: son la GUÍA de indentación (el `::before` del branch, una línea vertical). → `guide-width` + `guide-fg`, literalmente los nombres de tree-grid. Un `root-bg` que tiñe una línea es la clase de nombre que miente. 2. `branch-control-padding-inline-{k}` ⚠ fundía DOS ejes que se SUMAN en el mismo calc: el ritmo de la fila y el PASO DE PROFUNDIDAD (`--tree-depth × indent + padding`). Fundidos, tematizar la sangría habría movido el padding. → `row-padding-inline-{k}` + `indent-{k}`. 3. El prefijo `branch-control-` es erróneo: la regla reza `[data-tree-view-branch-control], [data-tree-view-item]` — las DOS filas. El CSS ya llamaba `row` a sus privados. → `row-*`, como tree-grid y como la lección de table. Y fuera el prefijo `root-` del chasis: el wrapper ES el componente. 4. `row-padding-block` ⚠ no eran dos valores en pugna sino cinco por talla: xs colapsa a `0`, las otras cuatro `--space-1`. 5. `branch-indicator-width` + `-height` son UN knob (`1em`) → `branch-indicator-size`. Bundle: la altura de fila casa 1:1; padding e indent llevan ritmo propio; la tipografía un paso por debajo desde lg — verbatim, como table y tree-grid. Los resueltos con `parts: ['root']` (el `data-size` va en `[data-tree-view-root]`, wrapper de eidos, no parte del morfo). Los `transparent` de outline/ghost y el `padding: 0` de ghost son identidad de variante y siguen literales. `hover-row-bg` SÍ se acuña, al revés que en grid-list, y por medición: la misma regla pinta dos nodos con arquetipo distinto, y `branch-control` NO lleva archetype — ahí el plano de la receta es la única pintura (velo medido ausente) y el token es toda la superficie de tema del hover. Tener un consumidor legítimo es lo que lo distingue del caso grid-list. EL DEFECTO QUE LA MEDICIÓN DESTAPÓ (preexistente, NO se toca aquí → next-features §12.8): al pasar el ratón por la fila de una carpeta abierta se tiñe LA CARPETA ENTERA, hijos incluidos. `archetype: 'item'` está declarado en el `
  • ` branch (`morfo/components/tree-view.ts:103`), que contiene el control Y todo el `branch-content`, mientras la fila que el usuario señala es el `branch-control` de dentro: el nodo velado mide 336 px frente a los 36 px de la fila — medido y capturado. Un branch anidado acumula además el velo de sus ancestros, así que sus filas se ven más oscuras que las hermanas. Mover el arquetipo es morfo y mueve píxel en toda la demo. Es la TERCERA variante de la misma familia: table lo tiene en fila Y celda, grid-list en fila Y celda sobre el mismo nodo, y tree-view en un ANCESTRO del nodo señalado. Artefactos y gates: - Sonda antes/después = 0 diffs en 8.062 valores × 7 estados. - R-5.4: 45/46; `disabled-row-fg` adjudicado en el ledger (la demo monta CERO filas deshabilitadas, contadas; forzada alcanza oklch(0.61 0 0) → rgb(1,2,3)). - censo --only 92 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio (72 globales, ninguno mío). - README con su «Talla y tema» y tab `Tokens` en la demo (46 claves, en vivo). Co-Authored-By: Claude Fable 5 --- docs/audit/theming/README.md | 20 +- docs/audit/theming/tree-view.md | 176 ++++++++---------- docs/next-features.md | 18 ++ scripts/theming-sentinel-exceptions.ts | 4 + src/uix/eidos/components/tree-view/README.md | 58 ++++++ .../eidos/components/tree-view/tree-view.css | 99 ++++------ src/uix/eidos/generated/base.css | 78 ++++++++ src/uix/eidos/lib/recipes/base.ts | 127 +++++++++++++ .../uix/components/tree-view/+page.svelte | 10 +- 9 files changed, 421 insertions(+), 169 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index b6152b954..3870f4802 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-21 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? -- **Alcance global**: **46%** — 2150 de 4680 knobs pasan por un token público del componente -- **Reparto**: público 2150 · privado 661 · global 1286 · literal 583 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 47 · **alcance < 20 %**: 41 · **alcance 100 %**: 7 · **con eje `size`**: 66 +- **Alcance global**: **46%** — 2174 de 4680 knobs pasan por un token público del componente +- **Reparto**: público 2174 · privado 654 · global 1272 · literal 580 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 46 · **alcance < 20 %**: 40 · **alcance 100 %**: 7 · **con eje `size`**: 65 ## Cómo se lee @@ -86,17 +86,17 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 14 | [radio-cards](./radio-cards.md) | 28 | 60% | 72 | 39 | | 15 | [drawer](./drawer.md) | 27 | 55% | 62 | 44 | | 16 | [code-block](./code-block.md) | 26 | 0% | 26 | 0 | -| 17 | [tree-view](./tree-view.md) | 26 | 0% | 28 | 0 | -| 18 | [mockup](./mockup.md) | 25 | 14% | 29 | 0 | -| 19 | [aura](./aura.md) | 24 | 31% | 35 | 24 | -| 20 | [password-field](./password-field.md) | 24 | 31% | 36 | 23 | +| 17 | [mockup](./mockup.md) | 25 | 14% | 29 | 0 | +| 18 | [aura](./aura.md) | 24 | 31% | 35 | 24 | +| 19 | [password-field](./password-field.md) | 24 | 31% | 36 | 23 | +| 20 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 | -## Sin ningún token público propio (47) +## Sin ningún token público propio (46) Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward (`_palette-*`), que no son contrato: un tema no puede nombrarlos. -[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`tree-view`](./tree-view.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) +[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) ## Tabla completa (162 recetas, por alcance ascendente) @@ -108,7 +108,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [month-grid](./month-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [year-grid](./year-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – | -| [tree-view](./tree-view.md) | 0% | 28 | 0 | 8 | 14 | 4 | 2 | 0 | y | | [code-block](./code-block.md) | 0% | 26 | 0 | 2 | 24 | 0 | 0 | 0 | – | | [card-group](./card-group.md) | 0% | 24 | 0 | 0 | 18 | 4 | 2 | 0 | y | | [spinner](./spinner.md) | 0% | 23 | 0 | 15 | 4 | 4 | 0 | 0 | y | @@ -240,6 +239,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [chat-typing](./chat-typing.md) | 89% | 9 | 8 | 0 | 1 | 0 | 0 | 7 | – | | [breadcrumb](./breadcrumb.md) | 90% | 23 | 18 | 2 | 0 | 0 | 3 | 28 | y | | [collapsible](./collapsible.md) | 90% | 12 | 9 | 0 | 1 | 0 | 2 | 11 | – | +| [tree-view](./tree-view.md) | 92% | 28 | 24 | 1 | 0 | 1 | 2 | 46 | – | | [feed](./feed.md) | 94% | 33 | 30 | 0 | 1 | 1 | 1 | 50 | – | | [scroll-area](./scroll-area.md) | 94% | 17 | 15 | 0 | 0 | 1 | 1 | 13 | – | | [tree-grid](./tree-grid.md) | 95% | 42 | 37 | 1 | 0 | 1 | 3 | 47 | – | diff --git a/docs/audit/theming/tree-view.md b/docs/audit/theming/tree-view.md index 69311bdd1..feff0f5fc 100644 --- a/docs/audit/theming/tree-view.md +++ b/docs/audit/theming/tree-view.md @@ -5,53 +5,28 @@ > Vista de conjunto: [README](./README.md) · método y protocolo: > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. -- **Medido**: 2026-08-20 · **Alcance**: **0%** — 0 de 26 knobs por token público -- **Knobs de apariencia**: 28 — público 0 · privado 8 · global 14 · literal 4 · sistema 2 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 0 pública(s) · 1 privada(s) forward — `_palette-element` -- **Eje `size`**: sí · **ficheros**: `tree-view.css` +- **Medido**: 2026-08-21 · **Alcance**: **92%** — 24 de 26 knobs por token público +- **Knobs de apariencia**: 28 — público 24 · privado 1 · global 0 · literal 1 · sistema 2 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 46 pública(s) — `row-height-xs`, `row-height-sm`, `row-height-md`, `row-height-lg`, `row-height-xl`, `row-padding-block-xs`, `row-padding-block-sm`, `row-padding-block-md`, `row-padding-block-lg`, `row-padding-block-xl`, `row-padding-inline-xs`, `row-padding-inline-sm`, `row-padding-inline-md`, `row-padding-inline-lg`, `row-padding-inline-xl`, `indent-xs`, `indent-sm`, `indent-md`, `indent-lg`, `indent-xl`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `row-height`, `row-padding-block`, `row-padding-inline`, `indent`, `font-size`, `radius`, `font-family`, `line-height`, `fg`, `border-width`, `border`, `surface-bg`, `padding`, `row-gap`, `row-radius`, `hover-row-bg`, `disabled-row-fg`, `branch-indicator-size`, `branch-indicator-fg`, `guide-width`, `guide-fg` · 1 privada(s) forward — `_palette-element` +- **Eje `size`**: no · **ficheros**: `tree-view.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (14) +### 1.1 Directo a primitivo global (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `tree-view.css:34` | `[data-tree-view-root]` | `font-family` | `var(--style-label-font-family)` | -| 2 | `tree-view.css:36` | `[data-tree-view-root]` | `line-height` | `var(--leading-ui)` | -| 3 | `tree-view.css:37` | `[data-tree-view-root]` | `color` | `var(--color-content-primary)` | -| 4 | `tree-view.css:47` | `[data-tree-view-root][data-variant='surface']` | `border` | `var(--border-width) solid var(--color-border-default)` | -| 5 | `tree-view.css:48` | `[data-tree-view-root][data-variant='surface']` | `background` | `var(--color-surface-default)` | -| 6 | `tree-view.css:49` | `[data-tree-view-root][data-variant='surface']` | `padding` | `var(--space-2)` | -| 7 | `tree-view.css:53` | `[data-tree-view-root][data-variant='outline']` | `border` | `var(--border-width) solid var(--color-border-default)` | -| 8 | `tree-view.css:55` | `[data-tree-view-root][data-variant='outline']` | `padding` | `var(--space-2)` | -| 9 | `tree-view.css:59` | `[data-tree-view-root][data-variant='ghost']` | `border` | `var(--border-width) solid transparent` | -| 10 | `tree-view.css:121` | `[data-tree-view-branch-control], [data-tree-view-item]` | `gap` | `var(--space-2)` | -| 11 | `tree-view.css:128` | `[data-tree-view-branch-control], [data-tree-view-item]` | `border-radius` | `var(--radius-sm)` | -| 12 | `tree-view.css:137` | `[data-tree-view-branch-control]:hover:not([data-disabled]), [data-tree-view-item]:hover:not([data-disabled])` | `background` | `var(--color-surface-overlay)` | -| 13 | `tree-view.css:147` | `[data-tree-view-branch-control][data-disabled], [data-tree-view-item][data-disabled]` | `color` | `var(--color-content-muted)` | -| 14 | `tree-view.css:166` | `[data-tree-view-branch-indicator]` | `color` | `var(--color-content-secondary)` | - -### 1.2 A través de un privado (8) +_Ninguno._ + +### 1.2 A través de un privado (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `tree-view.css:33` | `[data-tree-view-root]` | `border-radius` | `var(--_tree-view-radius)` | -| 2 | `tree-view.css:35` | `[data-tree-view-root]` | `font-size` | `var(--_tree-view-font-size)` | -| 3 | `tree-view.css:122` | `[data-tree-view-branch-control], [data-tree-view-item]` | `min-block-size` | `var(--_tree-view-row-block-size)` | -| 4 | `tree-view.css:123` | `[data-tree-view-branch-control], [data-tree-view-item]` | `padding-block` | `var(--_tree-view-row-padding-block)` | -| 5 | `tree-view.css:124` | `[data-tree-view-branch-control], [data-tree-view-item]` | `padding-inline-end` | `var(--_tree-view-row-padding-inline)` | -| 6 | `tree-view.css:125` | `[data-tree-view-branch-control], [data-tree-view-item]` | `padding-inline-start` | `calc( var(--tree-depth, 0) * var(--_tree-view-indent) + var(--_tree-view-row-padding-inline) )` | -| 7 | `tree-view.css:142` | `[data-tree-view-branch-control][data-selected]:not([data-disabled]), [data-tree-view-item][data-selected]:not([data-disabled])` | `background` | `var(--_tree-view-palette-element)` | -| 8 | `tree-view.css:200` | `[data-tree-view-root][data-indent-guides] [data-tree-view-branch]::before` | `background` | `var(--_tree-view-guide)` | +| 1 | `tree-view.css:115` | `[data-tree-view-branch-control][data-selected]:not([data-disabled]), [data-tree-view-item][data-selected]:not([data-disabled])` | `background` | `var(--_tree-view-palette-element)` | -### 1.3 Literales (4) +### 1.3 Literales (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `tree-view.css:42` | `[data-tree-view-root][data-block]` | `inline-size` | `100%` | -| 2 | `tree-view.css:164` | `[data-tree-view-branch-indicator]` | `inline-size` | `1em` | -| 3 | `tree-view.css:165` | `[data-tree-view-branch-indicator]` | `block-size` | `1em` | -| 4 | `tree-view.css:199` | `[data-tree-view-root][data-indent-guides] [data-tree-view-branch]::before` | `inline-size` | `1px` | +| 1 | `tree-view.css:38` | `[data-tree-view-root][data-block]` | `inline-size` | `100%` | ## 2. Sistema transversal (2) — informativo, fuera del ratio @@ -59,28 +34,18 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `tree-view.css:149` | `[data-tree-view-branch-control][data-disabled], [data-tree-view-item][data-disabled]` | `opacity` | `var(--opacity-disabled)` | -| 2 | `tree-view.css:154` | `[data-tree-view-branch-control]:focus-visible, [data-tree-view-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 1 | `tree-view.css:122` | `[data-tree-view-branch-control][data-disabled], [data-tree-view-item][data-disabled]` | `opacity` | `var(--opacity-disabled)` | +| 2 | `tree-view.css:127` | `[data-tree-view-branch-control]:focus-visible, [data-tree-view-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | ## 3. Privados de la receta — ¿de dónde sale su valor? -| privado | declaraciones | valor(es) | origen | ¿deriva de un público? | -| --- | ---: | --- | --- | :-: | -| `--_tree-view-radius` | 1 | `var(--radius-md)` | global | no | -| `--_tree-view-row-block-size` | 5 | `var(--control-height-md)`, `var(--control-height-xs)`, `var(--control-height-sm)`, `var(--control-height-lg)`, `var(--control-height-xl)` | global | no | -| `--_tree-view-row-padding-block` | 2 | `var(--space-1)`, `0` | global, literal | no | -| `--_tree-view-row-padding-inline` | 5 | `var(--space-2)`, `var(--space-1)`, `var(--space-3)`, `var(--space-4)` | global | no | -| `--_tree-view-indent` | 5 | `var(--space-4)`, `var(--space-3)`, `var(--space-5)`, `var(--space-6)` | global | no | -| `--_tree-view-font-size` | 5 | `var(--font-size-sm)`, `var(--font-size-xs)`, `var(--font-size-md)`, `var(--font-size-lg)` | global | no | -| `--_tree-view-guide` | 1 | `var(--color-border-default)` | global | no | +_La receta no declara privados propios en su CSS._ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_tree-view-palette-element`. ## 4. Propuesta de corrección -- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo). - -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (30) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0) Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla @@ -90,54 +55,12 @@ no distingue lo que debería — se marca `⚠`. | token (`--tree-view-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `branch-control-padding-block` | `root` | ⚠ `var(--space-1)` / `0` | 2 | -| `branch-control-padding-inline-md` | `size:md` | ⚠ `var(--space-2)` / `var(--space-4)` | 2 | -| `branch-control-padding-inline-xs` | `size:xs` | ⚠ `var(--space-1)` / `var(--space-3)` | 2 | -| `branch-control-padding-inline-sm` | `size:sm` | ⚠ `var(--space-2)` / `var(--space-3)` | 2 | -| `branch-control-padding-inline-lg` | `size:lg` | ⚠ `var(--space-3)` / `var(--space-5)` | 2 | -| `branch-control-padding-inline-xl` | `size:xl` | ⚠ `var(--space-4)` / `var(--space-6)` | 2 | -| `root-radius` | `root` | `var(--radius-md)` | 1 | -| `root-font-family` | `root` | `var(--style-label-font-family)` | 1 | -| `root-font-size-md` | `size:md` | `var(--font-size-sm)` | 1 | -| `root-font-size-xs` | `size:xs` | `var(--font-size-xs)` | 1 | -| `root-font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 | -| `root-font-size-lg` | `size:lg` | `var(--font-size-md)` | 1 | -| `root-font-size-xl` | `size:xl` | `var(--font-size-lg)` | 1 | -| `root-line-height` | `root` | `var(--leading-ui)` | 1 | -| `root-fg` | `root` | `var(--color-content-primary)` | 1 | -| `surface-root-bg` | `root` | `var(--color-surface-default)` | 1 | -| `branch-control-gap` | `root` | `var(--space-2)` | 1 | -| `branch-control-height-md` | `size:md` | `var(--control-height-md)` | 1 | -| `branch-control-height-xs` | `size:xs` | `var(--control-height-xs)` | 1 | -| `branch-control-height-sm` | `size:sm` | `var(--control-height-sm)` | 1 | -| `branch-control-height-lg` | `size:lg` | `var(--control-height-lg)` | 1 | -| `branch-control-height-xl` | `size:xl` | `var(--control-height-xl)` | 1 | -| `branch-control-radius` | `root` | `var(--radius-sm)` | 1 | -| `hover-branch-control-bg` | `root` | `var(--color-surface-overlay)` | 1 | -| `disabled-branch-control-fg` | `root` | `var(--color-content-muted)` | 1 | -| `branch-indicator-width` | `root` | `1em` | 1 | -| `branch-indicator-height` | `root` | `1em` | 1 | -| `branch-indicator-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `root-width` | `root` | `1px` | 1 | -| `root-bg` | `root` | `var(--color-border-default)` | 1 | - -### 4.2 Sin nombre mecánico (7) + +### 4.2 Sin nombre mecánico (2) - **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `inline-size`. -- **⚠ decisión: `border` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 3: `border`. -- **⚠ decisión: `padding` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 2: `padding`. - **⚠ decisión: el privado que alimenta este knob no se declara en el CSS (viene de `base.ts` o de un estilo inline) — hay que resolverlo antes de nombrarlo** — 1: `background`. -### 4.3 Avisos sobre los tokens propuestos (7) - -- **el privado `--_tree-view-radius` debe pasar a leer este público (o desaparecer)** — `--tree-view-root-radius` -- **el privado `--_tree-view-font-size` debe pasar a leer este público (o desaparecer)** — `--tree-view-root-font-size-md`, `--tree-view-root-font-size-xs`, `--tree-view-root-font-size-sm`, `--tree-view-root-font-size-lg`, `--tree-view-root-font-size-xl` -- **el privado `--_tree-view-row-block-size` debe pasar a leer este público (o desaparecer)** — `--tree-view-branch-control-height-md`, `--tree-view-branch-control-height-xs`, `--tree-view-branch-control-height-sm`, `--tree-view-branch-control-height-lg`, `--tree-view-branch-control-height-xl` -- **el privado `--_tree-view-row-padding-block` debe pasar a leer este público (o desaparecer)** — `--tree-view-branch-control-padding-block` -- **el privado `--_tree-view-row-padding-inline` debe pasar a leer este público (o desaparecer)** — `--tree-view-branch-control-padding-inline-md`, `--tree-view-branch-control-padding-inline-xs`, `--tree-view-branch-control-padding-inline-sm`, `--tree-view-branch-control-padding-inline-lg`, `--tree-view-branch-control-padding-inline-xl` -- **el privado `--_tree-view-indent` debe pasar a leer este público (o desaparecer)** — `--tree-view-branch-control-padding-inline-md`, `--tree-view-branch-control-padding-inline-xs`, `--tree-view-branch-control-padding-inline-sm`, `--tree-view-branch-control-padding-inline-lg`, `--tree-view-branch-control-padding-inline-xl` -- **el privado `--_tree-view-guide` debe pasar a leer este público (o desaparecer)** — `--tree-view-root-bg` - ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) - [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer. @@ -150,6 +73,69 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-21.** La §4 propone 30 tokens y **falla en el nombre de casi +todos los que importan**: el clasificador tomó el PRIMER selector de cada regla +como si fuera la parte pintada, y aquí ese primer selector es el root o el +branch-control cuando el nodo real es otro. Corregido queda en 46 claves, con +el molde del hermano `tree-grid` allí donde el problema es el mismo. + +1. **`root-width: 1px` y `root-bg: var(--color-border-default)` NO son del + root: son la GUÍA de indentación.** El selector es + `[data-tree-view-root][data-indent-guides] [data-tree-view-branch]::before` + y lo pintado es el pseudo del branch. Nombres correctos **`guide-width` + + `guide-fg`**, literalmente los de `tree-grid`. Un `root-bg` que en realidad + tiñe una línea vertical es la clase de nombre que miente. +2. **`branch-control-padding-inline-{k}` ⚠ funde DOS ejes distintos**: el + ritmo de la fila (`--_tree-view-row-padding-inline`, space-1/2/2/3/4) y el + PASO DE PROFUNDIDAD (`--_tree-view-indent`, space-3/3/4/5/6), que se + multiplican en el mismo `calc`. → `row-padding-inline-{k}` + `indent-{k}`, + como `tree-grid`. Fundirlos habría hecho imposible tematizar la sangría sin + mover el padding. +3. **El prefijo `branch-control-` es erróneo: el knob es de la FILA.** Las + reglas rezan `[data-tree-view-branch-control], [data-tree-view-item]` — las + dos filas del árbol. El propio CSS ya llama `row` a sus privados. → + `row-*`, coherente con `tree-grid` y con la lección de `table` («el valor es + la altura de la FILA, no del root»). Y fuera el prefijo `root-` del chasis: + el wrapper ES el componente (mismo criterio que `tree-grid`). +4. **`row-padding-block` ⚠ es POR TALLA**, no dos valores en pugna: xs vale + `0` y las otras cuatro `--space-1`. Cinco coordenadas. +5. **`branch-indicator-width` + `-height` son UN knob** (`1em` los dos) → + `branch-indicator-size`. Mismo caso que el checkbox de `grid-list`. +6. **Los shorthand se parten**: `border` de surface/outline/ghost → un + `border-width` + un `border` (color); `padding` de surface/outline → + `padding`. Los `transparent` de ghost/outline y el `padding: 0` de ghost son + IDENTIDAD de variante y se quedan literales, igual que el `100%` de + `[data-block]`. +7. **La tipografía va un paso por debajo desde lg** (xs→xs, sm→sm, md→sm, + lg→md, xl→lg) — el patrón exacto de `table` y `tree-grid`, verbatim, no se + «corrige» al 1:1. La altura de fila SÍ es 1:1 con el bundle. Los resueltos + llevan `parts: ['root']`: `data-size` se estampa en `[data-tree-view-root]`, + que es wrapper de eidos y no parte del morfo (precedente `tree-grid`). +8. **`hover-row-bg` SÍ se acuña — al revés que en `grid-list`, y por + medición.** La regla cubre dos nodos con arquetipo distinto: + `branch-control` **no lleva archetype**, así que ahí el plano de la receta + es la ÚNICA pintura (velo ausente: `background-image: none` medido), y el + token es la única superficie de tema del hover. En `[data-tree-view-item]` + (que sí es `item`) se le suma el velo encima, pero el token sigue moviendo + el plano de debajo. Tener un consumidor legítimo es lo que lo distingue del + caso `grid-list`, donde no tenía ninguno. + +### ⚠ El defecto que la medición destapó — el velo DERRAMA sobre el subárbol + +Al pasar el ratón por la fila de una carpeta abierta **se tiñe la carpeta +entera, hijos incluidos**: capturado sobre `src`, el nodo velado mide **336 px** +(la rama completa) en vez de los 36 px de la fila. La causa es que +`archetype: 'item'` está declarado en el **`
  • ` branch** +(`morfo/components/tree-view.ts:103`), que contiene el control Y el +`branch-content` con todo el subárbol, mientras la fila que el usuario señala +es el `branch-control` de dentro. Un branch anidado acumula además el velo de +sus ancestros: en la captura, `lib` se ve más oscuro que sus hermanos por +llevar dos. + +Es PREEXISTENTE (nada que ver con la tokenización), mueve píxel y **mover el +arquetipo es morfo**: se mide, se anota en `next-features.md` §12 y se sigue. +Es la tercera variante de la misma familia — `table` lo tiene en fila Y celda, +`grid-list` en fila Y celda sobre el mismo nodo, y `tree-view` en un ANCESTRO +del nodo señalado. diff --git a/docs/next-features.md b/docs/next-features.md index b29076455..037e79242 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -436,6 +436,24 @@ el mismo nodo entra en una relación que nunca se escribió. que bajo el puntero se apilan velo de celda + velo de fila + plano — tres capas. El par de portadores del punto 2 aplica igual aquí. +8. **En `tree-view` el velo DERRAMA sobre el subárbol entero** (medido y + capturado 2026-08-21 al tokenizarlo). `archetype: 'item'` está declarado en + el **`
  • ` branch** (`morfo/components/tree-view.ts:103`), que contiene el + `branch-control` Y el `branch-content` con todos los descendientes — así que + al pasar el ratón por la fila de una carpeta abierta se tiñe **la carpeta + entera**: el nodo velado mide **336 px** (la rama completa) frente a los + 36 px de la fila que el usuario señala. Un branch anidado acumula además el + velo de sus ancestros, así que sus filas se ven más oscuras que las + hermanas. Es la tercera variante de la misma familia: `table` lo tiene en + fila Y celda (punto 2), `grid-list` en fila Y celda sobre el MISMO nodo + (punto 7), y aquí en un **ANCESTRO** del nodo señalado. Mover el arquetipo + del `
  • ` al control es morfo, y mueve píxel en toda la demo. + + Ojo al tokenizar la familia: en `tree-view` el `branch-control` **no lleva + archetype**, así que ahí el plano de la receta es la única pintura y su + `hover-row-bg` SÍ es un token legítimo — al revés que en `grid-list`. La + misma regla CSS cubre los dos nodos. + ### Qué habría que decidir En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index ec8ed0343..aa2b48804 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -114,6 +114,10 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'sentinel-spinner-reduced-duration': 'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)' }, + 'tree-view': { + 'disabled-row-fg': + 'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))' + }, textarea: { 'focus-input-border': 'consumed only under [data-focused], which the guard blurs away ON PURPOSE (the click-focus poisoning fix); focused by hand with the pointer parked → reaches (rgb(4,5,6))', diff --git a/src/uix/eidos/components/tree-view/README.md b/src/uix/eidos/components/tree-view/README.md index 6f4df1b2c..413a2da40 100644 --- a/src/uix/eidos/components/tree-view/README.md +++ b/src/uix/eidos/components/tree-view/README.md @@ -78,6 +78,64 @@ Referencias: [APG TreeView](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) `aria-hidden`): la línea vertical de nivel es anatomía themeable, no un border mágico de la receta. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- tree-view `): 45/46 tokens +mueven un computed en vivo; el que calla está adjudicado con su porqué medido +en `scripts/theming-sentinel-exceptions.ts` (la demo no monta ninguna fila +deshabilitada). Corrido 2026-08-21. + +Contrato en `lib/recipes/base.ts > tree-view` (46 claves). Alcance del censo: +**92 %** (era 0 % antes del 2026-08-21), con **cero globales**. + +**El eje `size`** (xs..xl) se estampa en `[data-tree-view-root]` —un wrapper de +eidos, no una parte del morfo—, así que los cinco nombres resueltos declaran +`parts: ['root']`; el CSS ya no lleva bloques `[data-size]`. La altura de fila +casa **1:1** con el bundle; el padding y el `indent` llevan ritmo propio (el +spacing queda fuera del guard del bundle); y la tipografía casa **desplazada**, +un paso por debajo desde lg (`font-size-lg` = `var(--size-md-font-size)`), +igual que `table` y `tree-grid`. En xs el `row-padding-block` colapsa a `0`: a +esa talla la altura de fila lleva sola el ritmo. + +**El `indent` es un eje aparte del `row-padding-inline`, y tenía que serlo.** +Los dos se suman en el mismo `calc` (`--tree-depth × indent + padding`), y la +propuesta generada los fundió en un solo nombre por talla — con eso, tematizar +la sangría habría movido también el padding. Son dos knobs. + +**Las guías de indentación son `guide-width` + `guide-fg`**, los mismos nombres +que en `tree-grid`. La propuesta las llamaba `root-width` / `root-bg` porque el +selector empieza por `[data-tree-view-root]`, pero lo pintado es el `::before` +del branch: una línea vertical, no el fondo del root. + +### El hover: por qué aquí SÍ hay token y en `grid-list` no + +Una sola regla pinta las dos filas del árbol, y llevan arquetipo distinto: +`branch-control` **no tiene archetype**, así que ahí el plano de la receta es +la ÚNICA pintura (velo medido ausente, `background-image: none`) y +`hover-row-bg` es toda la superficie de tema del hover. En +`[data-tree-view-item]`, que sí es `item`, el velo del sistema se apila encima, +pero el token sigue moviendo el plano de debajo. Tener un consumidor legítimo +es lo que lo distingue de `grid-list`, donde el token no tenía ninguno. + +### ⚠ El velo derrama sobre el subárbol — preexistente, no es de este eje + +Medido y capturado el 2026-08-21, **anterior a la tokenización**: al pasar el +ratón por la fila de una carpeta abierta se tiñe **la carpeta entera, hijos +incluidos**. `archetype: 'item'` está declarado en el `
  • ` **branch** +(`morfo/components/tree-view.ts:103`), que contiene el control y todo el +`branch-content`, mientras la fila que el usuario señala es el +`branch-control` de dentro: el nodo velado mide **336 px** frente a los 36 px +de la fila. Un branch anidado acumula además el velo de sus ancestros, así que +sus filas se ven más oscuras que las hermanas. + +Mover el arquetipo del `
  • ` al control es **morfo** y mueve píxel en toda la +demo: se mide, se anota y se sigue (`next-features.md` §12, punto 8 — es la +tercera variante de la misma familia, con `table` y `grid-list`). + +`R-5.1 exception: inline-size: 100%` — `[data-block]` llena su contenedor. Los +`transparent` de outline/ghost y el `padding: 0` de ghost son identidad de +variante, no knobs. + ## Gaps | Gap | Disposición | Detalle | diff --git a/src/uix/eidos/components/tree-view/tree-view.css b/src/uix/eidos/components/tree-view/tree-view.css index 54b379f24..fa3ab60c7 100644 --- a/src/uix/eidos/components/tree-view/tree-view.css +++ b/src/uix/eidos/components/tree-view/tree-view.css @@ -11,8 +11,8 @@ * [data-tree-view-label] → text label slot * * Indentation uses the `--tree-depth` custom property that soma writes - * on every Branch / Item — multiplied by `--_tree-view-indent` (scaled - * by `data-size`). Wrapper `
    ` carries the + * on every Branch / Item — multiplied by `--tree-view-indent` (resolved + * per `data-size` by the TSC). Wrapper `
    ` carries the * eidos visual data-attrs (variant / size / color / block / scroll / * indent-guides) so the semantic `
      ` stays clean. * @@ -21,20 +21,16 @@ */ [data-tree-view-root] { - --_tree-view-radius: var(--radius-md); - --_tree-view-row-block-size: var(--control-height-md); - --_tree-view-row-padding-block: var(--space-1); - --_tree-view-row-padding-inline: var(--space-2); - --_tree-view-indent: var(--space-4); - --_tree-view-font-size: var(--font-size-sm); - --_tree-view-guide: var(--color-border-default); - + /* The per-size coordinates (row height, row padding, indent, font-size) are + EMITTED by the TSC from the recipe contract onto this part; the old + `[data-size]` blocks are gone — keeping them would shadow the new tokens + with the old values. */ display: block; - border-radius: var(--_tree-view-radius); - font-family: var(--style-label-font-family); - font-size: var(--_tree-view-font-size); - line-height: var(--leading-ui); - color: var(--color-content-primary); + border-radius: var(--tree-view-radius); + font-family: var(--tree-view-font-family); + font-size: var(--tree-view-font-size); + line-height: var(--tree-view-line-height); + color: var(--tree-view-fg); overflow: hidden; } @@ -44,49 +40,26 @@ /* ── Variant ─ */ [data-tree-view-root][data-variant='surface'] { - border: var(--border-width) solid var(--color-border-default); - background: var(--color-surface-default); - padding: var(--space-2); + border: var(--tree-view-border-width) solid var(--tree-view-border); + background: var(--tree-view-surface-bg); + padding: var(--tree-view-padding); } [data-tree-view-root][data-variant='outline'] { - border: var(--border-width) solid var(--color-border-default); + border: var(--tree-view-border-width) solid var(--tree-view-border); background: transparent; - padding: var(--space-2); + padding: var(--tree-view-padding); } +/* Ghost: `transparent` and `0` are variant IDENTITY (chrome-less), not knobs. */ [data-tree-view-root][data-variant='ghost'] { - border: var(--border-width) solid transparent; + border: var(--tree-view-border-width) solid transparent; background: transparent; padding: 0; } -/* ── Sizes ─ */ -[data-tree-view-root][data-size='xs'] { - --_tree-view-row-block-size: var(--control-height-xs); - --_tree-view-row-padding-block: 0; - --_tree-view-row-padding-inline: var(--space-1); - --_tree-view-indent: var(--space-3); - --_tree-view-font-size: var(--font-size-xs); -} -[data-tree-view-root][data-size='sm'] { - --_tree-view-row-block-size: var(--control-height-sm); - --_tree-view-row-padding-inline: var(--space-2); - --_tree-view-indent: var(--space-3); - --_tree-view-font-size: var(--font-size-sm); -} -[data-tree-view-root][data-size='lg'] { - --_tree-view-row-block-size: var(--control-height-lg); - --_tree-view-row-padding-inline: var(--space-3); - --_tree-view-indent: var(--space-5); - --_tree-view-font-size: var(--font-size-md); -} -[data-tree-view-root][data-size='xl'] { - --_tree-view-row-block-size: var(--control-height-xl); - --_tree-view-row-padding-inline: var(--space-4); - --_tree-view-indent: var(--space-6); - --_tree-view-font-size: var(--font-size-lg); -} +/* ── Sizes ─ + * Emitted by the TSC on `[data-tree-view-root]` from the recipe contract. */ /* Scrollable cap when consumer sets `maxHeight`. */ [data-tree-view-root][data-scrollable] { @@ -118,14 +91,14 @@ position: relative; display: flex; align-items: center; - gap: var(--space-2); - min-block-size: var(--_tree-view-row-block-size); - padding-block: var(--_tree-view-row-padding-block); - padding-inline-end: var(--_tree-view-row-padding-inline); + gap: var(--tree-view-row-gap); + min-block-size: var(--tree-view-row-height); + padding-block: var(--tree-view-row-padding-block); + padding-inline-end: var(--tree-view-row-padding-inline); padding-inline-start: calc( - var(--tree-depth, 0) * var(--_tree-view-indent) + var(--_tree-view-row-padding-inline) + var(--tree-depth, 0) * var(--tree-view-indent) + var(--tree-view-row-padding-inline) ); - border-radius: var(--radius-sm); + border-radius: var(--tree-view-row-radius); color: inherit; cursor: pointer; user-select: none; @@ -134,7 +107,7 @@ [data-tree-view-branch-control]:hover:not([data-disabled]), [data-tree-view-item]:hover:not([data-disabled]) { - background: var(--color-surface-overlay); + background: var(--tree-view-hover-row-bg); } [data-tree-view-branch-control][data-selected]:not([data-disabled]), @@ -144,7 +117,7 @@ [data-tree-view-branch-control][data-disabled], [data-tree-view-item][data-disabled] { - color: var(--color-content-muted); + color: var(--tree-view-disabled-row-fg); cursor: not-allowed; opacity: var(--opacity-disabled); } @@ -161,9 +134,9 @@ align-items: center; justify-content: center; flex: 0 0 auto; - inline-size: 1em; - block-size: 1em; - color: var(--color-content-secondary); + inline-size: var(--tree-view-branch-indicator-size); + block-size: var(--tree-view-branch-indicator-size); + color: var(--tree-view-branch-indicator-fg); transition: transform var(--duration-fast) var(--ease-default); } @@ -196,16 +169,16 @@ content: ''; position: absolute; inset-block: 0; - inline-size: 1px; - background: var(--_tree-view-guide); + inline-size: var(--tree-view-guide-width); + background: var(--tree-view-guide-fg); /* Logical inset, so the guide mirrors with the rows it marks. A `:dir(rtl)` rule that moved it to `inset-inline-end` used to live here: that is a DOUBLE flip — the logical inset had already mirrored — and it parked the guide on the far side of its own subtree. RTL-1 cannot see this shape. */ inset-inline-start: calc( - var(--tree-depth, 0) * var(--_tree-view-indent) + - var(--_tree-view-indent) / 2 + - var(--_tree-view-row-padding-inline) + var(--tree-depth, 0) * var(--tree-view-indent) + + var(--tree-view-indent) / 2 + + var(--tree-view-row-padding-inline) ); pointer-events: none; } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 9b863b0f9..b03b0360a 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -1739,6 +1739,47 @@ --tree-grid-guide-fg: var(--color-border-default); --tree-grid-guide-width: 1px; --tree-grid-branch-placeholder-size: 1em; + --tree-view-row-height-xs: var(--size-xs-control-height); + --tree-view-row-height-sm: var(--size-sm-control-height); + --tree-view-row-height-md: var(--size-md-control-height); + --tree-view-row-height-lg: var(--size-lg-control-height); + --tree-view-row-height-xl: var(--size-xl-control-height); + --tree-view-row-padding-block-xs: 0; + --tree-view-row-padding-block-sm: var(--space-1); + --tree-view-row-padding-block-md: var(--space-1); + --tree-view-row-padding-block-lg: var(--space-1); + --tree-view-row-padding-block-xl: var(--space-1); + --tree-view-row-padding-inline-xs: var(--space-1); + --tree-view-row-padding-inline-sm: var(--space-2); + --tree-view-row-padding-inline-md: var(--space-2); + --tree-view-row-padding-inline-lg: var(--space-3); + --tree-view-row-padding-inline-xl: var(--space-4); + --tree-view-indent-xs: var(--space-3); + --tree-view-indent-sm: var(--space-3); + --tree-view-indent-md: var(--space-4); + --tree-view-indent-lg: var(--space-5); + --tree-view-indent-xl: var(--space-6); + --tree-view-font-size-xs: var(--size-xs-font-size); + --tree-view-font-size-sm: var(--size-sm-font-size); + --tree-view-font-size-md: var(--size-sm-font-size); + --tree-view-font-size-lg: var(--size-md-font-size); + --tree-view-font-size-xl: var(--size-lg-font-size); + --tree-view-radius: var(--radius-md); + --tree-view-font-family: var(--style-label-font-family); + --tree-view-line-height: var(--leading-ui); + --tree-view-fg: var(--color-content-primary); + --tree-view-border-width: var(--border-width); + --tree-view-border: var(--color-border-default); + --tree-view-surface-bg: var(--color-surface-default); + --tree-view-padding: var(--space-2); + --tree-view-row-gap: var(--space-2); + --tree-view-row-radius: var(--radius-sm); + --tree-view-hover-row-bg: var(--color-surface-overlay); + --tree-view-disabled-row-fg: var(--color-content-muted); + --tree-view-branch-indicator-size: 1em; + --tree-view-branch-indicator-fg: var(--color-content-secondary); + --tree-view-guide-width: 1px; + --tree-view-guide-fg: var(--color-border-default); --feed-font-size-xs: var(--size-xs-font-size); --feed-font-size-sm: var(--size-sm-font-size); --feed-font-size-md: var(--size-sm-font-size); @@ -5581,9 +5622,46 @@ } [data-tree-view-root] { + --tree-view-row-height: var(--tree-view-row-height-md); + --tree-view-row-padding-block: var(--tree-view-row-padding-block-md); + --tree-view-row-padding-inline: var(--tree-view-row-padding-inline-md); + --tree-view-indent: var(--tree-view-indent-md); + --tree-view-font-size: var(--tree-view-font-size-md); --_tree-view-palette-element: var(--color-primary-element); } +[data-tree-view-root][data-size='xs'] { + --tree-view-row-height: var(--tree-view-row-height-xs); + --tree-view-row-padding-block: var(--tree-view-row-padding-block-xs); + --tree-view-row-padding-inline: var(--tree-view-row-padding-inline-xs); + --tree-view-indent: var(--tree-view-indent-xs); + --tree-view-font-size: var(--tree-view-font-size-xs); +} + +[data-tree-view-root][data-size='sm'] { + --tree-view-row-height: var(--tree-view-row-height-sm); + --tree-view-row-padding-block: var(--tree-view-row-padding-block-sm); + --tree-view-row-padding-inline: var(--tree-view-row-padding-inline-sm); + --tree-view-indent: var(--tree-view-indent-sm); + --tree-view-font-size: var(--tree-view-font-size-sm); +} + +[data-tree-view-root][data-size='lg'] { + --tree-view-row-height: var(--tree-view-row-height-lg); + --tree-view-row-padding-block: var(--tree-view-row-padding-block-lg); + --tree-view-row-padding-inline: var(--tree-view-row-padding-inline-lg); + --tree-view-indent: var(--tree-view-indent-lg); + --tree-view-font-size: var(--tree-view-font-size-lg); +} + +[data-tree-view-root][data-size='xl'] { + --tree-view-row-height: var(--tree-view-row-height-xl); + --tree-view-row-padding-block: var(--tree-view-row-padding-block-xl); + --tree-view-row-padding-inline: var(--tree-view-row-padding-inline-xl); + --tree-view-indent: var(--tree-view-indent-xl); + --tree-view-font-size: var(--tree-view-font-size-xl); +} + [data-tree-view-root][data-color], [data-tree-view-root][data-color-custom] { --_tree-view-palette-element: var(--palette-element, var(--color-primary-element)); } diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index b5d47eb03..d452db446 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -2198,6 +2198,133 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ } }, 'tree-view': { + // ── Per-size coordinates (xs..xl) ── + // Row height is 1:1 with the bundle. Padding and indent are the tree's OWN + // rhythm (spacing sits outside the bundle guard, which governs + // control-height / font-size / icon-size only) — same split as `tree-grid`. + 'row-height-xs': 'var(--size-xs-control-height)', + 'row-height-sm': 'var(--size-sm-control-height)', + 'row-height-md': 'var(--size-md-control-height)', + 'row-height-lg': 'var(--size-lg-control-height)', + 'row-height-xl': 'var(--size-xl-control-height)', + // xs collapses the block padding to zero: at that size the row height + // alone carries the rhythm. + 'row-padding-block-xs': '0', + 'row-padding-block-sm': 'var(--space-1)', + 'row-padding-block-md': 'var(--space-1)', + 'row-padding-block-lg': 'var(--space-1)', + 'row-padding-block-xl': 'var(--space-1)', + 'row-padding-inline-xs': 'var(--space-1)', + 'row-padding-inline-sm': 'var(--space-2)', + 'row-padding-inline-md': 'var(--space-2)', + 'row-padding-inline-lg': 'var(--space-3)', + 'row-padding-inline-xl': 'var(--space-4)', + // The depth step — the tree's own axis, multiplied by soma's + // `--tree-depth`. It is NOT the row's inline padding: the two add up in + // one calc, which is why the generated proposal fused them into a single + // name and why they must stay apart. + 'indent-xs': 'var(--space-3)', + 'indent-sm': 'var(--space-3)', + 'indent-md': 'var(--space-4)', + 'indent-lg': 'var(--space-5)', + 'indent-xl': 'var(--space-6)', + // Type: like `table` and `tree-grid`, one step below the 1:1 from lg up. + // The coordinate names that step instead of hiding it in a primitive. + 'font-size-xs': 'var(--size-xs-font-size)', + 'font-size-sm': 'var(--size-sm-font-size)', + 'font-size-md': 'var(--size-sm-font-size)', + 'font-size-lg': 'var(--size-md-font-size)', + 'font-size-xl': 'var(--size-lg-font-size)', + + // The RESOLVED names. `data-size` is stamped on `[data-tree-view-root]` — + // an eidos wrapper, not a morfo part — so the per-size cascade targets + // that part (precedent: tree-grid, proof-of-human). + 'row-height': { + parts: ['root'], + declarations: [ + { value: 'var(--tree-view-row-height-md)', scope: 'host' }, + { value: 'var(--tree-view-row-height-xs)', scope: 'size:xs' }, + { value: 'var(--tree-view-row-height-sm)', scope: 'size:sm' }, + { value: 'var(--tree-view-row-height-lg)', scope: 'size:lg' }, + { value: 'var(--tree-view-row-height-xl)', scope: 'size:xl' } + ] + }, + 'row-padding-block': { + parts: ['root'], + declarations: [ + { value: 'var(--tree-view-row-padding-block-md)', scope: 'host' }, + { value: 'var(--tree-view-row-padding-block-xs)', scope: 'size:xs' }, + { value: 'var(--tree-view-row-padding-block-sm)', scope: 'size:sm' }, + { value: 'var(--tree-view-row-padding-block-lg)', scope: 'size:lg' }, + { value: 'var(--tree-view-row-padding-block-xl)', scope: 'size:xl' } + ] + }, + 'row-padding-inline': { + parts: ['root'], + declarations: [ + { value: 'var(--tree-view-row-padding-inline-md)', scope: 'host' }, + { value: 'var(--tree-view-row-padding-inline-xs)', scope: 'size:xs' }, + { value: 'var(--tree-view-row-padding-inline-sm)', scope: 'size:sm' }, + { value: 'var(--tree-view-row-padding-inline-lg)', scope: 'size:lg' }, + { value: 'var(--tree-view-row-padding-inline-xl)', scope: 'size:xl' } + ] + }, + indent: { + parts: ['root'], + declarations: [ + { value: 'var(--tree-view-indent-md)', scope: 'host' }, + { value: 'var(--tree-view-indent-xs)', scope: 'size:xs' }, + { value: 'var(--tree-view-indent-sm)', scope: 'size:sm' }, + { value: 'var(--tree-view-indent-lg)', scope: 'size:lg' }, + { value: 'var(--tree-view-indent-xl)', scope: 'size:xl' } + ] + }, + 'font-size': { + parts: ['root'], + declarations: [ + { value: 'var(--tree-view-font-size-md)', scope: 'host' }, + { value: 'var(--tree-view-font-size-xs)', scope: 'size:xs' }, + { value: 'var(--tree-view-font-size-sm)', scope: 'size:sm' }, + { value: 'var(--tree-view-font-size-lg)', scope: 'size:lg' }, + { value: 'var(--tree-view-font-size-xl)', scope: 'size:xl' } + ] + }, + + // ── Chassis ── no `root-` prefix: the wrapper IS the component. + radius: 'var(--radius-md)', + 'font-family': 'var(--style-label-font-family)', + 'line-height': 'var(--leading-ui)', + fg: 'var(--color-content-primary)', + 'border-width': 'var(--border-width)', + // Shared by the surface and outline variants. Ghost's `transparent` is + // variant IDENTITY, not a knob, and stays literal. + border: 'var(--color-border-default)', + 'surface-bg': 'var(--color-surface-default)', + // Surface and outline pad alike; ghost's `0` is identity. + padding: 'var(--space-2)', + + // ── Rows ── one rule paints BOTH `branch-control` and `item`. + 'row-gap': 'var(--space-2)', + 'row-radius': 'var(--radius-sm)', + // Coined on purpose, unlike grid-list's: `branch-control` carries NO + // archetype, so on that node the recipe's plane is the ONLY paint (veil + // measured absent) and this token is the hover's whole theming surface. + // On `[data-tree-view-item]` the system veil stacks on top; see the + // README and next-features §12 for the branch-veil spill. + 'hover-row-bg': 'var(--color-surface-overlay)', + 'disabled-row-fg': 'var(--color-content-muted)', + + // ── BranchIndicator (chevron) ── + 'branch-indicator-size': '1em', + 'branch-indicator-fg': 'var(--color-content-secondary)', + + // ── Indent guides ── + // The generated proposal called these `root-width` / `root-bg`: they are + // neither. The painted node is the branch's `::before`, a vertical line. + // Same names as tree-grid's, for the same thing. + 'guide-width': '1px', + 'guide-fg': 'var(--color-border-default)', + '_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] diff --git a/web/routes/uix/components/tree-view/+page.svelte b/web/routes/uix/components/tree-view/+page.svelte index bb91597fc..3f2e8850a 100644 --- a/web/routes/uix/components/tree-view/+page.svelte +++ b/web/routes/uix/components/tree-view/+page.svelte @@ -9,10 +9,11 @@ import { compileMorfo } from '$uix/morfo'; import { treeViewMorfo } from '@/uix/morfo/components/tree-view'; import { getActiveUix } from '$active-uix'; + import TokensPanel from '../../lib/TokensPanel.svelte'; const uix = getActiveUix(); - type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; + type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y'; let tab = $state('live'); // ── Sample data ────────────────────────────────────────────────────── @@ -291,6 +292,9 @@ {events.length} +
    @@ -573,6 +577,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'a11y'}

    Accessibility