diff --git a/.changeset/clear-adults-pump.md b/.changeset/clear-adults-pump.md new file mode 100644 index 00000000000..c760fa98236 --- /dev/null +++ b/.changeset/clear-adults-pump.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Updates development mode indicator styling. diff --git a/packages/ui/src/components/Checkout/CheckoutForm.tsx b/packages/ui/src/components/Checkout/CheckoutForm.tsx index 935132c9981..9386f563663 100644 --- a/packages/ui/src/components/Checkout/CheckoutForm.tsx +++ b/packages/ui/src/components/Checkout/CheckoutForm.tsx @@ -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, @@ -370,17 +371,10 @@ export const PayWithTestPaymentMethod = () => { flexDirection: 'column', rowGap: t.space.$2, position: 'relative', + overflow: 'hidden', })} > - ({ - 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', - })} - /> + ({ alignItems: 'center', diff --git a/packages/ui/src/components/PaymentMethods/TestPaymentMethod.tsx b/packages/ui/src/components/PaymentMethods/TestPaymentMethod.tsx index 03bffce85e4..b0c21ae02ad 100644 --- a/packages/ui/src/components/PaymentMethods/TestPaymentMethod.tsx +++ b/packages/ui/src/components/PaymentMethods/TestPaymentMethod.tsx @@ -1,3 +1,4 @@ +import { DevModeOverlay } from '@/ui/elements/DevModeNotice'; import { LineItems } from '@/ui/elements/LineItems'; import { Box, localizationKeys, Text, useLocalizations } from '../../customizables'; @@ -17,17 +18,10 @@ export const TestPaymentMethod = () => { flexDirection: 'column', rowGap: t.space.$2, position: 'relative', + overflow: 'hidden', })} > - ({ - 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', - })} - /> + - {withDevOverlay && } + {withDevOverlay && } ({ - 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', })} > diff --git a/packages/ui/src/elements/Card/CardFooter.tsx b/packages/ui/src/elements/Card/CardFooter.tsx index 4ba2f2ab849..37d68ca08b4 100644 --- a/packages/ui/src/elements/Card/CardFooter.tsx +++ b/packages/ui/src/elements/Card/CardFooter.tsx @@ -67,6 +67,8 @@ export const CardFooter = React.forwardRef((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 diff --git a/packages/ui/src/elements/Card/CardRoot.tsx b/packages/ui/src/elements/Card/CardRoot.tsx index 92ee25fc983..252ae7ddc35 100644 --- a/packages/ui/src/elements/Card/CardRoot.tsx +++ b/packages/ui/src/elements/Card/CardRoot.tsx @@ -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', + }, }, }); diff --git a/packages/ui/src/elements/Card/__tests__/CardRoot.test.tsx b/packages/ui/src/elements/Card/__tests__/CardRoot.test.tsx index db0bc6b47df..4bedd883f84 100644 --- a/packages/ui/src/elements/Card/__tests__/CardRoot.test.tsx +++ b/packages/ui/src/elements/Card/__tests__/CardRoot.test.tsx @@ -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; @@ -48,6 +51,35 @@ const renderCard = ({ elevation, globalAppearance, appearance, withModal }: Rend ); }; +const devEnvironment = { + displayConfig: { + branded: false, + showDevModeWarning: true, + }, + isDevelopmentOrStaging: () => true, +}; + +const renderCardWithFooter = ({ appearance }: Pick = {}) => { + return render( + + + + + + + + + + + + + , + ); +}; + describe('CardRoot augmentedAppearance — raised (default)', () => { beforeEach(() => { captured = undefined as any; @@ -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', () => { diff --git a/packages/ui/src/elements/DevModeNotice.tsx b/packages/ui/src/elements/DevModeNotice.tsx index 93ab3c10233..577b34b19f0 100644 --- a/packages/ui/src/elements/DevModeNotice.tsx +++ b/packages/ui/src/elements/DevModeNotice.tsx @@ -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 += ``; + } + } + + const svg = `${rects}`; + 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) { @@ -17,15 +57,52 @@ export const DevModeOverlay = (props: DevModeOverlayProps) => { return ( ({ + 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, + }} + > + + ({ + 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', + })} + /> + + ({ + position: 'absolute', + insetInline: 0, + bottom: 0, + height: DEV_MODE_GRID.lineHeight, + backgroundColor: t.colors.$warning500, + maskImage: lineMask, + WebkitMaskImage: lineMask, + })} + /> + ); }; @@ -40,10 +117,11 @@ export const DevModeNotice = (props: DevModeNoticeProps) => { return ( ({ color: t.colors.$warning500, - fontWeight: t.fontWeights.$semibold, padding: t.space.$1x5, }), sx, diff --git a/packages/ui/src/elements/Navbar.tsx b/packages/ui/src/elements/Navbar.tsx index 538a83474cd..ba606002a6f 100644 --- a/packages/ui/src/elements/Navbar.tsx +++ b/packages/ui/src/elements/Navbar.tsx @@ -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)`, + })} /> );