From ecc514951f869f76cea51ab2ccd3dc2d312e5a93 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 5 Jun 2026 03:28:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(eidos):=20typography=20component=20props?= =?UTF-8?q?=20(Phase=203b)=20=E2=80=94=20wrap/tracking/leading/numeric/mea?= =?UTF-8?q?sure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the Phase 3 scales into and + fixes a token collision the scales introduced. - Text/Heading props (additive): `tracking` / `leading` (reuse the existing --_x-letter-spacing / --_x-line-height vars → override the size-derived values), `wrap` (text-wrap: balance/pretty/nowrap), `numeric` (tabular/oldstyle → font-feature-settings), `measure` (max-inline-size). Heading defaults to `text-wrap: balance` (reference-grade titles); Text defaults to the CSS initial so the axes are no-ops until a prop is set. Heading reuses Text's scale unions. - COLLISION FIX: Phase 3a's config-driven --tracking-{tight,normal,wide,wider} collided with a pre-existing HARDCODED tracking scale in render-css (semantic badge/ui/… + scale tight/normal/wide/wider, all 0) emitted later → it won (everything resolved to 0, so the tracking prop did nothing). Removed the hardcoded scale lines; the config (typography.tracking, real optical values) now owns tighter/tight/normal/wide/wider. The semantic tracking tokens (badge/label/ui/prose/heading/display) stay (recipes use them, e.g. card-title --tracking-tight now picks up the real -0.02em). leading/features /measure don't collide (distinct keys). Verified in browser: tracking-wide 0.02em -> 0.32px; tracking-tight -0.02em -> -0.32px; heading default text-wrap balance; wrap=pretty, numeric=tabular, measure, leading all apply. check 0 errors; eidos suite green (3 pre-existing words failures only). Note: the pre-existing hardcoded semantic typography block (leading-ui/prose/… + tracking-badge/…) is still hardcoded, not config-driven — a separate cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/uix/eidos/components/heading/heading.css | 6 +++++ .../eidos/components/heading/heading.svelte | 18 +++++++++++++ src/uix/eidos/components/heading/types.ts | 17 +++++++++++++ src/uix/eidos/components/text/text.css | 6 +++++ src/uix/eidos/components/text/text.svelte | 18 +++++++++++++ src/uix/eidos/components/text/types.ts | 25 +++++++++++++++++++ src/uix/eidos/generated/base.css | 4 --- src/uix/eidos/lib/render-css.ts | 8 +++--- 8 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/uix/eidos/components/heading/heading.css b/src/uix/eidos/components/heading/heading.css index cd9840fa0..335b3927b 100644 --- a/src/uix/eidos/components/heading/heading.css +++ b/src/uix/eidos/components/heading/heading.css @@ -23,6 +23,12 @@ letter-spacing: var(--_heading-letter-spacing, var(--style-h2-letter-spacing)); color: var(--_heading-color, var(--style-h2-color)); text-align: var(--_heading-align, inherit); + /* Phase 3b: headings default to `text-wrap: balance` (reference-grade titles — + * even line lengths); override via the `wrap` prop. feature/measure default to the + * CSS initial (no-ops until set). tracking/leading reuse the vars above. */ + text-wrap: var(--_heading-wrap, balance); + font-feature-settings: var(--_heading-font-feature, normal); + max-inline-size: var(--_heading-measure, none); font-style: normal; text-decoration: none; margin: 0; diff --git a/src/uix/eidos/components/heading/heading.svelte b/src/uix/eidos/components/heading/heading.svelte index 87704962e..428dc5088 100644 --- a/src/uix/eidos/components/heading/heading.svelte +++ b/src/uix/eidos/components/heading/heading.svelte @@ -24,6 +24,11 @@ weight, color, align, + tracking, + leading, + wrap, + numeric, + measure, italic = false, underline = false, truncate = false, @@ -64,6 +69,19 @@ if (alignKey) pushVar(decls, '--_heading-align', alignKey); if (typeof clamp === 'number' && clamp > 0) pushVar(decls, '--_heading-clamp', String(clamp)); + // Phase 3b scales (mirror ). tracking/leading override the size-derived + // letter-spacing / line-height; wrap/numeric/measure are new CSS axes. + const trackingKey = eidos.resolve(tracking); + const leadingKey = eidos.resolve(leading); + const wrapVal = eidos.resolve(wrap); + const numericKey = eidos.resolve(numeric); + const measureKey = eidos.resolve(measure); + if (trackingKey) pushVar(decls, '--_heading-letter-spacing', `var(--tracking-${trackingKey})`); + if (leadingKey) pushVar(decls, '--_heading-line-height', `var(--leading-${leadingKey})`); + if (wrapVal) pushVar(decls, '--_heading-wrap', wrapVal); + if (numericKey) pushVar(decls, '--_heading-font-feature', `var(--font-feature-${numericKey})`); + if (measureKey) pushVar(decls, '--_heading-measure', `var(--measure-${measureKey})`); + const inline = decls.join(' '); return [inline, tagStyle].filter(Boolean).join(' '); }); diff --git a/src/uix/eidos/components/heading/types.ts b/src/uix/eidos/components/heading/types.ts index deeb71cc7..e7bd2a5d1 100644 --- a/src/uix/eidos/components/heading/types.ts +++ b/src/uix/eidos/components/heading/types.ts @@ -1,6 +1,13 @@ import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; import type { ResponsiveProp } from '$uix/eidos/lib/types'; +import type { + TextTracking, + TextLeading, + TextWrap, + TextNumeric, + TextMeasure +} from '../text/types'; /** Named heading styles emitted by the foundation as `--style-h{n}-*`. */ export type HeadingStyle = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; @@ -47,6 +54,16 @@ export type HeadingProps = Omit, 'style' | 'c color?: ResponsiveProp; /** Text alignment. */ align?: ResponsiveProp; + /** Override letter-spacing with a tracking-scale step. */ + tracking?: ResponsiveProp; + /** Override line-height with a leading-scale step. */ + leading?: ResponsiveProp; + /** `text-wrap` mode. Headings default to `balance` (override here). */ + wrap?: ResponsiveProp; + /** Numeral variant — `tabular` aligns digits in columns. */ + numeric?: ResponsiveProp; + /** Constrain line length to a measure step. */ + measure?: ResponsiveProp; /** Italic shorthand. @default false */ italic?: boolean; /** Underline shorthand. @default false */ diff --git a/src/uix/eidos/components/text/text.css b/src/uix/eidos/components/text/text.css index cc0793706..f1e8973d9 100644 --- a/src/uix/eidos/components/text/text.css +++ b/src/uix/eidos/components/text/text.css @@ -21,6 +21,12 @@ letter-spacing: var(--_text-letter-spacing, var(--style-body-letter-spacing)); color: var(--_text-color, var(--style-body-color)); text-align: var(--_text-align, inherit); + /* Phase 3b axes — default to the CSS initial so they're no-ops until a prop sets + * the per-instance var (tracking/leading reuse --_text-letter-spacing / -line-height + * above). `measure` needs a block `as` (e.g. `p`) to take effect. */ + text-wrap: var(--_text-wrap, wrap); + font-feature-settings: var(--_text-font-feature, normal); + max-inline-size: var(--_text-measure, none); font-style: normal; text-decoration: none; margin: 0; diff --git a/src/uix/eidos/components/text/text.svelte b/src/uix/eidos/components/text/text.svelte index c081705e7..2c3a80461 100644 --- a/src/uix/eidos/components/text/text.svelte +++ b/src/uix/eidos/components/text/text.svelte @@ -27,6 +27,11 @@ weight, color, align, + tracking, + leading, + wrap, + numeric, + measure, italic = false, underline = false, truncate = false, @@ -62,6 +67,19 @@ if (alignKey) pushVar(decls, '--_text-align', alignKey); if (typeof clamp === 'number' && clamp > 0) pushVar(decls, '--_text-clamp', String(clamp)); + // Phase 3b scales. tracking/leading override the size-derived letter-spacing / + // line-height set above; wrap/numeric/measure are new CSS axes. + const trackingKey = eidos.resolve(tracking); + const leadingKey = eidos.resolve(leading); + const wrapVal = eidos.resolve(wrap); + const numericKey = eidos.resolve(numeric); + const measureKey = eidos.resolve(measure); + if (trackingKey) pushVar(decls, '--_text-letter-spacing', `var(--tracking-${trackingKey})`); + if (leadingKey) pushVar(decls, '--_text-line-height', `var(--leading-${leadingKey})`); + if (wrapVal) pushVar(decls, '--_text-wrap', wrapVal); + if (numericKey) pushVar(decls, '--_text-font-feature', `var(--font-feature-${numericKey})`); + if (measureKey) pushVar(decls, '--_text-measure', `var(--measure-${measureKey})`); + const inline = decls.join(' '); return [inline, tagStyle].filter(Boolean).join(' '); }); diff --git a/src/uix/eidos/components/text/types.ts b/src/uix/eidos/components/text/types.ts index 41e3f8645..c3209721e 100644 --- a/src/uix/eidos/components/text/types.ts +++ b/src/uix/eidos/components/text/types.ts @@ -20,6 +20,21 @@ export type TextColor = 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-sol /** Horizontal text alignment. */ export type TextAlign = 'start' | 'center' | 'end' | 'justify'; +/** Letter-spacing scale. Maps to `var(--tracking-{key})`. */ +export type TextTracking = 'tighter' | 'tight' | 'normal' | 'wide' | 'wider'; + +/** Line-height scale. Maps to `var(--leading-{key})`. */ +export type TextLeading = 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose'; + +/** `text-wrap` mode — `balance` for headings, `pretty` for prose. */ +export type TextWrap = 'wrap' | 'nowrap' | 'balance' | 'pretty'; + +/** Numeral variant via `font-feature-settings`. `tabular` = column-aligned digits. */ +export type TextNumeric = 'tabular' | 'oldstyle'; + +/** Line-length / measure scale. Maps to `var(--measure-{key})`. */ +export type TextMeasure = 'narrow' | 'normal' | 'wide'; + export type TextProps = Omit, 'style' | 'children'> & { /** * Named typography style. Applies the full set of `--style-{name}-*` @@ -39,6 +54,16 @@ export type TextProps = Omit, 'style' | 'children'> color?: ResponsiveProp; /** Text alignment. */ align?: ResponsiveProp; + /** Override letter-spacing with a tracking-scale step. */ + tracking?: ResponsiveProp; + /** Override line-height with a leading-scale step. */ + leading?: ResponsiveProp; + /** `text-wrap` mode — `balance` for headings, `pretty` for prose. */ + wrap?: ResponsiveProp; + /** Numeral variant — `tabular` aligns digits in columns (tables / data). */ + numeric?: ResponsiveProp; + /** Constrain line length to a measure step (needs a block `as`, e.g. `p`). */ + measure?: ResponsiveProp; /** Italic shorthand for `font-style: italic`. @default false */ italic?: boolean; /** Underline shorthand for `text-decoration: underline`. @default false */ diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 7a1a499bb..34a6fad06 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -481,10 +481,6 @@ --tracking-prose: 0; --tracking-heading: 0; --tracking-display: 0; - --tracking-tight: 0; - --tracking-normal: 0; - --tracking-wide: 0; - --tracking-wider: 0; --motion-spin-duration: 800ms; --color-focus-ring: var(--focus-ring-color); --accordion-provider-border-width: var(--border-width); diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index f25972761..e1d079699 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -1627,10 +1627,10 @@ function appendTypographyAliases( declarations.push(cssVar('tracking-prose', '0')) declarations.push(cssVar('tracking-heading', '0')) declarations.push(cssVar('tracking-display', '0')) - declarations.push(cssVar('tracking-tight', '0')) - declarations.push(cssVar('tracking-normal', '0')) - declarations.push(cssVar('tracking-wide', '0')) - declarations.push(cssVar('tracking-wider', '0')) + // The tracking SCALE (tighter/tight/normal/wide/wider) is config-driven now — + // emitted from `typography.tracking` in appendTypographyDeclarations (Fase 3) with + // real optical values. The semantic tracking tokens above (badge/ui/…) stay 0 + // until a theme sets them; recipes that want the scale use --tracking-tight etc. } function hasFocusColor(options: EidosConfig): boolean {