From c30f053bcd551226a648993717dd8c4a1449a0db Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 10 Aug 2026 11:18:25 -0600 Subject: [PATCH 1/5] feat(ui): add SettingsGroup block --- .changeset/settings-group-block.md | 2 + .../swingset/src/components/DocsViewer.tsx | 1 + packages/swingset/src/lib/registry.ts | 13 + .../swingset/src/stories/settings-group.mdx | 66 +++++ .../src/stories/settings-group.stories.tsx | 135 +++++++++ .../src/mosaic/block/settings-group.test.tsx | 180 ++++++++++++ .../ui/src/mosaic/block/settings-group.tsx | 268 ++++++++++++++++++ .../ui/src/mosaic/components/reset.test.tsx | 15 + packages/ui/src/mosaic/styles/index.ts | 11 + 9 files changed, 691 insertions(+) create mode 100644 .changeset/settings-group-block.md create mode 100644 packages/swingset/src/stories/settings-group.mdx create mode 100644 packages/swingset/src/stories/settings-group.stories.tsx create mode 100644 packages/ui/src/mosaic/block/settings-group.test.tsx create mode 100644 packages/ui/src/mosaic/block/settings-group.tsx diff --git a/.changeset/settings-group-block.md b/.changeset/settings-group-block.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/settings-group-block.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index fb680d13c3c..bca9e62bb2d 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -29,6 +29,7 @@ const docModules: Record> = { }, blocks: { destructive: dynamic(() => import('../stories/destructive.mdx')), + 'settings-group': dynamic(() => import('../stories/settings-group.mdx')), }, components: { avatar: dynamic(() => import('../stories/avatar.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 60a834b5d98..38850a36bca 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -106,6 +106,12 @@ import { ThemedScrollbar as ScrollAreaThemedScrollbar, } from '../stories/scroll-area.stories'; import { meta as selectMeta } from '../stories/select.stories'; +import { + ConnectedAccounts as SettingsGroupConnectedAccounts, + Default as SettingsGroupDefault, + Destructive as SettingsGroupDestructive, + meta as settingsGroupMeta, +} from '../stories/settings-group.stories'; import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories'; import { meta as tabsMeta } from '../stories/tabs.stories'; import { @@ -126,6 +132,12 @@ import { toSlug } from './slug'; import type { StoryModule } from './types'; const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault }; +const settingsGroupModule: StoryModule = { + meta: settingsGroupMeta, + Default: SettingsGroupDefault, + ConnectedAccounts: SettingsGroupConnectedAccounts, + Destructive: SettingsGroupDestructive, +}; const organizationProfileLeaveSectionModule: StoryModule = { meta: organizationProfileLeaveSectionMeta, Default: OrganizationProfileLeaveSectionDefault, @@ -269,6 +281,7 @@ export const registry: StoryModule[] = [ organizationProfileDeleteSectionModule, // Blocks destructiveModule, + settingsGroupModule, // Components avatarModule, badgeModule, diff --git a/packages/swingset/src/stories/settings-group.mdx b/packages/swingset/src/stories/settings-group.mdx new file mode 100644 index 00000000000..a1cbdeda326 --- /dev/null +++ b/packages/swingset/src/stories/settings-group.mdx @@ -0,0 +1,66 @@ +import * as SettingsGroupStories from './settings-group.stories'; + +# SettingsGroup + +A settings block that carries the canonical section, surface, separator, and label/control layout. Use it for product settings rather than assembling those decisions from generic cards and item groups. + +## Usage + +```tsx +import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group'; + + + Account + + + {icon} + + Name + + {control} + + + +``` + +`SettingsGroup.List` owns the outlined surface and separation between direct `SettingsGroup.Row` children. `SettingsGroup.Media` reserves a consistent slot for icons, avatars, and provider marks. Each row places its label opposite its control. Use `SettingsGroup.Control` for inputs, switches, menus, and buttons—not only actions. + +When a row contains one form control, add `field` to associate its label automatically: + +```tsx + + Name + + + + +``` + +## Examples + + + +### Connected accounts + + + +### Destructive setting + + diff --git a/packages/swingset/src/stories/settings-group.stories.tsx b/packages/swingset/src/stories/settings-group.stories.tsx new file mode 100644 index 00000000000..12d63cb8b8a --- /dev/null +++ b/packages/swingset/src/stories/settings-group.stories.tsx @@ -0,0 +1,135 @@ +/** @jsxImportSource @emotion/react */ +import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group'; +import { Avatar } from '@clerk/ui/mosaic/components/avatar'; +import { Button } from '@clerk/ui/mosaic/components/button'; +import { Icon } from '@clerk/ui/mosaic/components/icon'; +import { Input } from '@clerk/ui/mosaic/components/input'; + +import type { StoryMeta } from '@/lib/types'; + +export { default as __source } from './settings-group.stories?raw'; + +const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`; + +function ProviderIcon({ provider }: { provider: string }) { + return ( + + ); +} + +export const meta: StoryMeta = { + group: 'Blocks', + title: 'SettingsGroup', + source: 'packages/ui/src/mosaic/block/settings-group.tsx', + styleEngine: 'stylex', +}; + +export function Default() { + return ( + + Account + + + Profile picture + + + + PB + + + + + Name + + + + + + Username + + + + + + + ); +} + +export function ConnectedAccounts() { + return ( + + Connected accounts + + + + + + Google + + + + + + + + + Apple + + + + + + + ); +} + +export function Destructive() { + return ( + + Danger zone + + + + Delete account + + + + + + + + ); +} diff --git a/packages/ui/src/mosaic/block/settings-group.test.tsx b/packages/ui/src/mosaic/block/settings-group.test.tsx new file mode 100644 index 00000000000..3015496b8d0 --- /dev/null +++ b/packages/ui/src/mosaic/block/settings-group.test.tsx @@ -0,0 +1,180 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { Input } from '../components/input'; +import { SettingsGroup } from './settings-group'; + +describe('SettingsGroup', () => { + it('renders an accessible settings section and every compound part', () => { + render( + + Account + + + Icon + + Name + + Control + + + , + ); + + const section = screen.getByRole('region', { name: 'Account' }); + expect(section).toHaveClass('cl-settings-group'); + expect(screen.getByRole('heading', { level: 4, name: 'Account' })).toHaveClass('cl-settings-group-title'); + expect(screen.getByTestId('list')).toHaveClass('cl-settings-group-list'); + expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row'); + expect(screen.getByTestId('media')).toHaveClass('cl-settings-group-media'); + expect(screen.getByTestId('label')).toHaveClass('cl-settings-group-label'); + expect(screen.getByTestId('label').tagName).toBe('DIV'); + expect(screen.getByText('Shown throughout the application.')).toHaveClass('cl-settings-group-label-description'); + expect(screen.getByTestId('control')).toHaveClass('cl-settings-group-control'); + }); + + it('supports an explicit accessible name', () => { + render( + + Account + + , + ); + + expect(screen.getByRole('region', { name: 'Account preferences' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 4, name: 'Account' })).toBeInTheDocument(); + }); + + it('automatically associates a field row label and control', () => { + render( + + Account + + + Name + + + + + + , + ); + + const input = screen.getByRole('textbox', { name: 'Name' }); + expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row', 'cl-field-root'); + expect(screen.getByTestId('row')).toHaveAttribute('data-field'); + expect(input).toHaveValue('Ada Lovelace'); + expect(screen.getByText('Name').closest('label')).toHaveAttribute('for', input.id); + }); + + it('lets consumer className and style win on every part', () => { + render( + + + Account + + + + + Name + + + + + , + ); + + expect(screen.getByRole('region', { name: 'Account' })).toHaveClass('cl-settings-group', 'custom-root'); + expect(screen.getByRole('region', { name: 'Account' })).toHaveStyle({ rowGap: '20px' }); + expect(screen.getByRole('heading', { name: 'Account' })).toHaveClass('cl-settings-group-title', 'custom-title'); + expect(screen.getByRole('heading', { name: 'Account' })).toHaveStyle({ color: 'rgb(0, 0, 255)' }); + expect(screen.getByTestId('list')).toHaveClass('cl-settings-group-list', 'custom-list'); + expect(screen.getByTestId('list')).toHaveStyle({ borderWidth: '2px' }); + expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row', 'custom-row'); + expect(screen.getByTestId('row')).toHaveStyle({ minHeight: '80px' }); + expect(screen.getByTestId('label')).toHaveClass('cl-settings-group-label', 'custom-label'); + expect(screen.getByTestId('label')).toHaveStyle({ color: 'rgb(255, 0, 0)' }); + expect(screen.getByTestId('control')).toHaveClass('cl-settings-group-control', 'custom-control'); + expect(screen.getByTestId('control')).toHaveStyle({ width: '200px' }); + }); + + it('forwards refs, arbitrary props, and custom elements', () => { + const rootRef = React.createRef(); + const titleRef = React.createRef(); + const listRef = React.createRef(); + const rowRef = React.createRef(); + const fieldRowRef = React.createRef(); + const mediaRef = React.createRef(); + const labelRef = React.createRef(); + const controlRef = React.createRef(); + + render( +
} + > + Account + + + Icon + Name + + + + Username + + + + + + , + ); + + expect(rootRef.current?.tagName).toBe('ARTICLE'); + expect(rootRef.current).toHaveClass('cl-settings-group'); + expect(titleRef.current?.tagName).toBe('H4'); + expect(titleRef.current).toHaveClass('cl-settings-group-title'); + expect(listRef.current).toBe(screen.getByLabelText('Settings')); + expect(rowRef.current).toHaveClass('cl-settings-group-row'); + expect(fieldRowRef.current).toHaveClass('cl-settings-group-row', 'cl-field-root'); + expect(fieldRowRef.current).toHaveAttribute('data-field'); + expect(mediaRef.current).toHaveClass('cl-settings-group-media'); + expect(labelRef.current).toHaveClass('cl-settings-group-label'); + expect(controlRef.current).toHaveClass('cl-settings-group-control'); + }); +}); diff --git a/packages/ui/src/mosaic/block/settings-group.tsx b/packages/ui/src/mosaic/block/settings-group.tsx new file mode 100644 index 00000000000..3ef46ef3308 --- /dev/null +++ b/packages/ui/src/mosaic/block/settings-group.tsx @@ -0,0 +1,268 @@ +import { useRender } from '@clerk/headless/utils'; +import { useSafeLayoutEffect } from '@clerk/shared/react'; +import * as stylex from '@stylexjs/stylex'; +import React from 'react'; + +import { Field as MosaicField } from '../components/field'; +import type { HeadingProps } from '../components/heading'; +import { Heading } from '../components/heading'; +import { reset } from '../components/reset.styles'; +import type { MosaicComponentProps } from '../props'; +import { mergeStyleProps, themeProps } from '../props'; +import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex'; + +export type SettingsGroupRootProps = Omit, 'title'>; +export type SettingsGroupTitleProps = Omit; + +export type SettingsGroupListProps = MosaicComponentProps<'div'>; +export interface SettingsGroupRowProps extends MosaicComponentProps<'div'> { + /** Associates the row label with exactly one nested form control. */ + field?: boolean; +} +export type SettingsGroupMediaProps = MosaicComponentProps<'div'>; + +export interface SettingsGroupLabelProps extends MosaicComponentProps<'div'> { + description?: React.ReactNode; +} + +export type SettingsGroupControlProps = MosaicComponentProps<'div'>; + +const styles = stylex.create({ + root: { + display: 'flex', + flexDirection: 'column', + rowGap: space['2'], + width: '100%', + }, + list: { + padding: space['2'], + borderColor: colorVars['--cl-color-border'], + borderRadius: radiusVars['--cl-radius-xl'], + borderStyle: 'solid', + borderWidth: '1px', + overflow: 'hidden', + backgroundColor: colorVars['--cl-color-card'], + width: '100%', + }, + row: { + marginInline: space['2'], + paddingBlock: space['4'], + alignItems: 'center', + borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartStyle: 'solid', + borderBlockStartWidth: { + default: '1px', + ':first-child': '0px', + }, + columnGap: space['6'], + display: 'flex', + flexWrap: 'nowrap', + justifyContent: 'space-between', + rowGap: space['3'], + minHeight: space['13'], + width: 'auto', + }, + media: { + alignItems: 'center', + alignSelf: 'center', + display: 'flex', + flexShrink: 0, + justifyContent: 'center', + height: space['6'], + width: space['6'], + }, + label: { + color: colorVars['--cl-color-card-foreground'], + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + rowGap: space['0.5'], + minWidth: 0, + }, + labelText: { + fontSize: typeScaleVars['--cl-text-sm-size'], + fontWeight: fontWeightVars['--cl-font-medium'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], + }, + description: { + color: colorVars['--cl-color-neutral-faded'], + fontSize: typeScaleVars['--cl-text-sm-size'], + fontWeight: fontWeightVars['--cl-font-normal'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], + textWrap: 'balance', + }, + control: { + alignItems: 'center', + display: 'flex', + flexBasis: 'auto', + flexShrink: 0, + justifyContent: 'flex-end', + maxWidth: '60%', + minWidth: 0, + width: 'auto', + }, +}); + +const SettingsGroupRowFieldContext = React.createContext(false); +const SettingsGroupTitleContext = React.createContext> | null>(null); + +const Root = React.forwardRef(function SettingsGroupRoot( + { render, className, style, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...rest }, + ref, +) { + const [titleIds, setTitleIds] = React.useState([]); + + const element = useRender({ + defaultTagName: 'section', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-group'), stylex.props(reset.base, styles.root), className, style), + ...rest, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy ?? (ariaLabel ? undefined : titleIds.join(' ') || undefined), + }, + }); + + return {element}; +}); + +const Title = React.forwardRef(function SettingsGroupTitle( + { id: idProp, render, className, style, ...rest }, + ref, +) { + const setTitleIds = React.useContext(SettingsGroupTitleContext); + const generatedId = React.useId(); + const id = idProp ?? (setTitleIds ? `cl-settings-group-${generatedId}-title` : undefined); + + useSafeLayoutEffect(() => { + if (!id || !setTitleIds) { + return undefined; + } + + setTitleIds(ids => (ids.includes(id) ? ids : [...ids, id])); + return () => setTitleIds(ids => ids.filter(value => value !== id)); + }, [id, setTitleIds]); + + return ( +

)} + size='xs' + {...mergeStyleProps(themeProps('settings-group-title'), className, style)} + {...rest} + /> + ); +}); + +const List = React.forwardRef(function SettingsGroupList( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-group-list'), stylex.props(reset.base, styles.list), className, style), + ...rest, + }, + }); +}); + +const Row = React.forwardRef(function SettingsGroupRow( + { field = false, render, className, style, ...rest }, + ref, +) { + const resolvedRender = field ? : render; + const element = useRender({ + defaultTagName: 'div', + render: resolvedRender, + ref, + props: { + ...mergeStyleProps( + themeProps('settings-group-row', { field }), + stylex.props(reset.base, styles.row), + className, + style, + ), + ...rest, + }, + }); + + return {element}; +}); + +const Media = React.forwardRef(function SettingsGroupMedia( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-group-media'), stylex.props(reset.base, styles.media), className, style), + ...rest, + }, + }); +}); + +const Label = React.forwardRef(function SettingsGroupLabel( + { description, render, className, style, children, ...rest }, + ref, +) { + const field = React.useContext(SettingsGroupRowFieldContext); + const resolvedRender = field ? : render; + + return useRender({ + defaultTagName: 'div', + render: resolvedRender, + ref, + props: { + ...mergeStyleProps(themeProps('settings-group-label'), stylex.props(reset.base, styles.label), className, style), + ...rest, + children: ( + <> + + {children} + + {description ? ( + + {description} + + ) : null} + + ), + }, + }); +}); + +const Control = React.forwardRef(function SettingsGroupControl( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps( + themeProps('settings-group-control'), + stylex.props(reset.base, styles.control), + className, + style, + ), + ...rest, + }, + }); +}); + +/** + * A settings block that fixes section semantics, surface treatment, row separation, + * and label/control layout while leaving each control composable. + */ +export const SettingsGroup = { Root, Title, List, Row, Media, Label, Control }; diff --git a/packages/ui/src/mosaic/components/reset.test.tsx b/packages/ui/src/mosaic/components/reset.test.tsx index 861c05364db..53e3c185a9d 100644 --- a/packages/ui/src/mosaic/components/reset.test.tsx +++ b/packages/ui/src/mosaic/components/reset.test.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; +import { SettingsGroup } from '../block/settings-group'; import { Avatar } from './avatar'; import { Badge } from './badge'; import { Button } from './button'; @@ -60,6 +61,20 @@ const cases: Array<[string, React.ReactElement]> = [ ['Item', ], ['Item.Group', ], ['Item.Separator', ], + ['SettingsGroup', ], + ['SettingsGroup.Title', Account], + ['SettingsGroup.List', ], + ['SettingsGroup.Row', ], + [ + 'SettingsGroup.Row[field]', + , + ], + ['SettingsGroup.Media', ], + ['SettingsGroup.Label', ], + ['SettingsGroup.Control', ], ['Text', Body copy], ]; diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index adb03acb478..b6279d34f1b 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -6,6 +6,17 @@ export type { MosaicComponentProps, MosaicElementProps } from '../props'; +export { SettingsGroup } from '../block/settings-group'; +export type { + SettingsGroupControlProps, + SettingsGroupLabelProps, + SettingsGroupListProps, + SettingsGroupMediaProps, + SettingsGroupRowProps, + SettingsGroupRootProps, + SettingsGroupTitleProps, +} from '../block/settings-group'; + export { Avatar } from '../components/avatar'; export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from '../components/avatar'; export { Badge } from '../components/badge'; From 7d5faf94cbd1afbaf7b66fba3aefa8a3bc8a686c Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 11 Aug 2026 16:32:01 -0600 Subject: [PATCH 2/5] feat(ui): rework settings block composition --- .../src/stories/settings-group.stories.tsx | 408 ++++++++++++++---- .../block/settings-group.markers.stylex.ts | 3 + .../src/mosaic/block/settings-group.test.tsx | 253 +++++------ .../ui/src/mosaic/block/settings-group.tsx | 261 +++++++---- .../block/settings-group.vars.stylex.ts | 7 + .../ui/src/mosaic/components/reset.test.tsx | 27 +- packages/ui/src/mosaic/styles/index.ts | 21 +- packages/ui/src/mosaic/tokens.stylex.ts | 2 + 8 files changed, 639 insertions(+), 343 deletions(-) create mode 100644 packages/ui/src/mosaic/block/settings-group.markers.stylex.ts create mode 100644 packages/ui/src/mosaic/block/settings-group.vars.stylex.ts diff --git a/packages/swingset/src/stories/settings-group.stories.tsx b/packages/swingset/src/stories/settings-group.stories.tsx index 12d63cb8b8a..b9abf63d324 100644 --- a/packages/swingset/src/stories/settings-group.stories.tsx +++ b/packages/swingset/src/stories/settings-group.stories.tsx @@ -1,9 +1,9 @@ /** @jsxImportSource @emotion/react */ -import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group'; +import { Settings } from '@clerk/ui/mosaic/block/settings-group'; import { Avatar } from '@clerk/ui/mosaic/components/avatar'; +import { Badge } from '@clerk/ui/mosaic/components/badge'; import { Button } from '@clerk/ui/mosaic/components/button'; import { Icon } from '@clerk/ui/mosaic/components/icon'; -import { Input } from '@clerk/ui/mosaic/components/input'; import type { StoryMeta } from '@/lib/types'; @@ -23,113 +23,333 @@ function ProviderIcon({ provider }: { provider: string }) { export const meta: StoryMeta = { group: 'Blocks', - title: 'SettingsGroup', + title: 'Settings', source: 'packages/ui/src/mosaic/block/settings-group.tsx', styleEngine: 'stylex', }; export function Default() { return ( - - Account - - - Profile picture - - - - PB - - - - - Name - - - - - - Username - - - - - - + + Profile + + + + + Profile picture + PNG or JPEG, Recommended size 1:1, up to 10MB. + + + + + PB + + + + + + + + + Name + Preston Booth + + + + + + + + + + + Username + Prestonb.xyz + + + + + + + + + + + Email + item1@clerk.dev + + + + + + + + + ); +} + +export function MultipleEmailAndPhoneNumbers() { + return ( + + Profile + + + + + Profile picture + PNG or JPEG, Recommended size 1:1, up to 10MB. + + + + + PB + + + + + + + + + Name + Preston Booth + + + + + + + + + + + Username + Prestonb.xyz + + + + + + + + + + + Email + + + + + + + + + + item1@clerk.dev + Primary + + + + + + + + + item2@clerk.dev + + + + + + + + + + + + Phone + + + + + + + + + + +1 801-888-8181 + Primary + + + + + + + + + + ); } export function ConnectedAccounts() { return ( - - Connected accounts - - - - - - Google - - - - - - - - - Apple - - - - - - + + Connected accounts + + + + + + + + Google + test@google.com + + + + + + + + + + + + + Apple + + + + + + + + ); } export function Destructive() { return ( - - Danger zone - - - - Delete account - - - - - - - + + Danger zone + + + + + Delete account + + Permanently delete this profile and all its data. This cannot be undone. + + + + + + + + + ); } diff --git a/packages/ui/src/mosaic/block/settings-group.markers.stylex.ts b/packages/ui/src/mosaic/block/settings-group.markers.stylex.ts new file mode 100644 index 00000000000..ce4b0c07ebd --- /dev/null +++ b/packages/ui/src/mosaic/block/settings-group.markers.stylex.ts @@ -0,0 +1,3 @@ +import * as stylex from '@stylexjs/stylex'; + +export const settingsItemsMarker = stylex.defineMarker(); diff --git a/packages/ui/src/mosaic/block/settings-group.test.tsx b/packages/ui/src/mosaic/block/settings-group.test.tsx index 3015496b8d0..fab787ec4e0 100644 --- a/packages/ui/src/mosaic/block/settings-group.test.tsx +++ b/packages/ui/src/mosaic/block/settings-group.test.tsx @@ -2,179 +2,144 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Input } from '../components/input'; -import { SettingsGroup } from './settings-group'; +import { colorVars } from '../tokens.stylex'; +import { Settings, settingsVars } from './settings-group'; + +describe('Settings', () => { + it('aliases its background to the global background token', () => { + expect(colorVars).toMatchObject({ + '--cl-color-background': 'var(--cl-color-background)', + }); + expect(settingsVars).toMatchObject({ + '--cl-settings-background': 'var(--cl-settings-background)', + }); + }); -describe('SettingsGroup', () => { it('renders an accessible settings section and every compound part', () => { render( - - Account - - - Icon - - Name - - Control - - - , + + Account + + + + + Icon + + + Name + Shown throughout the application. + + Control + + + + , ); - const section = screen.getByRole('region', { name: 'Account' }); - expect(section).toHaveClass('cl-settings-group'); - expect(screen.getByRole('heading', { level: 4, name: 'Account' })).toHaveClass('cl-settings-group-title'); - expect(screen.getByTestId('list')).toHaveClass('cl-settings-group-list'); - expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row'); - expect(screen.getByTestId('media')).toHaveClass('cl-settings-group-media'); - expect(screen.getByTestId('label')).toHaveClass('cl-settings-group-label'); - expect(screen.getByTestId('label').tagName).toBe('DIV'); - expect(screen.getByText('Shown throughout the application.')).toHaveClass('cl-settings-group-label-description'); - expect(screen.getByTestId('control')).toHaveClass('cl-settings-group-control'); + expect(screen.getByRole('region', { name: 'Account' })).toHaveClass('cl-settings'); + expect(screen.getByRole('heading', { level: 4, name: 'Account' })).toHaveClass('cl-settings-title'); + expect(screen.getByTestId('group')).toHaveClass('cl-settings-group'); + expect(screen.getByTestId('row')).toHaveClass('cl-settings-row'); + expect(screen.getByTestId('item')).toHaveClass('cl-settings-item'); + expect(screen.getByTestId('media')).toHaveClass('cl-settings-media'); + expect(screen.getByTestId('media')).toHaveAttribute('data-size', 'lg'); + expect(screen.getByTestId('content')).toHaveClass('cl-settings-content'); + expect(screen.getByTestId('label')).toHaveClass('cl-settings-label'); + expect(screen.getByTestId('description')).toHaveClass('cl-settings-description'); + expect(screen.getByTestId('actions')).toHaveClass('cl-settings-actions'); }); it('supports an explicit accessible name', () => { render( - - Account - - , + + Account + + , ); expect(screen.getByRole('region', { name: 'Account preferences' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Account' })).toBeInTheDocument(); }); - it('automatically associates a field row label and control', () => { + it('composes multiple items in one row', () => { render( - - Account - - - Name - - - - - - , + + Profile + + + + + Email + + Edit + + + + + ada@example.com + + More + + + + + , ); - const input = screen.getByRole('textbox', { name: 'Name' }); - expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row', 'cl-field-root'); - expect(screen.getByTestId('row')).toHaveAttribute('data-field'); - expect(input).toHaveValue('Ada Lovelace'); - expect(screen.getByText('Name').closest('label')).toHaveAttribute('for', input.id); + expect(screen.getByText('Email')).toBeInTheDocument(); + expect(screen.getByText('ada@example.com')).toBeInTheDocument(); + expect(screen.getAllByText(/Edit|More/)).toHaveLength(2); + expect(screen.getByTestId('items')).toHaveClass('cl-settings-items'); + expect(screen.getByTestId('items')).toHaveAttribute('data-nested'); + expect(screen.getByTestId('nested-item')).toHaveAttribute('data-nested'); + expect(screen.getByTestId('nested-content')).toHaveAttribute('data-nested'); }); - it('lets consumer className and style win on every part', () => { - render( - - - Account - - - - - Name - - - - - , - ); - - expect(screen.getByRole('region', { name: 'Account' })).toHaveClass('cl-settings-group', 'custom-root'); - expect(screen.getByRole('region', { name: 'Account' })).toHaveStyle({ rowGap: '20px' }); - expect(screen.getByRole('heading', { name: 'Account' })).toHaveClass('cl-settings-group-title', 'custom-title'); - expect(screen.getByRole('heading', { name: 'Account' })).toHaveStyle({ color: 'rgb(0, 0, 255)' }); - expect(screen.getByTestId('list')).toHaveClass('cl-settings-group-list', 'custom-list'); - expect(screen.getByTestId('list')).toHaveStyle({ borderWidth: '2px' }); - expect(screen.getByTestId('row')).toHaveClass('cl-settings-group-row', 'custom-row'); - expect(screen.getByTestId('row')).toHaveStyle({ minHeight: '80px' }); - expect(screen.getByTestId('label')).toHaveClass('cl-settings-group-label', 'custom-label'); - expect(screen.getByTestId('label')).toHaveStyle({ color: 'rgb(255, 0, 0)' }); - expect(screen.getByTestId('control')).toHaveClass('cl-settings-group-control', 'custom-control'); - expect(screen.getByTestId('control')).toHaveStyle({ width: '200px' }); - }); - - it('forwards refs, arbitrary props, and custom elements', () => { + it('lets consumer props win and forwards refs and custom elements', () => { const rootRef = React.createRef(); - const titleRef = React.createRef(); - const listRef = React.createRef(); - const rowRef = React.createRef(); - const fieldRowRef = React.createRef(); - const mediaRef = React.createRef(); - const labelRef = React.createRef(); - const controlRef = React.createRef(); + const groupRef = React.createRef(); + const itemRef = React.createRef(); + const contentRef = React.createRef(); + const actionsRef = React.createRef(); render( -
} + className='custom-root' > - Account - Account + - - Icon - Name - - - - Username - - - - - - , + + + + Name + + + + + + , ); expect(rootRef.current?.tagName).toBe('ARTICLE'); - expect(rootRef.current).toHaveClass('cl-settings-group'); - expect(titleRef.current?.tagName).toBe('H4'); - expect(titleRef.current).toHaveClass('cl-settings-group-title'); - expect(listRef.current).toBe(screen.getByLabelText('Settings')); - expect(rowRef.current).toHaveClass('cl-settings-group-row'); - expect(fieldRowRef.current).toHaveClass('cl-settings-group-row', 'cl-field-root'); - expect(fieldRowRef.current).toHaveAttribute('data-field'); - expect(mediaRef.current).toHaveClass('cl-settings-group-media'); - expect(labelRef.current).toHaveClass('cl-settings-group-label'); - expect(controlRef.current).toHaveClass('cl-settings-group-control'); + expect(rootRef.current).toHaveClass('cl-settings', 'custom-root'); + expect(groupRef.current).toHaveClass('cl-settings-group', 'custom-group'); + expect(groupRef.current).toHaveStyle({ borderWidth: '2px' }); + expect(itemRef.current).toHaveClass('cl-settings-item'); + expect(itemRef.current).toHaveStyle({ minHeight: '80px' }); + expect(contentRef.current).toHaveClass('cl-settings-content'); + expect(screen.getByText('Name')).toHaveStyle({ color: 'rgb(255, 0, 0)' }); + expect(actionsRef.current).toHaveClass('cl-settings-actions'); }); }); diff --git a/packages/ui/src/mosaic/block/settings-group.tsx b/packages/ui/src/mosaic/block/settings-group.tsx index 3ef46ef3308..2b53d3bc042 100644 --- a/packages/ui/src/mosaic/block/settings-group.tsx +++ b/packages/ui/src/mosaic/block/settings-group.tsx @@ -3,30 +3,32 @@ import { useSafeLayoutEffect } from '@clerk/shared/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; -import { Field as MosaicField } from '../components/field'; import type { HeadingProps } from '../components/heading'; import { Heading } from '../components/heading'; import { reset } from '../components/reset.styles'; import type { MosaicComponentProps } from '../props'; import { mergeStyleProps, themeProps } from '../props'; import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex'; +import { settingsItemsMarker } from './settings-group.markers.stylex'; +import { settingsVars } from './settings-group.vars.stylex'; -export type SettingsGroupRootProps = Omit, 'title'>; -export type SettingsGroupTitleProps = Omit; +export { settingsVars } from './settings-group.vars.stylex'; -export type SettingsGroupListProps = MosaicComponentProps<'div'>; -export interface SettingsGroupRowProps extends MosaicComponentProps<'div'> { - /** Associates the row label with exactly one nested form control. */ - field?: boolean; -} -export type SettingsGroupMediaProps = MosaicComponentProps<'div'>; - -export interface SettingsGroupLabelProps extends MosaicComponentProps<'div'> { - description?: React.ReactNode; -} - -export type SettingsGroupControlProps = MosaicComponentProps<'div'>; +export type SettingsRootProps = Omit, 'title'>; +export type SettingsTitleProps = Omit; +export type SettingsGroupProps = MosaicComponentProps<'div'>; +export type SettingsRowProps = MosaicComponentProps<'div'>; +export type SettingsItemsProps = MosaicComponentProps<'div'>; +export type SettingsItemProps = MosaicComponentProps<'div'>; +export type SettingsMediaSize = 'sm' | 'md' | 'lg'; +export type SettingsMediaProps = MosaicComponentProps<'div'> & { size?: SettingsMediaSize }; +export type SettingsContentProps = MosaicComponentProps<'div'>; +export type SettingsLabelProps = MosaicComponentProps<'div'>; +export type SettingsDescriptionProps = MosaicComponentProps<'div'>; +export type SettingsActionsProps = MosaicComponentProps<'div'>; +/* eslint-disable @stylexjs/no-lookahead-selectors -- Mosaic's supported browsers include :has(); + the marker keeps this selector scoped to Settings.Items. */ const styles = stylex.create({ root: { display: 'flex', @@ -34,52 +36,82 @@ const styles = stylex.create({ rowGap: space['2'], width: '100%', }, - list: { - padding: space['2'], + group: { borderColor: colorVars['--cl-color-border'], borderRadius: radiusVars['--cl-radius-xl'], borderStyle: 'solid', borderWidth: '1px', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: settingsVars['--cl-settings-background'], width: '100%', }, row: { - marginInline: space['2'], - paddingBlock: space['4'], - alignItems: 'center', + marginInline: space['4'], borderBlockStartColor: colorVars['--cl-color-border'], borderBlockStartStyle: 'solid', borderBlockStartWidth: { default: '1px', ':first-child': '0px', }, - columnGap: space['6'], + display: 'flex', + flexDirection: 'column', + paddingBlockEnd: { + default: space['4'], + [stylex.when.descendant('[data-nested]', settingsItemsMarker)]: space['1'], + }, + paddingBlockStart: space['4'], + rowGap: space['2'], + width: 'auto', + }, + items: { + display: 'flex', + flexDirection: 'column', + width: '100%', + }, + item: { + alignItems: 'center', + columnGap: space['3'], display: 'flex', flexWrap: 'nowrap', justifyContent: 'space-between', - rowGap: space['3'], - minHeight: space['13'], - width: 'auto', + width: '100%', }, - media: { + nestedItem: { + minHeight: space['11'], + }, + mediaBase: { alignItems: 'center', alignSelf: 'center', + aspectRatio: '1/1', display: 'flex', flexShrink: 0, justifyContent: 'center', + }, + mediaSm: { + height: space['4'], + width: space['4'], + }, + mediaMd: { height: space['6'], width: space['6'], }, - label: { - color: colorVars['--cl-color-card-foreground'], + mediaLg: { + height: space['8'], + width: space['8'], + }, + content: { display: 'flex', flexDirection: 'column', flexGrow: 1, + justifyContent: 'center', rowGap: space['0.5'], minWidth: 0, }, - labelText: { + nestedContent: { + paddingBlock: space['3'], + }, + label: { + color: colorVars['--cl-color-card-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], @@ -87,26 +119,30 @@ const styles = stylex.create({ description: { color: colorVars['--cl-color-neutral-faded'], fontSize: typeScaleVars['--cl-text-sm-size'], - fontWeight: fontWeightVars['--cl-font-normal'], + fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], textWrap: 'balance', }, - control: { + actions: { alignItems: 'center', display: 'flex', - flexBasis: 'auto', flexShrink: 0, justifyContent: 'flex-end', - maxWidth: '60%', - minWidth: 0, - width: 'auto', + marginInlineStart: space['3'], }, }); +/* eslint-enable @stylexjs/no-lookahead-selectors */ -const SettingsGroupRowFieldContext = React.createContext(false); -const SettingsGroupTitleContext = React.createContext> | null>(null); +const mediaSizes = { + sm: styles.mediaSm, + md: styles.mediaMd, + lg: styles.mediaLg, +}; -const Root = React.forwardRef(function SettingsGroupRoot( +const SettingsTitleContext = React.createContext> | null>(null); +const SettingsItemsContext = React.createContext(false); + +const Root = React.forwardRef(function SettingsRoot( { render, className, style, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...rest }, ref, ) { @@ -117,23 +153,23 @@ const Root = React.forwardRef(function Sett render, ref, props: { - ...mergeStyleProps(themeProps('settings-group'), stylex.props(reset.base, styles.root), className, style), + ...mergeStyleProps(themeProps('settings'), stylex.props(reset.base, styles.root), className, style), ...rest, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy ?? (ariaLabel ? undefined : titleIds.join(' ') || undefined), }, }); - return {element}; + return {element}; }); -const Title = React.forwardRef(function SettingsGroupTitle( +const Title = React.forwardRef(function SettingsTitle( { id: idProp, render, className, style, ...rest }, ref, ) { - const setTitleIds = React.useContext(SettingsGroupTitleContext); + const setTitleIds = React.useContext(SettingsTitleContext); const generatedId = React.useId(); - const id = idProp ?? (setTitleIds ? `cl-settings-group-${generatedId}-title` : undefined); + const id = idProp ?? (setTitleIds ? `cl-settings-${generatedId}-title` : undefined); useSafeLayoutEffect(() => { if (!id || !setTitleIds) { @@ -149,14 +185,14 @@ const Title = React.forwardRef(func ref={ref} id={id} render={render ?? (props =>

)} - size='xs' - {...mergeStyleProps(themeProps('settings-group-title'), className, style)} + size='sm' + {...mergeStyleProps(themeProps('settings-title'), className, style)} {...rest} /> ); }); -const List = React.forwardRef(function SettingsGroupList( +const Group = React.forwardRef(function SettingsGroup( { render, className, style, ...rest }, ref, ) { @@ -165,25 +201,39 @@ const List = React.forwardRef(function S render, ref, props: { - ...mergeStyleProps(themeProps('settings-group-list'), stylex.props(reset.base, styles.list), className, style), + ...mergeStyleProps(themeProps('settings-group'), stylex.props(reset.base, styles.group), className, style), ...rest, }, }); }); -const Row = React.forwardRef(function SettingsGroupRow( - { field = false, render, className, style, ...rest }, +const Row = React.forwardRef(function SettingsRow( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-row'), stylex.props(reset.base, styles.row), className, style), + ...rest, + }, + }); +}); + +const Items = React.forwardRef(function SettingsItems( + { render, className, style, ...rest }, ref, ) { - const resolvedRender = field ? : render; const element = useRender({ defaultTagName: 'div', - render: resolvedRender, + render, ref, props: { ...mergeStyleProps( - themeProps('settings-group-row', { field }), - stylex.props(reset.base, styles.row), + themeProps('settings-items', { nested: true }), + stylex.props(reset.base, styles.items, settingsItemsMarker), className, style, ), @@ -191,57 +241,89 @@ const Row = React.forwardRef(function Set }, }); - return {element}; + return {element}; }); -const Media = React.forwardRef(function SettingsGroupMedia( +const Item = React.forwardRef(function SettingsItem( { render, className, style, ...rest }, ref, ) { + const nested = React.useContext(SettingsItemsContext); + return useRender({ defaultTagName: 'div', render, ref, props: { - ...mergeStyleProps(themeProps('settings-group-media'), stylex.props(reset.base, styles.media), className, style), + ...mergeStyleProps( + themeProps('settings-item', { nested }), + stylex.props(reset.base, styles.item, nested && styles.nestedItem), + className, + style, + ), ...rest, }, }); }); -const Label = React.forwardRef(function SettingsGroupLabel( - { description, render, className, style, children, ...rest }, +const Media = React.forwardRef(function SettingsMedia( + { size = 'md', render, className, style, ...rest }, ref, ) { - const field = React.useContext(SettingsGroupRowFieldContext); - const resolvedRender = field ? : render; - return useRender({ defaultTagName: 'div', - render: resolvedRender, + render, ref, props: { - ...mergeStyleProps(themeProps('settings-group-label'), stylex.props(reset.base, styles.label), className, style), + ...mergeStyleProps( + themeProps('settings-media', { size }), + stylex.props(reset.base, styles.mediaBase, mediaSizes[size]), + className, + style, + ), ...rest, - children: ( - <> - - {children} - - {description ? ( - - {description} - - ) : null} - + }, + }); +}); + +const Content = React.forwardRef(function SettingsContent( + { render, className, style, ...rest }, + ref, +) { + const nested = React.useContext(SettingsItemsContext); + + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps( + themeProps('settings-content', { nested }), + stylex.props(reset.base, styles.content, nested && styles.nestedContent), + className, + style, ), + ...rest, }, }); }); -const Control = React.forwardRef(function SettingsGroupControl( +const Label = React.forwardRef(function SettingsLabel( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-label'), stylex.props(reset.base, styles.label), className, style), + ...rest, + }, + }); +}); + +const Description = React.forwardRef(function SettingsDescription( { render, className, style, ...rest }, ref, ) { @@ -251,8 +333,8 @@ const Control = React.forwardRef(func ref, props: { ...mergeStyleProps( - themeProps('settings-group-control'), - stylex.props(reset.base, styles.control), + themeProps('settings-description'), + stylex.props(reset.base, styles.description), className, style, ), @@ -261,8 +343,23 @@ const Control = React.forwardRef(func }); }); +const Actions = React.forwardRef(function SettingsActions( + { render, className, style, ...rest }, + ref, +) { + return useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps(themeProps('settings-actions'), stylex.props(reset.base, styles.actions), className, style), + ...rest, + }, + }); +}); + /** - * A settings block that fixes section semantics, surface treatment, row separation, - * and label/control layout while leaving each control composable. + * A settings block that fixes section semantics, surface treatment, row grouping, + * and item layout while leaving each item's content composable. */ -export const SettingsGroup = { Root, Title, List, Row, Media, Label, Control }; +export const Settings = { Root, Title, Group, Row, Items, Item, Media, Content, Label, Description, Actions }; diff --git a/packages/ui/src/mosaic/block/settings-group.vars.stylex.ts b/packages/ui/src/mosaic/block/settings-group.vars.stylex.ts new file mode 100644 index 00000000000..750eb946e55 --- /dev/null +++ b/packages/ui/src/mosaic/block/settings-group.vars.stylex.ts @@ -0,0 +1,7 @@ +import * as stylex from '@stylexjs/stylex'; + +import { colorVars } from '../tokens.stylex'; + +export const settingsVars = stylex.defineVars({ + '--cl-settings-background': colorVars['--cl-color-background'], +}); diff --git a/packages/ui/src/mosaic/components/reset.test.tsx b/packages/ui/src/mosaic/components/reset.test.tsx index 53e3c185a9d..3268e1877eb 100644 --- a/packages/ui/src/mosaic/components/reset.test.tsx +++ b/packages/ui/src/mosaic/components/reset.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { SettingsGroup } from '../block/settings-group'; +import { Settings } from '../block/settings-group'; import { Avatar } from './avatar'; import { Badge } from './badge'; import { Button } from './button'; @@ -61,20 +61,17 @@ const cases: Array<[string, React.ReactElement]> = [ ['Item', ], ['Item.Group', ], ['Item.Separator', ], - ['SettingsGroup', ], - ['SettingsGroup.Title', Account], - ['SettingsGroup.List', ], - ['SettingsGroup.Row', ], - [ - 'SettingsGroup.Row[field]', - , - ], - ['SettingsGroup.Media', ], - ['SettingsGroup.Label', ], - ['SettingsGroup.Control', ], + ['Settings', ], + ['Settings.Title', Account], + ['Settings.Group', ], + ['Settings.Row', ], + ['Settings.Items', ], + ['Settings.Item', ], + ['Settings.Media', ], + ['Settings.Content', ], + ['Settings.Label', ], + ['Settings.Description', ], + ['Settings.Actions', ], ['Text', Body copy], ]; diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index b6279d34f1b..e3c1fad3b17 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -6,15 +6,20 @@ export type { MosaicComponentProps, MosaicElementProps } from '../props'; -export { SettingsGroup } from '../block/settings-group'; +export { Settings, settingsVars } from '../block/settings-group'; export type { - SettingsGroupControlProps, - SettingsGroupLabelProps, - SettingsGroupListProps, - SettingsGroupMediaProps, - SettingsGroupRowProps, - SettingsGroupRootProps, - SettingsGroupTitleProps, + SettingsActionsProps, + SettingsContentProps, + SettingsDescriptionProps, + SettingsGroupProps, + SettingsItemProps, + SettingsItemsProps, + SettingsLabelProps, + SettingsMediaProps, + SettingsMediaSize, + SettingsRootProps, + SettingsRowProps, + SettingsTitleProps, } from '../block/settings-group'; export { Avatar } from '../components/avatar'; diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 5b9ea1d20bc..6d249a5b660 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -44,6 +44,8 @@ const colorDefaults = { '--cl-color-warning-foreground': 'oklch(0.985 0 0)', '--cl-color-warning-faded': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', + '--cl-color-background': 'light-dark(oklch(0.985 0 0), oklch(0.205 0 0))', + '--cl-color-card': 'light-dark(oklch(1 0 0), oklch(0.205 0 0))', '--cl-color-card-foreground': 'light-dark(oklch(0.145 0 0), oklch(0.985 0 0))', From 2c8760c3fa90fb9930bb495dda3db6b5a54adf50 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 11 Aug 2026 16:32:13 -0600 Subject: [PATCH 3/5] docs(swingset): document settings composition --- .../swingset/src/components/DocsViewer.tsx | 2 +- packages/swingset/src/lib/registry.ts | 2 + .../swingset/src/stories/settings-group.mdx | 68 ++++++++++--------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index bca9e62bb2d..a119fb291b9 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -29,7 +29,7 @@ const docModules: Record> = { }, blocks: { destructive: dynamic(() => import('../stories/destructive.mdx')), - 'settings-group': dynamic(() => import('../stories/settings-group.mdx')), + settings: dynamic(() => import('../stories/settings-group.mdx')), }, components: { avatar: dynamic(() => import('../stories/avatar.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 38850a36bca..d3d386876da 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -111,6 +111,7 @@ import { Default as SettingsGroupDefault, Destructive as SettingsGroupDestructive, meta as settingsGroupMeta, + MultipleEmailAndPhoneNumbers as SettingsGroupMultipleEmailAndPhoneNumbers, } from '../stories/settings-group.stories'; import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories'; import { meta as tabsMeta } from '../stories/tabs.stories'; @@ -135,6 +136,7 @@ const destructiveModule: StoryModule = { meta: destructiveMeta, Default: Destruc const settingsGroupModule: StoryModule = { meta: settingsGroupMeta, Default: SettingsGroupDefault, + MultipleEmailAndPhoneNumbers: SettingsGroupMultipleEmailAndPhoneNumbers, ConnectedAccounts: SettingsGroupConnectedAccounts, Destructive: SettingsGroupDestructive, }; diff --git a/packages/swingset/src/stories/settings-group.mdx b/packages/swingset/src/stories/settings-group.mdx index a1cbdeda326..cf2d6a24d7f 100644 --- a/packages/swingset/src/stories/settings-group.mdx +++ b/packages/swingset/src/stories/settings-group.mdx @@ -1,49 +1,55 @@ -import * as SettingsGroupStories from './settings-group.stories'; +import * as SettingsStories from './settings-group.stories'; -# SettingsGroup +# Settings -A settings block that carries the canonical section, surface, separator, and label/control layout. Use it for product settings rather than assembling those decisions from generic cards and item groups. +A settings block that carries the canonical section, surface, row grouping, and item layout. Use it for product settings while composing each item's media, content, and actions as needed. ## Usage ```tsx -import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group'; +import { Settings } from '@clerk/ui/mosaic/block/settings-group'; - - Account - - - {icon} - - Name - - {control} - - - + + Account + + + + {icon} + + Name + Shown throughout the application. + + {control} + + + + ``` -`SettingsGroup.List` owns the outlined surface and separation between direct `SettingsGroup.Row` children. `SettingsGroup.Media` reserves a consistent slot for icons, avatars, and provider marks. Each row places its label opposite its control. Use `SettingsGroup.Control` for inputs, switches, menus, and buttons—not only actions. +`Settings.Group` owns the outlined surface. Its direct `Settings.Row` children own separation and may stack one or more `Settings.Item` entries. Each item lays out optional media, flexible content, and trailing actions. `Settings.Media` accepts `md` and `lg` sizes. -When a row contains one form control, add `field` to associate its label automatically: - -```tsx - - Name - - - - -``` +Use `Settings.Items` for a nested value list beneath a row's header item. The row keeps an 8px header-to-list gap, nested items stack without gaps at a 44px minimum height, and their content receives 12px vertical padding. ## Examples + +### Multiple email addresses and phone numbers + + @@ -51,7 +57,7 @@ When a row contains one form control, add `field` to associate its label automat From 5a6ccdbc962a0d97caf594d4bf3bef64705d9d0f Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 11 Aug 2026 18:25:47 -0600 Subject: [PATCH 4/5] feat(ui): rename Settings block and expose item gap --- ...tings-group-block.md => settings-block.md} | 0 .../swingset/src/components/DocsViewer.tsx | 2 +- packages/swingset/src/lib/registry.ts | 26 +++++++++---------- .../{settings-group.mdx => settings.mdx} | 6 +++-- ...group.stories.tsx => settings.stories.tsx} | 6 ++--- ...s.stylex.ts => settings.markers.stylex.ts} | 0 ...tings-group.test.tsx => settings.test.tsx} | 3 ++- .../{settings-group.tsx => settings.tsx} | 8 +++--- ...vars.stylex.ts => settings.vars.stylex.ts} | 3 ++- .../ui/src/mosaic/components/reset.test.tsx | 2 +- packages/ui/src/mosaic/styles/index.ts | 4 +-- 11 files changed, 32 insertions(+), 28 deletions(-) rename .changeset/{settings-group-block.md => settings-block.md} (100%) rename packages/swingset/src/stories/{settings-group.mdx => settings.mdx} (88%) rename packages/swingset/src/stories/{settings-group.stories.tsx => settings.stories.tsx} (98%) rename packages/ui/src/mosaic/block/{settings-group.markers.stylex.ts => settings.markers.stylex.ts} (100%) rename packages/ui/src/mosaic/block/{settings-group.test.tsx => settings.test.tsx} (98%) rename packages/ui/src/mosaic/block/{settings-group.tsx => settings.tsx} (97%) rename packages/ui/src/mosaic/block/{settings-group.vars.stylex.ts => settings.vars.stylex.ts} (63%) diff --git a/.changeset/settings-group-block.md b/.changeset/settings-block.md similarity index 100% rename from .changeset/settings-group-block.md rename to .changeset/settings-block.md diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index a119fb291b9..e0b8b276d02 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -29,7 +29,7 @@ const docModules: Record> = { }, blocks: { destructive: dynamic(() => import('../stories/destructive.mdx')), - settings: dynamic(() => import('../stories/settings-group.mdx')), + settings: dynamic(() => import('../stories/settings.mdx')), }, components: { avatar: dynamic(() => import('../stories/avatar.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index d3d386876da..8a56b3aad47 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -107,12 +107,12 @@ import { } from '../stories/scroll-area.stories'; import { meta as selectMeta } from '../stories/select.stories'; import { - ConnectedAccounts as SettingsGroupConnectedAccounts, - Default as SettingsGroupDefault, - Destructive as SettingsGroupDestructive, - meta as settingsGroupMeta, - MultipleEmailAndPhoneNumbers as SettingsGroupMultipleEmailAndPhoneNumbers, -} from '../stories/settings-group.stories'; + ConnectedAccounts as SettingsConnectedAccounts, + Default as SettingsDefault, + Destructive as SettingsDestructive, + meta as settingsMeta, + MultipleEmailAndPhoneNumbers as SettingsMultipleEmailAndPhoneNumbers, +} from '../stories/settings.stories'; import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories'; import { meta as tabsMeta } from '../stories/tabs.stories'; import { @@ -133,12 +133,12 @@ import { toSlug } from './slug'; import type { StoryModule } from './types'; const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault }; -const settingsGroupModule: StoryModule = { - meta: settingsGroupMeta, - Default: SettingsGroupDefault, - MultipleEmailAndPhoneNumbers: SettingsGroupMultipleEmailAndPhoneNumbers, - ConnectedAccounts: SettingsGroupConnectedAccounts, - Destructive: SettingsGroupDestructive, +const settingsModule: StoryModule = { + meta: settingsMeta, + Default: SettingsDefault, + MultipleEmailAndPhoneNumbers: SettingsMultipleEmailAndPhoneNumbers, + ConnectedAccounts: SettingsConnectedAccounts, + Destructive: SettingsDestructive, }; const organizationProfileLeaveSectionModule: StoryModule = { meta: organizationProfileLeaveSectionMeta, @@ -283,7 +283,7 @@ export const registry: StoryModule[] = [ organizationProfileDeleteSectionModule, // Blocks destructiveModule, - settingsGroupModule, + settingsModule, // Components avatarModule, badgeModule, diff --git a/packages/swingset/src/stories/settings-group.mdx b/packages/swingset/src/stories/settings.mdx similarity index 88% rename from packages/swingset/src/stories/settings-group.mdx rename to packages/swingset/src/stories/settings.mdx index cf2d6a24d7f..eba6b7ab384 100644 --- a/packages/swingset/src/stories/settings-group.mdx +++ b/packages/swingset/src/stories/settings.mdx @@ -1,4 +1,4 @@ -import * as SettingsStories from './settings-group.stories'; +import * as SettingsStories from './settings.stories'; # Settings @@ -7,7 +7,7 @@ A settings block that carries the canonical section, surface, row grouping, and ## Usage ```tsx -import { Settings } from '@clerk/ui/mosaic/block/settings-group'; +import { Settings } from '@clerk/ui/mosaic/block/settings'; Account @@ -30,6 +30,8 @@ import { Settings } from '@clerk/ui/mosaic/block/settings-group'; Use `Settings.Items` for a nested value list beneath a row's header item. The row keeps an 8px header-to-list gap, nested items stack without gaps at a 44px minimum height, and their content receives 12px vertical padding. +Override `--cl-settings-items-gap` on `Settings.Root` or a specific `Settings.Row` to customize the space between the row header and its nested value list. It defaults to 8px. + ## Examples `https://img.clerk.com/static/${provider}.svg`; @@ -24,7 +24,7 @@ function ProviderIcon({ provider }: { provider: string }) { export const meta: StoryMeta = { group: 'Blocks', title: 'Settings', - source: 'packages/ui/src/mosaic/block/settings-group.tsx', + source: 'packages/ui/src/mosaic/block/settings.tsx', styleEngine: 'stylex', }; diff --git a/packages/ui/src/mosaic/block/settings-group.markers.stylex.ts b/packages/ui/src/mosaic/block/settings.markers.stylex.ts similarity index 100% rename from packages/ui/src/mosaic/block/settings-group.markers.stylex.ts rename to packages/ui/src/mosaic/block/settings.markers.stylex.ts diff --git a/packages/ui/src/mosaic/block/settings-group.test.tsx b/packages/ui/src/mosaic/block/settings.test.tsx similarity index 98% rename from packages/ui/src/mosaic/block/settings-group.test.tsx rename to packages/ui/src/mosaic/block/settings.test.tsx index fab787ec4e0..ece5f7a7df5 100644 --- a/packages/ui/src/mosaic/block/settings-group.test.tsx +++ b/packages/ui/src/mosaic/block/settings.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { describe, expect, it } from 'vitest'; import { colorVars } from '../tokens.stylex'; -import { Settings, settingsVars } from './settings-group'; +import { Settings, settingsVars } from './settings'; describe('Settings', () => { it('aliases its background to the global background token', () => { @@ -12,6 +12,7 @@ describe('Settings', () => { }); expect(settingsVars).toMatchObject({ '--cl-settings-background': 'var(--cl-settings-background)', + '--cl-settings-items-gap': 'var(--cl-settings-items-gap)', }); }); diff --git a/packages/ui/src/mosaic/block/settings-group.tsx b/packages/ui/src/mosaic/block/settings.tsx similarity index 97% rename from packages/ui/src/mosaic/block/settings-group.tsx rename to packages/ui/src/mosaic/block/settings.tsx index 2b53d3bc042..93be408d4cb 100644 --- a/packages/ui/src/mosaic/block/settings-group.tsx +++ b/packages/ui/src/mosaic/block/settings.tsx @@ -9,10 +9,10 @@ import { reset } from '../components/reset.styles'; import type { MosaicComponentProps } from '../props'; import { mergeStyleProps, themeProps } from '../props'; import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex'; -import { settingsItemsMarker } from './settings-group.markers.stylex'; -import { settingsVars } from './settings-group.vars.stylex'; +import { settingsItemsMarker } from './settings.markers.stylex'; +import { settingsVars } from './settings.vars.stylex'; -export { settingsVars } from './settings-group.vars.stylex'; +export { settingsVars } from './settings.vars.stylex'; export type SettingsRootProps = Omit, 'title'>; export type SettingsTitleProps = Omit; @@ -60,7 +60,7 @@ const styles = stylex.create({ [stylex.when.descendant('[data-nested]', settingsItemsMarker)]: space['1'], }, paddingBlockStart: space['4'], - rowGap: space['2'], + rowGap: settingsVars['--cl-settings-items-gap'], width: 'auto', }, items: { diff --git a/packages/ui/src/mosaic/block/settings-group.vars.stylex.ts b/packages/ui/src/mosaic/block/settings.vars.stylex.ts similarity index 63% rename from packages/ui/src/mosaic/block/settings-group.vars.stylex.ts rename to packages/ui/src/mosaic/block/settings.vars.stylex.ts index 750eb946e55..bbe5ba3485b 100644 --- a/packages/ui/src/mosaic/block/settings-group.vars.stylex.ts +++ b/packages/ui/src/mosaic/block/settings.vars.stylex.ts @@ -1,7 +1,8 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars } from '../tokens.stylex'; +import { colorVars, space } from '../tokens.stylex'; export const settingsVars = stylex.defineVars({ '--cl-settings-background': colorVars['--cl-color-background'], + '--cl-settings-items-gap': space['2'], }); diff --git a/packages/ui/src/mosaic/components/reset.test.tsx b/packages/ui/src/mosaic/components/reset.test.tsx index 3268e1877eb..e4aabc0782d 100644 --- a/packages/ui/src/mosaic/components/reset.test.tsx +++ b/packages/ui/src/mosaic/components/reset.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Settings } from '../block/settings-group'; +import { Settings } from '../block/settings'; import { Avatar } from './avatar'; import { Badge } from './badge'; import { Button } from './button'; diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index e3c1fad3b17..ad2f02247d4 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -6,7 +6,7 @@ export type { MosaicComponentProps, MosaicElementProps } from '../props'; -export { Settings, settingsVars } from '../block/settings-group'; +export { Settings, settingsVars } from '../block/settings'; export type { SettingsActionsProps, SettingsContentProps, @@ -20,7 +20,7 @@ export type { SettingsRootProps, SettingsRowProps, SettingsTitleProps, -} from '../block/settings-group'; +} from '../block/settings'; export { Avatar } from '../components/avatar'; export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from '../components/avatar'; From c3954e586a5b49a3f65305bcc446e7e005a30bbc Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 12 Aug 2026 08:45:46 -0400 Subject: [PATCH 5/5] refactor(ui): move Settings from block to component Relocates the Settings compound component out of `mosaic/block` into `mosaic/components/settings`, and moves its swingset docs entry from the Blocks layer to Components. --- .../swingset/src/components/DocsViewer.tsx | 2 +- packages/swingset/src/lib/registry.ts | 2 +- packages/swingset/src/stories/settings.mdx | 4 +-- .../swingset/src/stories/settings.stories.tsx | 6 ++-- .../ui/src/mosaic/components/reset.test.tsx | 2 +- .../src/mosaic/components/settings/index.ts | 15 +++++++++ .../settings}/settings.markers.stylex.ts | 0 .../settings}/settings.test.tsx | 2 +- .../settings}/settings.tsx | 14 ++++----- .../settings}/settings.vars.stylex.ts | 2 +- packages/ui/src/mosaic/styles/index.ts | 31 +++++++++---------- 11 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 packages/ui/src/mosaic/components/settings/index.ts rename packages/ui/src/mosaic/{block => components/settings}/settings.markers.stylex.ts (100%) rename packages/ui/src/mosaic/{block => components/settings}/settings.test.tsx (99%) rename packages/ui/src/mosaic/{block => components/settings}/settings.tsx (96%) rename packages/ui/src/mosaic/{block => components/settings}/settings.vars.stylex.ts (78%) diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index e0b8b276d02..679269b376d 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -29,7 +29,6 @@ const docModules: Record> = { }, blocks: { destructive: dynamic(() => import('../stories/destructive.mdx')), - settings: dynamic(() => import('../stories/settings.mdx')), }, components: { avatar: dynamic(() => import('../stories/avatar.mdx')), @@ -43,6 +42,7 @@ const docModules: Record> = { icon: dynamic(() => import('../stories/icon.mdx')), menu: dynamic(() => import('../stories/menu.component.mdx')), popover: dynamic(() => import('../stories/popover.component.mdx')), + settings: dynamic(() => import('../stories/settings.mdx')), tabs: dynamic(() => import('../stories/tabs.component.mdx')), text: dynamic(() => import('../stories/text.mdx')), field: dynamic(() => import('../stories/field.component.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 8a56b3aad47..6a69e33d0a8 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -283,7 +283,6 @@ export const registry: StoryModule[] = [ organizationProfileDeleteSectionModule, // Blocks destructiveModule, - settingsModule, // Components avatarModule, badgeModule, @@ -296,6 +295,7 @@ export const registry: StoryModule[] = [ iconModule, menuComponentModule, popoverComponentModule, + settingsModule, tabsComponentModule, textModule, fieldModule, diff --git a/packages/swingset/src/stories/settings.mdx b/packages/swingset/src/stories/settings.mdx index eba6b7ab384..dd3c149f737 100644 --- a/packages/swingset/src/stories/settings.mdx +++ b/packages/swingset/src/stories/settings.mdx @@ -2,12 +2,12 @@ import * as SettingsStories from './settings.stories'; # Settings -A settings block that carries the canonical section, surface, row grouping, and item layout. Use it for product settings while composing each item's media, content, and actions as needed. +A settings component that carries the canonical section, surface, row grouping, and item layout. Use it for product settings while composing each item's media, content, and actions as needed. ## Usage ```tsx -import { Settings } from '@clerk/ui/mosaic/block/settings'; +import { Settings } from '@clerk/ui/mosaic/components/settings'; Account diff --git a/packages/swingset/src/stories/settings.stories.tsx b/packages/swingset/src/stories/settings.stories.tsx index d2acb1613b9..11346f9593b 100644 --- a/packages/swingset/src/stories/settings.stories.tsx +++ b/packages/swingset/src/stories/settings.stories.tsx @@ -1,9 +1,9 @@ /** @jsxImportSource @emotion/react */ -import { Settings } from '@clerk/ui/mosaic/block/settings'; import { Avatar } from '@clerk/ui/mosaic/components/avatar'; import { Badge } from '@clerk/ui/mosaic/components/badge'; import { Button } from '@clerk/ui/mosaic/components/button'; import { Icon } from '@clerk/ui/mosaic/components/icon'; +import { Settings } from '@clerk/ui/mosaic/components/settings'; import type { StoryMeta } from '@/lib/types'; @@ -22,9 +22,9 @@ function ProviderIcon({ provider }: { provider: string }) { } export const meta: StoryMeta = { - group: 'Blocks', + group: 'Components', title: 'Settings', - source: 'packages/ui/src/mosaic/block/settings.tsx', + source: 'packages/ui/src/mosaic/components/settings/settings.tsx', styleEngine: 'stylex', }; diff --git a/packages/ui/src/mosaic/components/reset.test.tsx b/packages/ui/src/mosaic/components/reset.test.tsx index e4aabc0782d..5f84c5de704 100644 --- a/packages/ui/src/mosaic/components/reset.test.tsx +++ b/packages/ui/src/mosaic/components/reset.test.tsx @@ -3,7 +3,6 @@ import { render } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Settings } from '../block/settings'; import { Avatar } from './avatar'; import { Badge } from './badge'; import { Button } from './button'; @@ -14,6 +13,7 @@ import { Icon } from './icon'; import { Input } from './input'; import { Item } from './item'; import { reset } from './reset.styles'; +import { Settings } from './settings'; import { Text } from './text'; // StyleX generates the same atom for the same property+value across separate `create` calls, so a diff --git a/packages/ui/src/mosaic/components/settings/index.ts b/packages/ui/src/mosaic/components/settings/index.ts new file mode 100644 index 00000000000..e259553aa4d --- /dev/null +++ b/packages/ui/src/mosaic/components/settings/index.ts @@ -0,0 +1,15 @@ +export { Settings, settingsVars } from './settings'; +export type { + SettingsActionsProps, + SettingsContentProps, + SettingsDescriptionProps, + SettingsGroupProps, + SettingsItemProps, + SettingsItemsProps, + SettingsLabelProps, + SettingsMediaProps, + SettingsMediaSize, + SettingsRootProps, + SettingsRowProps, + SettingsTitleProps, +} from './settings'; diff --git a/packages/ui/src/mosaic/block/settings.markers.stylex.ts b/packages/ui/src/mosaic/components/settings/settings.markers.stylex.ts similarity index 100% rename from packages/ui/src/mosaic/block/settings.markers.stylex.ts rename to packages/ui/src/mosaic/components/settings/settings.markers.stylex.ts diff --git a/packages/ui/src/mosaic/block/settings.test.tsx b/packages/ui/src/mosaic/components/settings/settings.test.tsx similarity index 99% rename from packages/ui/src/mosaic/block/settings.test.tsx rename to packages/ui/src/mosaic/components/settings/settings.test.tsx index ece5f7a7df5..6c239b70a06 100644 --- a/packages/ui/src/mosaic/block/settings.test.tsx +++ b/packages/ui/src/mosaic/components/settings/settings.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { colorVars } from '../tokens.stylex'; +import { colorVars } from '../../tokens.stylex'; import { Settings, settingsVars } from './settings'; describe('Settings', () => { diff --git a/packages/ui/src/mosaic/block/settings.tsx b/packages/ui/src/mosaic/components/settings/settings.tsx similarity index 96% rename from packages/ui/src/mosaic/block/settings.tsx rename to packages/ui/src/mosaic/components/settings/settings.tsx index 93be408d4cb..7573f55100e 100644 --- a/packages/ui/src/mosaic/block/settings.tsx +++ b/packages/ui/src/mosaic/components/settings/settings.tsx @@ -3,12 +3,12 @@ import { useSafeLayoutEffect } from '@clerk/shared/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; -import type { HeadingProps } from '../components/heading'; -import { Heading } from '../components/heading'; -import { reset } from '../components/reset.styles'; -import type { MosaicComponentProps } from '../props'; -import { mergeStyleProps, themeProps } from '../props'; -import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex'; +import type { MosaicComponentProps } from '../../props'; +import { mergeStyleProps, themeProps } from '../../props'; +import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import type { HeadingProps } from '../heading'; +import { Heading } from '../heading'; +import { reset } from '../reset.styles'; import { settingsItemsMarker } from './settings.markers.stylex'; import { settingsVars } from './settings.vars.stylex'; @@ -359,7 +359,7 @@ const Actions = React.forwardRef(function }); /** - * A settings block that fixes section semantics, surface treatment, row grouping, + * A settings component that fixes section semantics, surface treatment, row grouping, * and item layout while leaving each item's content composable. */ export const Settings = { Root, Title, Group, Row, Items, Item, Media, Content, Label, Description, Actions }; diff --git a/packages/ui/src/mosaic/block/settings.vars.stylex.ts b/packages/ui/src/mosaic/components/settings/settings.vars.stylex.ts similarity index 78% rename from packages/ui/src/mosaic/block/settings.vars.stylex.ts rename to packages/ui/src/mosaic/components/settings/settings.vars.stylex.ts index bbe5ba3485b..fc417af7a38 100644 --- a/packages/ui/src/mosaic/block/settings.vars.stylex.ts +++ b/packages/ui/src/mosaic/components/settings/settings.vars.stylex.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, space } from '../tokens.stylex'; +import { colorVars, space } from '../../tokens.stylex'; export const settingsVars = stylex.defineVars({ '--cl-settings-background': colorVars['--cl-color-background'], diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index ad2f02247d4..a02ac9439ca 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -6,22 +6,6 @@ export type { MosaicComponentProps, MosaicElementProps } from '../props'; -export { Settings, settingsVars } from '../block/settings'; -export type { - SettingsActionsProps, - SettingsContentProps, - SettingsDescriptionProps, - SettingsGroupProps, - SettingsItemProps, - SettingsItemsProps, - SettingsLabelProps, - SettingsMediaProps, - SettingsMediaSize, - SettingsRootProps, - SettingsRowProps, - SettingsTitleProps, -} from '../block/settings'; - export { Avatar } from '../components/avatar'; export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from '../components/avatar'; export { Badge } from '../components/badge'; @@ -50,6 +34,21 @@ export type { } from '../components/menu'; export { scrollAreaRoot, scrollAreaVars, scrollAreaViewport } from '../components/scroll-area'; export type { ScrollAreaGutter } from '../components/scroll-area'; +export { Settings, settingsVars } from '../components/settings'; +export type { + SettingsActionsProps, + SettingsContentProps, + SettingsDescriptionProps, + SettingsGroupProps, + SettingsItemProps, + SettingsItemsProps, + SettingsLabelProps, + SettingsMediaProps, + SettingsMediaSize, + SettingsRootProps, + SettingsRowProps, + SettingsTitleProps, +} from '../components/settings'; export { Spinner } from '../components/spinner'; export type { SpinnerProps } from '../components/spinner'; export { Text, TextContext } from '../components/text';