@workday/canvas-kit-react 16.0.0-alpha.0499-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 (51) hide show
  1. package/common/lib/CanvasProvider.tsx +156 -324
  2. package/common/lib/theming/README.md +49 -6
  3. package/common/lib/theming/brandScope.ts +566 -0
  4. package/common/lib/theming/index.ts +1 -0
  5. package/common/lib/theming/sanaTheme.ts +133 -0
  6. package/common/lib/theming/types.ts +292 -0
  7. package/dist/commonjs/common/lib/CanvasProvider.d.ts +296 -5
  8. package/dist/commonjs/common/lib/CanvasProvider.d.ts.map +1 -1
  9. package/dist/commonjs/common/lib/CanvasProvider.js +105 -287
  10. package/dist/commonjs/common/lib/theming/brandScope.d.ts +64 -0
  11. package/dist/commonjs/common/lib/theming/brandScope.d.ts.map +1 -0
  12. package/dist/commonjs/common/lib/theming/brandScope.js +499 -0
  13. package/dist/commonjs/common/lib/theming/index.d.ts +1 -0
  14. package/dist/commonjs/common/lib/theming/index.d.ts.map +1 -1
  15. package/dist/commonjs/common/lib/theming/index.js +1 -0
  16. package/dist/commonjs/common/lib/theming/sanaTheme.d.ts +33 -0
  17. package/dist/commonjs/common/lib/theming/sanaTheme.d.ts.map +1 -0
  18. package/dist/commonjs/common/lib/theming/sanaTheme.js +130 -0
  19. package/dist/commonjs/common/lib/theming/types.d.ts +162 -0
  20. package/dist/commonjs/common/lib/theming/types.d.ts.map +1 -1
  21. package/dist/commonjs/common/lib/theming/types.js +81 -1
  22. package/dist/commonjs/pill/lib/Pill.d.ts +2 -2
  23. package/dist/commonjs/popup/lib/hooks/usePopupStack.d.ts.map +1 -1
  24. package/dist/commonjs/popup/lib/hooks/usePopupStack.js +18 -16
  25. package/dist/commonjs/testing/lib/StaticStates.d.ts +2 -2
  26. package/dist/commonjs/testing/lib/StaticStates.d.ts.map +1 -1
  27. package/dist/commonjs/testing/lib/StaticStates.js +5 -2
  28. package/dist/es6/common/lib/CanvasProvider.d.ts +296 -5
  29. package/dist/es6/common/lib/CanvasProvider.d.ts.map +1 -1
  30. package/dist/es6/common/lib/CanvasProvider.js +82 -288
  31. package/dist/es6/common/lib/theming/brandScope.d.ts +64 -0
  32. package/dist/es6/common/lib/theming/brandScope.d.ts.map +1 -0
  33. package/dist/es6/common/lib/theming/brandScope.js +483 -0
  34. package/dist/es6/common/lib/theming/index.d.ts +1 -0
  35. package/dist/es6/common/lib/theming/index.d.ts.map +1 -1
  36. package/dist/es6/common/lib/theming/index.js +1 -0
  37. package/dist/es6/common/lib/theming/sanaTheme.d.ts +33 -0
  38. package/dist/es6/common/lib/theming/sanaTheme.d.ts.map +1 -0
  39. package/dist/es6/common/lib/theming/sanaTheme.js +127 -0
  40. package/dist/es6/common/lib/theming/types.d.ts +162 -0
  41. package/dist/es6/common/lib/theming/types.d.ts.map +1 -1
  42. package/dist/es6/common/lib/theming/types.js +78 -0
  43. package/dist/es6/pill/lib/Pill.d.ts +2 -2
  44. package/dist/es6/popup/lib/hooks/usePopupStack.d.ts.map +1 -1
  45. package/dist/es6/popup/lib/hooks/usePopupStack.js +13 -11
  46. package/dist/es6/testing/lib/StaticStates.d.ts +2 -2
  47. package/dist/es6/testing/lib/StaticStates.d.ts.map +1 -1
  48. package/dist/es6/testing/lib/StaticStates.js +6 -3
  49. package/package.json +4 -4
  50. package/popup/lib/hooks/usePopupStack.ts +13 -11
  51. package/testing/lib/StaticStates.tsx +11 -9
@@ -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).