@datalayer/primer-addons 1.0.10 → 1.0.12

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 (49) hide show
  1. package/lib/components/appearance/AppearanceControls.d.ts +10 -0
  2. package/lib/components/appearance/AppearanceControls.js +42 -0
  3. package/lib/components/appearance/AppearanceControlsWithStore.d.ts +8 -0
  4. package/lib/components/appearance/AppearanceControlsWithStore.js +21 -0
  5. package/lib/components/appearance/index.d.ts +2 -0
  6. package/lib/components/appearance/index.js +6 -0
  7. package/lib/components/index.d.ts +4 -0
  8. package/lib/components/index.js +4 -0
  9. package/lib/components/logo/DatalayerLogo.d.ts +18 -0
  10. package/lib/components/logo/DatalayerLogo.js +63 -0
  11. package/lib/components/logo/DatalayerLogoText.d.ts +22 -0
  12. package/lib/components/logo/DatalayerLogoText.js +29 -0
  13. package/lib/components/logo/DatalayerText.d.ts +16 -0
  14. package/lib/components/logo/DatalayerText.js +16 -0
  15. package/lib/components/logo/index.d.ts +3 -0
  16. package/lib/components/logo/index.js +3 -0
  17. package/lib/index.js +0 -1
  18. package/lib/theme/DatalayerThemeProvider.js +83 -6
  19. package/lib/theme/Palette.d.ts +8 -4
  20. package/lib/theme/Palette.js +13 -6
  21. package/lib/theme/ThemedProvider.d.ts +25 -0
  22. package/lib/theme/ThemedProvider.js +14 -0
  23. package/lib/theme/colors/datalayerColors.d.ts +14 -0
  24. package/lib/theme/colors/datalayerColors.js +17 -0
  25. package/lib/theme/colors/index.d.ts +1 -0
  26. package/lib/theme/colors/index.js +1 -0
  27. package/lib/theme/colors/indicatorColors.d.ts +50 -0
  28. package/lib/theme/colors/indicatorColors.js +68 -0
  29. package/lib/theme/colors/lovelyColors.d.ts +14 -0
  30. package/lib/theme/colors/lovelyColors.js +17 -0
  31. package/lib/theme/colors/matrixColors.d.ts +14 -0
  32. package/lib/theme/colors/matrixColors.js +17 -0
  33. package/lib/theme/colors/spatialColors.d.ts +14 -0
  34. package/lib/theme/colors/spatialColors.js +17 -0
  35. package/lib/theme/index.d.ts +2 -0
  36. package/lib/theme/index.js +2 -0
  37. package/lib/theme/themeRegistry.d.ts +75 -0
  38. package/lib/theme/themeRegistry.js +235 -0
  39. package/lib/theme/themes/createThemeCSSVars.d.ts +14 -1
  40. package/lib/theme/themes/createThemeCSSVars.js +70 -9
  41. package/lib/theme/themes/matrixTheme.d.ts +1 -4
  42. package/lib/theme/themes/matrixTheme.js +3 -7
  43. package/lib/theme/useThemeStore.d.ts +50 -0
  44. package/lib/theme/useThemeStore.js +65 -0
  45. package/lib/utils/Portals.d.ts +19 -0
  46. package/lib/utils/Portals.js +111 -9
  47. package/package.json +3 -2
  48. /package/lib/{_utils/story-helpers.d.ts → story-helpers.d.ts} +0 -0
  49. /package/lib/{_utils/story-helpers.js → story-helpers.js} +0 -0
@@ -26,6 +26,10 @@
26
26
  * | Outline button | `--button-outline-fgColor-rest` |
27
27
  * | Danger button | `--button-danger-bgColor-hover` |
28
28
  * | Button counters | `--buttonCounter-primary-bgColor-rest` |
29
+ * | Control track | `--controlTrack-bgColor-rest` |
30
+ * | Control checked | `--control-checked-bgColor-rest` |
31
+ * | Control knob | `--controlKnob-bgColor-rest` |
32
+ * | List hover | `--control-transparent-bgColor-hover` |
29
33
  * | Legacy aliases | `--color-btn-primary-bg` |
30
34
  * | Brand (custom) | `--brand-color-canvas-default` |
31
35
  */
@@ -34,12 +38,6 @@ export function colorDefsToCSS(defs) {
34
38
  if (defs.canvas.subtle) {
35
39
  optional['--bgColor-muted'] = defs.canvas.subtle;
36
40
  }
37
- if (defs.accent.subtle) {
38
- optional['--bgColor-accent-subtle'] = defs.accent.subtle;
39
- }
40
- if (defs.success.subtle) {
41
- optional['--bgColor-success-subtle'] = defs.success.subtle;
42
- }
43
41
  return {
44
42
  /* ── Canvas / Background ─────────────────────────────────────── */
45
43
  '--bgColor-default': defs.canvas.default,
@@ -54,13 +52,13 @@ export function colorDefsToCSS(defs) {
54
52
  /* ── Accent ──────────────────────────────────────────────────── */
55
53
  '--fgColor-accent': defs.accent.fg,
56
54
  '--bgColor-accent-emphasis': defs.accent.emphasis,
57
- '--bgColor-accent-muted': defs.accent.muted,
55
+ '--bgColor-accent-muted': defs.accent.subtle ?? defs.accent.muted,
58
56
  '--borderColor-accent-emphasis': defs.accent.emphasis,
59
57
  '--borderColor-accent-muted': defs.accent.muted,
60
58
  /* ── Success ─────────────────────────────────────────────────── */
61
59
  '--fgColor-success': defs.success.fg,
62
60
  '--bgColor-success-emphasis': defs.success.emphasis,
63
- '--bgColor-success-muted': defs.success.muted,
61
+ '--bgColor-success-muted': defs.success.subtle ?? defs.success.muted,
64
62
  '--borderColor-success-emphasis': defs.success.emphasis,
65
63
  '--borderColor-success-muted': defs.success.muted,
66
64
  /* ── Border (generic) ────────────────────────────────────────── */
@@ -141,6 +139,35 @@ export function colorDefsToCSS(defs) {
141
139
  '--button-invisible-iconColor-rest': defs.fg.muted,
142
140
  '--button-invisible-iconColor-hover': defs.fg.default,
143
141
  '--button-invisible-iconColor-disabled': defs.fg.muted,
142
+ /* ── Control track (unchecked toggle / switch) ────────────── */
143
+ '--controlTrack-bgColor-rest': defs.border?.muted ?? defs.btn.border,
144
+ '--controlTrack-bgColor-hover': defs.border?.default ?? defs.btn.border,
145
+ '--controlTrack-bgColor-active': defs.border?.default ?? defs.btn.hoverBorder,
146
+ '--controlTrack-bgColor-disabled': defs.fg.muted,
147
+ '--controlTrack-fgColor-rest': defs.fg.muted,
148
+ '--controlTrack-fgColor-disabled': defs.fg.onEmphasis,
149
+ '--controlTrack-borderColor-rest': 'transparent',
150
+ /* ── Control checked (checked toggle / switch track) ──────── */
151
+ '--control-checked-bgColor-rest': defs.accent.emphasis,
152
+ '--control-checked-bgColor-hover': defs.btn.primary.hoverBg,
153
+ '--control-checked-bgColor-active': defs.btn.primary.selectedBg,
154
+ '--control-checked-fgColor-rest': defs.btn.primary.text,
155
+ '--control-checked-fgColor-disabled': defs.btn.primary.text,
156
+ '--control-checked-borderColor-rest': 'transparent',
157
+ /* ── Control knob (toggle switch handle) ──────────────────── */
158
+ '--controlKnob-bgColor-rest': defs.canvas.default,
159
+ '--controlKnob-bgColor-checked': defs.canvas.default,
160
+ '--controlKnob-bgColor-disabled': defs.canvas.subtle ?? defs.canvas.default,
161
+ '--controlKnob-borderColor-rest': defs.border?.default ?? defs.btn.border,
162
+ '--controlKnob-borderColor-checked': defs.accent.emphasis,
163
+ /* ── Control transparent (action list / nav list hover) ───── */
164
+ '--control-transparent-bgColor-hover': defs.btn.hoverBg,
165
+ '--control-transparent-bgColor-active': defs.btn.activeBg,
166
+ '--control-transparent-bgColor-selected': defs.btn.selectedBg,
167
+ '--control-transparent-borderColor-hover': 'transparent',
168
+ '--control-transparent-borderColor-active': 'transparent',
169
+ /* ── Menu ────────────────────────────────────────────────────── */
170
+ '--menu-bgColor-active': defs.btn.hoverBg,
144
171
  /* ── Legacy / compat aliases ─────────────────────────────────── */
145
172
  '--color-btn-primary-bg': defs.btn.primary.bg,
146
173
  '--color-btn-primary-hover-bg': defs.btn.primary.hoverBg,
@@ -153,20 +180,54 @@ export function colorDefsToCSS(defs) {
153
180
  * Build a complete `ThemeStyles` object from light / dark
154
181
  * `ThemeColorDefs`. The result is ready to pass straight to
155
182
  * `<DatalayerThemeProvider themeStyles={…}>`.
183
+ *
184
+ * @param options.fontFamily When supplied, the Primer font-stack
185
+ * CSS custom properties (`--fontStack-sansSerif`,
186
+ * `--fontStack-sansSerifDisplay`) are overridden so that every
187
+ * Primer component that uses the CSS `font` shorthand (e.g.
188
+ * `Blankslate`) picks up the themed font instead of the
189
+ * hard-coded system-font fallback.
156
190
  */
157
- export function buildThemeStyles(light, dark) {
191
+ export function buildThemeStyles(light, dark, options) {
192
+ const fontVars = {};
193
+ if (options?.fontFamily) {
194
+ const f = options.fontFamily;
195
+ // Override the font-stack custom properties so that any consumer
196
+ // referencing `var(--fontStack-sansSerif)` picks up the theme font.
197
+ fontVars['--fontStack-sansSerif'] = f;
198
+ fontVars['--fontStack-sansSerifDisplay'] = f;
199
+ // Also override every font-shorthand token directly. Primer
200
+ // components like Blankslate use
201
+ // `font: var(--text-title-shorthand-medium, <hardcoded>)`
202
+ // and relying on nested `var()` resolution inside the CSS `font`
203
+ // shorthand is unreliable across browsers. Inlining the font
204
+ // family here guarantees the themed typeface is applied.
205
+ fontVars['--text-body-shorthand-large'] = `400 1rem/1.5 ${f}`;
206
+ fontVars['--text-body-shorthand-medium'] = `400 0.875rem/1.4285 ${f}`;
207
+ fontVars['--text-body-shorthand-small'] = `400 0.75rem/1.6666 ${f}`;
208
+ fontVars['--text-title-shorthand-large'] = `600 2rem/1.5 ${f}`;
209
+ fontVars['--text-title-shorthand-medium'] = `600 1.25rem/1.6 ${f}`;
210
+ fontVars['--text-title-shorthand-small'] = `600 1rem/1.5 ${f}`;
211
+ fontVars['--text-caption-shorthand'] = `400 0.75rem/1.3333 ${f}`;
212
+ fontVars['--text-subtitle-shorthand'] = `400 1.25rem/1.6 ${f}`;
213
+ fontVars['--text-display-shorthand'] = `500 2.5rem/1.4 ${f}`;
214
+ }
158
215
  return {
159
216
  light: {
160
217
  backgroundColor: light.canvas.default,
161
218
  color: light.fg.default,
162
219
  fontSize: 'var(--text-body-size-medium)',
220
+ ...(options?.fontFamily ? { fontFamily: options.fontFamily } : {}),
163
221
  ...colorDefsToCSS(light),
222
+ ...fontVars,
164
223
  },
165
224
  dark: {
166
225
  backgroundColor: dark.canvas.default,
167
226
  color: dark.fg.default,
168
227
  fontSize: 'var(--text-body-size-medium)',
228
+ ...(options?.fontFamily ? { fontFamily: options.fontFamily } : {}),
169
229
  ...colorDefsToCSS(dark),
230
+ ...fontVars,
170
231
  },
171
232
  };
172
233
  }
@@ -1089,8 +1089,5 @@ export declare const matrixTheme: {
1089
1089
  }>;
1090
1090
  };
1091
1091
  /** Comprehensive Primer CSS-variable overrides for light & dark mode. */
1092
- export declare const matrixThemeStyles: {
1093
- light: import("react").CSSProperties;
1094
- dark: import("react").CSSProperties;
1095
- };
1092
+ export declare const matrixThemeStyles: import("./createThemeCSSVars").ThemeStyles;
1096
1093
  export default matrixTheme;
@@ -27,11 +27,13 @@ const matrixLight = {
27
27
  fg: matrixColors.greenText,
28
28
  emphasis: matrixColors.greenBrand,
29
29
  muted: matrixColors.greenAccent,
30
+ subtle: matrixColors.greenTint,
30
31
  },
31
32
  success: {
32
33
  fg: matrixColors.greenText,
33
34
  emphasis: matrixColors.greenBrand,
34
35
  muted: matrixColors.greenAccent,
36
+ subtle: matrixColors.greenTint,
35
37
  },
36
38
  btn: {
37
39
  text: '#0A2E1A',
@@ -192,11 +194,5 @@ export const matrixTheme = primerTheme;
192
194
  */
193
195
  const matrixFontFamily = '"SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono", "Courier New", monospace';
194
196
  /** Comprehensive Primer CSS-variable overrides for light & dark mode. */
195
- export const matrixThemeStyles = (() => {
196
- const base = buildThemeStyles(matrixLight, matrixDark);
197
- return {
198
- light: { ...base.light, fontFamily: matrixFontFamily },
199
- dark: { ...base.dark, fontFamily: matrixFontFamily },
200
- };
201
- })();
197
+ export const matrixThemeStyles = buildThemeStyles(matrixLight, matrixDark, { fontFamily: matrixFontFamily });
202
198
  export default matrixTheme;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Zustand store for theme preferences – reusable across Datalayer apps.
3
+ *
4
+ * Persists `colorMode` and `theme` to localStorage. The store key is
5
+ * configurable so that different apps can maintain independent prefs.
6
+ *
7
+ * @module theme/useThemeStore
8
+ */
9
+ import { type StoreApi, type UseBoundStore } from 'zustand';
10
+ import type { ThemeVariant } from './themeRegistry';
11
+ import type { ColorMode } from './DatalayerBrandThemeProvider';
12
+ export type { ThemeVariant, ColorMode };
13
+ export interface ThemeState {
14
+ /** Current color mode (light, dark, or auto = follow OS). */
15
+ colorMode: ColorMode;
16
+ /** Current theme variant. */
17
+ theme: ThemeVariant;
18
+ /** Cycle through light → dark → auto. */
19
+ toggleColorMode: () => void;
20
+ /** Set a specific color mode. */
21
+ setColorMode: (mode: ColorMode) => void;
22
+ /**
23
+ * Set the active theme variant.
24
+ * @param applyDefaultColorMode When true (default), also switches the
25
+ * color mode to the theme's configured default.
26
+ */
27
+ setTheme: (theme: ThemeVariant, applyDefaultColorMode?: boolean) => void;
28
+ }
29
+ /**
30
+ * Create a theme store bound to a specific localStorage key.
31
+ *
32
+ * Usage:
33
+ * ```ts
34
+ * export const useMyAppThemeStore = createThemeStore('my-app-theme');
35
+ * ```
36
+ *
37
+ * @param storageKey localStorage key for persistence (e.g. `'otel-example-theme'`)
38
+ * @param defaults Optional overrides for the initial colorMode / theme
39
+ */
40
+ export declare function createThemeStore(storageKey: string, defaults?: {
41
+ colorMode?: ColorMode;
42
+ theme?: ThemeVariant;
43
+ }): UseBoundStore<StoreApi<ThemeState>>;
44
+ /**
45
+ * Default theme store for Datalayer applications.
46
+ *
47
+ * Persists to `localStorage` under the key `'datalayer-theme'`.
48
+ * Use `createThemeStore` if you need an app-specific key or defaults.
49
+ */
50
+ export declare const useThemeStore: UseBoundStore<StoreApi<ThemeState>>;
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Zustand store for theme preferences – reusable across Datalayer apps.
7
+ *
8
+ * Persists `colorMode` and `theme` to localStorage. The store key is
9
+ * configurable so that different apps can maintain independent prefs.
10
+ *
11
+ * @module theme/useThemeStore
12
+ */
13
+ import { create } from 'zustand';
14
+ import { persist, createJSONStorage } from 'zustand/middleware';
15
+ import { themeConfigs } from './themeRegistry';
16
+ /**
17
+ * Create a theme store bound to a specific localStorage key.
18
+ *
19
+ * Usage:
20
+ * ```ts
21
+ * export const useMyAppThemeStore = createThemeStore('my-app-theme');
22
+ * ```
23
+ *
24
+ * @param storageKey localStorage key for persistence (e.g. `'otel-example-theme'`)
25
+ * @param defaults Optional overrides for the initial colorMode / theme
26
+ */
27
+ export function createThemeStore(storageKey, defaults) {
28
+ return create()(persist(set => ({
29
+ colorMode: defaults?.colorMode ?? 'dark',
30
+ theme: defaults?.theme ?? 'matrix',
31
+ toggleColorMode: () => set(state => {
32
+ const cycle = {
33
+ light: 'dark',
34
+ dark: 'auto',
35
+ auto: 'light',
36
+ };
37
+ return { colorMode: cycle[state.colorMode] };
38
+ }),
39
+ setColorMode: (mode) => set({ colorMode: mode }),
40
+ setTheme: (theme, applyDefaultColorMode = true) => set(() => {
41
+ const next = { theme };
42
+ if (applyDefaultColorMode) {
43
+ next.colorMode = themeConfigs[theme].defaultColorMode;
44
+ }
45
+ return next;
46
+ }),
47
+ }), {
48
+ name: storageKey,
49
+ storage: createJSONStorage(() => localStorage),
50
+ partialize: state => ({
51
+ colorMode: state.colorMode,
52
+ theme: state.theme,
53
+ }),
54
+ }));
55
+ }
56
+ /**
57
+ * Default theme store for Datalayer applications.
58
+ *
59
+ * Persists to `localStorage` under the key `'datalayer-theme'`.
60
+ * Use `createThemeStore` if you need an app-specific key or defaults.
61
+ */
62
+ export const useThemeStore = createThemeStore('datalayer-theme', {
63
+ colorMode: 'dark',
64
+ theme: 'matrix',
65
+ });
@@ -1,10 +1,29 @@
1
+ import { type CSSProperties } from 'react';
1
2
  import '@primer/react-brand/lib/css/main.css';
2
3
  type Colormode = 'light' | 'dark' | 'auto';
3
4
  /**
4
5
  * Ensure we define a root for Primer portal root.
5
6
  *
7
+ * Creates a dedicated `<div>` appended to `<body>` with a high z-index
8
+ * so that portaled content (overlays, tooltips, action menus) renders
9
+ * above other positioned UI elements such as chat panels.
10
+ *
6
11
  * @see https://github.com/primer/react/blob/main/packages/react/src/Portal/Portal.tsx#L23
7
12
  * @see https://github.com/primer/react/blob/030fe020b48b7f12c2994c6614e5d4191fe764ee/src/Portal/Portal.tsx#L33
8
13
  */
9
14
  export declare const setupPrimerPortals: (colormode?: Colormode) => void;
15
+ /**
16
+ * Sync a set of CSS properties (including CSS custom properties) to
17
+ * `document.body` so that Primer portal content — which is a DOM child
18
+ * of `<body>`, not of the React-tree `<BaseStyles>` element — inherits
19
+ * the same theme tokens and cascade properties (font, color, background)
20
+ * as the rest of the application.
21
+ *
22
+ * This function is **idempotent**: calling it again replaces the
23
+ * properties from the previous call and removes any stale ones.
24
+ *
25
+ * Typical usage: call from `DatalayerThemeProvider`'s `useEffect` so
26
+ * portals stay in sync whenever theme or color-mode changes.
27
+ */
28
+ export declare function syncPortalThemeStyles(styles: CSSProperties): void;
10
29
  export default setupPrimerPortals;
@@ -5,6 +5,11 @@
5
5
  import { registerPortalRoot } from '@primer/react';
6
6
  import '@primer/react-brand/lib/css/main.css';
7
7
  const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__';
8
+ /**
9
+ * Key used on `document.body` to track the CSS-property names we
10
+ * previously applied so we can remove stale entries on theme switch.
11
+ */
12
+ const PORTAL_THEME_KEYS = '__primerPortalThemeKeys__';
8
13
  /**
9
14
  * Resolve 'auto' colormode to the actual OS preference.
10
15
  */
@@ -14,20 +19,117 @@ const resolveColormode = (colormode) => {
14
19
  }
15
20
  return colormode;
16
21
  };
22
+ /**
23
+ * Infer current app color mode from existing DOM theme markers.
24
+ *
25
+ * This avoids accidentally forcing light mode when callers invoke
26
+ * setupPrimerPortals() without passing an explicit mode.
27
+ */
28
+ const inferCurrentColormode = () => {
29
+ const portalRoot = document.getElementById(PRIMER_PORTAL_ROOT_ID);
30
+ const portalMode = portalRoot?.getAttribute('data-color-mode');
31
+ if (portalMode === 'light' || portalMode === 'dark' || portalMode === 'auto') {
32
+ return portalMode;
33
+ }
34
+ const htmlMode = document.documentElement.getAttribute('data-color-mode');
35
+ if (htmlMode === 'light' || htmlMode === 'dark' || htmlMode === 'auto') {
36
+ return htmlMode;
37
+ }
38
+ const bodyMode = document.body.getAttribute('data-color-mode');
39
+ if (bodyMode === 'light' || bodyMode === 'dark' || bodyMode === 'auto') {
40
+ return bodyMode;
41
+ }
42
+ return 'auto';
43
+ };
44
+ /**
45
+ * Infer Primer light/dark theme names from existing DOM markers.
46
+ */
47
+ const inferCurrentThemeNames = () => {
48
+ const portalRoot = document.getElementById(PRIMER_PORTAL_ROOT_ID);
49
+ const lightTheme = portalRoot?.getAttribute('data-light-theme') ||
50
+ document.documentElement.getAttribute('data-light-theme') ||
51
+ document.body.getAttribute('data-light-theme') ||
52
+ 'light';
53
+ const darkTheme = portalRoot?.getAttribute('data-dark-theme') ||
54
+ document.documentElement.getAttribute('data-dark-theme') ||
55
+ document.body.getAttribute('data-dark-theme') ||
56
+ 'dark';
57
+ return { lightTheme, darkTheme };
58
+ };
17
59
  /**
18
60
  * Ensure we define a root for Primer portal root.
19
61
  *
62
+ * Creates a dedicated `<div>` appended to `<body>` with a high z-index
63
+ * so that portaled content (overlays, tooltips, action menus) renders
64
+ * above other positioned UI elements such as chat panels.
65
+ *
20
66
  * @see https://github.com/primer/react/blob/main/packages/react/src/Portal/Portal.tsx#L23
21
67
  * @see https://github.com/primer/react/blob/030fe020b48b7f12c2994c6614e5d4191fe764ee/src/Portal/Portal.tsx#L33
22
68
  */
23
- export const setupPrimerPortals = (colormode = 'light') => {
24
- const resolved = resolveColormode(colormode);
25
- const body = document.body;
26
- body.dataset['portalRoot'] = 'true';
27
- body.dataset['colorMode'] = resolved;
28
- body.dataset['lightTheme'] = 'light';
29
- body.dataset['darkTheme'] = 'dark';
30
- body.id = PRIMER_PORTAL_ROOT_ID;
31
- registerPortalRoot(body);
69
+ export const setupPrimerPortals = (colormode) => {
70
+ const effectiveColormode = colormode ?? inferCurrentColormode();
71
+ const resolved = resolveColormode(effectiveColormode);
72
+ const { lightTheme, darkTheme } = inferCurrentThemeNames();
73
+ // Create or reuse a dedicated portal root div.
74
+ let portalRoot = document.getElementById(PRIMER_PORTAL_ROOT_ID);
75
+ if (!portalRoot) {
76
+ portalRoot = document.createElement('div');
77
+ portalRoot.id = PRIMER_PORTAL_ROOT_ID;
78
+ document.body.appendChild(portalRoot);
79
+ }
80
+ // Primer theme attributes — required for proper theming inside portals.
81
+ portalRoot.dataset['portalRoot'] = 'true';
82
+ portalRoot.dataset['colorMode'] = resolved;
83
+ portalRoot.dataset['lightTheme'] = lightTheme;
84
+ portalRoot.dataset['darkTheme'] = darkTheme;
85
+ // High z-index so overlays render above positioned UI (e.g. chat z-index 1001).
86
+ portalRoot.style.position = 'relative';
87
+ portalRoot.style.zIndex = '9999';
88
+ registerPortalRoot(portalRoot);
32
89
  };
90
+ /* ─── camelCase → kebab-case ─────────────────────────────────────────── */
91
+ const camelToKebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
92
+ /**
93
+ * Sync a set of CSS properties (including CSS custom properties) to
94
+ * `document.body` so that Primer portal content — which is a DOM child
95
+ * of `<body>`, not of the React-tree `<BaseStyles>` element — inherits
96
+ * the same theme tokens and cascade properties (font, color, background)
97
+ * as the rest of the application.
98
+ *
99
+ * This function is **idempotent**: calling it again replaces the
100
+ * properties from the previous call and removes any stale ones.
101
+ *
102
+ * Typical usage: call from `DatalayerThemeProvider`'s `useEffect` so
103
+ * portals stay in sync whenever theme or color-mode changes.
104
+ */
105
+ export function syncPortalThemeStyles(styles) {
106
+ const body = document.body;
107
+ // 1. Remove properties set by the previous invocation.
108
+ const prev = body[PORTAL_THEME_KEYS];
109
+ if (prev) {
110
+ for (const key of prev) {
111
+ body.style.removeProperty(key);
112
+ }
113
+ }
114
+ // 2. Apply the new properties.
115
+ const tracked = [];
116
+ for (const [key, value] of Object.entries(styles)) {
117
+ if (value == null)
118
+ continue;
119
+ const strVal = String(value);
120
+ if (key.startsWith('--')) {
121
+ // CSS custom property — must use setProperty
122
+ body.style.setProperty(key, strVal);
123
+ tracked.push(key);
124
+ }
125
+ else {
126
+ // Standard CSS property (camelCase → kebab-case)
127
+ const kebab = camelToKebab(key);
128
+ body.style.setProperty(kebab, strVal);
129
+ tracked.push(kebab);
130
+ }
131
+ }
132
+ // 3. Stash the list for the next cleanup.
133
+ body[PORTAL_THEME_KEYS] = tracked;
134
+ }
33
135
  export default setupPrimerPortals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/primer-addons",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "license": "BSD-3-Clause",
5
5
  "scripts": {
6
6
  "build": "tsc && vite build",
@@ -33,7 +33,8 @@
33
33
  "react": "^18.3.1 || ^19.0.0",
34
34
  "react-dom": "^18.3.1 || ^19.0.0",
35
35
  "react-loading-skeleton": "^3.5.0",
36
- "styled-components": "^5.3.10"
36
+ "styled-components": "^5.3.10",
37
+ "zustand": "^5.0.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@storybook/addon-docs": "^9.0.15",