feat(eidos foundation): emit --style-{name}-* tokens for named typography styles

`STATIC_TYPOGRAPHY.styles` already defined 11 named typography styles
(hero, h1..h6, body, prose, label, caption, code) but the renderer
only emitted families / sizes / weights — the style entries lived
as data with no CSS reflection.

Extend `appendTypographyDeclarations` so each named style emits:

- `--style-{name}-font-family`  → `var(--font-family-{family})`
- `--style-{name}-font-size`    → `var(--font-size-{size})`
- `--style-{name}-line-height`  → literal or `var(--font-line-height-{size})`
- `--style-{name}-letter-spacing` → literal or `var(--font-letter-spacing-{size})`
- `--style-{name}-font-weight`  → `var(--font-weight-{weight})` (or numeric)
- `--style-{name}-color`        → `var(--color-{dot.path → dash-path})`

Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
emit the base value into `:root` and per-breakpoint
`@media (min-width: …)` blocks that override the same variables.
Breakpoint thresholds match `$libs/dom/responsive` (sm: 480, md: 768,
lg: 1024, xl: 1280, xxl: 1536).

`renderStaticCss` now iterates `STYLE_BREAKPOINT_ORDER` after the
main `:root` block and appends one media-query block per breakpoint
that has at least one responsive override. The `indentBlock` helper
preserves indentation inside the wrapper.

`base.css` regenerated. svelte-check 0 errors, `npm run component:audit`
81 / 81 PASS unchanged. This is foundation-only; no components consume
the new tokens yet — that lands in the typography port (Text + Heading
+ Display + Code + CodeBlock + Kbd + Mark + Highlight + Link).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 376f9e33eb
commit 015bd761bd

@ -166,6 +166,78 @@
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--style-hero-font-family: var(--font-family-display);
--style-hero-font-size: var(--font-size-xxl);
--style-hero-line-height: 1;
--style-hero-letter-spacing: 0;
--style-hero-font-weight: var(--font-weight-bold);
--style-hero-color: var(--color-content-primary);
--style-h1-font-family: var(--font-family-display);
--style-h1-font-size: var(--font-size-xl);
--style-h1-line-height: 1.05;
--style-h1-letter-spacing: 0;
--style-h1-font-weight: var(--font-weight-bold);
--style-h1-color: var(--color-content-primary);
--style-h2-font-family: var(--font-family-display);
--style-h2-font-size: var(--font-size-lg);
--style-h2-line-height: 1.1;
--style-h2-letter-spacing: 0;
--style-h2-font-weight: var(--font-weight-bold);
--style-h2-color: var(--color-content-primary);
--style-h3-font-family: var(--font-family-primary);
--style-h3-font-size: var(--font-size-lg);
--style-h3-line-height: 1.2;
--style-h3-letter-spacing: 0;
--style-h3-font-weight: var(--font-weight-semibold);
--style-h3-color: var(--color-content-primary);
--style-h4-font-family: var(--font-family-primary);
--style-h4-font-size: var(--font-size-md);
--style-h4-line-height: 1.25;
--style-h4-letter-spacing: 0;
--style-h4-font-weight: var(--font-weight-semibold);
--style-h4-color: var(--color-content-primary);
--style-h5-font-family: var(--font-family-primary);
--style-h5-font-size: var(--font-size-sm);
--style-h5-line-height: 1.3;
--style-h5-letter-spacing: 0;
--style-h5-font-weight: var(--font-weight-semibold);
--style-h5-color: var(--color-content-primary);
--style-h6-font-family: var(--font-family-primary);
--style-h6-font-size: var(--font-size-xs);
--style-h6-line-height: 1.35;
--style-h6-letter-spacing: 0;
--style-h6-font-weight: var(--font-weight-semibold);
--style-h6-color: var(--color-content-primary);
--style-body-font-family: var(--font-family-primary);
--style-body-font-size: var(--font-size-md);
--style-body-line-height: 1.55;
--style-body-letter-spacing: 0;
--style-body-font-weight: var(--font-weight-regular);
--style-body-color: var(--color-content-primary);
--style-prose-font-family: var(--font-family-secondary);
--style-prose-font-size: var(--font-size-md);
--style-prose-line-height: 1.65;
--style-prose-letter-spacing: 0;
--style-prose-font-weight: var(--font-weight-regular);
--style-prose-color: var(--color-content-primary);
--style-label-font-family: var(--font-family-primary);
--style-label-font-size: var(--font-size-sm);
--style-label-line-height: 1.25;
--style-label-letter-spacing: 0;
--style-label-font-weight: var(--font-weight-medium);
--style-label-color: var(--color-content-primary);
--style-caption-font-family: var(--font-family-primary);
--style-caption-font-size: var(--font-size-xs);
--style-caption-line-height: 1.35;
--style-caption-letter-spacing: 0;
--style-caption-font-weight: var(--font-weight-regular);
--style-caption-color: var(--color-content-secondary);
--style-code-font-family: var(--font-family-mono);
--style-code-font-size: var(--font-size-sm);
--style-code-line-height: 1.45;
--style-code-letter-spacing: 0;
--style-code-font-weight: var(--font-weight-regular);
--style-code-color: var(--color-content-primary);
--size-xxs-control-height: var(--control-height-xxs);
--size-xxs-font-size: var(--font-size-xxs);
--size-xxs-font-line-height: var(--font-line-height-xxs);
@ -2414,6 +2486,20 @@
--tooltip-shadow: var(--shadow-overlay);
}
@media (min-width: 768px) {
:root {
--style-hero-font-size: var(--font-size-xxxl);
--style-hero-line-height: var(--font-line-height-xxxl);
--style-hero-letter-spacing: var(--font-letter-spacing-xxxl);
--style-h1-font-size: var(--font-size-xxl);
--style-h1-line-height: var(--font-line-height-xxl);
--style-h1-letter-spacing: var(--font-letter-spacing-xxl);
--style-h2-font-size: var(--font-size-xl);
--style-h2-line-height: var(--font-line-height-xl);
--style-h2-letter-spacing: var(--font-letter-spacing-xl);
}
}
[data-density='compact'] {
--density-scale: var(--density-compact-scale);
--density-space-scale: var(--density-compact-space-scale);

@ -159,11 +159,30 @@ export function renderStaticCss(options: EidosConfig): string {
appendRecipeDeclarations(declarations, options.recipes)
const blocks = [renderBlock(':root', declarations)]
// Responsive typography style overrides — one media-query block per
// breakpoint that has at least one responsive size override.
const styleResponsive = collectTypographyStyleResponsive(primitives.typography)
for (const bp of STYLE_BREAKPOINT_ORDER) {
const lines = styleResponsive[bp]
if (!lines || lines.length === 0) continue
const width = STYLE_BREAKPOINT_WIDTHS[bp]
const rootBlock = renderBlock(':root', lines)
blocks.push(`@media (min-width: ${width}px) {\n${indentBlock(rootBlock)}\n}`)
}
if (primitives.density) blocks.push(renderDensityBlocks())
return blocks.join('\n\n')
}
function indentBlock(block: string): string {
return block
.split('\n')
.map((line) => (line ? `\t${line}` : line))
.join('\n')
}
export function renderContractCss(
options: EidosConfig,
renderOptions: RenderContractCssOptions = {}
@ -342,6 +361,172 @@ function appendTypographyDeclarations(
for (const [name, weight] of Object.entries(typography.weights)) {
declarations.push(cssVar(`font-weight-${name}`, weight))
}
if (typography.styles) {
appendTypographyStyleDeclarations(declarations, typography)
}
}
/**
* Breakpoint min-width thresholds matching `$libs/dom/responsive`.
* `base` is the implicit 0 fallback — emitted into `:root`, not as a
* media query.
*/
const STYLE_BREAKPOINT_WIDTHS: Record<string, number> = {
sm: 480,
md: 768,
lg: 1024,
xl: 1280,
xxl: 1536
}
const STYLE_BREAKPOINT_ORDER = ['sm', 'md', 'lg', 'xl', 'xxl'] as const
/**
* Emit the named typography styles (hero, h1..h6, body, prose, label,
* caption, code, …) as `--style-{name}-{font-family,font-size,
* font-weight,line-height,letter-spacing,color}` custom properties.
*
* Each style entry references primitives the foundation already emits
* (`var(--font-family-X)`, `var(--font-size-Y)`, etc.). Components
* consume these tokens directly — `<Heading level=1>` reads
* `var(--style-h1-font-size)` and friends.
*
* Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
* are emitted as base declarations in `:root` plus per-breakpoint
* `@media (min-width: …)` overrides appended after the main block.
* Non-size fields (family, weight, line-height, letter-spacing, color)
* are not responsive in this contract.
*/
function appendTypographyStyleDeclarations(
declarations: string[],
typography: TypographyPrimitiveSet
): void {
const styles = typography.styles
if (!styles) return
// Capture responsive size overrides per breakpoint. Same shape as
// `declarations` but keyed by the active breakpoint so we can emit
// them as media-query blocks after the base.
for (const [name, style] of Object.entries(styles)) {
if (style.family) {
declarations.push(
cssVar(`style-${name}-font-family`, `var(--font-family-${style.family})`)
)
}
const sizeRes = style.size
if (sizeRes !== undefined) {
const baseSize = pickResponsiveBase(sizeRes)
if (baseSize !== undefined) {
declarations.push(
cssVar(`style-${name}-font-size`, resolveStyleSizeRef(baseSize))
)
declarations.push(
cssVar(
`style-${name}-line-height`,
style.lineHeight ?? resolveStyleLineHeightRef(baseSize)
)
)
declarations.push(
cssVar(
`style-${name}-letter-spacing`,
style.letterSpacing ?? resolveStyleLetterSpacingRef(baseSize)
)
)
} else if (style.lineHeight) {
declarations.push(cssVar(`style-${name}-line-height`, style.lineHeight))
}
} else if (style.lineHeight) {
declarations.push(cssVar(`style-${name}-line-height`, style.lineHeight))
}
if (style.weight !== undefined) {
const weightRef =
typeof style.weight === 'number'
? String(style.weight)
: `var(--font-weight-${style.weight})`
declarations.push(cssVar(`style-${name}-font-weight`, weightRef))
}
if (style.color) {
declarations.push(
cssVar(`style-${name}-color`, `var(--color-${style.color.replace(/\./g, '-')})`)
)
}
}
}
/**
* Collect per-breakpoint style size overrides. Returns a record mapping
* breakpoint -> declaration list. Empty buckets are dropped by the
* caller before emitting media-query blocks.
*/
function collectTypographyStyleResponsive(
typography: TypographyPrimitiveSet | undefined
): Record<string, string[]> {
const result: Record<string, string[]> = {}
if (!typography?.styles) return result
for (const [name, style] of Object.entries(typography.styles)) {
const sizeRes = style.size
if (sizeRes === undefined) continue
if (typeof sizeRes !== 'object' || sizeRes === null || isTextMetric(sizeRes)) continue
const responsive = sizeRes as Partial<Record<string, unknown>>
for (const bp of STYLE_BREAKPOINT_ORDER) {
const value = responsive[bp]
if (value === undefined) continue
const list = result[bp] ?? (result[bp] = [])
list.push(cssVar(`style-${name}-font-size`, resolveStyleSizeRef(value)))
list.push(cssVar(`style-${name}-line-height`, resolveStyleLineHeightRef(value)))
list.push(cssVar(`style-${name}-letter-spacing`, resolveStyleLetterSpacingRef(value)))
}
}
return result
}
function pickResponsiveBase(value: unknown): unknown {
if (value === undefined || value === null) return undefined
if (typeof value === 'string' || typeof value === 'number') return value
if (isTextMetric(value)) return value
if (typeof value === 'object') {
const obj = value as Partial<Record<string, unknown>>
if (obj.base !== undefined) return obj.base
// Fallback: take the first defined breakpoint in order.
for (const bp of STYLE_BREAKPOINT_ORDER) {
if (obj[bp] !== undefined) return obj[bp]
}
}
return undefined
}
function isTextMetric(value: unknown): boolean {
return (
typeof value === 'object' &&
value !== null &&
'size' in value &&
'lineHeight' in value &&
'letterSpacing' in value
)
}
function resolveStyleSizeRef(value: unknown): string {
if (typeof value === 'string') return `var(--font-size-${value})`
if (isTextMetric(value)) return (value as { size: string }).size
return 'inherit'
}
function resolveStyleLineHeightRef(value: unknown): string {
if (typeof value === 'string') return `var(--font-line-height-${value})`
if (isTextMetric(value)) return (value as { lineHeight: string }).lineHeight
return 'inherit'
}
function resolveStyleLetterSpacingRef(value: unknown): string {
if (typeof value === 'string') return `var(--font-letter-spacing-${value})`
if (isTextMetric(value)) return (value as { letterSpacing: string }).letterSpacing
return 'normal'
}
function appendSizeDeclarations(declarations: string[], sizes: SizePrimitiveSet): void {

Loading…
Cancel
Save

Powered by TurnKey Linux.