diff --git a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx index 1eaef2ce..4e9c2d5a 100644 --- a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx @@ -1,7 +1,6 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import {FlowMetadataResponse, WithPreferences, withVendorCSSClassPrefix} from '@thunderid/browser'; import { ButtonHTMLAttributes, @@ -12,6 +11,7 @@ import { Ref, RefAttributes, } from 'react'; +import {cx} from '../../../styles/emotion'; import Button from '../../primitives/Button/Button'; /** diff --git a/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx b/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx index fedef3c6..cdcebba4 100644 --- a/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx +++ b/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx @@ -1,7 +1,6 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import {FlowMetadataResponse, WithPreferences, withVendorCSSClassPrefix} from '@thunderid/browser'; import { forwardRef, @@ -12,6 +11,7 @@ import { Ref, RefAttributes, } from 'react'; +import {cx} from '../../../styles/emotion'; import Button from '../../primitives/Button/Button'; /** diff --git a/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx b/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx index 0f39b48e..e8514c45 100644 --- a/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx +++ b/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx @@ -1,7 +1,6 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import {FlowMetadataResponse, WithPreferences, withVendorCSSClassPrefix} from '@thunderid/browser'; import { forwardRef, @@ -12,6 +11,7 @@ import { Ref, RefAttributes, } from 'react'; +import {cx} from '../../../styles/emotion'; import Button from '../../primitives/Button/Button'; /** diff --git a/packages/react/src/components/adapters/Consent.tsx b/packages/react/src/components/adapters/Consent.tsx index dda5939b..fbe14bae 100644 --- a/packages/react/src/components/adapters/Consent.tsx +++ b/packages/react/src/components/adapters/Consent.tsx @@ -14,6 +14,7 @@ import Toggle from '../primitives/Toggle/Toggle'; import {Info} from '../primitives/Icons'; import Tooltip from '../primitives/Tooltip/Tooltip'; import {UseTranslation} from '../../hooks/useTranslation'; +import {css} from '../../styles/emotion'; /** * Backward-compatible consent purpose type exported by @thunderid/react. @@ -112,6 +113,28 @@ const Consent: FC = ({ meta, t, }: ConsentProps) => { + // Computed per render (not at module scope): a CSP nonce configured on + // isn't known until the provider renders, and Emotion needs it applied before any style + // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a + // no-op after the first render — this isn't a real per-render cost. + const purposesContainerClass: string = css({ + display: 'flex', + flexDirection: 'column', + gap: '1rem', + marginTop: '0.25rem', + }); + const purposeItemClass: string = css({paddingBottom: '1rem'}); + const sectionClass: string = css({marginTop: '0.5rem'}); + const sectionHeaderClass: string = css({alignItems: 'center', display: 'flex', gap: '4px', marginBottom: '10px'}); + const optionalSectionHeaderClass: string = css({ + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + marginBottom: '10px', + paddingRight: '4px', + }); + const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'}); + /** Resolve any remaining {{t()}} or {{meta()}} template expressions in a string at render time. */ const resolve = (text: string | undefined): string => { if (!text || (!t && !meta)) { @@ -172,9 +195,9 @@ const Consent: FC = ({ } return ( -
+
{purposes.map((purpose: ConsentPurposeData, purposeIndex: number) => ( -
+
{/* TODO: Uncomment when the backend supports multiple purposes for a application */} {/* {purpose.purposeName} @@ -184,8 +207,8 @@ const Consent: FC = ({ */} {purpose.essential && purpose.essential.length > 0 && ( -
-
+
+
{essentialLabel} @@ -205,17 +228,9 @@ const Consent: FC = ({ )} {purpose.optional && purpose.optional.length > 0 && ( -
-
-
+
+
+
{purpose.type === 'permissions' ? 'Permissions' : optionalLabel} diff --git a/packages/react/src/components/adapters/ConsentCheckboxList.styles.ts b/packages/react/src/components/adapters/ConsentCheckboxList.styles.ts index 5aa946f8..02cb65b9 100644 --- a/packages/react/src/components/adapters/ConsentCheckboxList.styles.ts +++ b/packages/react/src/components/adapters/ConsentCheckboxList.styles.ts @@ -1,9 +1,9 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {css} from '@emotion/css'; import {Theme} from '@thunderid/browser'; import {useMemo} from 'react'; +import {css} from '../../styles/emotion'; const useStyles = (theme: Theme, colorScheme: string): Record => useMemo( diff --git a/packages/react/src/components/adapters/ConsentCheckboxList.tsx b/packages/react/src/components/adapters/ConsentCheckboxList.tsx index 8d89282b..00d20040 100644 --- a/packages/react/src/components/adapters/ConsentCheckboxList.tsx +++ b/packages/react/src/components/adapters/ConsentCheckboxList.tsx @@ -1,11 +1,11 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import {type ConsentPurposeData, withVendorCSSClassPrefix, bem} from '@thunderid/browser'; import {type ChangeEvent, FC, ReactNode} from 'react'; import useStyles from './ConsentCheckboxList.styles'; import useTheme from '../../contexts/Theme/useTheme'; +import {cx} from '../../styles/emotion'; import Toggle from '../primitives/Toggle/Toggle'; import Typography from '../primitives/Typography/Typography'; diff --git a/packages/react/src/components/adapters/ImageComponent.tsx b/packages/react/src/components/adapters/ImageComponent.tsx index accf33d6..b3809261 100644 --- a/packages/react/src/components/adapters/ImageComponent.tsx +++ b/packages/react/src/components/adapters/ImageComponent.tsx @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import {resolveLogoUri, ResolvedLogo} from '@thunderid/browser'; -import {CSSProperties, FC, SyntheticEvent} from 'react'; +import {FC, SyntheticEvent} from 'react'; import useTheme from '../../contexts/Theme/useTheme'; import {AdapterProps} from '../../models/adapters'; +import {css} from '../../styles/emotion'; const DEFAULT_EMOJI_CONTAINER_HEIGHT = '4em'; @@ -13,6 +14,12 @@ const DEFAULT_EMOJI_CONTAINER_HEIGHT = '4em'; */ const ImageComponent: FC = ({component}: AdapterProps) => { const {theme} = useTheme(); + // Computed per render (not at module scope): a CSP nonce configured on + // isn't known until the provider renders, and Emotion needs it applied before any style + // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a + // no-op after the first render — this isn't a real per-render cost. + const centerClass: string = css({textAlign: 'center'}); + const emojiGlyphClass: string = css({fontSize: '100cqmin', lineHeight: 1}); const config: Record = component.config || {}; const src: string = (config['src'] as string) || ''; const alt: string = (config['alt'] as string) || (config['label'] as string) || 'Image'; @@ -20,12 +27,14 @@ const ImageComponent: FC = ({component}: AdapterProps) => { const height: string = (config['height'] as string) || 'auto'; const variant: string = component.variant?.toLowerCase() || 'image_block'; - const imageStyle: CSSProperties = { + const imageStyle = { borderRadius: theme.vars.borderRadius.small, display: 'block', margin: variant === 'image_block' ? '1rem auto' : '0', }; + const imageClass: string = css(imageStyle); + if (!src) { return null; } @@ -52,24 +61,24 @@ const ImageComponent: FC = ({component}: AdapterProps) => { containerHeight = DEFAULT_EMOJI_CONTAINER_HEIGHT; } + const emojiContainerClass: string = css({ + ...imageStyle, + containerType: 'size', + display: 'inline-grid', + height: containerHeight, + placeItems: 'center', + width: cssWidth, + }); + return ( -
+
{/* * container-type: size lets the inner span use cqmin (= min(cqw, cqh)) * so the emoji font-size tracks the rendered container dimensions * rather than the parent's font-size. */} - - + + {resolvedIcon.glyph} @@ -78,13 +87,13 @@ const ImageComponent: FC = ({component}: AdapterProps) => { } return ( -
+
{alt}): void => { // Hide broken images e.currentTarget.style.display = 'none'; diff --git a/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.styles.ts b/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.styles.ts index edc128fb..f2653033 100644 --- a/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.styles.ts +++ b/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.styles.ts @@ -1,9 +1,9 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {css} from '@emotion/css'; import {Theme} from '@thunderid/browser'; import {useMemo} from 'react'; +import {css} from '../../../styles/emotion'; const useStyles = (theme: Theme, colorScheme: string): Record => useMemo(() => { diff --git a/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.tsx b/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.tsx index 159fe010..acd19e09 100644 --- a/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.tsx +++ b/packages/react/src/components/presentation/LanguageSwitcher/BaseLanguageSwitcher.tsx @@ -1,7 +1,6 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import { autoUpdate, flip, @@ -18,6 +17,7 @@ import { import {FC, ReactElement, ReactNode, useEffect, useState} from 'react'; import useStyles from './BaseLanguageSwitcher.styles'; import useTheme from '../../../contexts/Theme/useTheme'; +import {cx} from '../../../styles/emotion'; import Check from '../../primitives/Icons/Check'; import ChevronDown from '../../primitives/Icons/ChevronDown'; diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts index 88c78fe2..bd4c722e 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts @@ -1,9 +1,9 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {css} from '@emotion/css'; import {Theme} from '@thunderid/browser'; import {useMemo} from 'react'; +import {css} from '../../../styles/emotion'; /** * Creates styles for the BaseUserDropdown component @@ -74,6 +74,10 @@ const useStyles = (theme: Theme, colorScheme: string): Record => width: 100%; `; + const menuItemHighlighted: string = css` + background-color: ${theme.vars.colors.action?.hover || 'rgba(0, 0, 0, 0.05)'}; + `; + const menuItem: string = css` display: flex; align-items: center; @@ -195,6 +199,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record => loadingText, menuItem, menuItemAnchor, + menuItemHighlighted, trigger, userName, }; diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 0b1aa7e8..90a5fe79 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -1,7 +1,6 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {cx} from '@emotion/css'; import { useFloating, autoUpdate, @@ -19,6 +18,7 @@ import {withVendorCSSClassPrefix} from '@thunderid/browser'; import {FC, ReactElement, ReactNode, useState} from 'react'; import useStyles from './BaseUserDropdown.styles'; import useTheme from '../../../contexts/Theme/useTheme'; +import {css, cx} from '../../../styles/emotion'; import getDisplayName from '../../../utils/getDisplayName'; import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; import {Avatar} from '../../primitives/Avatar/Avatar'; @@ -216,13 +216,19 @@ export const BaseUserDropdown: FC = ({
` element, so no inline `style` attribute is needed. + css({ + ...floatingStyles, + // Floating UI doesn't set a z-index by default, so we set a high value to ensure the dropdown appears above other elements. + // https://floating-ui.com/docs/misc#z-index-stacking + zIndex: 9999, + }), + )} {...getFloatingProps()} >
@@ -267,13 +273,10 @@ export const BaseUserDropdown: FC = ({ return ( setHoveredItemIndex(index)} onMouseLeave={(): void => setHoveredItemIndex(null)} @@ -288,11 +291,11 @@ export const BaseUserDropdown: FC = ({ return ( @@ -862,7 +862,7 @@ const BaseAcceptInvite: FC = ({ )} {apiError && ( -
+
)['errorContainer']}> {apiError.message} @@ -894,16 +894,20 @@ const BaseAcceptInvite: FC = ({ )} {isLoading && ( -
+
)['centeredSpinnerContainerSmall']}>
)}
{onGoToSignIn && ( -
+
)['alreadyHaveAccountContainer']}> Already have an account?{' '} - diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index 7ff5ce49..cd32552d 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -1,7 +1,7 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {css, cx} from '@emotion/css'; +import type {CSSInterpolation} from '@emotion/css/create-instance'; import { ConsentConstants, FieldType, @@ -30,6 +30,7 @@ import { ComponentRendererMap, } from '../../../contexts/ComponentRenderer/ComponentRendererContext'; import {UseTranslation} from '../../../hooks/useTranslation'; +import {css, cx} from '../../../styles/emotion'; import Consent from '../../adapters/Consent'; import {getConsentOptionalKey} from '../../adapters/ConsentCheckboxList'; import FacebookButton from '../../adapters/FacebookButton'; @@ -67,33 +68,51 @@ const logger: ReturnType = createPackageCom * - Any remaining `emoji:X` text occurrences → `X` */ +// This module's functions (renderSignInComponents, etc.) are called imperatively from a +// component's render, not React components/hooks themselves — so these classes can't be computed +// at module scope: that runs at import time, before has had a chance to +// configure Emotion's CSP nonce, which would insert them into an un-nonced