From 767bebc25fb5fef2d534339ff40ed2547aab1ef7 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 24 Jul 2026 15:59:42 -0400 Subject: [PATCH 1/4] feat(ui): migrate Mosaic Heading and Text to StyleX Move both typography components off the Emotion slot-recipe engine onto StyleX atoms, following the Button migration's contract: a stable `.cl-heading`/`.cl-text` class plus `data-size`/`data-intent` attrs, with the consumer's `className`/`style` merged last so they win. The size and intent axes are identical across the two components, so the atoms live once in a shared `typography.styles.ts`; Heading only adds its weight on top and Text adds nothing. Collapse the `--cl-text-label-*` tokens into one named step scale (`xs` -> `2xl`) that every text-bearing component sizes against -- the `label`/`label-sm` steps were byte-identical to `sm`/`xs`. Font weights become their own `--cl-font-{normal,medium,semibold,bold}` group, since weight and size vary independently. `render`, the Heading/Text contexts, and ref forwarding are unchanged. The `sx` prop is dropped as it was for Button; the three call sites that used it move to `style`. --- .changeset/mosaic-typography-stylex.md | 2 + .../swingset/src/stories/heading.stories.tsx | 24 +++++- .../swingset/src/stories/text.stories.tsx | 24 +++++- .../mosaic/components/button/button.styles.ts | 12 +-- packages/ui/src/mosaic/components/heading.tsx | 81 ------------------- .../components/heading/heading.styles.ts | 9 +++ .../components/heading/heading.test.tsx | 71 ++++++++++++++++ .../src/mosaic/components/heading/heading.tsx | 63 +++++++++++++++ .../ui/src/mosaic/components/heading/index.ts | 2 + packages/ui/src/mosaic/components/text.tsx | 79 ------------------ .../ui/src/mosaic/components/text/index.ts | 2 + .../src/mosaic/components/text/text.test.tsx | 72 +++++++++++++++++ .../ui/src/mosaic/components/text/text.tsx | 55 +++++++++++++ .../mosaic/components/typography.styles.ts | 53 ++++++++++++ ...anization-profile-domains-section.view.tsx | 2 +- ...rganization-profile-members-panel.view.tsx | 4 +- packages/ui/src/mosaic/styles/index.ts | 9 ++- packages/ui/src/mosaic/tokens.stylex.ts | 40 ++++++--- packages/ui/tsdown.mosaic.config.mts | 5 +- 19 files changed, 422 insertions(+), 187 deletions(-) create mode 100644 .changeset/mosaic-typography-stylex.md delete mode 100644 packages/ui/src/mosaic/components/heading.tsx create mode 100644 packages/ui/src/mosaic/components/heading/heading.styles.ts create mode 100644 packages/ui/src/mosaic/components/heading/heading.test.tsx create mode 100644 packages/ui/src/mosaic/components/heading/heading.tsx create mode 100644 packages/ui/src/mosaic/components/heading/index.ts delete mode 100644 packages/ui/src/mosaic/components/text.tsx create mode 100644 packages/ui/src/mosaic/components/text/index.ts create mode 100644 packages/ui/src/mosaic/components/text/text.test.tsx create mode 100644 packages/ui/src/mosaic/components/text/text.tsx create mode 100644 packages/ui/src/mosaic/components/typography.styles.ts diff --git a/.changeset/mosaic-typography-stylex.md b/.changeset/mosaic-typography-stylex.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-typography-stylex.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/stories/heading.stories.tsx b/packages/swingset/src/stories/heading.stories.tsx index 16e64a381bd..6908733f074 100644 --- a/packages/swingset/src/stories/heading.stories.tsx +++ b/packages/swingset/src/stories/heading.stories.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @emotion/react */ import type { HeadingProps } from '@clerk/ui/mosaic/components/heading'; -import { Heading, headingRecipe } from '@clerk/ui/mosaic/components/heading'; +import { Heading } from '@clerk/ui/mosaic/components/heading'; import type { StoryMeta } from '@/lib/types'; @@ -8,11 +8,29 @@ import type { StoryMeta } from '@/lib/types'; // renders a code footer with its function's source. See `StoryModule.__source`. export { default as __source } from './heading.stories?raw'; +// StyleX has no runtime recipe to derive knobs from, so the variant surface is described +// here to drive the playground + prop table. Keys mirror `HeadingProps`. export const meta: StoryMeta = { group: 'Components', title: 'Heading', - source: 'packages/ui/src/mosaic/components/heading.tsx', - styles: headingRecipe, + source: 'packages/ui/src/mosaic/components/heading/heading.tsx', + styles: { + _variants: { + size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, + intent: { + primary: {}, + primaryForeground: {}, + destructive: {}, + destructiveForeground: {}, + muted: {}, + mutedForeground: {}, + }, + }, + _defaultVariants: { + size: 'base', + intent: 'primary', + }, + }, }; // Story functions accept Record (knob values) and cast to HeadingProps. diff --git a/packages/swingset/src/stories/text.stories.tsx b/packages/swingset/src/stories/text.stories.tsx index ccff156c6aa..f161e132f0f 100644 --- a/packages/swingset/src/stories/text.stories.tsx +++ b/packages/swingset/src/stories/text.stories.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @emotion/react */ import type { TextProps } from '@clerk/ui/mosaic/components/text'; -import { Text, textRecipe } from '@clerk/ui/mosaic/components/text'; +import { Text } from '@clerk/ui/mosaic/components/text'; import type { StoryMeta } from '@/lib/types'; @@ -8,11 +8,29 @@ import type { StoryMeta } from '@/lib/types'; // renders a code footer with its function's source. See `StoryModule.__source`. export { default as __source } from './text.stories?raw'; +// StyleX has no runtime recipe to derive knobs from, so the variant surface is described +// here to drive the playground + prop table. Keys mirror `TextProps`. export const meta: StoryMeta = { group: 'Components', title: 'Text', - source: 'packages/ui/src/mosaic/components/text.tsx', - styles: textRecipe, + source: 'packages/ui/src/mosaic/components/text/text.tsx', + styles: { + _variants: { + size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, + intent: { + primary: {}, + primaryForeground: {}, + destructive: {}, + destructiveForeground: {}, + muted: {}, + mutedForeground: {}, + }, + }, + _defaultVariants: { + size: 'sm', + intent: 'primary', + }, + }, }; // Story functions accept Record (knob values) and cast to TextProps. diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index a2fa84cfc32..b46fe5f866c 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; export const styles = stylex.create({ base: { @@ -18,7 +18,7 @@ export const styles = stylex.create({ cursor: 'pointer', display: 'inline-flex', fontFamily: 'inherit', - fontWeight: typeScaleVars['--cl-text-label-weight'], + fontWeight: fontWeightVars['--cl-font-medium'], justifyContent: 'center', outlineOffset: '2px', transitionDuration: '150ms', @@ -68,14 +68,14 @@ export const styles = stylex.create({ // size — height-driven; padding sets only the inline axis sizeSm: { paddingInline: space['2'], - fontSize: typeScaleVars['--cl-text-label-sm-size'], - lineHeight: typeScaleVars['--cl-text-label-sm-leading'], + fontSize: typeScaleVars['--cl-text-xs-size'], + lineHeight: typeScaleVars['--cl-text-xs-leading'], height: space['7'], }, sizeMd: { paddingInline: space['3'], - fontSize: typeScaleVars['--cl-text-label-size'], - lineHeight: typeScaleVars['--cl-text-label-leading'], + fontSize: typeScaleVars['--cl-text-sm-size'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], height: space['8'], }, diff --git a/packages/ui/src/mosaic/components/heading.tsx b/packages/ui/src/mosaic/components/heading.tsx deleted file mode 100644 index e018da5258f..00000000000 --- a/packages/ui/src/mosaic/components/heading.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { ComponentProps, RenderProp } from '@clerk/headless/utils'; -import { useRender } from '@clerk/headless/utils'; -import React from 'react'; - -import type { RecipeVariantProps } from '../slot-recipe'; -import { defineSlotRecipe, useRecipe } from '../slot-recipe'; -import { useContextProps } from '../utils/context'; - -/** - * Mosaic heading slot recipe. - * Provides variants for size ('xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl') - * and intent ('primary' | 'mutedForeground' | 'destructive'). - */ -export const headingRecipe = defineSlotRecipe(theme => ({ - slot: 'heading', - base: { - fontWeight: theme.font.semibold, - }, - variants: { - size: { - xs: { ...theme.text('xs') }, - sm: { ...theme.text('sm') }, - base: { ...theme.text('base') }, - lg: { ...theme.text('lg') }, - xl: { ...theme.text('xl') }, - '2xl': { ...theme.text('2xl') }, - }, - intent: { - primary: { color: theme.color.primary }, - primaryForeground: { color: theme.color.primaryForeground }, - destructive: { color: theme.color.destructive }, - destructiveForeground: { color: theme.color.destructiveForeground }, - muted: { color: theme.color.muted }, - mutedForeground: { color: theme.color.mutedForeground }, - }, - }, - defaultVariants: { size: 'base', intent: 'primary' }, -})); - -declare module '../registry' { - interface MosaicSlotRegistry { - heading: true; - } -} - -/** Props for the Heading component combining h2 attributes with recipe variants. */ -export type HeadingProps = Omit, 'render'> & { - render?: RenderProp> | React.ReactElement; -} & RecipeVariantProps; - -export const HeadingContext = React.createContext | null>(null); - -/** - * Themeable heading component. - * Renders as an h2 element by default, forwards refs, and supports - * size and intent variants plus Mosaic styling (sx prop and render callback). - */ -export const Heading = React.forwardRef(function MosaicHeading(rawProps, ref) { - const { size, intent, sx, render, ...rest } = useContextProps(rawProps, HeadingContext); - const { root } = useRecipe(headingRecipe, { variants: { size, intent }, sx }); - // useRender only runs for `render` (function or element); Emotion processes `css` - // inside the consumer's own JSX there. The no-render fallback must stay JSX — - // React.createElement bypasses Emotion's factory, leaking css to the DOM. - const element = useRender({ - defaultTagName: 'h2', - render, - ref, - enabled: Boolean(render), - props: { ...root, ...rest }, - }); - if (element) { - return element; - } - return ( -

- ); -}); diff --git a/packages/ui/src/mosaic/components/heading/heading.styles.ts b/packages/ui/src/mosaic/components/heading/heading.styles.ts new file mode 100644 index 00000000000..e2674a5ec0c --- /dev/null +++ b/packages/ui/src/mosaic/components/heading/heading.styles.ts @@ -0,0 +1,9 @@ +import * as stylex from '@stylexjs/stylex'; + +import { fontWeightVars } from '../../tokens.stylex'; + +// Size and intent come from the shared `typography.styles`; a heading only adds +// its weight on top. +export const styles = stylex.create({ + base: { fontWeight: fontWeightVars['--cl-font-semibold'] }, +}); diff --git a/packages/ui/src/mosaic/components/heading/heading.test.tsx b/packages/ui/src/mosaic/components/heading/heading.test.tsx new file mode 100644 index 00000000000..3e7b47d3d3f --- /dev/null +++ b/packages/ui/src/mosaic/components/heading/heading.test.tsx @@ -0,0 +1,71 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { Heading, HeadingContext } from './heading'; + +describe('Mosaic Heading', () => { + it('renders an h2 with its children', () => { + render(Title); + expect(screen.getByRole('heading', { level: 2, name: 'Title' })).toBeInTheDocument(); + }); + + it('applies default variants when none are passed', () => { + render(Title); + const heading = screen.getByRole('heading'); + expect(heading).toHaveClass('cl-heading'); + expect(heading).toHaveAttribute('data-size', 'base'); + expect(heading).toHaveAttribute('data-intent', 'primary'); + }); + + it('wires variant props and consumer className/style through to the element', () => { + render( + + Title + , + ); + const heading = screen.getByRole('heading'); + expect(heading).toHaveAttribute('data-size', '2xl'); + expect(heading).toHaveAttribute('data-intent', 'destructive'); + expect(heading).toHaveClass('cl-heading', 'my-heading'); + expect(heading).toHaveStyle({ marginTop: '8px' }); + }); + + it('renders a different element through the render prop, keeping the slot props', () => { + render(

}>Title); + const heading = screen.getByRole('heading', { level: 3 }); + expect(heading).toHaveClass('cl-heading'); + expect(heading).toHaveAttribute('data-size', 'base'); + }); + + it('reads defaults from HeadingContext, with own props winning', () => { + render( + + Title + , + ); + const heading = screen.getByRole('heading'); + expect(heading).toHaveAttribute('data-size', 'xl'); + expect(heading).toHaveAttribute('data-intent', 'destructive'); + }); + + it('forwards arbitrary props and the ref', () => { + const ref = React.createRef(); + render( + + Title + , + ); + const heading = screen.getByRole('heading'); + expect(ref.current).toBe(heading); + expect(heading).toHaveAttribute('id', 'section-title'); + }); +}); diff --git a/packages/ui/src/mosaic/components/heading/heading.tsx b/packages/ui/src/mosaic/components/heading/heading.tsx new file mode 100644 index 00000000000..52b2733ae99 --- /dev/null +++ b/packages/ui/src/mosaic/components/heading/heading.tsx @@ -0,0 +1,63 @@ +import type { RenderProp } from '@clerk/headless/utils'; +import { useRender } from '@clerk/headless/utils'; +import * as stylex from '@stylexjs/stylex'; +import React from 'react'; + +import { mergeStyleProps, themeProps } from '../../props'; +import { useContextProps } from '../../utils/context'; +import type { TypographyIntent, TypographySize } from '../typography.styles'; +import { intents, sizes } from '../typography.styles'; +import { styles } from './heading.styles'; + +export interface HeadingProps extends React.ComponentPropsWithRef<'h2'> { + size?: TypographySize; + intent?: TypographyIntent; + render?: RenderProp> | React.ReactElement; +} + +export const HeadingContext = React.createContext | null>(null); + +/** + * Themeable heading. Renders an `

` by default, forwards refs, and supports + * `size` and `intent` variants. Pass `render` for a different heading level. + */ +export const Heading = React.forwardRef(function MosaicHeading(rawProps, ref) { + const { + size = 'base', + intent = 'primary', + render, + className, + style, + ...rest + } = useContextProps(rawProps, HeadingContext); + + const props = { + ...mergeStyleProps( + themeProps('heading', { size, intent }), + stylex.props(styles.base, sizes[size], intents[intent]), + className, + style, + ), + ...rest, + }; + + // useRender only runs for `render` (function or element); Emotion processes `css` + // inside the consumer's own JSX there. The no-render fallback must stay JSX — + // React.createElement bypasses Emotion's factory, leaking css to the DOM. + const element = useRender({ + defaultTagName: 'h2', + render, + ref, + enabled: Boolean(render), + props, + }); + if (element) { + return element; + } + return ( +

+ ); +}); diff --git a/packages/ui/src/mosaic/components/heading/index.ts b/packages/ui/src/mosaic/components/heading/index.ts new file mode 100644 index 00000000000..e3e166be302 --- /dev/null +++ b/packages/ui/src/mosaic/components/heading/index.ts @@ -0,0 +1,2 @@ +export { Heading, HeadingContext } from './heading'; +export type { HeadingProps } from './heading'; diff --git a/packages/ui/src/mosaic/components/text.tsx b/packages/ui/src/mosaic/components/text.tsx deleted file mode 100644 index 37477935f42..00000000000 --- a/packages/ui/src/mosaic/components/text.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { ComponentProps, RenderProp } from '@clerk/headless/utils'; -import { useRender } from '@clerk/headless/utils'; -import React from 'react'; - -import type { RecipeVariantProps } from '../slot-recipe'; -import { defineSlotRecipe, useRecipe } from '../slot-recipe'; -import { useContextProps } from '../utils/context'; - -/** - * Mosaic text slot recipe. - * Provides variants for size ('xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl') - * and intent ('primary' | 'mutedForeground' | 'destructive'). - */ -export const textRecipe = defineSlotRecipe(theme => ({ - slot: 'text', - base: {}, - variants: { - size: { - xs: { ...theme.text('xs') }, - sm: { ...theme.text('sm') }, - base: { ...theme.text('base') }, - lg: { ...theme.text('lg') }, - xl: { ...theme.text('xl') }, - '2xl': { ...theme.text('2xl') }, - }, - intent: { - primary: { color: theme.color.primary }, - primaryForeground: { color: theme.color.primaryForeground }, - destructive: { color: theme.color.destructive }, - destructiveForeground: { color: theme.color.destructiveForeground }, - muted: { color: theme.color.muted }, - mutedForeground: { color: theme.color.mutedForeground }, - }, - }, - defaultVariants: { size: 'sm', intent: 'primary' }, -})); - -declare module '../registry' { - interface MosaicSlotRegistry { - text: true; - } -} - -/** Props for the Text component combining p element attributes with recipe variants. */ -export type TextProps = Omit, 'render'> & { - render?: RenderProp> | React.ReactElement; -} & RecipeVariantProps; - -export const TextContext = React.createContext | null>(null); - -/** - * Themeable text component. - * Renders as a p element by default, forwards refs, and supports - * size and intent variants plus Mosaic styling (sx prop and render callback). - */ -export const Text = React.forwardRef(function MosaicText(rawProps, ref) { - const { size, intent, sx, render, ...rest } = useContextProps(rawProps, TextContext); - const { root } = useRecipe(textRecipe, { variants: { size, intent }, sx }); - // useRender only runs for `render` (function or element); Emotion processes `css` - // inside the consumer's own JSX there. The no-render fallback must stay JSX — - // React.createElement bypasses Emotion's factory, leaking css to the DOM. - const element = useRender({ - defaultTagName: 'p', - render, - ref, - enabled: Boolean(render), - props: { ...root, ...rest }, - }); - if (element) { - return element; - } - return ( -

- ); -}); diff --git a/packages/ui/src/mosaic/components/text/index.ts b/packages/ui/src/mosaic/components/text/index.ts new file mode 100644 index 00000000000..9491dcb2a55 --- /dev/null +++ b/packages/ui/src/mosaic/components/text/index.ts @@ -0,0 +1,2 @@ +export { Text, TextContext } from './text'; +export type { TextProps } from './text'; diff --git a/packages/ui/src/mosaic/components/text/text.test.tsx b/packages/ui/src/mosaic/components/text/text.test.tsx new file mode 100644 index 00000000000..cb3596c5948 --- /dev/null +++ b/packages/ui/src/mosaic/components/text/text.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { Text, TextContext } from './text'; + +describe('Mosaic Text', () => { + it('renders a p with its children', () => { + render(Body copy); + const text = screen.getByText('Body copy'); + expect(text.tagName).toBe('P'); + }); + + it('applies default variants when none are passed', () => { + render(Body copy); + const text = screen.getByText('Body copy'); + expect(text).toHaveClass('cl-text'); + expect(text).toHaveAttribute('data-size', 'sm'); + expect(text).toHaveAttribute('data-intent', 'primary'); + }); + + it('wires variant props and consumer className/style through to the element', () => { + render( + + Body copy + , + ); + const text = screen.getByText('Body copy'); + expect(text).toHaveAttribute('data-size', 'lg'); + expect(text).toHaveAttribute('data-intent', 'mutedForeground'); + expect(text).toHaveClass('cl-text', 'my-text'); + expect(text).toHaveStyle({ marginTop: '8px' }); + }); + + it('renders a different element through the render prop, keeping the slot props', () => { + render( }>Body copy); + const text = screen.getByText('Body copy'); + expect(text.tagName).toBe('SPAN'); + expect(text).toHaveClass('cl-text'); + expect(text).toHaveAttribute('data-size', 'sm'); + }); + + it('reads defaults from TextContext, with own props winning', () => { + render( + + Body copy + , + ); + const text = screen.getByText('Body copy'); + expect(text).toHaveAttribute('data-intent', 'mutedForeground'); + expect(text).toHaveAttribute('data-size', 'base'); + }); + + it('forwards arbitrary props and the ref', () => { + const ref = React.createRef(); + render( + + Body copy + , + ); + const text = screen.getByRole('alert'); + expect(ref.current).toBe(text); + }); +}); diff --git a/packages/ui/src/mosaic/components/text/text.tsx b/packages/ui/src/mosaic/components/text/text.tsx new file mode 100644 index 00000000000..6dd19a7c82c --- /dev/null +++ b/packages/ui/src/mosaic/components/text/text.tsx @@ -0,0 +1,55 @@ +import type { RenderProp } from '@clerk/headless/utils'; +import { useRender } from '@clerk/headless/utils'; +import * as stylex from '@stylexjs/stylex'; +import React from 'react'; + +import { mergeStyleProps, themeProps } from '../../props'; +import { useContextProps } from '../../utils/context'; +import type { TypographyIntent, TypographySize } from '../typography.styles'; +import { intents, sizes } from '../typography.styles'; + +export interface TextProps extends React.ComponentPropsWithRef<'p'> { + size?: TypographySize; + intent?: TypographyIntent; + render?: RenderProp> | React.ReactElement; +} + +export const TextContext = React.createContext | null>(null); + +/** + * Themeable body copy. Renders a `

` by default, forwards refs, and supports + * `size` and `intent` variants. Pass `render` for inline copy (``). + */ +export const Text = React.forwardRef(function MosaicText(rawProps, ref) { + const { size = 'sm', intent = 'primary', render, className, style, ...rest } = useContextProps(rawProps, TextContext); + + const props = { + ...mergeStyleProps( + themeProps('text', { size, intent }), + stylex.props(sizes[size], intents[intent]), + className, + style, + ), + ...rest, + }; + + // useRender only runs for `render` (function or element); Emotion processes `css` + // inside the consumer's own JSX there. The no-render fallback must stay JSX — + // React.createElement bypasses Emotion's factory, leaking css to the DOM. + const element = useRender({ + defaultTagName: 'p', + render, + ref, + enabled: Boolean(render), + props, + }); + if (element) { + return element; + } + return ( +

+ ); +}); diff --git a/packages/ui/src/mosaic/components/typography.styles.ts b/packages/ui/src/mosaic/components/typography.styles.ts new file mode 100644 index 00000000000..df24d0d16a6 --- /dev/null +++ b/packages/ui/src/mosaic/components/typography.styles.ts @@ -0,0 +1,53 @@ +import * as stylex from '@stylexjs/stylex'; + +import { colorVars, typeScaleVars } from '../tokens.stylex'; + +// Shared typography surface. `Heading` and `Text` render the same size and color +// axes, so the atoms live here once and each component composes what it needs on +// top (Heading adds a weight; Text adds nothing). + +export type TypographySize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl'; + +export type TypographyIntent = + | 'primary' + | 'primaryForeground' + | 'destructive' + | 'destructiveForeground' + | 'muted' + | 'mutedForeground'; + +export const sizes = stylex.create({ + xs: { + fontSize: typeScaleVars['--cl-text-xs-size'], + lineHeight: typeScaleVars['--cl-text-xs-leading'], + }, + sm: { + fontSize: typeScaleVars['--cl-text-sm-size'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], + }, + base: { + fontSize: typeScaleVars['--cl-text-base-size'], + lineHeight: typeScaleVars['--cl-text-base-leading'], + }, + lg: { + fontSize: typeScaleVars['--cl-text-lg-size'], + lineHeight: typeScaleVars['--cl-text-lg-leading'], + }, + xl: { + fontSize: typeScaleVars['--cl-text-xl-size'], + lineHeight: typeScaleVars['--cl-text-xl-leading'], + }, + '2xl': { + fontSize: typeScaleVars['--cl-text-2xl-size'], + lineHeight: typeScaleVars['--cl-text-2xl-leading'], + }, +}); + +export const intents = stylex.create({ + primary: { color: colorVars['--cl-color-primary'] }, + primaryForeground: { color: colorVars['--cl-color-primary-foreground'] }, + destructive: { color: colorVars['--cl-color-destructive'] }, + destructiveForeground: { color: colorVars['--cl-color-destructive-foreground'] }, + muted: { color: colorVars['--cl-color-muted'] }, + mutedForeground: { color: colorVars['--cl-color-muted-foreground'] }, +}); diff --git a/packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx b/packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx index ee25aad80bf..911e2f00fb4 100644 --- a/packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx +++ b/packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx @@ -108,7 +108,7 @@ function DomainRow({ domain, canManage, onManage, onVerify, onRemove }: DomainRo > } - sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} + style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} > {domain.name} diff --git a/packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx b/packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx index b68fca1efc7..57e8679543c 100644 --- a/packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx +++ b/packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx @@ -162,7 +162,7 @@ export function OrganizationProfileMembersPanelView({ intent='destructive' size='sm' role='alert' - sx={t => ({ marginBlockEnd: t.spacing(3) })} + style={{ marginBlockEnd: 'calc(var(--cl-spacing) * 3)' }} > {snapshot.context.error} @@ -233,7 +233,7 @@ export function OrganizationProfileMembersPanelView({ ({ display: 'flex', alignItems: 'center', gap: t.spacing(1.5) })}> ({ fontWeight: t.font.medium })} + style={{ fontWeight: 500 }} > {row.name} diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index f1716fae927..853cded8be2 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -6,15 +6,20 @@ export { Button } from '../components/button'; export type { ButtonProps } from '../components/button'; +export { Heading, HeadingContext } from '../components/heading'; +export type { HeadingProps } from '../components/heading'; +export { Text, TextContext } from '../components/text'; +export type { TextProps } from '../components/text'; -import { colorVars, radiusVars, space, spacingVars, typeScaleVars } from '../tokens.stylex'; +import { colorVars, fontWeightVars, radiusVars, space, spacingVars, typeScaleVars } from '../tokens.stylex'; -export { colorVars, radiusVars, space, spacingVars, typeScaleVars }; +export { colorVars, fontWeightVars, radiusVars, space, spacingVars, typeScaleVars }; // Derived here, not in `tokens.stylex.ts`: `@stylexjs/enforce-extension` requires a // `.stylex.ts` file to export nothing but its `defineVars` results. The vars are keyed // by the same `--cl-*` names, so `keyof typeof …Vars` reproduces each token union. export type ColorVarName = keyof typeof colorVars; +export type FontWeightVarName = keyof typeof fontWeightVars; export type RadiusVarName = keyof typeof radiusVars; export type SpacingVarName = keyof typeof spacingVars; export type TypeScaleVarName = keyof typeof typeScaleVars; diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 859d98ae9ac..054e5757ab2 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -92,17 +92,39 @@ export const space = stylex.defineConsts({ // ============================================================================= // Typography Tokens — type scale // ============================================================================= -// Size + weight + leading grouped per semantic slot, mirroring astryx's -// `typeScaleVars`. `defineVars` values are single CSS values, so each facet is -// its own var. +// One named step scale (`xs` → `2xl`) that every text-bearing component sizes +// against — Heading and Text expose it as their `size` variant, Button picks the +// steps its heights allow. `defineVars` values are single CSS values, so size and +// leading are each their own var. const typeScaleDefaults = { - '--cl-text-label-size': '0.875rem', - '--cl-text-label-weight': '500', - '--cl-text-label-leading': 'calc(1.25 / 0.875)', - '--cl-text-label-sm-size': '0.75rem', - '--cl-text-label-sm-weight': '500', - '--cl-text-label-sm-leading': 'calc(1 / 0.75)', + '--cl-text-xs-size': '0.75rem', + '--cl-text-xs-leading': 'calc(1 / 0.75)', + '--cl-text-sm-size': '0.875rem', + '--cl-text-sm-leading': 'calc(1.25 / 0.875)', + '--cl-text-base-size': '1rem', + '--cl-text-base-leading': 'calc(1.5 / 1)', + '--cl-text-lg-size': '1.125rem', + '--cl-text-lg-leading': 'calc(1.75 / 1.125)', + '--cl-text-xl-size': '1.25rem', + '--cl-text-xl-leading': 'calc(1.75 / 1.25)', + '--cl-text-2xl-size': '1.5rem', + '--cl-text-2xl-leading': 'calc(2 / 1.5)', } as const; export const typeScaleVars = stylex.defineVars(typeScaleDefaults); + +// ============================================================================= +// Typography Tokens — font weight +// ============================================================================= +// Named weights, kept separate from the step scale: weight and size vary +// independently (a `sm` Text can be medium; a `2xl` Heading is semibold). + +const fontWeightDefaults = { + '--cl-font-normal': '400', + '--cl-font-medium': '500', + '--cl-font-semibold': '600', + '--cl-font-bold': '700', +} as const; + +export const fontWeightVars = stylex.defineVars(fontWeightDefaults); diff --git a/packages/ui/tsdown.mosaic.config.mts b/packages/ui/tsdown.mosaic.config.mts index 6241344067b..b9963fe5096 100644 --- a/packages/ui/tsdown.mosaic.config.mts +++ b/packages/ui/tsdown.mosaic.config.mts @@ -37,7 +37,10 @@ export default defineConfig({ minify: false, // Use the standard React JSX runtime, not Emotion's — the Mosaic build must be Emotion-free. tsconfig: './tsconfig.mosaic.json', - external: ['react', 'react-dom', '@stylexjs/stylex'], + // `@clerk/headless` stays external here (the main build inlines it): this entry exists to + // extract `styles.css`, and only that file is exported from the package — so there is nothing + // to gain from pulling the primitives and their deps into a bundle nobody imports. + external: ['react', 'react-dom', '@stylexjs/stylex', /^@clerk\/headless/], plugins: [ stylexPlugin({ fileName: 'styles.css', From 03043ed8f174735ec35869f62629f746c35cc47e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 24 Jul 2026 16:03:39 -0400 Subject: [PATCH 2/4] feat(swingset): key the prop table's escape-hatch row to the styling engine `PropTable` appended an `sx` row unconditionally, but StyleX components have no `sx` -- they merge the consumer's `className`/`style` after their own atoms. `meta.styleEngine` now picks the row set, and Button, Heading, and Text declare `'stylex'`. --- packages/swingset/CLAUDE.md | 6 ++++-- packages/swingset/src/components/PropTable.tsx | 10 ++++++++-- packages/swingset/src/lib/types.ts | 6 ++++++ packages/swingset/src/stories/button.stories.tsx | 1 + packages/swingset/src/stories/heading.stories.tsx | 1 + packages/swingset/src/stories/text.stories.tsx | 1 + 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/swingset/CLAUDE.md b/packages/swingset/CLAUDE.md index 88a57bc4414..bc07c71686b 100644 --- a/packages/swingset/CLAUDE.md +++ b/packages/swingset/CLAUDE.md @@ -32,7 +32,7 @@ These require reading several files together; the `README.md` covers the step-by - **Every story renders inside `MosaicProvider`.** `StoryPreview` (the MDX ``) renders a named story with the playground's knob values as props, applies the variable overrides, and exposes a Reset button plus a collapsible `VariablesPanel` attached to the preview. `StoryEmbed` (the MDX ``) renders a single static variation with default knob values and no controls. -- **The prop table is the knob surface.** `PropTable` (MDX ``) derives rows from `meta.styles._variants`/`_defaultVariants` (always appends `sx`); each variant row renders a `KnobControl` in its **Value** column, seeded with the prop's default and bound to the playground context. Non-variant rows (`sx`, `extra`) stay static. +- **The prop table is the knob surface.** `PropTable` (MDX ``) derives rows from `meta.styles._variants`/`_defaultVariants`, then appends the escape-hatch rows for the component's styling engine (`meta.styleEngine`): Emotion components get `sx`, StyleX components get `className` + `style`. Each variant row renders a `KnobControl` in its **Value** column, seeded with the prop's default and bound to the playground context. The engine rows and `extra` stay static. - **Variables live in the preview.** The `VariablesPanel` is a collapsible attached to `StoryPreview` (toggled from the preview's header), bound to the shared playground context so editing a Mosaic token override immediately re-themes the story rendered above it. @@ -79,12 +79,14 @@ export const meta: StoryMeta = { title: 'Button', // drives slug + the page

label: 'Delete Org', // optional friendlier sidebar text source: 'packages/ui/src/mosaic/components/button.tsx', // repo-root path → "View source" + styleEngine: 'stylex', // set on migrated components; defaults to 'emotion' styles: buttonRecipe, // CVA recipe — archetype A · simple only }; ``` - `title` is the component's export name; it produces the slug and is what readers match against code. Set `label` only when the sidebar should read differently (the slug and page heading still come from `title`). - `source` is always a path **relative to the monorepo root**, pointing at the file that exports the documented component. Always set it — it powers the "View source" link. +- `styleEngine` names the styling engine behind the component. It only affects which escape-hatch row the `` appends (`sx` vs `className` + `style`), so it matters for archetype A. Set `'stylex'` on migrated components; leave it off for Emotion ones. - `styles` is the component's CVA recipe/style object and is **required for archetype A's simple (knob-driven) form** (it generates the knobs and the ``). Omit it for compound A components, and for B and C. Story files that render styled Mosaic components must start with the Emotion pragma `/** @jsxImportSource @emotion/react */`. Headless-primitive demos render raw and don't need it. Always import the component and its recipe explicitly — never `import *`. @@ -155,7 +157,7 @@ import * as ButtonStories from './button.stories'; - **Playground / Props / Usage are mandatory and always in this order.** The three share one playground state: editing a row in `` re-renders `` above it and regenerates the `` snippet below it. - The story file exports a primary demo (rendered by ``) plus one named export per variation under **Examples**. Each story takes `props: Record` and casts through a local `knobsAsProps` helper — knobs are dynamically typed, the component isn't. -- Use ``'s `extra` for documenting non-variant props; `sx` is appended for you. +- Use ``'s `extra` for documenting non-variant props; the styling escape hatch is appended for you (`sx`, or `className` + `style` when `meta.styleEngine` is `'stylex'`). - Use `` to pin static, non-knob props in the generated snippet. - `` renders `Prop | Type | Default | Value`: the **Default** column is filled automatically from the recipe's `_defaultVariants`, and the **Value** column is the live knob seeded with that default. No manual default annotation is needed; see _Document the default value_ under Archetype B. diff --git a/packages/swingset/src/components/PropTable.tsx b/packages/swingset/src/components/PropTable.tsx index dedf91462ba..066c40e1d6f 100644 --- a/packages/swingset/src/components/PropTable.tsx +++ b/packages/swingset/src/components/PropTable.tsx @@ -17,7 +17,13 @@ interface PropTableProps { extra?: ExtraProp[]; } +// Engine-specific escape hatches. StyleX components have no `sx`; they merge the +// consumer's `className`/`style` after their own atoms instead. const SX_ROW: ExtraProp = { name: 'sx', type: 'StyleRule | (theme) => StyleRule' }; +const STYLEX_ROWS: ExtraProp[] = [ + { name: 'className', type: 'string' }, + { name: 'style', type: 'CSSProperties' }, +]; export function PropTable({ meta, extra = [] }: PropTableProps) { const playground = usePlayground(); @@ -35,7 +41,7 @@ export function PropTable({ meta, extra = [] }: PropTableProps) { return { name, type, default: defDisplay }; }), ...extra, - SX_ROW, + ...(meta.styleEngine === 'stylex' ? STYLEX_ROWS : [SX_ROW]), ]; return ( @@ -51,7 +57,7 @@ export function PropTable({ meta, extra = [] }: PropTableProps) { {rows.map(row => { // The default is a static cell; the Value column is the live control. Variant - // props get a knob there; non-variant rows (sx, extra) have no control. + // props get a knob there; non-variant rows (the engine rows, extra) have no control. const knob = playground?.knobs[row.name]; return ( diff --git a/packages/swingset/src/lib/types.ts b/packages/swingset/src/lib/types.ts index 4ff6cb8a6df..de1faedc8ad 100644 --- a/packages/swingset/src/lib/types.ts +++ b/packages/swingset/src/lib/types.ts @@ -49,6 +49,12 @@ export interface StoryMeta { * source" link to the file on GitHub. See `lib/source.ts`. */ source?: string; + /** + * Which styling engine the documented component is built on. Drives the prop rows that + * are engine-specific: Emotion components take `sx`, StyleX components take `className` + * and `style`. Defaults to `'emotion'` — set `'stylex'` once a component is migrated. + */ + styleEngine?: 'emotion' | 'stylex'; styles?: { _variants: Record>; _defaultVariants?: Record; diff --git a/packages/swingset/src/stories/button.stories.tsx b/packages/swingset/src/stories/button.stories.tsx index f982c17cbff..d138673a8f7 100644 --- a/packages/swingset/src/stories/button.stories.tsx +++ b/packages/swingset/src/stories/button.stories.tsx @@ -14,6 +14,7 @@ export const meta: StoryMeta = { group: 'Components', title: 'Button', source: 'packages/ui/src/mosaic/components/button/button.tsx', + styleEngine: 'stylex', styles: { _variants: { intent: { primary: {}, destructive: {} }, diff --git a/packages/swingset/src/stories/heading.stories.tsx b/packages/swingset/src/stories/heading.stories.tsx index 6908733f074..d8563769f12 100644 --- a/packages/swingset/src/stories/heading.stories.tsx +++ b/packages/swingset/src/stories/heading.stories.tsx @@ -14,6 +14,7 @@ export const meta: StoryMeta = { group: 'Components', title: 'Heading', source: 'packages/ui/src/mosaic/components/heading/heading.tsx', + styleEngine: 'stylex', styles: { _variants: { size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, diff --git a/packages/swingset/src/stories/text.stories.tsx b/packages/swingset/src/stories/text.stories.tsx index f161e132f0f..a1dc3c7b1cc 100644 --- a/packages/swingset/src/stories/text.stories.tsx +++ b/packages/swingset/src/stories/text.stories.tsx @@ -14,6 +14,7 @@ export const meta: StoryMeta = { group: 'Components', title: 'Text', source: 'packages/ui/src/mosaic/components/text/text.tsx', + styleEngine: 'stylex', styles: { _variants: { size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, From 9c017006af967ff6869d7e7482b048631af24df4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 24 Jul 2026 16:07:42 -0400 Subject: [PATCH 3/4] docs(repo): forbid Testing sections in PR descriptions --- .claude/skills/clerk-monorepo/SKILL.md | 4 +++- AGENTS.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/skills/clerk-monorepo/SKILL.md b/.claude/skills/clerk-monorepo/SKILL.md index 6868bb72cbf..e94e35f3480 100644 --- a/.claude/skills/clerk-monorepo/SKILL.md +++ b/.claude/skills/clerk-monorepo/SKILL.md @@ -157,7 +157,9 @@ Each rule below restates `AGENTS.md`; the parenthetical is how it is enforced. 4. Verify locally: `pnpm build`, `pnpm test` (or the filtered forms above), `pnpm lint`, `pnpm format:check`. 5. Open the PR; the title must be a valid conventional commit (it becomes the squash commit). Fill in - the PR template. + the PR template and add nothing beyond its sections — in particular, never write a "Testing" / + "Test plan" / "How to test" section listing the tests added or the checks run. The Checklist + covers that and reviewers read the diff. Describe the change, not the work done on it. Release _policy_ (when/how things ship, canary, snapshot, backports) is in `docs/PUBLISH.md`. This skill stops at opening the PR. diff --git a/AGENTS.md b/AGENTS.md index 65565c4662b..99e9b9bfbbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ Clerk's JavaScript SDK and library monorepo. - Use `pnpm` only. `npm` and `yarn` are blocked by `preinstall`. Node `>=24.15`, pnpm `>=10.33`. - Every PR needs a changeset. `pnpm changeset` for package changes, `pnpm changeset:empty` for tooling/repo-only. Empty changesets are two `---` delimiters with no body. A changeset is a changelog entry for users upgrading the package, not a summary of the work done in the PR. Describe the user-facing change (what changed for someone consuming the library and how it affects them) rather than the implementation details of the diff. If a change has no user-facing impact, use an empty changeset. - Commits must be conventional: `type(scope):` (commitlint enforces, on the PR title). `scope` is the package name without `@clerk/`, or `repo` / `release` / `e2e` / `ci` / `*`. `clerk-js` uses scope `js` (`clerk-js` is also accepted). Scope is mandatory; `docs` is a type, not a scope. +- PR descriptions follow `.github/PULL_REQUEST_TEMPLATE.md` and add no sections of their own. Never add a "Testing" (or "Test plan" / "How to test") section summarizing the tests written or the checks run; the Checklist covers that and reviewers read the diff. Describe the change, not the work done on it. - Keep code comments minimal. Do not add a comment unless it is critical to explain WHY a non-obvious change was made; never restate what the code does. When one is warranted, keep it to a single terse line, not a verbose multi-line block. ## References From d97f67cae58659e806d3884163d4cf4c8c3ab5da Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 24 Jul 2026 16:19:52 -0400 Subject: [PATCH 4/4] refactor(repo): drop redundant comments from the StyleX migration --- packages/swingset/src/components/PropTable.tsx | 2 -- packages/swingset/src/stories/button.stories.tsx | 2 -- packages/swingset/src/stories/heading.stories.tsx | 2 -- packages/swingset/src/stories/text.stories.tsx | 2 -- .../ui/src/mosaic/components/heading/heading.styles.ts | 2 -- packages/ui/src/mosaic/components/typography.styles.ts | 4 ---- packages/ui/src/mosaic/tokens.stylex.ts | 9 +++------ 7 files changed, 3 insertions(+), 20 deletions(-) diff --git a/packages/swingset/src/components/PropTable.tsx b/packages/swingset/src/components/PropTable.tsx index 066c40e1d6f..b7084751802 100644 --- a/packages/swingset/src/components/PropTable.tsx +++ b/packages/swingset/src/components/PropTable.tsx @@ -17,8 +17,6 @@ interface PropTableProps { extra?: ExtraProp[]; } -// Engine-specific escape hatches. StyleX components have no `sx`; they merge the -// consumer's `className`/`style` after their own atoms instead. const SX_ROW: ExtraProp = { name: 'sx', type: 'StyleRule | (theme) => StyleRule' }; const STYLEX_ROWS: ExtraProp[] = [ { name: 'className', type: 'string' }, diff --git a/packages/swingset/src/stories/button.stories.tsx b/packages/swingset/src/stories/button.stories.tsx index d138673a8f7..8a347b791ec 100644 --- a/packages/swingset/src/stories/button.stories.tsx +++ b/packages/swingset/src/stories/button.stories.tsx @@ -8,8 +8,6 @@ import type { StoryMeta } from '@/lib/types'; // renders a code footer with its function's source. See `StoryModule.__source`. export { default as __source } from './button.stories?raw'; -// StyleX has no runtime recipe to derive knobs from, so the variant surface is described -// here to drive the playground + prop table. Keys mirror `ButtonProps`. export const meta: StoryMeta = { group: 'Components', title: 'Button', diff --git a/packages/swingset/src/stories/heading.stories.tsx b/packages/swingset/src/stories/heading.stories.tsx index d8563769f12..dfbd2cd4862 100644 --- a/packages/swingset/src/stories/heading.stories.tsx +++ b/packages/swingset/src/stories/heading.stories.tsx @@ -8,8 +8,6 @@ import type { StoryMeta } from '@/lib/types'; // renders a code footer with its function's source. See `StoryModule.__source`. export { default as __source } from './heading.stories?raw'; -// StyleX has no runtime recipe to derive knobs from, so the variant surface is described -// here to drive the playground + prop table. Keys mirror `HeadingProps`. export const meta: StoryMeta = { group: 'Components', title: 'Heading', diff --git a/packages/swingset/src/stories/text.stories.tsx b/packages/swingset/src/stories/text.stories.tsx index a1dc3c7b1cc..0964a38a5fd 100644 --- a/packages/swingset/src/stories/text.stories.tsx +++ b/packages/swingset/src/stories/text.stories.tsx @@ -8,8 +8,6 @@ import type { StoryMeta } from '@/lib/types'; // renders a code footer with its function's source. See `StoryModule.__source`. export { default as __source } from './text.stories?raw'; -// StyleX has no runtime recipe to derive knobs from, so the variant surface is described -// here to drive the playground + prop table. Keys mirror `TextProps`. export const meta: StoryMeta = { group: 'Components', title: 'Text', diff --git a/packages/ui/src/mosaic/components/heading/heading.styles.ts b/packages/ui/src/mosaic/components/heading/heading.styles.ts index e2674a5ec0c..032ba778fd1 100644 --- a/packages/ui/src/mosaic/components/heading/heading.styles.ts +++ b/packages/ui/src/mosaic/components/heading/heading.styles.ts @@ -2,8 +2,6 @@ import * as stylex from '@stylexjs/stylex'; import { fontWeightVars } from '../../tokens.stylex'; -// Size and intent come from the shared `typography.styles`; a heading only adds -// its weight on top. export const styles = stylex.create({ base: { fontWeight: fontWeightVars['--cl-font-semibold'] }, }); diff --git a/packages/ui/src/mosaic/components/typography.styles.ts b/packages/ui/src/mosaic/components/typography.styles.ts index df24d0d16a6..7e27bdf8ac1 100644 --- a/packages/ui/src/mosaic/components/typography.styles.ts +++ b/packages/ui/src/mosaic/components/typography.styles.ts @@ -2,10 +2,6 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, typeScaleVars } from '../tokens.stylex'; -// Shared typography surface. `Heading` and `Text` render the same size and color -// axes, so the atoms live here once and each component composes what it needs on -// top (Heading adds a weight; Text adds nothing). - export type TypographySize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl'; export type TypographyIntent = diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 054e5757ab2..54c9de9ef9f 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -92,10 +92,8 @@ export const space = stylex.defineConsts({ // ============================================================================= // Typography Tokens — type scale // ============================================================================= -// One named step scale (`xs` → `2xl`) that every text-bearing component sizes -// against — Heading and Text expose it as their `size` variant, Button picks the -// steps its heights allow. `defineVars` values are single CSS values, so size and -// leading are each their own var. +// One named step scale every text-bearing component sizes against. `defineVars` +// values are single CSS values, so size and leading are each their own var. const typeScaleDefaults = { '--cl-text-xs-size': '0.75rem', @@ -117,8 +115,7 @@ export const typeScaleVars = stylex.defineVars(typeScaleDefaults); // ============================================================================= // Typography Tokens — font weight // ============================================================================= -// Named weights, kept separate from the step scale: weight and size vary -// independently (a `sm` Text can be medium; a `2xl` Heading is semibold). +// Kept separate from the step scale: weight and size vary independently. const fontWeightDefaults = { '--cl-font-normal': '400',