From 2c599896e57e7f31c11d0610c11f08b7693fb9a3 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 3 Nov 2026 12:52:39 -0500 Subject: [PATCH 1/3] refactor(mosaic): set card alignment on Card.Root instead of Card.Header --- .changeset/mosaic-card-root-align.md | 2 + .../src/components/card/card.browser.test.tsx | 5 ++- .../mosaic/src/components/card/card.test.tsx | 10 ++--- packages/mosaic/src/components/card/card.tsx | 41 +++++++++++-------- packages/mosaic/src/components/card/index.ts | 2 +- .../swingset/src/stories/card.component.mdx | 2 +- .../src/stories/card.component.stories.tsx | 4 +- packages/swingset/src/stories/sign-in.mdx | 2 +- .../swingset/src/stories/sign-in.stories.tsx | 4 +- 9 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 .changeset/mosaic-card-root-align.md diff --git a/.changeset/mosaic-card-root-align.md b/.changeset/mosaic-card-root-align.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-card-root-align.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/components/card/card.browser.test.tsx b/packages/mosaic/src/components/card/card.browser.test.tsx index bf420f9bff9..d67ae5da380 100644 --- a/packages/mosaic/src/components/card/card.browser.test.tsx +++ b/packages/mosaic/src/components/card/card.browser.test.tsx @@ -30,14 +30,15 @@ describe('Card.Media', () => { }); }); -describe('Card.Header align', () => { +describe('Card.Root align', () => { function CenteredCard() { return ( - + { expect(screen.queryByRole('link', { name: 'Clerk' })).toBeNull(); }); - it('reflects the header alignment, starting by default', () => { + it('reflects the alignment on the root, starting by default', () => { render( - - - + + - , + , ); expect(screen.getByTestId('default')).toHaveAttribute('data-align', 'start'); diff --git a/packages/mosaic/src/components/card/card.tsx b/packages/mosaic/src/components/card/card.tsx index 74318bd4ce7..4645a38d8ee 100644 --- a/packages/mosaic/src/components/card/card.tsx +++ b/packages/mosaic/src/components/card/card.tsx @@ -24,13 +24,17 @@ type CardElevation = 'card' | 'flush' | 'overlay'; export type CardSize = keyof typeof slots.sizes; -export type CardHeaderAlign = keyof typeof slots.headerAlign; +export type CardAlign = keyof typeof slots.headerAlign; const DEFAULT_ELEVATION: CardElevation = 'card'; // The enclosing card and the dialog it was rendered in. Context crosses portals, so a card in a // dialog opened from inside another card is still the dialog's outermost one. -const CardContext = React.createContext<{ elevation: CardElevation; dialog: DialogContextValue | null } | null>(null); +const CardContext = React.createContext<{ + elevation: CardElevation; + align: CardAlign; + dialog: DialogContextValue | null; +} | null>(null); function CardBranding() { return ( @@ -45,6 +49,8 @@ export interface CardProps extends MosaicComponentProps<'div'> { elevation?: CardElevation; /** The card's width. `lg` is for forms that need the room, like a tag input. @default 'md' */ size?: CardSize; + /** Aligns the content of every `Card.Header` in the card. @default 'start' */ + align?: CardAlign; /** * Signs the foot of the card with "Secured by Clerk". An instance that has paid the branding off * carries none of it, so a connected surface passes `displayConfig.branded` here. @@ -55,20 +61,29 @@ export interface CardProps extends MosaicComponentProps<'div'> { } const Root = React.forwardRef(function CardRoot( - { elevation = DEFAULT_ELEVATION, size = 'md', renderBranding = true, render, xstyle, children, ...rest }, + { + elevation = DEFAULT_ELEVATION, + size = 'md', + align = 'start', + renderBranding = true, + render, + xstyle, + children, + ...rest + }, ref, ) { const dialog = React.useContext(DialogContext); const enclosing = React.useContext(CardContext); const isSurface = (enclosing === null || enclosing.dialog !== dialog) && elevation !== 'flush'; - const context = React.useMemo(() => ({ elevation, dialog }), [elevation, dialog]); + const context = React.useMemo(() => ({ elevation, align, dialog }), [elevation, align, dialog]); const element = useRender({ defaultTagName: 'div', render, ref, props: { ...mergeStyleProps( - themeProps('card-root', { elevation, size }), + themeProps('card-root', { elevation, size, align }), stylex.props( reset.base, slots.root.base, @@ -115,15 +130,11 @@ function HeaderCloseButton() { ); } -export interface CardHeaderProps extends MosaicComponentProps<'div'> { - /** Aligns the header's content. @default 'start' */ - align?: CardHeaderAlign; -} - -const Header = React.forwardRef(function CardHeader( - { align = 'start', render, xstyle, children, ...rest }, +const Header = React.forwardRef>(function CardHeader( + { render, xstyle, children, ...rest }, ref, ) { + const align = React.useContext(CardContext)?.align ?? 'start'; const dialog = React.useContext(DialogContext); const hasCloseButton = isInDialog(dialog) && dialog.showCloseButton; return useRender({ @@ -131,11 +142,7 @@ const Header = React.forwardRef(function CardHe render, ref, props: { - ...mergeStyleProps( - themeProps('card-header', { align }), - stylex.props(reset.base, slots.header.base, xstyle), - rest, - ), + ...mergeStyleProps(themeProps('card-header'), stylex.props(reset.base, slots.header.base, xstyle), rest), children: ( <> {/* First in the DOM, so it is the first tabbable element and takes the dialog's opening diff --git a/packages/mosaic/src/components/card/index.ts b/packages/mosaic/src/components/card/index.ts index e68e9b40726..bbb91c01e4b 100644 --- a/packages/mosaic/src/components/card/index.ts +++ b/packages/mosaic/src/components/card/index.ts @@ -1,2 +1,2 @@ export { Card } from './card'; -export type { CardBannerProps, CardHeaderAlign, CardHeaderProps, CardProps, CardSize } from './card'; +export type { CardAlign, CardBannerProps, CardProps, CardSize } from './card'; diff --git a/packages/swingset/src/stories/card.component.mdx b/packages/swingset/src/stories/card.component.mdx index 56e7846a96b..92cd008ace6 100644 --- a/packages/swingset/src/stories/card.component.mdx +++ b/packages/swingset/src/stories/card.component.mdx @@ -31,7 +31,7 @@ import { Card } from '@clerk/mosaic/components/card'; `Card.Media` goes first in `Card.Header`, above the title, for a logo, icon or image. It leaves 24px below it. -`Card.Header` takes `align='center'` to center its content. +`Card.Root` takes `align='center'` to center the content of every `Card.Header` in the card. `Card.Banner` sits between the header and the content and is always rendered. While its children hold a message, a `Banner` of the given `color` expands into it; once they are empty it collapses again. Give it `role='alert'` so the message is announced. diff --git a/packages/swingset/src/stories/card.component.stories.tsx b/packages/swingset/src/stories/card.component.stories.tsx index fabfea62d4a..d0240b4ae73 100644 --- a/packages/swingset/src/stories/card.component.stories.tsx +++ b/packages/swingset/src/stories/card.component.stories.tsx @@ -116,8 +116,8 @@ export function WithMedia() { export function OAuthConsent() { return ( - - + + + void return ( <> - + + Date: Tue, 3 Nov 2026 12:52:40 -0500 Subject: [PATCH 2/3] feat(mosaic): show the sign-in identifier as a card description --- .../sign-in/steps/sign-in-identity-preview.tsx | 14 ++++---------- .../features/sign-in/steps/sign-in-password.tsx | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/mosaic/src/features/sign-in/steps/sign-in-identity-preview.tsx b/packages/mosaic/src/features/sign-in/steps/sign-in-identity-preview.tsx index 489536ba26f..583e0ebc99b 100644 --- a/packages/mosaic/src/features/sign-in/steps/sign-in-identity-preview.tsx +++ b/packages/mosaic/src/features/sign-in/steps/sign-in-identity-preview.tsx @@ -1,8 +1,8 @@ import * as stylex from '@stylexjs/stylex'; import { Button } from '../../../components/button'; +import { Card } from '../../../components/card'; import { Icon } from '../../../components/icon'; -import { Text } from '../../../components/text'; import { Pen } from '../../../icons/glyphs/pen'; import { truncationStyles } from '../../../styles/typography.styles'; import { styles } from '../sign-in.styles'; @@ -16,14 +16,8 @@ export interface SignInIdentityPreviewProps { export function SignInIdentityPreview({ identifier, editLabel, disabled = false, onEdit }: SignInIdentityPreviewProps) { return ( -
- - {identifier} - + + {identifier} {onEdit ? ( ) : null} -
+ ); } diff --git a/packages/mosaic/src/features/sign-in/steps/sign-in-password.tsx b/packages/mosaic/src/features/sign-in/steps/sign-in-password.tsx index a88df4a9163..22360fb41f0 100644 --- a/packages/mosaic/src/features/sign-in/steps/sign-in-password.tsx +++ b/packages/mosaic/src/features/sign-in/steps/sign-in-password.tsx @@ -70,14 +70,14 @@ export function SignInPassword({ {messages.title} {messages.description} - - +
+
Date: Tue, 3 Nov 2026 13:08:40 -0500 Subject: [PATCH 3/3] fix(mosaic): keep centered card header content inside the card --- .../src/components/card/card.browser.test.tsx | 28 +++++++++++++++++++ .../mosaic/src/components/card/card.styles.ts | 9 +++++- packages/mosaic/src/components/card/card.tsx | 7 ++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/mosaic/src/components/card/card.browser.test.tsx b/packages/mosaic/src/components/card/card.browser.test.tsx index d67ae5da380..2f5d56d6c6b 100644 --- a/packages/mosaic/src/components/card/card.browser.test.tsx +++ b/packages/mosaic/src/components/card/card.browser.test.tsx @@ -66,6 +66,34 @@ describe('Card.Root align', () => { expectCentered('title'); }); + it('keeps a description that does not wrap inside the card', () => { + render( + + + + Title + + + {'jane@acme.com'.repeat(12)} + + + , + ); + + const root = screen.getByTestId('root').getBoundingClientRect(); + const description = screen.getByTestId('description').getBoundingClientRect(); + expect(description.left).toBeGreaterThanOrEqual(root.left); + expect(description.right).toBeLessThanOrEqual(root.right); + expectCentered('title'); + }); + it('keeps them centered in the card beside the dialog close button', () => { render( diff --git a/packages/mosaic/src/components/card/card.styles.ts b/packages/mosaic/src/components/card/card.styles.ts index 46a612d8032..bc5f79297fd 100644 --- a/packages/mosaic/src/components/card/card.styles.ts +++ b/packages/mosaic/src/components/card/card.styles.ts @@ -78,6 +78,7 @@ export const header = stylex.create({ flexDirection: 'column', flexGrow: '1', rowGap: HEADER_ROW_GAP, + minWidth: 0, }, contentBesideCloseButton: { paddingInlineStart: `calc(${CLOSE_BUTTON_WIDTH} + ${HEADER_COLUMN_GAP})`, @@ -104,11 +105,17 @@ export const header = stylex.create({ export const headerAlign = stylex.create({ start: {}, center: { - alignItems: 'center', textAlign: 'center', }, }); +export const mediaAlign = stylex.create({ + start: {}, + center: { + justifyContent: 'center', + }, +}); + const INSET = space['4']; export const banner = stylex.create({ diff --git a/packages/mosaic/src/components/card/card.tsx b/packages/mosaic/src/components/card/card.tsx index 4645a38d8ee..2114de982d3 100644 --- a/packages/mosaic/src/components/card/card.tsx +++ b/packages/mosaic/src/components/card/card.tsx @@ -175,12 +175,17 @@ const Media = React.forwardRef>(func { render, xstyle, ...rest }, ref, ) { + const align = React.useContext(CardContext)?.align ?? 'start'; return useRender({ defaultTagName: 'div', render, ref, props: { - ...mergeStyleProps(themeProps('card-media'), stylex.props(reset.base, slots.header.media, xstyle), rest), + ...mergeStyleProps( + themeProps('card-media'), + stylex.props(reset.base, slots.header.media, slots.mediaAlign[align], xstyle), + rest, + ), }, }); });