@dodlhuat/basix 1.3.2 → 1.3.3

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 (118) hide show
  1. package/README.md +13 -7
  2. package/css/accordion.scss +0 -5
  3. package/css/badge.scss +1 -6
  4. package/css/bottom-sheet.scss +3 -8
  5. package/css/breadcrumb.scss +6 -15
  6. package/css/button.scss +2 -1
  7. package/css/calendar.scss +0 -54
  8. package/css/card.scss +0 -5
  9. package/css/carousel.scss +0 -3
  10. package/css/chart.scss +0 -25
  11. package/css/chat-bubbles.scss +0 -15
  12. package/css/checkbox.scss +3 -2
  13. package/css/chips.scss +3 -7
  14. package/css/code-viewer.scss +1 -5
  15. package/css/context-menu.scss +4 -6
  16. package/css/datepicker.scss +4 -7
  17. package/css/docs.scss +0 -4
  18. package/css/dropdown.scss +1 -1
  19. package/css/editor.scss +1 -23
  20. package/css/file-uploader.scss +2 -2
  21. package/css/flyout-menu.scss +66 -44
  22. package/css/form.scss +0 -28
  23. package/css/gallery.scss +2 -3
  24. package/css/group-picker.scss +5 -35
  25. package/css/icons.scss +0 -3
  26. package/css/lightbox.scss +2 -4
  27. package/css/mixins.scss +8 -0
  28. package/css/modal.scss +3 -3
  29. package/css/parameters.scss +6 -1
  30. package/css/popover.scss +3 -15
  31. package/css/progress.scss +0 -6
  32. package/css/push-menu.scss +3 -28
  33. package/css/radiobutton.scss +2 -1
  34. package/css/range-slider.scss +1 -7
  35. package/css/scrollbar.scss +2 -6
  36. package/css/sidebar-nav.scss +0 -12
  37. package/css/stepper.scss +0 -4
  38. package/css/style.css +63 -68
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.min.css.map +1 -1
  42. package/css/style.scss +1 -1
  43. package/css/table.scss +0 -4
  44. package/css/tabs.scss +0 -2
  45. package/css/timeline.scss +1 -13
  46. package/css/timepicker.scss +6 -7
  47. package/css/toast.scss +1 -1
  48. package/css/tooltip.scss +1 -5
  49. package/css/tree.scss +1 -1
  50. package/css/typography.scss +3 -3
  51. package/css/virtual-dropdown.scss +3 -28
  52. package/js/bottom-sheet.d.ts +3 -1
  53. package/js/bottom-sheet.js +26 -27
  54. package/js/calendar.d.ts +7 -0
  55. package/js/calendar.js +14 -33
  56. package/js/carousel.d.ts +2 -0
  57. package/js/carousel.js +13 -5
  58. package/js/chart.d.ts +4 -0
  59. package/js/chart.js +13 -31
  60. package/js/code-viewer.d.ts +1 -0
  61. package/js/code-viewer.js +4 -0
  62. package/js/context-menu.d.ts +9 -2
  63. package/js/context-menu.js +17 -14
  64. package/js/datepicker.d.ts +4 -0
  65. package/js/datepicker.js +26 -11
  66. package/js/dropdown.d.ts +3 -3
  67. package/js/dropdown.js +6 -9
  68. package/js/editor.d.ts +1 -0
  69. package/js/editor.js +9 -3
  70. package/js/file-uploader.d.ts +4 -0
  71. package/js/file-uploader.js +52 -43
  72. package/js/flyout-menu.d.ts +5 -3
  73. package/js/flyout-menu.js +23 -46
  74. package/js/gallery.d.ts +3 -0
  75. package/js/gallery.js +22 -24
  76. package/js/group-picker.d.ts +5 -0
  77. package/js/group-picker.js +12 -17
  78. package/js/lightbox.d.ts +3 -0
  79. package/js/lightbox.js +12 -6
  80. package/js/modal.d.ts +3 -1
  81. package/js/modal.js +14 -11
  82. package/js/popover.d.ts +2 -0
  83. package/js/popover.js +26 -30
  84. package/js/position.d.ts +2 -0
  85. package/js/position.js +1 -5
  86. package/js/push-menu.d.ts +2 -0
  87. package/js/push-menu.js +22 -35
  88. package/js/range-slider.d.ts +1 -0
  89. package/js/range-slider.js +5 -3
  90. package/js/scroll.d.ts +2 -0
  91. package/js/scroll.js +1 -0
  92. package/js/scrollbar.d.ts +2 -0
  93. package/js/scrollbar.js +24 -36
  94. package/js/select.d.ts +1 -0
  95. package/js/select.js +5 -10
  96. package/js/sidebar-nav.d.ts +2 -0
  97. package/js/sidebar-nav.js +8 -0
  98. package/js/stepper.d.ts +2 -0
  99. package/js/stepper.js +7 -1
  100. package/js/table.d.ts +4 -0
  101. package/js/table.js +15 -22
  102. package/js/tabs.d.ts +2 -0
  103. package/js/tabs.js +6 -14
  104. package/js/theme.d.ts +1 -0
  105. package/js/theme.js +5 -13
  106. package/js/timepicker.d.ts +3 -0
  107. package/js/timepicker.js +81 -67
  108. package/js/toast.d.ts +3 -0
  109. package/js/toast.js +24 -15
  110. package/js/tooltip.d.ts +2 -0
  111. package/js/tooltip.js +21 -19
  112. package/js/tree.d.ts +3 -0
  113. package/js/tree.js +13 -0
  114. package/js/utils.d.ts +1 -3
  115. package/js/utils.js +0 -3
  116. package/js/virtual-dropdown.d.ts +3 -0
  117. package/js/virtual-dropdown.js +25 -0
  118. package/package.json +2 -2
package/js/gallery.js CHANGED
@@ -1,19 +1,15 @@
1
1
  import { escapeHtml } from './utils.js';
2
+ /** Infinite-scroll masonry gallery that distributes images across dynamically sized columns. */
2
3
  class MasonryGallery {
4
+ container;
5
+ loader;
6
+ options;
7
+ columns = [];
8
+ isFetching = false;
9
+ resizeObserver = null;
10
+ abortController = null;
11
+ reloaded = 0;
3
12
  constructor(containerId, options) {
4
- this.columns = [];
5
- this.isFetching = false;
6
- this.resizeObserver = null;
7
- this.abortController = null;
8
- this.reloaded = 0;
9
- this.handleScroll = () => {
10
- if (this.isFetching)
11
- return;
12
- const rect = this.container.getBoundingClientRect();
13
- if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
14
- this.loadMoreImages();
15
- }
16
- };
17
13
  const container = document.getElementById(containerId);
18
14
  if (!container) {
19
15
  throw new Error(`Container with id "${containerId}" not found`);
@@ -51,18 +47,14 @@ class MasonryGallery {
51
47
  }
52
48
  }
53
49
  addEventListeners() {
50
+ this.abortController = new AbortController();
51
+ const sig = this.abortController.signal;
54
52
  let resizeTimeout;
55
53
  window.addEventListener("resize", () => {
56
54
  clearTimeout(resizeTimeout);
57
- resizeTimeout = setTimeout(() => {
58
- this.reLayout();
59
- }, 200);
60
- });
61
- this.abortController = new AbortController();
62
- window.addEventListener("scroll", this.handleScroll, {
63
- passive: true,
64
- signal: this.abortController.signal,
65
- });
55
+ resizeTimeout = setTimeout(() => this.reLayout(), 200);
56
+ }, { signal: sig });
57
+ window.addEventListener("scroll", this.handleScroll, { passive: true, signal: sig });
66
58
  }
67
59
  reLayout() {
68
60
  const items = this.columns.flatMap(col => Array.from(col.children));
@@ -76,6 +68,14 @@ class MasonryGallery {
76
68
  }
77
69
  items.forEach(item => this.addToShortestColumn(item));
78
70
  }
71
+ handleScroll = () => {
72
+ if (this.isFetching)
73
+ return;
74
+ const rect = this.container.getBoundingClientRect();
75
+ if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
76
+ this.loadMoreImages();
77
+ }
78
+ };
79
79
  async loadMoreImages(isAutoFill = false) {
80
80
  if (!isAutoFill)
81
81
  this.reloaded++;
@@ -97,8 +97,6 @@ class MasonryGallery {
97
97
  finally {
98
98
  this.isFetching = false;
99
99
  this.toggleLoader(false);
100
- // If the rendered content doesn't fill the viewport, auto-load the next
101
- // batch without waiting for a scroll event (multi-column layout is shorter)
102
100
  requestAnimationFrame(() => {
103
101
  const rect = this.container.getBoundingClientRect();
104
102
  if (rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
@@ -1,12 +1,15 @@
1
+ /** A single subgroup item within a GroupPicker group. */
1
2
  interface SubgroupData {
2
3
  id: string;
3
4
  label: string;
4
5
  }
6
+ /** A group with an optional list of subgroups for GroupPicker. */
5
7
  interface GroupData {
6
8
  id: string;
7
9
  label: string;
8
10
  subgroups?: SubgroupData[];
9
11
  }
12
+ /** The current selection state returned by GroupPicker. */
10
13
  interface GroupPickerSelection {
11
14
  parentGroups: string[];
12
15
  subgroups: {
@@ -14,6 +17,7 @@ interface GroupPickerSelection {
14
17
  subgroupId: string;
15
18
  }[];
16
19
  }
20
+ /** Configuration options for the GroupPicker component. */
17
21
  interface GroupPickerOptions {
18
22
  onSelectionChange?: (selection: GroupPickerSelection) => void;
19
23
  searchPlaceholder?: string;
@@ -22,6 +26,7 @@ interface GroupPickerOptions {
22
26
  emptyLabel?: string;
23
27
  selectionPlaceholder?: string;
24
28
  }
29
+ /** Searchable picker for selecting groups and their subgroups. */
25
30
  declare class GroupPicker {
26
31
  private container;
27
32
  private data;
@@ -1,11 +1,18 @@
1
1
  import { escapeHtml } from './utils.js';
2
+ /** Searchable picker for selecting groups and their subgroups. */
2
3
  class GroupPicker {
4
+ container;
5
+ data;
6
+ options;
7
+ abortController;
8
+ selectedParents = new Set();
9
+ selectedSubs = new Map();
10
+ expandedGroups = new Set();
11
+ searchQuery = '';
12
+ searchInput;
13
+ listEl;
14
+ selectionEl;
3
15
  constructor(selector, data, options = {}) {
4
- // State
5
- this.selectedParents = new Set();
6
- this.selectedSubs = new Map();
7
- this.expandedGroups = new Set();
8
- this.searchQuery = '';
9
16
  const el = typeof selector === 'string'
10
17
  ? document.querySelector(selector)
11
18
  : selector;
@@ -31,11 +38,9 @@ class GroupPicker {
31
38
  }
32
39
  render() {
33
40
  this.container.innerHTML = '';
34
- // Selection summary — Basix .chips container
35
41
  this.selectionEl = document.createElement('div');
36
42
  this.selectionEl.className = 'chips group-picker__selection';
37
43
  this.selectionEl.dataset.placeholder = this.options.selectionPlaceholder;
38
- // Search — Basix form input with font icon overlay
39
44
  const searchWrap = document.createElement('div');
40
45
  searchWrap.className = 'group-picker__search';
41
46
  searchWrap.innerHTML = `
@@ -44,7 +49,6 @@ class GroupPicker {
44
49
  `;
45
50
  this.searchInput = searchWrap.querySelector('input');
46
51
  this.searchInput.placeholder = this.options.searchPlaceholder;
47
- // List
48
52
  this.listEl = document.createElement('div');
49
53
  this.listEl.className = 'group-picker__list';
50
54
  this.container.append(this.selectionEl, searchWrap, this.listEl);
@@ -99,15 +103,12 @@ class GroupPicker {
99
103
  ? this.highlightText(group.label, query)
100
104
  : escapeHtml(group.label);
101
105
  if (hasChildren) {
102
- // Chevron — Basix font icon
103
106
  const chevron = document.createElement('span');
104
107
  chevron.className = 'icon icon-navigate_next group-picker__chevron';
105
108
  chevron.setAttribute('aria-hidden', 'true');
106
- // Count — Basix badge
107
109
  const count = document.createElement('span');
108
110
  count.className = 'badge badge-sm';
109
111
  count.textContent = `${subs.length}`;
110
- // Action button — Basix button, button-primary when selected
111
112
  const actionBtn = document.createElement('button');
112
113
  actionBtn.className = 'group-picker__group-action';
113
114
  if (isParentSelected) {
@@ -125,14 +126,12 @@ class GroupPicker {
125
126
  header.addEventListener('click', () => {
126
127
  this.toggleExpand(group.id);
127
128
  }, { signal: this.abortController.signal });
128
- // Subgroups — Basix .chips container
129
129
  const subsContainer = document.createElement('div');
130
130
  subsContainer.className = 'group-picker__subgroups';
131
131
  const subsList = document.createElement('div');
132
132
  subsList.className = 'chips group-picker__subgroup-list';
133
133
  const displaySubs = query && !groupMatches ? matchingSubs : subs;
134
134
  for (const sub of displaySubs) {
135
- // Subgroup chip — Basix .chip.clickable
136
135
  const subEl = document.createElement('span');
137
136
  subEl.className = 'chip clickable group-picker__subgroup';
138
137
  subEl.dataset.subId = sub.id;
@@ -162,7 +161,6 @@ class GroupPicker {
162
161
  }
163
162
  }
164
163
  else {
165
- // Leaf group — Basix font icon check mark
166
164
  const checkEl = document.createElement('span');
167
165
  checkEl.className = 'icon icon-check group-picker__leaf-check';
168
166
  checkEl.setAttribute('aria-hidden', 'true');
@@ -194,7 +192,6 @@ class GroupPicker {
194
192
  }
195
193
  }
196
194
  }
197
- // Basix .chip.closeable structure
198
195
  createChip(label, isParent, onRemove) {
199
196
  const chip = document.createElement('span');
200
197
  chip.className = isParent
@@ -210,7 +207,6 @@ class GroupPicker {
210
207
  chip.append(document.createTextNode(label), btn);
211
208
  return chip;
212
209
  }
213
- // State management
214
210
  toggleParentGroup(groupId) {
215
211
  if (this.selectedParents.has(groupId)) {
216
212
  this.selectedParents.delete(groupId);
@@ -299,7 +295,6 @@ class GroupPicker {
299
295
  const regex = new RegExp(`(${escapedQuery})`, 'gi');
300
296
  return safeText.replace(regex, '<mark>$1</mark>');
301
297
  }
302
- // Public API
303
298
  getSelection() {
304
299
  const parentGroups = [...this.selectedParents];
305
300
  const subgroups = [];
package/js/lightbox.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ /** A single image entry for the Lightbox gallery. */
1
2
  interface LightboxImage {
2
3
  src: string;
3
4
  alt?: string;
4
5
  caption?: string;
5
6
  }
7
+ /** Configuration options for a Lightbox instance. */
6
8
  interface LightboxOptions {
7
9
  src?: string;
8
10
  alt?: string;
@@ -13,6 +15,7 @@ interface LightboxOptions {
13
15
  onOpen?: () => void;
14
16
  onClose?: () => void;
15
17
  }
18
+ /** Full-screen image viewer with gallery navigation, zoom, and touch support. */
16
19
  declare class Lightbox {
17
20
  private images;
18
21
  private currentIndex;
package/js/lightbox.js CHANGED
@@ -1,11 +1,17 @@
1
+ /** Full-screen image viewer with gallery navigation, zoom, and touch support. */
1
2
  class Lightbox {
3
+ images;
4
+ currentIndex;
5
+ closeable;
6
+ onOpen;
7
+ onClose;
8
+ wrapper = null;
9
+ imgEl = null;
10
+ captionEl = null;
11
+ counterEl = null;
12
+ isZoomed = false;
13
+ abortController = new AbortController();
2
14
  constructor(options) {
3
- this.wrapper = null;
4
- this.imgEl = null;
5
- this.captionEl = null;
6
- this.counterEl = null;
7
- this.isZoomed = false;
8
- this.abortController = new AbortController();
9
15
  if (options.images && options.images.length > 0) {
10
16
  this.images = options.images;
11
17
  }
package/js/modal.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  type ModalType = 'default' | 'success' | 'error' | 'warning' | 'info';
2
+ /** Configuration options for a Modal dialog. */
2
3
  interface ModalOptions {
3
4
  content: string;
4
5
  header?: string;
@@ -6,6 +7,7 @@ interface ModalOptions {
6
7
  closeable?: boolean;
7
8
  type?: ModalType;
8
9
  }
10
+ /** Overlay dialog with optional header, footer, close button, and type variants. */
9
11
  declare class Modal {
10
12
  private content;
11
13
  private readonly header?;
@@ -17,7 +19,7 @@ declare class Modal {
17
19
  constructor(options: ModalOptions);
18
20
  constructor(content: string, header?: string, footer?: string, closeable?: boolean, type?: ModalType);
19
21
  show(): void;
20
- hide(): void;
22
+ hide: () => void;
21
23
  private handleEscape;
22
24
  private handleBackgroundClick;
23
25
  private buildTemplate;
package/js/modal.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import { sanitizeHtml } from './utils.js';
2
2
  const CLOSE_ICON = '<div class="icon icon-close close"></div>';
3
+ /** Overlay dialog with optional header, footer, close button, and type variants. */
3
4
  class Modal {
5
+ content;
6
+ header;
7
+ footer;
8
+ closeable;
9
+ type;
10
+ template;
11
+ modalWrapper = null;
4
12
  constructor(contentOrOptions, header, footer, closeable = true, type = 'default') {
5
- this.modalWrapper = null;
6
13
  if (typeof contentOrOptions === 'object') {
7
14
  this.content = contentOrOptions.content;
8
15
  this.header = contentOrOptions.header;
@@ -18,9 +25,6 @@ class Modal {
18
25
  this.type = type;
19
26
  }
20
27
  this.template = this.buildTemplate();
21
- this.hide = this.hide.bind(this);
22
- this.handleEscape = this.handleEscape.bind(this);
23
- this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
24
28
  }
25
29
  show() {
26
30
  this.hide();
@@ -45,11 +49,10 @@ class Modal {
45
49
  wrapper.classList.add('is-visible');
46
50
  });
47
51
  }
48
- hide() {
52
+ hide = () => {
49
53
  const wrapper = this.modalWrapper;
50
54
  if (!wrapper)
51
55
  return;
52
- // Remove event listeners
53
56
  const closeBtn = wrapper.querySelector('.close');
54
57
  closeBtn?.removeEventListener('click', this.hide);
55
58
  const background = wrapper.querySelector('.modal-background');
@@ -63,17 +66,17 @@ class Modal {
63
66
  this.modalWrapper = null;
64
67
  }
65
68
  }, 300);
66
- }
67
- handleEscape(e) {
69
+ };
70
+ handleEscape = (e) => {
68
71
  if (e.key === 'Escape') {
69
72
  this.hide();
70
73
  }
71
- }
72
- handleBackgroundClick(e) {
74
+ };
75
+ handleBackgroundClick = (e) => {
73
76
  if (e.target?.classList.contains('modal-background')) {
74
77
  this.hide();
75
78
  }
76
- }
79
+ };
77
80
  buildTemplate() {
78
81
  const parts = ['<div class="modal">'];
79
82
  if (this.closeable) {
package/js/popover.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Placement } from './position.js';
2
2
  type PopoverPlacement = Placement | 'auto';
3
3
  type PopoverAlign = 'start' | 'center' | 'end';
4
4
  type PopoverTrigger = 'click' | 'hover';
5
+ /** Configuration options for a Popover instance. */
5
6
  interface PopoverOptions {
6
7
  content: string;
7
8
  placement?: PopoverPlacement;
@@ -15,6 +16,7 @@ interface PopoverOptions {
15
16
  onOpen?: () => void;
16
17
  onClose?: () => void;
17
18
  }
19
+ /** Anchored popover triggered by click or hover, with auto-placement and optional arrow. */
18
20
  declare class Popover {
19
21
  private static openPopovers;
20
22
  private static idCounter;
package/js/popover.js CHANGED
@@ -1,31 +1,16 @@
1
1
  import { computePosition } from './position.js';
2
2
  import { sanitizeHtml } from './utils.js';
3
- // Must match $arrow in popover.scss
4
3
  const ARROW_SIZE = 6;
4
+ /** Anchored popover triggered by click or hover, with auto-placement and optional arrow. */
5
5
  class Popover {
6
+ static openPopovers = new Set();
7
+ static idCounter = 0;
8
+ trigger;
9
+ opts;
10
+ popoverEl = null;
11
+ _isOpen = false;
12
+ hoverTimer = null;
6
13
  constructor(triggerEl, options) {
7
- this.popoverEl = null;
8
- this._isOpen = false;
9
- this.hoverTimer = null;
10
- // ── Event handlers ─────────────────────────────────────────────────────────
11
- this.onClick = () => { this.toggle(); };
12
- this.onMouseEnter = () => {
13
- if (this.hoverTimer !== null)
14
- clearTimeout(this.hoverTimer);
15
- this.open();
16
- };
17
- this.onMouseLeave = () => {
18
- this.hoverTimer = window.setTimeout(() => this.close(), 120);
19
- };
20
- this.onOutsideClick = (e) => {
21
- const t = e.target;
22
- if (!this.popoverEl?.contains(t) && !this.trigger.contains(t))
23
- this.close();
24
- };
25
- this.onEscape = (e) => {
26
- if (e.key === 'Escape')
27
- this.close();
28
- };
29
14
  const el = typeof triggerEl === 'string'
30
15
  ? document.querySelector(triggerEl)
31
16
  : triggerEl;
@@ -47,7 +32,6 @@ class Popover {
47
32
  };
48
33
  this.attachTrigger();
49
34
  }
50
- // ── Public API ─────────────────────────────────────────────────────────────
51
35
  get isOpen() { return this._isOpen; }
52
36
  open() {
53
37
  if (this._isOpen)
@@ -109,7 +93,6 @@ class Popover {
109
93
  });
110
94
  });
111
95
  }
112
- // ── Build ──────────────────────────────────────────────────────────────────
113
96
  buildEl() {
114
97
  const id = `popover-${++Popover.idCounter}`;
115
98
  const el = document.createElement('div');
@@ -117,8 +100,6 @@ class Popover {
117
100
  el.id = id;
118
101
  el.setAttribute('role', 'dialog');
119
102
  el.setAttribute('data-arrow', String(this.opts.arrow));
120
- // Wrap plain content in .popover-body so it gets proper padding.
121
- // Skip wrapping when content already uses structured popover elements.
122
103
  const hasStructure = /class="popover-(header|body|footer|menu)/.test(this.opts.content);
123
104
  const safeContent = sanitizeHtml(this.opts.content);
124
105
  el.innerHTML = hasStructure
@@ -128,7 +109,6 @@ class Popover {
128
109
  this.trigger.setAttribute('aria-controls', id);
129
110
  return el;
130
111
  }
131
- // ── Positioning ────────────────────────────────────────────────────────────
132
112
  reposition() {
133
113
  if (!this.popoverEl)
134
114
  return;
@@ -145,6 +125,24 @@ class Popover {
145
125
  this.popoverEl.style.left = `${left}px`;
146
126
  this.popoverEl.style.top = `${top}px`;
147
127
  }
128
+ onClick = () => { this.toggle(); };
129
+ onMouseEnter = () => {
130
+ if (this.hoverTimer !== null)
131
+ clearTimeout(this.hoverTimer);
132
+ this.open();
133
+ };
134
+ onMouseLeave = () => {
135
+ this.hoverTimer = window.setTimeout(() => this.close(), 120);
136
+ };
137
+ onOutsideClick = (e) => {
138
+ const t = e.target;
139
+ if (!this.popoverEl?.contains(t) && !this.trigger.contains(t))
140
+ this.close();
141
+ };
142
+ onEscape = (e) => {
143
+ if (e.key === 'Escape')
144
+ this.close();
145
+ };
148
146
  attachTrigger() {
149
147
  if (this.opts.triggerMode === 'click') {
150
148
  this.trigger.addEventListener('click', this.onClick);
@@ -160,6 +158,4 @@ class Popover {
160
158
  this.trigger.removeEventListener('mouseleave', this.onMouseLeave);
161
159
  }
162
160
  }
163
- Popover.openPopovers = new Set();
164
- Popover.idCounter = 0;
165
161
  export { Popover };
package/js/position.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  type Placement = 'top' | 'bottom' | 'left' | 'right';
6
6
  type Align = 'start' | 'center' | 'end';
7
+ /** Options accepted by `computePosition`. */
7
8
  interface PositionOptions {
8
9
  placement: Placement | 'auto';
9
10
  align?: Align;
@@ -11,6 +12,7 @@ interface PositionOptions {
11
12
  margin?: number;
12
13
  arrowSize?: number;
13
14
  }
15
+ /** Result returned by `computePosition`. */
14
16
  interface PositionResult {
15
17
  left: number;
16
18
  top: number;
package/js/position.js CHANGED
@@ -44,7 +44,6 @@ function computePosition(trigger, floating, opts) {
44
44
  const placement = opts.placement === 'auto'
45
45
  ? bestPlacement(trigger, floating, offset)
46
46
  : maybeFlip(opts.placement, trigger, floating, offset);
47
- // Main-axis offset
48
47
  let left = 0, top = 0;
49
48
  switch (placement) {
50
49
  case 'top':
@@ -60,7 +59,6 @@ function computePosition(trigger, floating, opts) {
60
59
  left = trigger.right + offset;
61
60
  break;
62
61
  }
63
- // Cross-axis alignment
64
62
  if (placement === 'top' || placement === 'bottom') {
65
63
  switch (align) {
66
64
  case 'start':
@@ -87,13 +85,11 @@ function computePosition(trigger, floating, opts) {
87
85
  break;
88
86
  }
89
87
  }
90
- // Clamp to viewport
91
88
  const l = Math.max(margin, Math.min(window.innerWidth - floating.width - margin, left));
92
89
  const t = Math.max(margin, Math.min(window.innerHeight - floating.height - margin, top));
93
- // Arrow offset: keep arrow centred on the trigger even after viewport clamping
94
90
  let arrowOffset;
95
91
  if (opts.arrowSize !== undefined) {
96
- const minOff = opts.arrowSize + 8; // min distance from rounded corner
92
+ const minOff = opts.arrowSize + 8;
97
93
  if (placement === 'top' || placement === 'bottom') {
98
94
  const raw = trigger.left + trigger.width / 2 - l;
99
95
  arrowOffset = Math.max(minOff, Math.min(floating.width - minOff, raw));
package/js/push-menu.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /** DOM element references managed by the PushMenu static class. */
1
2
  interface PushMenuElements {
2
3
  navigation: HTMLElement | null;
3
4
  content: HTMLElement | null;
@@ -6,6 +7,7 @@ interface PushMenuElements {
6
7
  controlIcon: HTMLElement | null;
7
8
  backdrop: HTMLElement | null;
8
9
  }
10
+ /** Static class that manages a push-style side navigation panel. */
9
11
  declare class PushMenu {
10
12
  private static elements;
11
13
  private static initialized;
package/js/push-menu.js CHANGED
@@ -1,4 +1,16 @@
1
+ /** Static class that manages a push-style side navigation panel. */
1
2
  class PushMenu {
3
+ static elements = {
4
+ navigation: null,
5
+ content: null,
6
+ menu: null,
7
+ header: null,
8
+ controlIcon: null,
9
+ backdrop: null
10
+ };
11
+ static initialized = false;
12
+ static panelStack = [];
13
+ static boundHandleNavigationChange;
2
14
  static init() {
3
15
  if (this.initialized) {
4
16
  console.warn('PushMenu: Already initialized');
@@ -14,7 +26,6 @@ class PushMenu {
14
26
  this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
15
27
  this.initialized = true;
16
28
  }
17
- // ─── Panel construction ────────────────────────────────────────────────
18
29
  static buildPanels() {
19
30
  const menu = this.elements.menu;
20
31
  if (!menu)
@@ -22,18 +33,15 @@ class PushMenu {
22
33
  const rootUl = menu.querySelector(':scope > ul');
23
34
  if (!rootUl)
24
35
  return;
25
- // Wrap root ul in a panel
26
36
  const rootPanel = document.createElement('div');
27
37
  rootPanel.classList.add('push-menu-panel', 'is-active');
28
38
  rootPanel.dataset.level = '0';
29
39
  rootPanel.appendChild(rootUl);
30
40
  menu.appendChild(rootPanel);
31
- // Recursively extract nested uls into sibling panels
32
41
  this.extractSubPanels(rootPanel, 1);
33
42
  this.panelStack = [rootPanel];
34
43
  }
35
44
  static extractSubPanels(panel, level) {
36
- // Collect all uls currently in this panel before any mutations
37
45
  const uls = Array.from(panel.querySelectorAll('ul'));
38
46
  for (const ul of uls) {
39
47
  const listItems = Array.from(ul.children);
@@ -41,14 +49,11 @@ class PushMenu {
41
49
  const childUl = li.querySelector(':scope > ul');
42
50
  if (!childUl)
43
51
  continue;
44
- // Determine label from the immediate anchor child
45
52
  const parentAnchor = li.querySelector(':scope > a');
46
53
  const title = parentAnchor?.textContent?.trim() ?? '';
47
- // ── Build sub-panel ──────────────────────────────────────
48
54
  const subPanel = document.createElement('div');
49
55
  subPanel.classList.add('push-menu-panel');
50
56
  subPanel.dataset.level = String(level);
51
- // Header: back button + breadcrumb title
52
57
  const header = document.createElement('div');
53
58
  header.classList.add('push-menu-panel-header');
54
59
  const backBtn = document.createElement('button');
@@ -62,15 +67,11 @@ class PushMenu {
62
67
  header.appendChild(backBtn);
63
68
  header.appendChild(titleEl);
64
69
  subPanel.appendChild(header);
65
- // Move the child ul into the sub-panel
66
70
  subPanel.appendChild(childUl);
67
- // Append sub-panel as sibling inside the nav
68
71
  this.elements.menu?.appendChild(subPanel);
69
- // ── Replace anchor with a trigger span in the parent li ──
70
72
  const trigger = document.createElement('span');
71
73
  trigger.classList.add('push-menu-item');
72
74
  trigger.textContent = title;
73
- // Chevron icon
74
75
  const chevron = document.createElement('span');
75
76
  chevron.classList.add('push-menu-chevron');
76
77
  chevron.setAttribute('aria-hidden', 'true');
@@ -83,12 +84,10 @@ class PushMenu {
83
84
  li.prepend(trigger);
84
85
  }
85
86
  trigger.addEventListener('click', () => PushMenu.openPanel(subPanel));
86
- // Recurse into the newly created sub-panel
87
87
  this.extractSubPanels(subPanel, level + 1);
88
88
  }
89
89
  }
90
90
  }
91
- // ─── Panel navigation ──────────────────────────────────────────────────
92
91
  static openPanel(panel) {
93
92
  const currentPanel = this.panelStack[this.panelStack.length - 1];
94
93
  currentPanel.classList.remove('is-active');
@@ -109,7 +108,6 @@ class PushMenu {
109
108
  const menu = this.elements.menu;
110
109
  if (!menu)
111
110
  return;
112
- // Wait for the close animation before snapping panels back
113
111
  setTimeout(() => {
114
112
  const panels = Array.from(menu.querySelectorAll('.push-menu-panel'));
115
113
  panels.forEach((panel, index) => {
@@ -122,7 +120,6 @@ class PushMenu {
122
120
  }
123
121
  }, 300);
124
122
  }
125
- // ─── Open / close ──────────────────────────────────────────────────────
126
123
  static handleNavigationChange() {
127
124
  const isPushed = this.elements.content?.classList.contains('pushed') ?? false;
128
125
  if (!isPushed) {
@@ -154,6 +151,16 @@ class PushMenu {
154
151
  }
155
152
  }
156
153
  }
154
+ static clickNav = () => {
155
+ const navigation = PushMenu.elements.navigation;
156
+ navigation?.click();
157
+ };
158
+ static handleBackdropClick = () => {
159
+ if (PushMenu.isOpen()) {
160
+ const navigation = PushMenu.elements.navigation;
161
+ navigation?.click();
162
+ }
163
+ };
157
164
  static open() {
158
165
  if (!this.elements.content?.classList.contains('pushed')) {
159
166
  this.pushToggle();
@@ -194,24 +201,4 @@ class PushMenu {
194
201
  this.elements.backdrop = document.querySelector('.push-menu-backdrop');
195
202
  }
196
203
  }
197
- PushMenu.elements = {
198
- navigation: null,
199
- content: null,
200
- menu: null,
201
- header: null,
202
- controlIcon: null,
203
- backdrop: null
204
- };
205
- PushMenu.initialized = false;
206
- PushMenu.panelStack = [];
207
- PushMenu.clickNav = () => {
208
- const navigation = PushMenu.elements.navigation;
209
- navigation?.click();
210
- };
211
- PushMenu.handleBackdropClick = () => {
212
- if (PushMenu.isOpen()) {
213
- const navigation = PushMenu.elements.navigation;
214
- navigation?.click();
215
- }
216
- };
217
204
  export { PushMenu };
@@ -1,3 +1,4 @@
1
+ /** Enhances a native range input with a CSS fill-percentage custom property. */
1
2
  declare class RangeSlider {
2
3
  private readonly input;
3
4
  constructor(input: HTMLInputElement);