@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":"AAEA,eAAO,IAAI,gBAAgB,QAAU,CAAC;AAGtC,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,iBAkB1C"}
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"}
@@ -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;AAEnC,MAAM,CAAC,IAAI,gBAAgB,GAAG,OAAO,CAAC;AACtC,IAAI,gBAAgB,GAAyB,IAAI,CAAC;AAElD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IACxC,4GAA4G;IAC5G,OAAO,OAAO,CAAC,SAAS,CAAC,cAAc,IAAI,YAAY,CAAC,CAAC;AAC7D,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY;IACjD,IAAI,KAAK,GAAW,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;IAClC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC;AACjB,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,IAAI;IACtB,gBAAgB,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,gBAAgB,EAAE,CAAC;QACnB,MAAM,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACJ,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;AACL,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACvC,IAAI,KAAK,GAAW,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;IAClC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,gBAAgB,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACpE,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;YACtB,QAAQ,CAAC,kBAAkB,GAAG,MAAM,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,QAAQ,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IAC1E,CAAC;IACD,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC"}
1
+ {"version":3,"file":"Theme.js","sourceRoot":"","sources":["../../src/foundation/Theme.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,UAAU,MAAM,mBAAmB,CAAC;AAE3C,MAAM,CAAC,IAAI,gBAAgB,GAAG,OAAO,CAAC;AACtC,IAAI,gBAAgB,GAAyB,IAAI,CAAC;AAClD,IAAI,oBAAoB,GAAG,KAAK,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IACxC,4GAA4G;IAC5G,OAAO,OAAO,CAAC,SAAS,CAAC,cAAc,IAAI,YAAY,CAAC,CAAC;AAC7D,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY;IACjD,IAAI,KAAK,GAAW,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;IAClC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC;AACjB,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,IAAI;IACtB,gBAAgB,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,gBAAgB,EAAE,CAAC;QACnB,MAAM,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACJ,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;AACL,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACvC,oGAAoG;IACpG,IAAI,CAAC,oBAAoB,IAAI,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC;QACtD,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,EAAG,CAAC;QAC5C,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;QACzB,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC/B,YAAY,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK,GAAW,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;IAClC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,gBAAgB,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACpE,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;YACtB,QAAQ,CAAC,kBAAkB,GAAG,MAAM,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,QAAQ,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IAC1E,CAAC;IACD,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAqBD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAsC,IAAI,UAAU,CAAwB,IAAI,CAAC,CAAC;AAE7G,2CAA2C;AAC3C,MAAM,gBAAgB,GAAgC,IAAI,GAAG,EAAE,CAAC;AAEhE,0DAA0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,MAAsB;IACnD,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,WAAW,CAAC,IAAY;IACpC,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,gBAAgB;IAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IAC/C,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,kBAAkB,CAAC,CAAC;IAE3E,qCAAqC;IACrC,MAAM,kBAAkB,EAAE,CAAC;IAE3B,gEAAgE;IAChE,oBAAoB,GAAG,IAAI,CAAC;IAC5B,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,oBAAoB,GAAG,KAAK,CAAC;IAC7B,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC;IAElC,yBAAyB;IACzB,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QACzB,8CAA8C;QAC9C,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;YACvD,IAAI,CAAC;gBACD,MAAM,WAAW,CAAC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAClD,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACT,OAAO,CAAC,IAAI,CAAC,8BAA8B,WAAW,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAChF,CAAC;QACL,CAAC;IACL,CAAC;IAED,aAAa;IACb,cAAc,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAEvC,yBAAyB;IACzB,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC;IAEtB,OAAO,CAAC,IAAI,CAAC,sBAAsB,IAAI,aAAa,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACpC,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,EAAE,CAAC;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,2BAA2B;IAC3B,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;IAEzB,qCAAqC;IACrC,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC1B,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YAClD,IAAI,CAAC;gBACD,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACvC,MAAM,SAAS,GAAG,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBAC3D,IAAI,SAAS,EAAE,CAAC;oBACZ,MAAM,WAAW,CAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,CAAC;YACL,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACT,OAAO,CAAC,IAAI,CAAC,8BAA8B,WAAW,IAAI,EAAE,CAAC,CAAC,CAAC;YACnE,CAAC;QACL,CAAC;IACL,CAAC;IAED,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,YAAY,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAEpC,OAAO,CAAC,IAAI,CAAC,sBAAsB,OAAO,CAAC,IAAI,eAAe,CAAC,CAAC;AACpE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reidelsaltres/pureper",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Minimal library extracted from the Pureper SPA foundation — utilities and base classes for components/pages.",
5
5
  "type": "module",
6
6
  "main": "out/src/index.js",
@@ -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
+