You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/recipe-css-contract.test.ts

634 lines
23 KiB

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { THEME_BASE_RECIPE_TOKENS } from './lib/recipes/base'
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import type { EidosConfig, RecipeTokenSet, RecipeTokenValue } from './lib/config-types'
import { renderStaticCss } from './lib/render-css'
import { createThemeBaseEidosConfig } from './lib/themes/base'
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint Variants son canon del eidos, NO del theme. Decisión arquitectónica firmemente sostenida: el vocabulario de variants (solid/outline/ghost/ soft/surface/line/pills) está fijo a nivel del framework — paralelo a las 8 sema families del libro. Theme = retintar lo perceptualmente fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`. Cambios: - lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes canónicos (control / selection / chip / marker / tabs). Los 5 union types se derivan via `[number]` indexed access — valor y tipo no pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con todos los valores canónicos + utilidades cross-component (`plain`, `subtle`). - recipe-css-contract.test.ts: nuevo test "variant CSS selectors per component match the declared type union". Por cada componente: extrae el union type de `components/{c}/types.ts` (soporta literal unions + archetype aliases; cae a advisory mode en Extract<> y conditional types); compara con `[data-{c}][data-variant='X']` selectores en `{c}.css`; reporta typos y unauthorized extensions bidireccionalmente. - THEMING.md §19: nueva sección "Variants son canon del eidos, NO del theme" con argumentación (portabilidad, type safety, archetypes perceptuales paralelos a sema families), tabla de las 3 capas de la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado. - eidos/README.md: tabla de referencia ampliada con §19. - CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)". - CONTINUE.md: nota de la decisión arquitectónica. Variants component-specific permitidos (Banner inline/overlay/ persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada `components/{c}/types.ts` y el lint los valida contra la CSS del componente. Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6 errores pre-existentes (lib/_demo, soma/components/internal, web/ routes/active) — no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import { EIDOS_VARIANTS, EIDOS_VARIANT_VALUES } from './lib/types'
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Build a minimal EidosConfig whose recipes are the given map. Reuses the
* base config's primitives/semantics so the generator has the same
* surface to render against as the production base.
*/
function configWithRecipes(recipes: RecipeTokenSet): EidosConfig {
const base = createThemeBaseEidosConfig()
return { ...base, recipes }
}
const COMPONENTS_DIR = 'src/uix/eidos/components'
const CSS_CUSTOM_PROPERTY = /--([a-z][a-z0-9]*-[a-z0-9-]+)/g
const COMPONENT_SOURCE_FILE = /\.(css|svelte|ts)$/
const RAW_COLOR_LITERAL = /#[0-9a-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\(/i
const RAW_FONT_SIZE_LITERAL = /font-size\s*:\s*[0-9.]+(?:px|rem)\b/i
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
// TSC v2.2 widened `RecipeTokenMap` to allow `composition` as a sibling
// of regular tokens. Cast through `unknown` so the test still treats
// the recipe map as a flat `Record<string, RecipeTokenValue>` — the
// `tokenKeys` / `tokenEntries` helpers below filter `composition` out
// of every iteration so the broader type doesn't leak into assertions.
const RECIPE_TOKENS = THEME_BASE_RECIPE_TOKENS as unknown as Readonly<
Record<string, Readonly<Record<string, RecipeTokenValue>>>
>
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/** Extract all CSS values from a recipe entry. Supports the three TSC
* forms: bare string/number, RecipeTokenSingle (one declaration), and
* RecipeTokenMultiDeclaration (declarations[]). Returns the values
* joined with newlines so the orphan-detector can do substring scans. */
function entryValues(entry: RecipeTokenValue): string {
if (typeof entry !== 'object') return String(entry)
if ('declarations' in entry) {
return entry.declarations.map((d) => d.value).join('\n')
}
return entry.value
}
/** Token keys from a recipe map, excluding TSC v2.2 reserved sibling
* keys (currently just `composition`). The orphan + missing tests
* iterate token keys; `composition` is a structural block, not a
* regular token, and must be skipped to avoid false positives like
* `--toggle-group-composition`. */
function tokenKeys(tokens: Readonly<Record<string, RecipeTokenValue>>): readonly string[] {
return Object.keys(tokens).filter((key) => key !== 'composition')
}
/** Token entries from a recipe map, with the `composition` reserved
* key filtered out. Companion to `tokenKeys` for value-based scans. */
function tokenEntries(
tokens: Readonly<Record<string, RecipeTokenValue>>
): readonly [string, RecipeTokenValue][] {
return Object.entries(tokens).filter(([key]) => key !== 'composition') as readonly [
string,
RecipeTokenValue
][]
}
function stripCssComments(css: string): string {
return css.replace(/\/\*[\s\S]*?\*\//g, '')
}
function recipeTokenName(component: string, key: string): string {
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
// Private tokens: key starts with `_` → emitted as `--_{c}-{rest}`.
// Public tokens: emitted as `--{c}-{key}`.
if (key.startsWith('_')) return `--_${component}-${key.slice(1)}`
return `--${component}-${key}`
}
function readComponentCssFiles(): readonly { component: string; css: string }[] {
return readdirSync(COMPONENTS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const file = join(COMPONENTS_DIR, entry.name, `${entry.name}.css`)
return { component: entry.name, file }
})
.filter(({ file }) => existsSync(file))
.map(({ component, file }) => ({
component,
css: stripCssComments(readFileSync(file, 'utf8'))
}))
}
function readTextFilesRecursive(dir: string): readonly string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) return readTextFilesRecursive(path)
if (!COMPONENT_SOURCE_FILE.test(entry.name)) return []
return [readFileSync(path, 'utf8')]
})
}
function readComponentSource(component: string): string {
const dir = join(COMPONENTS_DIR, component)
return existsSync(dir) ? readTextFilesRecursive(dir).join('\n') : ''
}
function readRecipeValueReferences(): string {
return Object.values(RECIPE_TOKENS)
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
.flatMap((tokens) => tokenEntries(tokens).map(([, value]) => entryValues(value)))
.join('\n')
}
function collectOwnPublicVariables(component: string, css: string): readonly string[] {
const ownPrefix = `--${component}-`
return [
...new Set(
[...css.matchAll(CSS_CUSTOM_PROPERTY)]
.map((match) => `--${match[1]}`)
.filter((name) => name.startsWith(ownPrefix))
)
].sort()
}
describe('Eidos recipe CSS contract', () => {
it('declares every public component variable consumed by component CSS', () => {
const missing: string[] = []
for (const { component, css } of readComponentCssFiles()) {
const declared = new Set(
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
tokenKeys(RECIPE_TOKENS[component] ?? {}).map((key) =>
recipeTokenName(component, key)
)
)
for (const name of collectOwnPublicVariables(component, css)) {
if (!declared.has(name)) missing.push(`${component}: ${name}`)
}
}
expect(missing).toEqual([])
})
it('does not declare recipes for missing component CSS files', () => {
const cssComponents = new Set(
readComponentCssFiles().map(({ component }) => component)
)
const extra = Object.keys(THEME_BASE_RECIPE_TOKENS)
.filter((component) => !cssComponents.has(component))
expect(extra).toEqual([])
})
it('loads every component CSS recipe exactly once (foundation @import XOR self-import)', () => {
// Bundle architecture (see optimize-bundle.md): recipes are code-split.
// Each component CSS must be loaded via EXACTLY ONE mechanism:
// - foundation: `@import` in index.css (only the layout primitives stay
// there — they're used pervasively and recipes layer on them), OR
// - self-import: `import './{c}.css'` in its own `{c}.svelte`, so Vite
// emits a per-component chunk loaded only when the component mounts.
// Both at once = double-load (the CSS ships in the aggregate AND a chunk);
// neither = the recipe never loads. Either is a violation.
const indexCss = readFileSync('src/uix/eidos/index.css', 'utf8')
const violations: string[] = []
for (const { component } of readComponentCssFiles()) {
const inFoundation = indexCss.includes(`./components/${component}/${component}.css`)
const ownSvelte = join(COMPONENTS_DIR, component, `${component}.svelte`)
const selfImports =
existsSync(ownSvelte) &&
readFileSync(ownSvelte, 'utf8').includes(`import './${component}.css'`)
if (inFoundation && selfImports) {
violations.push(`${component}: double-loaded (in index.css AND self-imported)`)
} else if (!inFoundation && !selfImports) {
violations.push(`${component}: not loaded (neither index.css nor self-import)`)
}
}
expect(violations).toEqual([])
})
it('keeps raw color values out of component CSS', () => {
const violations = readComponentCssFiles()
.filter(({ css }) => RAW_COLOR_LITERAL.test(css))
.map(({ component }) => component)
expect(violations).toEqual([])
})
it('keeps raw font-size literals out of component CSS', () => {
const violations = readComponentCssFiles()
.filter(({ css }) => RAW_FONT_SIZE_LITERAL.test(css))
.map(({ component }) => component)
expect(violations).toEqual([])
})
// Coherence guard (Fase 7 — theming audit): the type/size canon is only worth
// having if components CONSUME it. Recipe `font-size-*` / `icon-size-*` tokens
// MUST reference the `--font-size-*` / `--icon-size-*` scale (or another token),
// never a raw `px`/`rem` literal — else the text/icon stops responding to the
// type scale, `--scaling` and `applyTypeScale()`. The earlier CSS-only guard
// missed this (recipe-token values become custom properties, not `font-size:`).
// `words` / `palabras` / `chronos` are excluded (active dev tracks, per the audit).
it('keeps raw px/rem literals out of recipe font-size / icon-size tokens', () => {
const EXCLUDED = new Set(['words', 'palabras', 'chronos'])
const RAW_LENGTH = /^-?[0-9.]+(px|rem)$/
const violations: string[] = []
for (const [component, tokens] of Object.entries(RECIPE_TOKENS)) {
if (EXCLUDED.has(component)) continue
for (const [key, value] of tokenEntries(tokens)) {
if (!/(^|-)(font-size|icon-size)(-|$)/.test(key)) continue
for (const resolved of entryValues(value)) {
if (typeof resolved === 'string' && RAW_LENGTH.test(resolved.trim())) {
violations.push(`${component}.${key}: ${resolved}`)
}
}
}
}
expect(violations).toEqual([])
})
it('does not leave declared public recipe variables orphaned', () => {
const recipeValueReferences = readRecipeValueReferences()
const orphaned: string[] = []
for (const [component, tokens] of Object.entries(RECIPE_TOKENS)) {
const source = `${readComponentSource(component)}\n${recipeValueReferences}`
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
for (const key of tokenKeys(tokens)) {
const name = recipeTokenName(component, key)
if (!source.includes(name)) orphaned.push(`${component}: ${name}`)
}
}
expect(orphaned).toEqual([])
})
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Universal anti-eager-resolution guard.
*
* Closes the gap left by TSC: TSC only validates tokens in
* `recipes/base.ts`. Components that declare their palette logic
* directly in their `.css` file bypass TSC entirely. This guard
* runs at the CSS-output level and catches the bug pattern in
* EITHER source:
*
* :root {
* --{c}-derived: var(--{c}-palette-X); ← FORBIDDEN
* }
*
* Because CSS custom property substitution is eager, `--{c}-derived`
* would freeze at the :root value of `--{c}-palette-X` (the neutral
* default), and any per-instance override of `--{c}-palette-X` in
* `[data-{c}][data-color='affirm']` would be silently lost.
*
* The palette tokens themselves (`--{c}-palette-X` declared at
* `:root` as default) are NOT violations — those ARE supposed to
* have a root-level fallback. Only tokens that consume them at root
* are forbidden.
*
* Sources scanned:
* - `generated/base.css` (recipes config materialised)
* - every `src/uix/eidos/components/{c}/{c}.css` (manual recipes)
*/
it('forbids palette-derived tokens at :root scope (universal anti-eager-resolution)', () => {
const violations: string[] = []
const ROOT_BLOCK = /(?:^|\n)\s*:root\s*\{([^}]*)\}/g
const PALETTE_REF = /--([a-z][a-z0-9-]*)-palette-/g
const DECL = /(?:^|;)\s*--([a-z][a-z0-9-]*?)\s*:\s*([^;]+?)(?=;|$)/g
function scanFile(filename: string, css: string) {
const stripped = stripCssComments(css)
for (const rootMatch of stripped.matchAll(ROOT_BLOCK)) {
const block = rootMatch[1]
for (const declMatch of block.matchAll(DECL)) {
const name = declMatch[1]
const value = declMatch[2].trim()
// Skip the palette-* tokens themselves — those are
// supposed to live at :root as their neutral default.
if (/-palette-/.test(name)) continue
// Find palette refs in the value.
for (const refMatch of value.matchAll(PALETTE_REF)) {
const refComponent = refMatch[1]
violations.push(
`${filename}: \`--${name}: ${value}\` references ` +
`\`var(--${refComponent}-palette-*)\` from :root scope. ` +
`This freezes the token at the palette's root default. ` +
`Move the declaration to \`[data-${refComponent}]\` scope ` +
`(TSC scope:'host' if going through recipes/base.ts).`
)
}
}
}
}
// 1. The generator output.
scanFile(
'generated/base.css',
readFileSync('src/uix/eidos/generated/base.css', 'utf8')
)
// 2. Each component CSS recipe.
for (const { component, css } of readComponentCssFiles()) {
scanFile(`components/${component}/${component}.css`, css)
}
expect(violations).toEqual([])
})
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint Variants son canon del eidos, NO del theme. Decisión arquitectónica firmemente sostenida: el vocabulario de variants (solid/outline/ghost/ soft/surface/line/pills) está fijo a nivel del framework — paralelo a las 8 sema families del libro. Theme = retintar lo perceptualmente fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`. Cambios: - lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes canónicos (control / selection / chip / marker / tabs). Los 5 union types se derivan via `[number]` indexed access — valor y tipo no pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con todos los valores canónicos + utilidades cross-component (`plain`, `subtle`). - recipe-css-contract.test.ts: nuevo test "variant CSS selectors per component match the declared type union". Por cada componente: extrae el union type de `components/{c}/types.ts` (soporta literal unions + archetype aliases; cae a advisory mode en Extract<> y conditional types); compara con `[data-{c}][data-variant='X']` selectores en `{c}.css`; reporta typos y unauthorized extensions bidireccionalmente. - THEMING.md §19: nueva sección "Variants son canon del eidos, NO del theme" con argumentación (portabilidad, type safety, archetypes perceptuales paralelos a sema families), tabla de las 3 capas de la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado. - eidos/README.md: tabla de referencia ampliada con §19. - CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)". - CONTINUE.md: nota de la decisión arquitectónica. Variants component-specific permitidos (Banner inline/overlay/ persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada `components/{c}/types.ts` y el lint los valida contra la CSS del componente. Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6 errores pre-existentes (lib/_demo, soma/components/internal, web/ routes/active) — no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Variant canon enforcement (per-component).
*
* For every component CSS, the `[data-{component}][data-variant='X']`
* selectors must use variant values that the component's own
* `types.ts` declares — no typos, no unauthorized extensions.
*
* **Why this matters**: variants are part of the framework's
* perceptual canon (see THEMING.md §19). They are NOT
* theme-extensible — a `<Toggle variant="outline">` must render
* coherently in every theme. The 5 archetypes in `lib/types.ts >
* EIDOS_VARIANTS` cover the common cross-component vocabularies;
* component-specific variants (e.g. Banner's `inline`/`overlay`)
* are declared in each component's own `types.ts` and validated
* by this lint.
*
* **What this catches**:
* - CSS uses `data-variant='outlne'` (typo) → fails: not in type union
* - Adds `[data-toggle][data-variant='neon']` without updating
* `ToggleVariant` → fails: not in type
* - Removes `'ghost'` from type union but leaves the CSS rule →
* fails: type ⊃ CSS check
*
* **Type parsing**: extracts the union literal values from
* `export type {Pascal}Variant = ...` patterns. Handles direct
* literal unions and aliases to the 5 archetypes. Components with
* complex types (Extract / Omit / unions of multiple archetypes —
* Button, AlertDialog actions, etc.) are skipped with the variant
* pool defaulting to ALL canonical values + the component's own
* literal union; the lint runs in advisory mode there.
*/
it('variant CSS selectors per component match the declared type union', () => {
const violations: string[] = []
for (const { component } of readComponentCssFiles()) {
const cssVariants = collectComponentVariantValues(component)
if (cssVariants.size === 0) continue
const typeVariants = extractComponentVariantTypeValues(component)
if (!typeVariants) continue // type file missing or complex pattern; skip strict check
for (const variant of cssVariants) {
if (!typeVariants.has(variant)) {
violations.push(
`components/${component}/${component}.css uses ` +
`[data-${component}][data-variant='${variant}'] but ` +
`${component}/types.ts declares only [${[...typeVariants].sort().join(', ')}]`
)
}
}
}
expect(violations).toEqual([])
})
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Token Scope Contract (TSC) — the production recipes pass the
* generator's scope validation. If a recipe author mis-scopes a
* derived token (e.g. puts a palette-dependent token at :root),
* `renderStaticCss` throws and this test fails.
*
* The generator's algebra checks (a) deps inferred from `var()`
* refs and (b) cross-axis collisions across multi-declaration
* tokens. Both run on the base config below.
*/
it('the base recipe config passes the generator scope contract', () => {
expect(() => renderStaticCss(createThemeBaseEidosConfig())).not.toThrow()
})
})
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint Variants son canon del eidos, NO del theme. Decisión arquitectónica firmemente sostenida: el vocabulario de variants (solid/outline/ghost/ soft/surface/line/pills) está fijo a nivel del framework — paralelo a las 8 sema families del libro. Theme = retintar lo perceptualmente fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`. Cambios: - lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes canónicos (control / selection / chip / marker / tabs). Los 5 union types se derivan via `[number]` indexed access — valor y tipo no pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con todos los valores canónicos + utilidades cross-component (`plain`, `subtle`). - recipe-css-contract.test.ts: nuevo test "variant CSS selectors per component match the declared type union". Por cada componente: extrae el union type de `components/{c}/types.ts` (soporta literal unions + archetype aliases; cae a advisory mode en Extract<> y conditional types); compara con `[data-{c}][data-variant='X']` selectores en `{c}.css`; reporta typos y unauthorized extensions bidireccionalmente. - THEMING.md §19: nueva sección "Variants son canon del eidos, NO del theme" con argumentación (portabilidad, type safety, archetypes perceptuales paralelos a sema families), tabla de las 3 capas de la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado. - eidos/README.md: tabla de referencia ampliada con §19. - CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)". - CONTINUE.md: nota de la decisión arquitectónica. Variants component-specific permitidos (Banner inline/overlay/ persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada `components/{c}/types.ts` y el lint los valida contra la CSS del componente. Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6 errores pre-existentes (lib/_demo, soma/components/internal, web/ routes/active) — no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Pull every `[data-{component}][data-variant='X']` selector out of
* the component CSS, returning the set of X values. Strips comments
* first so commented-out selectors don't count.
*/
function collectComponentVariantValues(component: string): Set<string> {
const file = join(COMPONENTS_DIR, component, `${component}.css`)
if (!existsSync(file)) return new Set()
const css = stripCssComments(readFileSync(file, 'utf8'))
const ownPrefixes = [`[data-${component}]`, `[data-${component}-`]
const variants = new Set<string>()
const RE = /\[data-variant='([^']+)'\]/g
for (const match of css.matchAll(RE)) {
// Only consider matches where the variant selector follows a
// selector that scopes the component itself (root or any of its
// parts) — avoids picking up other component's selectors that
// happen to be in the same file (e.g. a banner CSS that styles
// a button inside it).
const ctx = css.slice(Math.max(0, match.index - 200), match.index)
if (ownPrefixes.some((pre) => ctx.includes(pre))) {
variants.add(match[1])
}
}
return variants
}
/**
* Parse `src/uix/eidos/components/{component}/types.ts` and extract
* the variant value set declared for that component. Handles:
* - Direct literal union: `export type FooVariant = 'a' | 'b' | 'c'`
* - Archetype alias: `export type FooVariant = ControlVariant`
* - Multiple variant types in one file (root + sub-parts) — merged.
*
* Returns `undefined` if the file doesn't exist OR the variant type
* uses a pattern the parser doesn't handle (Extract / Omit / unions
* of types). The caller skips strict validation for those components.
*/
function extractComponentVariantTypeValues(component: string): Set<string> | undefined {
const file = join(COMPONENTS_DIR, component, 'types.ts')
if (!existsSync(file)) return undefined
const src = readFileSync(file, 'utf8')
// Match any `export type FooVariant = ...;` declaration. The
// component might declare multiple (root + sub-part), e.g. Avatar
// has AvatarVariant + AvatarBadgeVariant. We merge all variant
// types found in the file.
const DECL = /export type \w*Variant(?:\w*)?\s*=\s*([^;]+?);/gs
const result = new Set<string>()
let sawAny = false
let sawUnknownPattern = false
for (const match of src.matchAll(DECL)) {
sawAny = true
const body = match[1].trim()
// Direct literal union
const literals = [...body.matchAll(/'([^']+)'/g)].map((m) => m[1])
if (literals.length > 0 && /^\s*'[^']+'(?:\s*\|\s*'[^']+')*\s*$/.test(body)) {
for (const v of literals) result.add(v)
continue
}
// Direct alias to a known archetype
const aliasMatch = body.match(/^\s*(ControlVariant|SelectionVariant|ChipVariant|MarkerVariant|TabsVariant)\s*$/)
if (aliasMatch) {
const archetype = aliasMatch[1].replace('Variant', '').toLowerCase() as keyof typeof EIDOS_VARIANTS
for (const v of EIDOS_VARIANTS[archetype]) result.add(v)
continue
}
// Unrecognised pattern — fall back to advisory mode.
sawUnknownPattern = true
}
if (!sawAny) return undefined
if (sawUnknownPattern) {
// Complex types (Extract<>, unions of archetypes, etc.). Be
// permissive: union all canonical values + any literals we
// did manage to extract. The check becomes "the variant must
// be canonical somewhere" rather than "it must be in THIS
// component's specific set".
return new Set([...EIDOS_VARIANT_VALUES, ...result])
}
return result
}
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage Token Scope Contract universal — no más excepciones arquitectónicas. Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar, toggle-group) ahora están dentro del contrato via dos extensiones nuevas. TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts): - `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para componentes con data-color cascadeado per-part. Consumer: select. - `composition: { foreignRecipe: { targetSelector, tokens } }` sibling key — overrides cross-recipe scoped a la cascade del host. Consumer: toggle-group modifica `--toggle-palette-*` en sus items. Migraciones: - select: 3 private `_accent-{track,border,text}` con parts: ['trigger', 'content'] + 7 cascades color:X each. Removed orphan `_accent-solid` (CSS no consumía). - avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques CSS × 2 partes. - toggle-group: composition block con 8 palette tokens × 4 colors. Reemplaza 4 bloques CSS per-color. Bug toggle-group post-composition (encontrado y arreglado): Tras la composition migration, el cascade del toggle-group seguía roto porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`, etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía undefined. Fix: inlined derivation expressions directamente en `[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost), referenciando palette tokens en su propio scope local. Validador + emisor + contract: - `validateRecipeComposition` valida el shape `{ targetSelector, tokens }` y rechaza composition entries con scope='root'. - `stripCompositionKey` + `emitComposition` separan el pipeline. - `appendRecipeContractTokens` skip-list para `composition` (no aparece como fake `--{c}-composition` knob). - `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts filtran composition en todos los iteradores. Documentación: - THEMING.md §18 reescrito como "Cobertura universal de TSC". §7 extendido con subsecciones "Multi-part scope" y "Cross-recipe composition" + ejemplos completos. TOC actualizado. - eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18. - CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal). - CONTINUE.md reescrito al estado actual de la sesión. Working tree también incluye sprint Words en paralelo (multiple authors): slash menu, find/replace regex, code language picker, table audit, toolbar family menu, code highlight engine. Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`: 6 errores pre-existentes (lib/_demo, soma/components/internal, web/routes/active) no relacionados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/**
* Token Scope Contract — generator-level scenario tests.
*
* These exercise the validation algebra directly through `renderStaticCss`
* with synthetic recipes. They lock in the contract semantics so any
* future regression in `appendRecipeDeclarations` (or in the algebra
* helpers) surfaces as a clear test failure.
*/
describe('Token Scope Contract — scope algebra', () => {
it('rejects: root token depending on host token (the original Toggle bug)', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': {
value: 'var(--color-neutral-solid)',
scope: 'host'
},
// Inferred dep on palette-solid. Consumer at root → fails.
'solid-on-bg': 'var(--synth-palette-solid)'
}
})
)
).toThrow(/synth\.solid-on-bg.*scope root/)
})
it('accepts: host token depending on host token', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-on-bg': { value: 'var(--synth-palette-solid)', scope: 'host' }
}
})
)
).not.toThrow()
})
it('rejects: host token depending on color-only token', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': {
value: 'var(--color-affirm-solid)',
scope: 'color:affirm'
},
// Dep only exists at color:affirm. Host scope is broader,
// applies to elements without data-color='affirm'. Fail.
'solid-on-bg': { value: 'var(--synth-palette-solid)', scope: 'host' }
}
})
)
).toThrow(/synth\.solid-on-bg.*dependency .palette-solid./)
})
it('accepts: color token depending on host token', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'affirm-bg': {
value: 'var(--synth-palette-solid)',
scope: 'color:affirm'
}
}
})
)
).not.toThrow()
})
it('accepts: declarations[] shape with one host + one color override', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': {
declarations: [
{ value: 'var(--color-neutral-solid)', scope: 'host' },
{ value: 'var(--color-affirm-solid)', scope: 'color:affirm' }
]
},
'solid-on-bg': { value: 'var(--synth-palette-solid)', scope: 'host' }
}
})
)
).not.toThrow()
})
it('rejects: cross-axis collision without composite declaration', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
bg: {
declarations: [
{ value: 'red', scope: 'color:affirm' },
{ value: 'blue', scope: 'state:on' }
]
}
}
})
)
).toThrow(/can both apply to the same element.*composite/)
})
it('accepts: cross-axis declarations with explicit composite intersection', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
bg: {
declarations: [
{ value: 'red', scope: 'color:affirm' },
{ value: 'blue', scope: 'state:on' },
{ value: 'purple', scope: ['color:affirm', 'state:on'] }
]
}
}
})
)
).not.toThrow()
})
it('accepts: palette:* legacy alias is normalized to color:*', () => {
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': {
declarations: [
{ value: 'var(--color-neutral-solid)', scope: 'host' },
{ value: 'var(--color-affirm-solid)', scope: 'palette:affirm' }
]
},
'solid-on-bg': { value: 'var(--synth-palette-solid)', scope: 'host' }
}
})
)
).not.toThrow()
})
it('infers deps from var() without explicit depends array', () => {
// No `depends: [...]` on solid-on-bg — the generator infers it
// from var(--synth-palette-solid). Without inference, this would
// pass spuriously (bare-string at root is the v1 escape hatch).
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-on-bg': 'var(--synth-palette-solid)' // bare string → root
}
})
)
).toThrow(/synth\.solid-on-bg.*scope root/)
})
it('ignores var() refs to tokens outside the same recipe', () => {
// `var(--color-neutral-solid)` references a primitive/theme token,
// not a fellow recipe token. Should NOT be treated as a dep.
expect(() =>
renderStaticCss(
configWithRecipes({
synth: {
bg: 'var(--color-neutral-solid)' // external ref, root scope OK
}
})
)
).not.toThrow()
})
})

Powered by TurnKey Linux.