@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
493 lines (492 loc) • 23 kB
JavaScript
import { colorSpace, maybeWrapCSSVariables } from '@workday/canvas-kit-styling';
import { brand, system } from '@workday/canvas-tokens-web';
import { defaultCanvasTheme } from './theme';
import { isNumericalTheme, } from './types';
/** True when a semantic theme intentionally passes a non-empty `canvas.palette`. */
export function hasExplicitSemanticPalette(theme) {
var _a;
return (!isNumericalTheme(theme) &&
!!((_a = theme.canvas) === null || _a === void 0 ? void 0 : _a.palette) &&
Object.keys(theme.canvas.palette).length > 0);
}
/** Tokens that change when a consumer sets only their primary brand color. */
export const BRAND_SCOPE_PRIMARY_BUNDLE = {
action: ['base', 'dark', 'darkest', 'accent'],
accent: ['primary', 'action'],
selected: {
fg: system.color.brand.fg.selected,
surface: system.color.brand.surface.selected,
},
};
/**
* Independently brandable tokens — writable when explicitly provided,
* but NEVER derived from primary.main / brand.primary.600 / brand.primary.500.
*/
export const BRAND_SCOPE_INDEPENDENT = {
focus: {
primary: system.color.brand.focus.primary,
semanticKey: 'focusOutline',
},
border: {
primary: system.color.brand.border.primary,
semanticKey: 'focusOutline',
},
};
const commonTokenMapping = {
focusOutline: brand.common.focus,
alertInner: brand.common.caution.inner,
alertOuter: brand.common.caution.outer,
errorInner: brand.common.critical,
};
const brandColorMapping = {
primary: 'primary',
error: 'critical',
success: 'positive',
alert: 'caution',
neutral: 'neutral',
};
/** Brand ramp keys defined in Sana CSS but not yet exported from canvas-tokens-web. */
const EXTENDED_BRAND_TOKEN_MAP = {
neutral150: '--cnvs-brand-neutral-150',
neutral850: '--cnvs-brand-neutral-850',
neutralA150: '--cnvs-brand-neutral-a150',
neutralA850: '--cnvs-brand-neutral-a850',
primaryA300: '--cnvs-brand-primary-a300',
criticalA300: '--cnvs-brand-critical-a300',
cautionA300: '--cnvs-brand-caution-a300',
positiveA300: '--cnvs-brand-positive-a300',
};
const setStyleVar = (style, token, value) => {
// @ts-ignore - CSS custom property key
style[token] = maybeWrapCSSVariables(value);
};
/** Maps a numerical brand ramp onto `brand.<color><rampKey>` CSS variables. */
export function writeNumericalBrandRamp(color, ramp, style, options) {
if (!ramp) {
return;
}
Object.keys(ramp).forEach(rampKey => {
var _a, _b;
if ((_a = options === null || options === void 0 ? void 0 : options.skipKeys) === null || _a === void 0 ? void 0 : _a.has(String(rampKey))) {
return;
}
const value = ramp[rampKey];
if (value == null) {
return;
}
const token = color === 'action'
? brand.action[rampKey]
: // @ts-ignore - dynamic token lookup
((_b = brand[`${color}${rampKey}`]) !== null && _b !== void 0 ? _b : EXTENDED_BRAND_TOKEN_MAP[`${color}${rampKey}`]);
if (token) {
setStyleVar(style, token, value);
}
});
}
/** Called when consumer sets only `primary.main` or `brand.primary['600']`. */
export function applyPrimaryBrandBundle(primaryColor, style) {
const value = maybeWrapCSSVariables(primaryColor);
setStyleVar(style, brand.primary.base, value);
setStyleVar(style, brand.primary600, value);
setStyleVar(style, brand.action.base, value);
if (system.color.brand.accent.primary) {
setStyleVar(style, system.color.brand.accent.primary, value);
}
if (system.color.brand.accent.action) {
setStyleVar(style, system.color.brand.accent.action, value);
}
const selectedFg = colorSpace.pressed({
color: `var(${brand.primary600})`,
fallback: value,
colorType: 'accent',
});
const selectedSurface = colorSpace.darken({
color: system.color.bg.default,
fallback: 'white',
mixinColor: `var(${brand.primary600})`,
mixinValue: '0.11',
});
setStyleVar(style, brand.primary700, selectedFg);
setStyleVar(style, brand.primary.dark, selectedFg);
setStyleVar(style, brand.primaryA50, selectedSurface);
if (BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg) {
setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg, selectedFg);
}
if (BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface) {
setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface, selectedSurface);
}
const hoverColor = colorSpace.hover({
color: `var(${brand.action.base})`,
fallback: value,
colorType: 'accent',
});
const pressedColor = colorSpace.pressed({
color: `var(${brand.action.base})`,
fallback: value,
colorType: 'accent',
});
setStyleVar(style, brand.action.dark, hoverColor);
setStyleVar(style, brand.action.darkest, pressedColor);
}
/** Called when consumer sets only `error.main` or `brand.critical['600']`. */
export function applyCriticalBrandBundle(criticalColor, style) {
var _a;
const value = maybeWrapCSSVariables(criticalColor);
setStyleVar(style, brand.error.base, value);
setStyleVar(style, brand.critical600, value);
setStyleVar(style, brand.critical500, value);
if (system.color.brand.accent.critical) {
setStyleVar(style, system.color.brand.accent.critical, value);
}
if ((_a = system.color.brand.fg.critical) === null || _a === void 0 ? void 0 : _a.default) {
setStyleVar(style, system.color.brand.fg.critical.default, value);
}
if (system.color.brand.border.critical) {
setStyleVar(style, system.color.brand.border.critical, value);
}
if (system.color.brand.focus.critical) {
setStyleVar(style, system.color.brand.focus.critical, value);
}
}
/** Called when consumer sets only `alert.main` or `brand.caution['400']`. */
export function applyCautionBrandBundle(cautionColor, style) {
var _a, _b, _c;
const value = maybeWrapCSSVariables(cautionColor);
setStyleVar(style, brand.alert.base, value);
setStyleVar(style, brand.caution400, value);
setStyleVar(style, brand.caution500, value);
if (system.color.brand.accent.caution) {
setStyleVar(style, system.color.brand.accent.caution, value);
}
if ((_a = system.color.brand.fg.caution) === null || _a === void 0 ? void 0 : _a.default) {
setStyleVar(style, system.color.brand.fg.caution.default, value);
}
if (system.color.brand.border.caution) {
setStyleVar(style, system.color.brand.border.caution, value);
}
if ((_b = system.color.brand.focus.caution) === null || _b === void 0 ? void 0 : _b.inner) {
setStyleVar(style, system.color.brand.focus.caution.inner, value);
}
if ((_c = system.color.brand.focus.caution) === null || _c === void 0 ? void 0 : _c.outer) {
setStyleVar(style, system.color.brand.focus.caution.outer, value);
}
}
/** Called when consumer sets only `success.main` or `brand.positive['600']`. */
export function applyPositiveBrandBundle(positiveColor, style) {
var _a;
const value = maybeWrapCSSVariables(positiveColor);
setStyleVar(style, brand.success.base, value);
setStyleVar(style, brand.positive600, value);
if (system.color.brand.accent.positive) {
setStyleVar(style, system.color.brand.accent.positive, value);
}
if ((_a = system.color.brand.fg.positive) === null || _a === void 0 ? void 0 : _a.default) {
setStyleVar(style, system.color.brand.fg.positive.default, value);
}
}
/** Called when consumer sets only `neutral.main` or `brand.neutral['600']`. */
export function applyNeutralBrandBundle(neutralColor, style) {
const value = maybeWrapCSSVariables(neutralColor);
setStyleVar(style, brand.neutral.base, value);
setStyleVar(style, brand.neutral600, value);
}
/** Writes first-class selected shortcuts from numerical theme input. */
export function writeSelectedShortcuts(selected, style) {
if (!selected) {
return;
}
if (selected.fg && BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg) {
setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg, selected.fg);
}
if (selected.surface && BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface) {
setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface, selected.surface);
}
}
/** Writes first-class focus shortcuts — never derived from brand.primary. */
export function writeFocusShortcuts(focus, style) {
if (focus === null || focus === void 0 ? void 0 : focus.primary) {
writeFocusBorderBundle(focus.primary, style);
}
}
/** Writes focus/border system tokens without touching brand.primary500. */
function writeFocusBorderBundle(value, style) {
setStyleVar(style, brand.common.focusOutline, value);
if (BRAND_SCOPE_INDEPENDENT.focus.primary) {
setStyleVar(style, BRAND_SCOPE_INDEPENDENT.focus.primary, value);
}
if (BRAND_SCOPE_INDEPENDENT.border.primary) {
setStyleVar(style, BRAND_SCOPE_INDEPENDENT.border.primary, value);
}
}
/**
* Writes focus/border from explicit input — never from primary.main or primary.500.
* Numerical themes use {@link writeFocusShortcuts}; legacy uses `common.focusOutline`.
*/
export function writeIndependentBrandTokens(theme, style) {
var _a, _b;
if (!theme) {
return;
}
if (isNumericalTheme(theme)) {
if (theme.system) {
writeSystemBrandOverrides(theme.system, style);
}
return;
}
const common = (_b = (_a = theme.canvas) === null || _a === void 0 ? void 0 : _a.palette) === null || _b === void 0 ? void 0 : _b.common;
if (!common) {
return;
}
['focusOutline', 'alertInner', 'alertOuter', 'errorInner'].forEach(key => {
var _a;
const rawValue = common[key];
if (rawValue == null) {
return;
}
if (rawValue === defaultCanvasTheme.palette.common[key]) {
return;
}
const value = maybeWrapCSSVariables(rawValue);
// @ts-ignore
setStyleVar(style, brand.common[key], value);
// @ts-ignore
setStyleVar(style, commonTokenMapping[key], value);
if (key === 'focusOutline') {
writeFocusBorderBundle(value, style);
}
if (key === 'alertInner' && ((_a = system.color.brand.focus.caution) === null || _a === void 0 ? void 0 : _a.inner)) {
setStyleVar(style, system.color.brand.focus.caution.inner, value);
}
if (key === 'alertOuter' && system.color.brand.border.caution) {
setStyleVar(style, system.color.brand.border.caution, value);
}
if (key === 'errorInner') {
if (system.color.brand.focus.critical) {
setStyleVar(style, system.color.brand.focus.critical, value);
}
if (system.color.brand.border.critical) {
setStyleVar(style, system.color.brand.border.critical, value);
}
}
});
}
/** Walks nested system override object and sets matching system CSS vars. */
export function writeSystemBrandOverrides(systemOverrides, style) {
var _a;
const brandOverrides = (_a = systemOverrides === null || systemOverrides === void 0 ? void 0 : systemOverrides.color) === null || _a === void 0 ? void 0 : _a.brand;
if (!brandOverrides) {
return;
}
const walk = (node, path) => {
Object.entries(node).forEach(([key, val]) => {
var _a, _b;
if (val && typeof val === 'object') {
walk(val, [...path, key]);
}
else if (typeof val === 'string') {
let tokenNode = system.color.brand;
for (const seg of [...path, key]) {
// @ts-ignore
tokenNode = tokenNode === null || tokenNode === void 0 ? void 0 : tokenNode[seg];
}
if (typeof tokenNode === 'string') {
const cssVarName = (_b = (_a = tokenNode.match(/--[\w-]+/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : tokenNode;
setStyleVar(style, cssVarName, val);
}
}
});
};
walk(brandOverrides, []);
}
const mappedKeys = {
lightest: 'lightest',
lighter: 'lighter',
light: 'light',
main: 'base',
dark: 'dark',
darkest: 'darkest',
contrast: 'accent',
};
const numericalTokenMapping = {
lightest: '25',
lighter: '50',
light: '200',
main: '600',
dark: '700',
darkest: '800',
};
/** Full-scope semantic writer — filled palette compared against defaults unless `writeAll`. */
export function writeSemanticTheme(palette, style, options) {
var _a;
const writeAll = (_a = options === null || options === void 0 ? void 0 : options.writeAll) !== null && _a !== void 0 ? _a : false;
['common', 'primary', 'error', 'alert', 'success', 'neutral'].forEach(color => {
if (color === 'common') {
['focusOutline', 'alertInner', 'alertOuter', 'errorInner'].forEach(key => {
var _a;
if (writeAll || palette.common[key] !== defaultCanvasTheme.palette.common[key]) {
const value = maybeWrapCSSVariables(palette.common[key]);
// @ts-ignore
setStyleVar(style, brand.common[key], value);
// @ts-ignore
setStyleVar(style, commonTokenMapping[key], value);
if (key === 'focusOutline') {
writeFocusBorderBundle(value, style);
}
if (key === 'alertInner' && ((_a = system.color.brand.focus.caution) === null || _a === void 0 ? void 0 : _a.inner)) {
setStyleVar(style, system.color.brand.focus.caution.inner, value);
}
if (key === 'alertOuter' && system.color.brand.border.caution) {
setStyleVar(style, system.color.brand.border.caution, value);
}
if (key === 'errorInner') {
if (system.color.brand.focus.critical) {
setStyleVar(style, system.color.brand.focus.critical, value);
}
if (system.color.brand.border.critical) {
setStyleVar(style, system.color.brand.border.critical, value);
}
}
}
});
}
else {
['lightest', 'lighter', 'light', 'main', 'dark', 'darkest', 'contrast'].forEach(key => {
var _a, _b, _c, _d, _e;
// @ts-ignore
if (writeAll || palette[color][key] !== defaultCanvasTheme.palette[color][key]) {
// @ts-ignore
const value = maybeWrapCSSVariables(palette[color][key]);
// @ts-ignore
setStyleVar(style, brand[color][mappedKeys[key]], value);
if (key !== 'contrast' && key in numericalTokenMapping) {
const newBrandColor = brandColorMapping[color];
const numericalSuffix = numericalTokenMapping[key];
// @ts-ignore
const numericalToken = brand[newBrandColor + numericalSuffix];
if (numericalToken) {
setStyleVar(style, numericalToken, value);
}
if (key === 'main') {
// @ts-ignore
const systemAccentToken = system.color.brand.accent[newBrandColor];
if (systemAccentToken) {
setStyleVar(style, systemAccentToken, value);
}
// @ts-ignore
const systemFgToken = (_a = system.color.brand.fg[newBrandColor]) === null || _a === void 0 ? void 0 : _a.default;
if (systemFgToken) {
setStyleVar(style, systemFgToken, value);
}
}
else if (key === 'dark') {
// @ts-ignore
const systemFgStrongToken = (_b = system.color.brand.fg[newBrandColor]) === null || _b === void 0 ? void 0 : _b.strong;
if (systemFgStrongToken) {
setStyleVar(style, systemFgStrongToken, value);
}
if (newBrandColor === 'primary' && system.color.brand.fg.selected) {
setStyleVar(style, system.color.brand.fg.selected, value);
}
if (newBrandColor === 'critical') {
if (system.color.brand.focus.critical) {
setStyleVar(style, system.color.brand.focus.critical, value);
}
if (system.color.brand.border.critical) {
setStyleVar(style, system.color.brand.border.critical, value);
}
}
if (newBrandColor === 'caution') {
if ((_c = system.color.brand.focus.caution) === null || _c === void 0 ? void 0 : _c.outer) {
setStyleVar(style, system.color.brand.focus.caution.outer, value);
}
if (system.color.brand.border.caution) {
setStyleVar(style, system.color.brand.border.caution, value);
}
}
}
else if (key === 'lighter') {
// @ts-ignore
const surfaceStrongToken = (_d = system.color.brand.surface[newBrandColor]) === null || _d === void 0 ? void 0 : _d.strong;
if (surfaceStrongToken) {
setStyleVar(style, surfaceStrongToken, value);
}
if (newBrandColor === 'primary' && system.color.brand.surface.selected) {
setStyleVar(style, system.color.brand.surface.selected, value);
}
}
else if (key === 'lightest') {
// @ts-ignore
const surfaceDefaultToken = (_e = system.color.brand.surface[newBrandColor]) === null || _e === void 0 ? void 0 : _e.default;
if (surfaceDefaultToken) {
setStyleVar(style, surfaceDefaultToken, value);
}
}
}
}
});
}
});
}
/** Writes numerical theme — brand scope applies per-family shortcuts; remaining keys write 1:1. */
export function writeNumericalTheme(theme, style, scope) {
var _a, _b, _c;
const brandPalette = theme.brand;
const skipRamp = {};
if (scope === 'brand' && brandPalette) {
if (((_a = brandPalette.primary) === null || _a === void 0 ? void 0 : _a['600']) && Object.keys(brandPalette.primary).length === 1) {
applyPrimaryBrandBundle(brandPalette.primary['600'], style);
skipRamp.primary = new Set(['600']);
// Do not skip action keys — explicit `brand.action.*` must win over derived shades
// written by applyPrimaryBrandBundle (writeNumericalBrandRamp runs after).
}
// Only the main ramp key triggers the family shortcut. Lone `'500'` must
// write 1:1 and must not overwrite critical600 / caution400 via the bundle.
const critical = brandPalette.critical;
if ((critical === null || critical === void 0 ? void 0 : critical['600']) && Object.keys(critical).length === 1) {
applyCriticalBrandBundle(critical['600'], style);
skipRamp.critical = new Set(['600']);
}
const caution = brandPalette.caution;
if ((caution === null || caution === void 0 ? void 0 : caution['400']) && Object.keys(caution).length === 1) {
applyCautionBrandBundle(caution['400'], style);
skipRamp.caution = new Set(['400']);
}
if (((_b = brandPalette.positive) === null || _b === void 0 ? void 0 : _b['600']) && Object.keys(brandPalette.positive).length === 1) {
applyPositiveBrandBundle(brandPalette.positive['600'], style);
skipRamp.positive = new Set(['600']);
}
if (((_c = brandPalette.neutral) === null || _c === void 0 ? void 0 : _c['600']) && Object.keys(brandPalette.neutral).length === 1) {
applyNeutralBrandBundle(brandPalette.neutral['600'], style);
skipRamp.neutral = new Set(['600']);
}
}
if (brandPalette) {
['primary', 'critical', 'caution', 'positive', 'neutral', 'action'].forEach(color => {
writeNumericalBrandRamp(color, brandPalette[color], style, { skipKeys: skipRamp[color] });
});
}
writeSelectedShortcuts(theme.selected, style);
writeFocusShortcuts(theme.focus, style);
writeIndependentBrandTokens(theme, style);
}
/** Brand-scope semantic path — reads raw input only, not useTheme-filled palette. */
export function writeBrandScopeSemantic(theme, style) {
var _a, _b, _c, _d, _e, _f;
const palette = (_a = theme.canvas) === null || _a === void 0 ? void 0 : _a.palette;
if ((_b = palette === null || palette === void 0 ? void 0 : palette.primary) === null || _b === void 0 ? void 0 : _b.main) {
applyPrimaryBrandBundle(palette.primary.main, style);
}
if ((_c = palette === null || palette === void 0 ? void 0 : palette.error) === null || _c === void 0 ? void 0 : _c.main) {
applyCriticalBrandBundle(palette.error.main, style);
}
if ((_d = palette === null || palette === void 0 ? void 0 : palette.alert) === null || _d === void 0 ? void 0 : _d.main) {
applyCautionBrandBundle(palette.alert.main, style);
}
if ((_e = palette === null || palette === void 0 ? void 0 : palette.success) === null || _e === void 0 ? void 0 : _e.main) {
applyPositiveBrandBundle(palette.success.main, style);
}
if ((_f = palette === null || palette === void 0 ? void 0 : palette.neutral) === null || _f === void 0 ? void 0 : _f.main) {
applyNeutralBrandBundle(palette.neutral.main, style);
}
writeIndependentBrandTokens(theme, style);
}