@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
576 lines (540 loc) • 20.3 kB
text/typescript
import {BreakpointFnParam, CanvasBreakpoints} from './breakpoints';
/**
* A single palette within a Canvas theme
*/
export type CanvasThemePalette = {
lightest: string;
lighter: string;
light: string;
main: string;
dark: string;
darkest: string;
contrast: string;
};
type CanvasThemeCommonPalette = {
focusOutline: string;
alertInner: string;
alertOuter: string;
errorInner: string;
};
/**
* Direction of page content for internationalization
* @deprecated ⚠️ `ContentDirection` is deprecated. Use the `:dir()` CSS pseudo-class selector and CSS logical properties instead. For more information, see [MDN :dir()](https://developer.mozilla.org/en-US/docs/Web/CSS/:dir) and [CSS Logical Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values).
*/
export enum ContentDirection {
LTR = 'ltr',
RTL = 'rtl',
}
/**
* The format of a Canvas theme for components that support it.
* @deprecated ⚠️ `CanvasTheme` is deprecated. In previous versions of Canvas Kit, we allowed teams to pass a theme object, this supported [Emotion's theming](https://emotion.sh/docs/theming). Now that we're shifting to a global theming approach based on CSS variables, we advise to no longer using the theme prop. For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
*/
export interface CanvasTheme {
palette: {
common: CanvasThemeCommonPalette;
primary: CanvasThemePalette;
error: CanvasThemePalette;
alert: CanvasThemePalette;
success: CanvasThemePalette;
neutral: CanvasThemePalette;
};
/**
* ### Theme Breakpoints
*
* Breakpoints are used by media queries to conditionally apply or modify styles based on the viewport width.
* This allows the UI to be responsive to various screen sizes.
*
* This breakpoints object contains `values` and several helper functions: `up`, `down`, `between`, and `only`.
* You can find more detailed information by inspecting individual values and functions.
*/
breakpoints: {
/**
* ### Breakpoint Values
*
* This object provides five breakpoint values
* that correspond to the min-widths of our standard screen sizes.
*
* - `zero`: 0
* - `s`: 320
* - `m`: 768
* - `l`: 1024
* - `xl`: 1440
*
* And these are our standard screen size ranges:
*
* - `small` (320px - 767px) Used for mobile-sized screens
* - `medium` (768px - 1023px) Used for tablet-sized screens
* - `large` - (1024px - 1439px) Used for laptop and small desktop screens
* - `extra-large` (≥1440px) Used for very large screens
*
* Note: Some applications may only require a subset of screen sizes and not use all breakpoints.
*
*/
values: CanvasBreakpoints;
/**
* ### Up
*
* _Returns a media query above the `min-width` for the range of a given breakpoint_
*
* Given a `start` breakpoint key ("zero", "s", "m", "l", "xl"),
* this function returns a media query (string) using a `min-width`.
*
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { up } = theme.canvas.breakpoints;
* const mediaQuery = up('l'); // Returns '@media (min-width: 1024px)'
* const styles = {
* [mediaQuery]: {
* padding: space.m,
* }
* };
* ```
*/
up: (key: BreakpointFnParam) => string;
/**
* ### Down
*
* _Returns a media query below the `max-width` for the range of a given breakpoint_
*
* Given an `end` breakpoint key ("zero", "s", "m", "l", "xl"),
* this function returns a media query (string) using a `max-width`.
*
* Note: This function subtracts `0.5px` from the next breakpoint value to prevent collisions.
* For example, `breakpoints.values.s`, has a `min-width` of `320px`, and the `max-width` is `767.5px`).
*
* If the `xl` breakpoint is provided, this function returns a media query with only a `min-width` of `0`,
* as seen in the second example below.
*
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { down } = theme.canvas.breakpoints;
* const mediaQuery = down('m'); // Returns '@media (max-width: 1023.5px)'
* const styles = {
* [mediaQuery]: {
* padding: space.m,
* }
* };
* ```
*
* This example uses the `xl` breakpoint and only adds a `min-width` of `0` to the media query.
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { down } = theme.canvas.breakpoints;
* const mediaQuery = down('xl'); // Returns '@media (min-width: 0)'
* const styles = {
* [mediaQuery]: {
* padding: space.m,
* }
* };
* ```
*/
down: (key: BreakpointFnParam) => string;
/**
* ### Between
*
* _Returns a media query between two given breakpoints_
*
* Given `start` and `end` breakpoint keys ("zero", "s", "m", "l", "xl"),
* this function returns a media query (string) using a min-width and max-width.
*
* Note: This function subtracts `0.5px` from the next breakpoint value to prevent collisions.
* For example, `breakpoints.values.s`, has a `min-width` of `320px`, and the `max-width` is `767.5px`).
*
* If the `xl` breakpoint is provided, this function returns a media query with only a `min-width`,
* as seen in the second example below.
*
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { between } = theme.canvas.breakpoints;
* // Returns '@media (min-width: 320px) and (max-width: 1023.5px)'
* const mediaQuery = between('s', 'm');
* const styles = {
* [mediaQuery]: {
* padding: space.s,
* }
* };
* ```
*
* This example uses `xl` as the `end` breakpoint and only adds a min-width to the media query.
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { between } = theme.canvas.breakpoints;
* const mediaQuery = between('m', 'xl'); // Returns '@media (min-width: 768px)'
* const styles = {
* [mediaQuery]: {
* padding: space.s,
* }
* };
* ```
*/
between: (start: BreakpointFnParam, end: BreakpointFnParam) => string;
/**
* ### Only
*
* _Returns a media query with a `min-width` and `max-width` for a given breakpoint_
*
* Given a breakpoint key ("zero", "s", "m", "l", "xl"),
* this function returns a media query (string) using a `min-width` and `max-width`.
*
* Note: This function subtracts `0.5px` from the next breakpoint value to prevent collisions.
* For example, `breakpoints.values.s`, has a `min-width` of `320px`, and the `max-width` is `767.5px`).
*
* If the `xl` breakpoint is provided, this function returns a media query with only a `min-width` of `1440px`,
* as seen in the second example below.
*
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { only } = theme.canvas.breakpoints;
* const mediaQuery = only('s'); // Returns '@media (min-width: 320px) and (max-width: 767.5px)'
* const styles = {
* [mediaQuery]: {
* padding: space.s,
* }
* };
* ```
*
* This example uses the `xl` breakpoint and only adds a `min-width` of `1440px` to the media query.
* @example
* ```ts
* import { useTheme } from '@workday/canvas-kit-react/common';
* import { space } from '@workday/canvas-kit-react/tokens';
*
* const theme = useTheme();
* const { only } = theme.canvas.breakpoints;
* const mediaQuery = only('xl'); // Returns '@media (min-width: 1440px)'
* const styles = {
* [mediaQuery]: {
* padding: space.s,
* }
* };
* ```
*/
only: (key: BreakpointFnParam) => string;
};
direction: ContentDirection;
}
/**
* Indicates a component is themeable with a CanvasTheme
* @deprecated `Themeable` is deprecated. If you want to theme your application, please use `<CanvasProvider theme={{canvas: {palette: {primary: {main: 'orange'}}}}} />` at the root of your application or use our CSS tokens to change individual component styles as seen in our [Button docs](https://workday.github.io/canvas-kit/?path=/docs/components-buttons--docs#custom-styles).
*/
export interface Themeable {
theme?: EmotionCanvasTheme;
}
/**
* For custom themes that do not overwrite every default.
* @deprecated ⚠️ `RecursivePartial` is deprecated. In previous versions of Canvas Kit, we allowed teams to pass a theme object, this supported [Emotion's theming](https://emotion.sh/docs/theming). Now that we're shifting to a global theming approach based on CSS variables, we advise to no longer using the theme prop. For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
*/
type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
/**
* @deprecated ⚠️ `PartialCanvasTheme` is deprecated. In previous versions of Canvas Kit, we allowed teams to pass a theme object, this supported [Emotion's theming](https://emotion.sh/docs/theming). Now that we're shifting to a global theming approach based on CSS variables, we advise to no longer using the theme prop. For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
*/
export type PartialCanvasTheme = RecursivePartial<CanvasTheme>;
/**
* @deprecated ⚠️ `PartialCanvasThemePalette` is deprecated. In previous versions of Canvas Kit, we allowed teams to pass a theme object, this supported [Emotion's theming](https://emotion.sh/docs/theming). Now that we're shifting to a global theming approach based on CSS variables, we advise to no longer using the theme prop. For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
*/
export type PartialCanvasThemePalette = RecursivePartial<CanvasThemePalette>;
export type PartialEmotionCanvasTheme = {canvas?: PartialCanvasTheme};
declare module '@emotion/react' {
export interface Theme {
canvas: CanvasTheme;
}
}
/**
* @deprecated ⚠️ `EmotionCanvasTheme` is deprecated. In previous versions of Canvas Kit, we allowed teams to pass a theme object, this supported [Emotion's theming](https://emotion.sh/docs/theming). Now that we're shifting to a global theming approach based on CSS variables, we advise to no longer using the theme prop. For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
*/
export type EmotionCanvasTheme = {canvas: CanvasTheme};
/**
* Numerical brand ramp keys. Each key maps 1:1 to a `--cnvs-brand-{family}-{key}` CSS variable.
*
* Common keys:
* - `'600'` — main accent / button fill / default brand fg
* - `'700'` — strong fg / selected text (when not using `selected.fg`)
* - `'500'` — focus rings and border primary (independent of `'600'`)
* - `'A50'` — selected surface tint (when not using `selected.surface`)
* - `'25'` / `'A25'` — subtle brand surfaces
*
* Neutral-only Sana steps (`'150'`, `'850'`, `'A150'`) live on {@link CanvasNeutralBrandRamp}.
*/
export type CanvasBrandRamp = Partial<
Record<
| '25'
| '50'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '950'
| '975'
| 'A25'
| 'A50'
| 'A100'
| 'A200',
string
>
>;
/**
* Neutral brand ramp — includes Sana-only steps (`150` / `850` / `A150`) that are not
* exported for primary/critical/caution/positive families.
*/
export type CanvasNeutralBrandRamp = CanvasBrandRamp &
Partial<Record<'150' | '850' | 'A150', string>>;
/** Semantic keys for `brand.action.*` CSS variables (PrimaryButton, etc.). */
export type CanvasActionBrandRamp = Partial<
Record<
'base' | 'lightest' | 'lighter' | 'light' | 'dark' | 'darkest' | 'darker' | 'accent',
string
>
>;
/**
* Controls how partial theme input is expanded.
*
* - `'brand'` (default): predictable, design-aligned behavior. Setting only
* `brand.primary['600']` themes PrimaryButton and selected list/menu
* states. Other keys write only their CSS variable — no auto-generated ramps.
* - `'full'`: legacy behavior for the deprecated `canvas.palette` shape — auto-fills
* lightest→darkest via `shiftColor` and forwards to many `system.color.brand.*`
* tokens. On the numerical `brand` shape, `'full'` disables the primary shortcut
* and writes each ramp key literally.
*/
export type CanvasThemingScope = 'brand' | 'full';
/**
* Preferred theme input for `CanvasProvider`. Each value maps directly to brand CSS
* variables unless noted as a shortcut below.
*
* @example Minimal — brand buttons + selected states only
* ```tsx
* <CanvasProvider theme={{brand: {primary: {'600': base.magenta600}}}} />
* ```
*
* @example Explicit — focus independent of primary
* ```tsx
* <CanvasProvider theme={{
* brand: {primary: {'600': base.red600, '500': base.blue500}},
* selected: {fg: base.red700, surface: base.redA50},
* }} />
* ```
*
* @see sanaCanvasProviderTheme when `<html>` is unavailable and popups need Sana brand forwarding
*/
export interface CanvasNumericalBrandTheme {
brand?: {
/**
* Primary brand ramp (`--cnvs-brand-primary-*`).
*
* **Shortcut (brand scope only):** when `'600'` is the only key under `primary`,
* also sets:
* - `PrimaryButton` — `brand.action.base`, `accent.primary`, `accent.action`
* - Selected `Menu.Item` — `system.color.brand.fg.selected`, `surface.selected`
*
* Does **not** set focus rings — use `'500'` or `canvas.palette.common.focusOutline`.
*
* | Key | CSS variable | Typical consumers |
* |-----|--------------|-------------------|
* | `'600'` | `--cnvs-brand-primary-600` | PrimaryButton, brand links, accent.primary |
* | `'700'` | `--cnvs-brand-primary-700` | Strong primary fg, selected text |
* | `'500'` | `--cnvs-brand-primary-500` | Focus rings, border primary |
* | `'A50'` | `--cnvs-brand-primary-A50` | Selected/hover surfaces |
*/
primary?: CanvasBrandRamp;
/**
* Button-specific ramp (`--cnvs-brand-action-*`). PrimaryButton reads
* these **before** `brand.primary`.
*
* | Key | Typical consumers |
* |-----|-------------------|
* | `base` | PrimaryButton background |
* | `dark` / `darkest` | PrimaryButton hover / pressed |
* | `accent` | PrimaryButton label color |
*/
action?: CanvasActionBrandRamp;
/**
* Critical/error ramp (`--cnvs-brand-critical-*`).
*
* | Key | Typical consumers |
* |-----|-------------------|
* | `'600'` | TextInput error, DeleteButton, critical fg |
* | `'500'` | Critical focus ring, error border |
* | `'A25'` / `'A50'` | Error surface tints |
*/
critical?: CanvasBrandRamp;
/**
* Caution/warning ramp (`--cnvs-brand-caution-*`).
*
* | Key | Typical consumers |
* |-----|-------------------|
* | `'400'` | Caution accent, TextInput caution |
* | `'500'` | Caution focus outer, caution border |
* | `'A25'` / `'A50'` | Caution surface tints |
*/
caution?: CanvasBrandRamp;
/**
* Positive/success ramp (`--cnvs-brand-positive-*`).
*
* | Key | Typical consumers |
* |-----|-------------------|
* | `'600'` | Checkbox/Radio checked, success fg |
* | `'A25'` / `'A50'` | Success surface tints |
*/
positive?: CanvasBrandRamp;
/**
* Neutral brand ramp (`--cnvs-brand-neutral-*`). Sana Canvas uses `base.neutral*`
* instead of legacy `base.slate*` — see {@link sanaCanvasNumericalTheme}.
*
* Affects brand-neutral text, borders, and surfaces where components reference
* `brand.neutral.*` or `system.color.brand` tokens tied to neutral.
*/
neutral?: CanvasNeutralBrandRamp;
};
/**
* Selected-state shortcuts. Prefer these over indirect `brand.primary['700']` /
* `brand.primary.A50` when customizing list/menu selection.
*
* | Key | CSS variable | Typical consumers |
* |-----|--------------|-------------------|
* | `fg` | `--cnvs-sys-color-brand-fg-selected` | Menu.Item, SegmentedControl selected text |
* | `surface` | `--cnvs-sys-color-brand-surface-selected` | Menu.Item, list selected background |
*/
selected?: {
/** Selected text/icon color */
fg?: string;
/** Selected row/chip background */
surface?: string;
};
/**
* Escape hatch for `system.color.brand.*` tokens not covered by `brand` ramps.
* Keys mirror the token path, e.g. `{color: {brand: {focus: {primary: '#00f'}}}}`.
*/
system?: {
color?: {
brand?: Record<string, unknown>;
};
};
/** Text direction for the provider subtree. */
direction?: ContentDirection;
/**
* @default 'brand'
* @see CanvasThemingScope
*/
themeScope?: CanvasThemingScope;
}
/**
* Theme input accepted by {@link CanvasProvider}.
* - Numerical `brand` shape (preferred)
* - Deprecated `canvas.palette` shape (legacy)
*/
export type CanvasProviderTheme =
| (PartialEmotionCanvasTheme & {themeScope?: CanvasThemingScope})
| CanvasNumericalBrandTheme;
export function isNumericalTheme(
theme: CanvasProviderTheme | undefined
): theme is CanvasNumericalBrandTheme {
if (!theme) {
return false;
}
if ('canvas' in theme) {
return false;
}
// `direction` / `themeScope` alone are valid numerical themes (e.g. RTL-only).
return (
'brand' in theme ||
'system' in theme ||
'selected' in theme ||
'direction' in theme ||
'themeScope' in theme
);
}
const EXTENDED_RAMP_KEYS = new Set([
'25',
'50',
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
'950',
'975',
'A25',
'A50',
'A100',
'A200',
'lightest',
'lighter',
'light',
'main',
'dark',
'darkest',
'contrast',
]);
export function resolveThemingScope(theme: CanvasProviderTheme | undefined): CanvasThemingScope {
if (!theme) {
return 'brand';
}
if (theme.themeScope) {
return theme.themeScope;
}
if (isNumericalTheme(theme)) {
return theme.themeScope ?? 'brand';
}
const palette = theme.canvas?.palette;
if (!palette) {
return 'brand';
}
if (palette.common && Object.keys(palette.common).length > 0) {
const commonOnlyFocus =
Object.keys(palette.common).length === 1 && palette.common.focusOutline != null;
if (!commonOnlyFocus) {
return 'full';
}
}
for (const color of ['primary', 'error', 'alert', 'success', 'neutral'] as const) {
const colorPalette = palette[color];
if (!colorPalette) {
continue;
}
for (const key of Object.keys(colorPalette)) {
// `main` alone uses brand-scope bundles for every semantic palette color.
if (key === 'main') {
continue;
}
if (EXTENDED_RAMP_KEYS.has(key)) {
return 'full';
}
}
}
return 'brand';
}