admin-lte 4.0.2 → 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 (146) hide show
  1. package/ACCESSIBILITY-COMPLIANCE.md +60 -339
  2. package/CHANGELOG.md +66 -0
  3. package/README.md +15 -2
  4. package/dist/css/adminlte-docs.css +878 -0
  5. package/dist/css/adminlte-docs.css.map +1 -0
  6. package/dist/css/adminlte-docs.min.css +7 -0
  7. package/dist/css/adminlte-docs.min.css.map +1 -0
  8. package/dist/css/adminlte-docs.rtl.css +877 -0
  9. package/dist/css/adminlte-docs.rtl.css.map +1 -0
  10. package/dist/css/adminlte-docs.rtl.min.css +7 -0
  11. package/dist/css/adminlte-docs.rtl.min.css.map +1 -0
  12. package/dist/css/adminlte.css +1176 -2718
  13. package/dist/css/adminlte.css.map +1 -1
  14. package/dist/css/adminlte.min.css +2 -2
  15. package/dist/css/adminlte.min.css.map +1 -1
  16. package/dist/css/adminlte.rtl.css +1176 -2718
  17. package/dist/css/adminlte.rtl.css.map +1 -1
  18. package/dist/css/adminlte.rtl.min.css +2 -2
  19. package/dist/css/adminlte.rtl.min.css.map +1 -1
  20. package/dist/js/adminlte.esm.js +1365 -0
  21. package/dist/js/adminlte.esm.js.map +1 -0
  22. package/dist/js/adminlte.esm.min.js +7 -0
  23. package/dist/js/adminlte.esm.min.js.map +1 -0
  24. package/dist/js/adminlte.js +606 -310
  25. package/dist/js/adminlte.js.map +1 -1
  26. package/dist/js/adminlte.min.js +2 -2
  27. package/dist/js/adminlte.min.js.map +1 -1
  28. package/dist/js/types/accessibility.d.ts +46 -0
  29. package/dist/js/types/adminlte.d.ts +9 -0
  30. package/dist/js/types/base-component.d.ts +36 -0
  31. package/dist/js/types/card-widget.d.ts +30 -0
  32. package/dist/js/types/color-mode.d.ts +56 -0
  33. package/dist/js/types/direct-chat.d.ts +19 -0
  34. package/dist/js/types/fullscreen.d.ts +21 -0
  35. package/dist/js/types/layout.d.ts +19 -0
  36. package/dist/js/types/push-menu.d.ts +102 -0
  37. package/dist/js/types/treeview.d.ts +27 -0
  38. package/dist/js/types/util/index.d.ts +42 -0
  39. package/package.json +60 -21
  40. package/src/scss/_app-header.scss +3 -1
  41. package/src/scss/_app-main.scss +4 -0
  42. package/src/scss/_app-sidebar.scss +3 -1
  43. package/src/scss/_bootstrap-overrides.scss +60 -0
  44. package/src/scss/_callouts.scss +13 -3
  45. package/src/scss/_dropdown.scss +3 -1
  46. package/src/scss/_info-box.scss +3 -1
  47. package/src/scss/_progress-bars.scss +3 -1
  48. package/src/scss/_table.scss +4 -2
  49. package/src/scss/adminlte-docs.scss +25 -0
  50. package/src/scss/adminlte.scss +5 -4
  51. package/src/scss/pages/_login_and_register.scss +3 -1
  52. package/src/scss/parts/_core.scss +0 -1
  53. package/src/ts/accessibility.ts +129 -69
  54. package/src/ts/adminlte.ts +3 -1
  55. package/src/ts/base-component.ts +84 -0
  56. package/src/ts/card-widget.ts +134 -85
  57. package/src/ts/color-mode.ts +174 -0
  58. package/src/ts/direct-chat.ts +38 -32
  59. package/src/ts/fullscreen.ts +78 -63
  60. package/src/ts/layout.ts +2 -1
  61. package/src/ts/push-menu.ts +93 -44
  62. package/src/ts/treeview.ts +107 -47
  63. package/src/ts/util/index.ts +114 -29
  64. package/.browserslistrc +0 -17
  65. package/.prettierrc +0 -6
  66. package/CODE_OF_CONDUCT.md +0 -43
  67. package/SECURITY.md +0 -99
  68. package/dist/UI/general.html +0 -2317
  69. package/dist/UI/icons.html +0 -1126
  70. package/dist/UI/timeline.html +0 -1212
  71. package/dist/docs/browser-support.html +0 -826
  72. package/dist/docs/color-mode.html +0 -1142
  73. package/dist/docs/components/main-header.html +0 -1209
  74. package/dist/docs/components/main-sidebar.html +0 -924
  75. package/dist/docs/customization.html +0 -1080
  76. package/dist/docs/deployment.html +0 -1145
  77. package/dist/docs/faq.html +0 -1392
  78. package/dist/docs/getting-started.html +0 -991
  79. package/dist/docs/how-to-contribute.html +0 -890
  80. package/dist/docs/index.html +0 -16
  81. package/dist/docs/integrations.html +0 -1231
  82. package/dist/docs/introduction.html +0 -1008
  83. package/dist/docs/javascript/accessibility.html +0 -949
  84. package/dist/docs/javascript/card-widget.html +0 -1019
  85. package/dist/docs/javascript/direct-chat.html +0 -896
  86. package/dist/docs/javascript/fullscreen.html +0 -905
  87. package/dist/docs/javascript/layout.html +0 -908
  88. package/dist/docs/javascript/plugins-overview.html +0 -1144
  89. package/dist/docs/javascript/pushmenu.html +0 -1045
  90. package/dist/docs/javascript/treeview.html +0 -1024
  91. package/dist/docs/layout-blueprint.html +0 -1087
  92. package/dist/docs/layout.html +0 -842
  93. package/dist/docs/license.html +0 -807
  94. package/dist/docs/migration.html +0 -1149
  95. package/dist/docs/recipes.html +0 -1185
  96. package/dist/docs/rtl.html +0 -957
  97. package/dist/examples/lockscreen.html +0 -249
  98. package/dist/examples/login-v2.html +0 -282
  99. package/dist/examples/login.html +0 -272
  100. package/dist/examples/register-v2.html +0 -291
  101. package/dist/examples/register.html +0 -277
  102. package/dist/forms/elements.html +0 -1405
  103. package/dist/forms/layout.html +0 -1267
  104. package/dist/forms/validation.html +0 -1275
  105. package/dist/forms/wizard.html +0 -1410
  106. package/dist/generate/theme.html +0 -1082
  107. package/dist/index.html +0 -1745
  108. package/dist/index2.html +0 -2277
  109. package/dist/index3.html +0 -1532
  110. package/dist/layout/collapsed-sidebar-without-hover.html +0 -1144
  111. package/dist/layout/collapsed-sidebar.html +0 -1137
  112. package/dist/layout/fixed-complete.html +0 -1138
  113. package/dist/layout/fixed-footer.html +0 -1136
  114. package/dist/layout/fixed-header.html +0 -1136
  115. package/dist/layout/fixed-sidebar.html +0 -1138
  116. package/dist/layout/layout-custom-area.html +0 -1177
  117. package/dist/layout/layout-rtl.html +0 -1137
  118. package/dist/layout/logo-switch.html +0 -683
  119. package/dist/layout/sidebar-mini.html +0 -1137
  120. package/dist/layout/unfixed-sidebar.html +0 -1136
  121. package/dist/mailbox/compose.html +0 -1151
  122. package/dist/mailbox/inbox.html +0 -1509
  123. package/dist/mailbox/read.html +0 -1202
  124. package/dist/pages/404.html +0 -239
  125. package/dist/pages/500.html +0 -227
  126. package/dist/pages/calendar.html +0 -1266
  127. package/dist/pages/chat.html +0 -1427
  128. package/dist/pages/faq.html +0 -1383
  129. package/dist/pages/file-manager.html +0 -1835
  130. package/dist/pages/invoice.html +0 -1199
  131. package/dist/pages/kanban.html +0 -1439
  132. package/dist/pages/maintenance.html +0 -230
  133. package/dist/pages/pricing.html +0 -1279
  134. package/dist/pages/profile.html +0 -1382
  135. package/dist/pages/projects.html +0 -1644
  136. package/dist/pages/settings.html +0 -1387
  137. package/dist/tables/data.html +0 -1319
  138. package/dist/tables/simple.html +0 -1435
  139. package/dist/widgets/cards.html +0 -1356
  140. package/dist/widgets/info-box.html +0 -1429
  141. package/dist/widgets/small-box.html +0 -1225
  142. package/eslint.config.js +0 -82
  143. package/github-issue-response.md +0 -40
  144. package/index.html +0 -12
  145. package/netlify.toml +0 -16
  146. package/src/scss/_bootstrap-variables.scss +0 -1766
@@ -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,68 +88,93 @@ 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)
95
+ if (dispatchCustomEvent(this._parent, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
96
+ return
97
+ }
78
98
 
79
- // Only target direct children to avoid affecting nested cards
80
- 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)
81
101
 
82
- elm.forEach(el => {
83
- if (el instanceof HTMLElement) {
84
- slideUp(el, this._config.animationSpeed)
85
- }
86
- })
102
+ // Only target direct children to avoid affecting nested cards
103
+ const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
87
104
 
88
- setTimeout(() => {
89
- if (this._parent) {
90
- this._parent.classList.add(CLASS_NAME_COLLAPSED)
91
- this._parent.classList.remove(CLASS_NAME_COLLAPSING)
92
- }
93
- }, this._config.animationSpeed)
94
- }
105
+ elm.forEach(el => {
106
+ if (el instanceof HTMLElement) {
107
+ slideUp(el, this._config.animationSpeed)
108
+ }
109
+ })
95
110
 
96
- 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)
97
120
  }
98
121
 
99
122
  expand() {
100
- const event = new Event(EVENT_EXPANDED)
123
+ if (!this._parent) {
124
+ return
125
+ }
101
126
 
102
- if (this._parent) {
103
- this._parent.classList.add(CLASS_NAME_EXPANDING)
127
+ if (dispatchCustomEvent(this._parent, EVENT_EXPAND, { cancelable: true }).defaultPrevented) {
128
+ return
129
+ }
104
130
 
105
- // Only target direct children to avoid affecting nested cards
106
- 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)
107
133
 
108
- elm.forEach(el => {
109
- if (el instanceof HTMLElement) {
110
- slideDown(el, this._config.animationSpeed)
111
- }
112
- })
134
+ // Only target direct children to avoid affecting nested cards
135
+ const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
113
136
 
114
- setTimeout(() => {
115
- if (this._parent) {
116
- this._parent.classList.remove(CLASS_NAME_COLLAPSED, CLASS_NAME_EXPANDING)
117
- }
118
- }, this._config.animationSpeed)
119
- }
137
+ elm.forEach(el => {
138
+ if (el instanceof HTMLElement) {
139
+ slideDown(el, this._config.animationSpeed)
140
+ }
141
+ })
120
142
 
121
- 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)
122
150
  }
123
151
 
124
152
  remove() {
125
- const event = new Event(EVENT_REMOVE)
153
+ if (!this._parent) {
154
+ return
155
+ }
126
156
 
127
- if (this._parent) {
128
- slideUp(this._parent, this._config.animationSpeed)
157
+ if (dispatchCustomEvent(this._parent, EVENT_REMOVE, { cancelable: true }).defaultPrevented) {
158
+ return
129
159
  }
130
160
 
131
- 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)
132
172
  }
133
173
 
134
174
  toggle() {
135
- if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) {
175
+ // A card mid-collapse counts as collapsed so a second click during the
176
+ // animation reverses it instead of being swallowed.
177
+ if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED) || this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
136
178
  this.expand()
137
179
  return
138
180
  }
@@ -141,8 +183,6 @@ class CardWidget {
141
183
  }
142
184
 
143
185
  maximize() {
144
- const event = new Event(EVENT_MAXIMIZED)
145
-
146
186
  if (this._parent) {
147
187
  this._parent.style.height = `${this._parent.offsetHeight}px`
148
188
  this._parent.style.width = `${this._parent.offsetWidth}px`
@@ -161,16 +201,14 @@ class CardWidget {
161
201
  if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {
162
202
  this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED)
163
203
  }
204
+
205
+ dispatchCustomEvent(this._parent, EVENT_MAXIMIZED)
164
206
  }
165
207
  }, 150)
166
208
  }
167
-
168
- this._element?.dispatchEvent(event)
169
209
  }
170
210
 
171
211
  minimize() {
172
- const event = new Event(EVENT_MINIMIZED)
173
-
174
212
  if (this._parent) {
175
213
  this._parent.style.height = 'auto'
176
214
  this._parent.style.width = 'auto'
@@ -189,11 +227,19 @@ class CardWidget {
189
227
  if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {
190
228
  this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED)
191
229
  }
230
+
231
+ dispatchCustomEvent(this._parent, EVENT_MINIMIZED)
232
+
233
+ // Drop the inline sizing/transition once the restore transition has
234
+ // finished so the card doesn't carry stale styles forever.
235
+ setTimeout(() => {
236
+ this._parent?.style.removeProperty('height')
237
+ this._parent?.style.removeProperty('width')
238
+ this._parent?.style.removeProperty('transition')
239
+ }, 150)
192
240
  }
193
241
  }, 10)
194
242
  }
195
-
196
- this._element?.dispatchEvent(event)
197
243
  }
198
244
 
199
245
  toggleMaximize() {
@@ -210,41 +256,44 @@ class CardWidget {
210
256
  *
211
257
  * Data Api implementation
212
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.
213
262
  */
214
263
 
215
- onDOMContentLoaded(() => {
216
- const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE)
264
+ document.addEventListener('click', event => {
265
+ const target = event.target
217
266
 
218
- collapseBtn.forEach(btn => {
219
- btn.addEventListener('click', event => {
220
- event.preventDefault()
221
- const target = event.target as HTMLElement
222
- const data = new CardWidget(target, Default)
223
- data.toggle()
224
- })
225
- })
267
+ if (!(target instanceof Element)) {
268
+ return
269
+ }
226
270
 
227
- const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE)
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
228
275
 
229
- removeBtn.forEach(btn => {
230
- btn.addEventListener('click', event => {
231
- event.preventDefault()
232
- const target = event.target as HTMLElement
233
- const data = new CardWidget(target, Default)
234
- data.remove()
235
- })
236
- })
276
+ if (!trigger) {
277
+ return
278
+ }
237
279
 
238
- const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE)
280
+ event.preventDefault()
239
281
 
240
- maxBtn.forEach(btn => {
241
- btn.addEventListener('click', event => {
242
- event.preventDefault()
243
- const target = event.target as HTMLElement
244
- const data = new CardWidget(target, Default)
245
- data.toggleMaximize()
246
- })
247
- })
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
+ }
248
297
  })
249
298
 
250
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