Skip to content

Commit 4b11db4

Browse files
committed
feat(ui): add ConfigureSSO layout components
Adds three new top-level components under packages/ui/src/components/ ConfigureSSO/ that consume the wizard primitive and the breadcrumbs primitive: - ConfigureSSOLayout: ProfileCard shell with the navbar sidebar and a body Col that owns the flex sizing for the wizard's step output. - ConfigureSSOHeader: thin wrapper that drives the declarative Breadcrumbs primitive from the wizard's active steps + currentStep. - ConfigureSSOFooter: shared Previous / Continue footer that dispatches to the deepest mounted wizard via the FooterActions context registry. Nothing wires these in yet — that swap lands in a follow-up commit.
1 parent 7304fd6 commit 4b11db4

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Button, descriptors, Flex, Icon, useLocalizations } from '@/customizables';
2+
import { CaretLeft, CaretRight } from '@/icons';
3+
4+
import { useFooterActions, useWizard } from './elements/Wizard';
5+
6+
interface ConfigureSSOFooterProps {
7+
/** Override label for the Previous button */
8+
previousLabel?: string;
9+
/** Override label for the Continue button (also overridable per step via `useRegisterContinueAction({ label })`) */
10+
continueLabel?: string;
11+
/** Hides the Previous button entirely */
12+
hidePrevious?: boolean;
13+
/** Force-disables both Previous and Continue regardless of wizard state */
14+
isDisabled?: boolean;
15+
}
16+
17+
/**
18+
* Shared Previous / Continue footer for the ConfigureSSO surface.
19+
* Dispatches to the deepest mounted wizard so Previous from a nested
20+
* sub-step lands on its own previous sibling instead of jumping out
21+
* to the parent wizard's previous main step
22+
*/
23+
export const ConfigureSSOFooter = ({
24+
previousLabel = 'Previous',
25+
continueLabel = 'Continue',
26+
hidePrevious = false,
27+
isDisabled = false,
28+
}: ConfigureSSOFooterProps): JSX.Element => {
29+
const { isLoading } = useWizard();
30+
const { continueAction, deepestWizardRef } = useFooterActions();
31+
const { t } = useLocalizations();
32+
33+
const isForceDisabled = isDisabled || isLoading;
34+
const deepest = deepestWizardRef.current?.current;
35+
const isFirstStep = deepest?.isFirstStep ?? true;
36+
const isLastStep = deepest?.isLastStep ?? true;
37+
38+
const continueLabelToShow =
39+
typeof continueAction?.label === 'string'
40+
? continueAction.label
41+
: continueAction?.label
42+
? t(continueAction.label)
43+
: continueLabel;
44+
45+
const handleContinue = () => {
46+
if (continueAction?.handler) {
47+
void continueAction.handler();
48+
return;
49+
}
50+
void deepestWizardRef.current?.current.goNext();
51+
};
52+
53+
const handlePrevious = () => {
54+
void deepestWizardRef.current?.current.goPrev();
55+
};
56+
57+
return (
58+
<Flex
59+
elementDescriptor={descriptors.footer}
60+
align='center'
61+
justify='end'
62+
sx={theme => ({
63+
gap: theme.space.$2,
64+
padding: `${theme.space.$3} ${theme.space.$6}`,
65+
borderTopWidth: theme.borderWidths.$normal,
66+
borderTopStyle: theme.borderStyles.$solid,
67+
borderTopColor: theme.colors.$borderAlpha100,
68+
})}
69+
>
70+
{!hidePrevious && (
71+
<Button
72+
elementDescriptor={descriptors.configureSSOWizardFooterPreviousButton}
73+
variant='outline'
74+
size='sm'
75+
isDisabled={isForceDisabled || isFirstStep}
76+
onClick={handlePrevious}
77+
>
78+
<Icon
79+
icon={CaretLeft}
80+
size='sm'
81+
sx={theme => ({ marginInlineEnd: theme.space.$1 })}
82+
/>
83+
{previousLabel}
84+
</Button>
85+
)}
86+
<Button
87+
elementDescriptor={descriptors.configureSSOWizardFooterContinueButton}
88+
variant='solid'
89+
size='sm'
90+
isDisabled={isForceDisabled || continueAction?.isDisabled || isLastStep}
91+
isLoading={continueAction?.isLoading}
92+
onClick={handleContinue}
93+
>
94+
{continueLabelToShow}
95+
<Icon
96+
icon={CaretRight}
97+
size='sm'
98+
sx={theme => ({ marginInlineStart: theme.space.$1 })}
99+
/>
100+
</Button>
101+
</Flex>
102+
);
103+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Breadcrumbs } from './elements/Breadcrumbs';
2+
import { useWizard } from './elements/Wizard';
3+
4+
/**
5+
* Renders the wizard's active steps as a numbered breadcrumb. Sits
6+
* above the body in the ConfigureSSO layout. Reads navigation state
7+
* from `useWizard()` and feeds the data declaratively into the
8+
* `<Breadcrumbs>` primitive
9+
*/
10+
export const ConfigureSSOHeader = (): JSX.Element => {
11+
const { activeSteps, currentStep, goToStep } = useWizard();
12+
13+
return (
14+
<Breadcrumbs
15+
currentId={currentStep?.id}
16+
onItemClick={id => {
17+
void goToStep(id);
18+
}}
19+
>
20+
{activeSteps.map(step => (
21+
<Breadcrumbs.Item
22+
key={step.id}
23+
id={step.id}
24+
label={step.label}
25+
isCompleted={step.isCompleted}
26+
/>
27+
))}
28+
</Breadcrumbs>
29+
);
30+
};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useOrganization } from '@clerk/shared/react';
2+
import React, { type PropsWithChildren } from 'react';
3+
4+
import { useEnvironment } from '@/contexts';
5+
import { Box, Col, descriptors, Flex, Icon, localizationKeys, Text, useAppearance } from '@/customizables';
6+
import { ApplicationLogo } from '@/elements/ApplicationLogo';
7+
import { NavBar, NavbarContextProvider } from '@/elements/Navbar';
8+
import { ProfileCard } from '@/elements/ProfileCard';
9+
import { BoxIcon } from '@/icons';
10+
11+
/**
12+
* Visual shell for the ConfigureSSO surface — ProfileCard with the
13+
* navbar sidebar and a body content area. Children render inside the
14+
* body Col with flex sizing so the wizard / pre-wizard gates can fill
15+
* the available space without needing their own sizing chrome
16+
*/
17+
export const ConfigureSSOLayout = ({ children }: PropsWithChildren): JSX.Element => {
18+
const contentRef = React.useRef<HTMLDivElement>(null);
19+
const { applicationName, logoImageUrl } = useEnvironment().displayConfig;
20+
const { organizationSettings } = useEnvironment();
21+
const { parsedOptions } = useAppearance();
22+
const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl);
23+
24+
return (
25+
<ProfileCard.Root
26+
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
27+
>
28+
<NavbarContextProvider contentRef={contentRef}>
29+
<NavBar
30+
header={
31+
<Flex
32+
align='center'
33+
sx={t => ({
34+
gap: t.space.$2,
35+
padding: `${t.space.$none} ${t.space.$3}`,
36+
maxWidth: '100%',
37+
})}
38+
>
39+
{hasLogo ? (
40+
<ApplicationLogo
41+
sx={t => ({ width: t.space.$9, height: t.space.$9, borderRadius: t.radii.$md, overflow: 'hidden' })}
42+
/>
43+
) : (
44+
<Box
45+
sx={t => ({
46+
width: t.space.$9,
47+
height: t.space.$9,
48+
flexShrink: 0,
49+
borderRadius: t.radii.$md,
50+
backgroundColor: t.colors.$primary500,
51+
color: t.colors.$colorPrimaryForeground,
52+
display: 'flex',
53+
alignItems: 'center',
54+
justifyContent: 'center',
55+
})}
56+
aria-hidden
57+
>
58+
<Icon
59+
icon={BoxIcon}
60+
sx={t => ({ width: t.sizes.$4, height: t.sizes.$4 })}
61+
/>
62+
</Box>
63+
)}
64+
65+
<Col sx={{ minWidth: 0 }}>
66+
<Text
67+
as='p'
68+
truncate
69+
>
70+
{applicationName}
71+
</Text>
72+
{organizationSettings.enabled && <OrganizationSidebarSubtitle />}
73+
</Col>
74+
</Flex>
75+
}
76+
titleSx={t => ({ fontSize: t.fontSizes.$lg })}
77+
title={localizationKeys('configureSSO.navbar.title')}
78+
routes={[]}
79+
contentRef={contentRef}
80+
/>
81+
<Col
82+
ref={contentRef}
83+
elementDescriptor={descriptors.scrollBox}
84+
sx={t => ({
85+
backgroundColor: t.colors.$colorBackground,
86+
position: 'relative',
87+
borderRadius: t.radii.$lg,
88+
width: '100%',
89+
overflow: 'hidden',
90+
borderWidth: t.borderWidths.$normal,
91+
borderStyle: t.borderStyles.$solid,
92+
borderColor: t.colors.$borderAlpha150,
93+
marginBlock: '-1px',
94+
marginInlineEnd: '-1px',
95+
flex: 1,
96+
})}
97+
>
98+
<Col sx={{ flex: 1, minHeight: 0 }}>{children}</Col>
99+
</Col>
100+
</NavbarContextProvider>
101+
</ProfileCard.Root>
102+
);
103+
};
104+
105+
const OrganizationSidebarSubtitle = (): JSX.Element | null => {
106+
const { organization } = useOrganization();
107+
108+
if (!organization) {
109+
return null;
110+
}
111+
112+
return (
113+
<Text
114+
as='span'
115+
truncate
116+
sx={t => ({ color: t.colors.$colorMutedForeground })}
117+
>
118+
{organization?.name}
119+
</Text>
120+
);
121+
};

0 commit comments

Comments
 (0)