@dodlhuat/basix 1.1.0 → 1.2.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 (274) hide show
  1. package/README.md +651 -482
  2. package/css/badge.scss +104 -0
  3. package/css/bottom-sheet.scss +192 -0
  4. package/css/breadcrumb.scss +158 -0
  5. package/css/context-menu.scss +182 -0
  6. package/css/editor.scss +628 -461
  7. package/css/form.scss +139 -0
  8. package/css/stepper.scss +212 -0
  9. package/css/style.css +1495 -70
  10. package/css/style.css.map +1 -1
  11. package/css/style.min.css +1 -1
  12. package/css/style.scss +7 -1
  13. package/css/typography.scss +194 -161
  14. package/js/bottom-sheet.js +173 -0
  15. package/js/bottom-sheet.ts +222 -0
  16. package/js/carousel.js +26 -13
  17. package/js/context-menu.js +212 -0
  18. package/js/context-menu.ts +252 -0
  19. package/js/editor.js +46 -32
  20. package/js/editor.ts +56 -37
  21. package/js/gallery.js +11 -10
  22. package/js/index.js +472 -374
  23. package/js/index.ts +116 -2
  24. package/js/push-menu.js +113 -113
  25. package/js/stepper.js +80 -0
  26. package/js/stepper.ts +104 -0
  27. package/js/timepicker.js +21 -8
  28. package/package.json +3 -2
  29. package/fonts/Outfit-VariableFont_wght.woff +0 -0
  30. package/fonts/material-icons.woff2 +0 -0
  31. package/icons/activity-outline.svg +0 -1
  32. package/icons/alert-circle-outline.svg +0 -1
  33. package/icons/alert-triangle-outline.svg +0 -1
  34. package/icons/archive-outline.svg +0 -1
  35. package/icons/arrow-back-outline.svg +0 -1
  36. package/icons/arrow-circle-down-outline.svg +0 -1
  37. package/icons/arrow-circle-left-outline.svg +0 -1
  38. package/icons/arrow-circle-right-outline.svg +0 -1
  39. package/icons/arrow-circle-up-outline.svg +0 -1
  40. package/icons/arrow-down-outline.svg +0 -1
  41. package/icons/arrow-downward-outline.svg +0 -1
  42. package/icons/arrow-forward-outline.svg +0 -1
  43. package/icons/arrow-ios-back-outline.svg +0 -1
  44. package/icons/arrow-ios-downward-outline.svg +0 -1
  45. package/icons/arrow-ios-forward-outline.svg +0 -1
  46. package/icons/arrow-ios-upward-outline.svg +0 -1
  47. package/icons/arrow-left-outline.svg +0 -1
  48. package/icons/arrow-right-outline.svg +0 -1
  49. package/icons/arrow-up-outline.svg +0 -1
  50. package/icons/arrow-upward-outline.svg +0 -1
  51. package/icons/arrowhead-down-outline.svg +0 -1
  52. package/icons/arrowhead-left-outline.svg +0 -1
  53. package/icons/arrowhead-right-outline.svg +0 -1
  54. package/icons/arrowhead-up-outline.svg +0 -1
  55. package/icons/at-outline.svg +0 -1
  56. package/icons/attach-2-outline.svg +0 -1
  57. package/icons/attach-outline.svg +0 -1
  58. package/icons/award-outline.svg +0 -1
  59. package/icons/backspace-outline.svg +0 -1
  60. package/icons/bar-chart-2-outline.svg +0 -1
  61. package/icons/bar-chart-outline.svg +0 -1
  62. package/icons/battery-outline.svg +0 -1
  63. package/icons/behance-outline.svg +0 -1
  64. package/icons/bell-off-outline.svg +0 -1
  65. package/icons/bell-outline.svg +0 -1
  66. package/icons/bluetooth-outline.svg +0 -1
  67. package/icons/book-open-outline.svg +0 -1
  68. package/icons/book-outline.svg +0 -1
  69. package/icons/bookmark-outline.svg +0 -1
  70. package/icons/briefcase-outline.svg +0 -1
  71. package/icons/browser-outline.svg +0 -1
  72. package/icons/brush-outline.svg +0 -1
  73. package/icons/bulb-outline.svg +0 -1
  74. package/icons/calendar-outline.svg +0 -1
  75. package/icons/camera-outline.svg +0 -1
  76. package/icons/car-outline.svg +0 -1
  77. package/icons/cast-outline.svg +0 -1
  78. package/icons/charging-outline.svg +0 -1
  79. package/icons/checkmark-circle-2-outline.svg +0 -1
  80. package/icons/checkmark-circle-outline.svg +0 -1
  81. package/icons/checkmark-outline.svg +0 -1
  82. package/icons/checkmark-square-2-outline.svg +0 -1
  83. package/icons/checkmark-square-outline.svg +0 -1
  84. package/icons/chevron-down-outline.svg +0 -1
  85. package/icons/chevron-left-outline.svg +0 -1
  86. package/icons/chevron-right-outline.svg +0 -1
  87. package/icons/chevron-up-outline.svg +0 -1
  88. package/icons/clipboard-outline.svg +0 -1
  89. package/icons/clock-outline.svg +0 -1
  90. package/icons/close-circle-outline.svg +0 -1
  91. package/icons/close-outline.svg +0 -1
  92. package/icons/close-square-outline.svg +0 -1
  93. package/icons/cloud-download-outline.svg +0 -1
  94. package/icons/cloud-upload-outline.svg +0 -1
  95. package/icons/code-download-outline.svg +0 -1
  96. package/icons/code-outline.svg +0 -1
  97. package/icons/collapse-outline.svg +0 -1
  98. package/icons/color-palette-outline.svg +0 -1
  99. package/icons/color-picker-outline.svg +0 -1
  100. package/icons/compass-outline.svg +0 -1
  101. package/icons/copy-outline.svg +0 -1
  102. package/icons/corner-down-left-outline.svg +0 -1
  103. package/icons/corner-down-right-outline.svg +0 -1
  104. package/icons/corner-left-down-outline.svg +0 -1
  105. package/icons/corner-left-up-outline.svg +0 -1
  106. package/icons/corner-right-down-outline.svg +0 -1
  107. package/icons/corner-right-up-outline.svg +0 -1
  108. package/icons/corner-up-left-outline.svg +0 -1
  109. package/icons/corner-up-right-outline.svg +0 -1
  110. package/icons/credit-card-outline.svg +0 -1
  111. package/icons/crop-outline.svg +0 -1
  112. package/icons/cube-outline.svg +0 -1
  113. package/icons/diagonal-arrow-left-down-outline.svg +0 -1
  114. package/icons/diagonal-arrow-left-up-outline.svg +0 -1
  115. package/icons/diagonal-arrow-right-down-outline.svg +0 -1
  116. package/icons/diagonal-arrow-right-up-outline.svg +0 -1
  117. package/icons/done-all-outline.svg +0 -1
  118. package/icons/download-outline.svg +0 -1
  119. package/icons/droplet-off-outline.svg +0 -1
  120. package/icons/droplet-outline.svg +0 -1
  121. package/icons/edit-2-outline.svg +0 -1
  122. package/icons/edit-outline.svg +0 -1
  123. package/icons/email-outline.svg +0 -1
  124. package/icons/expand-outline.svg +0 -1
  125. package/icons/external-link-outline.svg +0 -1
  126. package/icons/eye-off-2-outline.svg +0 -1
  127. package/icons/eye-off-outline.svg +0 -1
  128. package/icons/eye-outline.svg +0 -1
  129. package/icons/facebook-outline.svg +0 -1
  130. package/icons/file-add-outline.svg +0 -1
  131. package/icons/file-outline.svg +0 -1
  132. package/icons/file-remove-outline.svg +0 -1
  133. package/icons/file-text-outline.svg +0 -1
  134. package/icons/film-outline.svg +0 -1
  135. package/icons/flag-outline.svg +0 -1
  136. package/icons/flash-off-outline.svg +0 -1
  137. package/icons/flash-outline.svg +0 -1
  138. package/icons/flip-2-outline.svg +0 -1
  139. package/icons/flip-outline.svg +0 -1
  140. package/icons/folder-add-outline.svg +0 -1
  141. package/icons/folder-outline.svg +0 -1
  142. package/icons/folder-remove-outline.svg +0 -1
  143. package/icons/funnel-outline.svg +0 -1
  144. package/icons/gift-outline.svg +0 -1
  145. package/icons/github-outline.svg +0 -1
  146. package/icons/globe-2-outline.svg +0 -1
  147. package/icons/globe-outline.svg +0 -1
  148. package/icons/google-outline.svg +0 -1
  149. package/icons/grid-outline.svg +0 -1
  150. package/icons/hard-drive-outline.svg +0 -1
  151. package/icons/hash-outline.svg +0 -1
  152. package/icons/headphones-outline.svg +0 -1
  153. package/icons/heart-outline.svg +0 -1
  154. package/icons/home-outline.svg +0 -1
  155. package/icons/image-outline.svg +0 -1
  156. package/icons/inbox-outline.svg +0 -1
  157. package/icons/info-outline.svg +0 -1
  158. package/icons/keypad-outline.svg +0 -1
  159. package/icons/layers-outline.svg +0 -1
  160. package/icons/layout-outline.svg +0 -1
  161. package/icons/link-2-outline.svg +0 -1
  162. package/icons/link-outline.svg +0 -1
  163. package/icons/linkedin-outline.svg +0 -1
  164. package/icons/list-outline.svg +0 -1
  165. package/icons/loader-outline.svg +0 -1
  166. package/icons/lock-outline.svg +0 -1
  167. package/icons/log-in-outline.svg +0 -1
  168. package/icons/log-out-outline.svg +0 -1
  169. package/icons/map-outline.svg +0 -1
  170. package/icons/maximize-outline.svg +0 -1
  171. package/icons/menu-2-outline.svg +0 -1
  172. package/icons/menu-arrow-outline.svg +0 -1
  173. package/icons/menu-outline.svg +0 -1
  174. package/icons/message-circle-outline.svg +0 -1
  175. package/icons/message-square-outline.svg +0 -1
  176. package/icons/mic-off-outline.svg +0 -1
  177. package/icons/mic-outline.svg +0 -1
  178. package/icons/minimize-outline.svg +0 -1
  179. package/icons/minus-circle-outline.svg +0 -1
  180. package/icons/minus-outline.svg +0 -1
  181. package/icons/minus-square-outline.svg +0 -1
  182. package/icons/monitor-outline.svg +0 -1
  183. package/icons/moon-outline.svg +0 -1
  184. package/icons/more-horizontal-outline.svg +0 -1
  185. package/icons/more-vertical-outline.svg +0 -1
  186. package/icons/move-outline.svg +0 -1
  187. package/icons/music-outline.svg +0 -1
  188. package/icons/navigation-2-outline.svg +0 -1
  189. package/icons/navigation-outline.svg +0 -1
  190. package/icons/npm-outline.svg +0 -1
  191. package/icons/options-2-outline.svg +0 -1
  192. package/icons/options-outline.svg +0 -1
  193. package/icons/pantone-outline.svg +0 -1
  194. package/icons/paper-plane-outline.svg +0 -1
  195. package/icons/pause-circle-outline.svg +0 -1
  196. package/icons/people-outline.svg +0 -1
  197. package/icons/percent-outline.svg +0 -1
  198. package/icons/person-add-outline.svg +0 -1
  199. package/icons/person-delete-outline.svg +0 -1
  200. package/icons/person-done-outline.svg +0 -1
  201. package/icons/person-outline.svg +0 -1
  202. package/icons/person-remove-outline.svg +0 -1
  203. package/icons/phone-call-outline.svg +0 -1
  204. package/icons/phone-missed-outline.svg +0 -1
  205. package/icons/phone-off-outline.svg +0 -1
  206. package/icons/phone-outline.svg +0 -1
  207. package/icons/pie-chart-outline.svg +0 -1
  208. package/icons/pin-outline.svg +0 -1
  209. package/icons/play-circle-outline.svg +0 -1
  210. package/icons/plus-circle-outline.svg +0 -1
  211. package/icons/plus-outline.svg +0 -1
  212. package/icons/plus-square-outline.svg +0 -1
  213. package/icons/power-outline.svg +0 -1
  214. package/icons/pricetags-outline.svg +0 -1
  215. package/icons/printer-outline.svg +0 -1
  216. package/icons/question-mark-circle-outline.svg +0 -1
  217. package/icons/question-mark-outline.svg +0 -1
  218. package/icons/radio-button-off-outline.svg +0 -1
  219. package/icons/radio-button-on-outline.svg +0 -1
  220. package/icons/radio-outline.svg +0 -1
  221. package/icons/recording-outline.svg +0 -1
  222. package/icons/refresh-outline.svg +0 -1
  223. package/icons/repeat-outline.svg +0 -1
  224. package/icons/rewind-left-outline.svg +0 -1
  225. package/icons/rewind-right-outline.svg +0 -1
  226. package/icons/save-outline.svg +0 -1
  227. package/icons/scissors-outline.svg +0 -1
  228. package/icons/search-outline.svg +0 -1
  229. package/icons/settings-2-outline.svg +0 -1
  230. package/icons/settings-outline.svg +0 -1
  231. package/icons/shake-outline.svg +0 -1
  232. package/icons/share-outline.svg +0 -1
  233. package/icons/shield-off-outline.svg +0 -1
  234. package/icons/shield-outline.svg +0 -1
  235. package/icons/shopping-bag-outline.svg +0 -1
  236. package/icons/shopping-cart-outline.svg +0 -1
  237. package/icons/shuffle-2-outline.svg +0 -1
  238. package/icons/shuffle-outline.svg +0 -1
  239. package/icons/skip-back-outline.svg +0 -1
  240. package/icons/skip-forward-outline.svg +0 -1
  241. package/icons/slash-outline.svg +0 -1
  242. package/icons/smartphone-outline.svg +0 -1
  243. package/icons/smiling-face-outline.svg +0 -1
  244. package/icons/speaker-outline.svg +0 -1
  245. package/icons/square-outline.svg +0 -1
  246. package/icons/star-outline.svg +0 -1
  247. package/icons/stop-circle-outline.svg +0 -1
  248. package/icons/sun-outline.svg +0 -1
  249. package/icons/swap-outline.svg +0 -1
  250. package/icons/sync-outline.svg +0 -1
  251. package/icons/text-outline.svg +0 -1
  252. package/icons/thermometer-minus-outline.svg +0 -1
  253. package/icons/thermometer-outline.svg +0 -1
  254. package/icons/thermometer-plus-outline.svg +0 -1
  255. package/icons/toggle-left-outline.svg +0 -1
  256. package/icons/toggle-right-outline.svg +0 -1
  257. package/icons/trash-2-outline.svg +0 -1
  258. package/icons/trash-outline.svg +0 -1
  259. package/icons/trending-down-outline.svg +0 -1
  260. package/icons/trending-up-outline.svg +0 -1
  261. package/icons/tv-outline.svg +0 -1
  262. package/icons/twitter-outline.svg +0 -1
  263. package/icons/umbrella-outline.svg +0 -1
  264. package/icons/undo-outline.svg +0 -1
  265. package/icons/unlock-outline.svg +0 -1
  266. package/icons/upload-outline.svg +0 -1
  267. package/icons/video-off-outline.svg +0 -1
  268. package/icons/video-outline.svg +0 -1
  269. package/icons/volume-down-outline.svg +0 -1
  270. package/icons/volume-mute-outline.svg +0 -1
  271. package/icons/volume-off-outline.svg +0 -1
  272. package/icons/volume-up-outline.svg +0 -1
  273. package/icons/wifi-off-outline.svg +0 -1
  274. package/icons/wifi-outline.svg +0 -1
@@ -0,0 +1,222 @@
1
+ interface BottomSheetOptions {
2
+ content: string;
3
+ header?: string;
4
+ footer?: string;
5
+ closeable?: boolean;
6
+ snapHeight?: 'auto' | 'half' | 'full';
7
+ onClose?: () => void;
8
+ }
9
+
10
+ class BottomSheet {
11
+ private readonly content: string;
12
+ private readonly header?: string;
13
+ private readonly footer?: string;
14
+ private readonly closeable: boolean;
15
+ private snapHeight: 'auto' | 'half' | 'full';
16
+ private readonly onClose?: () => void;
17
+
18
+ private wrapper: HTMLElement | null = null;
19
+ private sheet: HTMLElement | null = null;
20
+ private handle: HTMLElement | null = null;
21
+ private body: HTMLElement | null = null;
22
+
23
+ // Touch drag state
24
+ private dragStartY = 0;
25
+ private currentDragY = 0;
26
+ private isDragging = false;
27
+
28
+ constructor(options: BottomSheetOptions) {
29
+ this.content = options.content;
30
+ this.header = options.header;
31
+ this.footer = options.footer;
32
+ this.closeable = options.closeable ?? true;
33
+ this.snapHeight = options.snapHeight ?? 'auto';
34
+ this.onClose = options.onClose;
35
+
36
+ this.hide = this.hide.bind(this);
37
+ this.handleEscape = this.handleEscape.bind(this);
38
+ this.handleBackdropClick = this.handleBackdropClick.bind(this);
39
+ this.handleTouchStart = this.handleTouchStart.bind(this);
40
+ this.handleTouchMove = this.handleTouchMove.bind(this);
41
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
42
+ }
43
+
44
+ public show(): void {
45
+ this.hide();
46
+
47
+ const wrapper = document.createElement('div');
48
+ wrapper.className = 'bottom-sheet-wrapper';
49
+ wrapper.innerHTML = this.buildTemplate();
50
+ document.body.append(wrapper);
51
+
52
+ this.wrapper = wrapper;
53
+ this.sheet = wrapper.querySelector('.bottom-sheet');
54
+ this.handle = wrapper.querySelector('.bottom-sheet-handle');
55
+ this.body = wrapper.querySelector('.bottom-sheet-body');
56
+
57
+ if (this.closeable) {
58
+ const backdrop = wrapper.querySelector('.bottom-sheet-backdrop');
59
+ backdrop?.addEventListener('click', this.handleBackdropClick);
60
+ document.addEventListener('keydown', this.handleEscape);
61
+
62
+ const closeBtn = wrapper.querySelector('.close');
63
+ closeBtn?.addEventListener('click', this.hide);
64
+ }
65
+
66
+ if (this.handle) {
67
+ this.handle.addEventListener('touchstart', this.handleTouchStart, { passive: true });
68
+ this.handle.addEventListener('touchmove', this.handleTouchMove, { passive: false });
69
+ this.handle.addEventListener('touchend', this.handleTouchEnd);
70
+ }
71
+
72
+ if (this.body) {
73
+ this.updateScrollMask();
74
+ this.body.addEventListener('scroll', () => this.updateScrollMask());
75
+ }
76
+
77
+ document.body.style.overflow = 'hidden';
78
+
79
+ requestAnimationFrame(() => {
80
+ wrapper.classList.add('is-visible');
81
+ });
82
+ }
83
+
84
+ public hide(): void {
85
+ if (!this.wrapper) return;
86
+
87
+ const backdrop = this.wrapper.querySelector('.bottom-sheet-backdrop');
88
+ backdrop?.removeEventListener('click', this.handleBackdropClick);
89
+ document.removeEventListener('keydown', this.handleEscape);
90
+
91
+ if (this.handle) {
92
+ this.handle.removeEventListener('touchstart', this.handleTouchStart);
93
+ this.handle.removeEventListener('touchmove', this.handleTouchMove);
94
+ this.handle.removeEventListener('touchend', this.handleTouchEnd);
95
+ }
96
+
97
+ document.body.style.overflow = '';
98
+ this.wrapper.classList.remove('is-visible');
99
+
100
+ const wrapper = this.wrapper;
101
+ this.wrapper = null;
102
+ this.sheet = null;
103
+ this.handle = null;
104
+ this.body = null;
105
+
106
+ setTimeout(() => {
107
+ wrapper.remove();
108
+ this.onClose?.();
109
+ }, 420);
110
+ }
111
+
112
+ public snapTo(height: 'auto' | 'half' | 'full'): void {
113
+ if (!this.sheet) return;
114
+
115
+ this.sheet.classList.remove(`snap-${this.snapHeight}`);
116
+ this.snapHeight = height;
117
+
118
+ if (height !== 'auto') {
119
+ this.sheet.classList.add(`snap-${height}`);
120
+ }
121
+ }
122
+
123
+ private handleEscape(e: KeyboardEvent): void {
124
+ if (e.key === 'Escape') this.hide();
125
+ }
126
+
127
+ private handleBackdropClick(e: Event): void {
128
+ if ((e.target as HTMLElement)?.classList.contains('bottom-sheet-backdrop')) {
129
+ this.hide();
130
+ }
131
+ }
132
+
133
+ private handleTouchStart(e: TouchEvent): void {
134
+ this.dragStartY = e.touches[0].clientY;
135
+ this.currentDragY = 0;
136
+ this.isDragging = true;
137
+
138
+ if (this.sheet) {
139
+ this.sheet.style.transition = 'none';
140
+ }
141
+ }
142
+
143
+ private handleTouchMove(e: TouchEvent): void {
144
+ if (!this.isDragging || !this.sheet) return;
145
+
146
+ const deltaY = e.touches[0].clientY - this.dragStartY;
147
+
148
+ // Rubber-band resistance going upward
149
+ if (deltaY < 0) {
150
+ const resistance = Math.log(1 + Math.abs(deltaY)) * 4;
151
+ this.currentDragY = -resistance;
152
+ } else {
153
+ this.currentDragY = deltaY;
154
+ }
155
+
156
+ const isDesktop = window.innerWidth >= 768;
157
+ const translateX = isDesktop ? '-50%' : '0';
158
+ this.sheet.style.transform = `translateX(${translateX}) translateY(${this.currentDragY}px)`;
159
+ e.preventDefault();
160
+ }
161
+
162
+ private handleTouchEnd(): void {
163
+ if (!this.isDragging || !this.sheet) return;
164
+ this.isDragging = false;
165
+
166
+ const threshold = this.sheet.offsetHeight * 0.3;
167
+
168
+ if (this.currentDragY > threshold) {
169
+ this.hide();
170
+ } else {
171
+ // Spring back
172
+ this.sheet.style.transition = '';
173
+ this.sheet.style.transform = '';
174
+ }
175
+ }
176
+
177
+ private updateScrollMask(): void {
178
+ if (!this.body) return;
179
+ const canScroll = this.body.scrollHeight > this.body.clientHeight;
180
+ const atBottom = this.body.scrollTop + this.body.clientHeight >= this.body.scrollHeight - 4;
181
+ this.body.classList.toggle('is-scrollable', canScroll && !atBottom);
182
+ }
183
+
184
+ private buildTemplate(): string {
185
+ const snapClass = this.snapHeight !== 'auto' ? ` snap-${this.snapHeight}` : '';
186
+
187
+ const closeButton = this.closeable
188
+ ? `<div class="icon icon-close close"></div>`
189
+ : '';
190
+
191
+ const headerHtml = this.header !== undefined
192
+ ? `<div class="bottom-sheet-header has-divider">
193
+ <span class="title">${this.header}</span>
194
+ ${closeButton}
195
+ </div>`
196
+ : '';
197
+
198
+ const footerHtml = this.footer !== undefined
199
+ ? `<div class="bottom-sheet-footer">${this.footer}</div>`
200
+ : '';
201
+
202
+ return `
203
+ <div class="bottom-sheet${snapClass}">
204
+ <div class="bottom-sheet-handle" role="button" aria-label="Drag to dismiss"></div>
205
+ ${headerHtml}
206
+ <div class="bottom-sheet-body">${this.content}</div>
207
+ ${footerHtml}
208
+ </div>
209
+ <div class="bottom-sheet-backdrop"></div>
210
+ `;
211
+ }
212
+
213
+ public isVisible(): boolean {
214
+ return this.wrapper !== null && document.body.contains(this.wrapper);
215
+ }
216
+
217
+ public destroy(): void {
218
+ this.hide();
219
+ }
220
+ }
221
+
222
+ export { BottomSheet, type BottomSheetOptions };
package/js/carousel.js CHANGED
@@ -59,6 +59,7 @@ class Carousel {
59
59
  this.dots.push(dot);
60
60
  });
61
61
  this.root.appendChild(this.dotsNav);
62
+ // Make focusable for keyboard nav
62
63
  this.root.setAttribute('tabindex', '0');
63
64
  }
64
65
  bindEvents() {
@@ -66,7 +67,8 @@ class Carousel {
66
67
  this.prevButton.addEventListener('click', () => this.moveToPrevSlide());
67
68
  this.dotsNav.addEventListener('click', (e) => {
68
69
  const targetDot = e.target.closest('button');
69
- if (!targetDot) return;
70
+ if (!targetDot)
71
+ return;
70
72
  const targetIndex = this.dots.findIndex(dot => dot === targetDot);
71
73
  this.moveToSlide(targetIndex);
72
74
  });
@@ -76,25 +78,32 @@ class Carousel {
76
78
  });
77
79
  // Keyboard navigation
78
80
  this.root.addEventListener('keydown', (e) => {
79
- if (e.key === 'ArrowLeft') this.moveToPrevSlide();
80
- if (e.key === 'ArrowRight') this.moveToNextSlide();
81
+ if (e.key === 'ArrowLeft')
82
+ this.moveToPrevSlide();
83
+ if (e.key === 'ArrowRight')
84
+ this.moveToNextSlide();
81
85
  });
82
86
  // Pause autoplay on hover / focus
83
87
  if (this.options.autoPlay) {
84
88
  this.root.addEventListener('mouseenter', () => this.pauseAutoPlay());
85
89
  this.root.addEventListener('mouseleave', () => this.resumeAutoPlay());
86
- this.root.addEventListener('focusin', () => this.pauseAutoPlay());
87
- this.root.addEventListener('focusout', () => this.resumeAutoPlay());
90
+ this.root.addEventListener('focusin', () => this.pauseAutoPlay());
91
+ this.root.addEventListener('focusout', () => this.resumeAutoPlay());
88
92
  }
89
93
  this.addTouchSupport();
90
94
  }
91
95
  moveToSlide(targetIndex, animate = true) {
92
96
  if (targetIndex < 0) {
93
- if (this.options.loop) targetIndex = this.slides.length - 1;
94
- else targetIndex = 0;
95
- } else if (targetIndex >= this.slides.length) {
96
- if (this.options.loop) targetIndex = 0;
97
- else targetIndex = this.slides.length - 1;
97
+ if (this.options.loop)
98
+ targetIndex = this.slides.length - 1;
99
+ else
100
+ targetIndex = 0;
101
+ }
102
+ else if (targetIndex >= this.slides.length) {
103
+ if (this.options.loop)
104
+ targetIndex = 0;
105
+ else
106
+ targetIndex = this.slides.length - 1;
98
107
  }
99
108
  if (!animate) {
100
109
  this.track.style.transitionDuration = '0ms';
@@ -102,6 +111,7 @@ class Carousel {
102
111
  const amountToMove = -1 * (this.slideWidth * targetIndex);
103
112
  this.track.style.transform = `translateX(${amountToMove}px)`;
104
113
  if (!animate) {
114
+ // Restore CSS transition after the paint to avoid a flash
105
115
  requestAnimationFrame(() => {
106
116
  this.track.style.transitionDuration = '';
107
117
  });
@@ -127,12 +137,15 @@ class Carousel {
127
137
  isDragging = true;
128
138
  }, { passive: true });
129
139
  this.track.addEventListener('touchend', (e) => {
130
- if (!isDragging) return;
140
+ if (!isDragging)
141
+ return;
131
142
  const endX = e.changedTouches[0].clientX;
132
143
  const diffX = startX - endX;
133
144
  if (Math.abs(diffX) > 50) {
134
- if (diffX > 0) this.moveToNextSlide();
135
- else this.moveToPrevSlide();
145
+ if (diffX > 0)
146
+ this.moveToNextSlide();
147
+ else
148
+ this.moveToPrevSlide();
136
149
  }
137
150
  isDragging = false;
138
151
  });
@@ -0,0 +1,212 @@
1
+ class ContextMenu {
2
+ constructor(selectorOrElement, items) {
3
+ this.menuEl = null;
4
+ this.currentTarget = null;
5
+ this.abortController = new AbortController();
6
+ this.items = items;
7
+ if (typeof selectorOrElement === 'string') {
8
+ this.targets = Array.from(document.querySelectorAll(selectorOrElement));
9
+ }
10
+ else if (Array.isArray(selectorOrElement)) {
11
+ this.targets = selectorOrElement;
12
+ }
13
+ else {
14
+ this.targets = [selectorOrElement];
15
+ }
16
+ this.init();
17
+ }
18
+ init() {
19
+ const { signal } = this.abortController;
20
+ this.targets.forEach((target) => {
21
+ target.addEventListener('contextmenu', (e) => {
22
+ e.preventDefault();
23
+ this.currentTarget = target;
24
+ this.open(e.clientX, e.clientY);
25
+ }, { signal });
26
+ });
27
+ document.addEventListener('click', () => this.close(), { signal });
28
+ // Close on right-click outside the menu
29
+ document.addEventListener('contextmenu', (e) => {
30
+ if (this.menuEl && !this.menuEl.contains(e.target)) {
31
+ this.close();
32
+ }
33
+ }, { signal, capture: true });
34
+ document.addEventListener('keydown', (e) => {
35
+ if (!this.menuEl)
36
+ return;
37
+ if (e.key === 'Escape') {
38
+ this.close();
39
+ }
40
+ if (e.key === 'ArrowDown') {
41
+ e.preventDefault();
42
+ this.moveFocus(1);
43
+ }
44
+ if (e.key === 'ArrowUp') {
45
+ e.preventDefault();
46
+ this.moveFocus(-1);
47
+ }
48
+ if (e.key === 'Enter') {
49
+ e.preventDefault();
50
+ this.activateFocused();
51
+ }
52
+ }, { signal });
53
+ // Close on scroll outside the menu
54
+ window.addEventListener('scroll', (e) => {
55
+ if (!this.menuEl?.contains(e.target))
56
+ this.close();
57
+ }, { signal, capture: true });
58
+ window.addEventListener('resize', () => this.close(), { signal });
59
+ }
60
+ open(x, y) {
61
+ this.close();
62
+ this.menuEl = this.buildMenu(this.items);
63
+ document.body.appendChild(this.menuEl);
64
+ // Use offsetWidth/offsetHeight — unaffected by CSS transform
65
+ const w = this.menuEl.offsetWidth;
66
+ const h = this.menuEl.offsetHeight;
67
+ const vw = window.innerWidth;
68
+ const vh = window.innerHeight;
69
+ const left = x + w > vw ? vw - w - 8 : x;
70
+ const top = y + h > vh ? vh - h - 8 : y;
71
+ // Set transform-origin to match the corner the menu opens from
72
+ const originX = x + w > vw ? 'right' : 'left';
73
+ const originY = y + h > vh ? 'bottom' : 'top';
74
+ this.menuEl.style.left = `${left}px`;
75
+ this.menuEl.style.top = `${top}px`;
76
+ this.menuEl.style.transformOrigin = `${originY} ${originX}`;
77
+ requestAnimationFrame(() => this.menuEl?.classList.add('is-visible'));
78
+ }
79
+ close() {
80
+ if (!this.menuEl)
81
+ return;
82
+ const el = this.menuEl;
83
+ this.menuEl = null;
84
+ el.classList.remove('is-visible');
85
+ // Wait for exit transition then remove from DOM
86
+ el.addEventListener('transitionend', () => el.remove(), { once: true });
87
+ setTimeout(() => el.isConnected && el.remove(), 200);
88
+ }
89
+ buildMenu(items) {
90
+ const ul = document.createElement('ul');
91
+ ul.className = 'context-menu';
92
+ for (const item of items) {
93
+ if (item === 'separator') {
94
+ const li = document.createElement('li');
95
+ li.className = 'context-menu-separator';
96
+ ul.appendChild(li);
97
+ continue;
98
+ }
99
+ if ('group' in item) {
100
+ const li = document.createElement('li');
101
+ li.className = 'context-menu-group-label';
102
+ li.textContent = item.group;
103
+ ul.appendChild(li);
104
+ continue;
105
+ }
106
+ ul.appendChild(this.buildItem(item));
107
+ }
108
+ return ul;
109
+ }
110
+ buildItem(def) {
111
+ const li = document.createElement('li');
112
+ li.className = 'context-menu-item';
113
+ if (def.disabled)
114
+ li.classList.add('is-disabled');
115
+ if (def.destructive)
116
+ li.classList.add('is-destructive');
117
+ if (def.submenu)
118
+ li.classList.add('has-submenu');
119
+ // Always render icon slot — keeps label column aligned across all items
120
+ const iconWrap = document.createElement('span');
121
+ iconWrap.className = 'context-menu-icon';
122
+ if (def.icon) {
123
+ iconWrap.innerHTML = `<svg class="icon-svg"><use href="svg-icons/icons.svg#${def.icon}"/></svg>`;
124
+ }
125
+ li.appendChild(iconWrap);
126
+ const label = document.createElement('span');
127
+ label.className = 'context-menu-label';
128
+ label.textContent = def.label;
129
+ li.appendChild(label);
130
+ if (def.shortcut) {
131
+ const sc = document.createElement('span');
132
+ sc.className = 'context-menu-shortcut';
133
+ sc.textContent = def.shortcut;
134
+ li.appendChild(sc);
135
+ }
136
+ if (def.submenu) {
137
+ const chevron = document.createElement('span');
138
+ chevron.className = 'context-menu-chevron';
139
+ li.appendChild(chevron);
140
+ const submenuEl = this.buildMenu(def.submenu);
141
+ li.appendChild(submenuEl);
142
+ // Determine flip synchronously from parent position — no rAF flash
143
+ const shouldFlip = () => {
144
+ const rect = li.getBoundingClientRect();
145
+ return rect.right + submenuEl.offsetWidth > window.innerWidth;
146
+ };
147
+ // Delay timer prevents the submenu closing when mouse travels from
148
+ // item → submenu (mouseleave fires before mouseenter on the submenu)
149
+ let closeTimer = null;
150
+ const openSub = () => {
151
+ if (closeTimer) {
152
+ clearTimeout(closeTimer);
153
+ closeTimer = null;
154
+ }
155
+ this.closeAllSubmenus(li.closest('.context-menu'));
156
+ li.classList.toggle('submenu-flip', shouldFlip());
157
+ li.classList.add('is-active');
158
+ };
159
+ const closeSub = () => {
160
+ closeTimer = setTimeout(() => li.classList.remove('is-active'), 120);
161
+ };
162
+ li.addEventListener('mouseenter', openSub);
163
+ li.addEventListener('mouseleave', closeSub);
164
+ submenuEl.addEventListener('mouseenter', () => {
165
+ if (closeTimer) {
166
+ clearTimeout(closeTimer);
167
+ closeTimer = null;
168
+ }
169
+ });
170
+ submenuEl.addEventListener('mouseleave', closeSub);
171
+ }
172
+ else if (!def.disabled) {
173
+ li.addEventListener('click', (e) => {
174
+ e.stopPropagation();
175
+ def.action?.(this.currentTarget);
176
+ this.close();
177
+ });
178
+ }
179
+ return li;
180
+ }
181
+ closeAllSubmenus(menu) {
182
+ // Only close direct-child submenus of this menu level
183
+ Array.from(menu.children).forEach((child) => {
184
+ child.classList.remove('is-active');
185
+ });
186
+ }
187
+ getFocusableItems() {
188
+ if (!this.menuEl)
189
+ return [];
190
+ return Array.from(this.menuEl.children).filter((el) => el.classList.contains('context-menu-item') &&
191
+ !el.classList.contains('is-disabled'));
192
+ }
193
+ moveFocus(direction) {
194
+ const items = this.getFocusableItems();
195
+ if (!items.length)
196
+ return;
197
+ const currentIndex = items.findIndex((el) => el.classList.contains('is-focused'));
198
+ const nextIndex = (currentIndex + direction + items.length) % items.length;
199
+ items[currentIndex]?.classList.remove('is-focused');
200
+ items[nextIndex].classList.add('is-focused');
201
+ }
202
+ activateFocused() {
203
+ this.menuEl
204
+ ?.querySelector('.context-menu-item.is-focused')
205
+ ?.click();
206
+ }
207
+ destroy() {
208
+ this.close();
209
+ this.abortController.abort();
210
+ }
211
+ }
212
+ export { ContextMenu };