From 3adc93b2d13364b8e1499f4d539267766becba84 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 1 Jun 2026 21:57:26 +0200 Subject: [PATCH] Banner: route palette through recipe forwarders (P1-5) - Add 40 recipes.banner.{intent}-{slot} forwarders (= var(--color-{intent}-{slot})) - banner.css reads --banner-{intent}-{slot} instead of foundation --color-{intent}-* directly - Banner is now retintable per-component via recipes.banner.* like button/badge; keeps data-intent (no API change) - Regenerated base.css; recipe contract balanced (40 declared = 40 consumed), no visual regression Co-Authored-By: Claude Opus 4.8 --- src/uix/eidos/THEMING_AUDIT_2026-06-01.md | 3 +- src/uix/eidos/components/banner/banner.css | 85 +++++++++++----------- src/uix/eidos/generated/base.css | 40 ++++++++++ src/uix/eidos/lib/recipes/base.ts | 47 +++++++++++- 4 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md index 95654ab73..3ec3128b1 100644 --- a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md +++ b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md @@ -65,9 +65,10 @@ definiciones vs referencias). Orden de prioridad para empezar a resolver. - **Fix**: sniff de color CSS (hex/rgb/hsl/color/var/color-mix/nombres) en rutas de color. - **✅ Hecho**: regla segura en `validateNonEmptyCssValue` — un valor que empieza por `#` debe ser hex válido (`#rgb/#rgba/#rrggbb/#rrggbbaa`); caza `#ggg` / hex tipografiado **sin** tocar `var()`/`color-mix()`/`rgb()` ni los placeholders de test (sin `#`). + test. **Pendiente**: el identificador-pelado tipo `'not-a-color'` sigue sin cazarse (requeriría allow-list de nombres CSS o reescribir el test de pass-through `'purple-alpha-4'`). -### P1-5 · Banner se salta el sistema de recipes/roles +### ✅ P1-5 · Banner se salta el sistema de recipes/roles - `components/banner/banner.css:57-112` cablea a mano 8 intents × 5 slots a `var(--color-{intent}-{slot})` y lee `data-intent`. Su recipe (`base.ts:3799-3802`) solo declara gap+radius. Es el único componente de color que **no se puede retintar vía `recipes.*`** y diverge del eje `data-color` del resto. - **Fix**: migrar Banner a tokens de recipe + cascada TSC `_accent-*`/`palette-*` como button/toggle. +- **✅ Hecho** (versión no-disruptiva): añadidos 40 forwarders `recipes.banner.{intent}-{slot}` (= `var(--color-{intent}-{slot})`) y banner.css lee `--banner-{intent}-{slot}` en vez de `--color-{intent}-*` directo → **Banner ya es retintable vía `recipes.banner.*`** (como button/badge). Verificado: cadena resuelve igual (`--banner-affirm-solid` = `#12a594`), sin regresión visual; contract balanceado (40 declarados = 40 consumidos). **Mantiene `data-intent`** (no cambio de API). **Pendiente futuro**: alinear del todo al patrón button (cascada `palette-*` en el recipe en vez de en CSS) requeriría cambiar el eje a `data-color` o un scope `intent:` en el TSC — decisión separada. ### P1-6 · Cobertura de tests: las dos clases de bug que ya se colaron siguen sin red - **`contrast`/on-solid: cero asserts** (`grep contrast|on-solid` en los `*.test.ts` = 0). El bug de ilegibilidad estaba en este slot. diff --git a/src/uix/eidos/components/banner/banner.css b/src/uix/eidos/components/banner/banner.css index e55f0184d..6b3d34faa 100644 --- a/src/uix/eidos/components/banner/banner.css +++ b/src/uix/eidos/components/banner/banner.css @@ -1,8 +1,9 @@ /* * Banner recipe — full-bleed announcement strip. Reads `data-intent` * (the standard UIX color-role channel) and `data-variant` (soft / solid - * / outline / ghost) to pick the right palette mix from the foundation - * `--color-{intent}-*` tokens. `data-size` controls vertical density. + * / outline / ghost) to pick the right palette mix from the recipe + * `--banner-{intent}-*` palette forwarders (overridable via + * `recipes.banner.{intent}-{slot}`). `data-size` controls vertical density. * * Variant cascade: * soft — `--color-{intent}-track` bg + `--color-{intent}-text` fg @@ -55,60 +56,60 @@ /* ── Intent palette resolution ─────────────────────────────────────── */ [data-banner][data-intent='primary'] { - --_banner-solid: var(--color-primary-solid); - --_banner-solid-contrast: var(--color-primary-contrast); - --_banner-track: var(--color-primary-track); - --_banner-border: var(--color-primary-border); - --_banner-text: var(--color-primary-text); + --_banner-solid: var(--banner-primary-solid); + --_banner-solid-contrast: var(--banner-primary-contrast); + --_banner-track: var(--banner-primary-track); + --_banner-border: var(--banner-primary-border); + --_banner-text: var(--banner-primary-text); } [data-banner][data-intent='secondary'] { - --_banner-solid: var(--color-secondary-solid); - --_banner-solid-contrast: var(--color-secondary-contrast); - --_banner-track: var(--color-secondary-track); - --_banner-border: var(--color-secondary-border); - --_banner-text: var(--color-secondary-text); + --_banner-solid: var(--banner-secondary-solid); + --_banner-solid-contrast: var(--banner-secondary-contrast); + --_banner-track: var(--banner-secondary-track); + --_banner-border: var(--banner-secondary-border); + --_banner-text: var(--banner-secondary-text); } [data-banner][data-intent='neutral'] { - --_banner-solid: var(--color-neutral-solid); - --_banner-solid-contrast: var(--color-neutral-contrast); - --_banner-track: var(--color-neutral-track); - --_banner-border: var(--color-neutral-border); - --_banner-text: var(--color-neutral-text); + --_banner-solid: var(--banner-neutral-solid); + --_banner-solid-contrast: var(--banner-neutral-contrast); + --_banner-track: var(--banner-neutral-track); + --_banner-border: var(--banner-neutral-border); + --_banner-text: var(--banner-neutral-text); } [data-banner][data-intent='affirm'] { - --_banner-solid: var(--color-affirm-solid); - --_banner-solid-contrast: var(--color-affirm-contrast); - --_banner-track: var(--color-affirm-track); - --_banner-border: var(--color-affirm-border); - --_banner-text: var(--color-affirm-text); + --_banner-solid: var(--banner-affirm-solid); + --_banner-solid-contrast: var(--banner-affirm-contrast); + --_banner-track: var(--banner-affirm-track); + --_banner-border: var(--banner-affirm-border); + --_banner-text: var(--banner-affirm-text); } [data-banner][data-intent='fulfill'] { - --_banner-solid: var(--color-fulfill-solid); - --_banner-solid-contrast: var(--color-fulfill-contrast); - --_banner-track: var(--color-fulfill-track); - --_banner-border: var(--color-fulfill-border); - --_banner-text: var(--color-fulfill-text); + --_banner-solid: var(--banner-fulfill-solid); + --_banner-solid-contrast: var(--banner-fulfill-contrast); + --_banner-track: var(--banner-fulfill-track); + --_banner-border: var(--banner-fulfill-border); + --_banner-text: var(--banner-fulfill-text); } [data-banner][data-intent='risk'] { - --_banner-solid: var(--color-risk-solid); - --_banner-solid-contrast: var(--color-risk-contrast); - --_banner-track: var(--color-risk-track); - --_banner-border: var(--color-risk-border); - --_banner-text: var(--color-risk-text); + --_banner-solid: var(--banner-risk-solid); + --_banner-solid-contrast: var(--banner-risk-contrast); + --_banner-track: var(--banner-risk-track); + --_banner-border: var(--banner-risk-border); + --_banner-text: var(--banner-risk-text); } [data-banner][data-intent='threat'] { - --_banner-solid: var(--color-threat-solid); - --_banner-solid-contrast: var(--color-threat-contrast); - --_banner-track: var(--color-threat-track); - --_banner-border: var(--color-threat-border); - --_banner-text: var(--color-threat-text); + --_banner-solid: var(--banner-threat-solid); + --_banner-solid-contrast: var(--banner-threat-contrast); + --_banner-track: var(--banner-threat-track); + --_banner-border: var(--banner-threat-border); + --_banner-text: var(--banner-threat-text); } [data-banner][data-intent='loss'] { - --_banner-solid: var(--color-loss-solid); - --_banner-solid-contrast: var(--color-loss-contrast); - --_banner-track: var(--color-loss-track); - --_banner-border: var(--color-loss-border); - --_banner-text: var(--color-loss-text); + --_banner-solid: var(--banner-loss-solid); + --_banner-solid-contrast: var(--banner-loss-contrast); + --_banner-track: var(--banner-loss-track); + --_banner-border: var(--banner-loss-border); + --_banner-text: var(--banner-loss-text); } /* ── Variant treatment ─────────────────────────────────────────────── */ diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 9484a9476..7523a7887 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -2968,6 +2968,46 @@ --group-attached-overlap: 1px; --banner-gap: var(--space-3); --banner-radius: 0; + --banner-primary-solid: var(--color-primary-solid); + --banner-primary-contrast: var(--color-primary-contrast); + --banner-primary-track: var(--color-primary-track); + --banner-primary-border: var(--color-primary-border); + --banner-primary-text: var(--color-primary-text); + --banner-secondary-solid: var(--color-secondary-solid); + --banner-secondary-contrast: var(--color-secondary-contrast); + --banner-secondary-track: var(--color-secondary-track); + --banner-secondary-border: var(--color-secondary-border); + --banner-secondary-text: var(--color-secondary-text); + --banner-neutral-solid: var(--color-neutral-solid); + --banner-neutral-contrast: var(--color-neutral-contrast); + --banner-neutral-track: var(--color-neutral-track); + --banner-neutral-border: var(--color-neutral-border); + --banner-neutral-text: var(--color-neutral-text); + --banner-affirm-solid: var(--color-affirm-solid); + --banner-affirm-contrast: var(--color-affirm-contrast); + --banner-affirm-track: var(--color-affirm-track); + --banner-affirm-border: var(--color-affirm-border); + --banner-affirm-text: var(--color-affirm-text); + --banner-fulfill-solid: var(--color-fulfill-solid); + --banner-fulfill-contrast: var(--color-fulfill-contrast); + --banner-fulfill-track: var(--color-fulfill-track); + --banner-fulfill-border: var(--color-fulfill-border); + --banner-fulfill-text: var(--color-fulfill-text); + --banner-risk-solid: var(--color-risk-solid); + --banner-risk-contrast: var(--color-risk-contrast); + --banner-risk-track: var(--color-risk-track); + --banner-risk-border: var(--color-risk-border); + --banner-risk-text: var(--color-risk-text); + --banner-threat-solid: var(--color-threat-solid); + --banner-threat-contrast: var(--color-threat-contrast); + --banner-threat-track: var(--color-threat-track); + --banner-threat-border: var(--color-threat-border); + --banner-threat-text: var(--color-threat-text); + --banner-loss-solid: var(--color-loss-solid); + --banner-loss-contrast: var(--color-loss-contrast); + --banner-loss-track: var(--color-loss-track); + --banner-loss-border: var(--color-loss-border); + --banner-loss-text: var(--color-loss-text); --container-width-xl: var(--layout-container-width-xl, 80rem); --section-padding-block-sm: var(--space-8); --section-padding-block-md: var(--space-12); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 63f949a9e..a82a38361 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -3798,7 +3798,52 @@ export const THEME_BASE_RECIPE_TOKENS = { // ───────────────────────────────────────────────────────────────────── banner: { gap: 'var(--space-3)', - radius: '0' + radius: '0', + // Per-intent palette forwarders → Banner is retintable via + // `recipes.banner.{intent}-{slot}` like every other color component + // (button/badge/toggle). banner.css reads these instead of the raw + // foundation `--color-{intent}-*` tokens, so a per-component recipe + // override actually reaches it. (audit P1-5) + 'primary-solid': 'var(--color-primary-solid)', + 'primary-contrast': 'var(--color-primary-contrast)', + 'primary-track': 'var(--color-primary-track)', + 'primary-border': 'var(--color-primary-border)', + 'primary-text': 'var(--color-primary-text)', + 'secondary-solid': 'var(--color-secondary-solid)', + 'secondary-contrast': 'var(--color-secondary-contrast)', + 'secondary-track': 'var(--color-secondary-track)', + 'secondary-border': 'var(--color-secondary-border)', + 'secondary-text': 'var(--color-secondary-text)', + 'neutral-solid': 'var(--color-neutral-solid)', + 'neutral-contrast': 'var(--color-neutral-contrast)', + 'neutral-track': 'var(--color-neutral-track)', + 'neutral-border': 'var(--color-neutral-border)', + 'neutral-text': 'var(--color-neutral-text)', + 'affirm-solid': 'var(--color-affirm-solid)', + 'affirm-contrast': 'var(--color-affirm-contrast)', + 'affirm-track': 'var(--color-affirm-track)', + 'affirm-border': 'var(--color-affirm-border)', + 'affirm-text': 'var(--color-affirm-text)', + 'fulfill-solid': 'var(--color-fulfill-solid)', + 'fulfill-contrast': 'var(--color-fulfill-contrast)', + 'fulfill-track': 'var(--color-fulfill-track)', + 'fulfill-border': 'var(--color-fulfill-border)', + 'fulfill-text': 'var(--color-fulfill-text)', + 'risk-solid': 'var(--color-risk-solid)', + 'risk-contrast': 'var(--color-risk-contrast)', + 'risk-track': 'var(--color-risk-track)', + 'risk-border': 'var(--color-risk-border)', + 'risk-text': 'var(--color-risk-text)', + 'threat-solid': 'var(--color-threat-solid)', + 'threat-contrast': 'var(--color-threat-contrast)', + 'threat-track': 'var(--color-threat-track)', + 'threat-border': 'var(--color-threat-border)', + 'threat-text': 'var(--color-threat-text)', + 'loss-solid': 'var(--color-loss-solid)', + 'loss-contrast': 'var(--color-loss-contrast)', + 'loss-track': 'var(--color-loss-track)', + 'loss-border': 'var(--color-loss-border)', + 'loss-text': 'var(--color-loss-text)' }, // ─────────────────────────────────────────────────────────────────────