admin-lte 4.0.4 → 4.1.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/css/adminlte-docs.css +878 -0
  3. package/dist/css/adminlte-docs.css.map +1 -0
  4. package/dist/css/adminlte-docs.min.css +7 -0
  5. package/dist/css/adminlte-docs.min.css.map +1 -0
  6. package/dist/css/adminlte-docs.rtl.css +877 -0
  7. package/dist/css/adminlte-docs.rtl.css.map +1 -0
  8. package/dist/css/adminlte-docs.rtl.min.css +7 -0
  9. package/dist/css/adminlte-docs.rtl.min.css.map +1 -0
  10. package/dist/css/adminlte.css +2 -782
  11. package/dist/css/adminlte.css.map +1 -1
  12. package/dist/css/adminlte.min.css +2 -2
  13. package/dist/css/adminlte.min.css.map +1 -1
  14. package/dist/css/adminlte.rtl.css +2 -782
  15. package/dist/css/adminlte.rtl.css.map +1 -1
  16. package/dist/css/adminlte.rtl.min.css +2 -2
  17. package/dist/css/adminlte.rtl.min.css.map +1 -1
  18. package/dist/js/adminlte.esm.js +1365 -0
  19. package/dist/js/adminlte.esm.js.map +1 -0
  20. package/dist/js/adminlte.esm.min.js +7 -0
  21. package/dist/js/adminlte.esm.min.js.map +1 -0
  22. package/dist/js/adminlte.js +391 -200
  23. package/dist/js/adminlte.js.map +1 -1
  24. package/dist/js/adminlte.min.js +2 -2
  25. package/dist/js/adminlte.min.js.map +1 -1
  26. package/dist/js/types/accessibility.d.ts +46 -0
  27. package/dist/js/types/adminlte.d.ts +9 -0
  28. package/dist/js/types/base-component.d.ts +36 -0
  29. package/dist/js/types/card-widget.d.ts +30 -0
  30. package/dist/js/types/color-mode.d.ts +56 -0
  31. package/dist/js/types/direct-chat.d.ts +19 -0
  32. package/dist/js/types/fullscreen.d.ts +21 -0
  33. package/dist/js/types/layout.d.ts +19 -0
  34. package/dist/js/types/push-menu.d.ts +102 -0
  35. package/dist/js/types/treeview.d.ts +27 -0
  36. package/dist/js/types/util/index.d.ts +42 -0
  37. package/package.json +32 -6
  38. package/src/scss/_app-header.scss +3 -1
  39. package/src/scss/_app-main.scss +4 -0
  40. package/src/scss/_app-sidebar.scss +3 -1
  41. package/src/scss/_bootstrap-overrides.scss +60 -0
  42. package/src/scss/_info-box.scss +3 -1
  43. package/src/scss/_progress-bars.scss +3 -1
  44. package/src/scss/_table.scss +4 -2
  45. package/src/scss/adminlte-docs.scss +25 -0
  46. package/src/scss/adminlte.scss +5 -4
  47. package/src/scss/pages/_login_and_register.scss +3 -1
  48. package/src/ts/adminlte.ts +3 -1
  49. package/src/ts/base-component.ts +84 -0
  50. package/src/ts/card-widget.ts +124 -100
  51. package/src/ts/color-mode.ts +174 -0
  52. package/src/ts/direct-chat.ts +38 -32
  53. package/src/ts/fullscreen.ts +32 -23
  54. package/src/ts/push-menu.ts +69 -36
  55. package/src/ts/treeview.ts +84 -50
  56. package/src/ts/util/index.ts +1 -1
  57. package/src/scss/_bootstrap-variables.scss +0 -1766
@@ -0,0 +1,84 @@
1
+ /**
2
+ * --------------------------------------------
3
+ * @file AdminLTE base-component.ts
4
+ * @description Shared component lifecycle for AdminLTE plugins: a per-element
5
+ * instance registry (getInstance / getOrCreateInstance / dispose) and a
6
+ * consistent custom-event contract, mirroring Bootstrap's component API.
7
+ * @license MIT
8
+ * --------------------------------------------
9
+ */
10
+
11
+ /**
12
+ * element -> (data key -> component instance). WeakMap keys don't prevent
13
+ * garbage collection, so instances die with their elements — important under
14
+ * Hotwired Turbo, which swaps the whole <body> on navigation.
15
+ */
16
+ const componentRegistry = new WeakMap<Element, Map<string, BaseComponent>>()
17
+
18
+ class BaseComponent {
19
+ static get NAME(): string {
20
+ throw new Error('Component subclasses must override the static NAME getter.')
21
+ }
22
+
23
+ static get DATA_KEY(): string {
24
+ return `lte.${this.NAME}`
25
+ }
26
+
27
+ /**
28
+ * Untyped registry lookup. Every component exposes a typed wrapper
29
+ * (e.g. CardWidget.getInstance()) built on top of this.
30
+ */
31
+ protected static _getInstance(element: Element | null | undefined): BaseComponent | null {
32
+ if (!element) {
33
+ return null
34
+ }
35
+
36
+ return componentRegistry.get(element)?.get(this.DATA_KEY) ?? null
37
+ }
38
+
39
+ _element: HTMLElement
40
+
41
+ constructor(element: HTMLElement) {
42
+ this._element = element
43
+
44
+ const instances = componentRegistry.get(element) ?? new Map<string, BaseComponent>()
45
+ componentRegistry.set(element, instances)
46
+ instances.set((this.constructor as typeof BaseComponent).DATA_KEY, this)
47
+ }
48
+
49
+ /**
50
+ * Remove this instance from the registry so getInstance() no longer
51
+ * returns it. Subclasses release their own resources, then call
52
+ * super.dispose().
53
+ */
54
+ dispose(): void {
55
+ const instances = componentRegistry.get(this._element)
56
+ instances?.delete((this.constructor as typeof BaseComponent).DATA_KEY)
57
+
58
+ if (instances?.size === 0) {
59
+ componentRegistry.delete(this._element)
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Dispatch a namespaced custom event that bubbles — so applications can
66
+ * listen once on `document` — and can optionally carry a payload or be
67
+ * canceled. Returns the event so callers can check `defaultPrevented`.
68
+ */
69
+ const dispatchCustomEvent = <T = undefined>(
70
+ element: Element,
71
+ name: string,
72
+ options: { cancelable?: boolean; detail?: T } = {}
73
+ ): CustomEvent<T | undefined> => {
74
+ const event = new CustomEvent<T | undefined>(name, {
75
+ bubbles: true,
76
+ cancelable: options.cancelable ?? false,
77
+ detail: options.detail
78
+ })
79
+
80
+ element.dispatchEvent(event)
81
+ return event
82
+ }
83
+
84
+ export { BaseComponent, dispatchCustomEvent }
@@ -6,8 +6,8 @@
6
6
  * --------------------------------------------
7
7
  */
8
8
 
9
+ import { BaseComponent, dispatchCustomEvent } from './base-component'
9
10
  import {
10
- onDOMContentLoaded,
11
11
  slideUp,
12
12
  slideDown
13
13
  } from './util/index'
@@ -17,11 +17,18 @@ import {
17
17
  * ====================================================
18
18
  */
19
19
 
20
- const DATA_KEY = 'lte.card-widget'
21
- const EVENT_KEY = `.${DATA_KEY}`
20
+ const NAME = 'card-widget'
21
+ const EVENT_KEY = `.lte.${NAME}`
22
+
23
+ // Cancelable "before" events, dispatched on the card when an action starts.
24
+ const EVENT_COLLAPSE = `collapse${EVENT_KEY}`
25
+ const EVENT_EXPAND = `expand${EVENT_KEY}`
26
+ const EVENT_REMOVE = `remove${EVENT_KEY}`
27
+
28
+ // "After" events, dispatched on the card when the action has completed.
22
29
  const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
23
30
  const EVENT_EXPANDED = `expanded${EVENT_KEY}`
24
- const EVENT_REMOVE = `remove${EVENT_KEY}`
31
+ const EVENT_REMOVED = `removed${EVENT_KEY}`
25
32
  const EVENT_MAXIMIZED = `maximized${EVENT_KEY}`
26
33
  const EVENT_MINIMIZED = `minimized${EVENT_KEY}`
27
34
 
@@ -53,14 +60,24 @@ const Default: Config = {
53
60
  maximizeTrigger: SELECTOR_DATA_MAXIMIZE
54
61
  }
55
62
 
56
- class CardWidget {
57
- _element: HTMLElement
63
+ class CardWidget extends BaseComponent {
64
+ static get NAME(): string {
65
+ return NAME
66
+ }
67
+
68
+ static getInstance(element: Element | null | undefined): CardWidget | null {
69
+ return this._getInstance(element) as CardWidget | null
70
+ }
71
+
72
+ static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): CardWidget {
73
+ return this.getInstance(element) ?? new this(element, config)
74
+ }
75
+
58
76
  _parent: HTMLElement | undefined
59
- _clone: HTMLElement | undefined
60
77
  _config: Config
61
78
 
62
- constructor(element: HTMLElement, config: Config) {
63
- this._element = element
79
+ constructor(element: HTMLElement, config: Partial<Config> = {}) {
80
+ super(element)
64
81
  this._parent = element.closest(SELECTOR_CARD) as HTMLElement | undefined
65
82
 
66
83
  if (element.classList.contains(CLASS_NAME_CARD)) {
@@ -71,76 +88,87 @@ class CardWidget {
71
88
  }
72
89
 
73
90
  collapse() {
74
- const event = new Event(EVENT_COLLAPSED)
91
+ if (!this._parent) {
92
+ return
93
+ }
75
94
 
76
- if (this._parent) {
77
- this._parent.classList.add(CLASS_NAME_COLLAPSING)
78
- this._parent.classList.remove(CLASS_NAME_EXPANDING)
95
+ if (dispatchCustomEvent(this._parent, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
96
+ return
97
+ }
79
98
 
80
- // Only target direct children to avoid affecting nested cards
81
- const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
99
+ this._parent.classList.add(CLASS_NAME_COLLAPSING)
100
+ this._parent.classList.remove(CLASS_NAME_EXPANDING)
82
101
 
83
- elm.forEach(el => {
84
- if (el instanceof HTMLElement) {
85
- slideUp(el, this._config.animationSpeed)
86
- }
87
- })
102
+ // Only target direct children to avoid affecting nested cards
103
+ const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
88
104
 
89
- setTimeout(() => {
90
- // Guard against a stale timer: if expand() ran while this collapse
91
- // was still animating, the card must not end up marked collapsed.
92
- if (this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
93
- this._parent.classList.add(CLASS_NAME_COLLAPSED)
94
- this._parent.classList.remove(CLASS_NAME_COLLAPSING)
95
- }
96
- }, this._config.animationSpeed)
97
- }
105
+ elm.forEach(el => {
106
+ if (el instanceof HTMLElement) {
107
+ slideUp(el, this._config.animationSpeed)
108
+ }
109
+ })
98
110
 
99
- this._element?.dispatchEvent(event)
111
+ setTimeout(() => {
112
+ // Guard against a stale timer: if expand() ran while this collapse
113
+ // was still animating, the card must not end up marked collapsed.
114
+ if (this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
115
+ this._parent.classList.add(CLASS_NAME_COLLAPSED)
116
+ this._parent.classList.remove(CLASS_NAME_COLLAPSING)
117
+ dispatchCustomEvent(this._parent, EVENT_COLLAPSED)
118
+ }
119
+ }, this._config.animationSpeed)
100
120
  }
101
121
 
102
122
  expand() {
103
- const event = new Event(EVENT_EXPANDED)
123
+ if (!this._parent) {
124
+ return
125
+ }
104
126
 
105
- if (this._parent) {
106
- this._parent.classList.add(CLASS_NAME_EXPANDING)
107
- this._parent.classList.remove(CLASS_NAME_COLLAPSING, CLASS_NAME_COLLAPSED)
127
+ if (dispatchCustomEvent(this._parent, EVENT_EXPAND, { cancelable: true }).defaultPrevented) {
128
+ return
129
+ }
108
130
 
109
- // Only target direct children to avoid affecting nested cards
110
- const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
131
+ this._parent.classList.add(CLASS_NAME_EXPANDING)
132
+ this._parent.classList.remove(CLASS_NAME_COLLAPSING, CLASS_NAME_COLLAPSED)
111
133
 
112
- elm.forEach(el => {
113
- if (el instanceof HTMLElement) {
114
- slideDown(el, this._config.animationSpeed)
115
- }
116
- })
134
+ // Only target direct children to avoid affecting nested cards
135
+ const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
117
136
 
118
- setTimeout(() => {
119
- // Same stale-timer guard as collapse(), in the other direction.
120
- if (this._parent?.classList.contains(CLASS_NAME_EXPANDING)) {
121
- this._parent.classList.remove(CLASS_NAME_EXPANDING)
122
- }
123
- }, this._config.animationSpeed)
124
- }
137
+ elm.forEach(el => {
138
+ if (el instanceof HTMLElement) {
139
+ slideDown(el, this._config.animationSpeed)
140
+ }
141
+ })
125
142
 
126
- this._element?.dispatchEvent(event)
143
+ setTimeout(() => {
144
+ // Same stale-timer guard as collapse(), in the other direction.
145
+ if (this._parent?.classList.contains(CLASS_NAME_EXPANDING)) {
146
+ this._parent.classList.remove(CLASS_NAME_EXPANDING)
147
+ dispatchCustomEvent(this._parent, EVENT_EXPANDED)
148
+ }
149
+ }, this._config.animationSpeed)
127
150
  }
128
151
 
129
152
  remove() {
130
- const event = new Event(EVENT_REMOVE)
131
-
132
- if (this._parent) {
133
- const parent = this._parent
134
- slideUp(parent, this._config.animationSpeed)
153
+ if (!this._parent) {
154
+ return
155
+ }
135
156
 
136
- // Actually remove the card once the animation finishes — hiding it
137
- // would keep its form fields submitting and its ids in the document.
138
- setTimeout(() => {
139
- parent.remove()
140
- }, this._config.animationSpeed)
157
+ if (dispatchCustomEvent(this._parent, EVENT_REMOVE, { cancelable: true }).defaultPrevented) {
158
+ return
141
159
  }
142
160
 
143
- this._element?.dispatchEvent(event)
161
+ const parent = this._parent
162
+ slideUp(parent, this._config.animationSpeed)
163
+
164
+ // Actually remove the card once the animation finishes — hiding it
165
+ // would keep its form fields submitting and its ids in the document.
166
+ // `removed` is dispatched just before detachment so it can still bubble.
167
+ setTimeout(() => {
168
+ dispatchCustomEvent(parent, EVENT_REMOVED)
169
+ parent.remove()
170
+ this.dispose()
171
+ }, this._config.animationSpeed)
144
172
  }
145
173
 
146
174
  toggle() {
@@ -155,8 +183,6 @@ class CardWidget {
155
183
  }
156
184
 
157
185
  maximize() {
158
- const event = new Event(EVENT_MAXIMIZED)
159
-
160
186
  if (this._parent) {
161
187
  this._parent.style.height = `${this._parent.offsetHeight}px`
162
188
  this._parent.style.width = `${this._parent.offsetWidth}px`
@@ -175,16 +201,14 @@ class CardWidget {
175
201
  if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {
176
202
  this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED)
177
203
  }
204
+
205
+ dispatchCustomEvent(this._parent, EVENT_MAXIMIZED)
178
206
  }
179
207
  }, 150)
180
208
  }
181
-
182
- this._element?.dispatchEvent(event)
183
209
  }
184
210
 
185
211
  minimize() {
186
- const event = new Event(EVENT_MINIMIZED)
187
-
188
212
  if (this._parent) {
189
213
  this._parent.style.height = 'auto'
190
214
  this._parent.style.width = 'auto'
@@ -204,6 +228,8 @@ class CardWidget {
204
228
  this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED)
205
229
  }
206
230
 
231
+ dispatchCustomEvent(this._parent, EVENT_MINIMIZED)
232
+
207
233
  // Drop the inline sizing/transition once the restore transition has
208
234
  // finished so the card doesn't carry stale styles forever.
209
235
  setTimeout(() => {
@@ -214,8 +240,6 @@ class CardWidget {
214
240
  }
215
241
  }, 10)
216
242
  }
217
-
218
- this._element?.dispatchEvent(event)
219
243
  }
220
244
 
221
245
  toggleMaximize() {
@@ -232,44 +256,44 @@ class CardWidget {
232
256
  *
233
257
  * Data Api implementation
234
258
  * ====================================================
259
+ * A single delegated listener on `document` handles all card toggles — so
260
+ * cards inserted later (AJAX, Turbo Frames) work without re-initialisation,
261
+ * and the listener itself survives Turbo's <body> swaps.
235
262
  */
236
263
 
237
- onDOMContentLoaded(() => {
238
- const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE)
239
-
240
- // Use currentTarget (the [data-lte-toggle] button itself), not target:
241
- // clicking the <i> icon inside the button would otherwise make the widget's
242
- // events fire on the icon instead of the button.
243
- collapseBtn.forEach(btn => {
244
- btn.addEventListener('click', event => {
245
- event.preventDefault()
246
- const target = event.currentTarget as HTMLElement
247
- const data = new CardWidget(target, Default)
248
- data.toggle()
249
- })
250
- })
264
+ document.addEventListener('click', event => {
265
+ const target = event.target
251
266
 
252
- const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE)
267
+ if (!(target instanceof Element)) {
268
+ return
269
+ }
253
270
 
254
- removeBtn.forEach(btn => {
255
- btn.addEventListener('click', event => {
256
- event.preventDefault()
257
- const target = event.currentTarget as HTMLElement
258
- const data = new CardWidget(target, Default)
259
- data.remove()
260
- })
261
- })
271
+ const collapseTrigger = target.closest(SELECTOR_DATA_COLLAPSE)
272
+ const removeTrigger = target.closest(SELECTOR_DATA_REMOVE)
273
+ const maximizeTrigger = target.closest(SELECTOR_DATA_MAXIMIZE)
274
+ const trigger = collapseTrigger ?? removeTrigger ?? maximizeTrigger
262
275
 
263
- const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE)
276
+ if (!trigger) {
277
+ return
278
+ }
264
279
 
265
- maxBtn.forEach(btn => {
266
- btn.addEventListener('click', event => {
267
- event.preventDefault()
268
- const target = event.currentTarget as HTMLElement
269
- const data = new CardWidget(target, Default)
270
- data.toggleMaximize()
271
- })
272
- })
280
+ event.preventDefault()
281
+
282
+ const card = trigger.closest(SELECTOR_CARD) as HTMLElement | null
283
+
284
+ if (!card) {
285
+ return
286
+ }
287
+
288
+ const widget = CardWidget.getOrCreateInstance(card)
289
+
290
+ if (collapseTrigger) {
291
+ widget.toggle()
292
+ } else if (removeTrigger) {
293
+ widget.remove()
294
+ } else {
295
+ widget.toggleMaximize()
296
+ }
273
297
  })
274
298
 
275
299
  export default CardWidget
@@ -0,0 +1,174 @@
1
+ /**
2
+ * --------------------------------------------
3
+ * @file AdminLTE color-mode.ts
4
+ * @description Color mode (light/dark/auto) switcher for AdminLTE.
5
+ * Persists the choice in localStorage, follows the OS preference in
6
+ * "auto" mode, and keeps [data-bs-theme-value] toggles and
7
+ * [data-lte-theme-icon] indicator icons in sync.
8
+ *
9
+ * Ships in the bundle so applications no longer need to copy the demo's
10
+ * inline script. The tiny no-flash snippet in <head> (see _head.astro)
11
+ * remains inline by design — it must run before first paint.
12
+ * @license MIT
13
+ * --------------------------------------------
14
+ */
15
+
16
+ import { getLifecycleSignal, onDOMContentLoaded } from './util/index'
17
+
18
+ /**
19
+ * Constants
20
+ * ====================================================
21
+ */
22
+
23
+ const DATA_KEY = 'lte.color-mode'
24
+ const EVENT_KEY = `.${DATA_KEY}`
25
+ const EVENT_CHANGED = `changed${EVENT_KEY}`
26
+
27
+ const STORAGE_KEY = 'lte-theme'
28
+
29
+ const SELECTOR_TOGGLE = '[data-bs-theme-value]'
30
+ const SELECTOR_ICON = '[data-lte-theme-icon]'
31
+
32
+ type Theme = 'light' | 'dark' | 'auto'
33
+
34
+ /**
35
+ * Class Definition
36
+ * ====================================================
37
+ */
38
+
39
+ class ColorMode {
40
+ /**
41
+ * Read the persisted theme choice, or null when nothing was stored or
42
+ * localStorage is unavailable (private mode, sandboxed iframe).
43
+ */
44
+ getStoredTheme(): Theme | null {
45
+ try {
46
+ const stored = localStorage.getItem(STORAGE_KEY)
47
+ return stored && ['light', 'dark', 'auto'].includes(stored) ? stored as Theme : null
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ /**
54
+ * The user's effective choice: the stored theme, falling back to the OS
55
+ * preference.
56
+ */
57
+ getPreferredTheme(): Theme {
58
+ const stored = this.getStoredTheme()
59
+ if (stored) {
60
+ return stored
61
+ }
62
+
63
+ return this._prefersDark() ? 'dark' : 'light'
64
+ }
65
+
66
+ /**
67
+ * Resolve "auto" against the OS preference.
68
+ */
69
+ resolveTheme(theme: Theme): 'light' | 'dark' {
70
+ if (theme === 'auto') {
71
+ return this._prefersDark() ? 'dark' : 'light'
72
+ }
73
+
74
+ return theme
75
+ }
76
+
77
+ /**
78
+ * Apply a theme and persist the choice. Dispatches
79
+ * `changed.lte.color-mode` on the document with { theme, resolved }.
80
+ */
81
+ setTheme(theme: Theme): void {
82
+ try {
83
+ localStorage.setItem(STORAGE_KEY, theme)
84
+ } catch {
85
+ // localStorage may be unavailable; the theme still applies for this page.
86
+ }
87
+
88
+ this._applyTheme(theme)
89
+ this._showActiveTheme(theme)
90
+
91
+ document.dispatchEvent(new CustomEvent(EVENT_CHANGED, {
92
+ detail: { theme, resolved: this.resolveTheme(theme) }
93
+ }))
94
+ }
95
+
96
+ /**
97
+ * Apply without persisting — used on init and when the OS preference
98
+ * changes while in "auto" mode.
99
+ */
100
+ _applyTheme(theme: Theme): void {
101
+ const resolved = this.resolveTheme(theme)
102
+ document.documentElement.setAttribute('data-bs-theme', resolved)
103
+ document.documentElement.style.colorScheme = resolved
104
+ }
105
+
106
+ _prefersDark(): boolean {
107
+ return globalThis.matchMedia('(prefers-color-scheme: dark)').matches
108
+ }
109
+
110
+ /**
111
+ * Sync the [data-bs-theme-value] toggles (active state, pressed state,
112
+ * check mark) and the [data-lte-theme-icon] indicator icons.
113
+ */
114
+ _showActiveTheme(theme: Theme): void {
115
+ document.querySelectorAll(SELECTOR_TOGGLE).forEach(toggle => {
116
+ const isActive = toggle.getAttribute('data-bs-theme-value') === theme
117
+ toggle.classList.toggle('active', isActive)
118
+ toggle.setAttribute('aria-pressed', String(isActive))
119
+ toggle.querySelector('.bi-check-lg')?.classList.toggle('d-none', !isActive)
120
+ })
121
+
122
+ document.querySelectorAll(SELECTOR_ICON).forEach(icon => {
123
+ icon.classList.toggle('d-none', (icon as HTMLElement).dataset.lteThemeIcon !== theme)
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Apply the preferred theme and sync the UI without persisting anything.
129
+ */
130
+ init(): void {
131
+ const theme = this.getPreferredTheme()
132
+ this._applyTheme(theme)
133
+ this._showActiveTheme(theme)
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Data API implementation
139
+ * ====================================================
140
+ * Toggle clicks are delegated on `document`, so switcher buttons added after
141
+ * load work and the listener survives Turbo's <body> swaps. The class is
142
+ * stateless — everything lives in localStorage and the DOM.
143
+ */
144
+
145
+ document.addEventListener('click', event => {
146
+ const target = event.target
147
+
148
+ if (!(target instanceof Element)) {
149
+ return
150
+ }
151
+
152
+ const toggle = target.closest(SELECTOR_TOGGLE)
153
+ const theme = toggle?.getAttribute('data-bs-theme-value') as Theme | null
154
+
155
+ if (theme) {
156
+ new ColorMode().setTheme(theme)
157
+ }
158
+ })
159
+
160
+ onDOMContentLoaded(() => {
161
+ const colorMode = new ColorMode()
162
+ colorMode.init()
163
+
164
+ // Follow the OS while no explicit choice (or "auto") is stored.
165
+ globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
166
+ const stored = colorMode.getStoredTheme()
167
+ if (!stored || stored === 'auto') {
168
+ colorMode._applyTheme('auto')
169
+ colorMode._showActiveTheme(stored ?? 'auto')
170
+ }
171
+ }, { signal: getLifecycleSignal() })
172
+ })
173
+
174
+ export default ColorMode
@@ -6,17 +6,15 @@
6
6
  * --------------------------------------------
7
7
  */
8
8
 
9
- import {
10
- onDOMContentLoaded
11
- } from './util/index'
9
+ import { BaseComponent, dispatchCustomEvent } from './base-component'
12
10
 
13
11
  /**
14
12
  * Constants
15
13
  * ====================================================
16
14
  */
17
15
 
18
- const DATA_KEY = 'lte.direct-chat'
19
- const EVENT_KEY = `.${DATA_KEY}`
16
+ const NAME = 'direct-chat'
17
+ const EVENT_KEY = `.lte.${NAME}`
20
18
  const EVENT_EXPANDED = `expanded${EVENT_KEY}`
21
19
  const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
22
20
 
@@ -30,25 +28,26 @@ const CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'
30
28
  * ====================================================
31
29
  */
32
30
 
33
- class DirectChat {
34
- _element: HTMLElement
35
- constructor(element: HTMLElement) {
36
- this._element = element
31
+ class DirectChat extends BaseComponent {
32
+ static get NAME(): string {
33
+ return NAME
34
+ }
35
+
36
+ static getInstance(element: Element | null | undefined): DirectChat | null {
37
+ return this._getInstance(element) as DirectChat | null
38
+ }
39
+
40
+ static getOrCreateInstance(element: HTMLElement): DirectChat {
41
+ return this.getInstance(element) ?? new this(element)
37
42
  }
38
43
 
39
44
  toggle(): void {
40
45
  if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {
41
- const event = new Event(EVENT_COLLAPSED)
42
-
43
46
  this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN)
44
-
45
- this._element.dispatchEvent(event)
47
+ dispatchCustomEvent(this._element, EVENT_COLLAPSED)
46
48
  } else {
47
- const event = new Event(EVENT_EXPANDED)
48
-
49
49
  this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN)
50
-
51
- this._element.dispatchEvent(event)
50
+ dispatchCustomEvent(this._element, EVENT_EXPANDED)
52
51
  }
53
52
  }
54
53
  }
@@ -57,23 +56,30 @@ class DirectChat {
57
56
  *
58
57
  * Data Api implementation
59
58
  * ====================================================
59
+ * One delegated listener on `document`: chat panes inserted later work, and
60
+ * the listener survives Turbo's <body> swaps.
60
61
  */
61
62
 
62
- onDOMContentLoaded(() => {
63
- const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)
64
-
65
- button.forEach(btn => {
66
- btn.addEventListener('click', event => {
67
- event.preventDefault()
68
- const target = event.target as HTMLElement
69
- const chatPane = target.closest(SELECTOR_DIRECT_CHAT) as HTMLElement | undefined
70
-
71
- if (chatPane) {
72
- const data = new DirectChat(chatPane)
73
- data.toggle()
74
- }
75
- })
76
- })
63
+ document.addEventListener('click', event => {
64
+ const target = event.target
65
+
66
+ if (!(target instanceof Element)) {
67
+ return
68
+ }
69
+
70
+ const trigger = target.closest(SELECTOR_DATA_TOGGLE)
71
+
72
+ if (!trigger) {
73
+ return
74
+ }
75
+
76
+ event.preventDefault()
77
+
78
+ const chatPane = trigger.closest(SELECTOR_DIRECT_CHAT) as HTMLElement | null
79
+
80
+ if (chatPane) {
81
+ DirectChat.getOrCreateInstance(chatPane).toggle()
82
+ }
77
83
  })
78
84
 
79
85
  export default DirectChat