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
@@ -0,0 +1,25 @@
1
+ /*!
2
+ * AdminLTE v4.0.4 — documentation styles
3
+ * Styles for the documentation and FAQ pages only (body.docs-page /
4
+ * body.faq-page). Applications never need this file; it is loaded by the
5
+ * docs pages in addition to adminlte.css.
6
+ */
7
+
8
+ // Bootstrap Configuration
9
+ // ---------------------------------------------------
10
+ @import "bootstrap/scss/functions";
11
+ @import "bootstrap-overrides";
12
+ @import "bootstrap/scss/variables";
13
+ @import "bootstrap/scss/variables-dark";
14
+ @import "bootstrap/scss/maps";
15
+ @import "bootstrap/scss/mixins";
16
+
17
+ // AdminLTE Configuration
18
+ // ---------------------------------------------------
19
+ @import "variables";
20
+ @import "variables-dark";
21
+ @import "mixins";
22
+
23
+ // Documentation styles
24
+ // ---------------------------------------------------
25
+ @import "docs";
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * AdminLTE v4.0.0
2
+ * AdminLTE v4.1.0
3
3
  * Author: Colorlib
4
4
  * Website: AdminLTE.io <https://adminlte.io>
5
5
  * License: Open source - MIT <https://opensource.org/licenses/MIT>
@@ -11,7 +11,7 @@
11
11
 
12
12
  // AdminLTE Configuration
13
13
  // ---------------------------------------------------
14
- @import "bootstrap-variables"; // little modified are here
14
+ @import "bootstrap-overrides"; // AdminLTE's ~10 changes to Bootstrap defaults
15
15
 
16
16
  // Bootstrap Configuration
17
17
  // ---------------------------------------------------
@@ -77,5 +77,6 @@
77
77
  // AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance
78
78
  @import "accessibility";
79
79
 
80
- // AdminLTE Documentation Styles (only applies to pages with body.docs-page)
81
- @import "docs";
80
+ // Documentation styles live in their own bundle (adminlte-docs.scss) so
81
+ // applications don't ship them — the docs pages load it in addition to
82
+ // this file.
@@ -1,3 +1,5 @@
1
+ @use "sass:map";
2
+
1
3
  //
2
4
  // Pages: Login & Register
3
5
  //
@@ -28,7 +30,7 @@
28
30
  .register-box {
29
31
  width: 400px;
30
32
 
31
- @media (max-width: map-get($grid-breakpoints, sm)) {
33
+ @media (max-width: map.get($grid-breakpoints, sm)) {
32
34
  width: 90%;
33
35
  margin-top: .5rem;
34
36
  }
@@ -12,4 +12,3 @@
12
12
  @import "../dropdown";
13
13
  @import "../callouts";
14
14
  @import "../compact-mode";
15
- @import "../docs";
@@ -3,6 +3,8 @@
3
3
  * WCAG 2.1 AA Compliance Features
4
4
  */
5
5
 
6
+ import { getLifecycleSignal } from './util/index'
7
+
6
8
  export interface AccessibilityConfig {
7
9
  announcements: boolean
8
10
  skipLinks: boolean
@@ -15,6 +17,9 @@ export class AccessibilityManager {
15
17
  private config: AccessibilityConfig
16
18
  private liveRegion: HTMLElement | null = null
17
19
  private focusHistory: HTMLElement[] = []
20
+ // Listeners bound to `document` survive Turbo's <body> swap, so they are
21
+ // registered with this signal and torn down before the next re-init.
22
+ private readonly signal: AbortSignal = getLifecycleSignal()
18
23
 
19
24
  constructor(config: Partial<AccessibilityConfig> = {}) {
20
25
  this.config = {
@@ -59,18 +64,34 @@ export class AccessibilityManager {
59
64
  private createLiveRegion(): void {
60
65
  if (this.liveRegion) return
61
66
 
67
+ // Adopt an existing region instead of appending a duplicate: Turbo caches
68
+ // the <body> including JS-injected nodes, so a region created before a
69
+ // navigation is restored along with the snapshot.
70
+ const existingRegion = document.getElementById('live-region')
71
+ if (existingRegion) {
72
+ this.liveRegion = existingRegion
73
+ return
74
+ }
75
+
62
76
  this.liveRegion = document.createElement('div')
63
77
  this.liveRegion.id = 'live-region'
64
78
  this.liveRegion.className = 'live-region'
65
79
  this.liveRegion.setAttribute('aria-live', 'polite')
66
80
  this.liveRegion.setAttribute('aria-atomic', 'true')
67
81
  this.liveRegion.setAttribute('role', 'status')
68
-
82
+
69
83
  document.body.append(this.liveRegion)
70
84
  }
71
85
 
72
86
  // WCAG 2.4.1: Bypass Blocks
73
87
  private addSkipLinks(): void {
88
+ // Same Turbo snapshot concern as createLiveRegion: skip links restored
89
+ // with a cached <body> must not be injected a second time.
90
+ if (document.querySelector('.skip-links')) {
91
+ this.ensureSkipTargets()
92
+ return
93
+ }
94
+
74
95
  const skipLinksContainer = document.createElement('div')
75
96
  skipLinksContainer.className = 'skip-links'
76
97
 
@@ -113,51 +134,22 @@ export class AccessibilityManager {
113
134
 
114
135
  // WCAG 2.4.3: Focus Order & 2.4.7: Focus Visible
115
136
  private initFocusManagement(): void {
137
+ // Note: focus must never be wrapped at the document edges — trapping Tab
138
+ // inside the page prevents keyboard users from reaching browser chrome
139
+ // and is itself a WCAG 2.1.2 (No Keyboard Trap) failure. Focus trapping
140
+ // is only legitimate inside active modal dialogs, which Bootstrap already
141
+ // handles.
116
142
  document.addEventListener('keydown', (event) => {
117
- if (event.key === 'Tab') {
118
- this.handleTabNavigation(event)
119
- }
120
143
  if (event.key === 'Escape') {
121
144
  this.handleEscapeKey(event)
122
145
  }
123
- })
146
+ }, { signal: this.signal })
124
147
 
125
148
  // Focus management for modals and dropdowns
126
149
  this.initModalFocusManagement()
127
150
  this.initDropdownFocusManagement()
128
151
  }
129
152
 
130
- private handleTabNavigation(event: KeyboardEvent): void {
131
- const focusableElements = this.getFocusableElements()
132
- const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement)
133
-
134
- if (event.shiftKey) {
135
- // Shift+Tab (backward)
136
- if (currentIndex <= 0) {
137
- event.preventDefault()
138
- focusableElements.at(-1)?.focus()
139
- }
140
- } else if (currentIndex >= focusableElements.length - 1) {
141
- // Tab (forward)
142
- event.preventDefault()
143
- focusableElements[0]?.focus()
144
- }
145
- }
146
-
147
- private getFocusableElements(): HTMLElement[] {
148
- const selector = [
149
- 'a[href]',
150
- 'button:not([disabled])',
151
- 'input:not([disabled])',
152
- 'select:not([disabled])',
153
- 'textarea:not([disabled])',
154
- '[tabindex]:not([tabindex="-1"])',
155
- '[contenteditable="true"]'
156
- ].join(', ')
157
-
158
- return Array.from(document.querySelectorAll(selector)) as HTMLElement[]
159
- }
160
-
161
153
  private handleEscapeKey(event: KeyboardEvent): void {
162
154
  // Close dropdowns, but let Bootstrap handle its own modal keyboard behavior
163
155
  const activeModal = document.querySelector('.modal.show')
@@ -181,18 +173,24 @@ export class AccessibilityManager {
181
173
  // Add keyboard support for custom components
182
174
  document.addEventListener('keydown', (event) => {
183
175
  const target = event.target as HTMLElement
184
-
176
+
177
+ // Never intercept keys inside editable controls: arrow keys must keep
178
+ // moving the caret in e.g. a navbar search input.
179
+ if (target.matches('input, textarea, select, [contenteditable], [contenteditable] *')) {
180
+ return
181
+ }
182
+
185
183
  // Handle arrow key navigation for menus
186
184
  if (target.closest('.nav, .navbar-nav, .dropdown-menu')) {
187
185
  this.handleMenuNavigation(event)
188
186
  }
189
-
187
+
190
188
  // Handle Enter and Space for custom buttons
191
189
  if ((event.key === 'Enter' || event.key === ' ') && target.hasAttribute('role') && target.getAttribute('role') === 'button' && !target.matches('button, input[type="button"], input[type="submit"]')) {
192
190
  event.preventDefault()
193
191
  target.click()
194
192
  }
195
- })
193
+ }, { signal: this.signal })
196
194
  }
197
195
 
198
196
  private handleMenuNavigation(event: KeyboardEvent): void {
@@ -201,9 +199,18 @@ export class AccessibilityManager {
201
199
  }
202
200
 
203
201
  const currentElement = event.target as HTMLElement
204
- const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []) as HTMLElement[]
202
+ const menuItems = (Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []) as HTMLElement[])
203
+ // Skip items hidden inside collapsed submenus or closed dropdowns.
204
+ .filter(item => item.offsetParent !== null)
205
205
  const currentIndex = menuItems.indexOf(currentElement)
206
-
206
+
207
+ // Only take over navigation when focus is actually on a menu item;
208
+ // otherwise stealing the arrow keys would yank focus out of unrelated
209
+ // controls that merely sit inside a nav container.
210
+ if (currentIndex === -1) {
211
+ return
212
+ }
213
+
207
214
  let nextIndex: number
208
215
 
209
216
  switch (event.key) {
@@ -244,16 +251,20 @@ export class AccessibilityManager {
244
251
  // Disable smooth scrolling
245
252
  document.documentElement.style.scrollBehavior = 'auto'
246
253
 
247
- // Reduce animation duration
248
- const style = document.createElement('style')
249
- style.textContent = `
250
- *, *::before, *::after {
251
- animation-duration: 0.01ms !important;
252
- animation-iteration-count: 1 !important;
253
- transition-duration: 0.01ms !important;
254
- }
255
- `
256
- document.head.append(style)
254
+ // Reduce animation duration. The <head> survives Turbo navigations, so
255
+ // only inject the override stylesheet once to avoid stacking duplicates.
256
+ if (!document.getElementById('adminlte-reduce-motion')) {
257
+ const style = document.createElement('style')
258
+ style.id = 'adminlte-reduce-motion'
259
+ style.textContent = `
260
+ *, *::before, *::after {
261
+ animation-duration: 0.01ms !important;
262
+ animation-iteration-count: 1 !important;
263
+ transition-duration: 0.01ms !important;
264
+ }
265
+ `
266
+ document.head.append(style)
267
+ }
257
268
  }
258
269
  }
259
270
 
@@ -283,6 +294,11 @@ export class AccessibilityManager {
283
294
  childList: true,
284
295
  subtree: true
285
296
  })
297
+
298
+ // Stop observing the old <body> before Turbo swaps in a new one.
299
+ this.signal.addEventListener('abort', () => {
300
+ observer.disconnect()
301
+ }, { once: true })
286
302
  }
287
303
 
288
304
  // WCAG 1.3.1: Info and Relationships
@@ -351,6 +367,14 @@ export class AccessibilityManager {
351
367
  }
352
368
 
353
369
  private handleFormError(input: HTMLInputElement): void {
370
+ // Inputs lacking both an id and a name would otherwise collide on a
371
+ // single "-error" id. Assign a stable generated id once and persist it on
372
+ // the element so repeated `invalid` events reuse the same error node
373
+ // instead of appending a new orphaned one each time.
374
+ if (!input.id && !input.name) {
375
+ input.id = accessibilityUtils.generateId('field')
376
+ }
377
+
354
378
  const errorId = `${input.id || input.name}-error`
355
379
  let errorElement = document.getElementById(errorId)
356
380
 
@@ -368,7 +392,14 @@ export class AccessibilityManager {
368
392
  }
369
393
 
370
394
  errorElement.textContent = input.validationMessage
371
- input.setAttribute('aria-describedby', errorId)
395
+ // Append to any existing aria-describedby (e.g. help text) instead of
396
+ // clobbering it.
397
+ const describedBy = (input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean)
398
+ if (!describedBy.includes(errorId)) {
399
+ describedBy.push(errorId)
400
+ }
401
+
402
+ input.setAttribute('aria-describedby', describedBy.join(' '))
372
403
  input.classList.add('is-invalid')
373
404
 
374
405
  this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive')
@@ -376,25 +407,33 @@ export class AccessibilityManager {
376
407
 
377
408
  // Modal focus management
378
409
  private initModalFocusManagement(): void {
410
+ // Capture the trigger on `show` — before Bootstrap (or the `shown`
411
+ // handler below) moves focus into the modal. Capturing on `shown` would
412
+ // store an element inside the modal itself, so closing it would "restore"
413
+ // focus to a now-hidden node and drop focus to <body>.
414
+ document.addEventListener('show.bs.modal', () => {
415
+ this.focusHistory.push(document.activeElement as HTMLElement)
416
+ }, { signal: this.signal })
417
+
379
418
  document.addEventListener('shown.bs.modal', (event) => {
380
419
  const modal = event.target as HTMLElement
381
- const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
382
-
383
- if (focusableElements.length > 0) {
384
- (focusableElements[0] as HTMLElement).focus()
385
- }
386
-
387
- // Store previous focus
388
- this.focusHistory.push(document.activeElement as HTMLElement)
389
- })
420
+ // Respect an explicit [autofocus] before falling back to the first
421
+ // focusable control.
422
+ const autofocusElement = modal.querySelector('[autofocus]') as HTMLElement | null
423
+ const firstFocusable = autofocusElement ||
424
+ (modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') as HTMLElement | null)
425
+
426
+ firstFocusable?.focus()
427
+ }, { signal: this.signal })
390
428
 
391
429
  document.addEventListener('hidden.bs.modal', () => {
392
- // Restore previous focus
430
+ // Restore previous focus, unless the trigger has since been removed
431
+ // from the document (e.g. a delete-confirmation modal removing its row).
393
432
  const previousElement = this.focusHistory.pop()
394
- if (previousElement) {
433
+ if (previousElement?.isConnected) {
395
434
  previousElement.focus()
396
435
  }
397
- })
436
+ }, { signal: this.signal })
398
437
  }
399
438
 
400
439
  // Dropdown focus management
@@ -407,7 +446,7 @@ export class AccessibilityManager {
407
446
  if (firstItem) {
408
447
  firstItem.focus()
409
448
  }
410
- })
449
+ }, { signal: this.signal })
411
450
  }
412
451
 
413
452
  // Public API methods
@@ -460,7 +499,7 @@ export class AccessibilityManager {
460
499
  event.preventDefault()
461
500
  }
462
501
  }
463
- })
502
+ }, { signal: this.signal })
464
503
  }
465
504
 
466
505
  public addLandmarks(): void {
@@ -470,7 +509,9 @@ export class AccessibilityManager {
470
509
  const appMain = document.querySelector('.app-main')
471
510
  if (appMain) {
472
511
  appMain.setAttribute('role', 'main')
473
- appMain.id = 'main'
512
+ if (!appMain.id) {
513
+ appMain.id = 'main'
514
+ }
474
515
  }
475
516
  }
476
517
 
@@ -505,10 +546,29 @@ export const initAccessibility = (config?: Partial<AccessibilityConfig>): Access
505
546
  return new AccessibilityManager(config)
506
547
  }
507
548
 
549
+ // Parse a CSS color into RGB channels. Supports rgb()/rgba() strings and
550
+ // 3- or 6-digit hex notation; anything else falls back to black.
551
+ const parseColorChannels = (color: string): number[] => {
552
+ const hexMatch = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(color.trim())
553
+ if (hexMatch) {
554
+ let hex = hexMatch[1]
555
+ if (hex.length === 3) {
556
+ hex = [...hex].map(character => character + character).join('')
557
+ }
558
+
559
+ return [
560
+ Number.parseInt(hex.slice(0, 2), 16),
561
+ Number.parseInt(hex.slice(2, 4), 16),
562
+ Number.parseInt(hex.slice(4, 6), 16)
563
+ ]
564
+ }
565
+
566
+ return color.match(/\d+/g)?.map(Number) || [0, 0, 0]
567
+ }
568
+
508
569
  // Utility function for luminance calculation
509
570
  const getLuminance = (color: string): number => {
510
- const rgb = color.match(/\d+/g)?.map(Number) || [0, 0, 0]
511
- const [r, g, b] = rgb.map(c => {
571
+ const [r, g, b] = parseColorChannels(color).map(c => {
512
572
  c = c / 255
513
573
  return c <= 0.039_28 ? c / 12.92 : (c + 0.055) ** 2.4 / (1.055 ** 2.4)
514
574
  })
@@ -5,10 +5,11 @@ import Treeview from './treeview.js'
5
5
  import DirectChat from './direct-chat.js'
6
6
  import FullScreen from './fullscreen.js'
7
7
  import PushMenu from './push-menu.js'
8
+ import ColorMode from './color-mode.js'
8
9
  import { initAccessibility } from './accessibility.js'
9
10
 
10
11
  /**
11
- * AdminLTE v4.0.0
12
+ * AdminLTE v4.1.0
12
13
  * Author: Colorlib
13
14
  * Website: AdminLTE.io <https://adminlte.io>
14
15
  * License: Open source - MIT <https://opensource.org/licenses/MIT>
@@ -38,5 +39,6 @@ export {
38
39
  DirectChat,
39
40
  FullScreen,
40
41
  PushMenu,
42
+ ColorMode,
41
43
  initAccessibility
42
44
  }
@@ -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 }