diff --git a/.changeset/flat-ravens-call.md b/.changeset/flat-ravens-call.md new file mode 100644 index 00000000000..4182328bd63 --- /dev/null +++ b/.changeset/flat-ravens-call.md @@ -0,0 +1,10 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/clerk-react': minor +'@clerk/vue': minor +--- + +Introduce in-app development prompt to enable the Organizations feature + +In development instances, when using organization components or hooks for the first time, developers will see a prompt to enable the Organizations feature directly in their app, eliminating the need to visit the Clerk Dashboard. diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 4b3ceae4b8c..e477bfe90cd 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,9 +1,9 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "840KB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "81KB" }, - { "path": "./dist/clerk.channel.browser.js", "maxSize": "81KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "83KB" }, + { "path": "./dist/clerk.channel.browser.js", "maxSize": "83KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "124KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "65KB" }, { "path": "./dist/ui-common*.js", "maxSize": "117.1KB" }, { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120.1KB" }, @@ -23,6 +23,7 @@ { "path": "./dist/onetap*.js", "maxSize": "1KB" }, { "path": "./dist/waitlist*.js", "maxSize": "1.5KB" }, { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, + { "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" }, { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, { "path": "./dist/checkout*.js", "maxSize": "8.82KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6353d4c09d3..6af0e410091 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -22,7 +22,9 @@ import { import type { __experimental_CheckoutInstance, __experimental_CheckoutOptions, + __internal_AttemptToEnableEnvironmentSettingParams, __internal_CheckoutProps, + __internal_EnableOrganizationsPromptProps, __internal_OAuthConsentProps, __internal_PlanDetailsProps, __internal_SubscriptionDetailsProps, @@ -35,9 +37,9 @@ import type { AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, BillingNamespace, - Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, + Clerk as ClerkInterface, ClerkOptions, ClientJSONSnapshot, ClientResource, @@ -745,6 +747,62 @@ export class Clerk implements ClerkInterface { void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification')); }; + public __internal_attemptToEnableEnvironmentSetting = ( + params: __internal_AttemptToEnableEnvironmentSettingParams, + ): { status: 'enabled' | 'prompt-shown' } => { + const { for: setting, caller } = params; + + if (!this.user) { + console.warn( + `Clerk: "${caller}" requires an active user session. Ensure a user is signed in before executing ${caller}.`, + ); + } + + if ( + // Do not open the prompt if the user is not loaded, since the endpoint + // relies on the session + // Organization components already don't render if the session is not active + !this.user || + // If not in development instance, return enabled status in order to not open the prompt + this.#instanceType !== 'development' + ) { + return { status: 'enabled' }; + } + + switch (setting) { + case 'organizations': + if (!disabledOrganizationsFeature(this, this.environment)) { + return { status: 'enabled' }; + } + + this.__internal_openEnableOrganizationsPrompt({ + caller, + // Reload current window to all invalidate all resources + // related to organizations, eg: roles + onSuccess: () => window.location.reload(), + onClose: params.onClose, + } as __internal_EnableOrganizationsPromptProps); + + return { status: 'prompt-shown' }; + default: + return { status: 'enabled' }; + } + }; + + public __internal_openEnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls + .ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' }) + .then(controls => controls.openModal('enableOrganizationsPrompt', props || {})); + + this.telemetry?.record(eventPrebuiltComponentMounted('EnableOrganizationsPrompt', props)); + }; + + public __internal_closeEnableOrganizationsPrompt = (): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted().then(controls => controls.closeModal('enableOrganizationsPrompt')); + }; + public __internal_openBlankCaptchaModal = (): Promise => { this.assertComponentsReady(this.#componentControls); return this.#componentControls @@ -816,14 +874,21 @@ export class Clerk implements ClerkInterface { public openOrganizationProfile = (props?: OrganizationProfileProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationProfile', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + if (noOrganizationExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenOrgDoesNotExist, { @@ -846,14 +911,21 @@ export class Clerk implements ClerkInterface { public openCreateOrganization = (props?: CreateOrganizationProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'CreateOrganization', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + void this.#componentControls .ensureMounted({ preloadHint: 'CreateOrganization' }) .then(controls => controls.openModal('createOrganization', props || {})); @@ -988,14 +1060,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationProfile', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + const userExists = !noUserExists(this); if (noOrganizationExists(this) && userExists) { if (this.#instanceType === 'development') { @@ -1028,14 +1107,21 @@ export class Clerk implements ClerkInterface { public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'CreateOrganization', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls => controls.mountComponent({ name: 'CreateOrganization', @@ -1059,14 +1145,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationSwitcher', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls => controls.mountComponent({ name: 'OrganizationSwitcher', @@ -1098,14 +1191,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationList', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } + void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls => controls.mountComponent({ name: 'OrganizationList', @@ -1294,12 +1394,17 @@ export class Clerk implements ClerkInterface { public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + const { status } = this.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'TaskChooseOrganization', + onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('TaskChooseOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); - } + }, + }); + + if (status === 'prompt-shown') { return; } diff --git a/packages/clerk-js/src/core/fapiClient.ts b/packages/clerk-js/src/core/fapiClient.ts index 7d03e620c27..42af60d0f61 100644 --- a/packages/clerk-js/src/core/fapiClient.ts +++ b/packages/clerk-js/src/core/fapiClient.ts @@ -66,7 +66,7 @@ export interface FapiClient { } // List of paths that should not receive the session ID parameter in the URL -const unauthorizedPathPrefixes = ['/client', '/waitlist']; +const unauthorizedPathPrefixes = ['/client', '/waitlist', '/dev_tools']; type FapiClientOptions = { frontendApi: string; diff --git a/packages/clerk-js/src/core/resources/DevTools.ts b/packages/clerk-js/src/core/resources/DevTools.ts new file mode 100644 index 00000000000..bccd33c44d9 --- /dev/null +++ b/packages/clerk-js/src/core/resources/DevTools.ts @@ -0,0 +1,21 @@ +import type { ClerkResourceJSON, DevToolsResource, EnableEnvironmentSettingParams } from '@clerk/shared/types'; + +import { BaseResource } from './Base'; + +/** + * @internal + */ +export class DevTools extends BaseResource implements DevToolsResource { + pathRoot = '/dev_tools'; + + protected fromJSON(_data: ClerkResourceJSON | null): this { + return this; + } + + async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { + await this._basePatch({ + path: `${this.pathRoot}/${BaseResource.clerk.session?.id}/enable_environment_setting`, + body: params, + }); + } +} diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx index 148736665ce..6f3be02ea95 100644 --- a/packages/clerk-js/src/ui/Components.tsx +++ b/packages/clerk-js/src/ui/Components.tsx @@ -1,5 +1,6 @@ import type { __internal_CheckoutProps, + __internal_EnableOrganizationsPromptProps, __internal_PlanDetailsProps, __internal_SubscriptionDetailsProps, __internal_UserVerificationProps, @@ -27,6 +28,7 @@ import type { ClerkComponentName } from './lazyModules/components'; import { BlankCaptchaModal, CreateOrganizationModal, + EnableOrganizationsPrompt, ImpersonationFab, KeylessPrompt, OrganizationProfileModal, @@ -40,6 +42,7 @@ import { import { MountedCheckoutDrawer, MountedPlanDetailDrawer, MountedSubscriptionDetailDrawer } from './lazyModules/drawers'; import { LazyComponentRenderer, + LazyEnableOrganizationsPromptProvider, LazyImpersonationFabProvider, LazyModalRenderer, LazyOneTapRenderer, @@ -79,7 +82,8 @@ export type ComponentControls = { | 'createOrganization' | 'userVerification' | 'waitlist' - | 'blankCaptcha', + | 'blankCaptcha' + | 'enableOrganizationsPrompt', >( modal: T, props: T extends 'signIn' @@ -90,7 +94,9 @@ export type ComponentControls = { ? __internal_UserVerificationProps : T extends 'waitlist' ? WaitlistProps - : UserProfileProps, + : T extends 'enableOrganizationsPrompt' + ? __internal_EnableOrganizationsPromptProps + : UserProfileProps, ) => void; closeModal: ( modal: @@ -102,7 +108,8 @@ export type ComponentControls = { | 'createOrganization' | 'userVerification' | 'waitlist' - | 'blankCaptcha', + | 'blankCaptcha' + | 'enableOrganizationsPrompt', options?: { notify?: boolean; }, @@ -152,6 +159,7 @@ interface ComponentsState { userVerificationModal: null | __internal_UserVerificationProps; organizationProfileModal: null | OrganizationProfileProps; createOrganizationModal: null | CreateOrganizationProps; + enableOrganizationsPromptModal: null | __internal_EnableOrganizationsPromptProps; blankCaptchaModal: null; organizationSwitcherPrefetch: boolean; waitlistModal: null | WaitlistProps; @@ -245,6 +253,7 @@ const Components = (props: ComponentsProps) => { userVerificationModal: null, organizationProfileModal: null, createOrganizationModal: null, + enableOrganizationsPromptModal: null, organizationSwitcherPrefetch: false, waitlistModal: null, blankCaptchaModal: null, @@ -325,9 +334,10 @@ const Components = (props: ComponentsProps) => { clearUrlStateParam(); setState(s => { function handleCloseModalForExperimentalUserVerification() { - const modal = s[`${name}Modal`] || {}; + const modal = s[`${name}Modal`]; if (modal && typeof modal === 'object' && 'afterVerificationCancelled' in modal && notify) { - modal.afterVerificationCancelled?.(); + // TypeScript doesn't narrow properly with template literal access and 'in' operator + (modal as { afterVerificationCancelled?: () => void }).afterVerificationCancelled?.(); } } @@ -342,6 +352,20 @@ const Components = (props: ComponentsProps) => { }; componentsControls.openModal = (name, props) => { + // Prevent opening enableOrganizations prompt if it's already open + // It should open the first call and ignore the subsequent calls + if (name === 'enableOrganizationsPrompt') { + setState(prev => { + // Modal is already open, don't update state + if (prev.enableOrganizationsPromptModal) { + return prev; + } + + return { ...prev, [`${name}Modal`]: props }; + }); + return; + } + function handleCloseModalForExperimentalUserVerification() { if (!('afterVerificationCancelled' in props)) { return; @@ -612,6 +636,12 @@ const Components = (props: ComponentsProps) => { )} + {state.enableOrganizationsPromptModal && ( + + + + )} + {state.options?.__internal_keyless_claimKeylessApplicationUrl && state.options?.__internal_keyless_copyInstanceKeysUrl && ( diff --git a/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx b/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx new file mode 100644 index 00000000000..ec97c5858f7 --- /dev/null +++ b/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx @@ -0,0 +1,590 @@ +import { useClerk } from '@clerk/shared/react'; +import type { __internal_EnableOrganizationsPromptProps } from '@clerk/shared/types'; +// eslint-disable-next-line no-restricted-imports +import { css } from '@emotion/react'; +import { forwardRef, useMemo, useState } from 'react'; + +import { Modal } from '@/ui/elements/Modal'; +import { common, InternalThemeProvider } from '@/ui/styledSystem'; + +import { DevTools } from '../../../../core/resources/DevTools'; +import type { Environment } from '../../../../core/resources/Environment'; +import { Flex } from '../../../customizables'; +import { Portal } from '../../../elements/Portal'; +import { basePromptElementStyles, handleDashboardUrlParsing, PromptContainer, PromptSuccessIcon } from '../shared'; + +/** + * If we cannot reconstruct the url properly, then simply fallback to Clerk Dashboard + */ +function withLastActiveFallback(cb: () => string): string { + try { + return cb(); + } catch { + return 'https://dashboard.clerk.com/last-active?path=organization-settings'; + } +} + +const EnableOrganizationsPromptInternal = ({ + caller, + onSuccess, + onClose, +}: __internal_EnableOrganizationsPromptProps) => { + const clerk = useClerk(); + const [isLoading, setIsLoading] = useState(false); + const [isEnabled, setIsEnabled] = useState(false); + const [allowPersonalAccount, setAllowPersonalAccount] = useState(false); + + // @ts-expect-error - __unstable__environment is not typed + const environment = clerk?.__unstable__environment as Environment | undefined; + + const organizationsDashboardUrl = useMemo(() => { + return withLastActiveFallback(() => { + const currentUrl = window.location.href; + try { + const redirectUrlParts = handleDashboardUrlParsing(currentUrl); + const url = new URL( + `${redirectUrlParts.baseDomain}/apps/${redirectUrlParts.appId}/instances/${redirectUrlParts.instanceId}/organizations`, + ); + return url.href; + } catch { + if (!environment?.id) { + throw new Error('Cannot construct dashboard URL'); + } + + return 'https://dashboard.clerk.com/last-active?path=organization-settings'; + } + }); + }, [environment?.id]); + + const handleEnableOrganizations = () => { + setIsLoading(true); + + void new DevTools() + .__internal_enableEnvironmentSetting({ + enable_organizations: true, + organization_allow_personal_accounts: allowPersonalAccount, + }) + .then(() => { + setIsEnabled(true); + setIsLoading(false); + }) + .catch(() => { + setIsLoading(false); + }); + }; + + const isComponent = !caller.startsWith('use'); + + return ( + + ({ alignItems: 'center' })} + > + ({ + display: 'flex', + flexDirection: 'column', + maxWidth: '30rem', + })} + > + ({ + padding: `${t.sizes.$4} ${t.sizes.$6}`, + paddingBottom: t.sizes.$4, + gap: t.sizes.$2, + })} + > + ({ + gap: t.sizes.$2, + })} + > +
+ + + + + + + + + + + + +
+ +

+ {isEnabled ? 'Organizations feature enabled' : 'Organizations feature required'} +

+
+ + ({ + gap: t.sizes.$0x5, + })} + > + {isEnabled ? ( + + The Organizations feature has been enabled for your application. A default organization named "My + Organization" was created automatically. You can manage or rename it in your{' '} + + dashboard + + . + + ) : ( + <> + + To use the{' '} + + {isComponent ? `<${caller} />` : caller} + {' '} + {isComponent ? 'component' : 'hook'}, you'll need to enable the Organizations feature for your + app first. + + + + Learn more about Organizations. + + + )} + + + {!isEnabled && ( + ({ marginTop: t.sizes.$3 })}> + setAllowPersonalAccount(!allowPersonalAccount)} + isDisabled={false} + /> + + )} +
+ + + + ({ + padding: `${t.sizes.$4} ${t.sizes.$6}`, + gap: t.sizes.$3, + justifyContent: 'flex-end', + })} + > + {isEnabled ? ( + onSuccess?.()} + > + Continue + + ) : ( + <> + { + clerk?.__internal_closeEnableOrganizationsPrompt?.(); + onClose?.(); + }} + > + I'll remove it myself + + + + Enable Organizations + + + )} + +
+
+
+ ); +}; + +/** + * A prompt that allows the user to enable the Organizations feature for their development instance + * @internal + */ +export const EnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps) => { + return ( + + + + ); +}; + +const mainCTAStyles = css` + ${basePromptElementStyles}; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + height: 1.75rem; + padding: 0.375rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.12px; + color: white; + text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.32); + white-space: nowrap; + user-select: none; + color: white; + transition: all 120ms ease-in-out; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &:focus-visible { + outline: none; + } +`; + +type PromptButtonVariant = 'solid' | 'outline'; + +type PromptButtonProps = Pick, 'onClick' | 'children' | 'disabled'> & { + variant?: PromptButtonVariant; +}; + +const PromptButton = ({ variant = 'solid', ...props }: PromptButtonProps) => { + const solidStyles = css` + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.05) 100%), #454545; + box-shadow: + 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.12), + 0px 1.5px 2px 0px rgba(0, 0, 0, 0.48), + 0px 0px 4px 0px rgba(243, 107, 22, 0) inset; + + &:hover:not(:disabled) { + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.15) 100%), #5f5f5f; + box-shadow: + 0 0 3px 0 rgba(253, 224, 71, 0) inset, + 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 1px 0 0 rgba(255, 255, 255, 0.04) inset, + 0 0 0 1px rgba(0, 0, 0, 0.12), + 0 1.5px 2px 0 rgba(0, 0, 0, 0.48); + } + + &:focus:not(:disabled) { + border: 1px solid rgba(115, 115, 115); + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.05) 100%), #454545; + box-shadow: + 0 0 0 4px rgba(255, 255, 255, 0.05), + 0 0 3px 0 rgba(255, 255, 255, 0) inset, + 0 0 0 1px rgba(255, 255, 255, 0.25) inset, + 0 1px 0 0 rgba(255, 255, 255, 0.04) inset, + 0 0 0 1px rgba(0, 0, 0, 0.12), + 0 1.5px 2px 0 rgba(0, 0, 0, 0.48); + } + `; + + const outlineStyles = css` + background: rgba(69, 69, 69, 0.1); + border: 1px solid rgba(118, 118, 132, 0.25); + + &:hover:not(:disabled) { + box-shadow: + 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), + 0px 1.5px 2px 0px rgba(0, 0, 0, 0.48); + } + + &:hover:not(:disabled) { + border: 1px solid rgba(115, 115, 115); + } + + &:focus:not(:disabled) { + border: 1px solid rgba(115, 115, 115); + + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.05); + } + `; + + return ( +