@dodlhuat/basix 1.3.2 → 1.3.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 (119) hide show
  1. package/README.md +13 -7
  2. package/css/accordion.scss +0 -5
  3. package/css/badge.scss +9 -6
  4. package/css/bottom-sheet.scss +3 -8
  5. package/css/breadcrumb.scss +6 -15
  6. package/css/button.scss +63 -16
  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 +13 -7
  14. package/css/code-viewer.scss +1 -5
  15. package/css/colors.scss +4 -0
  16. package/css/context-menu.scss +4 -6
  17. package/css/datepicker.scss +4 -7
  18. package/css/docs.scss +0 -8
  19. package/css/dropdown.scss +15 -1
  20. package/css/editor.scss +1 -23
  21. package/css/file-uploader.scss +2 -2
  22. package/css/flyout-menu.scss +66 -44
  23. package/css/form.scss +0 -28
  24. package/css/gallery.scss +2 -3
  25. package/css/group-picker.scss +5 -35
  26. package/css/icons.scss +0 -3
  27. package/css/lightbox.scss +2 -4
  28. package/css/mixins.scss +8 -0
  29. package/css/modal.scss +24 -3
  30. package/css/parameters.scss +6 -1
  31. package/css/popover.scss +3 -15
  32. package/css/progress.scss +0 -6
  33. package/css/push-menu.scss +3 -28
  34. package/css/radiobutton.scss +2 -1
  35. package/css/range-slider.scss +1 -7
  36. package/css/scrollbar.scss +2 -6
  37. package/css/sidebar-nav.scss +24 -11
  38. package/css/stepper.scss +0 -4
  39. package/css/style.css +287 -89
  40. package/css/style.css.map +1 -1
  41. package/css/style.min.css +1 -1
  42. package/css/style.min.css.map +1 -1
  43. package/css/style.scss +1 -1
  44. package/css/table.scss +0 -4
  45. package/css/tabs.scss +49 -2
  46. package/css/timeline.scss +1 -13
  47. package/css/timepicker.scss +6 -7
  48. package/css/toast.scss +1 -1
  49. package/css/tooltip.scss +1 -5
  50. package/css/tree.scss +1 -1
  51. package/css/typography.scss +38 -9
  52. package/css/virtual-dropdown.scss +3 -28
  53. package/js/bottom-sheet.d.ts +3 -1
  54. package/js/bottom-sheet.js +26 -27
  55. package/js/calendar.d.ts +7 -0
  56. package/js/calendar.js +14 -33
  57. package/js/carousel.d.ts +2 -0
  58. package/js/carousel.js +13 -5
  59. package/js/chart.d.ts +4 -0
  60. package/js/chart.js +13 -31
  61. package/js/code-viewer.d.ts +1 -0
  62. package/js/code-viewer.js +4 -0
  63. package/js/context-menu.d.ts +9 -2
  64. package/js/context-menu.js +17 -14
  65. package/js/datepicker.d.ts +4 -0
  66. package/js/datepicker.js +26 -11
  67. package/js/dropdown.d.ts +4 -3
  68. package/js/dropdown.js +17 -9
  69. package/js/editor.d.ts +1 -0
  70. package/js/editor.js +9 -3
  71. package/js/file-uploader.d.ts +4 -0
  72. package/js/file-uploader.js +52 -43
  73. package/js/flyout-menu.d.ts +5 -3
  74. package/js/flyout-menu.js +23 -46
  75. package/js/gallery.d.ts +5 -0
  76. package/js/gallery.js +44 -26
  77. package/js/group-picker.d.ts +5 -0
  78. package/js/group-picker.js +12 -17
  79. package/js/lightbox.d.ts +3 -0
  80. package/js/lightbox.js +12 -6
  81. package/js/modal.d.ts +3 -1
  82. package/js/modal.js +15 -12
  83. package/js/popover.d.ts +2 -0
  84. package/js/popover.js +26 -30
  85. package/js/position.d.ts +2 -0
  86. package/js/position.js +1 -5
  87. package/js/push-menu.d.ts +2 -0
  88. package/js/push-menu.js +22 -35
  89. package/js/range-slider.d.ts +1 -0
  90. package/js/range-slider.js +5 -3
  91. package/js/scroll.d.ts +2 -0
  92. package/js/scroll.js +1 -0
  93. package/js/scrollbar.d.ts +2 -0
  94. package/js/scrollbar.js +24 -36
  95. package/js/select.d.ts +1 -0
  96. package/js/select.js +5 -10
  97. package/js/sidebar-nav.d.ts +12 -0
  98. package/js/sidebar-nav.js +47 -0
  99. package/js/stepper.d.ts +2 -0
  100. package/js/stepper.js +7 -1
  101. package/js/table.d.ts +4 -0
  102. package/js/table.js +15 -22
  103. package/js/tabs.d.ts +2 -0
  104. package/js/tabs.js +6 -14
  105. package/js/theme.d.ts +1 -0
  106. package/js/theme.js +5 -13
  107. package/js/timepicker.d.ts +3 -0
  108. package/js/timepicker.js +81 -67
  109. package/js/toast.d.ts +3 -0
  110. package/js/toast.js +24 -15
  111. package/js/tooltip.d.ts +2 -0
  112. package/js/tooltip.js +21 -19
  113. package/js/tree.d.ts +3 -0
  114. package/js/tree.js +13 -0
  115. package/js/utils.d.ts +1 -3
  116. package/js/utils.js +0 -3
  117. package/js/virtual-dropdown.d.ts +3 -0
  118. package/js/virtual-dropdown.js +25 -0
  119. package/package.json +2 -2
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,19 +66,19 @@ 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
- const parts = ['<div class="modal">'];
81
+ const parts = [`<div class="modal modal-${this.type}">`];
79
82
  if (this.closeable) {
80
83
  parts.push(CLOSE_ICON);
81
84
  }
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);
@@ -1,8 +1,7 @@
1
+ /** Enhances a native range input with a CSS fill-percentage custom property. */
1
2
  class RangeSlider {
3
+ input;
2
4
  constructor(input) {
3
- this.handleInput = () => {
4
- this.update();
5
- };
6
5
  this.input = input;
7
6
  this.update();
8
7
  this.input.addEventListener('input', this.handleInput);
@@ -18,6 +17,9 @@ class RangeSlider {
18
17
  const pct = ((+this.input.value - min) / (max - min)) * 100;
19
18
  this.input.style.setProperty('--range-fill', `${pct}%`);
20
19
  }
20
+ handleInput = () => {
21
+ this.update();
22
+ };
21
23
  destroy() {
22
24
  this.input.removeEventListener('input', this.handleInput);
23
25
  this.input.style.removeProperty('--range-fill');
package/js/scroll.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ /** Options for the Scroll.to() utility. */
1
2
  interface ScrollOptions {
2
3
  behavior?: ScrollBehavior;
3
4
  offset?: number;
4
5
  block?: ScrollLogicalPosition;
5
6
  }
7
+ /** Static utility for smooth-scrolling to a target element with header offset support. */
6
8
  declare class Scroll {
7
9
  static to(target: string | Element, options?: ScrollOptions): void;
8
10
  }
package/js/scroll.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** Static utility for smooth-scrolling to a target element with header offset support. */
1
2
  class Scroll {
2
3
  static to(target, options = {}) {
3
4
  const fixed_header = document.querySelector('.main-header');
package/js/scrollbar.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ /** DOM element references for a Scrollbar instance. */
1
2
  interface ScrollbarElements {
2
3
  viewport: HTMLElement;
3
4
  content: HTMLElement;
4
5
  track: HTMLElement;
5
6
  thumb: HTMLElement;
6
7
  }
8
+ /** Custom scrollbar overlay that replaces the native scrollbar with a draggable thumb. */
7
9
  declare class Scrollbar {
8
10
  private static readonly instances;
9
11
  private static activeInstance;
package/js/scrollbar.js CHANGED
@@ -1,19 +1,36 @@
1
+ /** Custom scrollbar overlay that replaces the native scrollbar with a draggable thumb. */
1
2
  class Scrollbar {
3
+ static instances = new WeakMap();
4
+ static activeInstance = null;
5
+ static globalListenersInstalled = false;
6
+ static instanceCount = 0;
7
+ static globalListenerAbortController = null;
8
+ container;
9
+ viewport;
10
+ content;
11
+ track;
12
+ thumb;
13
+ MIN_THUMB_HEIGHT;
14
+ ro;
15
+ dragging = false;
16
+ activePointerId = null;
17
+ startPointerY = 0;
18
+ startThumbTop = 0;
19
+ boundPointerMove;
20
+ boundPointerUp;
21
+ boundThumbPointerDown;
22
+ boundTrackClick;
23
+ boundViewportScroll;
24
+ boundUpdateThumb;
25
+ boundContainerWheel;
2
26
  constructor(container) {
3
- this.dragging = false;
4
- this.activePointerId = null;
5
- this.startPointerY = 0;
6
- this.startThumbTop = 0;
7
27
  this.container = container;
8
- // Query and validate required elements
9
28
  const elements = this.getRequiredElements(container);
10
29
  this.viewport = elements.viewport;
11
30
  this.content = elements.content;
12
31
  this.track = elements.track;
13
32
  this.thumb = elements.thumb;
14
- // Get minimum thumb height from CSS variable or use default
15
33
  this.MIN_THUMB_HEIGHT = this.getMinThumbHeight();
16
- // Bind all event handlers once
17
34
  this.boundPointerMove = this.handlePointerMove.bind(this);
18
35
  this.boundPointerUp = this.handlePointerUp.bind(this);
19
36
  this.boundThumbPointerDown = this.handleThumbPointerDown.bind(this);
@@ -21,17 +38,13 @@ class Scrollbar {
21
38
  this.boundViewportScroll = this.updateThumb.bind(this);
22
39
  this.boundUpdateThumb = this.updateThumb.bind(this);
23
40
  this.boundContainerWheel = this.handleContainerWheel.bind(this);
24
- // Setup ResizeObserver
25
41
  this.ro = new ResizeObserver(this.boundUpdateThumb);
26
- // Initialize
27
42
  this.attachEventListeners();
28
43
  Scrollbar.instances.set(container, this);
29
- // Track instances and install global listeners once for all
30
44
  Scrollbar.instanceCount++;
31
45
  if (!Scrollbar.globalListenersInstalled) {
32
46
  Scrollbar.installGlobalListeners();
33
47
  }
34
- // Initial thumb update
35
48
  requestAnimationFrame(this.boundUpdateThumb);
36
49
  }
37
50
  getRequiredElements(container) {
@@ -68,12 +81,10 @@ class Scrollbar {
68
81
  Scrollbar.globalListenersInstalled = true;
69
82
  }
70
83
  attachEventListeners() {
71
- // Instance-specific events
72
84
  this.viewport.addEventListener('scroll', this.boundViewportScroll, { passive: true });
73
85
  this.thumb.addEventListener('pointerdown', this.boundThumbPointerDown);
74
86
  this.track.addEventListener('click', this.boundTrackClick);
75
87
  this.container.addEventListener('wheel', this.boundContainerWheel, { passive: false });
76
- // Observe size changes
77
88
  this.ro.observe(this.viewport);
78
89
  this.ro.observe(this.content);
79
90
  window.addEventListener('resize', this.boundUpdateThumb);
@@ -82,17 +93,14 @@ class Scrollbar {
82
93
  const viewportHeight = this.viewport.clientHeight;
83
94
  const contentHeight = this.content.scrollHeight;
84
95
  const trackHeight = this.track.clientHeight;
85
- // Hide thumb if content fits in viewport
86
96
  if (contentHeight <= viewportHeight + 1) {
87
97
  this.thumb.style.display = 'none';
88
98
  return;
89
99
  }
90
100
  this.thumb.style.display = '';
91
- // Calculate thumb size
92
101
  const ratio = viewportHeight / contentHeight;
93
102
  const thumbHeight = Math.max(Math.floor(ratio * trackHeight), this.MIN_THUMB_HEIGHT);
94
103
  this.thumb.style.height = `${thumbHeight}px`;
95
- // Calculate thumb position
96
104
  const maxScroll = contentHeight - viewportHeight;
97
105
  const maxThumbTop = trackHeight - thumbHeight;
98
106
  const scrollRatio = this.viewport.scrollTop / (maxScroll || 1);
@@ -104,7 +112,6 @@ class Scrollbar {
104
112
  this.dragging = true;
105
113
  this.activePointerId = e.pointerId;
106
114
  Scrollbar.activeInstance = this;
107
- // Capture pointer for reliable tracking
108
115
  try {
109
116
  this.thumb.setPointerCapture(e.pointerId);
110
117
  }
@@ -115,11 +122,9 @@ class Scrollbar {
115
122
  const thumbRect = this.thumb.getBoundingClientRect();
116
123
  const trackRect = this.track.getBoundingClientRect();
117
124
  this.startThumbTop = thumbRect.top - trackRect.top;
118
- // Prevent text selection during drag
119
125
  document.body.style.userSelect = 'none';
120
126
  }
121
127
  handlePointerMove(e) {
122
- // Only handle events for the active pointer
123
128
  if (!this.dragging || this.activePointerId !== e.pointerId) {
124
129
  return;
125
130
  }
@@ -128,10 +133,8 @@ class Scrollbar {
128
133
  const trackHeight = this.track.clientHeight;
129
134
  const thumbHeight = this.thumb.clientHeight;
130
135
  const maxThumbTop = trackHeight - thumbHeight;
131
- // Calculate new thumb position
132
136
  const newThumbTop = Math.max(0, Math.min(maxThumbTop, this.startThumbTop + pointerDelta));
133
137
  this.thumb.style.top = `${newThumbTop}px`;
134
- // Update viewport scroll position
135
138
  const contentHeight = this.content.scrollHeight;
136
139
  const viewportHeight = this.viewport.clientHeight;
137
140
  const maxScroll = contentHeight - viewportHeight;
@@ -143,7 +146,6 @@ class Scrollbar {
143
146
  return;
144
147
  }
145
148
  this.dragging = false;
146
- // Release pointer capture
147
149
  try {
148
150
  this.thumb.releasePointerCapture(e.pointerId);
149
151
  }
@@ -155,7 +157,6 @@ class Scrollbar {
155
157
  document.body.style.userSelect = '';
156
158
  }
157
159
  handleTrackClick(e) {
158
- // Ignore clicks directly on the thumb
159
160
  if (e.target === this.thumb) {
160
161
  return;
161
162
  }
@@ -163,11 +164,9 @@ class Scrollbar {
163
164
  const clickY = e.clientY - trackRect.top;
164
165
  const thumbHeight = this.thumb.clientHeight;
165
166
  const trackHeight = this.track.clientHeight;
166
- // Center thumb on click position
167
167
  const targetThumbTop = clickY - thumbHeight / 2;
168
168
  const maxThumbTop = trackHeight - thumbHeight;
169
169
  const clampedThumbTop = Math.max(0, Math.min(maxThumbTop, targetThumbTop));
170
- // Calculate corresponding scroll position
171
170
  const contentHeight = this.content.scrollHeight;
172
171
  const viewportHeight = this.viewport.clientHeight;
173
172
  const maxScroll = contentHeight - viewportHeight;
@@ -186,21 +185,16 @@ class Scrollbar {
186
185
  this.viewport.scrollTop += e.deltaY;
187
186
  }
188
187
  destroy() {
189
- // Remove event listeners
190
188
  this.viewport.removeEventListener('scroll', this.boundViewportScroll);
191
189
  this.thumb.removeEventListener('pointerdown', this.boundThumbPointerDown);
192
190
  this.track.removeEventListener('click', this.boundTrackClick);
193
191
  this.container.removeEventListener('wheel', this.boundContainerWheel);
194
192
  window.removeEventListener('resize', this.boundUpdateThumb);
195
- // Disconnect observer
196
193
  this.ro.disconnect();
197
- // Clear from instances map
198
194
  Scrollbar.instances.delete(this.container);
199
- // Clear active instance if this was it
200
195
  if (Scrollbar.activeInstance === this) {
201
196
  Scrollbar.activeInstance = null;
202
197
  }
203
- // Uninstall global listeners when last instance is destroyed
204
198
  Scrollbar.instanceCount--;
205
199
  if (Scrollbar.instanceCount === 0) {
206
200
  Scrollbar.globalListenerAbortController?.abort();
@@ -208,7 +202,6 @@ class Scrollbar {
208
202
  Scrollbar.globalListenersInstalled = false;
209
203
  }
210
204
  }
211
- // Static factory methods
212
205
  static create(elementOrSelector) {
213
206
  const container = typeof elementOrSelector === 'string'
214
207
  ? document.querySelector(elementOrSelector)
@@ -232,9 +225,4 @@ class Scrollbar {
232
225
  return Scrollbar.instances.get(container);
233
226
  }
234
227
  }
235
- Scrollbar.instances = new WeakMap();
236
- Scrollbar.activeInstance = null;
237
- Scrollbar.globalListenersInstalled = false;
238
- Scrollbar.instanceCount = 0;
239
- Scrollbar.globalListenerAbortController = null;
240
228
  export { Scrollbar };
package/js/select.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /** Enhances a native `<select>` element with a custom styled dropdown. */
1
2
  declare class Select {
2
3
  private readonly element;
3
4
  private readonly isMultiselect;