admin-lte 4.0.2 → 4.0.4

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 (110) hide show
  1. package/ACCESSIBILITY-COMPLIANCE.md +60 -339
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +15 -2
  4. package/dist/css/adminlte.css +24 -786
  5. package/dist/css/adminlte.css.map +1 -1
  6. package/dist/css/adminlte.min.css +2 -2
  7. package/dist/css/adminlte.min.css.map +1 -1
  8. package/dist/css/adminlte.rtl.css +24 -786
  9. package/dist/css/adminlte.rtl.css.map +1 -1
  10. package/dist/css/adminlte.rtl.min.css +2 -2
  11. package/dist/css/adminlte.rtl.min.css.map +1 -1
  12. package/dist/js/adminlte.js +233 -128
  13. package/dist/js/adminlte.js.map +1 -1
  14. package/dist/js/adminlte.min.js +2 -2
  15. package/dist/js/adminlte.min.js.map +1 -1
  16. package/package.json +30 -17
  17. package/src/scss/_callouts.scss +13 -3
  18. package/src/scss/_dropdown.scss +3 -1
  19. package/src/scss/adminlte.scss +1 -1
  20. package/src/scss/parts/_core.scss +0 -1
  21. package/src/ts/accessibility.ts +129 -69
  22. package/src/ts/adminlte.ts +1 -1
  23. package/src/ts/card-widget.ts +33 -8
  24. package/src/ts/fullscreen.ts +48 -42
  25. package/src/ts/layout.ts +2 -1
  26. package/src/ts/push-menu.ts +24 -8
  27. package/src/ts/treeview.ts +27 -1
  28. package/src/ts/util/index.ts +114 -29
  29. package/.browserslistrc +0 -17
  30. package/.prettierrc +0 -6
  31. package/CODE_OF_CONDUCT.md +0 -43
  32. package/SECURITY.md +0 -99
  33. package/dist/UI/general.html +0 -2317
  34. package/dist/UI/icons.html +0 -1126
  35. package/dist/UI/timeline.html +0 -1212
  36. package/dist/docs/browser-support.html +0 -826
  37. package/dist/docs/color-mode.html +0 -1142
  38. package/dist/docs/components/main-header.html +0 -1209
  39. package/dist/docs/components/main-sidebar.html +0 -924
  40. package/dist/docs/customization.html +0 -1080
  41. package/dist/docs/deployment.html +0 -1145
  42. package/dist/docs/faq.html +0 -1392
  43. package/dist/docs/getting-started.html +0 -991
  44. package/dist/docs/how-to-contribute.html +0 -890
  45. package/dist/docs/index.html +0 -16
  46. package/dist/docs/integrations.html +0 -1231
  47. package/dist/docs/introduction.html +0 -1008
  48. package/dist/docs/javascript/accessibility.html +0 -949
  49. package/dist/docs/javascript/card-widget.html +0 -1019
  50. package/dist/docs/javascript/direct-chat.html +0 -896
  51. package/dist/docs/javascript/fullscreen.html +0 -905
  52. package/dist/docs/javascript/layout.html +0 -908
  53. package/dist/docs/javascript/plugins-overview.html +0 -1144
  54. package/dist/docs/javascript/pushmenu.html +0 -1045
  55. package/dist/docs/javascript/treeview.html +0 -1024
  56. package/dist/docs/layout-blueprint.html +0 -1087
  57. package/dist/docs/layout.html +0 -842
  58. package/dist/docs/license.html +0 -807
  59. package/dist/docs/migration.html +0 -1149
  60. package/dist/docs/recipes.html +0 -1185
  61. package/dist/docs/rtl.html +0 -957
  62. package/dist/examples/lockscreen.html +0 -249
  63. package/dist/examples/login-v2.html +0 -282
  64. package/dist/examples/login.html +0 -272
  65. package/dist/examples/register-v2.html +0 -291
  66. package/dist/examples/register.html +0 -277
  67. package/dist/forms/elements.html +0 -1405
  68. package/dist/forms/layout.html +0 -1267
  69. package/dist/forms/validation.html +0 -1275
  70. package/dist/forms/wizard.html +0 -1410
  71. package/dist/generate/theme.html +0 -1082
  72. package/dist/index.html +0 -1745
  73. package/dist/index2.html +0 -2277
  74. package/dist/index3.html +0 -1532
  75. package/dist/layout/collapsed-sidebar-without-hover.html +0 -1144
  76. package/dist/layout/collapsed-sidebar.html +0 -1137
  77. package/dist/layout/fixed-complete.html +0 -1138
  78. package/dist/layout/fixed-footer.html +0 -1136
  79. package/dist/layout/fixed-header.html +0 -1136
  80. package/dist/layout/fixed-sidebar.html +0 -1138
  81. package/dist/layout/layout-custom-area.html +0 -1177
  82. package/dist/layout/layout-rtl.html +0 -1137
  83. package/dist/layout/logo-switch.html +0 -683
  84. package/dist/layout/sidebar-mini.html +0 -1137
  85. package/dist/layout/unfixed-sidebar.html +0 -1136
  86. package/dist/mailbox/compose.html +0 -1151
  87. package/dist/mailbox/inbox.html +0 -1509
  88. package/dist/mailbox/read.html +0 -1202
  89. package/dist/pages/404.html +0 -239
  90. package/dist/pages/500.html +0 -227
  91. package/dist/pages/calendar.html +0 -1266
  92. package/dist/pages/chat.html +0 -1427
  93. package/dist/pages/faq.html +0 -1383
  94. package/dist/pages/file-manager.html +0 -1835
  95. package/dist/pages/invoice.html +0 -1199
  96. package/dist/pages/kanban.html +0 -1439
  97. package/dist/pages/maintenance.html +0 -230
  98. package/dist/pages/pricing.html +0 -1279
  99. package/dist/pages/profile.html +0 -1382
  100. package/dist/pages/projects.html +0 -1644
  101. package/dist/pages/settings.html +0 -1387
  102. package/dist/tables/data.html +0 -1319
  103. package/dist/tables/simple.html +0 -1435
  104. package/dist/widgets/cards.html +0 -1356
  105. package/dist/widgets/info-box.html +0 -1429
  106. package/dist/widgets/small-box.html +0 -1225
  107. package/eslint.config.js +0 -82
  108. package/github-issue-response.md +0 -40
  109. package/index.html +0 -12
  110. package/netlify.toml +0 -16
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * AdminLTE v4.0.2 (https://adminlte.io)
2
+ * AdminLTE v4.0.4 (https://adminlte.io)
3
3
  * Copyright 2014-2026 Colorlib <https://colorlib.com>
4
4
  * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE)
5
5
  */
@@ -9,25 +9,57 @@
9
9
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.adminlte = {}));
10
10
  })(this, (function (exports) { 'use strict';
11
11
 
12
- const domContentLoadedCallbacks = [];
13
- const onDOMContentLoaded = (callback) => {
14
- if (document.readyState === 'loading') {
15
- if (!domContentLoadedCallbacks.length) {
16
- document.addEventListener('DOMContentLoaded', () => {
17
- for (const callback of domContentLoadedCallbacks) {
18
- callback();
19
- }
20
- });
21
- }
22
- domContentLoadedCallbacks.push(callback);
12
+ const lifecycleCallbacks = [];
13
+ const lifecycleState = {
14
+ controller: new AbortController(),
15
+ hasInitialized: false
16
+ };
17
+ const getLifecycleSignal = () => lifecycleState.controller.signal;
18
+ const runLifecycleCallbacks = () => {
19
+ if (lifecycleState.hasInitialized) {
20
+ return;
23
21
  }
24
- else {
22
+ lifecycleState.hasInitialized = true;
23
+ for (const callback of lifecycleCallbacks) {
25
24
  callback();
26
25
  }
27
26
  };
27
+ const onDOMContentLoaded = (callback) => {
28
+ lifecycleCallbacks.push(callback);
29
+ if (lifecycleState.hasInitialized) {
30
+ callback();
31
+ }
32
+ };
33
+ if (document.readyState === 'loading') {
34
+ document.addEventListener('DOMContentLoaded', runLifecycleCallbacks, { once: true });
35
+ }
36
+ else {
37
+ runLifecycleCallbacks();
38
+ }
39
+ document.addEventListener('turbo:before-render', () => {
40
+ lifecycleState.controller.abort();
41
+ lifecycleState.controller = new AbortController();
42
+ lifecycleState.hasInitialized = false;
43
+ });
44
+ document.addEventListener('turbo:load', runLifecycleCallbacks);
45
+ const slideTimers = new WeakMap();
46
+ const cancelSlide = (target) => {
47
+ const timers = slideTimers.get(target) ?? [];
48
+ for (const timer of timers) {
49
+ globalThis.clearTimeout(timer);
50
+ }
51
+ slideTimers.delete(target);
52
+ };
53
+ const clearSlideStyles = (target) => {
54
+ for (const property of ['height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'overflow', 'transition-duration', 'transition-property']) {
55
+ target.style.removeProperty(property);
56
+ }
57
+ };
28
58
  const slideUp = (target, duration = 500) => {
59
+ cancelSlide(target);
29
60
  if (duration <= 1) {
30
61
  target.style.display = 'none';
62
+ clearSlideStyles(target);
31
63
  return;
32
64
  }
33
65
  target.style.transitionProperty = 'height, margin, padding';
@@ -35,26 +67,23 @@
35
67
  target.style.boxSizing = 'border-box';
36
68
  target.style.height = `${target.offsetHeight}px`;
37
69
  target.style.overflow = 'hidden';
38
- globalThis.setTimeout(() => {
70
+ const stepTimer = globalThis.setTimeout(() => {
39
71
  target.style.height = '0';
40
72
  target.style.paddingTop = '0';
41
73
  target.style.paddingBottom = '0';
42
74
  target.style.marginTop = '0';
43
75
  target.style.marginBottom = '0';
44
76
  }, 1);
45
- globalThis.setTimeout(() => {
77
+ const cleanupTimer = globalThis.setTimeout(() => {
46
78
  target.style.display = 'none';
47
- target.style.removeProperty('height');
48
- target.style.removeProperty('padding-top');
49
- target.style.removeProperty('padding-bottom');
50
- target.style.removeProperty('margin-top');
51
- target.style.removeProperty('margin-bottom');
52
- target.style.removeProperty('overflow');
53
- target.style.removeProperty('transition-duration');
54
- target.style.removeProperty('transition-property');
79
+ clearSlideStyles(target);
80
+ slideTimers.delete(target);
55
81
  }, duration);
82
+ slideTimers.set(target, [stepTimer, cleanupTimer]);
56
83
  };
57
84
  const slideDown = (target, duration = 500) => {
85
+ cancelSlide(target);
86
+ clearSlideStyles(target);
58
87
  target.style.removeProperty('display');
59
88
  let { display } = globalThis.getComputedStyle(target);
60
89
  if (display === 'none') {
@@ -71,7 +100,7 @@
71
100
  target.style.paddingBottom = '0';
72
101
  target.style.marginTop = '0';
73
102
  target.style.marginBottom = '0';
74
- globalThis.setTimeout(() => {
103
+ const stepTimer = globalThis.setTimeout(() => {
75
104
  target.style.boxSizing = 'border-box';
76
105
  target.style.transitionProperty = 'height, margin, padding';
77
106
  target.style.transitionDuration = `${duration}ms`;
@@ -81,12 +110,11 @@
81
110
  target.style.removeProperty('margin-top');
82
111
  target.style.removeProperty('margin-bottom');
83
112
  }, 1);
84
- globalThis.setTimeout(() => {
85
- target.style.removeProperty('height');
86
- target.style.removeProperty('overflow');
87
- target.style.removeProperty('transition-duration');
88
- target.style.removeProperty('transition-property');
113
+ const cleanupTimer = globalThis.setTimeout(() => {
114
+ clearSlideStyles(target);
115
+ slideTimers.delete(target);
89
116
  }, duration);
117
+ slideTimers.set(target, [stepTimer, cleanupTimer]);
90
118
  };
91
119
 
92
120
  const CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition';
@@ -110,7 +138,7 @@
110
138
  }
111
139
  onDOMContentLoaded(() => {
112
140
  const layout = new Layout(document.body);
113
- window.addEventListener('resize', () => layout.holdTransition(200));
141
+ window.addEventListener('resize', () => layout.holdTransition(200), { signal: getLifecycleSignal() });
114
142
  setTimeout(() => {
115
143
  document.body.classList.add(CLASS_NAME_APP_LOADED);
116
144
  }, 400);
@@ -158,6 +186,7 @@
158
186
  const event = new Event(EVENT_COLLAPSED$2);
159
187
  if (this._parent) {
160
188
  this._parent.classList.add(CLASS_NAME_COLLAPSING);
189
+ this._parent.classList.remove(CLASS_NAME_EXPANDING);
161
190
  const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`);
162
191
  elm.forEach(el => {
163
192
  if (el instanceof HTMLElement) {
@@ -165,7 +194,7 @@
165
194
  }
166
195
  });
167
196
  setTimeout(() => {
168
- if (this._parent) {
197
+ if (this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
169
198
  this._parent.classList.add(CLASS_NAME_COLLAPSED);
170
199
  this._parent.classList.remove(CLASS_NAME_COLLAPSING);
171
200
  }
@@ -177,6 +206,7 @@
177
206
  const event = new Event(EVENT_EXPANDED$2);
178
207
  if (this._parent) {
179
208
  this._parent.classList.add(CLASS_NAME_EXPANDING);
209
+ this._parent.classList.remove(CLASS_NAME_COLLAPSING, CLASS_NAME_COLLAPSED);
180
210
  const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`);
181
211
  elm.forEach(el => {
182
212
  if (el instanceof HTMLElement) {
@@ -184,8 +214,8 @@
184
214
  }
185
215
  });
186
216
  setTimeout(() => {
187
- if (this._parent) {
188
- this._parent.classList.remove(CLASS_NAME_COLLAPSED, CLASS_NAME_EXPANDING);
217
+ if (this._parent?.classList.contains(CLASS_NAME_EXPANDING)) {
218
+ this._parent.classList.remove(CLASS_NAME_EXPANDING);
189
219
  }
190
220
  }, this._config.animationSpeed);
191
221
  }
@@ -194,12 +224,16 @@
194
224
  remove() {
195
225
  const event = new Event(EVENT_REMOVE);
196
226
  if (this._parent) {
197
- slideUp(this._parent, this._config.animationSpeed);
227
+ const parent = this._parent;
228
+ slideUp(parent, this._config.animationSpeed);
229
+ setTimeout(() => {
230
+ parent.remove();
231
+ }, this._config.animationSpeed);
198
232
  }
199
233
  this._element?.dispatchEvent(event);
200
234
  }
201
235
  toggle() {
202
- if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) {
236
+ if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED) || this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
203
237
  this.expand();
204
238
  return;
205
239
  }
@@ -242,6 +276,11 @@
242
276
  if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {
243
277
  this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED);
244
278
  }
279
+ setTimeout(() => {
280
+ this._parent?.style.removeProperty('height');
281
+ this._parent?.style.removeProperty('width');
282
+ this._parent?.style.removeProperty('transition');
283
+ }, 150);
245
284
  }
246
285
  }, 10);
247
286
  }
@@ -260,7 +299,7 @@
260
299
  collapseBtn.forEach(btn => {
261
300
  btn.addEventListener('click', event => {
262
301
  event.preventDefault();
263
- const target = event.target;
302
+ const target = event.currentTarget;
264
303
  const data = new CardWidget(target, Default$1);
265
304
  data.toggle();
266
305
  });
@@ -269,7 +308,7 @@
269
308
  removeBtn.forEach(btn => {
270
309
  btn.addEventListener('click', event => {
271
310
  event.preventDefault();
272
- const target = event.target;
311
+ const target = event.currentTarget;
273
312
  const data = new CardWidget(target, Default$1);
274
313
  data.remove();
275
314
  });
@@ -278,7 +317,7 @@
278
317
  maxBtn.forEach(btn => {
279
318
  btn.addEventListener('click', event => {
280
319
  event.preventDefault();
281
- const target = event.target;
320
+ const target = event.currentTarget;
282
321
  const data = new CardWidget(target, Default$1);
283
322
  data.toggleMaximize();
284
323
  });
@@ -299,6 +338,10 @@
299
338
  animationSpeed: 300,
300
339
  accordion: true
301
340
  };
341
+ const setAriaExpanded = (navItem, expanded) => {
342
+ const link = navItem.querySelector(`:scope > ${SELECTOR_NAV_LINK}`);
343
+ link?.setAttribute('aria-expanded', String(expanded));
344
+ };
302
345
  class Treeview {
303
346
  _element;
304
347
  _config;
@@ -311,8 +354,9 @@
311
354
  if (this._config.accordion) {
312
355
  const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`);
313
356
  openMenuList?.forEach(openMenu => {
314
- if (openMenu !== this._element.parentElement) {
357
+ if (!this._element.contains(openMenu)) {
315
358
  openMenu.classList.remove(CLASS_NAME_MENU_OPEN);
359
+ setAriaExpanded(openMenu, false);
316
360
  const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU);
317
361
  if (childElement) {
318
362
  slideUp(childElement, this._config.animationSpeed);
@@ -321,6 +365,7 @@
321
365
  });
322
366
  }
323
367
  this._element.classList.add(CLASS_NAME_MENU_OPEN);
368
+ setAriaExpanded(this._element, true);
324
369
  const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU);
325
370
  if (childElement) {
326
371
  slideDown(childElement, this._config.animationSpeed);
@@ -330,6 +375,7 @@
330
375
  close() {
331
376
  const event = new Event(EVENT_COLLAPSED$1);
332
377
  this._element.classList.remove(CLASS_NAME_MENU_OPEN);
378
+ setAriaExpanded(this._element, false);
333
379
  const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU);
334
380
  if (childElement) {
335
381
  slideUp(childElement, this._config.animationSpeed);
@@ -355,6 +401,13 @@
355
401
  menuItem.dispatchEvent(event);
356
402
  }
357
403
  });
404
+ document.querySelectorAll(SELECTOR_DATA_TOGGLE$1).forEach(root => {
405
+ root.querySelectorAll(SELECTOR_NAV_ITEM).forEach(item => {
406
+ if (item.querySelector(`:scope > ${SELECTOR_TREEVIEW_MENU}`)) {
407
+ setAriaExpanded(item, item.classList.contains(CLASS_NAME_MENU_OPEN));
408
+ }
409
+ });
410
+ });
358
411
  const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE$1);
359
412
  button.forEach(btn => {
360
413
  btn.addEventListener('click', event => {
@@ -430,6 +483,17 @@
430
483
  const SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle="fullscreen"]';
431
484
  const SELECTOR_MAXIMIZE_ICON = '[data-lte-icon="maximize"]';
432
485
  const SELECTOR_MINIMIZE_ICON = '[data-lte-icon="minimize"]';
486
+ function syncFullScreenState() {
487
+ const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
488
+ const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
489
+ const isFullScreen = Boolean(document.fullscreenElement);
490
+ iconMaximize?.classList.toggle('d-none', isFullScreen);
491
+ iconMinimize?.classList.toggle('d-none', !isFullScreen);
492
+ const eventName = isFullScreen ? EVENT_MAXIMIZED : EVENT_MINIMIZED;
493
+ document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE).forEach(button => {
494
+ button.dispatchEvent(new Event(eventName));
495
+ });
496
+ }
433
497
  class FullScreen {
434
498
  _element;
435
499
  _config;
@@ -438,43 +502,27 @@
438
502
  this._config = config;
439
503
  }
440
504
  inFullScreen() {
441
- const event = new Event(EVENT_MAXIMIZED);
442
- const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
443
- const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
444
- void document.documentElement.requestFullscreen();
445
- if (iconMaximize) {
446
- iconMaximize.classList.add('d-none');
447
- }
448
- if (iconMinimize) {
449
- iconMinimize.classList.remove('d-none');
450
- }
451
- this._element.dispatchEvent(event);
505
+ void document.documentElement.requestFullscreen().catch(() => {
506
+ });
452
507
  }
453
508
  outFullscreen() {
454
- const event = new Event(EVENT_MINIMIZED);
455
- const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
456
- const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
457
- void document.exitFullscreen();
458
- if (iconMaximize) {
459
- iconMaximize.classList.remove('d-none');
460
- }
461
- if (iconMinimize) {
462
- iconMinimize.classList.add('d-none');
463
- }
464
- this._element.dispatchEvent(event);
509
+ void document.exitFullscreen().catch(() => {
510
+ });
465
511
  }
466
512
  toggleFullScreen() {
467
- if (document.fullscreenEnabled) {
468
- if (document.fullscreenElement) {
469
- this.outFullscreen();
470
- }
471
- else {
472
- this.inFullScreen();
473
- }
513
+ if (!document.fullscreenEnabled) {
514
+ return;
515
+ }
516
+ if (document.fullscreenElement) {
517
+ this.outFullscreen();
518
+ }
519
+ else {
520
+ this.inFullScreen();
474
521
  }
475
522
  }
476
523
  }
477
524
  onDOMContentLoaded(() => {
525
+ document.addEventListener('fullscreenchange', syncFullScreenState, { signal: getLifecycleSignal() });
478
526
  const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE);
479
527
  buttons.forEach(btn => {
480
528
  btn.addEventListener('click', event => {
@@ -504,7 +552,7 @@
504
552
  const SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle="sidebar"]';
505
553
  const STORAGE_KEY_SIDEBAR_STATE = 'lte.sidebar.state';
506
554
  const Defaults = {
507
- sidebarBreakpoint: 992,
555
+ sidebarBreakpoint: 991.98,
508
556
  enablePersistence: false
509
557
  };
510
558
  class PushMenu {
@@ -648,13 +696,17 @@
648
696
  };
649
697
  const pushMenu = new PushMenu(sidebar, config);
650
698
  pushMenu.init();
651
- window.addEventListener('resize', () => {
652
- pushMenu.setupSidebarBreakPoint();
699
+ const breakpointQuery = globalThis.matchMedia(`(max-width: ${pushMenu._config.sidebarBreakpoint}px)`);
700
+ breakpointQuery.addEventListener('change', () => {
653
701
  pushMenu.updateStateByResponsiveLogic();
654
- });
655
- const sidebarOverlay = document.createElement('div');
656
- sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY;
657
- document.querySelector(SELECTOR_APP_WRAPPER)?.append(sidebarOverlay);
702
+ }, { signal: getLifecycleSignal() });
703
+ const appWrapper = document.querySelector(SELECTOR_APP_WRAPPER);
704
+ let sidebarOverlay = appWrapper?.querySelector(`:scope > .${CLASS_NAME_SIDEBAR_OVERLAY}`);
705
+ if (!sidebarOverlay) {
706
+ sidebarOverlay = document.createElement('div');
707
+ sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY;
708
+ appWrapper?.append(sidebarOverlay);
709
+ }
658
710
  let overlayTouchMoved = false;
659
711
  sidebarOverlay.addEventListener('touchstart', () => {
660
712
  overlayTouchMoved = false;
@@ -693,6 +745,7 @@
693
745
  config;
694
746
  liveRegion = null;
695
747
  focusHistory = [];
748
+ signal = getLifecycleSignal();
696
749
  constructor(config = {}) {
697
750
  this.config = {
698
751
  announcements: true,
@@ -727,6 +780,11 @@
727
780
  createLiveRegion() {
728
781
  if (this.liveRegion)
729
782
  return;
783
+ const existingRegion = document.getElementById('live-region');
784
+ if (existingRegion) {
785
+ this.liveRegion = existingRegion;
786
+ return;
787
+ }
730
788
  this.liveRegion = document.createElement('div');
731
789
  this.liveRegion.id = 'live-region';
732
790
  this.liveRegion.className = 'live-region';
@@ -736,6 +794,10 @@
736
794
  document.body.append(this.liveRegion);
737
795
  }
738
796
  addSkipLinks() {
797
+ if (document.querySelector('.skip-links')) {
798
+ this.ensureSkipTargets();
799
+ return;
800
+ }
739
801
  const skipLinksContainer = document.createElement('div');
740
802
  skipLinksContainer.className = 'skip-links';
741
803
  const skipToMain = document.createElement('a');
@@ -769,42 +831,13 @@
769
831
  }
770
832
  initFocusManagement() {
771
833
  document.addEventListener('keydown', (event) => {
772
- if (event.key === 'Tab') {
773
- this.handleTabNavigation(event);
774
- }
775
834
  if (event.key === 'Escape') {
776
835
  this.handleEscapeKey(event);
777
836
  }
778
- });
837
+ }, { signal: this.signal });
779
838
  this.initModalFocusManagement();
780
839
  this.initDropdownFocusManagement();
781
840
  }
782
- handleTabNavigation(event) {
783
- const focusableElements = this.getFocusableElements();
784
- const currentIndex = focusableElements.indexOf(document.activeElement);
785
- if (event.shiftKey) {
786
- if (currentIndex <= 0) {
787
- event.preventDefault();
788
- focusableElements.at(-1)?.focus();
789
- }
790
- }
791
- else if (currentIndex >= focusableElements.length - 1) {
792
- event.preventDefault();
793
- focusableElements[0]?.focus();
794
- }
795
- }
796
- getFocusableElements() {
797
- const selector = [
798
- 'a[href]',
799
- 'button:not([disabled])',
800
- 'input:not([disabled])',
801
- 'select:not([disabled])',
802
- 'textarea:not([disabled])',
803
- '[tabindex]:not([tabindex="-1"])',
804
- '[contenteditable="true"]'
805
- ].join(', ');
806
- return Array.from(document.querySelectorAll(selector));
807
- }
808
841
  handleEscapeKey(event) {
809
842
  const activeModal = document.querySelector('.modal.show');
810
843
  if (activeModal) {
@@ -820,6 +853,9 @@
820
853
  initKeyboardNavigation() {
821
854
  document.addEventListener('keydown', (event) => {
822
855
  const target = event.target;
856
+ if (target.matches('input, textarea, select, [contenteditable], [contenteditable] *')) {
857
+ return;
858
+ }
823
859
  if (target.closest('.nav, .navbar-nav, .dropdown-menu')) {
824
860
  this.handleMenuNavigation(event);
825
861
  }
@@ -827,15 +863,19 @@
827
863
  event.preventDefault();
828
864
  target.click();
829
865
  }
830
- });
866
+ }, { signal: this.signal });
831
867
  }
832
868
  handleMenuNavigation(event) {
833
869
  if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
834
870
  return;
835
871
  }
836
872
  const currentElement = event.target;
837
- const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []);
873
+ const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || [])
874
+ .filter(item => item.offsetParent !== null);
838
875
  const currentIndex = menuItems.indexOf(currentElement);
876
+ if (currentIndex === -1) {
877
+ return;
878
+ }
839
879
  let nextIndex;
840
880
  switch (event.key) {
841
881
  case 'ArrowDown':
@@ -868,15 +908,18 @@
868
908
  if (prefersReducedMotion) {
869
909
  document.body.classList.add('reduce-motion');
870
910
  document.documentElement.style.scrollBehavior = 'auto';
871
- const style = document.createElement('style');
872
- style.textContent = `
873
- *, *::before, *::after {
874
- animation-duration: 0.01ms !important;
875
- animation-iteration-count: 1 !important;
876
- transition-duration: 0.01ms !important;
877
- }
878
- `;
879
- document.head.append(style);
911
+ if (!document.getElementById('adminlte-reduce-motion')) {
912
+ const style = document.createElement('style');
913
+ style.id = 'adminlte-reduce-motion';
914
+ style.textContent = `
915
+ *, *::before, *::after {
916
+ animation-duration: 0.01ms !important;
917
+ animation-iteration-count: 1 !important;
918
+ transition-duration: 0.01ms !important;
919
+ }
920
+ `;
921
+ document.head.append(style);
922
+ }
880
923
  }
881
924
  }
882
925
  initErrorAnnouncements() {
@@ -899,6 +942,9 @@
899
942
  childList: true,
900
943
  subtree: true
901
944
  });
945
+ this.signal.addEventListener('abort', () => {
946
+ observer.disconnect();
947
+ }, { once: true });
902
948
  }
903
949
  initTableAccessibility() {
904
950
  document.querySelectorAll('table').forEach((table) => {
@@ -950,6 +996,9 @@
950
996
  });
951
997
  }
952
998
  handleFormError(input) {
999
+ if (!input.id && !input.name) {
1000
+ input.id = accessibilityUtils.generateId('field');
1001
+ }
953
1002
  const errorId = `${input.id || input.name}-error`;
954
1003
  let errorElement = document.getElementById(errorId);
955
1004
  if (!errorElement) {
@@ -960,25 +1009,31 @@
960
1009
  input.parentNode?.append(errorElement);
961
1010
  }
962
1011
  errorElement.textContent = input.validationMessage;
963
- input.setAttribute('aria-describedby', errorId);
1012
+ const describedBy = (input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
1013
+ if (!describedBy.includes(errorId)) {
1014
+ describedBy.push(errorId);
1015
+ }
1016
+ input.setAttribute('aria-describedby', describedBy.join(' '));
964
1017
  input.classList.add('is-invalid');
965
1018
  this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive');
966
1019
  }
967
1020
  initModalFocusManagement() {
1021
+ document.addEventListener('show.bs.modal', () => {
1022
+ this.focusHistory.push(document.activeElement);
1023
+ }, { signal: this.signal });
968
1024
  document.addEventListener('shown.bs.modal', (event) => {
969
1025
  const modal = event.target;
970
- const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
971
- if (focusableElements.length > 0) {
972
- focusableElements[0].focus();
973
- }
974
- this.focusHistory.push(document.activeElement);
975
- });
1026
+ const autofocusElement = modal.querySelector('[autofocus]');
1027
+ const firstFocusable = autofocusElement ||
1028
+ modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
1029
+ firstFocusable?.focus();
1030
+ }, { signal: this.signal });
976
1031
  document.addEventListener('hidden.bs.modal', () => {
977
1032
  const previousElement = this.focusHistory.pop();
978
- if (previousElement) {
1033
+ if (previousElement?.isConnected) {
979
1034
  previousElement.focus();
980
1035
  }
981
- });
1036
+ }, { signal: this.signal });
982
1037
  }
983
1038
  initDropdownFocusManagement() {
984
1039
  document.addEventListener('shown.bs.dropdown', (event) => {
@@ -988,7 +1043,7 @@
988
1043
  if (firstItem) {
989
1044
  firstItem.focus();
990
1045
  }
991
- });
1046
+ }, { signal: this.signal });
992
1047
  }
993
1048
  announce(message, priority = 'polite') {
994
1049
  if (!this.liveRegion) {
@@ -1029,7 +1084,7 @@
1029
1084
  event.preventDefault();
1030
1085
  }
1031
1086
  }
1032
- });
1087
+ }, { signal: this.signal });
1033
1088
  }
1034
1089
  addLandmarks() {
1035
1090
  const main = document.querySelector('main');
@@ -1037,7 +1092,9 @@
1037
1092
  const appMain = document.querySelector('.app-main');
1038
1093
  if (appMain) {
1039
1094
  appMain.setAttribute('role', 'main');
1040
- appMain.id = 'main';
1095
+ if (!appMain.id) {
1096
+ appMain.id = 'main';
1097
+ }
1041
1098
  }
1042
1099
  }
1043
1100
  document.querySelectorAll('.navbar-nav, .nav').forEach((nav, index) => {
@@ -1060,6 +1117,54 @@
1060
1117
  const initAccessibility = (config) => {
1061
1118
  return new AccessibilityManager(config);
1062
1119
  };
1120
+ const parseColorChannels = (color) => {
1121
+ const hexMatch = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(color.trim());
1122
+ if (hexMatch) {
1123
+ let hex = hexMatch[1];
1124
+ if (hex.length === 3) {
1125
+ hex = [...hex].map(character => character + character).join('');
1126
+ }
1127
+ return [
1128
+ Number.parseInt(hex.slice(0, 2), 16),
1129
+ Number.parseInt(hex.slice(2, 4), 16),
1130
+ Number.parseInt(hex.slice(4, 6), 16)
1131
+ ];
1132
+ }
1133
+ return color.match(/\d+/g)?.map(Number) || [0, 0, 0];
1134
+ };
1135
+ const getLuminance = (color) => {
1136
+ const [r, g, b] = parseColorChannels(color).map(c => {
1137
+ c = c / 255;
1138
+ return c <= 0.039_28 ? c / 12.92 : (c + 0.055) ** 2.4 / (1.055 ** 2.4);
1139
+ });
1140
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1141
+ };
1142
+ const accessibilityUtils = {
1143
+ checkColorContrast: (foreground, background) => {
1144
+ const l1 = getLuminance(foreground);
1145
+ const l2 = getLuminance(background);
1146
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1147
+ return {
1148
+ ratio: Math.round(ratio * 100) / 100,
1149
+ passes: ratio >= 4.5
1150
+ };
1151
+ },
1152
+ generateId: (prefix = 'a11y') => {
1153
+ return `${prefix}-${Math.random().toString(36).slice(2, 11)}`;
1154
+ },
1155
+ isFocusable: (element) => {
1156
+ const focusableSelectors = [
1157
+ 'a[href]',
1158
+ 'button:not([disabled])',
1159
+ 'input:not([disabled])',
1160
+ 'select:not([disabled])',
1161
+ 'textarea:not([disabled])',
1162
+ '[tabindex]:not([tabindex="-1"])',
1163
+ '[contenteditable="true"]'
1164
+ ];
1165
+ return focusableSelectors.some(selector => element.matches(selector));
1166
+ }
1167
+ };
1063
1168
 
1064
1169
  onDOMContentLoaded(() => {
1065
1170
  const accessibilityManager = initAccessibility({