perf(eidos): prune base.css to role-referenced scales (Phase 5) — −18 KB gz

base.css shipped all 31 Radix donor scales, but the theme's roles only
reference 9 (gray green indigo orange plum purple red slate teal). The
other 22 were dead weight on every page: no component references raw
--scale-* (they use --color-{role}-*), and runtime theming builds from JS
scale data and writes RESOLVED values (build-scheme: variables[k]=hex),
so it never reads the CSS scale tokens.

renderThemeCss now emits only role-referenced scales by default; the full
31-scale donor palette ships opt-in in generated/palette.css (new
renderColorPaletteCss). The CSS contract stays the full vocabulary
(apps can still override any scale; load palette.css to use the extra 22).

base.css 53.7→35.4 KB gz. Monolith (every page) 54.4→36.2 KB gz —
combined with Phase 3, 113.5→36.2 KB gz total (−68%). Verified: roles
resolve (primary=purple, button bg correct), pruned scales empty on
regular pages, restored on /temas/color via palette.css. Updated the
contract test to verify coverage against the full palette. check clean
(13 errors are untracked heroscrolling demo + gitignored tmp/lexical);
eidos suite at baseline (192/195, 3 pre-existing words failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 693ed54572
commit 95be3b6cb1

@ -2,13 +2,20 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { renderGeneratedBaseEidosCss } from '../src/uix/eidos/lib/generated-css.ts';
import {
renderGeneratedBaseEidosCss,
renderGeneratedPaletteCss
} from '../src/uix/eidos/lib/generated-css.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const outputPath = resolve(__dirname, '..', 'src', 'uix', 'eidos', 'generated', 'base.css');
const generatedDir = resolve(__dirname, '..', 'src', 'uix', 'eidos', 'generated');
const basePath = resolve(generatedDir, 'base.css');
const palettePath = resolve(generatedDir, 'palette.css');
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, `${renderGeneratedBaseEidosCss()}\n`);
mkdirSync(generatedDir, { recursive: true });
writeFileSync(basePath, `${renderGeneratedBaseEidosCss()}\n`);
writeFileSync(palettePath, `${renderGeneratedPaletteCss()}\n`);
console.log(`Generated ${outputPath}`);
console.log(`Generated ${basePath}`);
console.log(`Generated ${palettePath}`);

@ -1109,7 +1109,13 @@ describe('ActiveEidos config', () => {
const eidos = createThemeBaseEidos()
const contract = eidos.getCssContract()
const staticDeclarations = cssVariableDeclarations(eidos.renderStaticCss())
const themeDeclarations = cssVariableDeclarations(eidos.renderThemeCss('base-light'))
// The CSS contract is the full token vocabulary. base.css (renderThemeCss
// default) ships only the role-referenced donor scales; the other scales
// live in the opt-in palette (scales: 'all'). Verify the contract is
// covered by the FULL emission, not just the slim foundation.
const themeDeclarations = cssVariableDeclarations(
eidos.renderThemeCss('base-light', { scales: 'all' })
)
for (const token of contract.static) {
expect(staticDeclarations.has(token.cssVar), token.cssVar).toBe(true)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,6 +1,9 @@
import { renderStaticCss, renderThemeCss } from './render-css';
import { renderColorPaletteCss, renderStaticCss, renderThemeCss } from './render-css';
import { createThemeBaseEidosConfig } from './themes/base';
const LIGHT_SELECTOR = ":root,\n[data-theme='base-light'],\n[data-theme='light']";
const DARK_SELECTOR = "[data-theme='base-dark'],\n[data-theme='dark']";
export function renderGeneratedBaseEidosCss(): string {
const config = createThemeBaseEidosConfig();
@ -12,13 +15,38 @@ export function renderGeneratedBaseEidosCss(): string {
* Command: npm run generate:eidos-css
*
* Do not edit this file by hand.
*
* The color foundation emits only the donor scales the theme's roles
* reference. The full 31-scale palette ships opt-in in palette.css —
* import it only if you reference raw --scale-{name}-{step} tokens.
*/`,
renderStaticCss(config),
renderThemeCss(config, 'base-light', {
selector: ":root,\n[data-theme='base-light'],\n[data-theme='light']"
}),
renderThemeCss(config, 'base-dark', {
selector: "[data-theme='base-dark'],\n[data-theme='dark']"
})
renderThemeCss(config, 'base-light', { selector: LIGHT_SELECTOR }),
renderThemeCss(config, 'base-dark', { selector: DARK_SELECTOR })
].join('\n\n');
}
/**
* The full donor palette — all 31 raw `--scale-*` scales (light + dark).
* Opt-in: `base.css` (the foundation) only ships the scales the theme's roles
* use; this file restores the complete palette for apps/tools that reference
* raw scale tokens directly (e.g. the color showcase). The framework itself
* never imports it — components use `--color-{role}-*`.
*/
export function renderGeneratedPaletteCss(): string {
const config = createThemeBaseEidosConfig();
return [
`/*
* Generated Eidos full color palette (all 31 donor scales).
*
* Source: src/uix/eidos/lib/themes/base.ts
* Command: npm run generate:eidos-css
*
* Do not edit this file by hand. Opt-in — import only if you reference raw
* --scale-{name}-{step} tokens; the foundation ships role scales only.
*/`,
renderColorPaletteCss(config, 'base-light', { selector: LIGHT_SELECTOR }),
renderColorPaletteCss(config, 'base-dark', { selector: DARK_SELECTOR })
].join('\n\n');
}

@ -61,6 +61,16 @@ import { EidosCssVariableError, EidosThemeNotFoundError } from '../errors'
export interface RenderThemeCssOptions {
readonly selector?: string
/**
* Which raw `--scale-*` donor tokens to emit. `'roleReferenced'`
* (default) emits only the scales the theme's roles reference — the slim
* foundation. `'all'` emits the full donor palette (used by the opt-in
* `palette.css`). Roles reference scales via `var()`, so the
* role-referenced subset is sufficient for the foundation; runtime theming
* builds from JS scale data and writes resolved values, so it never depends
* on the unemitted scales.
*/
readonly scales?: 'roleReferenced' | 'all'
}
export interface RenderContractCssOptions {
@ -396,7 +406,23 @@ export function renderThemeCss(
)
const declarations: string[] = []
const scaleNames = new Set([...Object.keys(color.scales), ...Object.keys(color.alphaScales)])
const allScaleNames = new Set([...Object.keys(color.scales), ...Object.keys(color.alphaScales)])
// Intents omitted from the theme's role map auto-derive from the palette by
// the book's canonical convention (CANONICAL_INTENT_SCALES). Identity is the
// solid (step 9); slots derive normally below. (color grammar)
const completedRoles = completeColorRoleMap(color.roles)
// Foundation slimming: emit only the donor scales the roles actually
// reference (the role loop below binds `--primitive-{role}-*` to
// `var(--scale-{name}-*)`). The full 31-scale palette ships opt-in via
// `renderColorPaletteCss` → `generated/palette.css`. Runtime theming writes
// RESOLVED values from JS scale data, so the unemitted scales are never
// needed at the foundation.
const scaleNames =
renderOptions.scales === 'all'
? allScaleNames
: roleReferencedScaleNames(completedRoles, allScaleNames)
for (const scaleName of scaleNames) {
const scale = color.scales[scaleName]
@ -404,11 +430,6 @@ export function renderThemeCss(
appendColorAlphaScaleDeclarations(declarations, scaleName, scale, color.alphaScales[scaleName])
}
// Intents omitted from the theme's role map auto-derive from the palette by
// the book's canonical convention (CANONICAL_INTENT_SCALES). Identity is the
// solid (step 9); slots derive normally below. (color grammar)
const completedRoles = completeColorRoleMap(color.roles)
for (const role of COLOR_ROLES) {
const definition = completedRoles[role]
const scaleName = getColorRoleScaleName(definition)
@ -477,6 +498,56 @@ export function renderThemeCss(
return renderBlock(renderOptions.selector ?? getThemeSelector(themeId), declarations)
}
/** The donor scales a theme's roles reference (incl. canonically-derived
* intents). The foundation emits only these; the full palette ships opt-in. */
function roleReferencedScaleNames(
completedRoles: ReturnType<typeof completeColorRoleMap>,
available: ReadonlySet<string>
): Set<string> {
const used = new Set<string>()
for (const role of COLOR_ROLES) {
const definition = completedRoles[role]
if (definition === undefined) continue
const name = getColorRoleScaleName(definition)
if (available.has(name)) used.add(name)
}
return used
}
/**
* Render ONLY the full `--scale-*` donor palette (all scales) for a theme,
* wrapped in the theme selector. Opt-in companion to the slim foundation:
* `renderThemeCss` emits only role-referenced scales, while apps/tools that
* reference raw `--scale-{name}-{step}` tokens import the palette generated
* from this (see `generated/palette.css`). The framework itself never needs
* it — components consume `--color-{role}-*`; runtime theming uses JS scale
* data and writes resolved values.
*/
export function renderColorPaletteCss(
options: EidosConfig,
themeId: string,
renderOptions: RenderThemeCssOptions = {}
): string {
const theme = options.themes?.[themeId]
if (!theme) {
throw new EidosThemeNotFoundError(themeId)
}
const color = mergeThemeColor(
options.primitives.color?.scales,
options.semantics.color,
theme.color
)
const declarations: string[] = []
const scaleNames = new Set([...Object.keys(color.scales), ...Object.keys(color.alphaScales)])
for (const scaleName of scaleNames) {
const scale = color.scales[scaleName]
if (scale) appendColorScaleDeclarations(declarations, scaleName, scale)
appendColorAlphaScaleDeclarations(declarations, scaleName, scale, color.alphaScales[scaleName])
}
return renderBlock(renderOptions.selector ?? getThemeSelector(themeId), declarations)
}
export { EidosCssVariableError } from '../errors'
function appendBorderDeclarations(declarations: string[], border: BorderPrimitiveSet): void {

@ -18,6 +18,10 @@
* project to documentElement.
*/
import '@/uix/eidos/index.css'
// This showcase renders swatches for ALL 31 donor scales via raw
// var(--scale-{name}-{step}). The foundation now ships only the role-used
// scales, so pull in the opt-in full palette here.
import '@/uix/eidos/generated/palette.css'
import {
THEME_BASE_COLOR_ROLES,
THEME_BASE_LIGHT_COLOR_SCALES,

Loading…
Cancel
Save

Powered by TurnKey Linux.