@genesislcap/rapid-design-system 14.484.2 → 14.486.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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Runtime controller for modal themes (named modes + a light/dark or N-mode switcher).
3
+ *
4
+ * A theme is a set of named modes. Switching a mode is *two halves*:
5
+ * (a) swap the provider's `theme-<name>` class — the compiled, injected sheet
6
+ * supplies the pinned surfaces + parts, and the cascade tears down the
7
+ * previous mode automatically;
8
+ * (b) `setValueFor` the mode's design_tokens (accent, luminance, neutral seed)
9
+ * so FAST regenerates the derived recipe outputs (e.g.
10
+ * `--accent-foreground-rest`). Reuses `configureRapidDesignSystem` for (b).
11
+ *
12
+ * Selection (which mode is active) is app state, persisted per-theme in
13
+ * localStorage — never written back into the theme file.
14
+ *
15
+ * See `docs/modal-theming-migration.md` (repo root) for how to adopt this in an existing app.
16
+ *
17
+ * @public
18
+ */
19
+ import type { DesignTokensConfig } from '@genesislcap/foundation-ui';
20
+ import { CompilableTheme, ThemeSection } from './theme-compiler';
21
+ /** Recursively-optional slice of a token tree. */
22
+ type DeepPartial<T> = T extends object ? {
23
+ [K in keyof T]?: DeepPartial<T[K]>;
24
+ } : T;
25
+ /**
26
+ * The design-token inputs a theme may carry: any recursive slice of the full
27
+ * {@link DesignTokensConfig} token tree. `configureDesignSystem` merges the slice
28
+ * over the built-in defaults at apply time, so partial trees are valid input.
29
+ * @public
30
+ */
31
+ export type ModalThemeDesignTokens = DeepPartial<DesignTokensConfig['design_tokens']>;
32
+ /** A modal theme: mode-invariant `shared` tokens plus one bag of inputs per named mode. */
33
+ export interface ModalTheme extends CompilableTheme {
34
+ id?: string;
35
+ defaultMode?: string;
36
+ colorStrategy?: 'adaptive' | 'explicit';
37
+ /**
38
+ * Whether the app should offer a mode toggle. Omit to infer from the mode count
39
+ * (a toggle only makes sense with >1 mode); set explicitly to force it — e.g. ship
40
+ * two modes but launch single-mode (`false`), keeping the second mode for later.
41
+ */
42
+ showModeToggle?: boolean;
43
+ shared?: ThemeSection & {
44
+ design_tokens?: ModalThemeDesignTokens;
45
+ };
46
+ modes: Record<string, ThemeSection & {
47
+ design_tokens?: ModalThemeDesignTokens;
48
+ }>;
49
+ }
50
+ /**
51
+ * Narrow a theme loaded from JSON (or any untyped value) to a {@link ModalTheme},
52
+ * checking the structure a theme cannot function without. Prefer this over casting
53
+ * the JSON module: a malformed theme fails loudly here instead of silently
54
+ * rendering unthemed.
55
+ * @public
56
+ */
57
+ export declare const toModalTheme: (json: unknown) => ModalTheme;
58
+ /**
59
+ * The mode to start on: remembered choice for this theme, else the declared default.
60
+ * @public
61
+ */
62
+ export declare const resolveInitialMode: (theme: ModalTheme) => string;
63
+ /**
64
+ * Cycle to the next mode (for the light/dark toggle; generalises to N modes).
65
+ * @public
66
+ */
67
+ export declare const nextMode: (theme: ModalTheme, current: string) => string;
68
+ /**
69
+ * Whether the app should offer a mode toggle. Defaults to "more than one mode exists";
70
+ * a `showModeToggle` on the theme overrides the inference. Use to set the header's
71
+ * toggle-button visibility.
72
+ * @public
73
+ */
74
+ export declare const isModeToggleEnabled: (theme: ModalTheme) => boolean;
75
+ /**
76
+ * Compile the theme to a single mode-agnostic sheet and adopt it into the scope
77
+ * that actually contains the provider. The sheet contains every mode; switching
78
+ * never regenerates it.
79
+ *
80
+ * The provider is typically rendered inside its host app's shadow root, not the
81
+ * document light DOM — so a document-scoped `rapid-design-system-provider { … }`
82
+ * rule would never match it. We adopt the sheet onto `provider.getRootNode()` (the
83
+ * containing ShadowRoot, or Document if the provider sits at document scope) so the
84
+ * selector resolves and the surface custom properties land on the provider,
85
+ * inheriting down through every nested shadow boundary.
86
+ *
87
+ * Re-entrant per root: the first call for a root adopts a new sheet; later calls
88
+ * for the same root update it in place (`replaceSync`) rather than adopting a
89
+ * duplicate — this is what lets a hot theme edit refresh styles without a reload.
90
+ * Providers sharing one root share its sheet (the compiled CSS targets the provider
91
+ * tag), so themes applied to the same root contend — give concurrently-themed
92
+ * providers their own shadow roots.
93
+ *
94
+ * @public
95
+ */
96
+ export declare const injectThemeStyles: (provider: HTMLElement, theme: ModalTheme) => void;
97
+ /**
98
+ * Recompile and re-apply the theme after a hot edit to the theme file: updates the
99
+ * adopted sheet in place and re-runs the active mode's design tokens on the most
100
+ * recently applied provider (falling back to the theme's initial mode if the edit
101
+ * removed the active one). Wire to a bundler's HMR hook so theme edits show without
102
+ * a full page reload.
103
+ *
104
+ * @public
105
+ */
106
+ export declare const reapplyTheme: (theme: ModalTheme) => void;
107
+ /**
108
+ * Apply a mode: design-token half via FAST, surface/parts half via the class swap.
109
+ * @public
110
+ */
111
+ export declare const applyMode: (provider: HTMLElement, theme: ModalTheme, mode: string) => void;
112
+ export {};
113
+ //# sourceMappingURL=modal-theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modal-theme.d.ts","sourceRoot":"","sources":["../../../src/modal-theme/modal-theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAgB,eAAe,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE/E,kDAAkD;AAClD,KAAK,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,CAAC,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,WAAW,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC,CAAC;AAEtF,2FAA2F;AAC3F,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACxC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,YAAY,GAAG;QAAE,aAAa,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;IACnE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG;QAAE,aAAa,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC,CAAC;CAClF;AAwBD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,OAAO,KAAG,UAgB5C,CAAC;AA6BF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,UAAU,KAAG,MAGtD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,UAAU,EAAE,SAAS,MAAM,KAAG,MAI7D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,UAAU,KAAG,OACH,CAAC;AActD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,WAAW,EAAE,OAAO,UAAU,KAAG,IAsB5E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,UAAU,KAAG,IAKhD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,UAAU,WAAW,EAAE,OAAO,UAAU,EAAE,MAAM,MAAM,KAAG,IA2ClF,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Compiles a modal theme file into class-scoped CSS (native nesting) for deployment.
3
+ *
4
+ * customTokens -> custom-property declarations
5
+ * componentOverrides -> nested rules keyed by any selector (::part(), a host tag, a class)
6
+ *
7
+ * Everything nests under one `rapid-design-system-provider` root. `shared` lives
8
+ * at the root (applies in every mode); each mode is a nested `&.theme-<name>`
9
+ * block whose compound selector out-specifies the root, so mode values win.
10
+ *
11
+ * design_tokens are NOT emitted: they are FAST recipe *inputs* consumed in JS
12
+ * (accent, luminance), applied at runtime via setValueFor (see modal-theme.ts).
13
+ *
14
+ * Input is validated, not escaped: keys, selectors, and values containing `{`/`}`
15
+ * throw (they would splice arbitrary rules into the sheet), and unknown section
16
+ * keys warn (they would otherwise be silently dropped).
17
+ *
18
+ * @public
19
+ */
20
+ /** A declaration block: CSS property/custom-property name -> value. */
21
+ export type ComponentOverrideDeclarations = Record<string, string>;
22
+ export interface ThemeSection {
23
+ customTokens?: Record<string, string>;
24
+ /** Rules keyed by any CSS selector (a host tag, a class, or `::part()`) -> declarations. */
25
+ componentOverrides?: Record<string, ComponentOverrideDeclarations>;
26
+ }
27
+ export interface CompilableTheme {
28
+ shared?: ThemeSection;
29
+ modes?: Record<string, ThemeSection>;
30
+ }
31
+ /**
32
+ * Compile a modal theme into class-scoped nested CSS.
33
+ *
34
+ * @param theme - the modal theme (shared + modes; customTokens + componentOverrides).
35
+ * @param host - the selector the rules nest under (default `rapid-design-system-provider`).
36
+ * @public
37
+ */
38
+ export declare function compileTheme(theme: CompilableTheme, { host }?: {
39
+ host?: string;
40
+ }): string;
41
+ //# sourceMappingURL=theme-compiler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-compiler.d.ts","sourceRoot":"","sources":["../../../src/modal-theme/theme-compiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,uEAAuE;AACvE,MAAM,MAAM,6BAA6B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnE,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACtC;AA8ED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,eAAe,EACtB,EAAE,IAAW,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GACtC,MAAM,CAoBR"}
@@ -1 +1 @@
1
- {"version":3,"file":"tab.styles.d.ts","sourceRoot":"","sources":["../../../src/tab/tab.styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,cAAc,GACzB,SAAS,wBAAwB,EACjC,YAAY,2BAA2B,KACtC,aAgBF,CAAC"}
1
+ {"version":3,"file":"tab.styles.d.ts","sourceRoot":"","sources":["../../../src/tab/tab.styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,cAAc,GACzB,SAAS,wBAAwB,EACjC,YAAY,2BAA2B,KACtC,aAiBF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tab-panel.styles.d.ts","sourceRoot":"","sources":["../../../src/tab-panel/tab-panel.styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,mBAAmB,GAC9B,SAAS,wBAAwB,EACjC,YAAY,2BAA2B,KACtC,aAUF,CAAC"}
1
+ {"version":3,"file":"tab-panel.styles.d.ts","sourceRoot":"","sources":["../../../src/tab-panel/tab-panel.styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,mBAAmB,GAC9B,SAAS,wBAAwB,EACjC,YAAY,2BAA2B,KACtC,aAWF,CAAC"}
@@ -48,22 +48,33 @@ const rapidSecondaryStyles = css `
48
48
  const rapidNeutralStyles = css `
49
49
  :host([appearance='neutral']) {
50
50
  background: var(--neutral-button-fill, var(--neutral-fill-rest));
51
- border: 0;
51
+ border: var(--neutral-button-border, 0);
52
52
  color: var(--neutral-button-foreground, var(--neutral-foreground-rest));
53
53
  }
54
54
 
55
55
  :host([appearance='neutral']:hover) {
56
- background: var(--neutral-fill-hover);
56
+ background: color-mix(
57
+ in srgb,
58
+ var(--neutral-button-fill, var(--neutral-fill-hover)),
59
+ transparent 20%
60
+ );
57
61
  }
58
62
 
59
63
  :host([appearance='neutral']:active) {
60
- background: var(--neutral-fill-active);
64
+ background: color-mix(
65
+ in srgb,
66
+ var(--neutral-button-fill, var(--neutral-fill-active)),
67
+ transparent 30%
68
+ );
61
69
  }
62
70
 
63
71
  :host([appearance='neutral'][disabled]),
64
72
  :host([appearance='neutral'][disabled]):hover,
65
73
  :host([appearance='neutral'][disabled]):active {
66
74
  opacity: 50%;
75
+ background: var(--neutral-button-fill, var(--neutral-fill-rest));
76
+ color: var(--neutral-button-foreground, var(--neutral-foreground-rest));
77
+ border: var(--neutral-button-border, 0);
67
78
  }
68
79
  `;
69
80
  const rapidDangerStyles = css `
@@ -7,6 +7,7 @@ export const rapidCardStyles = (context, definition) => css `
7
7
  background: var(--neutral-layer-card-container);
8
8
  color: ${neutralForegroundRest};
9
9
  border-radius: var(--card-corner-radius, calc(var(--control-corner-radius) * 1px));
10
+ box-shadow: var(--card-box-shadow, none);
10
11
  border: var(
11
12
  --card-border,
12
13
  1px solid var(--neutral-stroke-rest)
@@ -879,9 +879,7 @@ export const FLEXLAYOUT_REACT_THEME_CSS = `/* ==================================
879
879
  }
880
880
 
881
881
  .flexlayout__tab {
882
- border-radius: calc(var(--control-corner-radius) * 1.5px)
883
- calc(var(--control-corner-radius) * 1.5px)
884
- var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
882
+ border-radius: 0 0 var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
885
883
  var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
886
884
  border-top: var(--card-border, 1px solid var(--neutral-stroke-rest));
887
885
  background: var(--neutral-layer-card-container);
@@ -149,9 +149,7 @@
149
149
  }
150
150
 
151
151
  .flexlayout__tab {
152
- border-radius: calc(var(--control-corner-radius) * 1.5px)
153
- calc(var(--control-corner-radius) * 1.5px)
154
- var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
152
+ border-radius: 0 0 var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
155
153
  var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
156
154
  border-top: var(--card-border, 1px solid var(--neutral-stroke-rest));
157
155
  background: var(--neutral-layer-card-container);
package/dist/esm/index.js CHANGED
@@ -51,6 +51,7 @@ export * from './listbox-option';
51
51
  export * from './menu';
52
52
  export * from './menu-item';
53
53
  export * from './modal';
54
+ export * from './modal-theme';
54
55
  export * from './multiselect';
55
56
  export * from './number-field';
56
57
  export * from './progress';
@@ -0,0 +1,2 @@
1
+ export * from './theme-compiler';
2
+ export * from './modal-theme';
@@ -0,0 +1,198 @@
1
+ import { baseLayerLuminance, configureRapidDesignSystem } from '../_config';
2
+ import { compileTheme } from './theme-compiler';
3
+ const STORAGE_KEY = 'foundation-ui:theme-modes';
4
+ const THEME_CLASS_PREFIX = 'theme-';
5
+ /** Applied luminance at or above this reads as "light" (LightMode = 1, DarkMode = 0.23). */
6
+ const LIGHT_LUMINANCE_THRESHOLD = 0.5;
7
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ /** Deep-merge two design_tokens trees; `override` (the mode) wins at the leaves. */
9
+ const deepMerge = (base, override = {}) => {
10
+ const out = Object.assign({}, base);
11
+ for (const [key, value] of Object.entries(override)) {
12
+ const existing = out[key];
13
+ out[key] = isPlainObject(value) && isPlainObject(existing) ? deepMerge(existing, value) : value;
14
+ }
15
+ return out;
16
+ };
17
+ /**
18
+ * Narrow a theme loaded from JSON (or any untyped value) to a {@link ModalTheme},
19
+ * checking the structure a theme cannot function without. Prefer this over casting
20
+ * the JSON module: a malformed theme fails loudly here instead of silently
21
+ * rendering unthemed.
22
+ * @public
23
+ */
24
+ export const toModalTheme = (json) => {
25
+ if (!isPlainObject(json)) {
26
+ throw new Error('[modal-theme] A theme must be a plain JSON object.');
27
+ }
28
+ const modes = json.modes;
29
+ if (!isPlainObject(modes) || Object.keys(modes).length === 0) {
30
+ throw new Error('[modal-theme] A theme must declare at least one mode under "modes".');
31
+ }
32
+ const theme = json;
33
+ if (theme.defaultMode && !modes[theme.defaultMode]) {
34
+ console.warn(`[modal-theme] defaultMode "${theme.defaultMode}" is not a declared mode ` +
35
+ `(${Object.keys(modes).join(', ')}); the first declared mode will be used.`);
36
+ }
37
+ return theme;
38
+ };
39
+ // --- selection state (per-theme memory) -------------------------------------
40
+ const readModeMemory = () => {
41
+ var _a;
42
+ try {
43
+ const parsed = JSON.parse((_a = localStorage.getItem(STORAGE_KEY)) !== null && _a !== void 0 ? _a : '{}');
44
+ return isPlainObject(parsed) ? parsed : {};
45
+ }
46
+ catch (_b) {
47
+ return {};
48
+ }
49
+ };
50
+ const rememberMode = (themeId, mode) => {
51
+ const memory = readModeMemory();
52
+ memory[themeId] = mode;
53
+ try {
54
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(memory));
55
+ }
56
+ catch (_a) {
57
+ /* storage unavailable (private mode / quota) — selection is best-effort */
58
+ }
59
+ };
60
+ const themeKey = (theme) => { var _a; return (_a = theme.id) !== null && _a !== void 0 ? _a : 'default'; };
61
+ const modeNames = (theme) => { var _a; return Object.keys((_a = theme.modes) !== null && _a !== void 0 ? _a : {}); };
62
+ /** The declared default when it names a real mode, else the first declared mode. */
63
+ const fallbackMode = (theme) => { var _a; return theme.defaultMode && ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[theme.defaultMode]) ? theme.defaultMode : modeNames(theme)[0]; };
64
+ /**
65
+ * The mode to start on: remembered choice for this theme, else the declared default.
66
+ * @public
67
+ */
68
+ export const resolveInitialMode = (theme) => {
69
+ var _a;
70
+ const remembered = readModeMemory()[themeKey(theme)];
71
+ return remembered && ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[remembered]) ? remembered : fallbackMode(theme);
72
+ };
73
+ /**
74
+ * Cycle to the next mode (for the light/dark toggle; generalises to N modes).
75
+ * @public
76
+ */
77
+ export const nextMode = (theme, current) => {
78
+ const names = modeNames(theme);
79
+ const index = names.indexOf(current);
80
+ return names[(index + 1) % names.length];
81
+ };
82
+ /**
83
+ * Whether the app should offer a mode toggle. Defaults to "more than one mode exists";
84
+ * a `showModeToggle` on the theme overrides the inference. Use to set the header's
85
+ * toggle-button visibility.
86
+ * @public
87
+ */
88
+ export const isModeToggleEnabled = (theme) => { var _a; return (_a = theme.showModeToggle) !== null && _a !== void 0 ? _a : modeNames(theme).length > 1; };
89
+ // --- application ------------------------------------------------------------
90
+ // One adopted sheet per root (Document or ShadowRoot). Keying by the provider's root
91
+ // means a second provider in another root gets its own sheet (instead of silently
92
+ // rewriting the first one's), and a remounted provider that lands in an
93
+ // already-themed root updates the existing sheet in place. WeakMap keeps detached
94
+ // roots collectable, so tests and remounts need no explicit teardown.
95
+ const sheetByRoot = new WeakMap();
96
+ // The most recent applyMode target — what reapplyTheme (the HMR hook) re-applies.
97
+ let lastApplied = null;
98
+ /**
99
+ * Compile the theme to a single mode-agnostic sheet and adopt it into the scope
100
+ * that actually contains the provider. The sheet contains every mode; switching
101
+ * never regenerates it.
102
+ *
103
+ * The provider is typically rendered inside its host app's shadow root, not the
104
+ * document light DOM — so a document-scoped `rapid-design-system-provider { … }`
105
+ * rule would never match it. We adopt the sheet onto `provider.getRootNode()` (the
106
+ * containing ShadowRoot, or Document if the provider sits at document scope) so the
107
+ * selector resolves and the surface custom properties land on the provider,
108
+ * inheriting down through every nested shadow boundary.
109
+ *
110
+ * Re-entrant per root: the first call for a root adopts a new sheet; later calls
111
+ * for the same root update it in place (`replaceSync`) rather than adopting a
112
+ * duplicate — this is what lets a hot theme edit refresh styles without a reload.
113
+ * Providers sharing one root share its sheet (the compiled CSS targets the provider
114
+ * tag), so themes applied to the same root contend — give concurrently-themed
115
+ * providers their own shadow roots.
116
+ *
117
+ * @public
118
+ */
119
+ export const injectThemeStyles = (provider, theme) => {
120
+ const css = compileTheme(theme);
121
+ const root = provider.getRootNode();
122
+ // A detached provider's root is the provider itself (or a plain DocumentFragment),
123
+ // which cannot adopt stylesheets — surface an actionable diagnostic instead of
124
+ // crashing on the adoptedStyleSheets spread below.
125
+ if (!('adoptedStyleSheets' in root)) {
126
+ console.warn('[modal-theme] injectThemeStyles skipped: the provider is not connected to a Document ' +
127
+ 'or ShadowRoot. Connect the provider before applying the theme.');
128
+ return;
129
+ }
130
+ const existing = sheetByRoot.get(root);
131
+ if (existing) {
132
+ existing.replaceSync(css);
133
+ return;
134
+ }
135
+ const sheet = new CSSStyleSheet();
136
+ sheet.replaceSync(css);
137
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
138
+ sheetByRoot.set(root, sheet);
139
+ };
140
+ /**
141
+ * Recompile and re-apply the theme after a hot edit to the theme file: updates the
142
+ * adopted sheet in place and re-runs the active mode's design tokens on the most
143
+ * recently applied provider (falling back to the theme's initial mode if the edit
144
+ * removed the active one). Wire to a bundler's HMR hook so theme edits show without
145
+ * a full page reload.
146
+ *
147
+ * @public
148
+ */
149
+ export const reapplyTheme = (theme) => {
150
+ var _a;
151
+ if (!lastApplied)
152
+ return;
153
+ const { provider, mode } = lastApplied;
154
+ injectThemeStyles(provider, theme);
155
+ applyMode(provider, theme, ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[mode]) ? mode : resolveInitialMode(theme));
156
+ };
157
+ /**
158
+ * Apply a mode: design-token half via FAST, surface/parts half via the class swap.
159
+ * @public
160
+ */
161
+ export const applyMode = (provider, theme, mode) => {
162
+ var _a, _b, _c, _d, _e;
163
+ lastApplied = { provider, mode };
164
+ // (b) FAST recipe inputs — accent, luminance, neutral seed, typography, etc.
165
+ // The API/JSON key is snake_case (`design_tokens`); use a camelCase local to satisfy lint.
166
+ const designTokens = deepMerge(((_b = (_a = theme.shared) === null || _a === void 0 ? void 0 : _a.design_tokens) !== null && _b !== void 0 ? _b : {}), ((_e = (_d = (_c = theme.modes) === null || _c === void 0 ? void 0 : _c[mode]) === null || _d === void 0 ? void 0 : _d.design_tokens) !== null && _e !== void 0 ? _e : {}));
167
+ // configureDesignSystem merges the slice over the built-in defaults, so a partial
168
+ // tree is valid at runtime; the config type wants the full tree — widen here only.
169
+ configureRapidDesignSystem(provider, {
170
+ design_tokens: designTokens,
171
+ });
172
+ // (a) surfaces + parts are pure CSS — swap the mode class; the sheet does the rest.
173
+ for (const cls of Array.from(provider.classList)) {
174
+ if (cls.startsWith(THEME_CLASS_PREFIX))
175
+ provider.classList.remove(cls);
176
+ }
177
+ provider.classList.add(`${THEME_CLASS_PREFIX}${mode}`);
178
+ // Read the luminance FAST actually applied (the mode's tokens merged over the
179
+ // built-in defaults) — one source of truth, so the classes below can never
180
+ // disagree with the rendered surfaces, whatever the mode is named.
181
+ const isLight = baseLayerLuminance.getValueFor(provider) >= LIGHT_LUMINANCE_THRESHOLD;
182
+ // Back-compat: existing code/components may still key off the legacy `.light` class.
183
+ document.body.classList.toggle('light', isLight);
184
+ provider.classList.toggle('light', isLight);
185
+ // Keep the platform `luminance` key in sync with the actually-applied mode.
186
+ // foundation-header (the toggle icon) writes its own, often-inverse value
187
+ // synchronously right after emitting the toggle event, and grid-pro reads this
188
+ // key to theme grids — so we write in a microtask to land last and win.
189
+ queueMicrotask(() => {
190
+ try {
191
+ localStorage.setItem('luminance', isLight ? 'light' : 'dark');
192
+ }
193
+ catch (_a) {
194
+ /* storage unavailable — best effort */
195
+ }
196
+ });
197
+ rememberMode(themeKey(theme), mode);
198
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Compiles a modal theme file into class-scoped CSS (native nesting) for deployment.
3
+ *
4
+ * customTokens -> custom-property declarations
5
+ * componentOverrides -> nested rules keyed by any selector (::part(), a host tag, a class)
6
+ *
7
+ * Everything nests under one `rapid-design-system-provider` root. `shared` lives
8
+ * at the root (applies in every mode); each mode is a nested `&.theme-<name>`
9
+ * block whose compound selector out-specifies the root, so mode values win.
10
+ *
11
+ * design_tokens are NOT emitted: they are FAST recipe *inputs* consumed in JS
12
+ * (accent, luminance), applied at runtime via setValueFor (see modal-theme.ts).
13
+ *
14
+ * Input is validated, not escaped: keys, selectors, and values containing `{`/`}`
15
+ * throw (they would splice arbitrary rules into the sheet), and unknown section
16
+ * keys warn (they would otherwise be silently dropped).
17
+ *
18
+ * @public
19
+ */
20
+ const HOST = 'rapid-design-system-provider';
21
+ /** Section keys the theme runtime consumes; anything else is an authoring mistake. */
22
+ const KNOWN_SECTION_KEYS = new Set(['customTokens', 'componentOverrides', 'design_tokens']);
23
+ // A `{`/`}` inside a key, selector, or value terminates the current rule early and
24
+ // splices arbitrary rules into the adopted sheet — reject outright so an authoring
25
+ // typo fails loudly at compile time instead of silently corrupting the whole sheet.
26
+ // Semicolons are deliberately allowed: they occur in legitimate values (e.g.
27
+ // `url("data:image/svg+xml;base64,…")`) and can at most add a sibling declaration
28
+ // inside the same app-authored block — the theme author already controls whole
29
+ // property names, so `;` grants no capability braces don't already guard.
30
+ const assertBraceFree = (kind, text) => {
31
+ if (text.includes('{') || text.includes('}')) {
32
+ throw new Error(`[theme-compiler] Invalid ${kind} ${JSON.stringify(text)}: "{" and "}" are not allowed.`);
33
+ }
34
+ };
35
+ // Declaration values arrive from JSON, so numbers and booleans are representable even
36
+ // though the TS type says string — stringify those; reject shapes with no CSS form
37
+ // (objects/arrays/null would otherwise emit "[object Object]" silently).
38
+ const coerceDeclarationValue = (key, value) => {
39
+ if (typeof value === 'string')
40
+ return value;
41
+ if (typeof value === 'number' || typeof value === 'boolean')
42
+ return String(value);
43
+ throw new Error(`[theme-compiler] Invalid value for "${key}" ${JSON.stringify(value)}: expected a string, number, or boolean.`);
44
+ };
45
+ // The compiler silently ignores keys it doesn't read, which is exactly how a whole
46
+ // override block goes missing (e.g. the pre-rename `parts` key) — surface it.
47
+ const warnUnknownKeys = (sectionName, sectionObj) => {
48
+ for (const key of Object.keys(sectionObj)) {
49
+ if (!KNOWN_SECTION_KEYS.has(key)) {
50
+ console.warn(`[theme-compiler] Unknown key "${key}" in ${sectionName} is ignored — did you mean ` +
51
+ `"componentOverrides"? (supported: customTokens, componentOverrides, design_tokens)`);
52
+ }
53
+ }
54
+ };
55
+ const props = (map, pad) => Object.entries(map)
56
+ .map(([k, v]) => {
57
+ assertBraceFree('property name', k);
58
+ const value = coerceDeclarationValue(k, v);
59
+ assertBraceFree(`value of "${k}"`, value);
60
+ return `${pad}${k}: ${value};`;
61
+ })
62
+ .join('\n');
63
+ const overrideRules = (overrides, pad) => Object.entries(overrides)
64
+ .map(([sel, decls]) => {
65
+ assertBraceFree('selector', sel);
66
+ return `${pad}${sel} {\n${props(decls, pad + ' ')}\n${pad}}`;
67
+ })
68
+ .join('\n\n');
69
+ const section = (obj, pad) => {
70
+ const out = [];
71
+ if (obj.customTokens && Object.keys(obj.customTokens).length) {
72
+ out.push(props(obj.customTokens, pad));
73
+ }
74
+ if (obj.componentOverrides && Object.keys(obj.componentOverrides).length) {
75
+ out.push(overrideRules(obj.componentOverrides, pad));
76
+ }
77
+ return out.join('\n\n');
78
+ };
79
+ /**
80
+ * Compile a modal theme into class-scoped nested CSS.
81
+ *
82
+ * @param theme - the modal theme (shared + modes; customTokens + componentOverrides).
83
+ * @param host - the selector the rules nest under (default `rapid-design-system-provider`).
84
+ * @public
85
+ */
86
+ export function compileTheme(theme, { host = HOST } = {}) {
87
+ var _a, _b;
88
+ const body = [];
89
+ const sharedSection = (_a = theme.shared) !== null && _a !== void 0 ? _a : {};
90
+ warnUnknownKeys('shared', sharedSection);
91
+ const shared = section(sharedSection, ' ');
92
+ if (shared)
93
+ body.push(shared);
94
+ for (const [name, mode] of Object.entries((_b = theme.modes) !== null && _b !== void 0 ? _b : {})) {
95
+ assertBraceFree('mode name', name);
96
+ warnUnknownKeys(`mode "${name}"`, mode);
97
+ const modeContent = section(mode, ' ');
98
+ // Skip modes with no CSS to emit (e.g. adaptive modes that only carry
99
+ // design_tokens) so the sheet doesn't gain empty `&.theme-<name> {}` blocks.
100
+ if (modeContent) {
101
+ body.push(` &.theme-${name} {\n${modeContent}\n }`);
102
+ }
103
+ }
104
+ return `${host} {\n${body.join('\n\n')}\n}\n`;
105
+ }
@@ -15,5 +15,6 @@ export const rapidTabStyles = (context, definition) => css `
15
15
  :host([aria-selected='true']),
16
16
  :host([aria-selected='true']:hover) {
17
17
  background-color: var(--neutral-layer-4);
18
+ font-weight: 700;
18
19
  }
19
20
  `;
@@ -5,7 +5,8 @@ export const rapidTabPanelStyles = (context, definition) => css `
5
5
  :host {
6
6
  padding: 0;
7
7
  background-color: var(--neutral-layer-card-container);
8
- border-radius: var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
8
+ border-radius: 0 0 var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
9
+ var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
9
10
  border-style: solid;
10
11
  border-width: var(--tabs-border-width, 1px);
11
12
  border-color: var(--tabs-border-color, var(--neutral-stroke-rest));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/rapid-design-system",
3
3
  "description": "Rapid Design System",
4
- "version": "14.484.2",
4
+ "version": "14.486.0",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -25,7 +25,9 @@
25
25
  "dev:tsc": "genx dev -b ts",
26
26
  "flexlayout:generate": "node ./scripts/generate-flexlayout-theme.mjs",
27
27
  "lint": "genx lint -l ox",
28
- "lint:fix": "genx lint -l ox --fix"
28
+ "lint:fix": "genx lint -l ox --fix",
29
+ "test": "genx test",
30
+ "test:unit:watch": "genx test --watch"
29
31
  },
30
32
  "madge": {
31
33
  "detectiveOptions": {
@@ -36,12 +38,13 @@
36
38
  },
37
39
  "devDependencies": {
38
40
  "@genesiscommunitysuccess/analyzer-import-alias-plugin": "^5.0.3",
39
- "@genesislcap/genx": "14.484.2",
40
- "@genesislcap/rollup-builder": "14.484.2",
41
- "@genesislcap/ts-builder": "14.484.2",
42
- "@genesislcap/uvu-playwright-builder": "14.484.2",
43
- "@genesislcap/vite-builder": "14.484.2",
44
- "@genesislcap/webpack-builder": "14.484.2",
41
+ "@genesislcap/foundation-testing": "14.486.0",
42
+ "@genesislcap/genx": "14.486.0",
43
+ "@genesislcap/rollup-builder": "14.486.0",
44
+ "@genesislcap/ts-builder": "14.486.0",
45
+ "@genesislcap/uvu-playwright-builder": "14.486.0",
46
+ "@genesislcap/vite-builder": "14.486.0",
47
+ "@genesislcap/webpack-builder": "14.486.0",
45
48
  "flexlayout-react": "^0.8.19"
46
49
  },
47
50
  "peerDependencies": {
@@ -53,9 +56,9 @@
53
56
  }
54
57
  },
55
58
  "dependencies": {
56
- "@genesislcap/foundation-logger": "14.484.2",
57
- "@genesislcap/foundation-ui": "14.484.2",
58
- "@genesislcap/foundation-utils": "14.484.2",
59
+ "@genesislcap/foundation-logger": "14.486.0",
60
+ "@genesislcap/foundation-ui": "14.486.0",
61
+ "@genesislcap/foundation-utils": "14.486.0",
59
62
  "@microsoft/fast-colors": "5.3.1",
60
63
  "@microsoft/fast-components": "2.30.6",
61
64
  "@microsoft/fast-element": "1.14.0",
@@ -83,5 +86,5 @@
83
86
  "require": "./dist/react.cjs"
84
87
  }
85
88
  },
86
- "gitHead": "a7740b909c78ff2335e44ecd9d9d61d9ecb0f557"
89
+ "gitHead": "5de11f8989291fe8aaf19a7048dd6be68c0be9c8"
87
90
  }