diff --git a/.nx/version-plans/version-plan-1791834654983.md b/.nx/version-plans/version-plan-1791834654983.md
new file mode 100644
index 00000000000..91f7abe2b34
--- /dev/null
+++ b/.nx/version-plans/version-plan-1791834654983.md
@@ -0,0 +1,5 @@
+---
+gamut: patch
+---
+
+Fix Card border radius to align to theme values
diff --git a/packages/gamut/src/Card/__tests__/Card.test.tsx b/packages/gamut/src/Card/__tests__/Card.test.tsx
index 2016e097d01..a6242008ef0 100644
--- a/packages/gamut/src/Card/__tests__/Card.test.tsx
+++ b/packages/gamut/src/Card/__tests__/Card.test.tsx
@@ -1,4 +1,10 @@
-import { theme } from '@codecademy/gamut-styles';
+import {
+ coreTheme,
+ GamutProvider,
+ lxStudioTheme,
+ percipioTheme,
+ theme,
+} from '@codecademy/gamut-styles';
import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests';
import { renderHook } from '@testing-library/react';
@@ -37,7 +43,7 @@ describe('Card', () => {
describe('useCardElevation', () => {
it('maps the theme elevation tokens onto the motion variants', () => {
- const { animate, initial } = renderElevation('none');
+ const { animate, initial } = renderElevation('none', 'md');
expect(initial).toMatchObject({
boxShadow: theme.elevation['rest-shadow'],
@@ -50,7 +56,7 @@ describe('useCardElevation', () => {
});
it('uses the mirrored hover tokens for patternRight shadows', () => {
- const { animate } = renderElevation('patternRight');
+ const { animate } = renderElevation('patternRight', 'md');
expect(animate).toMatchObject({
boxShadow: theme.elevation['hoverMirrored-shadow'],
@@ -59,7 +65,7 @@ describe('useCardElevation', () => {
});
it('applies outline shadows but shares the elevation transforms', () => {
- const { animateOutline, initialOutline } = renderElevation('outline');
+ const { animateOutline, initialOutline } = renderElevation('outline', 'md');
expect(initialOutline.boxShadow).not.toBe(theme.elevation['rest-shadow']);
expect(initialOutline.transform).toBe(theme.elevation['rest-transform']);
@@ -68,10 +74,34 @@ describe('useCardElevation', () => {
});
it('passes borderRadius through to every variant', () => {
- const variants = renderElevation('none', '8px');
+ const variants = renderElevation('none', 'lg');
Object.values(variants).forEach((variant) => {
- expect(variant.borderRadius).toBe('8px');
+ expect(variant.borderRadius).toBe(theme.borderRadii.lg);
+ });
+ });
+
+ it.each([
+ ['coreTheme', coreTheme],
+ ['percipioTheme', percipioTheme],
+ ['lxStudioTheme', lxStudioTheme],
+ ])('resolves borderRadius from %s', (_name, activeTheme) => {
+ (['sm', 'md', 'lg'] as const).forEach((token) => {
+ const { result } = renderHook(() => useCardElevation('none', token), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ expect(result.current.initial.borderRadius).toBe(
+ activeTheme.borderRadii[token]
+ );
});
});
});
diff --git a/packages/gamut/src/Card/index.tsx b/packages/gamut/src/Card/index.tsx
index 413d80b269e..880ffd08bea 100644
--- a/packages/gamut/src/Card/index.tsx
+++ b/packages/gamut/src/Card/index.tsx
@@ -1,13 +1,11 @@
import { CheckerDense } from '@codecademy/gamut-patterns';
-import { borderRadii, Colors } from '@codecademy/gamut-styles';
+import { Colors } from '@codecademy/gamut-styles';
import * as React from 'react';
import { DynamicCardWrapper, MotionBox, StaticCardWrapper } from './elements';
import { patternFadeInOut, useCardElevation } from './styles';
import { CardProps } from './types';
-type BorderRadiusToken = keyof typeof borderRadii;
-
export const Card: React.FC = ({
children,
variant = 'default',
@@ -19,10 +17,7 @@ export const Card: React.FC = ({
height = '100%',
...rest
}) => {
- const defaultBorderRadius: BorderRadiusToken = isInteractive ? 'md' : 'none';
- const trueBorderRadius = borderRadius ?? defaultBorderRadius;
- const resolvedBorderRadius =
- borderRadii[trueBorderRadius as BorderRadiusToken];
+ const trueBorderRadius = borderRadius ?? (isInteractive ? 'md' : 'none');
const SelectedWrapper =
variant === 'default' ? DynamicCardWrapper : StaticCardWrapper;
@@ -30,7 +25,7 @@ export const Card: React.FC = ({
const hasPattern = shadow === 'patternLeft' || shadow === 'patternRight';
const isOutline = shadow === 'outline';
- const setHoverShadow = useCardElevation(shadow, resolvedBorderRadius);
+ const setHoverShadow = useCardElevation(shadow, trueBorderRadius);
const initialVariant = isOutline ? 'initialOutline' : 'initial';
const animateVariant = isOutline ? 'animateOutline' : 'animate';
diff --git a/packages/gamut/src/Card/styles.tsx b/packages/gamut/src/Card/styles.tsx
index 2a32935f8ab..32c7f6fc7b9 100644
--- a/packages/gamut/src/Card/styles.tsx
+++ b/packages/gamut/src/Card/styles.tsx
@@ -7,6 +7,8 @@ import {
import { StyleProps } from '@codecademy/variance';
import { Theme, useTheme } from '@emotion/react';
+import type { CardWrapperProps } from './types';
+
/**
* The theme's elevation scale is flattened to dashcase tokens (`rest-shadow`,
* `hoverMirrored-transform`, ...), so per-state groups don't exist on the
@@ -94,34 +96,41 @@ export const patternFadeInOut = {
export const useCardElevation = (
shadow: StyleProps['shadow'],
- borderRadius?: string
+ borderRadius: CardWrapperProps['borderRadius']
) => {
- const { elevation } = useTheme();
+ const { borderRadii, elevation } = useTheme();
const hoverState: ElevationState =
shadow === 'patternRight' ? 'hoverMirrored' : 'hover';
+ // framer-motion can only animate a concrete value, so responsive or
+ // function-valued radii are not resolved
+ const resolvedBorderRadius =
+ typeof borderRadius === 'string'
+ ? borderRadii[borderRadius as keyof Theme['borderRadii']] ?? borderRadius
+ : undefined;
+
return {
initial: {
...getElevationStyles(elevation, 'rest'),
- borderRadius,
+ borderRadius: resolvedBorderRadius,
transition: REST_TRANSITION,
},
animate: {
...getElevationStyles(elevation, hoverState),
- borderRadius,
+ borderRadius: resolvedBorderRadius,
transition: HOVER_TRANSITION,
},
// Outline styles are specific to Codecademy and will not be included in a theme
initialOutline: {
...getElevationStyles(elevation, 'rest'),
boxShadow: `-${SHADOW_OFFSET_INITIAL}px ${SHADOW_OFFSET_INITIAL}px 0 0px ${theme.colors['background-current']}, -${SHADOW_OFFSET_INITIAL}px ${SHADOW_OFFSET_INITIAL}px 0 1px ${theme.colors['border-primary']}`,
- borderRadius,
+ borderRadius: resolvedBorderRadius,
transition: REST_TRANSITION,
},
animateOutline: {
...getElevationStyles(elevation, 'hover'),
boxShadow: `-${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 0px ${theme.colors['shadow-primary']}, -${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 1px ${theme.colors['shadow-primary']}`,
- borderRadius,
+ borderRadius: resolvedBorderRadius,
transition: HOVER_TRANSITION,
},
};