1- import { createContextAndHook , useClerk } from '@clerk/shared/react' ;
1+ import { useClerk } from '@clerk/shared/react' ;
22import type { __internal_EnableOrganizationsPromptProps , EnableEnvironmentSettingParams } from '@clerk/shared/types' ;
33// eslint-disable-next-line no-restricted-imports
44import type { SerializedStyles } from '@emotion/react' ;
55// eslint-disable-next-line no-restricted-imports
66import { css } from '@emotion/react' ;
7- import React , { forwardRef , useId , useLayoutEffect , useRef , useState } from 'react' ;
7+ import { type ComponentProps , forwardRef , useId , useLayoutEffect , useRef , useState } from 'react' ;
88
99import { useEnvironment } from '@/ui/contexts' ;
1010import { Modal } from '@/ui/elements/Modal' ;
1111import { InternalThemeProvider } from '@/ui/styledSystem' ;
1212
13- import { Flex } from '../../../customizables' ;
13+ import { Col , Flex } from '../../../customizables' ;
1414import { Portal } from '../../../elements/Portal' ;
15+ import { RadioInput } from '../../../primitives' ;
1516import { basePromptElementStyles , ClerkLogoIcon , PromptContainer , PromptSuccessIcon } from '../shared' ;
1617
1718const organizationsDashboardUrl = 'https://dashboard.clerk.com/~/organizations-settings' ;
1819
20+ const RADIO_INPUT_SIZE = '1rem' ;
21+ const RADIO_GAP = '0.5rem' ;
22+
23+ const radioOptions = [
24+ {
25+ value : 'require' ,
26+ label : 'Require organization membership' ,
27+ description : 'Users need to belong to at least one organization.' ,
28+ } ,
29+ {
30+ value : 'allow' ,
31+ label : 'Allow personal accounts' ,
32+ description : 'Users can work outside of an organization with a personal account' ,
33+ } ,
34+ ] ;
35+
1936const EnableOrganizationsPromptInternal = ( {
2037 caller,
2138 onSuccess,
@@ -24,11 +41,11 @@ const EnableOrganizationsPromptInternal = ({
2441 const clerk = useClerk ( ) ;
2542 const [ isLoading , setIsLoading ] = useState ( false ) ;
2643 const [ isEnabled , setIsEnabled ] = useState ( false ) ;
27- const [ allowPersonalAccount , setAllowPersonalAccount ] = useState ( false ) ;
44+ const [ membershipOption , setMembershipOption ] = useState < 'require' | 'allow' > ( 'require' ) ;
2845
46+ const radioGroupName = useId ( ) ;
2947 const initialFocusRef = useRef < HTMLHeadingElement > ( null ) ;
3048 const environment = useEnvironment ( ) ;
31- const radioGroupLabelId = useId ( ) ;
3249
3350 const isComponent = ! caller . startsWith ( 'use' ) ;
3451
@@ -44,7 +61,7 @@ const EnableOrganizationsPromptInternal = ({
4461 } ;
4562
4663 if ( hasPersonalAccountsEnabled ) {
47- params . organization_allow_personal_accounts = allowPersonalAccount ;
64+ params . organization_allow_personal_accounts = membershipOption === 'allow' ;
4865 }
4966
5067 void environment
@@ -141,7 +158,6 @@ const EnableOrganizationsPromptInternal = ({
141158 ) : (
142159 < >
143160 < p
144- id = { radioGroupLabelId }
145161 css = { [
146162 basePromptElementStyles ,
147163 css `
@@ -180,32 +196,82 @@ const EnableOrganizationsPromptInternal = ({
180196 </ Flex >
181197
182198 { hasPersonalAccountsEnabled && (
183- < Flex
199+ < Col
200+ role = 'radiogroup'
201+ gap = { 3 }
184202 sx = { t => ( { marginTop : t . sizes . $2 } ) }
185- direction = 'col'
186203 >
187- < RadioGroup
188- value = { allowPersonalAccount ? 'allow' : 'require' }
189- onChange = { value => setAllowPersonalAccount ( value === 'allow' ) }
190- labelledBy = { radioGroupLabelId }
191- >
192- < RadioGroupItem
193- value = 'require'
194- label = {
195- < Flex gap = { 2 } >
196- < span > Require organization membership</ span >
197- < PromptBadge > Standard</ PromptBadge >
198- </ Flex >
199- }
200- description = 'Users need to belong to at least one organization.'
201- />
202- < RadioGroupItem
203- value = 'allow'
204- label = 'Allow personal accounts'
205- description = 'Users can work outside of an organization with a personal account'
206- />
207- </ RadioGroup >
208- </ Flex >
204+ { radioOptions . map ( option => {
205+ const optionId = `${ radioGroupName } -${ option . value } ` ;
206+ const descriptionId = `${ optionId } -description` ;
207+ return (
208+ < Flex
209+ key = { option . value }
210+ direction = 'col'
211+ gap = { 1 }
212+ >
213+ < label
214+ htmlFor = { optionId }
215+ css = { css `
216+ ${ basePromptElementStyles } ;
217+ dis play: flex;
218+ align- items: flex- start;
219+ gap: ${ RADIO_GAP } ;
220+ cursor : pointer;
221+ user- select: none;
222+ ` }
223+ >
224+ < RadioInput
225+ id = { optionId }
226+ name = { radioGroupName }
227+ value = { option . value }
228+ checked = { membershipOption === option . value }
229+ onChange = { ( ) => setMembershipOption ( option . value as 'require' | 'allow' ) }
230+ aria-describedby = { option . description ? descriptionId : undefined }
231+ css = { css `
232+ width: ${ RADIO_INPUT_SIZE } ;
233+ height: ${ RADIO_INPUT_SIZE } ;
234+ margin- to p: 0.125rem;
235+ flex- shrink: 0;
236+ ` }
237+ />
238+
239+ < span
240+ css = { [
241+ basePromptElementStyles ,
242+ css `
243+ font- size: 0.875rem;
244+ font- weight: 500;
245+ line-height: 1.25;
246+ color : white;
247+ ` ,
248+ ] }
249+ >
250+ { option . label }
251+ </ span >
252+ </ label >
253+
254+ { option . description && (
255+ < span
256+ id = { descriptionId }
257+ css = { [
258+ basePromptElementStyles ,
259+ css `
260+ padding- inline-start: calc(${ RADIO_INPUT_SIZE } + ${ RADIO_GAP } );
261+ font- size: 0.75rem;
262+ line-height: 1.33;
263+ color : # c3c3c6 ;
264+ text- wrap: pretty;
265+ ` ,
266+ ] }
267+ >
268+ { option . description }
269+ </ span >
270+ ) }
271+ </ Flex >
272+ ) ;
273+ } ) }
274+ </ Col >
209275 ) }
210276 </ Flex >
211277
@@ -353,7 +419,7 @@ const buttonVariantStyles = {
353419
354420type PromptButtonVariant = keyof typeof buttonVariantStyles ;
355421
356- type PromptButtonProps = Pick < React . ComponentProps < 'button' > , 'onClick' | 'children' | 'disabled' > & {
422+ type PromptButtonProps = Pick < ComponentProps < 'button' > , 'onClick' | 'children' | 'disabled' > & {
357423 variant ?: PromptButtonVariant ;
358424} ;
359425
@@ -368,208 +434,7 @@ const PromptButton = forwardRef<HTMLButtonElement, PromptButtonProps>(({ variant
368434 ) ;
369435} ) ;
370436
371- type PromptBadgeProps = {
372- children : React . ReactNode ;
373- } ;
374-
375- const PromptBadge = ( { children } : PromptBadgeProps ) : JSX . Element => {
376- return (
377- < span
378- css = { css `
379- ${ basePromptElementStyles } ;
380- dis play: inline-flex;
381- align- items: center;
382- padding: 0.125rem 0.375rem;
383- bor der- radius: 0.25rem;
384- font- size: 0.6875rem;
385- font- weight: 500;
386- line-height: 1.23;
387- background- color : # ebebeb ;
388- color : # 2b2b34;
389- ` }
390- >
391- { children }
392- </ span >
393- ) ;
394- } ;
395-
396- type RadioGroupContextValue = {
397- name : string ;
398- value : string ;
399- onChange : ( value : string ) => void ;
400- } ;
401-
402- const [ RadioGroupContext , useRadioGroup ] = createContextAndHook < RadioGroupContextValue > ( 'RadioGroupContext' ) ;
403-
404- type RadioGroupProps = {
405- value : string ;
406- onChange : ( value : string ) => void ;
407- children : React . ReactNode ;
408- labelledBy ?: string ;
409- } ;
410-
411- const RadioGroup = ( { value, onChange, children, labelledBy } : RadioGroupProps ) : JSX . Element => {
412- const name = useId ( ) ;
413- const contextValue = React . useMemo ( ( ) => ( { value : { name, value, onChange } } ) , [ name , value , onChange ] ) ;
414-
415- return (
416- < RadioGroupContext . Provider value = { contextValue } >
417- < Flex
418- role = 'radiogroup'
419- direction = 'col'
420- gap = { 3 }
421- aria-orientation = 'vertical'
422- aria-labelledby = { labelledBy }
423- >
424- { children }
425- </ Flex >
426- </ RadioGroupContext . Provider >
427- ) ;
428- } ;
429-
430- type RadioGroupItemProps = {
431- value : string ;
432- label : React . ReactNode ;
433- description ?: React . ReactNode ;
434- } ;
435-
436- const RADIO_INDICATOR_SIZE = '1rem' ;
437- const RADIO_GAP = '0.5rem' ;
438-
439- const RadioGroupItem = ( { value, label, description } : RadioGroupItemProps ) : JSX . Element => {
440- const { name, value : selectedValue , onChange } = useRadioGroup ( ) ;
441- const descriptionId = useId ( ) ;
442- const checked = value === selectedValue ;
443-
444- return (
445- < Flex
446- direction = 'col'
447- gap = { 1 }
448- >
449- < label
450- css = { css `
451- ${ basePromptElementStyles } ;
452- dis play: flex;
453- align- items: flex- start;
454- gap: ${ RADIO_GAP } ;
455- cursor : pointer;
456- user- select: none;
457-
458- & : has (input : focus-visible ) > span : first-of-type {
459- outline : 2px solid white;
460- outline-offset : 2px ;
461- }
462-
463- & : hover : has (input : not (: checked )) > span : first-of-type {
464- background-color : rgba (255 , 255 , 255 , 0.08 );
465- }
466-
467- & : hover : has (input : checked ) > span : first-of-type {
468- background-color : rgba (108 , 71 , 255 , 0.8 );
469- background-color : color-mix (in srgb, # 6c47ff 80% , transparent);
470- }
471- ` }
472- >
473- < input
474- type = 'radio'
475- name = { name }
476- value = { value }
477- checked = { checked }
478- onChange = { ( ) => onChange ( value ) }
479- aria-describedby = { description ? descriptionId : undefined }
480- css = { css `
481- ${ basePromptElementStyles } ;
482- position: absolute;
483- width: 1px;
484- height: 1px;
485- padding: 0;
486- margin: -1px;
487- overflow: hidden;
488- clip: rect(0, 0, 0, 0);
489- white-space: nowrap;
490- bor der- width: 0;
491- ` }
492- />
493-
494- < span
495- aria-hidden = 'true'
496- css = { css `
497- ${ basePromptElementStyles } ;
498- position: relative;
499- dis play: inline-flex;
500- align- items: center;
501- justify- content: center;
502- width: ${ RADIO_INDICATOR_SIZE } ;
503- height: ${ RADIO_INDICATOR_SIZE } ;
504- margin- to p: 0.125rem;
505- flex- shrink: 0;
506- bor der- radius: 50%;
507- bor der: 1px solid rgba(255, 255, 255, 0.3);
508- background- color : transparent;
509- transition: 120ms ease- in- out;
510- transition- property: bor der- color , background- color , box- shadow;
511-
512- ${ checked &&
513- css `
514- bor der- width: 2px;
515- bor der- color : # 6c47ff;
516- background- color : # 6c47ff;
517- background- color : color - mix(in srgb, # 6c47ff 100%, transparent);
518- box- shadow: 0 0 0 2px rgba(108, 71, 255, 0.2);
519- ` }
520-
521- & ::after {
522- content : '' ;
523- position : absolute;
524- width : 0.375rem ;
525- height : 0.375rem ;
526- border-radius : 50% ;
527- background-color : white;
528- opacity : ${ checked ? 1 : 0 } ;
529- transform : scale (${ checked ? 1 : 0 } );
530- transition : 120ms ease-in-out;
531- transition-property : opacity, transform;
532- }
533- ` }
534- />
535-
536- < span
537- css = { [
538- basePromptElementStyles ,
539- css `
540- font-size : 0.875rem ;
541- font-weight : 500 ;
542- line-height : 1.25 ;
543- color : white;
544- ` ,
545- ] }
546- >
547- { label }
548- </ span >
549- </ label >
550-
551- { description && (
552- < span
553- id = { descriptionId }
554- css = { [
555- basePromptElementStyles ,
556- css `
557- padding-inline-start : calc (${ RADIO_INDICATOR_SIZE } + ${ RADIO_GAP } );
558- font-size : 0.75rem ;
559- line-height : 1.33 ;
560- color : # c3c3c6 ;
561- text-wrap : pretty;
562- ` ,
563- ] }
564- >
565- { description }
566- </ span >
567- ) }
568- </ Flex >
569- ) ;
570- } ;
571-
572- const Link = forwardRef < HTMLAnchorElement , React . ComponentProps < 'a' > & { css ?: SerializedStyles } > (
437+ const Link = forwardRef < HTMLAnchorElement , ComponentProps < 'a' > & { css ?: SerializedStyles } > (
573438 ( { children, css : cssProp , ...props } , ref ) => {
574439 return (
575440 < a
0 commit comments