@datalayer/primer-addons 1.0.9 → 1.0.11

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.
@@ -0,0 +1,288 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { datalayerTheme, datalayerThemeStyles } from './themes/datalayerTheme';
6
+ import { spatialTheme, spatialThemeStyles } from './themes/spatialTheme';
7
+ import { lovelyTheme, lovelyThemeStyles } from './themes/lovelyTheme';
8
+ import { matrixTheme, matrixThemeStyles } from './themes/matrixTheme';
9
+ import { datalayerColors } from './colors/datalayerColors';
10
+ import { spatialColors } from './colors/spatialColors';
11
+ import { lovelyColors } from './colors/lovelyColors';
12
+ import { matrixColors } from './colors/matrixColors';
13
+ /* ─── Registry ────────────────────────────────────────────────────────── */
14
+ export const themeConfigs = {
15
+ datalayer: {
16
+ label: 'Datalayer',
17
+ description: 'Default green palette — the classic Datalayer look.',
18
+ brandColor: datalayerColors.greenBrand,
19
+ defaultColorMode: 'auto',
20
+ primerTheme: datalayerTheme,
21
+ themeStyles: datalayerThemeStyles,
22
+ cardGradient: {
23
+ light: { from: datalayerColors.greenBrand, to: datalayerColors.greenText }, // #16A085 → #117A65
24
+ dark: { from: datalayerColors.greenAccent, to: datalayerColors.greenBrand }, // #1ABC9C → #16A085
25
+ },
26
+ brightPalette: {
27
+ glow: datalayerColors.brightGlow, // #00D68F — vivid emerald
28
+ onGlow: '#1b1f24', // dark text on bright green
29
+ pop: datalayerColors.brightPop, // #00E5FF — electric cyan
30
+ spark: datalayerColors.brightSpark, // #76FF03 — lime neon
31
+ blaze: datalayerColors.brightBlaze, // #FF1744 — vivid red
32
+ surge: datalayerColors.brightSurge, // #2979FF — electric blue
33
+ flame: datalayerColors.brightFlame, // #FF6D00 — vivid orange
34
+ gold: datalayerColors.brightGold, // #FFD600 — vivid yellow
35
+ },
36
+ brightPaletteLight: {
37
+ glow: datalayerColors.brightLightGlow, // #00BFA5 — vivid aqua-green
38
+ onGlow: '#1b1f24', // dark text on aqua-green
39
+ pop: datalayerColors.brightLightPop, // #00B0FF — vivid sky blue
40
+ spark: datalayerColors.brightLightSpark, // #64DD17 — vivid chartreuse
41
+ blaze: datalayerColors.brightLightBlaze, // #FF1744 — vivid red
42
+ surge: datalayerColors.brightLightSurge, // #2979FF — electric blue
43
+ flame: datalayerColors.brightLightFlame, // #FF6D00 — vivid orange
44
+ gold: datalayerColors.brightLightGold, // #FFAB00 — vivid amber
45
+ },
46
+ },
47
+ spatial: {
48
+ label: 'Spatial',
49
+ description: 'Cosmic indigo & deep blues — inspired by space.',
50
+ brandColor: spatialColors.indigoBrand,
51
+ defaultColorMode: 'auto',
52
+ primerTheme: spatialTheme,
53
+ themeStyles: spatialThemeStyles,
54
+ cardGradient: {
55
+ light: { from: spatialColors.indigoBrand, to: spatialColors.indigoText }, // #4F46E5 → #3730A3
56
+ dark: { from: spatialColors.indigoAccent, to: spatialColors.indigoBrand }, // #6366F1 → #4F46E5
57
+ },
58
+ brightPalette: {
59
+ glow: spatialColors.brightGlow, // #7C4DFF — electric violet
60
+ onGlow: '#ffffff', // white text on violet
61
+ pop: spatialColors.brightPop, // #448AFF — neon blue
62
+ spark: spatialColors.brightSpark, // #E040FB — vivid magenta
63
+ blaze: spatialColors.brightBlaze, // #FF5252 — neon red
64
+ surge: spatialColors.brightSurge, // #00B0FF — deep sky blue
65
+ flame: spatialColors.brightFlame, // #FF9100 — amber orange
66
+ gold: spatialColors.brightGold, // #FFEA00 — electric yellow
67
+ },
68
+ brightPaletteLight: {
69
+ glow: spatialColors.brightLightGlow, // #7C4DFF — electric violet
70
+ onGlow: '#ffffff', // white text on violet
71
+ pop: spatialColors.brightLightPop, // #448AFF — neon blue
72
+ spark: spatialColors.brightLightSpark, // #D500F9 — vivid purple
73
+ blaze: spatialColors.brightLightBlaze, // #FF5252 — neon red
74
+ surge: spatialColors.brightLightSurge, // #00B0FF — vivid sky blue
75
+ flame: spatialColors.brightLightFlame, // #FF9100 — vivid amber orange
76
+ gold: spatialColors.brightLightGold, // #FFC400 — vivid gold
77
+ },
78
+ },
79
+ lovely: {
80
+ label: 'Lovely',
81
+ description: 'Warm rose & magenta — soft and inviting.',
82
+ brandColor: lovelyColors.roseBrand,
83
+ defaultColorMode: 'auto',
84
+ primerTheme: lovelyTheme,
85
+ themeStyles: lovelyThemeStyles,
86
+ cardGradient: {
87
+ light: { from: lovelyColors.roseBrand, to: lovelyColors.roseText }, // #DB2777 → #9D174D
88
+ dark: { from: lovelyColors.roseAccent, to: lovelyColors.roseBrand }, // #EC4899 → #DB2777
89
+ },
90
+ brightPalette: {
91
+ glow: lovelyColors.brightGlow, // #FF4081 — hot pink
92
+ onGlow: '#ffffff', // white text on hot pink
93
+ pop: lovelyColors.brightPop, // #FF6E40 — vivid coral
94
+ spark: lovelyColors.brightSpark, // #EA80FC — electric fuchsia
95
+ blaze: lovelyColors.brightBlaze, // #FF1744 — vivid crimson
96
+ surge: lovelyColors.brightSurge, // #536DFE — indigo blue
97
+ flame: lovelyColors.brightFlame, // #FF6E40 — coral orange
98
+ gold: lovelyColors.brightGold, // #FFD740 — amber gold
99
+ },
100
+ brightPaletteLight: {
101
+ glow: lovelyColors.brightLightGlow, // #FF4081 — hot pink
102
+ onGlow: '#ffffff', // white text on hot pink
103
+ pop: lovelyColors.brightLightPop, // #FF6E40 — vivid coral
104
+ spark: lovelyColors.brightLightSpark, // #EA80FC — electric fuchsia
105
+ blaze: lovelyColors.brightLightBlaze, // #FF1744 — vivid crimson
106
+ surge: lovelyColors.brightLightSurge, // #536DFE — vivid indigo
107
+ flame: lovelyColors.brightLightFlame, // #FF6E40 — vivid coral orange
108
+ gold: lovelyColors.brightLightGold, // #FFD740 — vivid amber gold
109
+ },
110
+ },
111
+ matrix: {
112
+ label: 'Matrix',
113
+ description: 'Phosphor green on black — CRT terminal aesthetic.',
114
+ brandColor: matrixColors.greenPhosphor,
115
+ defaultColorMode: 'dark',
116
+ primerTheme: matrixTheme,
117
+ themeStyles: matrixThemeStyles,
118
+ cardGradient: {
119
+ light: { from: matrixColors.greenBrand, to: matrixColors.greenText }, // #16A085 → #117A65
120
+ dark: { from: matrixColors.greenGlow, to: matrixColors.greenHover }, // #39FF14 → #0E6655
121
+ },
122
+ brightPalette: {
123
+ glow: matrixColors.brightGlow, // #39FF14 — neon green
124
+ onGlow: '#1b1f24', // dark text on neon green
125
+ pop: matrixColors.brightPop, // #00FF88 — phosphor cyan
126
+ spark: matrixColors.brightSpark, // #CCFF00 — acid lime
127
+ blaze: matrixColors.brightBlaze, // #FF0040 — matrix red pill
128
+ surge: matrixColors.brightSurge, // #00E5FF — electric blue
129
+ flame: matrixColors.brightFlame, // #FF6D00 — electric orange
130
+ gold: matrixColors.brightGold, // #FFEA00 — glitch yellow
131
+ },
132
+ brightPaletteLight: {
133
+ glow: matrixColors.brightLightGlow, // #00E676 — vivid green
134
+ onGlow: '#1b1f24', // dark text on vivid green
135
+ pop: matrixColors.brightLightPop, // #1DE9B6 — vivid teal-mint
136
+ spark: matrixColors.brightLightSpark, // #AEEA00 — vivid lime
137
+ blaze: matrixColors.brightLightBlaze, // #FF1744 — vivid red
138
+ surge: matrixColors.brightLightSurge, // #00B0FF — vivid sky blue
139
+ flame: matrixColors.brightLightFlame, // #FF6D00 — vivid orange
140
+ gold: matrixColors.brightLightGold, // #FFAB00 — vivid amber
141
+ },
142
+ },
143
+ };
144
+ /** All available theme variants in display order. */
145
+ export const themeVariants = ['datalayer', 'spatial', 'lovely', 'matrix'];
146
+ /** Look up a theme config by variant name. */
147
+ export function getThemeConfig(variant) {
148
+ return themeConfigs[variant];
149
+ }
150
+ /**
151
+ * Resolve the card gradient for a given theme variant and colour mode.
152
+ * Falls back to `datalayer` / `light` when values are missing.
153
+ */
154
+ export function getCardGradient(variant = 'datalayer', colorMode = 'light') {
155
+ const mode = colorMode === 'auto' ? 'light' : colorMode;
156
+ return themeConfigs[variant]?.cardGradient?.[mode] ?? themeConfigs.datalayer.cardGradient.light;
157
+ }
158
+ /**
159
+ * Get the bright (vivid / OpenAI-blog-style) palette for a given theme variant.
160
+ *
161
+ * When `colorMode` is `'light'` the vivid / saturated light-background palette
162
+ * is returned — these are punchy, high-saturation colours designed to keep SVG
163
+ * illustrations vibrant and energetic on near-white surfaces.
164
+ * When `colorMode` is `'auto'`, the OS preference is queried via
165
+ * `prefers-color-scheme` so palettes always match the effective background.
166
+ * Defaults to the dark-optimised neon palette for backward compatibility.
167
+ */
168
+ export function getBrightPalette(variant = 'datalayer', colorMode) {
169
+ const cfg = themeConfigs[variant] ?? themeConfigs.datalayer;
170
+ let resolvedMode;
171
+ if (colorMode === 'auto') {
172
+ resolvedMode =
173
+ typeof window !== 'undefined' &&
174
+ window.matchMedia('(prefers-color-scheme: dark)').matches
175
+ ? 'dark'
176
+ : 'light';
177
+ }
178
+ else if (colorMode === 'light') {
179
+ resolvedMode = 'light';
180
+ }
181
+ else {
182
+ resolvedMode = 'dark';
183
+ }
184
+ if (resolvedMode === 'light') {
185
+ return cfg.brightPaletteLight ?? cfg.brightPalette;
186
+ }
187
+ return cfg.brightPalette;
188
+ }
189
+ /**
190
+ * Avatar colour palettes per theme × colour mode.
191
+ *
192
+ * Each palette is a 5-colour array tuned for the `boring-avatars` library.
193
+ * The colours are picked from each theme's brand + bright palette so the
194
+ * avatar always feels native to the current theme.
195
+ *
196
+ * - **Light mode** uses the deeper / accessible colour variants so the
197
+ * avatar is legible on light backgrounds.
198
+ * - **Dark mode** uses the brighter / glowing variants so the avatar
199
+ * pops against dark surfaces.
200
+ */
201
+ const avatarColorPalettes = {
202
+ datalayer: {
203
+ light: [
204
+ datalayerColors.greenText, // #117A65 — deep green
205
+ datalayerColors.greenBrand, // #16A085 — brand green
206
+ datalayerColors.brightLightPop, // #00B0FF — sky blue
207
+ datalayerColors.brightLightGold, // #FFAB00 — amber
208
+ datalayerColors.gray, // #59595C — neutral
209
+ ],
210
+ dark: [
211
+ datalayerColors.greenAccent, // #1ABC9C — bright teal
212
+ datalayerColors.greenBright, // #2ECC71 — bright green
213
+ datalayerColors.brightPop, // #00E5FF — electric cyan
214
+ datalayerColors.brightGold, // #FFD600 — vivid yellow
215
+ datalayerColors.brightFlame, // #FF6D00 — vivid orange
216
+ ],
217
+ },
218
+ spatial: {
219
+ light: [
220
+ spatialColors.indigoText, // #3730A3 — deep indigo
221
+ spatialColors.indigoBrand, // #4F46E5 — brand indigo
222
+ spatialColors.brightLightSpark, // #D500F9 — vivid purple
223
+ spatialColors.brightLightGold, // #FFC400 — vivid gold
224
+ spatialColors.gray, // #8892B0 — nebula gray
225
+ ],
226
+ dark: [
227
+ spatialColors.indigoAccent, // #6366F1 — bright indigo
228
+ spatialColors.indigoBright, // #818CF8 — light indigo
229
+ spatialColors.brightPop, // #448AFF — neon blue
230
+ spatialColors.brightSpark, // #E040FB — vivid magenta
231
+ spatialColors.brightGold, // #FFEA00 — electric yellow
232
+ ],
233
+ },
234
+ lovely: {
235
+ light: [
236
+ lovelyColors.roseText, // #9D174D — deep rose
237
+ lovelyColors.roseBrand, // #DB2777 — brand pink
238
+ lovelyColors.brightLightSurge, // #536DFE — vivid indigo
239
+ lovelyColors.brightLightGold, // #FFD740 — amber gold
240
+ lovelyColors.gray, // #9D7A8F — mauve gray
241
+ ],
242
+ dark: [
243
+ lovelyColors.roseAccent, // #EC4899 — bright pink
244
+ lovelyColors.roseBright, // #F472B6 — light pink
245
+ lovelyColors.brightPop, // #FF6E40 — vivid coral
246
+ lovelyColors.brightSpark, // #EA80FC — electric fuchsia
247
+ lovelyColors.brightGold, // #FFD740 — amber gold
248
+ ],
249
+ },
250
+ matrix: {
251
+ light: [
252
+ matrixColors.greenText, // #117A65 — deep green
253
+ matrixColors.greenBrand, // #16A085 — brand green
254
+ matrixColors.brightLightPop, // #1DE9B6 — vivid teal
255
+ matrixColors.brightLightGold, // #FFAB00 — vivid amber
256
+ matrixColors.gray, // #4A7856 — dim green-gray
257
+ ],
258
+ dark: [
259
+ matrixColors.greenPhosphor, // #00FF41 — Matrix green
260
+ matrixColors.greenGlow, // #39FF14 — neon phosphor
261
+ matrixColors.brightPop, // #00FF88 — phosphor cyan
262
+ matrixColors.brightSpark, // #CCFF00 — acid lime
263
+ matrixColors.brightSurge, // #00E5FF — electric blue
264
+ ],
265
+ },
266
+ };
267
+ /**
268
+ * Get a 5-colour avatar palette for the given theme variant and colour mode.
269
+ *
270
+ * Designed for use with the `boring-avatars` library's `colors` prop.
271
+ * When `colorMode` is `'auto'`, the OS preference is queried via
272
+ * `prefers-color-scheme` so avatars always match the effective background.
273
+ * Falls back to `datalayer` / `light` when values are missing.
274
+ */
275
+ export function getAvatarColors(variant = 'datalayer', colorMode = 'light') {
276
+ let mode;
277
+ if (colorMode === 'auto') {
278
+ mode =
279
+ typeof window !== 'undefined' &&
280
+ window.matchMedia('(prefers-color-scheme: dark)').matches
281
+ ? 'dark'
282
+ : 'light';
283
+ }
284
+ else {
285
+ mode = colorMode;
286
+ }
287
+ return avatarColorPalettes[variant]?.[mode] ?? avatarColorPalettes.datalayer.light;
288
+ }
@@ -31,6 +31,10 @@ export interface ThemeColorDefs {
31
31
  muted: string;
32
32
  subtle?: string;
33
33
  };
34
+ border?: {
35
+ default: string;
36
+ muted: string;
37
+ };
34
38
  btn: {
35
39
  text: string;
36
40
  bg: string;
@@ -114,6 +118,10 @@ export interface ThemeColorDefs {
114
118
  * | Outline button | `--button-outline-fgColor-rest` |
115
119
  * | Danger button | `--button-danger-bgColor-hover` |
116
120
  * | Button counters | `--buttonCounter-primary-bgColor-rest` |
121
+ * | Control track | `--controlTrack-bgColor-rest` |
122
+ * | Control checked | `--control-checked-bgColor-rest` |
123
+ * | Control knob | `--controlKnob-bgColor-rest` |
124
+ * | List hover | `--control-transparent-bgColor-hover` |
117
125
  * | Legacy aliases | `--color-btn-primary-bg` |
118
126
  * | Brand (custom) | `--brand-color-canvas-default` |
119
127
  */
@@ -127,5 +135,14 @@ export interface ThemeStyles {
127
135
  * Build a complete `ThemeStyles` object from light / dark
128
136
  * `ThemeColorDefs`. The result is ready to pass straight to
129
137
  * `<DatalayerThemeProvider themeStyles={…}>`.
138
+ *
139
+ * @param options.fontFamily When supplied, the Primer font-stack
140
+ * CSS custom properties (`--fontStack-sansSerif`,
141
+ * `--fontStack-sansSerifDisplay`) are overridden so that every
142
+ * Primer component that uses the CSS `font` shorthand (e.g.
143
+ * `Blankslate`) picks up the themed font instead of the
144
+ * hard-coded system-font fallback.
130
145
  */
131
- export declare function buildThemeStyles(light: ThemeColorDefs, dark: ThemeColorDefs): ThemeStyles;
146
+ export declare function buildThemeStyles(light: ThemeColorDefs, dark: ThemeColorDefs, options?: {
147
+ fontFamily?: string;
148
+ }): ThemeStyles;
@@ -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,18 +52,18 @@ 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) ────────────────────────────────────────── */
67
- '--borderColor-default': defs.btn.border,
68
- '--borderColor-muted': defs.btn.border,
65
+ '--borderColor-default': defs.border?.default ?? defs.btn.border,
66
+ '--borderColor-muted': defs.border?.muted ?? defs.btn.border,
69
67
  /* ── Default button ──────────────────────────────────────────── */
70
68
  '--button-default-fgColor-rest': defs.btn.text,
71
69
  '--button-default-bgColor-rest': defs.btn.bg,
@@ -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
  }
@@ -34,6 +34,10 @@ const datalayerLight = {
34
34
  emphasis: datalayerColors.greenBrand,
35
35
  muted: datalayerColors.greenAccent,
36
36
  },
37
+ border: {
38
+ default: '#d1d9e0',
39
+ muted: '#e0e6eb',
40
+ },
37
41
  btn: {
38
42
  text: datalayerColors.black,
39
43
  bg: datalayerColors.white,
@@ -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',
@@ -185,6 +187,12 @@ const matrixDark = {
185
187
  * default Primer theme kept for backward compatibility.
186
188
  */
187
189
  export const matrixTheme = primerTheme;
190
+ /**
191
+ * Monospace font stack for the Matrix terminal aesthetic.
192
+ * Uses fonts pre-installed across macOS / Windows / Linux so no
193
+ * web-font loading is required.
194
+ */
195
+ const matrixFontFamily = '"SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono", "Courier New", monospace';
188
196
  /** Comprehensive Primer CSS-variable overrides for light & dark mode. */
189
- export const matrixThemeStyles = buildThemeStyles(matrixLight, matrixDark);
197
+ export const matrixThemeStyles = buildThemeStyles(matrixLight, matrixDark, { fontFamily: matrixFontFamily });
190
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,3 +1,4 @@
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
  /**
@@ -7,4 +8,18 @@ type Colormode = 'light' | 'dark' | 'auto';
7
8
  * @see https://github.com/primer/react/blob/030fe020b48b7f12c2994c6614e5d4191fe764ee/src/Portal/Portal.tsx#L33
8
9
  */
9
10
  export declare const setupPrimerPortals: (colormode?: Colormode) => void;
11
+ /**
12
+ * Sync a set of CSS properties (including CSS custom properties) to
13
+ * `document.body` so that Primer portal content — which is a DOM child
14
+ * of `<body>`, not of the React-tree `<BaseStyles>` element — inherits
15
+ * the same theme tokens and cascade properties (font, color, background)
16
+ * as the rest of the application.
17
+ *
18
+ * This function is **idempotent**: calling it again replaces the
19
+ * properties from the previous call and removes any stale ones.
20
+ *
21
+ * Typical usage: call from `DatalayerThemeProvider`'s `useEffect` so
22
+ * portals stay in sync whenever theme or color-mode changes.
23
+ */
24
+ export declare function syncPortalThemeStyles(styles: CSSProperties): void;
10
25
  export default setupPrimerPortals;