@reidelsaltres/pureper 0.2.27 → 0.2.28
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.
|
@@ -1,6 +1,45 @@
|
|
|
1
|
+
import Observable from "./api/Observer.js";
|
|
1
2
|
export declare let ACTIVE_THEME_KEY: string;
|
|
2
3
|
export declare function loadTheme(name: string): Promise<string>;
|
|
3
4
|
export declare function loadThemeAsInstant(name: string): Promise<CSSStyleSheet>;
|
|
4
5
|
export declare function init(): Promise<void>;
|
|
5
6
|
export declare function setTheme(name: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* AppTheme descriptor — combines a color palette with optional
|
|
9
|
+
* component implementation switches and lifecycle callbacks.
|
|
10
|
+
*/
|
|
11
|
+
export interface AppThemeConfig {
|
|
12
|
+
/** Name of the theme */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Color palette CSS file name (without .theme.css extension), e.g. "Winter" */
|
|
15
|
+
palette: string;
|
|
16
|
+
/** Map of placeholder name → implementation name to switch when theme activates */
|
|
17
|
+
implementations?: Map<string, string>;
|
|
18
|
+
/** Called when theme is activated — use for effects (e.g., snow animation) */
|
|
19
|
+
onActivate?: () => void;
|
|
20
|
+
/** Called when theme is deactivated — use to clean up effects */
|
|
21
|
+
onDeactivate?: () => void;
|
|
22
|
+
}
|
|
23
|
+
/** Currently active AppTheme, or null if only a color palette is active */
|
|
24
|
+
export declare const activeAppTheme: Observable<AppThemeConfig | null>;
|
|
25
|
+
/** Register an AppTheme so it can be activated by name */
|
|
26
|
+
export declare function registerAppTheme(config: AppThemeConfig): void;
|
|
27
|
+
/** Get a registered AppTheme by name */
|
|
28
|
+
export declare function getAppTheme(name: string): AppThemeConfig | undefined;
|
|
29
|
+
/** Get all registered AppTheme names */
|
|
30
|
+
export declare function getAppThemeNames(): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Activate an AppTheme by name.
|
|
33
|
+
* - Applies its color palette
|
|
34
|
+
* - Switches component implementations listed in the config
|
|
35
|
+
* - Calls onActivate callback
|
|
36
|
+
*/
|
|
37
|
+
export declare function activateAppTheme(name: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Deactivate the currently active AppTheme.
|
|
40
|
+
* - Calls onDeactivate callback
|
|
41
|
+
* - Reverts implementations to defaults
|
|
42
|
+
* - Does NOT change the color palette (that's separate)
|
|
43
|
+
*/
|
|
44
|
+
export declare function deactivateAppTheme(): Promise<void>;
|
|
6
45
|
//# sourceMappingURL=Theme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Theme.d.ts","sourceRoot":"","sources":["../../src/foundation/Theme.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Theme.d.ts","sourceRoot":"","sources":["../../src/foundation/Theme.ts"],"names":[],"mappings":"AACA,OAAO,UAAU,MAAM,mBAAmB,CAAC;AAE3C,eAAO,IAAI,gBAAgB,QAAU,CAAC;AAItC,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7D;AACD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAK7E;AACD,wBAAsB,IAAI,kBAOzB;AACD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,iBA0B1C;AAID;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC3B,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,2EAA2E;AAC3E,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,cAAc,GAAG,IAAI,CAA+C,CAAC;AAK7G,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAE7D;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEpE;AAED,wCAAwC;AACxC,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkClE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA2BxD"}
|
package/out/foundation/Theme.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Fetcher from "./Fetcher.js";
|
|
2
|
+
import Observable from "./api/Observer.js";
|
|
2
3
|
export let ACTIVE_THEME_KEY = "Empty";
|
|
3
4
|
let activeThemeSheet = null;
|
|
5
|
+
let _internalThemeSwitch = false;
|
|
4
6
|
export async function loadTheme(name) {
|
|
5
7
|
// Use hosting-root absolute path so GitHub Pages subfolder deployments (e.g. /Hellper/) keep the subfolder.
|
|
6
8
|
return Fetcher.fetchText(`/resources/${name}.theme.css`);
|
|
@@ -21,6 +23,13 @@ export async function init() {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
export async function setTheme(name) {
|
|
26
|
+
// Deactivate AppTheme when switching to a plain palette (but not when called from activateAppTheme)
|
|
27
|
+
if (!_internalThemeSwitch && activeAppTheme.getObject()) {
|
|
28
|
+
const current = activeAppTheme.getObject();
|
|
29
|
+
current.onDeactivate?.();
|
|
30
|
+
activeAppTheme.setObject(null);
|
|
31
|
+
localStorage.removeItem('appTheme');
|
|
32
|
+
}
|
|
24
33
|
let theme = await loadTheme(name);
|
|
25
34
|
theme = theme.replace(/\.[\w-]+-theme/g, ":root");
|
|
26
35
|
const sheet = new CSSStyleSheet();
|
|
@@ -41,4 +50,89 @@ export async function setTheme(name) {
|
|
|
41
50
|
}
|
|
42
51
|
activeThemeSheet = sheet;
|
|
43
52
|
}
|
|
53
|
+
/** Currently active AppTheme, or null if only a color palette is active */
|
|
54
|
+
export const activeAppTheme = new Observable(null);
|
|
55
|
+
/** Registry of all registered AppThemes */
|
|
56
|
+
const appThemeRegistry = new Map();
|
|
57
|
+
/** Register an AppTheme so it can be activated by name */
|
|
58
|
+
export function registerAppTheme(config) {
|
|
59
|
+
appThemeRegistry.set(config.name, config);
|
|
60
|
+
}
|
|
61
|
+
/** Get a registered AppTheme by name */
|
|
62
|
+
export function getAppTheme(name) {
|
|
63
|
+
return appThemeRegistry.get(name);
|
|
64
|
+
}
|
|
65
|
+
/** Get all registered AppTheme names */
|
|
66
|
+
export function getAppThemeNames() {
|
|
67
|
+
return Array.from(appThemeRegistry.keys());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Activate an AppTheme by name.
|
|
71
|
+
* - Applies its color palette
|
|
72
|
+
* - Switches component implementations listed in the config
|
|
73
|
+
* - Calls onActivate callback
|
|
74
|
+
*/
|
|
75
|
+
export async function activateAppTheme(name) {
|
|
76
|
+
const config = appThemeRegistry.get(name);
|
|
77
|
+
if (!config)
|
|
78
|
+
throw new Error(`[Theme]: AppTheme "${name}" not registered`);
|
|
79
|
+
// Deactivate current AppTheme if any
|
|
80
|
+
await deactivateAppTheme();
|
|
81
|
+
// Apply palette (guarded to avoid deactivation inside setTheme)
|
|
82
|
+
_internalThemeSwitch = true;
|
|
83
|
+
await setTheme(config.palette);
|
|
84
|
+
_internalThemeSwitch = false;
|
|
85
|
+
ACTIVE_THEME_KEY = config.palette;
|
|
86
|
+
// Switch implementations
|
|
87
|
+
if (config.implementations) {
|
|
88
|
+
// Dynamic import to avoid circular dependency
|
|
89
|
+
const { Placeholder } = await import('./Injection.js');
|
|
90
|
+
for (const [placeholder, impl] of config.implementations) {
|
|
91
|
+
try {
|
|
92
|
+
await Placeholder.switchTo(placeholder, impl);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.warn(`[Theme]: Failed to switch "${placeholder}" to "${impl}":`, e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Set active
|
|
100
|
+
activeAppTheme.setObject(config);
|
|
101
|
+
localStorage.setItem('appTheme', name);
|
|
102
|
+
// Call activate callback
|
|
103
|
+
config.onActivate?.();
|
|
104
|
+
console.info(`[Theme]: AppTheme "${name}" activated`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Deactivate the currently active AppTheme.
|
|
108
|
+
* - Calls onDeactivate callback
|
|
109
|
+
* - Reverts implementations to defaults
|
|
110
|
+
* - Does NOT change the color palette (that's separate)
|
|
111
|
+
*/
|
|
112
|
+
export async function deactivateAppTheme() {
|
|
113
|
+
const current = activeAppTheme.getObject();
|
|
114
|
+
if (!current)
|
|
115
|
+
return;
|
|
116
|
+
// Call deactivate callback
|
|
117
|
+
current.onDeactivate?.();
|
|
118
|
+
// Revert implementations to defaults
|
|
119
|
+
if (current.implementations) {
|
|
120
|
+
const { Placeholder } = await import('./Injection.js');
|
|
121
|
+
for (const [placeholder] of current.implementations) {
|
|
122
|
+
try {
|
|
123
|
+
const p = Placeholder.get(placeholder);
|
|
124
|
+
const firstImpl = p.implementations.entries().next().value;
|
|
125
|
+
if (firstImpl) {
|
|
126
|
+
await Placeholder.switchTo(placeholder, firstImpl[0]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.warn(`[Theme]: Failed to revert "${placeholder}":`, e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
activeAppTheme.setObject(null);
|
|
135
|
+
localStorage.removeItem('appTheme');
|
|
136
|
+
console.info(`[Theme]: AppTheme "${current.name}" deactivated`);
|
|
137
|
+
}
|
|
44
138
|
//# sourceMappingURL=Theme.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Theme.js","sourceRoot":"","sources":["../../src/foundation/Theme.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"Theme.js","sourceRoot":"","sources":["../../src/foundation/Theme.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/foundation/Theme.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Fetcher from "./Fetcher.js";
|
|
2
|
+
import Observable from "./api/Observer.js";
|
|
2
3
|
|
|
3
4
|
export let ACTIVE_THEME_KEY = "Empty";
|
|
4
5
|
let activeThemeSheet: CSSStyleSheet | null = null;
|
|
6
|
+
let _internalThemeSwitch = false;
|
|
5
7
|
|
|
6
8
|
export async function loadTheme(name: string): Promise<string> {
|
|
7
9
|
// Use hosting-root absolute path so GitHub Pages subfolder deployments (e.g. /Hellper/) keep the subfolder.
|
|
@@ -22,6 +24,14 @@ export async function init() {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
export async function setTheme(name: string) {
|
|
27
|
+
// Deactivate AppTheme when switching to a plain palette (but not when called from activateAppTheme)
|
|
28
|
+
if (!_internalThemeSwitch && activeAppTheme.getObject()) {
|
|
29
|
+
const current = activeAppTheme.getObject()!;
|
|
30
|
+
current.onDeactivate?.();
|
|
31
|
+
activeAppTheme.setObject(null);
|
|
32
|
+
localStorage.removeItem('appTheme');
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
let theme: string = await loadTheme(name);
|
|
26
36
|
theme = theme.replace(/\.[\w-]+-theme/g, ":root");
|
|
27
37
|
const sheet = new CSSStyleSheet();
|
|
@@ -41,3 +51,120 @@ export async function setTheme(name: string) {
|
|
|
41
51
|
activeThemeSheet = sheet;
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
// ── AppTheme system ─────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* AppTheme descriptor — combines a color palette with optional
|
|
58
|
+
* component implementation switches and lifecycle callbacks.
|
|
59
|
+
*/
|
|
60
|
+
export interface AppThemeConfig {
|
|
61
|
+
/** Name of the theme */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Color palette CSS file name (without .theme.css extension), e.g. "Winter" */
|
|
64
|
+
palette: string;
|
|
65
|
+
/** Map of placeholder name → implementation name to switch when theme activates */
|
|
66
|
+
implementations?: Map<string, string>;
|
|
67
|
+
/** Called when theme is activated — use for effects (e.g., snow animation) */
|
|
68
|
+
onActivate?: () => void;
|
|
69
|
+
/** Called when theme is deactivated — use to clean up effects */
|
|
70
|
+
onDeactivate?: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Currently active AppTheme, or null if only a color palette is active */
|
|
74
|
+
export const activeAppTheme: Observable<AppThemeConfig | null> = new Observable<AppThemeConfig | null>(null);
|
|
75
|
+
|
|
76
|
+
/** Registry of all registered AppThemes */
|
|
77
|
+
const appThemeRegistry: Map<string, AppThemeConfig> = new Map();
|
|
78
|
+
|
|
79
|
+
/** Register an AppTheme so it can be activated by name */
|
|
80
|
+
export function registerAppTheme(config: AppThemeConfig): void {
|
|
81
|
+
appThemeRegistry.set(config.name, config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Get a registered AppTheme by name */
|
|
85
|
+
export function getAppTheme(name: string): AppThemeConfig | undefined {
|
|
86
|
+
return appThemeRegistry.get(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get all registered AppTheme names */
|
|
90
|
+
export function getAppThemeNames(): string[] {
|
|
91
|
+
return Array.from(appThemeRegistry.keys());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Activate an AppTheme by name.
|
|
96
|
+
* - Applies its color palette
|
|
97
|
+
* - Switches component implementations listed in the config
|
|
98
|
+
* - Calls onActivate callback
|
|
99
|
+
*/
|
|
100
|
+
export async function activateAppTheme(name: string): Promise<void> {
|
|
101
|
+
const config = appThemeRegistry.get(name);
|
|
102
|
+
if (!config) throw new Error(`[Theme]: AppTheme "${name}" not registered`);
|
|
103
|
+
|
|
104
|
+
// Deactivate current AppTheme if any
|
|
105
|
+
await deactivateAppTheme();
|
|
106
|
+
|
|
107
|
+
// Apply palette (guarded to avoid deactivation inside setTheme)
|
|
108
|
+
_internalThemeSwitch = true;
|
|
109
|
+
await setTheme(config.palette);
|
|
110
|
+
_internalThemeSwitch = false;
|
|
111
|
+
ACTIVE_THEME_KEY = config.palette;
|
|
112
|
+
|
|
113
|
+
// Switch implementations
|
|
114
|
+
if (config.implementations) {
|
|
115
|
+
// Dynamic import to avoid circular dependency
|
|
116
|
+
const { Placeholder } = await import('./Injection.js');
|
|
117
|
+
for (const [placeholder, impl] of config.implementations) {
|
|
118
|
+
try {
|
|
119
|
+
await Placeholder.switchTo(placeholder, impl);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.warn(`[Theme]: Failed to switch "${placeholder}" to "${impl}":`, e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set active
|
|
127
|
+
activeAppTheme.setObject(config);
|
|
128
|
+
localStorage.setItem('appTheme', name);
|
|
129
|
+
|
|
130
|
+
// Call activate callback
|
|
131
|
+
config.onActivate?.();
|
|
132
|
+
|
|
133
|
+
console.info(`[Theme]: AppTheme "${name}" activated`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Deactivate the currently active AppTheme.
|
|
138
|
+
* - Calls onDeactivate callback
|
|
139
|
+
* - Reverts implementations to defaults
|
|
140
|
+
* - Does NOT change the color palette (that's separate)
|
|
141
|
+
*/
|
|
142
|
+
export async function deactivateAppTheme(): Promise<void> {
|
|
143
|
+
const current = activeAppTheme.getObject();
|
|
144
|
+
if (!current) return;
|
|
145
|
+
|
|
146
|
+
// Call deactivate callback
|
|
147
|
+
current.onDeactivate?.();
|
|
148
|
+
|
|
149
|
+
// Revert implementations to defaults
|
|
150
|
+
if (current.implementations) {
|
|
151
|
+
const { Placeholder } = await import('./Injection.js');
|
|
152
|
+
for (const [placeholder] of current.implementations) {
|
|
153
|
+
try {
|
|
154
|
+
const p = Placeholder.get(placeholder);
|
|
155
|
+
const firstImpl = p.implementations.entries().next().value;
|
|
156
|
+
if (firstImpl) {
|
|
157
|
+
await Placeholder.switchTo(placeholder, firstImpl[0]);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.warn(`[Theme]: Failed to revert "${placeholder}":`, e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
activeAppTheme.setObject(null);
|
|
166
|
+
localStorage.removeItem('appTheme');
|
|
167
|
+
|
|
168
|
+
console.info(`[Theme]: AppTheme "${current.name}" deactivated`);
|
|
169
|
+
}
|
|
170
|
+
|