From 966efc37417dba1b752bea0294e43bec15bd9d2a Mon Sep 17 00:00:00 2001 From: Max Yinger Date: Thu, 24 Sep 2026 13:20:46 -0600 Subject: [PATCH] feat(mosaic): flat user profile story, single-card active devices, password header Add a FlatContainedGroups UserProfile story that restyles every section group as a tinted containing box. Active devices now lists every device as a row in one surface with a ghost sign-out-all button beside the title, and Password renders as a surface header with its action on the value row. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../user-profile-security-panel.view.test.tsx | 5 +- ...er-profile-active-devices-section.view.tsx | 73 ++++++---------- .../user-profile-active-devices.messages.ts | 3 - .../user-profile-password-row.view.tsx | 42 +++++---- .../user-profile-password-section.view.tsx | 5 +- .../user-profile-security-panel.styles.ts | 6 ++ .../swingset/src/stories/user-profile.mdx | 10 +++ .../src/stories/user-profile.stories.tsx | 87 +++++++++++++++++++ 8 files changed, 160 insertions(+), 71 deletions(-) diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx index c0c7b3ab75b..2eabf8a18dd 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx @@ -70,7 +70,7 @@ describe('UserProfileSecurityPanelView', () => { expect(screen.getByRole('group', { name: 'Passkeys' })).toBeInTheDocument(); expect(screen.getByRole('group', { name: '2-step verification' })).toBeInTheDocument(); expect(screen.getByText('This device')).toBeInTheDocument(); - expect(screen.getByText('2 other devices')).toBeInTheDocument(); + expect(screen.getByText('Clerk App on macOS')).toBeInTheDocument(); expect( screen.getByText('Permanently delete this account and all its data. This cannot be undone.'), ).toBeInTheDocument(); @@ -131,8 +131,7 @@ describe('UserProfileSecurityPanelView', () => { await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove', exact: true })); await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); - const otherDevices = screen.getByRole('group', { name: 'Other devices' }); - await user.click(within(otherDevices).getByRole('button', { name: 'Manage Safari on iOS' })); + await user.click(screen.getByRole('button', { name: 'Manage Safari on iOS' })); await user.click(screen.getByRole('menuitem', { name: 'Sign out' })); await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Sign out' })); await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section.view.tsx index 1be9f5b1369..70d1c50b48c 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section.view.tsx @@ -73,9 +73,9 @@ export function UserProfileActiveDevicesSectionView({ } }; - // Confirming takes the whole card with it, trigger included — hence the dialog mounted outside - // it, and the current device as the place focus lands. Cancelling keeps the trigger, so focus - // goes back to it. + // Confirming takes the trigger with the other devices — hence the dialog mounted outside it, and + // the current device as the place focus lands. Cancelling keeps the trigger, so focus goes back + // to it. const focusAfterSignOutAll = () => { const signedOut = signedOutAll.current; signedOutAll.current = false; @@ -86,7 +86,20 @@ export function UserProfileActiveDevicesSectionView({
- {m.title} +
+ {m.title} + {onSignOutAllOtherDevices && otherDevices.length > 0 ? ( + + ) : null} +
{currentDevices.length > 0 ? ( currentDevices.map(device => ( @@ -107,50 +120,18 @@ export function UserProfileActiveDevicesSectionView({ )} + {otherDevices.map(device => ( + + deviceDetails.open(device)} + /> + + ))}
- {otherDevices.length > 0 ? ( - - - - - - {fill(otherDevices.length === 1 ? m.otherDevice : m.otherDevices, { - count: String(otherDevices.length), - })} - - - {onSignOutAllOtherDevices ? ( - - - - ) : null} - - - {otherDevices.map(device => ( - deviceDetails.open(device)} - /> - ))} - - - - ) : null}
{onSignOutAllOtherDevices ? ( ) { const m = useMessages('userProfilePasswordSection'); return ( - - + <> + {m.label} - {hasPassword ? m.masked : m.noPasswordSet} - {managedBy ? ( - - - - ) : onSubmitPassword ? ( - - - - ) : null} - - + + + + + {hasPassword ? m.masked : m.noPasswordSet} + + {managedBy ? ( + + + + ) : onSubmitPassword ? ( + + + + ) : null} + + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx index f0c0099cc4a..eb411405887 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx @@ -25,7 +25,10 @@ export function UserProfilePasswordSectionView({ } const group = ( - + {title ? {title} : null} + +## Flat contained groups + +Every contained group restyled flat from external CSS, using the same selectors as the Section +`FlatContainedGroups` example. + + diff --git a/packages/swingset/src/stories/user-profile.stories.tsx b/packages/swingset/src/stories/user-profile.stories.tsx index 04a8034c036..8ed0b330b12 100644 --- a/packages/swingset/src/stories/user-profile.stories.tsx +++ b/packages/swingset/src/stories/user-profile.stories.tsx @@ -53,3 +53,90 @@ export function Overlay() { ); } + +export function FlatContainedGroups() { + const { activePage, setActivePage, pages } = useUserProfileFixture(); + return ( + <> + + + + ); +}