(function Profile
styles.layout,
inline && styles.layoutInline,
dialog !== null && !inline && styles.layoutInDialog,
+ fullscreen && styles.layoutFullscreen,
),
)}
>
@@ -209,6 +230,32 @@ type NavItemMode = 'tab' | 'option';
// `Profile.Nav` children render again as the select's options.
const NavItemModeContext = React.createContext
('tab');
+function BackButton() {
+ const m = useMessages('profile');
+ return (
+
+
+
+
+
+ {m.backToApp}
+
+
+ );
+}
+
function NavBranding() {
return (
@@ -225,8 +272,18 @@ const Nav = React.forwardRef(function ProfileNav(
{ children, render, xstyle, ...rest },
ref,
) {
- const { titleId, renderBranding, compact, navLayout, navOpen, closeNav, setNavItems, navTriggerRef, inline } =
- useProfileContext('Profile.Nav');
+ const {
+ titleId,
+ renderBranding,
+ compact,
+ navLayout,
+ navOpen,
+ closeNav,
+ setNavItems,
+ navTriggerRef,
+ inline,
+ dismiss,
+ } = useProfileContext('Profile.Nav');
useSafeLayoutEffect(() => {
setNavItems(children);
}, [children, setNavItems]);
@@ -244,6 +301,7 @@ const Nav = React.forwardRef(function ProfileNav(
),
children: (
<>
+ {dismiss === 'back' ? : null}
{children}
diff --git a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
index d26c79c8c8d..52dcbb5d6f2 100644
--- a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
+++ b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import { expect, it, vi } from 'vitest';
+import { Dialog } from '../../../components/dialog';
import { MosaicProvider } from '../../../mosaic-provider';
import { OrganizationProfileView } from '../organization-profile.view';
@@ -37,3 +38,22 @@ it('warns when there are no pages to show', () => {
expect(warn).toHaveBeenCalledWith('[Clerk] OrganizationProfile has no pages to show.');
warn.mockRestore();
});
+
+it('replaces the corner dismiss with a back button in a fullscreen dialog', () => {
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toContainElement(screen.getByRole('button', { name: 'Back to app' }));
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
+});
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile.view.test.tsx
index 97bd72979ac..b6d7eaf4836 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile.view.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile.view.test.tsx
@@ -153,6 +153,35 @@ describe('UserProfileView', () => {
expect(popup).toContainElement(screen.getByRole('tab', { name: 'Security' }));
});
+ it('replaces the close button with a back button in a fullscreen dialog', async () => {
+ const onOpenChange = vi.fn();
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ const popup = screen.getByRole('dialog', { name: 'User profile' });
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
+ const back = screen.getByRole('button', { name: 'Back to app' });
+ expect(popup).toContainElement(back);
+
+ await userEvent.setup().click(back);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false, expect.anything());
+ });
+
it('warns when there are no pages to show', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
diff --git a/packages/mosaic/src/localization/registry.ts b/packages/mosaic/src/localization/registry.ts
index be29ada81c3..762e0927a02 100644
--- a/packages/mosaic/src/localization/registry.ts
+++ b/packages/mosaic/src/localization/registry.ts
@@ -1,4 +1,5 @@
import { formMessages } from '../components/form/form.messages';
+import { profileMessages } from '../components/profile/profile.messages';
import { apiKeysTableMessages } from '../features/api-keys/api-keys-table.messages';
import { invitationsTableTabMessages } from '../features/organization-profile/invitations-table-tab.messages';
import { membersTableTabMessages } from '../features/organization-profile/members-table-tab.messages';
@@ -43,6 +44,7 @@ export const mosaicMessages = {
organizationProfileDangerSection: organizationProfileDangerSectionMessages,
organizationProfileProfileSection: organizationProfileProfileSectionMessages,
organizationProfileInviteMembers: organizationProfileInviteMembersMessages,
+ profile: profileMessages,
reverification: reverificationMessages,
userButton: userButtonMessages,
userProfile: userProfileMessages,
diff --git a/packages/swingset/src/stories/dialog.component.mdx b/packages/swingset/src/stories/dialog.component.mdx
index 14b5403d9db..649bdd2f7b4 100644
--- a/packages/swingset/src/stories/dialog.component.mdx
+++ b/packages/swingset/src/stories/dialog.component.mdx
@@ -69,10 +69,11 @@ committing.
### Variant
-| Value | Width | For |
-| --------- | ------------------------------------------ | -------------------------------------------------------------------- |
-| `card` | Width from its `Card`, height from content | Everything a `Card` holds — a flow, a confirmation, a form (default) |
-| `profile` | Width from its surface, fills the height | Account profile and settings — a surface you navigate |
+| Value | Width | For |
+| ------------ | ------------------------------------------- | -------------------------------------------------------------------- |
+| `card` | Width from its `Card`, height from content | Everything a `Card` holds — a flow, a confirmation, a form (default) |
+| `profile` | Width from its surface, fills the height | Account profile and settings — a surface you navigate |
+| `fullscreen` | Fills the viewport at every width, no inset | The same profile surface taking the whole screen |
### Every dialog brings its own surface
@@ -107,6 +108,10 @@ import { UserProfileView } from '@clerk/mosaic/features/user-profile/user-profil
;
```
+`fullscreen` holds the same `Profile` and fills the viewport at every width, with no inset around
+it. The profile replaces its corner dismiss with a "Back to app" row above the navigation; below
+`48rem`, where the navigation collapses into the page title, the corner dismiss returns.
+
### Compact placement
In the compact band, a window under `40rem`, a card is a bottom sheet: flush to the sides and the
@@ -122,7 +127,7 @@ bottom edge, sliding up from it. `compactPlacement='center'` keeps it centered t
| `sheet` | Bottom-anchored, slides up (default) | Centered |
| `center` | Centered | Centered |
-A `profile` takes no placement and warns in dev if given one.
+A `profile` and a `fullscreen` take no placement and warn in dev if given one.
### Alert dialogs
@@ -251,14 +256,15 @@ State attributes:
A dialog opened inside another is one of two relationships, decided by the variant beneath it:
-| | Nested | Stack |
-| ------------------- | -------------------------- | ---------------------------------- |
-| Opens over | a `profile` | a `card` |
-| Its own scrim | Yes, lighter than the base | None — one scrim serves the stack |
-| The surface beneath | Unchanged | Dims and recedes |
-| Attributes | `data-nested` | `data-stacked` / `data-stack-base` |
+| | Nested | Stack |
+| ------------------- | --------------------------- | ---------------------------------- |
+| Opens over | a `profile` or `fullscreen` | a `card` |
+| Its own scrim | Yes, lighter than the base | None — one scrim serves the stack |
+| The surface beneath | Unchanged | Dims and recedes |
+| Attributes | `data-nested` | `data-stacked` / `data-stack-base` |
-A `profile` never opens inside another dialog — it warns in dev. A `card` is what opens over one.
+A `profile` and a `fullscreen` never open inside another dialog — they warn in dev. A `card` is
+what opens over one.
---
diff --git a/packages/swingset/src/stories/dialog.component.stories.tsx b/packages/swingset/src/stories/dialog.component.stories.tsx
index b5ae406bad0..8d455a6fa66 100644
--- a/packages/swingset/src/stories/dialog.component.stories.tsx
+++ b/packages/swingset/src/stories/dialog.component.stories.tsx
@@ -24,7 +24,7 @@ export const meta: StoryMeta = {
source: 'packages/mosaic/src/components/dialog/dialog.tsx',
styles: {
_variants: {
- variant: { card: {}, profile: {} },
+ variant: { card: {}, profile: {}, fullscreen: {} },
compactPlacement: { center: {}, sheet: {} },
},
_defaultVariants: {
diff --git a/packages/swingset/src/stories/organization-profile.mdx b/packages/swingset/src/stories/organization-profile.mdx
index 19d70d381d3..194739d8749 100644
--- a/packages/swingset/src/stories/organization-profile.mdx
+++ b/packages/swingset/src/stories/organization-profile.mdx
@@ -26,3 +26,13 @@ and animates the popup; the profile paints it, names it, and carries the dismiss
name='Overlay'
storyModule={Stories}
/>
+
+## As a full screen overlay
+
+The same view inside a `fullscreen` dialog, which fills the viewport. A back button above the
+navigation dismisses it in place of the corner close button.
+
+
diff --git a/packages/swingset/src/stories/organization-profile.stories.tsx b/packages/swingset/src/stories/organization-profile.stories.tsx
index c62fdbfe65a..5b6ebcbb853 100644
--- a/packages/swingset/src/stories/organization-profile.stories.tsx
+++ b/packages/swingset/src/stories/organization-profile.stories.tsx
@@ -51,3 +51,14 @@ export function Overlay() {
);
}
+
+export function Fullscreen() {
+ return (
+
+ }>Manage organization
+
+
+
+
+ );
+}
diff --git a/packages/swingset/src/stories/profile.component.mdx b/packages/swingset/src/stories/profile.component.mdx
index bac6a1c6376..9668b920fa7 100644
--- a/packages/swingset/src/stories/profile.component.mdx
+++ b/packages/swingset/src/stories/profile.component.mdx
@@ -5,7 +5,8 @@ import * as ProfileStories from './profile.component.stories';
A surface you navigate: a column of destinations beside the page each one opens. The user profile
and the organization profile are both one of these — `UserProfileView` composes it from the pages an
instance has content for. Rendered as the content of a `profile` dialog it names the dialog, carries
-its dismiss, and fills the popup; standalone it paints the same frame and takes its own height.
+its dismiss, and fills the popup; in a `fullscreen` dialog it fills the viewport and moves its
+dismiss into the navigation; standalone it paints the same frame and takes its own height.
## Playground
@@ -71,6 +72,7 @@ reaches the page title instead.
| `Profile.Root` | `div` (the container) | `cl-profile` |
| — | `span` (query sentinel) | `cl-profile-sentinel` |
| `Profile.Nav` | `nav` (labelled) | `cl-profile-nav` |
+| — | back `button` | `cl-profile-back-button` |
| — | tablist | `cl-profile-nav-list` |
| `Profile.NavItem` | `button[role=tab]` | `cl-profile-nav-item` |
| `Profile.Content` | `div` (scroll region) | `cl-profile-content` |
@@ -81,6 +83,10 @@ reaches the page title instead.
`Content` is a plain `div`, not a `main`: the profile is usually the content of the host's own
`main`, or of a dialog. While compact, `ContentPanel` is a `div[role=group]` named by the page title.
+The back `button` renders only inside a `fullscreen` dialog, where it replaces `Dialog.CloseButton`.
+It is first in the navigation, sized like a `NavItem`, and its label is the `profile.backToApp`
+message. Below `48rem` the corner dismiss returns and it does not render.
+
## Heading levels
The profile's headings follow the nearest `HeadingLevelProvider`. `Profile.Title` renders at the
diff --git a/packages/swingset/src/stories/user-profile.mdx b/packages/swingset/src/stories/user-profile.mdx
index 5fd35d80134..c9277186ba3 100644
--- a/packages/swingset/src/stories/user-profile.mdx
+++ b/packages/swingset/src/stories/user-profile.mdx
@@ -28,3 +28,13 @@ and animates the popup; the profile paints it, names it, and carries the dismiss
name='Overlay'
storyModule={Stories}
/>
+
+## As a full screen overlay
+
+The same view inside a `fullscreen` dialog, which fills the viewport. A back button above the
+navigation dismisses it in place of the corner close button.
+
+
diff --git a/packages/swingset/src/stories/user-profile.stories.tsx b/packages/swingset/src/stories/user-profile.stories.tsx
index 04a8034c036..0ef8a86a063 100644
--- a/packages/swingset/src/stories/user-profile.stories.tsx
+++ b/packages/swingset/src/stories/user-profile.stories.tsx
@@ -53,3 +53,19 @@ export function Overlay() {
);
}
+
+export function Fullscreen() {
+ const { activePage, setActivePage, pages } = useUserProfileFixture();
+ return (
+
+ }>Manage account
+
+
+
+
+ );
+}