Skip to content

refactor(ui): share one base props type across Mosaic components - #9267

Open
alexcarpenter wants to merge 14 commits into
mainfrom
carp/mosaic-component-props
Open

refactor(ui): share one base props type across Mosaic components#9267
alexcarpenter wants to merge 14 commits into
mainfrom
carp/mosaic-component-props

Conversation

@alexcarpenter

Copy link
Copy Markdown
Member

Description

Heading, Text and Badge each accept render and each expose color as a variant, but all three derived their props differently: two base types (React.ComponentPropsWithRef vs headless ComponentProps), two different things omitted (color vs render), the render union restated inline in one and imported in the other, and two mechanisms for narrowing colorOmit then redeclare in the interfaces, silent intersection in the type alias.

This introduces one shared base in mosaic/props.ts, mirroring Base UI's BaseUIComponentProps:

export type MosaicComponentProps<Tag extends keyof React.JSX.IntrinsicElements> =
  Omit<React.ComponentPropsWithRef<Tag>, 'color'> & {
    render?: RenderPropOrElement<Tag>;
  };

color is omitted once rather than per component, so the next component to expose it as a variant inherits the narrowing instead of rediscovering the collision with the non-standard HTML color attribute. Badge also stops omitting render only to re-add it.

Type-only — all three components resolve to the same props as before, so no changeset body.

Stacked on carp/mosaic-typography-stylex (#9244); merge that first.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@changeset-bot

changeset-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a934303

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 28, 2026 8:02pm
swingset Ready Ready Preview, Comment Jul 28, 2026 8:02pm

Request Review

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`.
…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'`.
Heading and Text take `color` (primary | neutral | warning | negative |
positive) instead of `intent`, matching Badge and the palette tokens, and
emit `data-color`. Badge moves onto the shared type scale now that the
`--cl-text-label-*` tokens are gone.
`useRender` has always cloned a React element passed to `render`, and its own
doc comment advertises `render={<Link/>}`. The public `ComponentProps["render"]`
type only allowed the function form, so the element form was unreachable from
any primitive's props.

The two forms are not interchangeable: a render function is typed to receive the
tag's props verbatim, so it cannot render a component whose props diverge from
those of the tag. Widen the prop to the `RenderPropOrElement<Tag>` union the hook
already implements, and export it for consumers restating it inline.
`Heading` and `Text` type `color` as a variant, replacing the legacy HTML
`color` attribute. A render function receives the tag's props verbatim, so
spreading them into either component required destructuring `color` back out at
every call site to drop a value Dialog never set — it was only present because
`DialogTitleProps` extends `ComponentProps<"h2">`.

The element form carries no such constraint, and was already the documented way
to swap a Mosaic component's host element.
The StyleX migration left this weight as a literal `500`, since `Text` no longer
accepts Emotion's `sx` and that is where `t.font.medium` came from. Read the
migrated token instead, so a theme overriding `--cl-font-medium` still applies.
`appearance.icons` clones whatever element it is given; nothing requires an
`svg`. The Icon tests override with a `span`.
Heading, Text and Badge each derived their props differently: two base types
(`React.ComponentPropsWithRef` vs headless `ComponentProps`), two things omitted
(`color` vs `render`), the render union restated inline in one and imported in
the other, and two ways of narrowing `color` — `Omit` then redeclare in the
interfaces, silent intersection in the type alias.

Collapse all of it into `MosaicComponentProps<Tag>`, mirroring Base UI's
`BaseUIComponentProps`. `color` is omitted once, so the next component to expose
it as a variant inherits the narrowing instead of rediscovering it.

Type-only: all three components resolve to the same props as before.
Base automatically changed from carp/mosaic-typography-stylex to main July 28, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants