@workday/canvas-kit-react 16.0.0-alpha.0494-next.0 → 16.0.0-alpha.0500-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/button/lib/ToolbarDropdownButton.tsx +3 -18
  2. package/button/lib/ToolbarIconButton.tsx +2 -10
  3. package/common/lib/CanvasProvider.tsx +156 -324
  4. package/common/lib/theming/README.md +49 -6
  5. package/common/lib/theming/brandScope.ts +566 -0
  6. package/common/lib/theming/index.ts +1 -0
  7. package/common/lib/theming/sanaTheme.ts +133 -0
  8. package/common/lib/theming/types.ts +292 -0
  9. package/dist/commonjs/banner/lib/Banner.js +2 -2
  10. package/dist/commonjs/button/lib/DeleteButton.js +1 -1
  11. package/dist/commonjs/button/lib/PrimaryButton.js +2 -2
  12. package/dist/commonjs/button/lib/ToolbarDropdownButton.d.ts.map +1 -1
  13. package/dist/commonjs/button/lib/ToolbarDropdownButton.js +4 -4
  14. package/dist/commonjs/button/lib/ToolbarIconButton.d.ts.map +1 -1
  15. package/dist/commonjs/button/lib/ToolbarIconButton.js +1 -1
  16. package/dist/commonjs/common/lib/CanvasProvider.d.ts +296 -5
  17. package/dist/commonjs/common/lib/CanvasProvider.d.ts.map +1 -1
  18. package/dist/commonjs/common/lib/CanvasProvider.js +105 -287
  19. package/dist/commonjs/common/lib/theming/brandScope.d.ts +64 -0
  20. package/dist/commonjs/common/lib/theming/brandScope.d.ts.map +1 -0
  21. package/dist/commonjs/common/lib/theming/brandScope.js +499 -0
  22. package/dist/commonjs/common/lib/theming/index.d.ts +1 -0
  23. package/dist/commonjs/common/lib/theming/index.d.ts.map +1 -1
  24. package/dist/commonjs/common/lib/theming/index.js +1 -0
  25. package/dist/commonjs/common/lib/theming/sanaTheme.d.ts +33 -0
  26. package/dist/commonjs/common/lib/theming/sanaTheme.d.ts.map +1 -0
  27. package/dist/commonjs/common/lib/theming/sanaTheme.js +130 -0
  28. package/dist/commonjs/common/lib/theming/types.d.ts +162 -0
  29. package/dist/commonjs/common/lib/theming/types.d.ts.map +1 -1
  30. package/dist/commonjs/common/lib/theming/types.js +81 -1
  31. package/dist/commonjs/pill/lib/Pill.d.ts +2 -2
  32. package/dist/commonjs/pill/lib/Pill.js +4 -4
  33. package/dist/commonjs/popup/lib/hooks/usePopupStack.d.ts.map +1 -1
  34. package/dist/commonjs/popup/lib/hooks/usePopupStack.js +18 -16
  35. package/dist/commonjs/testing/lib/StaticStates.d.ts +2 -2
  36. package/dist/commonjs/testing/lib/StaticStates.d.ts.map +1 -1
  37. package/dist/commonjs/testing/lib/StaticStates.js +5 -2
  38. package/dist/es6/banner/lib/Banner.js +2 -2
  39. package/dist/es6/button/lib/DeleteButton.js +1 -1
  40. package/dist/es6/button/lib/PrimaryButton.js +2 -2
  41. package/dist/es6/button/lib/ToolbarDropdownButton.d.ts.map +1 -1
  42. package/dist/es6/button/lib/ToolbarDropdownButton.js +5 -5
  43. package/dist/es6/button/lib/ToolbarIconButton.d.ts.map +1 -1
  44. package/dist/es6/button/lib/ToolbarIconButton.js +1 -1
  45. package/dist/es6/common/lib/CanvasProvider.d.ts +296 -5
  46. package/dist/es6/common/lib/CanvasProvider.d.ts.map +1 -1
  47. package/dist/es6/common/lib/CanvasProvider.js +82 -288
  48. package/dist/es6/common/lib/theming/brandScope.d.ts +64 -0
  49. package/dist/es6/common/lib/theming/brandScope.d.ts.map +1 -0
  50. package/dist/es6/common/lib/theming/brandScope.js +483 -0
  51. package/dist/es6/common/lib/theming/index.d.ts +1 -0
  52. package/dist/es6/common/lib/theming/index.d.ts.map +1 -1
  53. package/dist/es6/common/lib/theming/index.js +1 -0
  54. package/dist/es6/common/lib/theming/sanaTheme.d.ts +33 -0
  55. package/dist/es6/common/lib/theming/sanaTheme.d.ts.map +1 -0
  56. package/dist/es6/common/lib/theming/sanaTheme.js +127 -0
  57. package/dist/es6/common/lib/theming/types.d.ts +162 -0
  58. package/dist/es6/common/lib/theming/types.d.ts.map +1 -1
  59. package/dist/es6/common/lib/theming/types.js +78 -0
  60. package/dist/es6/pill/lib/Pill.d.ts +2 -2
  61. package/dist/es6/pill/lib/Pill.js +5 -5
  62. package/dist/es6/popup/lib/hooks/usePopupStack.d.ts.map +1 -1
  63. package/dist/es6/popup/lib/hooks/usePopupStack.js +13 -11
  64. package/dist/es6/testing/lib/StaticStates.d.ts +2 -2
  65. package/dist/es6/testing/lib/StaticStates.d.ts.map +1 -1
  66. package/dist/es6/testing/lib/StaticStates.js +6 -3
  67. package/package.json +6 -6
  68. package/pill/lib/Pill.tsx +5 -5
  69. package/popup/lib/hooks/usePopupStack.ts +13 -11
  70. package/testing/lib/StaticStates.tsx +11 -9
@@ -6,6 +6,13 @@
6
6
  > guide, see our
7
7
  > [Theming Documentation](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs).
8
8
 
9
+ ## Sana Canvas Theme
10
+
11
+ For application-level theming in v16, import the Sana CSS variables and set `data-theme="sana-canvas"`
12
+ on `<html>`. See the
13
+ [v16 Upgrade Guide](https://workday.github.io/canvas-kit/?path=/docs/guides-upgrade-guides-v-16-0-overview--docs#sana-canvas-theme)
14
+ for the canonical setup. The `CanvasProvider` `theme` prop is for **scoped** theming only.
15
+
9
16
  ## Installation
10
17
 
11
18
  ```sh
@@ -14,7 +21,7 @@ yarn add @workday/canvas-kit-react/common
14
21
 
15
22
  ## Recommended Approach: CSS Variables
16
23
 
17
- Canvas Kit v14+ promotes using CSS variables for theming. Import CSS variable files and override
24
+ Canvas Kit v16 promotes using CSS variables for theming. Import CSS variable files and override
18
25
  values in your root CSS:
19
26
 
20
27
  ```css
@@ -23,12 +30,13 @@ values in your root CSS:
23
30
  @import '@workday/canvas-tokens-web/css/system/_variables.css';
24
31
  @import '@workday/canvas-tokens-web/css/brand/_variables.css';
25
32
  @import '@workday/canvas-tokens-web/css/component/_variables.css';
33
+ @import '@workday/canvas-tokens-web/css/sana/_variables.css';
26
34
 
27
35
  :root {
28
36
  /* Override brand primary colors */
29
- --cnvs-brand-primary-base: var(--cnvs-base-palette-magenta-600);
30
- --cnvs-brand-primary-light: var(--cnvs-base-palette-magenta-200);
31
- --cnvs-brand-primary-dark: var(--cnvs-base-palette-magenta-700);
37
+ --cnvs-brand-primary-600: var(--cnvs-base-palette-magenta-600);
38
+ --cnvs-brand-primary-500: var(--cnvs-base-palette-magenta-500);
39
+ --cnvs-brand-primary-A50: var(--cnvs-base-palette-magenta-A50);
32
40
  }
33
41
  ```
34
42
 
@@ -40,8 +48,8 @@ import {createStyles} from '@workday/canvas-kit-styling';
40
48
  import {base, brand} from '@workday/canvas-tokens-web';
41
49
 
42
50
  const themedBrand = createStyles({
43
- [brand.primary.base]: base.legacy.magenta600,
44
- [brand.primary.dark]: base.legacy.magenta700,
51
+ [brand.primary600]: base.magenta600,
52
+ [brand.primary700]: base.magenta700,
45
53
  });
46
54
 
47
55
  <CanvasProvider className={themedBrand}>
@@ -49,6 +57,41 @@ const themedBrand = createStyles({
49
57
  </CanvasProvider>;
50
58
  ```
51
59
 
60
+ ## Scoped Theming (CanvasProvider)
61
+
62
+ For embedded or multi-brand sections, use the numerical `brand` shape:
63
+
64
+ ```tsx
65
+ import {CanvasProvider} from '@workday/canvas-kit-react/common';
66
+ import {base} from '@workday/canvas-tokens-web';
67
+
68
+ <CanvasProvider theme={{brand: {primary: {'600': base.magenta600}}}}>
69
+ <ScopedSection />
70
+ </CanvasProvider>
71
+ ```
72
+
73
+ ### Popups and `sanaCanvasProviderTheme`
74
+
75
+ Popups (menus, selects, modals) portal to `document.body`. Theme inheritance depends on where
76
+ `data-theme="sana-canvas"` lives:
77
+
78
+ - **You control `<html>`:** set `data-theme="sana-canvas"` there. Popups inherit Sana variables —
79
+ no `theme` prop needed.
80
+ - **You cannot control `<html>`** (embedded apps, microfrontends, third-party shells): pass
81
+ `sanaCanvasProviderTheme` to your root `CanvasProvider`. Nested `data-theme` on a wrapper does
82
+ not reach portaled popups; this preset forwards Sana brand variables onto the popup stack.
83
+
84
+ ```tsx
85
+ import {CanvasProvider, sanaCanvasProviderTheme} from '@workday/canvas-kit-react/common';
86
+
87
+ // Required when <html> is unavailable
88
+ <CanvasProvider theme={sanaCanvasProviderTheme}>
89
+ <App />
90
+ </CanvasProvider>
91
+ ```
92
+
93
+ See the [Theming documentation](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs) for details.
94
+
52
95
  ## Bidirectionality (RTL Support)
53
96
 
54
97
  ### Setting RTL Direction
@@ -0,0 +1,566 @@
1
+ import * as React from 'react';
2
+
3
+ import {colorSpace, maybeWrapCSSVariables} from '@workday/canvas-kit-styling';
4
+ import {brand, system} from '@workday/canvas-tokens-web';
5
+
6
+ import {defaultCanvasTheme} from './theme';
7
+ import {
8
+ CanvasActionBrandRamp,
9
+ CanvasBrandRamp,
10
+ CanvasNeutralBrandRamp,
11
+ CanvasNumericalBrandTheme,
12
+ CanvasProviderTheme,
13
+ CanvasTheme,
14
+ PartialEmotionCanvasTheme,
15
+ isNumericalTheme,
16
+ } from './types';
17
+
18
+ /** True when a semantic theme intentionally passes a non-empty `canvas.palette`. */
19
+ export function hasExplicitSemanticPalette(
20
+ theme: CanvasProviderTheme
21
+ ): theme is PartialEmotionCanvasTheme {
22
+ return (
23
+ !isNumericalTheme(theme) &&
24
+ !!theme.canvas?.palette &&
25
+ Object.keys(theme.canvas.palette).length > 0
26
+ );
27
+ }
28
+
29
+ /** Tokens that change when a consumer sets only their primary brand color. */
30
+ export const BRAND_SCOPE_PRIMARY_BUNDLE = {
31
+ action: ['base', 'dark', 'darkest', 'accent'] as const,
32
+ accent: ['primary', 'action'] as const,
33
+ selected: {
34
+ fg: system.color.brand.fg.selected,
35
+ surface: system.color.brand.surface.selected,
36
+ },
37
+ } as const;
38
+
39
+ /**
40
+ * Independently brandable tokens — writable when explicitly provided,
41
+ * but NEVER derived from primary.main / brand.primary.600.
42
+ */
43
+ export const BRAND_SCOPE_INDEPENDENT = {
44
+ focus: {
45
+ primary: system.color.brand.focus.primary,
46
+ brandToken: brand.primary500,
47
+ semanticKey: 'focusOutline' as const,
48
+ },
49
+ border: {
50
+ primary: system.color.brand.border.primary,
51
+ brandToken: brand.primary500,
52
+ semanticKey: 'focusOutline' as const,
53
+ },
54
+ } as const;
55
+
56
+ const commonTokenMapping = {
57
+ focusOutline: brand.common.focus,
58
+ alertInner: brand.common.caution.inner,
59
+ alertOuter: brand.common.caution.outer,
60
+ errorInner: brand.common.critical,
61
+ } as const;
62
+
63
+ type BrandColor = 'primary' | 'critical' | 'caution' | 'positive' | 'neutral';
64
+
65
+ const brandColorMapping: Record<string, BrandColor> = {
66
+ primary: 'primary',
67
+ error: 'critical',
68
+ success: 'positive',
69
+ alert: 'caution',
70
+ neutral: 'neutral',
71
+ };
72
+
73
+ /** Brand ramp keys defined in Sana CSS but not yet exported from canvas-tokens-web. */
74
+ const EXTENDED_BRAND_TOKEN_MAP: Record<string, string> = {
75
+ neutral150: '--cnvs-brand-neutral-150',
76
+ neutral850: '--cnvs-brand-neutral-850',
77
+ neutralA150: '--cnvs-brand-neutral-a150',
78
+ };
79
+
80
+ const setStyleVar = (style: React.CSSProperties, token: string, value: string) => {
81
+ // @ts-ignore - CSS custom property key
82
+ style[token] = maybeWrapCSSVariables(value);
83
+ };
84
+
85
+ /** Maps a numerical brand ramp onto `brand.<color><rampKey>` CSS variables. */
86
+ export function writeNumericalBrandRamp(
87
+ color: BrandColor | 'action',
88
+ ramp: CanvasBrandRamp | CanvasNeutralBrandRamp | CanvasActionBrandRamp | undefined,
89
+ style: React.CSSProperties,
90
+ options?: {skipKeys?: Set<string>}
91
+ ) {
92
+ if (!ramp) {
93
+ return;
94
+ }
95
+ (
96
+ Object.keys(ramp) as Array<
97
+ keyof (CanvasBrandRamp | CanvasNeutralBrandRamp | CanvasActionBrandRamp)
98
+ >
99
+ ).forEach(rampKey => {
100
+ if (options?.skipKeys?.has(String(rampKey))) {
101
+ return;
102
+ }
103
+ const value = ramp[rampKey as keyof typeof ramp];
104
+ if (value == null) {
105
+ return;
106
+ }
107
+ const token =
108
+ color === 'action'
109
+ ? brand.action[rampKey as keyof typeof brand.action]
110
+ : // @ts-ignore - dynamic token lookup
111
+ (brand[`${color}${rampKey}`] ?? EXTENDED_BRAND_TOKEN_MAP[`${color}${rampKey}`]);
112
+ if (token) {
113
+ setStyleVar(style, token, value);
114
+ }
115
+ });
116
+ }
117
+
118
+ /** Called when consumer sets only `primary.main` or `brand.primary['600']`. */
119
+ export function applyPrimaryBrandBundle(primaryColor: string, style: React.CSSProperties) {
120
+ const value = maybeWrapCSSVariables(primaryColor);
121
+
122
+ setStyleVar(style, brand.primary.base, value);
123
+ setStyleVar(style, brand.primary600, value);
124
+ setStyleVar(style, brand.action.base, value);
125
+
126
+ if (system.color.brand.accent.primary) {
127
+ setStyleVar(style, system.color.brand.accent.primary, value);
128
+ }
129
+ if (system.color.brand.accent.action) {
130
+ setStyleVar(style, system.color.brand.accent.action, value);
131
+ }
132
+
133
+ const selectedFg = colorSpace.pressed({
134
+ color: `var(${brand.primary600})`,
135
+ fallback: value,
136
+ colorType: 'accent',
137
+ });
138
+ const selectedSurface = colorSpace.darken({
139
+ color: system.color.bg.default,
140
+ fallback: 'white',
141
+ mixinColor: `var(${brand.primary600})`,
142
+ mixinValue: '0.11',
143
+ });
144
+
145
+ setStyleVar(style, brand.primary700, selectedFg);
146
+ setStyleVar(style, brand.primary.dark, selectedFg);
147
+ setStyleVar(style, brand.primaryA50, selectedSurface);
148
+
149
+ if (BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg) {
150
+ setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg, selectedFg);
151
+ }
152
+ if (BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface) {
153
+ setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface, selectedSurface);
154
+ }
155
+
156
+ const hoverColor = colorSpace.hover({
157
+ color: `var(${brand.action.base})`,
158
+ fallback: value,
159
+ colorType: 'accent',
160
+ });
161
+ const pressedColor = colorSpace.pressed({
162
+ color: `var(${brand.action.base})`,
163
+ fallback: value,
164
+ colorType: 'accent',
165
+ });
166
+ setStyleVar(style, brand.action.dark, hoverColor);
167
+ setStyleVar(style, brand.action.darkest, pressedColor);
168
+ }
169
+
170
+ /** Called when consumer sets only `error.main` or `brand.critical['600']`. */
171
+ export function applyCriticalBrandBundle(criticalColor: string, style: React.CSSProperties) {
172
+ const value = maybeWrapCSSVariables(criticalColor);
173
+
174
+ setStyleVar(style, brand.error.base, value);
175
+ setStyleVar(style, brand.critical600, value);
176
+ setStyleVar(style, brand.critical500, value);
177
+
178
+ if (system.color.brand.accent.critical) {
179
+ setStyleVar(style, system.color.brand.accent.critical, value);
180
+ }
181
+ if (system.color.brand.fg.critical?.default) {
182
+ setStyleVar(style, system.color.brand.fg.critical.default, value);
183
+ }
184
+ if (system.color.brand.border.critical) {
185
+ setStyleVar(style, system.color.brand.border.critical, value);
186
+ }
187
+ if (system.color.brand.focus.critical) {
188
+ setStyleVar(style, system.color.brand.focus.critical, value);
189
+ }
190
+ }
191
+
192
+ /** Called when consumer sets only `alert.main` or `brand.caution['400']`. */
193
+ export function applyCautionBrandBundle(cautionColor: string, style: React.CSSProperties) {
194
+ const value = maybeWrapCSSVariables(cautionColor);
195
+
196
+ setStyleVar(style, brand.alert.base, value);
197
+ setStyleVar(style, brand.caution400, value);
198
+ setStyleVar(style, brand.caution500, value);
199
+
200
+ if (system.color.brand.accent.caution) {
201
+ setStyleVar(style, system.color.brand.accent.caution, value);
202
+ }
203
+ if (system.color.brand.fg.caution?.default) {
204
+ setStyleVar(style, system.color.brand.fg.caution.default, value);
205
+ }
206
+ if (system.color.brand.border.caution) {
207
+ setStyleVar(style, system.color.brand.border.caution, value);
208
+ }
209
+ if (system.color.brand.focus.caution?.inner) {
210
+ setStyleVar(style, system.color.brand.focus.caution.inner, value);
211
+ }
212
+ if (system.color.brand.focus.caution?.outer) {
213
+ setStyleVar(style, system.color.brand.focus.caution.outer, value);
214
+ }
215
+ }
216
+
217
+ /** Called when consumer sets only `success.main` or `brand.positive['600']`. */
218
+ export function applyPositiveBrandBundle(positiveColor: string, style: React.CSSProperties) {
219
+ const value = maybeWrapCSSVariables(positiveColor);
220
+
221
+ setStyleVar(style, brand.success.base, value);
222
+ setStyleVar(style, brand.positive600, value);
223
+
224
+ if (system.color.brand.accent.positive) {
225
+ setStyleVar(style, system.color.brand.accent.positive, value);
226
+ }
227
+ if (system.color.brand.fg.positive?.default) {
228
+ setStyleVar(style, system.color.brand.fg.positive.default, value);
229
+ }
230
+ }
231
+
232
+ /** Called when consumer sets only `neutral.main` or `brand.neutral['600']`. */
233
+ export function applyNeutralBrandBundle(neutralColor: string, style: React.CSSProperties) {
234
+ const value = maybeWrapCSSVariables(neutralColor);
235
+
236
+ setStyleVar(style, brand.neutral.base, value);
237
+ setStyleVar(style, brand.neutral600, value);
238
+ }
239
+
240
+ /** Writes first-class selected shortcuts from numerical theme input. */
241
+ export function writeSelectedShortcuts(
242
+ selected: CanvasNumericalBrandTheme['selected'] | undefined,
243
+ style: React.CSSProperties
244
+ ) {
245
+ if (!selected) {
246
+ return;
247
+ }
248
+ if (selected.fg && BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg) {
249
+ setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.fg, selected.fg);
250
+ }
251
+ if (selected.surface && BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface) {
252
+ setStyleVar(style, BRAND_SCOPE_PRIMARY_BUNDLE.selected.surface, selected.surface);
253
+ }
254
+ }
255
+
256
+ function writeFocusBorderBundle(value: string, style: React.CSSProperties) {
257
+ setStyleVar(style, brand.common.focusOutline, value);
258
+ setStyleVar(style, brand.primary500, value);
259
+ if (BRAND_SCOPE_INDEPENDENT.focus.primary) {
260
+ setStyleVar(style, BRAND_SCOPE_INDEPENDENT.focus.primary, value);
261
+ }
262
+ if (BRAND_SCOPE_INDEPENDENT.border.primary) {
263
+ setStyleVar(style, BRAND_SCOPE_INDEPENDENT.border.primary, value);
264
+ }
265
+ }
266
+
267
+ /** Writes focus/border from explicit common or brand.primary.500 input — never from primary.main. */
268
+ export function writeIndependentBrandTokens(
269
+ theme: CanvasProviderTheme | undefined,
270
+ style: React.CSSProperties
271
+ ) {
272
+ if (!theme) {
273
+ return;
274
+ }
275
+
276
+ if (isNumericalTheme(theme)) {
277
+ const focusValue = theme.brand?.primary?.['500'];
278
+ if (focusValue) {
279
+ writeFocusBorderBundle(focusValue, style);
280
+ }
281
+ if (theme.system) {
282
+ writeSystemBrandOverrides(theme.system, style);
283
+ }
284
+ return;
285
+ }
286
+
287
+ const common = theme.canvas?.palette?.common;
288
+ if (!common) {
289
+ return;
290
+ }
291
+
292
+ (['focusOutline', 'alertInner', 'alertOuter', 'errorInner'] as const).forEach(key => {
293
+ const rawValue = common[key];
294
+ if (rawValue == null) {
295
+ return;
296
+ }
297
+ if (rawValue === defaultCanvasTheme.palette.common[key]) {
298
+ return;
299
+ }
300
+
301
+ const value = maybeWrapCSSVariables(rawValue);
302
+ // @ts-ignore
303
+ setStyleVar(style, brand.common[key], value);
304
+ // @ts-ignore
305
+ setStyleVar(style, commonTokenMapping[key], value);
306
+
307
+ if (key === 'focusOutline') {
308
+ writeFocusBorderBundle(value, style);
309
+ }
310
+ if (key === 'alertInner' && system.color.brand.focus.caution?.inner) {
311
+ setStyleVar(style, system.color.brand.focus.caution.inner, value);
312
+ }
313
+ if (key === 'alertOuter' && system.color.brand.border.caution) {
314
+ setStyleVar(style, system.color.brand.border.caution, value);
315
+ }
316
+ if (key === 'errorInner') {
317
+ if (system.color.brand.focus.critical) {
318
+ setStyleVar(style, system.color.brand.focus.critical, value);
319
+ }
320
+ if (system.color.brand.border.critical) {
321
+ setStyleVar(style, system.color.brand.border.critical, value);
322
+ }
323
+ }
324
+ });
325
+ }
326
+
327
+ /** Walks nested system override object and sets matching system CSS vars. */
328
+ export function writeSystemBrandOverrides(
329
+ systemOverrides: {color?: {brand?: Record<string, unknown>}} | undefined,
330
+ style: React.CSSProperties
331
+ ) {
332
+ const brandOverrides = systemOverrides?.color?.brand;
333
+ if (!brandOverrides) {
334
+ return;
335
+ }
336
+
337
+ const walk = (node: Record<string, unknown>, path: string[]) => {
338
+ Object.entries(node).forEach(([key, val]) => {
339
+ if (val && typeof val === 'object') {
340
+ walk(val as Record<string, unknown>, [...path, key]);
341
+ } else if (typeof val === 'string') {
342
+ let tokenNode: unknown = system.color.brand;
343
+ for (const seg of [...path, key]) {
344
+ // @ts-ignore
345
+ tokenNode = tokenNode?.[seg];
346
+ }
347
+ if (typeof tokenNode === 'string') {
348
+ const cssVarName = tokenNode.match(/--[\w-]+/)?.[0] ?? tokenNode;
349
+ setStyleVar(style, cssVarName, val);
350
+ }
351
+ }
352
+ });
353
+ };
354
+ walk(brandOverrides, []);
355
+ }
356
+
357
+ const mappedKeys = {
358
+ lightest: 'lightest',
359
+ lighter: 'lighter',
360
+ light: 'light',
361
+ main: 'base',
362
+ dark: 'dark',
363
+ darkest: 'darkest',
364
+ contrast: 'accent',
365
+ } as const;
366
+
367
+ const numericalTokenMapping = {
368
+ lightest: '25',
369
+ lighter: '50',
370
+ light: '200',
371
+ main: '600',
372
+ dark: '700',
373
+ darkest: '800',
374
+ } as const;
375
+
376
+ /** Full-scope semantic writer — filled palette compared against defaults unless `writeAll`. */
377
+ export function writeSemanticTheme(
378
+ palette: CanvasTheme['palette'],
379
+ style: React.CSSProperties,
380
+ options?: {writeAll?: boolean}
381
+ ) {
382
+ const writeAll = options?.writeAll ?? false;
383
+
384
+ (['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).forEach(color => {
385
+ if (color === 'common') {
386
+ (['focusOutline', 'alertInner', 'alertOuter', 'errorInner'] as const).forEach(key => {
387
+ if (writeAll || palette.common[key] !== defaultCanvasTheme.palette.common[key]) {
388
+ const value = maybeWrapCSSVariables(palette.common[key]);
389
+ // @ts-ignore
390
+ setStyleVar(style, brand.common[key], value);
391
+ // @ts-ignore
392
+ setStyleVar(style, commonTokenMapping[key], value);
393
+
394
+ if (key === 'focusOutline') {
395
+ writeFocusBorderBundle(value, style);
396
+ }
397
+ if (key === 'alertInner' && system.color.brand.focus.caution?.inner) {
398
+ setStyleVar(style, system.color.brand.focus.caution.inner, value);
399
+ }
400
+ if (key === 'alertOuter' && system.color.brand.border.caution) {
401
+ setStyleVar(style, system.color.brand.border.caution, value);
402
+ }
403
+ if (key === 'errorInner') {
404
+ if (system.color.brand.focus.critical) {
405
+ setStyleVar(style, system.color.brand.focus.critical, value);
406
+ }
407
+ if (system.color.brand.border.critical) {
408
+ setStyleVar(style, system.color.brand.border.critical, value);
409
+ }
410
+ }
411
+ }
412
+ });
413
+ } else {
414
+ (['lightest', 'lighter', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(
415
+ key => {
416
+ // @ts-ignore
417
+ if (writeAll || palette[color][key] !== defaultCanvasTheme.palette[color][key]) {
418
+ // @ts-ignore
419
+ const value = maybeWrapCSSVariables(palette[color][key]);
420
+ // @ts-ignore
421
+ setStyleVar(style, brand[color][mappedKeys[key]], value);
422
+
423
+ if (key !== 'contrast' && key in numericalTokenMapping) {
424
+ const newBrandColor = brandColorMapping[color];
425
+ const numericalSuffix =
426
+ numericalTokenMapping[key as keyof typeof numericalTokenMapping];
427
+ // @ts-ignore
428
+ const numericalToken = brand[newBrandColor + numericalSuffix];
429
+ if (numericalToken) {
430
+ setStyleVar(style, numericalToken, value);
431
+ }
432
+
433
+ if (key === 'main') {
434
+ // @ts-ignore
435
+ const systemAccentToken = system.color.brand.accent[newBrandColor];
436
+ if (systemAccentToken) {
437
+ setStyleVar(style, systemAccentToken, value);
438
+ }
439
+ // @ts-ignore
440
+ const systemFgToken = system.color.brand.fg[newBrandColor]?.default;
441
+ if (systemFgToken) {
442
+ setStyleVar(style, systemFgToken, value);
443
+ }
444
+ } else if (key === 'dark') {
445
+ // @ts-ignore
446
+ const systemFgStrongToken = system.color.brand.fg[newBrandColor]?.strong;
447
+ if (systemFgStrongToken) {
448
+ setStyleVar(style, systemFgStrongToken, value);
449
+ }
450
+ if (newBrandColor === 'primary' && system.color.brand.fg.selected) {
451
+ setStyleVar(style, system.color.brand.fg.selected, value);
452
+ }
453
+ if (newBrandColor === 'critical') {
454
+ if (system.color.brand.focus.critical) {
455
+ setStyleVar(style, system.color.brand.focus.critical, value);
456
+ }
457
+ if (system.color.brand.border.critical) {
458
+ setStyleVar(style, system.color.brand.border.critical, value);
459
+ }
460
+ }
461
+ if (newBrandColor === 'caution') {
462
+ if (system.color.brand.focus.caution?.outer) {
463
+ setStyleVar(style, system.color.brand.focus.caution.outer, value);
464
+ }
465
+ if (system.color.brand.border.caution) {
466
+ setStyleVar(style, system.color.brand.border.caution, value);
467
+ }
468
+ }
469
+ } else if (key === 'lighter') {
470
+ // @ts-ignore
471
+ const surfaceStrongToken = system.color.brand.surface[newBrandColor]?.strong;
472
+ if (surfaceStrongToken) {
473
+ setStyleVar(style, surfaceStrongToken, value);
474
+ }
475
+ if (newBrandColor === 'primary' && system.color.brand.surface.selected) {
476
+ setStyleVar(style, system.color.brand.surface.selected, value);
477
+ }
478
+ } else if (key === 'lightest') {
479
+ // @ts-ignore
480
+ const surfaceDefaultToken = system.color.brand.surface[newBrandColor]?.default;
481
+ if (surfaceDefaultToken) {
482
+ setStyleVar(style, surfaceDefaultToken, value);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ );
489
+ }
490
+ });
491
+ }
492
+
493
+ /** Writes numerical theme — brand scope applies per-family shortcuts; remaining keys write 1:1. */
494
+ export function writeNumericalTheme(
495
+ theme: CanvasNumericalBrandTheme,
496
+ style: React.CSSProperties,
497
+ scope: 'brand' | 'full'
498
+ ) {
499
+ const brandPalette = theme.brand;
500
+ const skipRamp: Partial<Record<BrandColor | 'action', Set<string>>> = {};
501
+
502
+ if (scope === 'brand' && brandPalette) {
503
+ if (brandPalette.primary?.['600'] && Object.keys(brandPalette.primary).length === 1) {
504
+ applyPrimaryBrandBundle(brandPalette.primary['600'], style);
505
+ skipRamp.primary = new Set(['600']);
506
+ // Do not skip action keys — explicit `brand.action.*` must win over derived shades
507
+ // written by applyPrimaryBrandBundle (writeNumericalBrandRamp runs after).
508
+ }
509
+ // Only the main ramp key triggers the family shortcut. Lone `'500'` (focus/border) must
510
+ // write 1:1 and must not overwrite critical600 / caution400 via the bundle.
511
+ const critical = brandPalette.critical;
512
+ if (critical?.['600'] && Object.keys(critical).length === 1) {
513
+ applyCriticalBrandBundle(critical['600'], style);
514
+ skipRamp.critical = new Set(['600']);
515
+ }
516
+ const caution = brandPalette.caution;
517
+ if (caution?.['400'] && Object.keys(caution).length === 1) {
518
+ applyCautionBrandBundle(caution['400'], style);
519
+ skipRamp.caution = new Set(['400']);
520
+ }
521
+ if (brandPalette.positive?.['600'] && Object.keys(brandPalette.positive).length === 1) {
522
+ applyPositiveBrandBundle(brandPalette.positive['600'], style);
523
+ skipRamp.positive = new Set(['600']);
524
+ }
525
+ if (brandPalette.neutral?.['600'] && Object.keys(brandPalette.neutral).length === 1) {
526
+ applyNeutralBrandBundle(brandPalette.neutral['600'], style);
527
+ skipRamp.neutral = new Set(['600']);
528
+ }
529
+ }
530
+
531
+ if (brandPalette) {
532
+ (['primary', 'critical', 'caution', 'positive', 'neutral', 'action'] as const).forEach(
533
+ color => {
534
+ writeNumericalBrandRamp(color, brandPalette[color], style, {skipKeys: skipRamp[color]});
535
+ }
536
+ );
537
+ }
538
+
539
+ writeSelectedShortcuts(theme.selected, style);
540
+ writeIndependentBrandTokens(theme, style);
541
+ }
542
+
543
+ /** Brand-scope semantic path — reads raw input only, not useTheme-filled palette. */
544
+ export function writeBrandScopeSemantic(
545
+ theme: PartialEmotionCanvasTheme,
546
+ style: React.CSSProperties
547
+ ) {
548
+ const palette = theme.canvas?.palette;
549
+
550
+ if (palette?.primary?.main) {
551
+ applyPrimaryBrandBundle(palette.primary.main, style);
552
+ }
553
+ if (palette?.error?.main) {
554
+ applyCriticalBrandBundle(palette.error.main, style);
555
+ }
556
+ if (palette?.alert?.main) {
557
+ applyCautionBrandBundle(palette.alert.main, style);
558
+ }
559
+ if (palette?.success?.main) {
560
+ applyPositiveBrandBundle(palette.success.main, style);
561
+ }
562
+ if (palette?.neutral?.main) {
563
+ applyNeutralBrandBundle(palette.neutral.main, style);
564
+ }
565
+ writeIndependentBrandTokens(theme, style);
566
+ }
@@ -14,6 +14,7 @@ export {default as styled, type StyleRewriteFn, filterOutProps} from './styled';
14
14
  * For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).
15
15
  */
16
16
  export * from './theme';
17
+ export * from './sanaTheme';
17
18
  /**
18
19
  * @deprecated ⚠️ `useTheme` and `getTheme` are deprecated. Use CSS variables from `@workday/canvas-tokens-web` instead.
19
20
  * For more information, view our [Theming Docs](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs#-preferred-approach-v14).