11import { useClerk } from '@clerk/shared/react' ;
22import type { __internal_EnableOrganizationsPromptProps } from '@clerk/shared/types' ;
33// eslint-disable-next-line no-restricted-imports
4- import { css } from '@emotion/react' ;
5- import { forwardRef , useMemo , useRef , useState } from 'react' ;
4+ import { css , type Theme } from '@emotion/react' ;
5+ import { forwardRef , useId , useMemo , useRef , useState } from 'react' ;
66
77import { Modal } from '@/ui/elements/Modal' ;
88import { common , InternalThemeProvider } from '@/ui/styledSystem' ;
99
1010import { DevTools } from '../../../../core/resources/DevTools' ;
1111import type { Environment } from '../../../../core/resources/Environment' ;
12- import { Flex } from '../../../customizables' ;
12+ import { Flex , Span } from '../../../customizables' ;
1313import { Portal } from '../../../elements/Portal' ;
1414import { basePromptElementStyles , handleDashboardUrlParsing , PromptContainer , PromptSuccessIcon } from '../shared' ;
1515
@@ -88,7 +88,8 @@ const EnableOrganizationsPromptInternal = ({
8888 sx = { ( ) => ( {
8989 display : 'flex' ,
9090 flexDirection : 'column' ,
91- maxWidth : '30rem' ,
91+ width : '30rem' ,
92+ maxWidth : 'calc(100vw - 2rem)' ,
9293 } ) }
9394 >
9495 < Flex
@@ -302,10 +303,11 @@ const EnableOrganizationsPromptInternal = ({
302303
303304 { ! isEnabled && clerk ?. user && (
304305 < Flex sx = { t => ( { marginTop : t . sizes . $3 } ) } >
305- < AllowPersonalAccountSwitch
306+ < Switch
307+ label = 'Allow personal account'
308+ description = 'This is an uncommon setting, meant for applications that sell to both organizations and individual users. Most B2B applications require users to be part of an organization, and should keep this setting disabled.'
306309 checked = { allowPersonalAccount }
307- onChange = { ( ) => setAllowPersonalAccount ( ! allowPersonalAccount ) }
308- isDisabled = { false }
310+ onChange = { ( ) => setAllowPersonalAccount ( prev => ! prev ) }
309311 />
310312 </ Flex >
311313 ) }
@@ -314,8 +316,10 @@ const EnableOrganizationsPromptInternal = ({
314316 < span
315317 css = { css `
316318 height : 1px ;
319+ display : block;
320+ width : calc (100% - 2px );
321+ margin-inline : auto;
317322 background-color : # 151515 ;
318- width : 100% ;
319323 box-shadow : 0px 1px 0px 0px # 424242 ;
320324 ` }
321325 />
@@ -461,118 +465,134 @@ const PromptButton = forwardRef<HTMLButtonElement, PromptButtonProps>(({ variant
461465 ) ;
462466} ) ;
463467
464- type AllowPersonalAccountSwitchProps = {
465- checked : boolean ;
466- isDisabled : boolean ;
467- onChange : ( checked : boolean ) => void ;
468+ type SwitchProps = React . ComponentProps < 'input' > & {
469+ label : string ;
470+ description ?: string ;
468471} ;
469472
470- const AllowPersonalAccountSwitch = forwardRef < HTMLDivElement , AllowPersonalAccountSwitchProps > (
471- ( { checked, onChange, isDisabled = false } , ref ) => {
473+ const TRACK_PADDING = '2px' ;
474+ const TRACK_INNER_WIDTH = ( t : Theme ) => t . sizes . $6 ;
475+ const TRACK_HEIGHT = ( t : Theme ) => t . sizes . $4 ;
476+ const THUMB_WIDTH = ( t : Theme ) => t . sizes . $3 ;
477+
478+ const Switch = forwardRef < HTMLInputElement , SwitchProps > (
479+ ( { label, description, checked : controlledChecked , defaultChecked, onChange, ...props } , ref ) => {
480+ const descriptionId = useId ( ) ;
481+
482+ const isControlled = controlledChecked !== undefined ;
483+ const [ internalChecked , setInternalChecked ] = useState ( ! ! defaultChecked ) ;
484+ const checked = isControlled ? controlledChecked : internalChecked ;
485+
472486 const handleChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
473- if ( isDisabled ) {
474- return ;
487+ if ( ! isControlled ) {
488+ setInternalChecked ( e . target . checked ) ;
475489 }
476-
477- onChange ?.( e . target . checked ) ;
490+ onChange ?.( e ) ;
478491 } ;
479492
480493 return (
481494 < Flex
482- ref = { ref }
483- direction = 'row'
484- align = 'center'
485- as = 'label'
486- gap = { 2 }
487- sx = { t => ( {
488- isolation : 'isolate' ,
489- width : 'fit-content' ,
490- '&:has(input:focus-visible) > input + span' : {
491- ...common . focusRingStyles ( t ) ,
492- } ,
493- } ) }
495+ direction = 'col'
496+ gap = { 1 }
494497 >
495- { /* The order of the elements is important here for the focus ring to work. The input is visually hidden, so the focus ring is applied to the span. */ }
496- < input
497- type = 'checkbox'
498- role = 'switch'
499- disabled = { isDisabled }
500- checked = { checked }
501- onChange = { handleChange }
502- style = { {
503- ...common . visuallyHidden ( ) ,
504- } }
505- />
506498 < Flex
507- as = 'span'
508- data-checked = { checked }
509- sx = { t => ( {
510- minWidth : t . sizes . $7 ,
511- alignSelf : 'flex-start' ,
512- height : t . sizes . $4 ,
513- alignItems : 'center' ,
514- position : 'relative' ,
515- borderColor : '#DBDBE0' ,
516- backgroundColor : checked ? '#DBDBE0' : t . colors . $primary500 ,
517- borderRadius : 999 ,
518- transition : 'background-color 0.2s' ,
519- opacity : isDisabled ? 0.6 : 1 ,
520- cursor : isDisabled ? 'not-allowed' : 'pointer' ,
521- outline : 'none' ,
522- boxSizing : 'border-box' ,
523- boxShadow :
524- '0px 0px 6px 0px rgba(255, 255, 255, 0.04) inset, 0px 0px 0px 1px rgba(255, 255, 255, 0.04) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.04) inset, 0px 0px 0px 1px rgba(0, 0, 0, 0.1)' ,
525- } ) }
499+ as = 'label'
500+ gap = { 2 }
501+ align = 'center'
502+ sx = { {
503+ isolation : 'isolate' ,
504+ userSelect : 'none' ,
505+ '&:has(input:focus-visible) > input + span' : {
506+ outline : '2px solid white' ,
507+ outlineOffset : '2px' ,
508+ } ,
509+ '&:has(input:disabled) > input + span' : {
510+ opacity : 0.6 ,
511+ cursor : 'not-allowed' ,
512+ pointerEvents : 'none' ,
513+ } ,
514+ } }
526515 >
527- < Flex
528- sx = { t => ( {
529- position : 'absolute' ,
530- left : t . sizes . $0x5 ,
531- width : t . sizes . $3 ,
532- height : t . sizes . $3 ,
533- borderRadius : '50%' ,
534- backgroundColor : 'white' ,
535- boxShadow : t . shadows . $switchControl ,
536- transform : `translateX(${ checked ? t . sizes . $3 : 0 } )` ,
537- transition : 'transform 0.2s' ,
538- zIndex : 1 ,
539- } ) }
516+ < input
517+ type = 'checkbox'
518+ { ...props }
519+ ref = { ref }
520+ role = 'switch'
521+ { ...( isControlled ? { checked } : { defaultChecked } ) }
522+ onChange = { handleChange }
523+ css = { { ...common . visuallyHidden ( ) } }
524+ aria-describedby = { description ? descriptionId : undefined }
540525 />
541- </ Flex >
542-
543- < Flex
544- direction = 'col'
545- gap = { 1 }
546- >
526+ < Span
527+ sx = { t => {
528+ const trackWidth = `calc(${ TRACK_INNER_WIDTH ( t ) } + ${ TRACK_PADDING } + ${ TRACK_PADDING } )` ;
529+ const trackHeight = `calc(${ TRACK_HEIGHT ( t ) } + ${ TRACK_PADDING } )` ;
530+ return {
531+ display : 'flex' ,
532+ alignItems : 'center' ,
533+ paddingInline : TRACK_PADDING ,
534+ width : trackWidth ,
535+ height : trackHeight ,
536+ border : `1px solid rgba(118, 118, 132, 0.25)` ,
537+ backgroundColor : checked ? 'rgba(255, 255, 255, 0.75)' : `rgba(255, 255, 255, 0.1)` ,
538+ borderRadius : 999 ,
539+ transition : 'background-color 0.2s ease-in-out' ,
540+ '&:hover' : {
541+ borderColor : `rgba(118, 118, 132, 0.5)` ,
542+ } ,
543+ } ;
544+ } }
545+ >
546+ < Span
547+ sx = { t => {
548+ const size = THUMB_WIDTH ( t ) ;
549+ const maxTranslateX = `calc(${ TRACK_INNER_WIDTH ( t ) } - ${ size } - ${ TRACK_PADDING } )` ;
550+ return {
551+ width : size ,
552+ height : size ,
553+ borderRadius : 9999 ,
554+ backgroundColor : 'white' ,
555+ transform : `translateX(${ checked ? maxTranslateX : '0' } )` ,
556+ transition : 'transform 0.2s ease-in-out' ,
557+ '@media (prefers-reduced-motion: reduce)' : {
558+ transition : 'none' ,
559+ } ,
560+ } ;
561+ } }
562+ />
563+ </ Span >
547564 < span
548565 css = { [
549566 basePromptElementStyles ,
550567 css `
551568 font-size : 0.875rem ;
552- font-weight : 400 ;
553- line-height : 1.23 ;
569+ font-weight : 500 ;
570+ line-height : 1.25 ;
554571 color : white;
555572 ` ,
556573 ] }
557574 >
558- Allow personal account
575+ { label }
559576 </ span >
560-
561- < span
562- css = { [
577+ </ Flex >
578+ { description ? (
579+ < Span
580+ id = { descriptionId }
581+ sx = { t => [
563582 basePromptElementStyles ,
564- css `
565- color : # b4b4b4 ;
566- font-size : 0.8125rem ;
567- font-weight : 400 ;
568- line-height : 1.23 ;
569- ` ,
583+ {
584+ display : 'block' ,
585+ paddingInlineStart : `calc(${ TRACK_INNER_WIDTH ( t ) } + ${ TRACK_PADDING } + ${ TRACK_PADDING } + ${ t . sizes . $2 } )` ,
586+ fontSize : '0.75rem' ,
587+ lineHeight : '1.3333333333' ,
588+ color : '#c3c3c6' ,
589+ textWrap : 'pretty' ,
590+ } ,
570591 ] }
571592 >
572- This is an uncommon setting, meant for applications that sell to both organizations and individual users.
573- Most B2B applications require users to be part of an organization, and should keep this setting disabled.
574- </ span >
575- </ Flex >
593+ { description }
594+ </ Span >
595+ ) : null }
576596 </ Flex >
577597 ) ;
578598 } ,
0 commit comments