Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/bordered-security-icons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/mosaic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<UserProfileProviderMedia
provider='OKX Wallet'
iconId='okx_wallet'
{...props}
/>,
);

expect(container.querySelector('.cl-provider-icon')).toHaveAttribute('data-provider', 'okx-wallet');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function UserProfileConnectProviderRowView({
<Section.Item>
<UserProfileProviderMedia
provider={provider.provider}
iconId={provider.iconId}
iconUrl={provider.iconUrl}
monochromeIcon={provider.monochromeIcon}
/>
Expand Down Expand Up @@ -104,6 +105,7 @@ export function UserProfileConnectedAccountRowView({
<Section.Item>
<UserProfileProviderMedia
provider={account.provider}
iconId={account.iconId}
iconUrl={account.iconUrl}
monochromeIcon={account.monochromeIcon}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function createProviderCatalog(
enabled: enabled.has(strategy),
display: {
provider: name,
iconId: provider,
iconUrl: iconImageUrl(provider),
monochromeIcon: isMonochromeProvider(provider),
},
Expand All @@ -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 [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ConnectedAccountActionResult = 'redirecting' | void;

export interface ConnectedAccountProviderDisplay {
provider: string;
iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export function UserProfileAddMfaView({ methods, onSelect }: UserProfileAddMfaVi
}
>
<Item.Media>
<IconFrame filled>
<IconFrame
filled
bordered={false}
>
<Icon glyph={icons[type]} />
</IconFrame>
</Item.Media>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ type UserProfileProviderIconProps = { iconUrl: string; glyph?: never } | { iconU
export function UserProfileProviderIcon(props: UserProfileProviderIconProps) {
return (
<Section.Media size='lg'>
<IconFrame>
<IconFrame
filled
bordered={false}
>
{'iconUrl' in props ? (
<img
alt=''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import * as stylex from '@stylexjs/stylex';
import { colorVars, radiusVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
icon: { display: 'block', objectFit: 'contain', height: space['6'], width: space['6'] },
icon: { display: 'block', objectFit: 'contain', height: space['5'], width: space['5'] },
maskedIcon: {
backgroundColor: 'currentColor',
display: 'block',
maskPosition: 'center',
maskRepeat: 'no-repeat',
maskSize: 'contain',
height: space['6'],
width: space['6'],
height: space['5'],
width: space['5'],
},
appleOffset: {
transform: 'translateY(-1px)',
},
maskImage: (url: string) => ({ maskImage: `url(${JSON.stringify(url)})` }),
fallback: {
Expand All @@ -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'],
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Section.Media size='lg'>
<IconFrame>
<IconFrame
filled
bordered={false}
>
{url && monochromeIcon ? (
<span
aria-hidden
{...stylex.props(styles.maskedIcon, styles.maskImage(url))}
{...mergeStyleProps(slot, stylex.props(styles.maskedIcon, styles.maskImage(url), offset))}
/>
) : url ? (
<img
src={url}
alt=''
aria-hidden
{...stylex.props(styles.icon)}
{...mergeStyleProps(slot, stylex.props(styles.icon, offset))}
/>
) : (
<span
aria-hidden
{...stylex.props(styles.fallback)}
{...mergeStyleProps(slot, stylex.props(styles.fallback))}
>
{provider.trim().charAt(0).toUpperCase()}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,15 +20,17 @@ const icons = {

export function UserProfileSecurityIcon({ name }: { name: UserProfileSecurityIconName }) {
return (
<Section.Media
size='lg'
xstyle={styles.media}
>
<Icon
glyph={icons[name]}
size='lg'
xstyle={styles.icon}
/>
<Section.Media size='lg'>
<IconFrame
filled
bordered={false}
>
<Icon
glyph={icons[name]}
size='lg'
xstyle={styles.icon}
/>
</IconFrame>
</Section.Media>
);
}
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function UserProfileWeb3WalletRowView({
{wallet.provider ? (
<UserProfileProviderMedia
provider={wallet.provider}
iconId={wallet.iconId}
iconUrl={wallet.iconUrl}
monochromeIcon={wallet.monochromeIcon}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface UserProfileWeb3Provider {
id: Web3Strategy;
walletPicker?: 'solana';
provider: string;
iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
connectError?: string;
Expand All @@ -21,6 +22,7 @@ export interface UserProfileWeb3Wallet {
walletPicker?: 'solana';
canVerify?: boolean;
provider?: string;
iconId?: string;
iconUrl?: string;
monochromeIcon?: boolean;
isPrimary?: boolean;
Expand Down
5 changes: 2 additions & 3 deletions packages/mosaic/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,8 +64,6 @@ export type MosaicComponentProps<Tag extends keyof React.JSX.IntrinsicElements>

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.
*
Expand All @@ -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 };
}
Expand Down
17 changes: 17 additions & 0 deletions packages/mosaic/src/utils/__tests__/kebab-case.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
12 changes: 12 additions & 0 deletions packages/mosaic/src/utils/kebab-case.ts
Original file line number Diff line number Diff line change
@@ -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) ?? []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading