diff --git a/ui-parts/theme/stories/constants.ts b/ui-parts/theme/stories/constants.ts new file mode 100644 index 000000000..f0bb07c69 --- /dev/null +++ b/ui-parts/theme/stories/constants.ts @@ -0,0 +1,5 @@ +import type { ElevationRole } from './interfaces.js' +import type { ElevationState } from './interfaces.js' + +export const roles: Array = ['xs', 'sm', 'md', 'lg'] +export const states: Array = ['default', 'hover', 'pressed', 'disabled'] diff --git a/ui-parts/theme/stories/elevation.stories.tsx b/ui-parts/theme/stories/elevation.stories.tsx new file mode 100644 index 000000000..08562b5aa --- /dev/null +++ b/ui-parts/theme/stories/elevation.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta } from '@storybook/react' +import type { StoryObj } from '@storybook/react' + +import type { ElevationStoryProps } from './interfaces.js' + +import { darkTheme } from '@atls-ui/theme' +import { lightTheme } from '@atls-ui/theme' + +import { roles } from './constants.js' +import { states } from './constants.js' + +const meta: Meta = { + title: 'Theme/Elevation', + args: { + theme: 'light', + }, + argTypes: { + theme: { + control: 'inline-radio', + options: ['light', 'dark'], + }, + }, + render: ({ theme: themeName }) => { + const theme = themeName === 'light' ? lightTheme : darkTheme + const surface = themeName === 'light' ? theme.colors.surface.soft : theme.colors.surface.muted + + return ( +
+
+
+ {states.map((state) => ( + {state} + ))} + + {roles.map((role) => ( +
+ {role} + {states.map((state) => ( +
+ Surface +
+ ))} +
+ ))} +
+ +
+ modal +

Overlay surface

+
+
+ ) + }, +} + +export default meta + +export const Elevation: StoryObj = {} diff --git a/ui-parts/theme/stories/interfaces.ts b/ui-parts/theme/stories/interfaces.ts new file mode 100644 index 000000000..7336e4ad8 --- /dev/null +++ b/ui-parts/theme/stories/interfaces.ts @@ -0,0 +1,7 @@ +export type ThemeName = 'dark' | 'light' +export type ElevationRole = 'lg' | 'md' | 'sm' | 'xs' +export type ElevationState = 'default' | 'disabled' | 'hover' | 'pressed' + +export interface ElevationStoryProps { + theme: ThemeName +} diff --git a/ui/theme/src/theme.ts b/ui/theme/src/theme.ts index 527a1b2bd..12b7d6f04 100644 --- a/ui/theme/src/theme.ts +++ b/ui/theme/src/theme.ts @@ -1,26 +1,31 @@ import type { BordersTokens } from './tokens/index.js' import type { Colors } from './tokens/index.js' +import type { ElevationsTokens } from './tokens/index.js' import type { TypographyTokens } from './tokens/index.js' import { borders } from './tokens/index.js' import { darkColors } from './tokens/index.js' +import { elevations } from './tokens/index.js' import { lightColors } from './tokens/index.js' import { typography } from './tokens/index.js' export interface Theme { borders: BordersTokens colors: Colors + elevations: ElevationsTokens typography: TypographyTokens } export const lightTheme: Theme = { borders: borders(lightColors), colors: lightColors, + elevations: elevations(lightColors), typography, } export const darkTheme: Theme = { borders: borders(darkColors), colors: darkColors, + elevations: elevations(darkColors), typography, } diff --git a/ui/theme/src/tokens/colors/dark.ts b/ui/theme/src/tokens/colors/dark.ts index bc2f8469b..48d06b62b 100644 --- a/ui/theme/src/tokens/colors/dark.ts +++ b/ui/theme/src/tokens/colors/dark.ts @@ -6,7 +6,7 @@ export const darkColors: Colors = { hover: '#7AA7FF', pressed: '#3F74FF', disabled: '#5F8FFF', - subtle: '#394A6B', + subtle: '#5F8FFF4D', }, surface: { base: '#111111', diff --git a/ui/theme/src/tokens/colors/light.ts b/ui/theme/src/tokens/colors/light.ts index 68165a067..1aed12d1c 100644 --- a/ui/theme/src/tokens/colors/light.ts +++ b/ui/theme/src/tokens/colors/light.ts @@ -6,7 +6,7 @@ export const lightColors: Colors = { hover: '#1643A3', pressed: '#12357F', disabled: '#1E56C740', - subtle: '#6B86C8', + subtle: '#1E56C740', }, surface: { base: '#F8F9FF', diff --git a/ui/theme/src/tokens/elevation/blurs.ts b/ui/theme/src/tokens/elevation/blurs.ts new file mode 100644 index 000000000..c7f5af2d7 --- /dev/null +++ b/ui/theme/src/tokens/elevation/blurs.ts @@ -0,0 +1,14 @@ +export const shadowBlurs = { + xs5: '1px', + xs4: '2px', + xs3: '3px', + xs2: '4px', + xs: '5px', + sm: '6px', + md: '8px', + lg: '12px', + xl: '14px', + xl2: '16px', + xl3: '20px', +} as const + diff --git a/ui/theme/src/tokens/elevation/elevations.ts b/ui/theme/src/tokens/elevation/elevations.ts new file mode 100644 index 000000000..1f23f3066 --- /dev/null +++ b/ui/theme/src/tokens/elevation/elevations.ts @@ -0,0 +1,184 @@ +import type { Colors } from '../colors/index.js' +import type { ElevationsTokens } from './interfaces.js' + +import { shadowBlurs } from './blurs.js' +import { shadowOffsets } from './offsets.js' +import { shadowSpreads } from './spreads.js' +import { createElevation } from './utils/index.js' + +export const elevations = (colors: Colors): ElevationsTokens => ({ + xs: { + default: createElevation({ + color: colors.elevation.xs, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs4, + }), + hover: createElevation( + { + color: colors.elevation.xs, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs3, + spread: shadowSpreads.xs, + }, + { + color: colors.action.subtle, + offsetX: shadowOffsets.xs, + offsetY: shadowOffsets.md, + blur: shadowBlurs.sm, + spread: shadowSpreads.negativeSm, + } + ), + pressed: createElevation( + { + color: colors.elevation.xs, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs5, + inset: true, + }, + { + color: colors.elevation.xs, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs5, + spread: shadowSpreads.negativeXs, + } + ), + disabled: createElevation({ + color: colors.elevation.xs, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs5, + }), + }, + sm: { + default: createElevation({ + color: colors.elevation.sm, + offsetY: shadowOffsets.sm, + blur: shadowBlurs.sm, + }), + hover: createElevation( + { + color: colors.elevation.sm, + offsetY: shadowOffsets.sm, + blur: shadowBlurs.md, + spread: shadowSpreads.xs, + }, + { + color: colors.action.subtle, + offsetX: shadowOffsets.xs, + offsetY: shadowOffsets.lg, + blur: shadowBlurs.md, + spread: shadowSpreads.negativeSm, + } + ), + pressed: createElevation( + { + color: colors.elevation.sm, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs4, + inset: true, + }, + { + color: colors.elevation.sm, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs3, + spread: shadowSpreads.negativeSm, + } + ), + disabled: createElevation({ + color: colors.elevation.sm, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs4, + }), + }, + md: { + default: createElevation({ + color: colors.elevation.md, + offsetY: shadowOffsets.lg, + blur: shadowBlurs.lg, + }), + hover: createElevation( + { + color: colors.elevation.md, + offsetY: shadowOffsets.lg, + blur: shadowBlurs.xl, + }, + { + color: colors.action.subtle, + offsetX: shadowOffsets.xs, + offsetY: shadowOffsets.xl, + blur: shadowBlurs.xl, + spread: shadowSpreads.negativeLg, + } + ), + pressed: createElevation( + { + color: colors.elevation.md, + offsetY: shadowOffsets.xs, + blur: shadowBlurs.xs3, + inset: true, + }, + { + color: colors.elevation.md, + offsetY: shadowOffsets.sm, + blur: shadowBlurs.sm, + spread: shadowSpreads.negativeXs, + } + ), + disabled: createElevation({ + color: colors.elevation.md, + offsetY: shadowOffsets.sm, + blur: shadowBlurs.xs, + }), + }, + lg: { + default: createElevation({ + color: colors.elevation.lg, + offsetY: shadowOffsets.xl, + blur: shadowBlurs.lg, + spread: shadowSpreads.negativeLg, + }), + hover: createElevation( + { + color: colors.elevation.lg, + offsetY: shadowOffsets.xl2, + blur: shadowBlurs.xl2, + spread: shadowSpreads.negativeLg, + }, + { + color: colors.action.subtle, + offsetX: shadowOffsets.xs, + offsetY: shadowOffsets.xl, + blur: shadowBlurs.md, + spread: shadowSpreads.negativeXs, + } + ), + pressed: createElevation( + { + color: colors.elevation.lg, + offsetY: shadowOffsets.sm, + blur: shadowBlurs.xs2, + inset: true, + }, + { + color: colors.elevation.lg, + offsetY: shadowOffsets.md, + blur: shadowBlurs.md, + spread: shadowSpreads.negativeLg, + } + ), + disabled: createElevation({ + color: colors.elevation.lg, + offsetY: shadowOffsets.md, + blur: shadowBlurs.sm, + spread: shadowSpreads.negativeMd, + }), + }, + modal: { + default: createElevation({ + color: colors.elevation.md, + offsetY: shadowOffsets.xl3, + blur: shadowBlurs.xl3, + spread: shadowSpreads.negativeSm, + }), + }, +}) + diff --git a/ui/theme/src/tokens/elevation/index.ts b/ui/theme/src/tokens/elevation/index.ts new file mode 100644 index 000000000..39160c868 --- /dev/null +++ b/ui/theme/src/tokens/elevation/index.ts @@ -0,0 +1,7 @@ +export * from './blurs.js' +export * from './elevations.js' +export type * from './interfaces.js' +export * from './offsets.js' +export * from './spreads.js' +export * from './utils/index.js' + diff --git a/ui/theme/src/tokens/elevation/interfaces.ts b/ui/theme/src/tokens/elevation/interfaces.ts new file mode 100644 index 000000000..e5f5dcbd8 --- /dev/null +++ b/ui/theme/src/tokens/elevation/interfaces.ts @@ -0,0 +1,38 @@ +import type { Properties } from 'csstype' + +import type { shadowBlurs } from './blurs.js' +import type { shadowOffsets } from './offsets.js' +import type { shadowSpreads } from './spreads.js' + +export type Elevation = NonNullable +export type ShadowBlur = (typeof shadowBlurs)[keyof typeof shadowBlurs] +export type ShadowOffset = (typeof shadowOffsets)[keyof typeof shadowOffsets] +export type ShadowSpread = (typeof shadowSpreads)[keyof typeof shadowSpreads] + +export interface ShadowOptions { + color: string + offsetX?: ShadowOffset + offsetY: ShadowOffset + blur: ShadowBlur + spread?: ShadowSpread + inset?: boolean +} + +export interface ElevationStates { + default: Elevation +} + +export interface PressableElevationStates extends ElevationStates { + hover: Elevation + pressed: Elevation + disabled: Elevation +} + +export interface ElevationsTokens { + xs: PressableElevationStates + sm: PressableElevationStates + md: PressableElevationStates + lg: PressableElevationStates + modal: ElevationStates +} + diff --git a/ui/theme/src/tokens/elevation/offsets.ts b/ui/theme/src/tokens/elevation/offsets.ts new file mode 100644 index 000000000..5318ab066 --- /dev/null +++ b/ui/theme/src/tokens/elevation/offsets.ts @@ -0,0 +1,11 @@ +export const shadowOffsets = { + none: '0', + xs: '1px', + sm: '2px', + md: '3px', + lg: '4px', + xl: '6px', + xl2: '7px', + xl3: '12px', +} as const + diff --git a/ui/theme/src/tokens/elevation/spreads.ts b/ui/theme/src/tokens/elevation/spreads.ts new file mode 100644 index 000000000..9923fc7d3 --- /dev/null +++ b/ui/theme/src/tokens/elevation/spreads.ts @@ -0,0 +1,9 @@ +export const shadowSpreads = { + negativeLg: '-4px', + negativeMd: '-3px', + negativeSm: '-2px', + negativeXs: '-1px', + none: '0', + xs: '1px', +} as const + diff --git a/ui/theme/src/tokens/elevation/utils/createElevation.ts b/ui/theme/src/tokens/elevation/utils/createElevation.ts new file mode 100644 index 000000000..1fc7523d3 --- /dev/null +++ b/ui/theme/src/tokens/elevation/utils/createElevation.ts @@ -0,0 +1,20 @@ +import type { Elevation } from '../interfaces.js' +import type { ShadowOptions } from '../interfaces.js' + +import { shadowOffsets } from '../offsets.js' +import { shadowSpreads } from '../spreads.js' + +export const createElevation = (...shadows: Array): Elevation => + shadows + .map( + ({ + color, + offsetX = shadowOffsets.none, + offsetY, + blur, + spread = shadowSpreads.none, + inset = false, + }) => `${inset ? 'inset ' : ''}${offsetX} ${offsetY} ${blur} ${spread} ${color}` + ) + .join(', ') + diff --git a/ui/theme/src/tokens/elevation/utils/index.ts b/ui/theme/src/tokens/elevation/utils/index.ts new file mode 100644 index 000000000..025294029 --- /dev/null +++ b/ui/theme/src/tokens/elevation/utils/index.ts @@ -0,0 +1,2 @@ +export * from './createElevation.js' + diff --git a/ui/theme/src/tokens/index.ts b/ui/theme/src/tokens/index.ts index 41f7e7d43..59d742faf 100644 --- a/ui/theme/src/tokens/index.ts +++ b/ui/theme/src/tokens/index.ts @@ -1,6 +1,7 @@ export * from './animations.js' export * from './borders/index.js' export * from './colors/index.js' +export * from './elevation/index.js' export * from './layout/spaces.js' export * from './opacities.js' export * from './layout/radii.js'