@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.
- package/dist/custom-elements.json +300 -1
- package/dist/dts/button/button.styles.d.ts.map +1 -1
- package/dist/dts/card/card.styles.d.ts.map +1 -1
- package/dist/dts/flexlayout-react-theme/flexlayout-react-theme.generated.d.ts +1 -1
- package/dist/dts/flexlayout-react-theme/flexlayout-react-theme.generated.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/modal-theme/index.d.ts +3 -0
- package/dist/dts/modal-theme/index.d.ts.map +1 -0
- package/dist/dts/modal-theme/modal-theme.d.ts +113 -0
- package/dist/dts/modal-theme/modal-theme.d.ts.map +1 -0
- package/dist/dts/modal-theme/theme-compiler.d.ts +41 -0
- package/dist/dts/modal-theme/theme-compiler.d.ts.map +1 -0
- package/dist/dts/tab/tab.styles.d.ts.map +1 -1
- package/dist/dts/tab-panel/tab-panel.styles.d.ts.map +1 -1
- package/dist/esm/button/button.styles.js +14 -3
- package/dist/esm/card/card.styles.js +1 -0
- package/dist/esm/flexlayout-react-theme/flexlayout-react-theme.generated.js +1 -3
- package/dist/esm/flexlayout-react-theme/flexlayout-react.overrides.css +1 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/modal-theme/index.js +2 -0
- package/dist/esm/modal-theme/modal-theme.js +198 -0
- package/dist/esm/modal-theme/theme-compiler.js +105 -0
- package/dist/esm/tab/tab.styles.js +1 -0
- package/dist/esm/tab-panel/tab-panel.styles.js +2 -1
- package/package.json +15 -12
|
@@ -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,
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
@@ -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
|
+
}
|
|
@@ -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.
|
|
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/
|
|
40
|
-
"@genesislcap/
|
|
41
|
-
"@genesislcap/
|
|
42
|
-
"@genesislcap/
|
|
43
|
-
"@genesislcap/
|
|
44
|
-
"@genesislcap/
|
|
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.
|
|
57
|
-
"@genesislcap/foundation-ui": "14.
|
|
58
|
-
"@genesislcap/foundation-utils": "14.
|
|
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": "
|
|
89
|
+
"gitHead": "5de11f8989291fe8aaf19a7048dd6be68c0be9c8"
|
|
87
90
|
}
|