Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/clear-adults-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Updates development mode indicator styling.
12 changes: 3 additions & 9 deletions packages/ui/src/components/Checkout/CheckoutForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Drawer } from '@/ui/elements/Drawer';
import { LineItems } from '@/ui/elements/LineItems';
import { SegmentedControl } from '@/ui/elements/SegmentedControl';
import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select';
import { DevModeOverlay } from '@/ui/elements/DevModeNotice';
import { Tooltip } from '@/ui/elements/Tooltip';
import {
getCheckoutSeatUnitTotal,
Expand Down Expand Up @@ -370,17 +371,10 @@ export const PayWithTestPaymentMethod = () => {
flexDirection: 'column',
rowGap: t.space.$2,
position: 'relative',
overflow: 'hidden',
})}
>
<Box
sx={t => ({
position: 'absolute',
inset: 0,
background: `repeating-linear-gradient(-45deg,${t.colors.$warningAlpha100},${t.colors.$warningAlpha100} 6px,${t.colors.$warningAlpha150} 6px,${t.colors.$warningAlpha150} 12px)`,
maskImage: `linear-gradient(transparent 20%, black)`,
pointerEvents: 'none',
})}
/>
<DevModeOverlay />
<Flex
sx={t => ({
alignItems: 'center',
Expand Down
12 changes: 3 additions & 9 deletions packages/ui/src/components/PaymentMethods/TestPaymentMethod.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DevModeOverlay } from '@/ui/elements/DevModeNotice';
import { LineItems } from '@/ui/elements/LineItems';

import { Box, localizationKeys, Text, useLocalizations } from '../../customizables';
Expand All @@ -17,17 +18,10 @@ export const TestPaymentMethod = () => {
flexDirection: 'column',
rowGap: t.space.$2,
position: 'relative',
overflow: 'hidden',
})}
>
<Box
sx={t => ({
position: 'absolute',
inset: 0,
background: `repeating-linear-gradient(-45deg,${t.colors.$warningAlpha100},${t.colors.$warningAlpha100} 6px,${t.colors.$warningAlpha150} 6px,${t.colors.$warningAlpha150} 12px)`,
maskImage: `linear-gradient(transparent 20%, black)`,
pointerEvents: 'none',
})}
/>
<DevModeOverlay />
<Box
sx={{
display: 'flex',
Expand Down
6 changes: 2 additions & 4 deletions packages/ui/src/elements/Card/CardClerkAndPagesTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,18 @@ export const CardClerkAndPagesTag = React.memo(
{
width: '100%',
position: 'relative',
isolation: 'isolate',
},
outerSx,
]}
>
{withDevOverlay && <DevModeOverlay gradient={0} />}
{withDevOverlay && <DevModeOverlay />}
<Col
sx={t => ({
gap: displayConfig.branded || withFooterPages ? t.space.$2 : 0,
gap: displayConfig.branded || withFooterPages ? t.space.$4 : 0,
marginInline: 'auto',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
position: 'relative',
})}
>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/elements/Card/CardFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>((pro
withFooterPages={showSponsorAndLinks && !isProfileFooter}
devModeNoticeSx={t => ({
padding: t.space.$none,
// The footer adds $4 (16px) below this section; pull the notice down so it sits ~12px from the bottom.
marginBottom: `calc(${t.space.$1} * -1)`,
})}
outerSx={isProfileFooter ? profileCardFooterStyles : undefined}
withDevOverlay
Expand Down
10 changes: 10 additions & 0 deletions packages/ui/src/elements/Card/CardRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ const getFlushElements = (t: InternalTheme) => ({
borderTopWidth: 0,
padding: 0,
},
'& [data-clerk-dev-mode-overlay]': {
display: 'none',
},
'& [data-clerk-dev-mode-notice]': {
alignSelf: 'center',
marginBottom: `calc(${t.space.$2} * -1)`,
marginInline: 'auto',
padding: 0,
textAlign: 'center',
},
},
});

Expand Down
61 changes: 60 additions & 1 deletion packages/ui/src/elements/Card/__tests__/CardRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { render } from '@testing-library/react';
import { ClerkInstanceContext } from '@clerk/shared/react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { beforeEach, describe, expect, it } from 'vitest';

import { EnvironmentProvider } from '@/contexts';
import { AppearanceProvider, useAppearance } from '@/customizables';
import type { ParsedAppearance } from '@/customizables/parseAppearance';
import { FlowMetadataProvider } from '@/elements/contexts';
import { ModalContext } from '@/elements/Modal';
import { InternalThemeProvider } from '@/styledSystem';

import { CardFooter } from '../CardFooter';
import { CardRoot } from '../CardRoot';

let captured: ParsedAppearance;
Expand Down Expand Up @@ -48,6 +51,35 @@ const renderCard = ({ elevation, globalAppearance, appearance, withModal }: Rend
);
};

const devEnvironment = {
displayConfig: {
branded: false,
showDevModeWarning: true,
},
isDevelopmentOrStaging: () => true,
};

const renderCardWithFooter = ({ appearance }: Pick<RenderCardOptions, 'appearance'> = {}) => {
return render(
<ClerkInstanceContext.Provider value={{ value: { client: {}, user: {} } as any }}>
<EnvironmentProvider value={devEnvironment as any}>
<AppearanceProvider
appearanceKey='signIn'
appearance={appearance as any}
>
<InternalThemeProvider>
<FlowMetadataProvider flow='signIn'>
<CardRoot>
<CardFooter />
</CardRoot>
</FlowMetadataProvider>
</InternalThemeProvider>
</AppearanceProvider>
</EnvironmentProvider>
</ClerkInstanceContext.Provider>,
);
};

describe('CardRoot augmentedAppearance — raised (default)', () => {
beforeEach(() => {
captured = undefined as any;
Expand Down Expand Up @@ -160,6 +192,33 @@ describe('CardRoot augmentedAppearance — flush', () => {
expect(captured.parsedElements[2].footer?.background).toBe('blue');
expect(captured.parsedElements[2].footer?.['>:first-of-type']?.padding).toBe('16px');
});

it('adds flush-only dev mode indicator overrides', () => {
renderCard({ elevation: 'flush' });
expect(captured.parsedElements[1].footer?.['& [data-clerk-dev-mode-overlay]']?.display).toBe('none');
expect(captured.parsedElements[1].footer?.['& [data-clerk-dev-mode-notice]']?.textAlign).toBe('center');
expect(captured.parsedElements[1].footer?.['& [data-clerk-dev-mode-notice]']?.padding).toBe(0);
});
});

describe('CardRoot dev mode indicator rendering', () => {
it('raised cards render both the patterned overlay and development mode text markers', () => {
const { container } = renderCardWithFooter();
const notice = screen.getByText('Development mode');

expect(notice.hasAttribute('data-clerk-dev-mode-notice')).toBe(true);
expect(container.querySelector('[data-clerk-dev-mode-overlay]')).not.toBeNull();
});

it('flush cards keep the development mode text and hide the patterned overlay through flush styling', () => {
const { container } = renderCardWithFooter({ appearance: { options: { elevation: 'flush' } } });
const notice = screen.getByText('Development mode');
const overlay = container.querySelector('[data-clerk-dev-mode-overlay]');

expect(notice.hasAttribute('data-clerk-dev-mode-notice')).toBe(true);
expect(overlay).not.toBeNull();
expect(getComputedStyle(overlay as HTMLElement).display).toBe('none');
});
});

describe('CardRoot elevation resolution priority', () => {
Expand Down
100 changes: 89 additions & 11 deletions packages/ui/src/elements/DevModeNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,52 @@ import type { ThemableCssProp } from '@/ui/styledSystem';
import { Box, Text } from '../customizables';
import { useDevMode } from '../hooks/useDevMode';

type DevModeOverlayProps = {
gradient?: number;
const DEV_MODE_GRID = {
width: 400,
height: 60,
squareSize: 1.5,
gap: 2,
minOpacity: 0.1,
maxOpacity: 0.7,
contrast: 2,
fadeWidth: 45, // % of width; horizontal radius of the oval — smaller pulls the sides in / curves them more
fadeHeight: 20, // px; vertical radius of the oval — smaller makes the top edge curve down more sharply
fadeCenterY: 85, // % of height; vertical center of the oval — 100 = on the bottom line; >100 pushes it below for a gentler top arc
fadeStrength: 0.98, // 0–1; how strongly the oval fade dims toward its edge (1 = fully transparent, 0 = no fade)
fadeCenter: 0.82, // 0–1; mask opacity at the very center — below 1 fades the middle a touch so the falloff is gentler
lineHeight: 1,
} as const;

const cellRandom = (x: number, y: number) => {
let h = (x * 374761393 + y * 668265263 + 2246822519) | 0;
h = (h ^ (h >>> 13)) * 1274126177;
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
};

const buildDevModeGridTile = (options: typeof DEV_MODE_GRID) => {
const step = options.squareSize + options.gap;
const tileWidth = Math.max(1, Math.round(options.width / step)) * step;
const tileHeight = options.height - options.lineHeight;
let rects = '';

for (let y = 0; y < tileHeight; y += step) {
for (let x = 0; x < tileWidth; x += step) {
const opacity =
options.minOpacity + (options.maxOpacity - options.minOpacity) * Math.pow(cellRandom(x, y), options.contrast);
rects += `<rect x="${x}" y="${y}" width="${options.squareSize}" height="${options.squareSize}" fill="#fff" opacity="${opacity.toFixed(2)}"/>`;
}
}

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${tileWidth}" height="${tileHeight}">${rects}</svg>`;
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
};

export const DevModeOverlay = (props: DevModeOverlayProps) => {
const { gradient = 60 } = props;
const gridTile = buildDevModeGridTile(DEV_MODE_GRID);
// A single oval mask: its curve fades the grid on the top AND the sides, so the top edge curves down toward the corners.
const gridMask = `radial-gradient(ellipse ${DEV_MODE_GRID.fadeWidth}% ${(DEV_MODE_GRID.fadeHeight / DEV_MODE_GRID.height) * 100}% at 50% ${DEV_MODE_GRID.fadeCenterY}%, rgba(0,0,0,${DEV_MODE_GRID.fadeCenter}) 0%, rgba(0,0,0,${1 - DEV_MODE_GRID.fadeStrength}) 100%)`;
const lineMask = 'linear-gradient(to right, transparent 2%, #000 50%, transparent 98%)';

export const DevModeOverlay = () => {
const { showDevModeNotice } = useDevMode();

if (!showDevModeNotice) {
Expand All @@ -17,15 +57,52 @@ export const DevModeOverlay = (props: DevModeOverlayProps) => {

return (
<Box
sx={t => ({
data-clerk-dev-mode-overlay=''
sx={{
userSelect: 'none',
pointerEvents: 'none',
inset: 0,
insetInline: 0,
bottom: 0,
position: 'absolute',
background: `repeating-linear-gradient(-45deg,${t.colors.$warningAlpha100},${t.colors.$warningAlpha100} 6px,${t.colors.$warningAlpha150} 6px,${t.colors.$warningAlpha150} 12px)`,
maskImage: `linear-gradient(transparent ${gradient}%, black)`,
})}
/>
height: DEV_MODE_GRID.height,
}}
>
<Box
sx={{
position: 'absolute',
inset: 0,
maskImage: gridMask,
WebkitMaskImage: gridMask,
}}
>
<Box
sx={t => ({
position: 'absolute',
top: 0,
insetInline: 0,
bottom: DEV_MODE_GRID.lineHeight,
backgroundColor: t.colors.$warning500,
maskImage: gridTile,
maskRepeat: 'repeat-x',
maskPosition: 'left bottom',
WebkitMaskImage: gridTile,
WebkitMaskRepeat: 'repeat-x',
WebkitMaskPosition: 'left bottom',
})}
/>
</Box>
<Box
sx={t => ({
position: 'absolute',
insetInline: 0,
bottom: 0,
height: DEV_MODE_GRID.lineHeight,
backgroundColor: t.colors.$warning500,
maskImage: lineMask,
WebkitMaskImage: lineMask,
})}
/>
</Box>
);
};

Expand All @@ -40,10 +117,11 @@ export const DevModeNotice = (props: DevModeNoticeProps) => {

return (
<Text
data-clerk-dev-mode-notice=''
variant='buttonSmall'
sx={[
t => ({
color: t.colors.$warning500,
fontWeight: t.fontWeights.$semibold,
padding: t.space.$1x5,
}),
sx,
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/elements/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ const NavbarContainer = (
sx={{
width: 'fit-content',
}}
devModeNoticeSx={t => ({
// Match the card footer so the notice sits the same distance from the bottom as sign-in/sign-up.
padding: t.space.$none,
marginBottom: `calc(${t.space.$1} * -1)`,
})}
/>
</Col>
);
Expand Down
Loading