diff --git a/.changeset/bordered-security-icons.md b/.changeset/bordered-security-icons.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/bordered-security-icons.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/mosaic/AGENTS.md b/packages/mosaic/AGENTS.md
index 1936031f567..127bb615991 100644
--- a/packages/mosaic/AGENTS.md
+++ b/packages/mosaic/AGENTS.md
@@ -47,6 +47,7 @@ Most problems a feature hits already have a shared answer. Find the row for your
| A spinner that should not flash or flicker | `useSpinDelay` (`src/hooks/`) |
| A placeholder for content that is still loading | `Skeleton` (`src/components/skeleton/`), see [Skeletons](docs/skeletons.md) |
| A date shown to the user | `formatDate` (`src/utils/format-date.ts`) |
+| A value turned into kebab case, e.g. for a `data-*` attribute | `kebabCase` (`src/utils/kebab-case.ts`) |
| Current time for display labels | `useNow` + `MosaicProvider` ([#10098](https://github.com/clerk/javascript/pull/10098), once available) |
| Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) |
| An inline message row that animates open and closed | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `styles/feedback.styles.ts` |
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx
new file mode 100644
index 00000000000..a9a14b66ef0
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { UserProfileProviderMedia } from '../user-profile-provider-media';
+
+describe('UserProfileProviderMedia', () => {
+ it.each([
+ ['an image logo', { iconUrl: 'https://img.clerk.com/static/okx_wallet.svg' }],
+ ['a masked logo', { iconUrl: 'https://img.clerk.com/static/okx_wallet.svg', monochromeIcon: true }],
+ ['the initial fallback', {}],
+ ])('marks %s with the kebab-cased provider id', (_, props) => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector('.cl-provider-icon')).toHaveAttribute('data-provider', 'okx-wallet');
+ });
+});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx
index 9e2f5df2392..cd82e7776e2 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx
@@ -36,6 +36,7 @@ export function UserProfileConnectProviderRowView({
@@ -104,6 +105,7 @@ export function UserProfileConnectedAccountRowView({
diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts
index 00d2f9b60d9..e0cc0b7249d 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts
@@ -88,6 +88,7 @@ export function createProviderCatalog(
enabled: enabled.has(strategy),
display: {
provider: name,
+ iconId: provider,
iconUrl: iconImageUrl(provider),
monochromeIcon: isMonochromeProvider(provider),
},
@@ -109,7 +110,7 @@ export function createProviderCatalog(
strategy,
provider,
enabled: enabled.has(strategy),
- display: { provider: settings?.name || provider, iconUrl: settings?.logo_url || undefined },
+ display: { provider: settings?.name || provider, iconId: provider, iconUrl: settings?.logo_url || undefined },
});
}
return [
diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts
index 77168d0dfd5..bfd25079a0b 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts
@@ -8,6 +8,7 @@ export type ConnectedAccountActionResult = 'redirecting' | void;
export interface ConnectedAccountProviderDisplay {
provider: string;
+ iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
}
diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx
index 28f0c576cbc..51415a3af39 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx
@@ -44,7 +44,10 @@ export function UserProfileAddMfaView({ methods, onSelect }: UserProfileAddMfaVi
}
>
-
+
diff --git a/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx b/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx
index 01eb9cd0498..0fbbef9c7d4 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx
@@ -10,7 +10,10 @@ type UserProfileProviderIconProps = { iconUrl: string; glyph?: never } | { iconU
export function UserProfileProviderIcon(props: UserProfileProviderIconProps) {
return (
-
+
{'iconUrl' in props ? (
({ maskImage: `url(${JSON.stringify(url)})` }),
fallback: {
@@ -22,7 +25,7 @@ export const styles = stylex.create({
display: 'inline-flex',
fontSize: '0.625rem',
justifyContent: 'center',
- height: space['6'],
- width: space['6'],
+ height: space['5'],
+ width: space['5'],
},
});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx b/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx
index 52b3c75b331..429b3191644 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx
@@ -2,37 +2,46 @@ import * as stylex from '@stylexjs/stylex';
import { IconFrame } from '../../components/icon';
import { Section } from '../../components/section';
+import { mergeStyleProps, themeProps } from '../../props';
+import { kebabCase } from '../../utils/kebab-case';
import { styles } from './user-profile-provider-media.styles';
export function UserProfileProviderMedia({
provider,
+ iconId,
iconUrl,
monochromeIcon,
}: {
provider: string;
+ iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
}) {
const url = iconUrl?.trim();
+ const slot = themeProps('provider-icon', { provider: iconId && kebabCase(iconId) });
+ const offset = iconId === 'apple' && styles.appleOffset;
return (
-
+
{url && monochromeIcon ? (
) : url ? (
) : (
{provider.trim().charAt(0).toUpperCase()}
diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx b/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx
index 7de7823e8cf..af73a0516c8 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx
@@ -1,4 +1,4 @@
-import { Icon } from '../../components/icon';
+import { Icon, IconFrame } from '../../components/icon';
import { Section } from '../../components/section';
import { DeviceLaptop } from '../../icons/glyphs/device-laptop';
import { DevicePhone } from '../../icons/glyphs/device-phone';
@@ -20,15 +20,17 @@ const icons = {
export function UserProfileSecurityIcon({ name }: { name: UserProfileSecurityIconName }) {
return (
-
-
+
+
+
+
);
}
diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts
index 4038ea7bb12..db674839f5a 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts
@@ -1,6 +1,6 @@
import * as stylex from '@stylexjs/stylex';
-import { colorVars, radiusVars, space } from '../../tokens.stylex';
+import { colorVars, space } from '../../tokens.stylex';
export const styles = stylex.create({
icon: {
@@ -9,13 +9,6 @@ export const styles = stylex.create({
height: space['4.5'],
width: space['4.5'],
},
- media: {
- borderColor: colorVars['--cl-color-border-subtle'],
- borderRadius: radiusVars['--cl-radius-md'],
- borderStyle: 'solid',
- borderWidth: '1px',
- backgroundColor: colorVars['--cl-color-background-subtle'],
- },
sectionCards: {
gap: space['3'],
display: 'flex',
diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx
index 1f94832d3ac..e788761c532 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx
@@ -55,6 +55,7 @@ export function UserProfileWeb3WalletRowView({
{wallet.provider ? (
diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts
index 21faca20f15..9da99dda1c5 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts
@@ -80,6 +80,7 @@ export function projectWeb3Wallets({
allowCreation &&
wallet.verification.strategy !== null &&
enabledStrategies.includes(wallet.verification.strategy),
+ iconId: provider?.provider,
iconUrl: provider ? iconImageUrl(provider.provider) : undefined,
monochromeIcon: provider ? isMonochromeProvider(provider.provider) : false,
isPrimary: wallet.id === primaryId,
@@ -93,6 +94,7 @@ export function projectWeb3Wallets({
id: provider.strategy,
walletPicker: provider.provider === 'solana' ? 'solana' : undefined,
provider: provider.name,
+ iconId: provider.provider,
iconUrl: iconImageUrl(provider.provider),
monochromeIcon: isMonochromeProvider(provider.provider),
}))
diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts
index 0b0d9666a4c..a0ce8fec689 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts
@@ -9,6 +9,7 @@ export interface UserProfileWeb3Provider {
id: Web3Strategy;
walletPicker?: 'solana';
provider: string;
+ iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
connectError?: string;
@@ -21,6 +22,7 @@ export interface UserProfileWeb3Wallet {
walletPicker?: 'solana';
canVerify?: boolean;
provider?: string;
+ iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
isPrimary?: boolean;
diff --git a/packages/mosaic/src/props.ts b/packages/mosaic/src/props.ts
index 97abd70c7de..d12f2aabeaf 100644
--- a/packages/mosaic/src/props.ts
+++ b/packages/mosaic/src/props.ts
@@ -2,6 +2,7 @@ import type * as stylex from '@stylexjs/stylex';
import type React from 'react';
import type { ComponentProps } from './primitives/utils';
+import { kebabCase } from './utils/kebab-case';
/**
* What `xstyle` accepts: exactly what `stylex.props(...)` does, since that is where a part
@@ -63,8 +64,6 @@ export type MosaicComponentProps
type VariantValue = string | number | boolean | null | undefined;
-const kebab = (name: string) => name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
-
/**
* The stable class + variant reflection for a slot.
*
@@ -84,7 +83,7 @@ export function themeProps(
if (value == null || value === false) {
continue;
}
- attrs[`data-${kebab(axis)}`] = value === true ? '' : String(value);
+ attrs[`data-${kebabCase(axis)}`] = value === true ? '' : String(value);
}
return { className: `cl-${slot}`, ...attrs };
}
diff --git a/packages/mosaic/src/utils/__tests__/kebab-case.test.ts b/packages/mosaic/src/utils/__tests__/kebab-case.test.ts
new file mode 100644
index 00000000000..d9fb6194d2f
--- /dev/null
+++ b/packages/mosaic/src/utils/__tests__/kebab-case.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest';
+
+import { kebabCase } from '../kebab-case';
+
+describe('kebabCase', () => {
+ it.each([
+ ['okx_wallet', 'okx-wallet'],
+ ['colorScheme', 'color-scheme'],
+ ['HTTPRequest', 'http-request'],
+ ['some whitespace', 'some-whitespace'],
+ ['already-kebab', 'already-kebab'],
+ ['apple', 'apple'],
+ ['', ''],
+ ])('converts %j to %j', (input, expected) => {
+ expect(kebabCase(input)).toBe(expected);
+ });
+});
diff --git a/packages/mosaic/src/utils/kebab-case.ts b/packages/mosaic/src/utils/kebab-case.ts
new file mode 100644
index 00000000000..bc72432a214
--- /dev/null
+++ b/packages/mosaic/src/utils/kebab-case.ts
@@ -0,0 +1,12 @@
+export function kebabCase(value: string): string {
+ return words(value)
+ .map(word => word.toLowerCase())
+ .join('-');
+}
+
+const CASE_SPLIT_PATTERN =
+ /\p{Lu}?\p{Ll}+|[0-9]+|\p{Lu}+(?!\p{Ll})|\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{L}+/gu;
+
+function words(value: string): string[] {
+ return Array.from(value.match(CASE_SPLIT_PATTERN) ?? []);
+}
diff --git a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts
index b0070f08823..28951126ece 100644
--- a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts
+++ b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts
@@ -13,13 +13,20 @@ import { idleReverification } from './reverification';
export const connectedAccount: UserProfileConnectedAccount = {
id: 'google',
provider: 'Google',
+ iconId: 'google',
identifier: 'test@example.com',
iconUrl: 'https://img.clerk.com/static/google.svg',
status: 'connected',
};
const connectionProviders: UserProfileConnectionProvider[] = [
- { id: 'google', provider: 'Google', iconUrl: 'https://img.clerk.com/static/google.svg' },
- { id: 'apple', provider: 'Apple', iconUrl: 'https://img.clerk.com/static/apple.svg', monochromeIcon: true },
+ { id: 'google', provider: 'Google', iconId: 'google', iconUrl: 'https://img.clerk.com/static/google.svg' },
+ {
+ id: 'apple',
+ provider: 'Apple',
+ iconId: 'apple',
+ iconUrl: 'https://img.clerk.com/static/apple.svg',
+ monochromeIcon: true,
+ },
];
export function useConnectedAccountsFixture({
@@ -60,6 +67,7 @@ export function useConnectedAccountsFixture({
{
id: provider.id,
provider: provider.provider,
+ iconId: provider.iconId,
iconUrl: provider.iconUrl,
monochromeIcon: provider.monochromeIcon,
identifier: 'test@example.com',
diff --git a/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts b/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts
index 4bd6ba7260f..8fd97ba984c 100644
--- a/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts
+++ b/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts
@@ -12,10 +12,16 @@ import { chaosText } from '@/lib/chaos';
import { idleReverification } from './reverification';
const providers: UserProfileWeb3Provider[] = [
- { id: 'web3_metamask_signature', provider: 'MetaMask', iconUrl: 'https://img.clerk.com/static/metamask.svg' },
+ {
+ id: 'web3_metamask_signature',
+ provider: 'MetaMask',
+ iconId: 'metamask',
+ iconUrl: 'https://img.clerk.com/static/metamask.svg',
+ },
{
id: 'web3_coinbase_wallet_signature',
provider: 'Coinbase Wallet',
+ iconId: 'coinbase_wallet',
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
},
];
@@ -24,6 +30,7 @@ export const primaryWallet: UserProfileWeb3Wallet = {
id: 'wallet_1',
providerId: 'web3_metamask_signature',
provider: 'MetaMask',
+ iconId: 'metamask',
iconUrl: 'https://img.clerk.com/static/metamask.svg',
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
isPrimary: true,
@@ -34,6 +41,7 @@ export const secondaryWallet: UserProfileWeb3Wallet = {
id: 'wallet_2',
providerId: 'web3_coinbase_wallet_signature',
provider: 'Coinbase Wallet',
+ iconId: 'coinbase_wallet',
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
address: '0x1234567890abcdef1234567890abcdef12345678',
isVerified: true,
@@ -87,6 +95,7 @@ export function useWeb3WalletsFixture({
...current,
{
provider: provider.provider,
+ iconId: provider.iconId,
iconUrl: provider.iconUrl,
id: `wallet_${id}`,
providerId: id,
diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx
index a51b328ee87..344d0e27ad4 100644
--- a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx
+++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx
@@ -46,6 +46,7 @@ export function ConnectionError() {
{
id: 'apple',
provider: 'Apple',
+ iconId: 'apple',
iconUrl: 'https://img.clerk.com/static/apple.svg',
monochromeIcon: true,
connectError: 'Unable to connect. Please try again.',
diff --git a/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx b/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx
index 8420f89a827..6e1fad77714 100644
--- a/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx
+++ b/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx
@@ -35,7 +35,9 @@ export function ConnectOnly() {
export function UnverifiedWallet() {
const fixture = useWeb3WalletsFixture({
initialWallets: [{ ...primaryWallet, isPrimary: false, isVerified: false }],
- availableProviders: [{ id: 'web3_metamask_signature', provider: 'MetaMask', iconUrl: primaryWallet.iconUrl }],
+ availableProviders: [
+ { id: 'web3_metamask_signature', provider: 'MetaMask', iconId: 'metamask', iconUrl: primaryWallet.iconUrl },
+ ],
});
return (