feat(eidos): typography component props (Phase 3b) — wrap/tracking/leading/numeric/measure

Wires the Phase 3 scales into <Text> and <Heading> + 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) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent d85c2fd376
commit ecc514951f

@ -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;

@ -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 <Text>). 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(' ');
});

@ -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<HTMLAttributes<HTMLHeadingElement>, 'style' | 'c
color?: ResponsiveProp<HeadingColor>;
/** Text alignment. */
align?: ResponsiveProp<HeadingAlign>;
/** Override letter-spacing with a tracking-scale step. */
tracking?: ResponsiveProp<TextTracking>;
/** Override line-height with a leading-scale step. */
leading?: ResponsiveProp<TextLeading>;
/** `text-wrap` mode. Headings default to `balance` (override here). */
wrap?: ResponsiveProp<TextWrap>;
/** Numeral variant — `tabular` aligns digits in columns. */
numeric?: ResponsiveProp<TextNumeric>;
/** Constrain line length to a measure step. */
measure?: ResponsiveProp<TextMeasure>;
/** Italic shorthand. @default false */
italic?: boolean;
/** Underline shorthand. @default false */

@ -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;

@ -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(' ');
});

@ -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<HTMLAttributes<HTMLElement>, 'style' | 'children'> & {
/**
* Named typography style. Applies the full set of `--style-{name}-*`
@ -39,6 +54,16 @@ export type TextProps = Omit<HTMLAttributes<HTMLElement>, 'style' | 'children'>
color?: ResponsiveProp<TextColor>;
/** Text alignment. */
align?: ResponsiveProp<TextAlign>;
/** Override letter-spacing with a tracking-scale step. */
tracking?: ResponsiveProp<TextTracking>;
/** Override line-height with a leading-scale step. */
leading?: ResponsiveProp<TextLeading>;
/** `text-wrap` mode — `balance` for headings, `pretty` for prose. */
wrap?: ResponsiveProp<TextWrap>;
/** Numeral variant — `tabular` aligns digits in columns (tables / data). */
numeric?: ResponsiveProp<TextNumeric>;
/** Constrain line length to a measure step (needs a block `as`, e.g. `p`). */
measure?: ResponsiveProp<TextMeasure>;
/** Italic shorthand for `font-style: italic`. @default false */
italic?: boolean;
/** Underline shorthand for `text-decoration: underline`. @default false */

@ -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);

@ -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 {

Loading…
Cancel
Save

Powered by TurnKey Linux.