theme-loader-api 1.0.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/README.md +210 -0
- package/fesm2022/theme-loader-api.mjs +110 -0
- package/fesm2022/theme-loader-api.mjs.map +1 -0
- package/package.json +35 -0
- package/types/theme-loader-api.d.ts +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
The Web theme loader API exposes 3 contracts:
|
|
4
|
+
1. `static init()` of themeLoader to be called during app startup.
|
|
5
|
+
2. `static loadTheme(picked: string | null, appColorsDir?: string | null)` to be called when the app user picks one from available themes.
|
|
6
|
+
3. `static get selectedTheme(): string | null` of themeLoader to give the URL of the selected theme, so GUI may display which theme is in-use.
|
|
7
|
+
|
|
8
|
+
Because the theme should be loaded at startup before the Web app rendering, the respective config must be loaded synchronously ASAP.
|
|
9
|
+
|
|
10
|
+
The GUI of theme selection is independent of the Web theme loader API. For example, in addition to Select and Menu for multiple themes, you may use Switch for switching between light and dark.
|
|
11
|
+
|
|
12
|
+
Remarks:
|
|
13
|
+
* Modern browsers like Chrome, Edge, Safari, and Firefox support a built-in concept of light/dark preference. Depending on your UX design, if you would not provide UI component for changing theme, then CSS only solution works well:
|
|
14
|
+
```css
|
|
15
|
+
<link rel="stylesheet" href="my-light.css" media="(prefers-color-scheme: light)">
|
|
16
|
+
<link rel="stylesheet" href="my-dark.css" media="(prefers-color-scheme: dark)">
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
1. Install [theme-loader-api](https://www.npmjs.com/package/theme-loader-api).
|
|
21
|
+
|
|
22
|
+
## Integration
|
|
23
|
+
1. Call `ThemeLoader.loadTheme()` before the [bootstrap of the Web app](https://github.com/zijianhuang/nmce/blob/master/projects/demoapp/src/main.ts).
|
|
24
|
+
1. In the [UI component presenting the theme picker](https://github.com/zijianhuang/nmce/blob/master/projects/demoapp/src/app/app.component.ts), convert the themes dictionary to an array which will be used to present the list. And call `ThemeLoader.loadTheme()` when the picker picks a theme.
|
|
25
|
+
1. Prepare [`siteconfig.js`](https://github.com/zijianhuang/nmce/blob/master/projects/demoapp/src/conf/siteconfig.js) and add `<script src="conf/siteconfig.js"></script>` to [index.html](https://github.com/zijianhuang/nmce/blob/master/projects/demoapp/src/index.html) if you want flexibility after build and deployment. Or, simply provide constant THEME_CONFIG in app code.
|
|
26
|
+
|
|
27
|
+
### [Angular Example](https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/)
|
|
28
|
+
|
|
29
|
+
[main.ts](https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/main.ts)
|
|
30
|
+
```ts
|
|
31
|
+
ThemeLoader.init();
|
|
32
|
+
bootstrapApplication(AppComponent, appConfig);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
[theme-select.component.ts](https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/app/theme-select.component.ts)
|
|
36
|
+
```ts
|
|
37
|
+
constructor() {
|
|
38
|
+
this.themes = AppConfigConstants.themesDic ? Object.keys(AppConfigConstants.themesDic).map(k => {
|
|
39
|
+
const c = AppConfigConstants.themesDic![k];
|
|
40
|
+
const obj: ThemeDef = {
|
|
41
|
+
display: c.display,
|
|
42
|
+
filePath: k,
|
|
43
|
+
dark: c.dark
|
|
44
|
+
};
|
|
45
|
+
return obj;
|
|
46
|
+
}) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
themeSelectionChang(e: MatSelectChange) {
|
|
50
|
+
ThemeLoader.loadTheme(e.value);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
[theme-select.component.html](https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/app/theme-select.component.html)
|
|
55
|
+
```html
|
|
56
|
+
<mat-select #themeSelect (selectionChange)="themeSelectionChang($event)" [value]="currentTheme">
|
|
57
|
+
@for (item of themes; track $index) {
|
|
58
|
+
<mat-option [value]="item.filePath">{{item.display}}</mat-option>
|
|
59
|
+
}
|
|
60
|
+
</mat-select>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
[siteconfig.js](https://github.com/zijianhuang/DemoCoreWeb/blob/master/docs/angular/conf/siteconfig.js)
|
|
64
|
+
```js
|
|
65
|
+
const THEME_CONFIG = {
|
|
66
|
+
apiBaseUri: 'https://mybackend.com/',
|
|
67
|
+
themesDic: {
|
|
68
|
+
"assets/themes/azure-blue.css": { display: "Azure & Blue", dark: false },
|
|
69
|
+
"assets/themes/rose-red.css": { display: "Roes & Red", dark: false },
|
|
70
|
+
"assets/themes/magenta-violet.css": { display: "Magenta & Violet", dark: true },
|
|
71
|
+
"assets/themes/cyan-orange.css": { display: "Cyan & Orange", dark: true }
|
|
72
|
+
},
|
|
73
|
+
themeLoaderSettings: {
|
|
74
|
+
storageKey: 'app.theme',
|
|
75
|
+
themeLinkId: 'theme',
|
|
76
|
+
appColorsDir: 'conf/',
|
|
77
|
+
appColorsLinkId: 'app-colors',
|
|
78
|
+
colorsCss: 'colors.css',
|
|
79
|
+
colorsDarkCss: 'colors-dark.css'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
[index.html](https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/index.html)
|
|
85
|
+
```html
|
|
86
|
+
<script src="conf/siteconfig.js"></script>
|
|
87
|
+
</head>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### React Example
|
|
91
|
+
|
|
92
|
+
[index.tsx](https://github.com/zijianhuang/DemoCoreWeb/blob/master/ReactHeroes/src/index.tsx)
|
|
93
|
+
```ts
|
|
94
|
+
ThemeLoader.init();
|
|
95
|
+
|
|
96
|
+
const root = ReactDOM.createRoot(
|
|
97
|
+
document.getElementById('root') as HTMLElement
|
|
98
|
+
);
|
|
99
|
+
root.render(...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
[Home.tsx](https://github.com/zijianhuang/DemoCoreWeb/blob/master/ReactHeroes/src/Home.tsx)
|
|
103
|
+
```ts
|
|
104
|
+
const themes = AppConfigConstants.themesDic ? Object.keys(AppConfigConstants.themesDic).map(k => {
|
|
105
|
+
const c = AppConfigConstants.themesDic![k];
|
|
106
|
+
const obj: ThemeDef = {
|
|
107
|
+
display: c.display,
|
|
108
|
+
filePath: k,
|
|
109
|
+
dark: c.dark
|
|
110
|
+
};
|
|
111
|
+
return obj;
|
|
112
|
+
}) : undefined;
|
|
113
|
+
|
|
114
|
+
const [currentTheme, setCurrentTheme] = useState(() => ThemeLoader.selectedTheme ?? undefined);
|
|
115
|
+
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
116
|
+
const v = event.target.value;
|
|
117
|
+
setCurrentTheme(v);
|
|
118
|
+
ThemeLoader.loadTheme(v);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<h1>React Heroes!</h1>
|
|
124
|
+
<div>
|
|
125
|
+
<label htmlFor="theme-select">Themes </label>
|
|
126
|
+
<select
|
|
127
|
+
id="theme-select"
|
|
128
|
+
value={currentTheme ?? ""}
|
|
129
|
+
onChange={handleChange}
|
|
130
|
+
>
|
|
131
|
+
{themes?.map((item) => (
|
|
132
|
+
<option key={item.filePath} value={item.filePath}>
|
|
133
|
+
{item.display}
|
|
134
|
+
</option>
|
|
135
|
+
))}
|
|
136
|
+
</select>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
[siteconfig.js](https://github.com/zijianhuang/DemoCoreWeb/blob/master/docs/react/conf/siteconfig.js)
|
|
141
|
+
```js
|
|
142
|
+
const THEME_CONFIG = {
|
|
143
|
+
apiBaseUri: 'https://mybackend.com/',
|
|
144
|
+
themesDic: {
|
|
145
|
+
"assets/themes/azure-blue.css": { display: "Azure & Blue", dark: false },
|
|
146
|
+
"assets/themes/rose-red.css": { display: "Roes & Red", dark: false },
|
|
147
|
+
"assets/themes/magenta-violet.css": { display: "Magenta & Violet", dark: true },
|
|
148
|
+
"assets/themes/cyan-orange.css": { display: "Cyan & Orange", dark: true }
|
|
149
|
+
},
|
|
150
|
+
themeLoaderSettings: {
|
|
151
|
+
storageKey: 'app.theme',
|
|
152
|
+
themeLinkId: 'theme',
|
|
153
|
+
appColorsDir: 'conf/',
|
|
154
|
+
appColorsLinkId: 'app-colors',
|
|
155
|
+
colorsCss: 'colors.css',
|
|
156
|
+
colorsDarkCss: 'colors-dark.css'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
[index.html](https://github.com/zijianhuang/DemoCoreWeb/blob/master/ReactHeroes/public/index.html)
|
|
162
|
+
```html
|
|
163
|
+
<script src="conf/siteconfig.js"></script>
|
|
164
|
+
</head>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Respect prefers-color-scheme
|
|
168
|
+
|
|
169
|
+
By default, this API will pick the first available theme in the dictionary during the first startup of the Web app, and use the last pick afterward. If you want to respect prefers-color-scheme during the initial load of the Web app, you may use the following in the app's bootstrap:
|
|
170
|
+
```ts
|
|
171
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
172
|
+
var r = findFirstTheme(isDark);
|
|
173
|
+
if (r) {
|
|
174
|
+
ThemeLoader.loadTheme(r.filePath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
platformBrowser().bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], })
|
|
178
|
+
.catch(err => console.error(err));
|
|
179
|
+
|
|
180
|
+
function findFirstTheme(dark: boolean): { filePath: string; theme: ThemeValue } | undefined {
|
|
181
|
+
const entry = Object.entries(AppConfigConstants.themesDic!).find(
|
|
182
|
+
([, theme]) => theme.dark === dark
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return entry
|
|
186
|
+
? { filePath: entry[0], theme: entry[1] }
|
|
187
|
+
: undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## How About I18N and L10N?
|
|
193
|
+
|
|
194
|
+
The only things need to be translated is the display name of each theme.
|
|
195
|
+
|
|
196
|
+
### Solution 1: No need for I18N and Use Icon To Represent Theme Impression
|
|
197
|
+
|
|
198
|
+
You may extend `interface ThemeDef`, and make it contain some meta info of generating SVG icons presenting respective theme. And the icons will be inline with the HTML template. [Angular Material Components site](https://material.angular.dev/) uses [this approach](https://github.com/angular/components/blob/main/docs/src/app/shared/theme-picker/).
|
|
199
|
+
|
|
200
|
+
Or you may just hand-draw some SVG icons and linked it in the HTML template.
|
|
201
|
+
|
|
202
|
+
### Solution 2: Create Dictionary in App Code
|
|
203
|
+
|
|
204
|
+
Depending the framework like Angular or the library like React, there could be a few ways to create a dictionary to lookup translations and create translations.
|
|
205
|
+
|
|
206
|
+
### Solution 3: Post Build Processing
|
|
207
|
+
|
|
208
|
+
If you are using `siteconfig.js`, the JS file should not be included in the hash tables of the service worker for automatic update.
|
|
209
|
+
|
|
210
|
+
In Angular, each locale has its own build, therefore, you may craft some post build script to inject the translated names into the `siteconfig.js` of each build.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const ThemeConfigConstants = {
|
|
2
|
+
...(typeof THEME_CONFIG === 'undefined' ? {} : THEME_CONFIG),
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper class to load default theme or selected theme among themes defined in app startup settings.
|
|
7
|
+
* index.html should not have the theme css link during design time.
|
|
8
|
+
* In addition to the main theme which could be one the prebuilt themes reusable across apps, like one of those of Angular Material,
|
|
9
|
+
* the app may optionally has its own app css file for colors.
|
|
10
|
+
*/
|
|
11
|
+
class ThemeLoader {
|
|
12
|
+
static settings = ThemeConfigConstants.themeLoaderSettings;
|
|
13
|
+
/**
|
|
14
|
+
* selected theme file name saved in localStorage.
|
|
15
|
+
*/
|
|
16
|
+
static get selectedTheme() {
|
|
17
|
+
return this.settings ? localStorage.getItem(this.settings.storageKey) : null;
|
|
18
|
+
}
|
|
19
|
+
;
|
|
20
|
+
static set selectedTheme(v) {
|
|
21
|
+
if (this.settings) {
|
|
22
|
+
localStorage.setItem(this.settings.storageKey, v);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
;
|
|
26
|
+
/**
|
|
27
|
+
* Load default or previously selected theme during app startup, typically used before calling `bootstrapApplication()`.
|
|
28
|
+
*/
|
|
29
|
+
static init() {
|
|
30
|
+
this.loadTheme(this.selectedTheme);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load theme during operation through `ThemeLoader.loadTheme(themeDicKey);`.
|
|
34
|
+
* @param picked one of the prebuilt themes, typically used with the app's theme picker.
|
|
35
|
+
*/
|
|
36
|
+
static loadTheme(picked) {
|
|
37
|
+
if (!ThemeConfigConstants.themesDic || !this.settings || Object.keys(ThemeConfigConstants.themesDic).length === 0) {
|
|
38
|
+
console.error('AppConfigConstants need to have themesDic with at least 1 item, and themeKeys.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let themeLink = document.getElementById(this.settings.themeLinkId);
|
|
42
|
+
if (themeLink) { // app has been loaded in the browser page/tab.
|
|
43
|
+
const currentTheme = themeLink.href.substring(themeLink.href.lastIndexOf('/') + 1);
|
|
44
|
+
const notToLoad = picked == currentTheme;
|
|
45
|
+
if (notToLoad) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const themeValue = ThemeConfigConstants.themesDic[picked];
|
|
49
|
+
if (!themeValue) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
themeLink.href = picked;
|
|
53
|
+
this.selectedTheme = picked;
|
|
54
|
+
console.info(`theme altered to ${picked}.`);
|
|
55
|
+
if (this.settings.appColorsLinkId) {
|
|
56
|
+
let appColorsLink = document.getElementById(this.settings.appColorsLinkId);
|
|
57
|
+
if (appColorsLink) {
|
|
58
|
+
if (themeValue.dark != null && this.settings.colorsDarkCss && this.settings.colorsCss) {
|
|
59
|
+
const customFile = themeValue.dark ? this.settings.colorsDarkCss : this.settings.colorsCss;
|
|
60
|
+
appColorsLink.href = (this.settings.appColorsDir ?? '') + customFile;
|
|
61
|
+
}
|
|
62
|
+
else if (this.settings.colorsCss) {
|
|
63
|
+
appColorsLink.href = (this.settings.appColorsDir ?? '') + this.settings.colorsCss;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else { // when app is loaded for the first time, then create
|
|
69
|
+
themeLink = document.createElement('link');
|
|
70
|
+
themeLink.id = this.settings.themeLinkId;
|
|
71
|
+
themeLink.rel = 'stylesheet';
|
|
72
|
+
const themeDicKey = picked ?? Object.keys(ThemeConfigConstants.themesDic)[0];
|
|
73
|
+
themeLink.href = themeDicKey;
|
|
74
|
+
document.head.appendChild(themeLink);
|
|
75
|
+
this.selectedTheme = themeDicKey;
|
|
76
|
+
console.info(`Initially loaded theme ${themeDicKey}`);
|
|
77
|
+
if (this.settings.appColorsLinkId) {
|
|
78
|
+
const appColorsLink = document.createElement('link');
|
|
79
|
+
appColorsLink.id = this.settings.appColorsLinkId;
|
|
80
|
+
appColorsLink.rel = 'stylesheet';
|
|
81
|
+
const themeValue = ThemeConfigConstants.themesDic[themeDicKey];
|
|
82
|
+
if (themeValue.dark != null && this.settings.colorsDarkCss && this.settings.colorsCss) {
|
|
83
|
+
const customFile = themeValue.dark ? this.settings.colorsDarkCss : this.settings.colorsCss;
|
|
84
|
+
appColorsLink.href = (this.settings.appColorsDir ?? '') + customFile;
|
|
85
|
+
}
|
|
86
|
+
else if (this.settings.colorsCss) {
|
|
87
|
+
appColorsLink.href = (this.settings.appColorsDir ?? '') + this.settings.colorsCss;
|
|
88
|
+
}
|
|
89
|
+
if (appColorsLink.href) {
|
|
90
|
+
document.head.appendChild(appColorsLink);
|
|
91
|
+
console.info(`appColors ${appColorsLink} loaded.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.warn(`With appColorsLinkId defined, dark&colorsCss&colorDarkCss or colorsCss should be defined.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Public API Surface of theme-loader-api
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generated bundle index. Do not edit.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
export { ThemeConfigConstants, ThemeLoader };
|
|
110
|
+
//# sourceMappingURL=theme-loader-api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-loader-api.mjs","sources":["../../../projects/theme-loader-api/src/lib/themeDef.ts","../../../projects/theme-loader-api/src/lib/themeLoader.ts","../../../projects/theme-loader-api/src/public-api.ts","../../../projects/theme-loader-api/src/theme-loader-api.ts"],"sourcesContent":["export interface ThemeValue {\r\n\t/** Display name used in the UI like menu or dropdown */\r\n\tdisplay: string;\r\n\r\n\t/** Dark them or not. Optionally to tell which optional app level colors CSS to use, if some app level colors need to adapt the light or dark theme. */\r\n\tdark?: boolean;\r\n}\r\n\r\nexport interface ThemeDef extends ThemeValue {\r\n\t/** Relative path or URL to CDN */\r\n\tfilePath: string;\r\n}\r\n\r\nexport interface ThemesDic {\r\n\t[filePath: string]: ThemeValue\r\n}\r\n\r\nexport interface ThemeLoaderSettings {\r\n\t/**\r\n\t * The key of themeDic to store the selected theme in local storage of browser. Each app or site must have a unique key to avoid conflict with other apps or sites.\r\n\t */\r\n\tstorageKey: string;\r\n\r\n\t/**\r\n\t * The id of the link element in index.html for loading the theme CSS file dynamically during app startup and operation.\r\n\t */\r\n\tthemeLinkId: string;\r\n\r\n\t/** \r\n\t * Optionally the app may has an app level colors CSS declaring colors neutral to the light or dark theme, in addition to a prebuilt theme reused across apps. \r\n\t * If some colors need to adapt the light or dark theme, having those colors defined in colorsCss and colorsDarkCss is convenient for SDLC, since you can\r\n\t * use tools to flip colors to dark or light.\r\n\t */\r\n\tappColorsLinkId?: string;\r\n\r\n\t/**\r\n\t * If undefined or null, app colors css is in root.\r\n\t * Effected only when appColorsLinkId is defined.\r\n\t */\r\n\tappColorsDir?: string;\r\n\r\n\t/** \r\n\t * Optionally the app may has an app level colors CSS declaring colors adapting to the light theme. \r\n\t * If the app uses only light or dark theme, for example ThemeValue.dark is not defined, this alone is enough, not needing colorsDarkCss. \r\n\t*/\r\n\tcolorsCss?: string;\r\n\r\n\t/** \r\n\t * Optionally the app may has an app level colors CSS declaring colors adapting to the dark theme. \r\n\t * If the app uses only light or dark theme, there's no need to declare this. \r\n\t */\r\n\tcolorsDarkCss?: string;\r\n}\r\n\r\ninterface Theme_Config {\r\n\tthemesDic?: ThemesDic,\r\n\r\n\tthemeLoaderSettings?: ThemeLoaderSettings\r\n}\r\n\r\ndeclare const THEME_CONFIG: Theme_Config\r\n\r\nexport const ThemeConfigConstants: Theme_Config = {\r\n\t...(typeof THEME_CONFIG === 'undefined' ? {} : THEME_CONFIG),\r\n}\r\n","import { ThemeConfigConstants } from \"./themeDef\"; //just for typed\r\n\r\n/**\r\n * Helper class to load default theme or selected theme among themes defined in app startup settings.\r\n * index.html should not have the theme css link during design time.\r\n * In addition to the main theme which could be one the prebuilt themes reusable across apps, like one of those of Angular Material, \r\n * the app may optionally has its own app css file for colors.\r\n */\r\nexport class ThemeLoader {\r\n\tprivate static readonly settings = ThemeConfigConstants.themeLoaderSettings;\r\n\r\n\t/**\r\n\t * selected theme file name saved in localStorage.\r\n\t */\r\n\tstatic get selectedTheme(): string | null {\r\n\t\treturn this.settings ? localStorage.getItem(this.settings.storageKey) : null;\r\n\t};\r\n\tprivate static set selectedTheme(v: string) {\r\n\t\tif (this.settings) {\r\n\t\t\tlocalStorage.setItem(this.settings.storageKey, v);\r\n\t\t}\r\n\t};\r\n\r\n\t/**\r\n\t * Load default or previously selected theme during app startup, typically used before calling `bootstrapApplication()`.\r\n\t */\r\n\tstatic init(){\r\n\t\tthis.loadTheme(this.selectedTheme);\r\n\t}\r\n\r\n\t/**\r\n\t * Load theme during operation through `ThemeLoader.loadTheme(themeDicKey);`.\r\n\t * @param picked one of the prebuilt themes, typically used with the app's theme picker.\r\n\t */\r\n\tstatic loadTheme(picked: string | null) {\r\n\t\tif (!ThemeConfigConstants.themesDic || !this.settings || Object.keys(ThemeConfigConstants.themesDic).length === 0) {\r\n\t\t\tconsole.error('AppConfigConstants need to have themesDic with at least 1 item, and themeKeys.');\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tlet themeLink = document.getElementById(this.settings.themeLinkId) as HTMLLinkElement;\r\n\t\tif (themeLink) { // app has been loaded in the browser page/tab.\r\n\t\t\tconst currentTheme = themeLink.href.substring(themeLink.href.lastIndexOf('/') + 1);\r\n\t\t\tconst notToLoad = picked == currentTheme;\r\n\t\t\tif (notToLoad) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tconst themeValue = ThemeConfigConstants.themesDic[picked!];\r\n\t\t\tif (!themeValue) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tthemeLink.href = picked!;\r\n\t\t\tthis.selectedTheme = picked!;\r\n\t\t\tconsole.info(`theme altered to ${picked}.`);\r\n\r\n\t\t\tif (this.settings.appColorsLinkId) {\r\n\t\t\t\tlet appColorsLink = document.getElementById(this.settings.appColorsLinkId) as HTMLLinkElement;\r\n\t\t\t\tif (appColorsLink) {\r\n\t\t\t\t\tif (themeValue.dark != null && this.settings.colorsDarkCss && this.settings.colorsCss) {\r\n\t\t\t\t\t\tconst customFile = themeValue.dark ? this.settings.colorsDarkCss : this.settings.colorsCss;\r\n\t\t\t\t\t\tappColorsLink.href = (this.settings.appColorsDir ?? '') + customFile;\r\n\t\t\t\t\t} else if (this.settings.colorsCss) {\r\n\t\t\t\t\t\tappColorsLink.href = (this.settings.appColorsDir ?? '') + this.settings.colorsCss;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t} else { // when app is loaded for the first time, then create \r\n\t\t\tthemeLink = document.createElement('link');\r\n\t\t\tthemeLink.id = this.settings.themeLinkId;\r\n\t\t\tthemeLink.rel = 'stylesheet';\r\n\t\t\tconst themeDicKey = picked ?? Object.keys(ThemeConfigConstants.themesDic!)[0];\r\n\t\t\tthemeLink.href = themeDicKey;\r\n\t\t\tdocument.head.appendChild(themeLink);\r\n\t\t\tthis.selectedTheme = themeDicKey;\r\n\t\t\tconsole.info(`Initially loaded theme ${themeDicKey}`);\r\n\r\n\t\t\tif (this.settings.appColorsLinkId) {\r\n\t\t\t\tconst appColorsLink = document.createElement('link');\r\n\t\t\t\tappColorsLink.id = this.settings.appColorsLinkId;\r\n\t\t\t\tappColorsLink.rel = 'stylesheet';\r\n\t\t\t\tconst themeValue = ThemeConfigConstants.themesDic[themeDicKey];\r\n\t\t\t\tif (themeValue.dark != null && this.settings.colorsDarkCss && this.settings.colorsCss) {\r\n\t\t\t\t\tconst customFile = themeValue.dark ? this.settings.colorsDarkCss : this.settings.colorsCss;\r\n\t\t\t\t\tappColorsLink.href = (this.settings.appColorsDir ?? '') + customFile;\r\n\t\t\t\t} else if (this.settings.colorsCss) {\r\n\t\t\t\t\tappColorsLink.href = (this.settings.appColorsDir ?? '') + this.settings.colorsCss;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (appColorsLink.href) {\r\n\t\t\t\t\tdocument.head.appendChild(appColorsLink);\r\n\t\t\t\t\tconsole.info(`appColors ${appColorsLink} loaded.`)\r\n\t\t\t\t} else {\r\n\t\t\t\t\tconsole.warn(`With appColorsLinkId defined, dark&colorsCss&colorDarkCss or colorsCss should be defined.`)\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n","/*\r\n * Public API Surface of theme-loader-api\r\n */\r\n\r\nexport * from './lib/themeDef';\r\nexport * from './lib/themeLoader';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "theme-loader-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Z"
|
|
6
|
+
},
|
|
7
|
+
"description": "Theme loader API independent of framework, lib and UI design of Web app.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"url": "git+https://github.com/zijianhuang/nmce.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"theme",
|
|
14
|
+
"angular",
|
|
15
|
+
"react",
|
|
16
|
+
"vue",
|
|
17
|
+
"web"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"tslib": "^2.3.0"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"module": "fesm2022/theme-loader-api.mjs",
|
|
25
|
+
"typings": "types/theme-loader-api.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": {
|
|
28
|
+
"default": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./types/theme-loader-api.d.ts",
|
|
32
|
+
"default": "./fesm2022/theme-loader-api.mjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
interface ThemeValue {
|
|
2
|
+
/** Display name used in the UI like menu or dropdown */
|
|
3
|
+
display: string;
|
|
4
|
+
/** Dark them or not. Optionally to tell which optional app level colors CSS to use, if some app level colors need to adapt the light or dark theme. */
|
|
5
|
+
dark?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ThemeDef extends ThemeValue {
|
|
8
|
+
/** Relative path or URL to CDN */
|
|
9
|
+
filePath: string;
|
|
10
|
+
}
|
|
11
|
+
interface ThemesDic {
|
|
12
|
+
[filePath: string]: ThemeValue;
|
|
13
|
+
}
|
|
14
|
+
interface ThemeLoaderSettings {
|
|
15
|
+
/**
|
|
16
|
+
* The key of themeDic to store the selected theme in local storage of browser. Each app or site must have a unique key to avoid conflict with other apps or sites.
|
|
17
|
+
*/
|
|
18
|
+
storageKey: string;
|
|
19
|
+
/**
|
|
20
|
+
* The id of the link element in index.html for loading the theme CSS file dynamically during app startup and operation.
|
|
21
|
+
*/
|
|
22
|
+
themeLinkId: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optionally the app may has an app level colors CSS declaring colors neutral to the light or dark theme, in addition to a prebuilt theme reused across apps.
|
|
25
|
+
* If some colors need to adapt the light or dark theme, having those colors defined in colorsCss and colorsDarkCss is convenient for SDLC, since you can
|
|
26
|
+
* use tools to flip colors to dark or light.
|
|
27
|
+
*/
|
|
28
|
+
appColorsLinkId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* If undefined or null, app colors css is in root.
|
|
31
|
+
* Effected only when appColorsLinkId is defined.
|
|
32
|
+
*/
|
|
33
|
+
appColorsDir?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Optionally the app may has an app level colors CSS declaring colors adapting to the light theme.
|
|
36
|
+
* If the app uses only light or dark theme, for example ThemeValue.dark is not defined, this alone is enough, not needing colorsDarkCss.
|
|
37
|
+
*/
|
|
38
|
+
colorsCss?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Optionally the app may has an app level colors CSS declaring colors adapting to the dark theme.
|
|
41
|
+
* If the app uses only light or dark theme, there's no need to declare this.
|
|
42
|
+
*/
|
|
43
|
+
colorsDarkCss?: string;
|
|
44
|
+
}
|
|
45
|
+
interface Theme_Config {
|
|
46
|
+
themesDic?: ThemesDic;
|
|
47
|
+
themeLoaderSettings?: ThemeLoaderSettings;
|
|
48
|
+
}
|
|
49
|
+
declare const ThemeConfigConstants: Theme_Config;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Helper class to load default theme or selected theme among themes defined in app startup settings.
|
|
53
|
+
* index.html should not have the theme css link during design time.
|
|
54
|
+
* In addition to the main theme which could be one the prebuilt themes reusable across apps, like one of those of Angular Material,
|
|
55
|
+
* the app may optionally has its own app css file for colors.
|
|
56
|
+
*/
|
|
57
|
+
declare class ThemeLoader {
|
|
58
|
+
private static readonly settings;
|
|
59
|
+
/**
|
|
60
|
+
* selected theme file name saved in localStorage.
|
|
61
|
+
*/
|
|
62
|
+
static get selectedTheme(): string | null;
|
|
63
|
+
private static set selectedTheme(value);
|
|
64
|
+
/**
|
|
65
|
+
* Load default or previously selected theme during app startup, typically used before calling `bootstrapApplication()`.
|
|
66
|
+
*/
|
|
67
|
+
static init(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Load theme during operation through `ThemeLoader.loadTheme(themeDicKey);`.
|
|
70
|
+
* @param picked one of the prebuilt themes, typically used with the app's theme picker.
|
|
71
|
+
*/
|
|
72
|
+
static loadTheme(picked: string | null): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { ThemeConfigConstants, ThemeLoader };
|
|
76
|
+
export type { ThemeDef, ThemeLoaderSettings, ThemeValue, ThemesDic };
|