@cedx/base 0.2.0 → 0.3.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 CHANGED
@@ -1,6 +1,5 @@
1
1
  # Belin.io Base
2
- ![.NET](https://badgen.net/badge/.net/%3E%3D9.0/green) ![NuGet](https://badgen.net/nuget/v/Belin.Base) ![License](https://badgen.net/badge/license/MIT/blue) ![Downloads](https://badgen.net/nuget/dt/Belin.Base)
3
- ![Node.js](https://badgen.net/npm/node/@cedx/base) ![npm](https://badgen.net/npm/v/@cedx/base) ![License](https://badgen.net/npm/license/@cedx/base) ![Downloads](https://badgen.net/npm/dt/@cedx/base)
2
+ ![.NET](https://badgen.net/badge/.net/%3E%3D9.0/green) ![Version](https://badgen.net/badge/project/v0.3.0/blue) ![Licence](https://badgen.net/badge/licence/MIT/blue)
4
3
 
5
4
  Base library by [Cédric Belin](https://belin.io), full stack developer,
6
5
  implemented in [C#](https://learn.microsoft.com/en-us/dotnet/csharp) and [TypeScript](https://www.typescriptlang.org).
@@ -21,14 +21,14 @@ export declare const AppTheme: Readonly<{
21
21
  export type AppTheme = typeof AppTheme[keyof typeof AppTheme];
22
22
  /**
23
23
  * Gets the icon corresponding to the specified theme.
24
- * @param theme The theme mode.
24
+ * @param theme The application theme.
25
25
  * @returns The icon corresponding to the specified theme.
26
26
  */
27
- export declare function themeIcon(theme: AppTheme): string;
27
+ export declare function getIcon(theme: AppTheme): string;
28
28
  /**
29
29
  * Gets the label corresponding to the specified theme.
30
- * @param theme The theme mode.
30
+ * @param theme The application theme.
31
31
  * @returns The label corresponding to the specified theme.
32
32
  */
33
- export declare function themeLabel(theme: AppTheme): string;
33
+ export declare function getLabel(theme: AppTheme): string;
34
34
  //# sourceMappingURL=AppTheme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppTheme.d.ts","sourceRoot":"","sources":["../../src/Client/Html/AppTheme.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,QAAQ;IAEpB;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;EAEF,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,MAAM,OAAO,QAAQ,CAAC,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAMjD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAMlD"}
1
+ {"version":3,"file":"AppTheme.d.ts","sourceRoot":"","sources":["../../src/Client/Html/AppTheme.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,QAAQ;IAEpB;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;EAEF,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,MAAM,OAAO,QAAQ,CAAC,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAM/C;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAMhD"}
@@ -17,10 +17,10 @@ export const AppTheme = Object.freeze({
17
17
  });
18
18
  /**
19
19
  * Gets the icon corresponding to the specified theme.
20
- * @param theme The theme mode.
20
+ * @param theme The application theme.
21
21
  * @returns The icon corresponding to the specified theme.
22
22
  */
23
- export function themeIcon(theme) {
23
+ export function getIcon(theme) {
24
24
  switch (theme) {
25
25
  case AppTheme.Dark: return "dark_mode";
26
26
  case AppTheme.Light: return "light_mode";
@@ -29,10 +29,10 @@ export function themeIcon(theme) {
29
29
  }
30
30
  /**
31
31
  * Gets the label corresponding to the specified theme.
32
- * @param theme The theme mode.
32
+ * @param theme The application theme.
33
33
  * @returns The label corresponding to the specified theme.
34
34
  */
35
- export function themeLabel(theme) {
35
+ export function getLabel(theme) {
36
36
  switch (theme) {
37
37
  case AppTheme.Dark: return "Sombre";
38
38
  case AppTheme.Light: return "Clair";
@@ -4,10 +4,6 @@ import type { ILoadingIndicator } from "#Abstractions/ILoadingIndicator.js";
4
4
  */
5
5
  export declare class LoadingIndicator extends HTMLElement implements ILoadingIndicator {
6
6
  #private;
7
- /**
8
- * Creates a new loading indicator.
9
- */
10
- constructor();
11
7
  /**
12
8
  * Starts the loading indicator.
13
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingIndicator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/LoadingIndicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oCAAoC,CAAC;AAE1E;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAY,YAAW,iBAAiB;;IAO7E;;OAEG;;IAcH;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,IAAI,CAAC,OAAO,GAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAM,GAAG,IAAI;CAQ3C;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
1
+ {"version":3,"file":"LoadingIndicator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/LoadingIndicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oCAAoC,CAAC;AAE1E;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAY,YAAW,iBAAiB;;IAc7E;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,IAAI,CAAC,OAAO,GAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAM,GAAG,IAAI;CAQ3C;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
@@ -6,14 +6,6 @@ export class LoadingIndicator extends HTMLElement {
6
6
  * The number of concurrent HTTP requests.
7
7
  */
8
8
  #requestCount = 0;
9
- /**
10
- * Creates a new loading indicator.
11
- */
12
- constructor() {
13
- super();
14
- this.hidden = true;
15
- this.attachShadow({ mode: "open" }).appendChild(document.createElement("slot"));
16
- }
17
9
  /**
18
10
  * Registers the component.
19
11
  */
@@ -2,10 +2,7 @@
2
2
  * A component that activates the items of a menu based on the current document URL.
3
3
  */
4
4
  export declare class MenuActivator extends HTMLElement {
5
- /**
6
- * Creates a new menu activator.
7
- */
8
- constructor();
5
+ #private;
9
6
  /**
10
7
  * Method invoked when this component is connected.
11
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MenuActivator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/MenuActivator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;IAE7C;;OAEG;;IAaH;;OAEG;IACH,iBAAiB,IAAI,IAAI;CASzB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
1
+ {"version":3,"file":"MenuActivator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/MenuActivator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;;IAc7C;;OAEG;IACH,iBAAiB,IAAI,IAAI;CAQzB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
@@ -3,12 +3,9 @@
3
3
  */
4
4
  export class MenuActivator extends HTMLElement {
5
5
  /**
6
- * Creates a new menu activator.
6
+ * The root element.
7
7
  */
8
- constructor() {
9
- super();
10
- this.attachShadow({ mode: "open" }).appendChild(document.createElement("slot"));
11
- }
8
+ #root = this.firstElementChild;
12
9
  /**
13
10
  * Registers the component.
14
11
  */
@@ -19,14 +16,12 @@ export class MenuActivator extends HTMLElement {
19
16
  * Method invoked when this component is connected.
20
17
  */
21
18
  connectedCallback() {
22
- const menu = this.shadowRoot.querySelector("slot").assignedElements().at(0);
23
- if (menu)
24
- for (const anchor of menu.getElementsByTagName("a"))
25
- if (anchor.href != location.href)
26
- anchor.classList.remove("active");
27
- else {
28
- anchor.classList.add("active");
29
- anchor.closest(".nav-item.dropdown")?.querySelector(".nav-link")?.classList.add("active");
30
- }
19
+ for (const anchor of this.#root.getElementsByTagName("a"))
20
+ if (anchor.href != location.href)
21
+ anchor.classList.remove("active");
22
+ else {
23
+ anchor.classList.add("active");
24
+ anchor.closest(".nav-item.dropdown")?.querySelector(".nav-link")?.classList.add("active");
25
+ }
31
26
  }
32
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineIndicator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/OfflineIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IAEhD;;OAEG;;IAcH;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,WAAW,IAAI,IAAI;CAGnB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
1
+ {"version":3,"file":"OfflineIndicator.d.ts","sourceRoot":"","sources":["../../src/Client/UI/OfflineIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IAEhD;;OAEG;;IAaH;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,WAAW,IAAI,IAAI;CAGnB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
@@ -8,7 +8,6 @@ export class OfflineIndicator extends HTMLElement {
8
8
  constructor() {
9
9
  super();
10
10
  this.hidden = navigator.onLine;
11
- this.attachShadow({ mode: "open" }).appendChild(document.createElement("slot"));
12
11
  }
13
12
  /**
14
13
  * Registers the component.
@@ -1,36 +1,12 @@
1
- import { AppTheme } from "#Html/AppTheme.js";
2
- import { MenuAlignment } from "#Html/MenuAlignment.js";
3
- import { LitElement, type TemplateResult } from "lit";
4
1
  /**
5
- * A dropdown menu for switching the color mode.
2
+ * A dropdown menu for switching the application theme.
6
3
  */
7
- export declare class ThemeDropdown extends LitElement {
4
+ export declare class ThemeDropdown extends HTMLElement {
8
5
  #private;
9
- /**
10
- * The alignment of the dropdown menu.
11
- */
12
- alignment: MenuAlignment;
13
- /**
14
- * The label of the dropdown menu.
15
- */
16
- label: string;
17
- /**
18
- * The key of the storage entry providing the saved theme.
19
- */
20
- storageKey: string;
21
- /**
22
- * The current application theme.
23
- */
24
- private appTheme;
25
6
  /**
26
7
  * Creates a new theme dropdown.
27
8
  */
28
9
  constructor();
29
- /**
30
- * The current theme mode.
31
- */
32
- get theme(): AppTheme;
33
- set theme(value: AppTheme);
34
10
  /**
35
11
  * Method invoked when this component is connected.
36
12
  */
@@ -43,16 +19,6 @@ export declare class ThemeDropdown extends LitElement {
43
19
  * Handles the events.
44
20
  */
45
21
  handleEvent(): void;
46
- /**
47
- * Returns the node into which this component should render.
48
- * @returns The node into which this component should render.
49
- */
50
- protected createRenderRoot(): DocumentFragment | HTMLElement;
51
- /**
52
- * Renders this component.
53
- * @returns The view template.
54
- */
55
- protected render(): TemplateResult;
56
22
  }
57
23
  /**
58
24
  * Declaration merging.
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../src/Client/UI/ThemeDropdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAwB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAC,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAO,UAAU,EAAE,KAAK,cAAc,EAAC,MAAM,KAAK,CAAC;AAK1D;;GAEG;AACH,qBACa,aAAc,SAAQ,UAAU;;IAE5C;;OAEG;IACS,SAAS,EAAE,aAAa,CAAqB;IAEzD;;OAEG;IACS,KAAK,SAAM;IAEvB;;OAEG;IACS,UAAU,SAAc;IAEpC;;OAEG;IACM,OAAO,CAAC,QAAQ,CAAW;IAOpC;;OAEG;;IAOH;;OAEG;IACH,IAAI,KAAK,IAAI,QAAQ,CAA0B;IAC/C,IAAI,KAAK,CAAC,KAAK,EAAE,QAAQ,EAGxB;IAED;;OAEG;IACM,iBAAiB,IAAI,IAAI;IAMlC;;OAEG;IACM,oBAAoB,IAAI,IAAI;IAKrC;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;;OAGG;cACgB,gBAAgB,IAAI,gBAAgB,GAAC,WAAW;IAInE;;;OAGG;cACgB,MAAM,IAAI,cAAc;CA4B3C;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
1
+ {"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../src/Client/UI/ThemeDropdown.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;;IAsB7C;;OAEG;;IAeH;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAKzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,WAAW,IAAI,IAAI;CA2BnB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
@@ -1,64 +1,52 @@
1
- import { __decorate } from "tslib";
2
- import { AppTheme, themeIcon, themeLabel } from "#Html/AppTheme.js";
3
- import { MenuAlignment } from "#Html/MenuAlignment.js";
4
- import { html, LitElement } from "lit";
5
- import { customElement, property, state } from "lit/decorators.js";
6
- import { classMap } from "lit/directives/class-map.js";
7
- import { when } from "lit/directives/when.js";
1
+ import { AppTheme, getIcon } from "#Html/AppTheme.js";
8
2
  /**
9
- * A dropdown menu for switching the color mode.
3
+ * A dropdown menu for switching the application theme.
10
4
  */
11
- let ThemeDropdown = class ThemeDropdown extends LitElement {
5
+ export class ThemeDropdown extends HTMLElement {
12
6
  /**
13
- * The media query used to check the system theme.
7
+ * The media query used to check the application theme.
14
8
  */
15
- #mediaQuery;
9
+ #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
10
+ /**
11
+ * The root element.
12
+ */
13
+ #root = this.firstElementChild;
14
+ /**
15
+ * The key of the storage entry providing the saved theme mode.
16
+ */
17
+ #storageKey = this.getAttribute("storageKey") ?? "AppTheme";
18
+ /**
19
+ * The current application theme.
20
+ */
21
+ #theme;
16
22
  /**
17
23
  * Creates a new theme dropdown.
18
24
  */
19
25
  constructor() {
20
26
  super();
21
- /**
22
- * The alignment of the dropdown menu.
23
- */
24
- this.alignment = MenuAlignment.End;
25
- /**
26
- * The label of the dropdown menu.
27
- */
28
- this.label = "";
29
- /**
30
- * The key of the storage entry providing the saved theme.
31
- */
32
- this.storageKey = "AppTheme";
33
- /**
34
- * The media query used to check the system theme.
35
- */
36
- this.#mediaQuery = matchMedia("(prefers-color-scheme: dark)");
37
- const theme = localStorage.getItem(this.storageKey);
38
- this.appTheme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
27
+ const theme = localStorage.getItem(this.#storageKey);
28
+ this.#theme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
29
+ for (const button of this.#root.querySelectorAll("button"))
30
+ button.addEventListener("click", this.#setTheme.bind(this));
39
31
  }
40
32
  /**
41
- * The current theme mode.
33
+ * Registers the component.
42
34
  */
43
- get theme() { return this.appTheme; }
44
- set theme(value) {
45
- localStorage.setItem(this.storageKey, this.appTheme = value);
46
- this.#applyTheme();
35
+ static {
36
+ customElements.define("theme-dropdown", this);
47
37
  }
48
38
  /**
49
39
  * Method invoked when this component is connected.
50
40
  */
51
41
  connectedCallback() {
52
- super.connectedCallback();
53
- this.#applyTheme();
54
42
  this.#mediaQuery.addEventListener("change", this);
43
+ this.#applyTheme();
55
44
  }
56
45
  /**
57
46
  * Method invoked when this component is disconnected.
58
47
  */
59
48
  disconnectedCallback() {
60
49
  this.#mediaQuery.removeEventListener("change", this);
61
- super.disconnectedCallback();
62
50
  }
63
51
  /**
64
52
  * Handles the events.
@@ -66,58 +54,25 @@ let ThemeDropdown = class ThemeDropdown extends LitElement {
66
54
  handleEvent() {
67
55
  this.#applyTheme();
68
56
  }
69
- /**
70
- * Returns the node into which this component should render.
71
- * @returns The node into which this component should render.
72
- */
73
- createRenderRoot() {
74
- return this;
75
- }
76
- /**
77
- * Renders this component.
78
- * @returns The view template.
79
- */
80
- render() {
81
- return html `
82
- <li class="nav-item dropdown">
83
- <a class="dropdown-toggle nav-link" data-bs-toggle="dropdown" href="#">
84
- <i class="icon icon-fill">${themeIcon(this.appTheme)}</i>
85
- ${when(this.label, () => html `<span class="ms-1">${this.label}</span>`)}
86
- </a>
87
- <ul class="dropdown-menu ${classMap({ "dropdown-menu-end": this.alignment == MenuAlignment.End })}">
88
- ${Object.values(AppTheme).map(value => html `
89
- <li>
90
- <button class="dropdown-item d-flex align-items-center justify-content-between" @click=${() => this.theme = value}>
91
- <span><i class="icon icon-fill me-1">${themeIcon(value)}</i> ${themeLabel(value)}</span>
92
- ${when(value == this.appTheme, () => html `<i class="icon ms-2">check</i>`)}
93
- </button>
94
- </li>
95
- `)}
96
- </ul>
97
- </li>
98
- `;
99
- }
100
57
  /**
101
58
  * Applies the theme to the document.
102
59
  */
103
60
  #applyTheme() {
104
- const theme = this.appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.appTheme;
61
+ const theme = this.#theme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.#theme;
105
62
  document.documentElement.dataset.bsTheme = theme.toLowerCase();
63
+ this.#root.querySelector(".dropdown-toggle > .icon").textContent = getIcon(this.#theme);
64
+ const checkIcon = this.#root.querySelector(".dropdown-item > .icon");
65
+ checkIcon.remove();
66
+ const activeButton = this.#root.querySelector(`button[data-theme="${this.#theme}"]`);
67
+ activeButton.appendChild(checkIcon);
68
+ }
69
+ /**
70
+ * Changes the current theme.
71
+ * @param event The dispatched event.
72
+ */
73
+ #setTheme(event) {
74
+ const button = event.target.closest("button");
75
+ localStorage.setItem(this.#storageKey, this.#theme = button.dataset.theme);
76
+ this.#applyTheme();
106
77
  }
107
- };
108
- __decorate([
109
- property()
110
- ], ThemeDropdown.prototype, "alignment", void 0);
111
- __decorate([
112
- property()
113
- ], ThemeDropdown.prototype, "label", void 0);
114
- __decorate([
115
- property()
116
- ], ThemeDropdown.prototype, "storageKey", void 0);
117
- __decorate([
118
- state()
119
- ], ThemeDropdown.prototype, "appTheme", void 0);
120
- ThemeDropdown = __decorate([
121
- customElement("theme-dropdown")
122
- ], ThemeDropdown);
123
- export { ThemeDropdown };
78
+ }
package/package.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "name": "@cedx/base",
8
8
  "repository": "cedx/base",
9
9
  "type": "module",
10
- "version": "0.2.0",
10
+ "version": "0.3.0",
11
11
  "devDependencies": {
12
12
  "@playwright/browser-chromium": "^1.54.2",
13
13
  "@types/bootstrap": "^5.2.10",
14
14
  "@types/chai": "^5.2.2",
15
15
  "@types/mocha": "^10.0.10",
16
- "@types/node": "^24.2.0",
16
+ "@types/node": "^24.2.1",
17
17
  "@types/serve-handler": "^6.1.4",
18
18
  "chai": "^5.2.1",
19
19
  "esbuild": "^0.25.8",
@@ -51,7 +51,6 @@
51
51
  ],
52
52
  "peerDependencies": {
53
53
  "bootstrap": ">=5.3.0",
54
- "lit": ">=3.3.0",
55
54
  "tslib": ">=2.8.0"
56
55
  },
57
56
  "publishConfig": {
@@ -26,10 +26,10 @@ export type AppTheme = typeof AppTheme[keyof typeof AppTheme];
26
26
 
27
27
  /**
28
28
  * Gets the icon corresponding to the specified theme.
29
- * @param theme The theme mode.
29
+ * @param theme The application theme.
30
30
  * @returns The icon corresponding to the specified theme.
31
31
  */
32
- export function themeIcon(theme: AppTheme): string {
32
+ export function getIcon(theme: AppTheme): string {
33
33
  switch (theme) {
34
34
  case AppTheme.Dark: return "dark_mode";
35
35
  case AppTheme.Light: return "light_mode";
@@ -39,10 +39,10 @@ export function themeIcon(theme: AppTheme): string {
39
39
 
40
40
  /**
41
41
  * Gets the label corresponding to the specified theme.
42
- * @param theme The theme mode.
42
+ * @param theme The application theme.
43
43
  * @returns The label corresponding to the specified theme.
44
44
  */
45
- export function themeLabel(theme: AppTheme): string {
45
+ export function getLabel(theme: AppTheme): string {
46
46
  switch (theme) {
47
47
  case AppTheme.Dark: return "Sombre";
48
48
  case AppTheme.Light: return "Clair";
@@ -44,7 +44,7 @@ export class ViewportScroller {
44
44
  this.#scrollOffset += Number.isNaN(navbarHeight) ? 0 : navbarHeight;
45
45
  }
46
46
 
47
- const actionBar: HTMLElement|null = document.body.querySelector("action-bar");
47
+ const actionBar = document.body.querySelector<HTMLElement>("action-bar");
48
48
  return this.#scrollOffset + (actionBar?.offsetHeight ?? 0);
49
49
  }
50
50
 
@@ -10,15 +10,6 @@ export class LoadingIndicator extends HTMLElement implements ILoadingIndicator {
10
10
  */
11
11
  #requestCount = 0;
12
12
 
13
- /**
14
- * Creates a new loading indicator.
15
- */
16
- constructor() {
17
- super();
18
- this.hidden = true;
19
- this.attachShadow({mode: "open"}).appendChild(document.createElement("slot"));
20
- }
21
-
22
13
  /**
23
14
  * Registers the component.
24
15
  */
@@ -4,12 +4,9 @@
4
4
  export class MenuActivator extends HTMLElement {
5
5
 
6
6
  /**
7
- * Creates a new menu activator.
7
+ * The root element.
8
8
  */
9
- constructor() {
10
- super();
11
- this.attachShadow({mode: "open"}).appendChild(document.createElement("slot"));
12
- }
9
+ readonly #root = this.firstElementChild!;
13
10
 
14
11
  /**
15
12
  * Registers the component.
@@ -22,8 +19,7 @@ export class MenuActivator extends HTMLElement {
22
19
  * Method invoked when this component is connected.
23
20
  */
24
21
  connectedCallback(): void {
25
- const menu = this.shadowRoot!.querySelector("slot")!.assignedElements().at(0);
26
- if (menu) for (const anchor of menu.getElementsByTagName("a"))
22
+ for (const anchor of this.#root.getElementsByTagName("a"))
27
23
  if (anchor.href != location.href) anchor.classList.remove("active");
28
24
  else {
29
25
  anchor.classList.add("active");
@@ -9,7 +9,6 @@ export class OfflineIndicator extends HTMLElement {
9
9
  constructor() {
10
10
  super();
11
11
  this.hidden = navigator.onLine;
12
- this.attachShadow({mode: "open"}).appendChild(document.createElement("slot"));
13
12
  }
14
13
 
15
14
  /**
@@ -1,74 +1,60 @@
1
- import {AppTheme, themeIcon, themeLabel} from "#Html/AppTheme.js";
2
- import {MenuAlignment} from "#Html/MenuAlignment.js";
3
- import {html, LitElement, type TemplateResult} from "lit";
4
- import {customElement, property, state} from "lit/decorators.js";
5
- import {classMap} from "lit/directives/class-map.js";
6
- import {when} from "lit/directives/when.js";
1
+ import {AppTheme, getIcon} from "#Html/AppTheme.js";
7
2
 
8
3
  /**
9
- * A dropdown menu for switching the color mode.
4
+ * A dropdown menu for switching the application theme.
10
5
  */
11
- @customElement("theme-dropdown")
12
- export class ThemeDropdown extends LitElement {
6
+ export class ThemeDropdown extends HTMLElement {
13
7
 
14
8
  /**
15
- * The alignment of the dropdown menu.
9
+ * The media query used to check the application theme.
16
10
  */
17
- @property() alignment: MenuAlignment = MenuAlignment.End;
11
+ readonly #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
18
12
 
19
13
  /**
20
- * The label of the dropdown menu.
14
+ * The root element.
21
15
  */
22
- @property() label = "";
16
+ readonly #root = this.firstElementChild!;
23
17
 
24
18
  /**
25
- * The key of the storage entry providing the saved theme.
19
+ * The key of the storage entry providing the saved theme mode.
26
20
  */
27
- @property() storageKey = "AppTheme";
21
+ readonly #storageKey = this.getAttribute("storageKey") ?? "AppTheme";
28
22
 
29
23
  /**
30
24
  * The current application theme.
31
25
  */
32
- @state() private appTheme: AppTheme;
33
-
34
- /**
35
- * The media query used to check the system theme.
36
- */
37
- readonly #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
26
+ #theme: AppTheme;
38
27
 
39
28
  /**
40
29
  * Creates a new theme dropdown.
41
30
  */
42
31
  constructor() {
43
32
  super();
44
- const theme = localStorage.getItem(this.storageKey) as AppTheme;
45
- this.appTheme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
33
+ const theme = localStorage.getItem(this.#storageKey) as AppTheme;
34
+ this.#theme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
35
+ for (const button of this.#root.querySelectorAll("button")) button.addEventListener("click", this.#setTheme.bind(this));
46
36
  }
47
37
 
48
38
  /**
49
- * The current theme mode.
39
+ * Registers the component.
50
40
  */
51
- get theme(): AppTheme { return this.appTheme; }
52
- set theme(value: AppTheme) {
53
- localStorage.setItem(this.storageKey, this.appTheme = value);
54
- this.#applyTheme();
41
+ static {
42
+ customElements.define("theme-dropdown", this);
55
43
  }
56
44
 
57
45
  /**
58
46
  * Method invoked when this component is connected.
59
47
  */
60
- override connectedCallback(): void {
61
- super.connectedCallback();
62
- this.#applyTheme();
48
+ connectedCallback(): void {
63
49
  this.#mediaQuery.addEventListener("change", this);
50
+ this.#applyTheme();
64
51
  }
65
52
 
66
53
  /**
67
54
  * Method invoked when this component is disconnected.
68
55
  */
69
- override disconnectedCallback(): void {
56
+ disconnectedCallback(): void {
70
57
  this.#mediaQuery.removeEventListener("change", this);
71
- super.disconnectedCallback();
72
58
  }
73
59
 
74
60
  /**
@@ -79,44 +65,27 @@ export class ThemeDropdown extends LitElement {
79
65
  }
80
66
 
81
67
  /**
82
- * Returns the node into which this component should render.
83
- * @returns The node into which this component should render.
68
+ * Applies the theme to the document.
84
69
  */
85
- protected override createRenderRoot(): DocumentFragment|HTMLElement {
86
- return this;
87
- }
70
+ #applyTheme(): void {
71
+ const theme = this.#theme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.#theme;
72
+ document.documentElement.dataset.bsTheme = theme.toLowerCase();
73
+ this.#root.querySelector(".dropdown-toggle > .icon")!.textContent = getIcon(this.#theme);
88
74
 
89
- /**
90
- * Renders this component.
91
- * @returns The view template.
92
- */
93
- protected override render(): TemplateResult {
94
- return html`
95
- <li class="nav-item dropdown">
96
- <a class="dropdown-toggle nav-link" data-bs-toggle="dropdown" href="#">
97
- <i class="icon icon-fill">${themeIcon(this.appTheme)}</i>
98
- ${when(this.label, () => html`<span class="ms-1">${this.label}</span>`)}
99
- </a>
100
- <ul class="dropdown-menu ${classMap({"dropdown-menu-end": this.alignment == MenuAlignment.End})}">
101
- ${Object.values(AppTheme).map(value => html`
102
- <li>
103
- <button class="dropdown-item d-flex align-items-center justify-content-between" @click=${() => this.theme = value}>
104
- <span><i class="icon icon-fill me-1">${themeIcon(value)}</i> ${themeLabel(value)}</span>
105
- ${when(value == this.appTheme, () => html`<i class="icon ms-2">check</i>`)}
106
- </button>
107
- </li>
108
- `)}
109
- </ul>
110
- </li>
111
- `;
75
+ const checkIcon = this.#root.querySelector(".dropdown-item > .icon")!;
76
+ checkIcon.remove();
77
+ const activeButton = this.#root.querySelector(`button[data-theme="${this.#theme}"]`)!
78
+ activeButton.appendChild(checkIcon);
112
79
  }
113
80
 
114
81
  /**
115
- * Applies the theme to the document.
82
+ * Changes the current theme.
83
+ * @param event The dispatched event.
116
84
  */
117
- #applyTheme(): void {
118
- const theme = this.appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.appTheme;
119
- document.documentElement.dataset.bsTheme = theme.toLowerCase();
85
+ #setTheme(event: Event): void {
86
+ const button = (event.target as HTMLElement).closest("button")!;
87
+ localStorage.setItem(this.#storageKey, this.#theme = button.dataset.theme as AppTheme);
88
+ this.#applyTheme();
120
89
  }
121
90
  }
122
91