@memberjunction/react-runtime 5.50.0 → 6.1.0-edge.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.
@@ -3,7 +3,7 @@
3
3
  * @module @memberjunction/react-runtime/utilities
4
4
  */
5
5
 
6
- import { ComponentStyles } from '@memberjunction/interactive-component-types';
6
+ import { ComponentStyles, StyleOverrides } from '@memberjunction/interactive-component-types';
7
7
 
8
8
  /**
9
9
  * Creates the default component styles for Skip components
@@ -27,17 +27,26 @@ export function SetupStyles(): ComponentStyles {
27
27
  // Status colors
28
28
  success: '#10B981',
29
29
  successLight: '#D1FAE5',
30
+ successText: '#047857',
31
+ successBorder: '#b7eb8f',
30
32
  warning: '#F59E0B',
31
33
  warningLight: '#FEF3C7',
34
+ warningText: '#8c6c00',
35
+ warningBorder: '#ffe58f',
32
36
  error: '#EF4444',
33
37
  errorLight: '#FEE2E2',
38
+ errorText: '#B91C1C',
39
+ errorBorder: '#FECACA',
34
40
  info: '#3B82F6',
35
41
  infoLight: '#DBEAFE',
36
-
42
+ infoText: '#1D4ED8',
43
+ infoBorder: '#91d5ff',
44
+
37
45
  // Base colors
38
46
  background: '#FFFFFF',
39
47
  surface: '#F8FAFC',
40
48
  surfaceHover: '#F1F5F9',
49
+ overlay: 'rgba(0, 0, 0, 0.5)', // Modal scrim
41
50
 
42
51
  // Text colors with better contrast
43
52
  text: '#1E293B',
@@ -125,6 +134,18 @@ export function SetupStyles(): ComponentStyles {
125
134
  '#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0',
126
135
  '#00BCD4', '#F44336', '#8BC34A', '#FF5722', '#3F51B5',
127
136
  ],
137
+ // Default sequential (single-hue) intensity ramp, light-to-dark, for heatmaps
138
+ // and density shading. Overridden per-theme from `--mj-viz-seq-*`.
139
+ sequentialScale: [
140
+ '#E3F2FD', '#BBDEFB', '#90CAF9', '#42A5F5', '#2196F3', '#1976D2', '#0D47A1',
141
+ ],
142
+ // Default diverging ramp for measures with a meaningful midpoint.
143
+ // Overridden per-theme from `--mj-viz-div-*`.
144
+ divergingScale: {
145
+ low: '#F44336',
146
+ mid: '#EEEEEE',
147
+ high: '#4CAF50',
148
+ },
128
149
  }
129
150
  }
130
151
 
@@ -143,19 +164,31 @@ const THEME_COLOR_TOKEN_MAP: Record<string, string> = {
143
164
  primaryHover: '--mj-brand-primary-hover',
144
165
  primaryActive: '--mj-brand-primary-active',
145
166
  primaryLight: '--mj-brand-primary-light',
167
+ // Secondary
168
+ secondary: '--mj-brand-secondary',
169
+ secondaryHover: '--mj-brand-secondary-hover',
146
170
  // Status
147
171
  success: '--mj-status-success',
148
172
  successLight: '--mj-status-success-bg',
173
+ successText: '--mj-status-success-text',
174
+ successBorder: '--mj-status-success-border',
149
175
  warning: '--mj-status-warning',
150
176
  warningLight: '--mj-status-warning-bg',
177
+ warningText: '--mj-status-warning-text',
178
+ warningBorder: '--mj-status-warning-border',
151
179
  error: '--mj-status-error',
152
180
  errorLight: '--mj-status-error-bg',
181
+ errorText: '--mj-status-error-text',
182
+ errorBorder: '--mj-status-error-border',
153
183
  info: '--mj-status-info',
154
184
  infoLight: '--mj-status-info-bg',
185
+ infoText: '--mj-status-info-text',
186
+ infoBorder: '--mj-status-info-border',
155
187
  // Surfaces (MJ: page = tinted, surface = elevated/white in light mode)
156
188
  background: '--mj-bg-page',
157
189
  surface: '--mj-bg-surface',
158
190
  surfaceHover: '--mj-bg-surface-hover',
191
+ overlay: '--mj-bg-overlay',
159
192
  // Text
160
193
  text: '--mj-text-primary',
161
194
  textSecondary: '--mj-text-secondary',
@@ -173,6 +206,9 @@ const THEME_COLOR_TOKEN_MAP: Record<string, string> = {
173
206
  /** Number of `--mj-viz-N` categorical tokens the bridge probes for `chartPalette`. */
174
207
  const VIZ_TOKEN_COUNT = 10;
175
208
 
209
+ /** Number of `--mj-viz-seq-N` tokens the bridge probes for `sequentialScale`. */
210
+ const VIZ_SEQ_TOKEN_COUNT = 7;
211
+
176
212
  /**
177
213
  * Reads the live MJ theme (`--mj-*` custom properties on the document root) and
178
214
  * layers it over `SetupStyles()`, producing a `ComponentStyles` that follows the
@@ -216,5 +252,97 @@ export function BuildStylesFromTheme(root?: Element): ComponentStyles {
216
252
  base.chartPalette = palette;
217
253
  }
218
254
 
255
+ const sequential: string[] = [];
256
+ for (let i = 1; i <= VIZ_SEQ_TOKEN_COUNT; i++) {
257
+ const value = read(`--mj-viz-seq-${i}`);
258
+ if (value) {
259
+ sequential.push(value);
260
+ }
261
+ }
262
+ // A ramp needs at least two stops to interpolate; a single resolved token is
263
+ // treated as an incomplete theme and left on the default.
264
+ if (sequential.length > 1) {
265
+ base.sequentialScale = sequential;
266
+ }
267
+
268
+ // Endpoints are required for a diverging scale to mean anything; `mid` is
269
+ // optional, so only the low/high pair gates the swap.
270
+ const divLow = read('--mj-viz-div-low');
271
+ const divHigh = read('--mj-viz-div-high');
272
+ if (divLow && divHigh) {
273
+ const divMid = read('--mj-viz-div-mid');
274
+ base.divergingScale = divMid ? { low: divLow, mid: divMid, high: divHigh } : { low: divLow, high: divHigh };
275
+ }
276
+
219
277
  return base;
278
+ }
279
+
280
+ /** Multipliers applied to the whole `fontSize` ladder per `StyleOverrides.fontScale`. */
281
+ const FONT_SCALE_FACTOR: Record<string, number> = { small: 0.875, large: 1.25 };
282
+
283
+ /** Smallest px size the scale may produce, so `small` cannot render text illegible. */
284
+ const MIN_FONT_SIZE_PX = 10;
285
+
286
+ /**
287
+ * Rescales every px value in a `fontSize` token map, leaving anything not expressed
288
+ * in whole px (rem, em, clamp(), a keyword) untouched rather than guessing at it.
289
+ */
290
+ function scaleFontSizes(fontSize: Record<string, string | undefined>, factor: number): Record<string, string | undefined> {
291
+ const scaled: Record<string, string | undefined> = {};
292
+ for (const [key, value] of Object.entries(fontSize)) {
293
+ const px = typeof value === 'string' ? /^\s*(\d+(?:\.\d+)?)px\s*$/.exec(value) : null;
294
+ scaled[key] = px
295
+ ? `${Math.max(MIN_FONT_SIZE_PX, Math.round(parseFloat(px[1]) * factor))}px`
296
+ : value;
297
+ }
298
+ return scaled;
299
+ }
300
+
301
+ /**
302
+ * Layers explicitly user-requested styling (`ComponentSpec.styleOverrides`) over
303
+ * theme-resolved styles, producing the `styles` a component actually receives.
304
+ *
305
+ * This is what lets "make the charts blue" be honored without a color literal ever
306
+ * entering generated code: the request is carried as spec data and resolved here,
307
+ * above the org theme, so the component keeps reading `styles.chartPalette` and
308
+ * friends. Of the color slots only visualization ones are merged — the override
309
+ * contract deliberately has no background/text/border slots, since those cannot be
310
+ * flipped for dark mode without derived per-mode variants.
311
+ *
312
+ * `fontScale` is the one non-color slot, and it rescales the `typography.fontSize`
313
+ * ladder in place. Doing it here rather than in the generator is what makes a type
314
+ * scale hold: every token moves at once, so text inside registry components and
315
+ * third-party libraries scales along with the generated markup.
316
+ *
317
+ * Returns `base` unchanged when there are no overrides, and never mutates `base`.
318
+ *
319
+ * @param base theme-resolved styles (from `BuildStylesFromTheme()` or `SetupStyles()`)
320
+ * @param overrides the spec's `styleOverrides`, if any
321
+ */
322
+ export function ApplyStyleOverrides<T extends Partial<ComponentStyles>>(base: T, overrides?: StyleOverrides): T {
323
+ if (!overrides) {
324
+ return base;
325
+ }
326
+
327
+ const hasChartPalette = Array.isArray(overrides.chartPalette) && overrides.chartPalette.length > 0;
328
+ // A ramp needs at least two stops to interpolate between.
329
+ const hasSequential = Array.isArray(overrides.sequentialScale) && overrides.sequentialScale.length > 1;
330
+ const hasDiverging = !!overrides.divergingScale?.low && !!overrides.divergingScale?.high;
331
+ // 'normal' — and any unrecognized value — leaves the ladder alone.
332
+ const fontFactor = overrides.fontScale ? FONT_SCALE_FACTOR[overrides.fontScale] : undefined;
333
+ const hasFontScale = !!fontFactor && !!base.typography?.fontSize;
334
+
335
+ if (!hasChartPalette && !hasSequential && !hasDiverging && !hasFontScale) {
336
+ return base;
337
+ }
338
+
339
+ return {
340
+ ...base,
341
+ ...(hasChartPalette ? { chartPalette: overrides.chartPalette } : {}),
342
+ ...(hasSequential ? { sequentialScale: overrides.sequentialScale } : {}),
343
+ ...(hasDiverging ? { divergingScale: overrides.divergingScale } : {}),
344
+ ...(hasFontScale
345
+ ? { typography: { ...base.typography!, fontSize: scaleFontSizes(base.typography!.fontSize, fontFactor!) } }
346
+ : {}),
347
+ };
220
348
  }
@@ -133,16 +133,39 @@ export class LibraryLoader {
133
133
  * Load libraries based on the current configuration
134
134
  */
135
135
  static async loadLibrariesFromConfig(options?: ConfigLoadOptions, debug?: boolean): Promise<LibraryLoadResult> {
136
- // Always load core runtime libraries first
136
+ // Load core runtime libraries in dependency order.
137
+ // ReactDOM's UMD factory captures `window.React` at execution time,
138
+ // so React MUST execute before ReactDOM. Loading them in parallel with
139
+ // async scripts causes an intermittent race condition where ReactDOM
140
+ // executes first and gets `undefined` for React, permanently breaking
141
+ // `createRoot` on that object instance.
137
142
  const coreLibraries = getCoreRuntimeLibraries(debug);
138
- const corePromises = coreLibraries.map(lib =>
139
- this.loadScript(lib.cdnUrl, lib.globalVariable, debug, lib.fallbackCdnUrls)
140
- );
141
-
142
- const coreResults = await Promise.all(corePromises);
143
- const React = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'React');
144
- const ReactDOM = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'ReactDOM');
145
- const Babel = coreResults.find((_, i) => coreLibraries[i].globalVariable === 'Babel');
143
+ const reactLib = coreLibraries.find(lib => lib.globalVariable === 'React');
144
+ const reactDOMLib = coreLibraries.find(lib => lib.globalVariable === 'ReactDOM');
145
+ const babelLib = coreLibraries.find(lib => lib.globalVariable === 'Babel');
146
+
147
+ // Phase 1: React must load first (ReactDOM depends on it)
148
+ let React: unknown;
149
+ if (reactLib) {
150
+ React = await this.loadScript(reactLib.cdnUrl, reactLib.globalVariable, debug, reactLib.fallbackCdnUrls);
151
+ }
152
+
153
+ // Phase 2: ReactDOM and Babel can load in parallel (both only depend on React)
154
+ const phase2Promises: Promise<unknown>[] = [];
155
+ const phase2Labels: string[] = [];
156
+
157
+ if (reactDOMLib) {
158
+ phase2Promises.push(this.loadScript(reactDOMLib.cdnUrl, reactDOMLib.globalVariable, debug, reactDOMLib.fallbackCdnUrls));
159
+ phase2Labels.push('ReactDOM');
160
+ }
161
+ if (babelLib) {
162
+ phase2Promises.push(this.loadScript(babelLib.cdnUrl, babelLib.globalVariable, debug, babelLib.fallbackCdnUrls));
163
+ phase2Labels.push('Babel');
164
+ }
165
+
166
+ const phase2Results = await Promise.all(phase2Promises);
167
+ const ReactDOM = phase2Labels.indexOf('ReactDOM') >= 0 ? phase2Results[phase2Labels.indexOf('ReactDOM')] : undefined;
168
+ const Babel = phase2Labels.indexOf('Babel') >= 0 ? phase2Results[phase2Labels.indexOf('Babel')] : undefined;
146
169
 
147
170
  // Expose React and ReactDOM as globals for UMD libraries that expect them
148
171
  // Many React component libraries (Recharts, Victory, etc.) expect these as globals