From ae6a5aeb621fcb61a39c7ad17d6982c8da8b61d9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 28 Jul 2026 15:38:52 -0400 Subject: [PATCH] refactor(ui): share one base props type across Mosaic components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`, 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. --- .changeset/mosaic-component-props.md | 2 ++ .../ui/src/mosaic/components/badge/badge.tsx | 6 +++--- .../ui/src/mosaic/components/heading/heading.tsx | 6 ++---- packages/ui/src/mosaic/components/text/text.tsx | 6 ++---- packages/ui/src/mosaic/props.ts | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 .changeset/mosaic-component-props.md diff --git a/.changeset/mosaic-component-props.md b/.changeset/mosaic-component-props.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-component-props.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/ui/src/mosaic/components/badge/badge.tsx b/packages/ui/src/mosaic/components/badge/badge.tsx index fced1156e66..b689c778db2 100644 --- a/packages/ui/src/mosaic/components/badge/badge.tsx +++ b/packages/ui/src/mosaic/components/badge/badge.tsx @@ -1,13 +1,13 @@ -import { type ComponentProps, type RenderProp, useRender } from '@clerk/headless/utils'; +import { useRender } from '@clerk/headless/utils'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; +import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { colors, styles } from './badge.styles'; -export type BadgeProps = Omit, 'render'> & { +export type BadgeProps = MosaicComponentProps<'span'> & { color?: 'primary' | 'neutral' | 'warning' | 'negative' | 'positive'; - render?: RenderProp> | React.ReactElement; }; /** diff --git a/packages/ui/src/mosaic/components/heading/heading.tsx b/packages/ui/src/mosaic/components/heading/heading.tsx index 5548b87636a..dfb97a4d299 100644 --- a/packages/ui/src/mosaic/components/heading/heading.tsx +++ b/packages/ui/src/mosaic/components/heading/heading.tsx @@ -1,19 +1,17 @@ -import type { RenderPropOrElement } from '@clerk/headless/utils'; import { useRender } from '@clerk/headless/utils'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; +import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { useContextProps } from '../../utils/context'; import type { TypographyColor, TypographySize } from '../typography.styles'; import { colors, sizes } from '../typography.styles'; import { styles } from './heading.styles'; -// `color` replaces the legacy HTML `color` attribute, whose `string` type would widen the variant. -export interface HeadingProps extends Omit, 'color'> { +export interface HeadingProps extends MosaicComponentProps<'h2'> { size?: TypographySize; color?: TypographyColor; - render?: RenderPropOrElement<'h2'>; } export const HeadingContext = React.createContext | null>(null); diff --git a/packages/ui/src/mosaic/components/text/text.tsx b/packages/ui/src/mosaic/components/text/text.tsx index 516d3a4a53b..e43bb7204ec 100644 --- a/packages/ui/src/mosaic/components/text/text.tsx +++ b/packages/ui/src/mosaic/components/text/text.tsx @@ -1,18 +1,16 @@ -import type { RenderPropOrElement } from '@clerk/headless/utils'; import { useRender } from '@clerk/headless/utils'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; +import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { useContextProps } from '../../utils/context'; import type { TypographyColor, TypographySize } from '../typography.styles'; import { colors, sizes } from '../typography.styles'; -// `color` replaces the legacy HTML `color` attribute, whose `string` type would widen the variant. -export interface TextProps extends Omit, 'color'> { +export interface TextProps extends MosaicComponentProps<'p'> { size?: TypographySize; color?: TypographyColor; - render?: RenderPropOrElement<'p'>; } export const TextContext = React.createContext | null>(null); diff --git a/packages/ui/src/mosaic/props.ts b/packages/ui/src/mosaic/props.ts index 16000abdb05..ff31214b8bb 100644 --- a/packages/ui/src/mosaic/props.ts +++ b/packages/ui/src/mosaic/props.ts @@ -1,5 +1,21 @@ +import type { RenderPropOrElement } from '@clerk/headless/utils'; import type React from 'react'; +/** + * The base props every Mosaic component accepts: the native props for its default + * tag, plus the `render` escape hatch that swaps the rendered element. + * + * `color` is omitted because it is a non-standard HTML attribute typed `string`, + * which would widen any component that exposes `color` as a variant. Omitting it + * here rather than per component means a new component inherits the narrowing. + */ +export type MosaicComponentProps = Omit< + React.ComponentPropsWithRef, + 'color' +> & { + render?: RenderPropOrElement; +}; + // The public styling contract, emitted onto a component's root element: // 1. `--cl-*` vars — from `tokens.stylex.ts` (`:root { --cl-color-primary: … }`) // 2. `.cl-` class — from `themeProps` (`.cl-button { … }`)