@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.
- package/lib/components/appearance/AppearanceControls.d.ts +10 -0
- package/lib/components/appearance/AppearanceControls.js +42 -0
- package/lib/components/appearance/AppearanceControlsWithStore.d.ts +8 -0
- package/lib/components/appearance/AppearanceControlsWithStore.js +21 -0
- package/lib/components/appearance/index.d.ts +2 -0
- package/lib/components/appearance/index.js +6 -0
- package/lib/components/index.d.ts +4 -0
- package/lib/components/index.js +4 -0
- package/lib/components/logo/DatalayerLogo.d.ts +18 -0
- package/lib/components/logo/DatalayerLogo.js +63 -0
- package/lib/components/logo/DatalayerLogoText.d.ts +22 -0
- package/lib/components/logo/DatalayerLogoText.js +29 -0
- package/lib/components/logo/DatalayerText.d.ts +16 -0
- package/lib/components/logo/DatalayerText.js +16 -0
- package/lib/components/logo/index.d.ts +3 -0
- package/lib/components/logo/index.js +3 -0
- package/lib/index.js +0 -1
- package/lib/theme/DatalayerThemeProvider.js +83 -6
- package/lib/theme/Palette.d.ts +8 -4
- package/lib/theme/Palette.js +13 -6
- package/lib/theme/ThemedProvider.d.ts +25 -0
- package/lib/theme/ThemedProvider.js +14 -0
- package/lib/theme/colors/datalayerColors.d.ts +14 -0
- package/lib/theme/colors/datalayerColors.js +17 -0
- package/lib/theme/colors/index.d.ts +1 -0
- package/lib/theme/colors/index.js +1 -0
- package/lib/theme/colors/indicatorColors.d.ts +50 -0
- package/lib/theme/colors/indicatorColors.js +68 -0
- package/lib/theme/colors/lovelyColors.d.ts +14 -0
- package/lib/theme/colors/lovelyColors.js +17 -0
- package/lib/theme/colors/matrixColors.d.ts +14 -0
- package/lib/theme/colors/matrixColors.js +17 -0
- package/lib/theme/colors/spatialColors.d.ts +14 -0
- package/lib/theme/colors/spatialColors.js +17 -0
- package/lib/theme/index.d.ts +2 -0
- package/lib/theme/index.js +2 -0
- package/lib/theme/themeRegistry.d.ts +75 -0
- package/lib/theme/themeRegistry.js +235 -0
- package/lib/theme/themes/createThemeCSSVars.d.ts +14 -1
- package/lib/theme/themes/createThemeCSSVars.js +70 -9
- package/lib/theme/themes/matrixTheme.d.ts +1 -4
- package/lib/theme/themes/matrixTheme.js +3 -7
- package/lib/theme/useThemeStore.d.ts +50 -0
- package/lib/theme/useThemeStore.js +65 -0
- package/lib/utils/Portals.d.ts +19 -0
- package/lib/utils/Portals.js +111 -9
- package/package.json +3 -2
- /package/lib/{_utils/story-helpers.d.ts → story-helpers.d.ts} +0 -0
- /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
|
+
});
|
package/lib/utils/Portals.d.ts
CHANGED
|
@@ -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;
|
package/lib/utils/Portals.js
CHANGED
|
@@ -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
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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",
|
|
File without changes
|
|
File without changes
|