From 9ccf5c9cd4f0477f14962d8e9f426e861c240d85 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 10 Aug 2026 16:02:35 -0400 Subject: [PATCH 1/6] created elevation scales for respective themes --- .../gamut-styles/src/variables/elevation.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/gamut-styles/src/variables/elevation.ts diff --git a/packages/gamut-styles/src/variables/elevation.ts b/packages/gamut-styles/src/variables/elevation.ts new file mode 100644 index 0000000000..63abf96228 --- /dev/null +++ b/packages/gamut-styles/src/variables/elevation.ts @@ -0,0 +1,56 @@ +import { flattenScale } from '@codecademy/variance'; + +import { coreTheme } from '../themes/core'; +import { lxStudioTheme } from '../themes/lxStudio'; +import { percipioTheme } from '../themes/percipio'; + +const coreElevationScale = { + rest: { + 'box-shadow': `0 0 0 0 ${coreTheme.colors['shadow-primary']}`, + transform: 'translate(4px, -4px)', + }, + hover: { + 'box-shadow': `-8px 8px 0 0 ${coreTheme.colors['shadow-primary']}`, + transform: 'translate(4px, -4px)', + }, + 'hover-mirrored': { + 'box-shadow': `-8px 8px 0 0 ${coreTheme.colors['shadow-primary']}`, + transform: 'translate(4px, -4px)', + }, +}; + +export const coreElevation = flattenScale(coreElevationScale); + +const lxStudioElevationScale = { + rest: { + 'box-shadow': `0 0 0 0 ${lxStudioTheme.colors['shadow-primary']}`, + transform: 'none', + }, + hover: { + 'box-shadow': `-8px 8px 0 0 ${lxStudioTheme.colors['shadow-primary']}`, + transform: 'translate(4px, -4px)', + }, + 'hover-mirrored': { + 'box-shadow': `-8px 8px 0 0 ${lxStudioTheme.colors['shadow-primary']}`, + transform: 'translate(4px, -4px)', + }, +}; + +export const lxStudioElevation = flattenScale(lxStudioElevationScale); + +const percipioElevationScale = { + rest: { + 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, + 0 2px 7px 0 ${percipioTheme.colors['shadow-primary']}`, + }, + hover: { + 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, + 0 2px 11px 0 ${percipioTheme.colors['shadow-primary']}`, + }, + 'hover-mirrored': { + 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, + 0 2px 11px 0 ${percipioTheme.colors['shadow-primary']}`, + }, +}; + +export const percipioElevation = flattenScale(percipioElevationScale); From e488f5be55ffafffc7db208f6fc33e7976965100 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 10 Aug 2026 16:17:19 -0400 Subject: [PATCH 2/6] move scale to theme file --- packages/gamut-styles/src/themes/core.ts | 18 ++++++ packages/gamut-styles/src/themes/percipio.ts | 14 +++++ .../gamut-styles/src/variables/elevation.ts | 56 ------------------- 3 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 packages/gamut-styles/src/variables/elevation.ts diff --git a/packages/gamut-styles/src/themes/core.ts b/packages/gamut-styles/src/themes/core.ts index 3a6da84433..8ce6a53b09 100644 --- a/packages/gamut-styles/src/themes/core.ts +++ b/packages/gamut-styles/src/themes/core.ts @@ -140,6 +140,24 @@ export const coreTheme = createTheme({ 1: `1px solid ${colors['border-primary']}`, 2: `2px solid ${colors['border-primary']}`, })) + .addScale('elevation', ({ colors }: { colors: Record }) => { + const shadowPrimary = colors['shadow-primary']; + const offset = 8; + const lift = 4; + const shadow = (x: number) => `${x}px ${offset}px 0 0 ${shadowPrimary}`; + + return { + rest: { shadow: `0 0 0 0 ${shadowPrimary}`, transform: 'none' }, + hover: { + shadow: shadow(-offset), + transform: `translate(${lift}px, -${lift}px)`, + }, + 'hover-mirrored': { + shadow: shadow(offset), + transform: `translate(-${lift}px, -${lift}px)`, + }, + }; + }) .createScaleVariables('elements') .addName('core') .build(); diff --git a/packages/gamut-styles/src/themes/percipio.ts b/packages/gamut-styles/src/themes/percipio.ts index 71b6a5416b..43a75f3e90 100644 --- a/packages/gamut-styles/src/themes/percipio.ts +++ b/packages/gamut-styles/src/themes/percipio.ts @@ -62,6 +62,20 @@ export const percipioTheme = createTheme({ }, }, }) + .addScale('elevation', ({ colors }: { colors: Record }) => { + const shadowPrimary = colors['shadow-primary']; + const shadowSecondary = colors['shadow-secondary']; + const hover = `0 1px 4px 0 ${shadowPrimary}, 0 2px 11px 0 ${shadowSecondary}`; + + return { + rest: { + shadow: `0 1px 4px 0 ${shadowPrimary}, 0 2px 7px 0 ${shadowPrimary}`, + transform: 'none', + }, + hover: { shadow: hover, transform: 'none' }, + 'hover-mirrored': { shadow: hover, transform: 'none' }, + }; + }) .addName('percipio') .build(); diff --git a/packages/gamut-styles/src/variables/elevation.ts b/packages/gamut-styles/src/variables/elevation.ts deleted file mode 100644 index 63abf96228..0000000000 --- a/packages/gamut-styles/src/variables/elevation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { flattenScale } from '@codecademy/variance'; - -import { coreTheme } from '../themes/core'; -import { lxStudioTheme } from '../themes/lxStudio'; -import { percipioTheme } from '../themes/percipio'; - -const coreElevationScale = { - rest: { - 'box-shadow': `0 0 0 0 ${coreTheme.colors['shadow-primary']}`, - transform: 'translate(4px, -4px)', - }, - hover: { - 'box-shadow': `-8px 8px 0 0 ${coreTheme.colors['shadow-primary']}`, - transform: 'translate(4px, -4px)', - }, - 'hover-mirrored': { - 'box-shadow': `-8px 8px 0 0 ${coreTheme.colors['shadow-primary']}`, - transform: 'translate(4px, -4px)', - }, -}; - -export const coreElevation = flattenScale(coreElevationScale); - -const lxStudioElevationScale = { - rest: { - 'box-shadow': `0 0 0 0 ${lxStudioTheme.colors['shadow-primary']}`, - transform: 'none', - }, - hover: { - 'box-shadow': `-8px 8px 0 0 ${lxStudioTheme.colors['shadow-primary']}`, - transform: 'translate(4px, -4px)', - }, - 'hover-mirrored': { - 'box-shadow': `-8px 8px 0 0 ${lxStudioTheme.colors['shadow-primary']}`, - transform: 'translate(4px, -4px)', - }, -}; - -export const lxStudioElevation = flattenScale(lxStudioElevationScale); - -const percipioElevationScale = { - rest: { - 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, - 0 2px 7px 0 ${percipioTheme.colors['shadow-primary']}`, - }, - hover: { - 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, - 0 2px 11px 0 ${percipioTheme.colors['shadow-primary']}`, - }, - 'hover-mirrored': { - 'box-shadow': `0 1px 4px 0 ${percipioTheme.colors['shadow-primary']}, - 0 2px 11px 0 ${percipioTheme.colors['shadow-primary']}`, - }, -}; - -export const percipioElevation = flattenScale(percipioElevationScale); From 677f893384bddf4907c6fd9f1824db2049f23109 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 12 Aug 2026 12:12:11 -0400 Subject: [PATCH 3/6] update elevation scale and update card styles to use the scale --- packages/gamut-styles/src/themes/core.ts | 4 +- packages/gamut-styles/src/themes/percipio.ts | 2 +- packages/gamut/src/Card/index.tsx | 7 +- packages/gamut/src/Card/styles.tsx | 105 +++++++++---------- 4 files changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/gamut-styles/src/themes/core.ts b/packages/gamut-styles/src/themes/core.ts index 8ce6a53b09..1aa715dc1c 100644 --- a/packages/gamut-styles/src/themes/core.ts +++ b/packages/gamut-styles/src/themes/core.ts @@ -152,7 +152,9 @@ export const coreTheme = createTheme({ shadow: shadow(-offset), transform: `translate(${lift}px, -${lift}px)`, }, - 'hover-mirrored': { + // camelCase: variance's `LiteralPaths` splits token paths on `-`, so a + // hyphenated key would resolve to `never` and drop out of the theme type. + hoverMirrored: { shadow: shadow(offset), transform: `translate(-${lift}px, -${lift}px)`, }, diff --git a/packages/gamut-styles/src/themes/percipio.ts b/packages/gamut-styles/src/themes/percipio.ts index 43a75f3e90..d6c60f7393 100644 --- a/packages/gamut-styles/src/themes/percipio.ts +++ b/packages/gamut-styles/src/themes/percipio.ts @@ -73,7 +73,7 @@ export const percipioTheme = createTheme({ transform: 'none', }, hover: { shadow: hover, transform: 'none' }, - 'hover-mirrored': { shadow: hover, transform: 'none' }, + hoverMirrored: { shadow: hover, transform: 'none' }, }; }) .addName('percipio') diff --git a/packages/gamut/src/Card/index.tsx b/packages/gamut/src/Card/index.tsx index 4098940d18..30cb2b4801 100644 --- a/packages/gamut/src/Card/index.tsx +++ b/packages/gamut/src/Card/index.tsx @@ -3,7 +3,7 @@ import { borderRadii, Colors } from '@codecademy/gamut-styles'; import * as React from 'react'; import { DynamicCardWrapper, MotionBox, StaticCardWrapper } from './elements'; -import { hoverShadowLeft, hoverShadowRight, patternFadeInOut } from './styles'; +import { patternFadeInOut, useCardElevation } from './styles'; import { CardProps } from './types'; type BorderRadiusToken = keyof typeof borderRadii; @@ -30,10 +30,7 @@ export const Card: React.FC = ({ const hasPattern = shadow === 'patternLeft' || shadow === 'patternRight'; const isOutline = shadow === 'outline'; - const setHoverShadow = - shadow === 'patternRight' - ? hoverShadowRight(resolvedBorderRadius) - : hoverShadowLeft(resolvedBorderRadius); + const setHoverShadow = useCardElevation(shadow, resolvedBorderRadius); 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 1a080095ea..919aa858dd 100644 --- a/packages/gamut/src/Card/styles.tsx +++ b/packages/gamut/src/Card/styles.tsx @@ -1,8 +1,19 @@ import { theme, timingValues, variant } from '@codecademy/gamut-styles'; +import { StyleProps } from '@codecademy/variance'; +import { useTheme } from '@emotion/react'; const SHADOW_OFFSET = 8; const SHADOW_OFFSET_INITIAL = 6; -const TRANSFORM_OFFSET = 4; + +const REST_TRANSITION = { + duration: timingValues.fast / 1000, + ease: 'easeOut', +} as const; + +const HOVER_TRANSITION = { + duration: timingValues.fast / 1000, + ease: 'easeIn', +} as const; export const cardVariants = variant({ defaultVariant: 'default', @@ -61,59 +72,47 @@ export const patternFadeInOut = { }, }; -export const hoverShadowLeft = (borderRadius?: string) => ({ - initial: { - boxShadow: `0px 0px 0 ${theme.colors['shadow-primary']}`, - borderRadius, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeOut', +/** + * Motion variants for a Card's hover elevation, read from the active theme's + * `elevation` scale so each theme controls its own shadow and lift. + * `patternRight` cards cast their shadow on the opposite side, so they use the + * `hoverMirrored` tokens. + */ +export const useCardElevation = ( + shadow: StyleProps['shadow'], + borderRadius?: string +) => { + const { elevation } = useTheme(); + const isMirrored = shadow === 'patternRight'; + + return { + initial: { + boxShadow: elevation['rest-shadow'], + transform: elevation['rest-transform'], + borderRadius, + transition: REST_TRANSITION, }, - }, - initialOutline: { - 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, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeOut', + initialOutline: { + 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']}`, + transform: elevation['rest-transform'], + borderRadius, + transition: REST_TRANSITION, }, - }, - animate: { - transform: `translate(${TRANSFORM_OFFSET}px, -${TRANSFORM_OFFSET}px)`, - boxShadow: `-${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 ${theme.colors['shadow-primary']}`, - borderRadius, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeIn', + animate: { + boxShadow: isMirrored + ? elevation['hoverMirrored-shadow'] + : elevation['hover-shadow'], + transform: isMirrored + ? elevation['hoverMirrored-transform'] + : elevation['hover-transform'], + borderRadius, + transition: HOVER_TRANSITION, }, - }, - animateOutline: { - transform: `translate(${TRANSFORM_OFFSET}px, -${TRANSFORM_OFFSET}px)`, - 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, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeIn', + animateOutline: { + 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']}`, + transform: elevation['hover-transform'], + borderRadius, + transition: HOVER_TRANSITION, }, - }, -}); - -export const hoverShadowRight = (borderRadius?: string) => ({ - initial: { - boxShadow: `0px 0px 0 ${theme.colors['shadow-primary']}`, - borderRadius, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeOut', - }, - }, - animate: { - transform: `translate(-${TRANSFORM_OFFSET}px, -${TRANSFORM_OFFSET}px)`, - boxShadow: `${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 ${theme.colors['shadow-primary']}`, - borderRadius, - transition: { - duration: timingValues.fast / 1000, - ease: 'easeIn', - }, - }, -}); + }; +}; From 71aa7fcc4056c01f370817e56382d65538af0309 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 12 Aug 2026 14:33:07 -0400 Subject: [PATCH 4/6] go back to separate files for elevation --- packages/gamut-styles/src/themes/core.ts | 22 +----- packages/gamut-styles/src/themes/percipio.ts | 16 +--- .../gamut-styles/src/variables/elevation.ts | 74 +++++++++++++++++++ packages/gamut-styles/src/variables/index.ts | 1 + 4 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 packages/gamut-styles/src/variables/elevation.ts diff --git a/packages/gamut-styles/src/themes/core.ts b/packages/gamut-styles/src/themes/core.ts index 1aa715dc1c..c9b52b390c 100644 --- a/packages/gamut-styles/src/themes/core.ts +++ b/packages/gamut-styles/src/themes/core.ts @@ -3,6 +3,7 @@ import { createTheme } from '@codecademy/variance'; import { borderRadii, containerQueries, + coreElevation, corePalette, elements, fontFamily, @@ -140,26 +141,7 @@ export const coreTheme = createTheme({ 1: `1px solid ${colors['border-primary']}`, 2: `2px solid ${colors['border-primary']}`, })) - .addScale('elevation', ({ colors }: { colors: Record }) => { - const shadowPrimary = colors['shadow-primary']; - const offset = 8; - const lift = 4; - const shadow = (x: number) => `${x}px ${offset}px 0 0 ${shadowPrimary}`; - - return { - rest: { shadow: `0 0 0 0 ${shadowPrimary}`, transform: 'none' }, - hover: { - shadow: shadow(-offset), - transform: `translate(${lift}px, -${lift}px)`, - }, - // camelCase: variance's `LiteralPaths` splits token paths on `-`, so a - // hyphenated key would resolve to `never` and drop out of the theme type. - hoverMirrored: { - shadow: shadow(offset), - transform: `translate(-${lift}px, -${lift}px)`, - }, - }; - }) + .addScale('elevation', coreElevation) .createScaleVariables('elements') .addName('core') .build(); diff --git a/packages/gamut-styles/src/themes/percipio.ts b/packages/gamut-styles/src/themes/percipio.ts index d6c60f7393..a6fda37992 100644 --- a/packages/gamut-styles/src/themes/percipio.ts +++ b/packages/gamut-styles/src/themes/percipio.ts @@ -2,6 +2,7 @@ import { createTheme } from '@codecademy/variance'; import { fontWeightMediumTitle, + percipioElevation, percipioFontFamily, percipioPalette, } from '../variables'; @@ -62,20 +63,7 @@ export const percipioTheme = createTheme({ }, }, }) - .addScale('elevation', ({ colors }: { colors: Record }) => { - const shadowPrimary = colors['shadow-primary']; - const shadowSecondary = colors['shadow-secondary']; - const hover = `0 1px 4px 0 ${shadowPrimary}, 0 2px 11px 0 ${shadowSecondary}`; - - return { - rest: { - shadow: `0 1px 4px 0 ${shadowPrimary}, 0 2px 7px 0 ${shadowPrimary}`, - transform: 'none', - }, - hover: { shadow: hover, transform: 'none' }, - hoverMirrored: { shadow: hover, transform: 'none' }, - }; - }) + .addScale('elevation', percipioElevation) .addName('percipio') .build(); diff --git a/packages/gamut-styles/src/variables/elevation.ts b/packages/gamut-styles/src/variables/elevation.ts new file mode 100644 index 0000000000..a64fe98908 --- /dev/null +++ b/packages/gamut-styles/src/variables/elevation.ts @@ -0,0 +1,74 @@ +type ThemeColors = Record; + +/** + * The visual treatment of a surface at a single elevation state. + * These are type aliases rather than interfaces so they keep the implicit + * index signatures `addScale`'s constraint requires. + */ +export type ElevationStateStyles = { + shadow: string; + transform: string; +}; + +/** + * State keys are camelCase: variance's `LiteralPaths` splits token paths on + * `-`, so a hyphenated key like `hover-mirrored` would resolve to `never` and + * drop out of the theme type. `hoverMirrored` is for surfaces that cast their + * shadow to the opposite side (e.g. Card's `patternRight` shadow). + */ +export type ElevationScale = { + rest: ElevationStateStyles; + hover: ElevationStateStyles; + hoverMirrored: ElevationStateStyles; +}; + +export type ElevationState = keyof ElevationScale; + +/** + * Elevation scales describe how a surface renders shadow and lift at rest and + * on hover. Each theme provides its own scale via + * `.addScale('elevation', ...)`, which passes in that theme's `colors` — so + * `colors['shadow-primary']` resolves per theme and per color mode without + * this file ever importing a theme (which would be a circular dependency). + * + * The shared return type guarantees every theme emits the same elevation + * tokens, so lookups like `theme.elevation['hover-shadow']` are safe under + * any theme. + */ +export type ElevationScaleFactory = (theme: { + colors: ThemeColors; +}) => ElevationScale; + +export const coreElevation: ElevationScaleFactory = ({ colors }) => { + const shadowPrimary = colors['shadow-primary']; + const offset = 8; + const lift = 4; + const shadow = (x: number) => `${x}px ${offset}px 0 0 ${shadowPrimary}`; + + return { + rest: { shadow: `0 0 0 0 ${shadowPrimary}`, transform: 'none' }, + hover: { + shadow: shadow(-offset), + transform: `translate(${lift}px, -${lift}px)`, + }, + hoverMirrored: { + shadow: shadow(offset), + transform: `translate(-${lift}px, -${lift}px)`, + }, + }; +}; + +export const percipioElevation: ElevationScaleFactory = ({ colors }) => { + const shadowPrimary = colors['shadow-primary']; + const shadowSecondary = colors['shadow-secondary']; + const hover = `0 1px 4px 0 ${shadowPrimary}, 0 2px 11px 0 ${shadowSecondary}`; + + return { + rest: { + shadow: `0 1px 4px 0 ${shadowPrimary}, 0 2px 7px 0 ${shadowPrimary}`, + transform: 'none', + }, + hover: { shadow: hover, transform: 'none' }, + hoverMirrored: { shadow: hover, transform: 'none' }, + }; +}; diff --git a/packages/gamut-styles/src/variables/index.ts b/packages/gamut-styles/src/variables/index.ts index d4f074462e..33dc5a67c2 100644 --- a/packages/gamut-styles/src/variables/index.ts +++ b/packages/gamut-styles/src/variables/index.ts @@ -1,6 +1,7 @@ export * from './borderRadii'; export * from './colors'; export * from './elements'; +export * from './elevation'; export * from './responsive'; export * from './spacing'; export * from './timing'; From e0cd7c181448d4c3b3de60ad8cc0d94cc783efd3 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 12 Aug 2026 14:33:58 -0400 Subject: [PATCH 5/6] update Card styles to use elevation tokens --- packages/gamut/src/Card/styles.tsx | 43 +++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/gamut/src/Card/styles.tsx b/packages/gamut/src/Card/styles.tsx index 919aa858dd..5a43de4f40 100644 --- a/packages/gamut/src/Card/styles.tsx +++ b/packages/gamut/src/Card/styles.tsx @@ -1,6 +1,26 @@ -import { theme, timingValues, variant } from '@codecademy/gamut-styles'; +import { + ElevationState, + theme, + timingValues, + variant, +} from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; -import { useTheme } from '@emotion/react'; +import { Theme, useTheme } from '@emotion/react'; + +/** + * The theme's elevation scale is flattened to dashcase tokens (`rest-shadow`, + * `hoverMirrored-transform`, ...), so per-state groups don't exist on the + * theme at runtime. This regroups one state's tokens into a spreadable style + * object, mapping the `shadow` token onto the `boxShadow` property + * framer-motion animates. + */ +const getElevationStyles = ( + elevation: Theme['elevation'], + state: ElevationState +) => ({ + boxShadow: elevation[`${state}-shadow`], + transform: elevation[`${state}-transform`], +}); const SHADOW_OFFSET = 8; const SHADOW_OFFSET_INITIAL = 6; @@ -83,34 +103,31 @@ export const useCardElevation = ( borderRadius?: string ) => { const { elevation } = useTheme(); - const isMirrored = shadow === 'patternRight'; + const hoverState: ElevationState = + shadow === 'patternRight' ? 'hoverMirrored' : 'hover'; return { initial: { - boxShadow: elevation['rest-shadow'], - transform: elevation['rest-transform'], + ...getElevationStyles(elevation, 'rest'), borderRadius, transition: REST_TRANSITION, }, + // outline variants keep their bespoke two-layer shadow but share the + // elevation scale's transforms 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']}`, - transform: elevation['rest-transform'], borderRadius, transition: REST_TRANSITION, }, animate: { - boxShadow: isMirrored - ? elevation['hoverMirrored-shadow'] - : elevation['hover-shadow'], - transform: isMirrored - ? elevation['hoverMirrored-transform'] - : elevation['hover-transform'], + ...getElevationStyles(elevation, hoverState), borderRadius, transition: HOVER_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']}`, - transform: elevation['hover-transform'], borderRadius, transition: HOVER_TRANSITION, }, From 8878e2c5e16fb872834867054f17dc9e0891a7f3 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Thu, 13 Aug 2026 09:54:08 -0400 Subject: [PATCH 6/6] start on doc changes --- .../src/lib/Foundations/Theme/CoreTheme.mdx | 10 +++ .../lib/Foundations/Theme/PercipioTheme.mdx | 8 ++ .../src/lib/Foundations/shared/elements.tsx | 80 ++++++++++++++++++- .../Foundations/shared/percipioElements.tsx | 3 + 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/packages/styleguide/src/lib/Foundations/Theme/CoreTheme.mdx b/packages/styleguide/src/lib/Foundations/Theme/CoreTheme.mdx index b52036e132..d9c951ed44 100644 --- a/packages/styleguide/src/lib/Foundations/Theme/CoreTheme.mdx +++ b/packages/styleguide/src/lib/Foundations/Theme/CoreTheme.mdx @@ -111,3 +111,13 @@ export const lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, s **Key**: `borderRadii` + +## Elevation + +**Key**: `elevation` + +Elevation describes how a surface (such as `Card`) renders shadow and lift. The scale has three states — `rest`, `hover`, and `hoverMirrored` — each providing a `shadow` and a `transform` token, accessible with a dashcase key `elevation['${state}-${property}']`. The `hoverMirrored` state is used when a surface casts its shadow to the right instead of the left (e.g. `Card`'s `patternRight` shadow). + +Shadow colors reference the `shadow-primary` color mode alias, so they respond to light and dark mode automatically, and themes can provide their own scale via `addScale('elevation', ...)` — Percipio, for example, replaces the hard offset shadow with a soft blurred one and no lift. + + diff --git a/packages/styleguide/src/lib/Foundations/Theme/PercipioTheme.mdx b/packages/styleguide/src/lib/Foundations/Theme/PercipioTheme.mdx index ccccfd3e97..10f19928db 100644 --- a/packages/styleguide/src/lib/Foundations/Theme/PercipioTheme.mdx +++ b/packages/styleguide/src/lib/Foundations/Theme/PercipioTheme.mdx @@ -96,3 +96,11 @@ Percipio currently only supports `light` mode. The Percipio theme uses Skillsoft Sans for accent text and Skillsoft Text for body text. Roboto Mono is used for monospace, and Roboto sans-serif for the `system` font family slot. + +## Elevation + +**Key**: `elevation` + +The Percipio theme provides its own elevation scale: surfaces cast a soft, blurred shadow rather than the Core theme's hard offset shadow, and they do not lift on hover — every `transform` token is `none`. The scale has the same three states as the Core theme — `rest`, `hover`, and `hoverMirrored` — each providing a `shadow` and a `transform` token accessible with a dashcase key `elevation['${state}-${property}']`, so components like `Card` work across themes without changes. + + diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index 1ec160156a..4558c54527 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -3,13 +3,14 @@ import { Background, coreSwatches, css, + ElevationState, lxStudioColors, theme, trueColors, } from '@codecademy/gamut-styles'; // eslint-disable-next-line gamut/import-paths import * as ALL_PROPS from '@codecademy/gamut-styles/src/variance/config'; -import { useTheme } from '@emotion/react'; +import { Theme, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import kebabCase from 'lodash/kebabCase'; import { useMemo } from 'react'; @@ -541,3 +542,80 @@ export const getPropRows = (key: keyof typeof ALL_PROPS) => id: prop, ...config, })); + +const ElevationExample = styled(Box)<{ + exampleShadow?: string; + exampleTransform?: string; +}>( + css({ + bg: 'background-current', + border: 1, + display: 'inline-block', + height: '3rem', + width: '5rem', + }), + ({ exampleShadow, exampleTransform }) => ({ + boxShadow: exampleShadow, + transform: exampleTransform, + }) +); + +const elevationStates: ElevationState[] = ['rest', 'hover', 'hoverMirrored']; + +/** + * Every theme emits the same elevation tokens (see `ElevationScale` in + * gamut-styles), so each theme's docs page can build its table from its own + * scale — see `percipioElements` for the Percipio table. + */ +export const createElevationTable = (elevationScale: Theme['elevation']) => ({ + rows: elevationStates.map((id) => ({ + id, + shadow: elevationScale[`${id}-shadow`], + transform: elevationScale[`${id}-transform`], + })), + columns: [ + PROP_COLUMN, + { + ...PATH_COLUMN, + render: ({ id }: any) => ( + + {`theme.elevation['${id}-shadow']`} + {`theme.elevation['${id}-transform']`} + + ), + }, + { + key: 'value', + name: 'Value', + size: 'lg', + render: ({ shadow, transform }: any) => ( + + {shadow} + {transform} + + ), + }, + { + key: 'example', + name: 'Example', + size: 'fill', + // dashed outline marks the resting position the transform lifts from + render: ({ shadow, transform }: any) => ( + + + + ), + }, + ], +}); + +export const elevation = createElevationTable(theme.elevation); diff --git a/packages/styleguide/src/lib/Foundations/shared/percipioElements.tsx b/packages/styleguide/src/lib/Foundations/shared/percipioElements.tsx index d33c0c7bd3..570948edb0 100644 --- a/packages/styleguide/src/lib/Foundations/shared/percipioElements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/percipioElements.tsx @@ -9,12 +9,15 @@ import { Code, ColorScale, TokenTable } from '~styleguide/blocks'; import { applyCorrectNotation } from './applyCorrectNotation'; import { + createElevationTable, createExampleColumn, PATH_COLUMN, PROP_COLUMN, VALUE_COLUMN, } from './elements'; +export const percipioElevation = createElevationTable(percipioTheme.elevation); + export const percipioColor = { rows: Object.entries(percipioColors).map(([id, value]) => ({ id,