@dodlhuat/basix 1.1.1 → 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 +1 -1
  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,252 @@
1
+ interface ContextMenuItemDef {
2
+ label: string;
3
+ icon?: string;
4
+ shortcut?: string;
5
+ disabled?: boolean;
6
+ destructive?: boolean;
7
+ action?: (target: HTMLElement) => void;
8
+ submenu?: ContextMenuInput[];
9
+ }
10
+
11
+ type ContextMenuInput = ContextMenuItemDef | 'separator' | { group: string };
12
+
13
+ class ContextMenu {
14
+ private items: ContextMenuInput[];
15
+ private targets: HTMLElement[];
16
+ private menuEl: HTMLElement | null = null;
17
+ private currentTarget: HTMLElement | null = null;
18
+ private abortController = new AbortController();
19
+
20
+ constructor(
21
+ selectorOrElement: string | HTMLElement | HTMLElement[],
22
+ items: ContextMenuInput[]
23
+ ) {
24
+ this.items = items;
25
+
26
+ if (typeof selectorOrElement === 'string') {
27
+ this.targets = Array.from(document.querySelectorAll<HTMLElement>(selectorOrElement));
28
+ } else if (Array.isArray(selectorOrElement)) {
29
+ this.targets = selectorOrElement;
30
+ } else {
31
+ this.targets = [selectorOrElement];
32
+ }
33
+
34
+ this.init();
35
+ }
36
+
37
+ private init(): void {
38
+ const { signal } = this.abortController;
39
+
40
+ this.targets.forEach((target) => {
41
+ target.addEventListener('contextmenu', (e: MouseEvent) => {
42
+ e.preventDefault();
43
+ this.currentTarget = target;
44
+ this.open(e.clientX, e.clientY);
45
+ }, { signal });
46
+ });
47
+
48
+ document.addEventListener('click', () => this.close(), { signal });
49
+
50
+ // Close on right-click outside the menu
51
+ document.addEventListener('contextmenu', (e: MouseEvent) => {
52
+ if (this.menuEl && !this.menuEl.contains(e.target as Node)) {
53
+ this.close();
54
+ }
55
+ }, { signal, capture: true });
56
+
57
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
58
+ if (!this.menuEl) return;
59
+ if (e.key === 'Escape') { this.close(); }
60
+ if (e.key === 'ArrowDown') { e.preventDefault(); this.moveFocus(1); }
61
+ if (e.key === 'ArrowUp') { e.preventDefault(); this.moveFocus(-1); }
62
+ if (e.key === 'Enter') { e.preventDefault(); this.activateFocused(); }
63
+ }, { signal });
64
+
65
+ // Close on scroll outside the menu
66
+ window.addEventListener('scroll', (e: Event) => {
67
+ if (!this.menuEl?.contains(e.target as Node)) this.close();
68
+ }, { signal, capture: true });
69
+
70
+ window.addEventListener('resize', () => this.close(), { signal });
71
+ }
72
+
73
+ private open(x: number, y: number): void {
74
+ this.close();
75
+
76
+ this.menuEl = this.buildMenu(this.items);
77
+ document.body.appendChild(this.menuEl);
78
+
79
+ // Use offsetWidth/offsetHeight — unaffected by CSS transform
80
+ const w = this.menuEl.offsetWidth;
81
+ const h = this.menuEl.offsetHeight;
82
+ const vw = window.innerWidth;
83
+ const vh = window.innerHeight;
84
+
85
+ const left = x + w > vw ? vw - w - 8 : x;
86
+ const top = y + h > vh ? vh - h - 8 : y;
87
+
88
+ // Set transform-origin to match the corner the menu opens from
89
+ const originX = x + w > vw ? 'right' : 'left';
90
+ const originY = y + h > vh ? 'bottom' : 'top';
91
+
92
+ this.menuEl.style.left = `${left}px`;
93
+ this.menuEl.style.top = `${top}px`;
94
+ this.menuEl.style.transformOrigin = `${originY} ${originX}`;
95
+
96
+ requestAnimationFrame(() => this.menuEl?.classList.add('is-visible'));
97
+ }
98
+
99
+ private close(): void {
100
+ if (!this.menuEl) return;
101
+ const el = this.menuEl;
102
+ this.menuEl = null;
103
+
104
+ el.classList.remove('is-visible');
105
+
106
+ // Wait for exit transition then remove from DOM
107
+ el.addEventListener('transitionend', () => el.remove(), { once: true });
108
+ setTimeout(() => el.isConnected && el.remove(), 200);
109
+ }
110
+
111
+ private buildMenu(items: ContextMenuInput[]): HTMLElement {
112
+ const ul = document.createElement('ul');
113
+ ul.className = 'context-menu';
114
+
115
+ for (const item of items) {
116
+ if (item === 'separator') {
117
+ const li = document.createElement('li');
118
+ li.className = 'context-menu-separator';
119
+ ul.appendChild(li);
120
+ continue;
121
+ }
122
+
123
+ if ('group' in item) {
124
+ const li = document.createElement('li');
125
+ li.className = 'context-menu-group-label';
126
+ li.textContent = item.group;
127
+ ul.appendChild(li);
128
+ continue;
129
+ }
130
+
131
+ ul.appendChild(this.buildItem(item));
132
+ }
133
+
134
+ return ul;
135
+ }
136
+
137
+ private buildItem(def: ContextMenuItemDef): HTMLElement {
138
+ const li = document.createElement('li');
139
+ li.className = 'context-menu-item';
140
+
141
+ if (def.disabled) li.classList.add('is-disabled');
142
+ if (def.destructive) li.classList.add('is-destructive');
143
+ if (def.submenu) li.classList.add('has-submenu');
144
+
145
+ // Always render icon slot — keeps label column aligned across all items
146
+ const iconWrap = document.createElement('span');
147
+ iconWrap.className = 'context-menu-icon';
148
+ if (def.icon) {
149
+ iconWrap.innerHTML = `<svg class="icon-svg"><use href="svg-icons/icons.svg#${def.icon}"/></svg>`;
150
+ }
151
+ li.appendChild(iconWrap);
152
+
153
+ const label = document.createElement('span');
154
+ label.className = 'context-menu-label';
155
+ label.textContent = def.label;
156
+ li.appendChild(label);
157
+
158
+ if (def.shortcut) {
159
+ const sc = document.createElement('span');
160
+ sc.className = 'context-menu-shortcut';
161
+ sc.textContent = def.shortcut;
162
+ li.appendChild(sc);
163
+ }
164
+
165
+ if (def.submenu) {
166
+ const chevron = document.createElement('span');
167
+ chevron.className = 'context-menu-chevron';
168
+ li.appendChild(chevron);
169
+
170
+ const submenuEl = this.buildMenu(def.submenu);
171
+ li.appendChild(submenuEl);
172
+
173
+ // Determine flip synchronously from parent position — no rAF flash
174
+ const shouldFlip = (): boolean => {
175
+ const rect = li.getBoundingClientRect();
176
+ return rect.right + submenuEl.offsetWidth > window.innerWidth;
177
+ };
178
+
179
+ // Delay timer prevents the submenu closing when mouse travels from
180
+ // item → submenu (mouseleave fires before mouseenter on the submenu)
181
+ let closeTimer: ReturnType<typeof setTimeout> | null = null;
182
+
183
+ const openSub = () => {
184
+ if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
185
+ this.closeAllSubmenus(li.closest<HTMLElement>('.context-menu')!);
186
+ li.classList.toggle('submenu-flip', shouldFlip());
187
+ li.classList.add('is-active');
188
+ };
189
+
190
+ const closeSub = () => {
191
+ closeTimer = setTimeout(() => li.classList.remove('is-active'), 120);
192
+ };
193
+
194
+ li.addEventListener('mouseenter', openSub);
195
+ li.addEventListener('mouseleave', closeSub);
196
+ submenuEl.addEventListener('mouseenter', () => {
197
+ if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
198
+ });
199
+ submenuEl.addEventListener('mouseleave', closeSub);
200
+ } else if (!def.disabled) {
201
+ li.addEventListener('click', (e: MouseEvent) => {
202
+ e.stopPropagation();
203
+ def.action?.(this.currentTarget!);
204
+ this.close();
205
+ });
206
+ }
207
+
208
+ return li;
209
+ }
210
+
211
+ private closeAllSubmenus(menu: HTMLElement): void {
212
+ // Only close direct-child submenus of this menu level
213
+ Array.from(menu.children).forEach((child) => {
214
+ child.classList.remove('is-active');
215
+ });
216
+ }
217
+
218
+ private getFocusableItems(): HTMLElement[] {
219
+ if (!this.menuEl) return [];
220
+ return Array.from(
221
+ this.menuEl.children
222
+ ).filter(
223
+ (el): el is HTMLElement =>
224
+ el.classList.contains('context-menu-item') &&
225
+ !el.classList.contains('is-disabled')
226
+ ) as HTMLElement[];
227
+ }
228
+
229
+ private moveFocus(direction: 1 | -1): void {
230
+ const items = this.getFocusableItems();
231
+ if (!items.length) return;
232
+
233
+ const currentIndex = items.findIndex((el) => el.classList.contains('is-focused'));
234
+ const nextIndex = (currentIndex + direction + items.length) % items.length;
235
+
236
+ items[currentIndex]?.classList.remove('is-focused');
237
+ items[nextIndex].classList.add('is-focused');
238
+ }
239
+
240
+ private activateFocused(): void {
241
+ this.menuEl
242
+ ?.querySelector<HTMLElement>('.context-menu-item.is-focused')
243
+ ?.click();
244
+ }
245
+
246
+ public destroy(): void {
247
+ this.close();
248
+ this.abortController.abort();
249
+ }
250
+ }
251
+
252
+ export { ContextMenu, type ContextMenuInput, type ContextMenuItemDef };
package/js/editor.js CHANGED
@@ -1,20 +1,31 @@
1
1
  class Editor {
2
- constructor() {
2
+ constructor(options = {}) {
3
3
  this.undoStack = [];
4
4
  this.redoStack = [];
5
5
  const editable = document.getElementById('editable');
6
- const code = document.getElementById('code');
7
- const preview = document.getElementById('preview');
8
- const sidePanel = document.getElementById('sidePanel');
9
- const wordCount = document.getElementById('wordCount');
10
- if (!editable || !code || !preview || !sidePanel) {
11
- throw new Error('Editor: Required elements not found');
6
+ if (!editable) {
7
+ throw new Error('Editor: #editable element not found');
12
8
  }
13
9
  this.editable = editable;
14
- this.code = code;
15
- this.preview = preview;
16
- this.sidePanel = sidePanel;
17
- this.wordCount = wordCount;
10
+ this.wordCount = document.getElementById('wordCount');
11
+ if (options.simple) {
12
+ this.code = null;
13
+ this.preview = null;
14
+ this.sidePanel = document.getElementById('sidePanel');
15
+ this.sidePanel?.classList.add('hidden');
16
+ }
17
+ else {
18
+ const code = document.getElementById('code');
19
+ const preview = document.getElementById('preview');
20
+ const sidePanel = document.getElementById('sidePanel');
21
+ if (!code || !preview || !sidePanel) {
22
+ throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
23
+ }
24
+ this.code = code;
25
+ this.preview = preview;
26
+ this.sidePanel = sidePanel;
27
+ this.sidePanel.classList.add('hidden');
28
+ }
18
29
  this.bindToolbar();
19
30
  this.bindActions();
20
31
  this.bindKeyboard();
@@ -22,8 +33,6 @@ class Editor {
22
33
  this.bindTabs();
23
34
  this.syncViews();
24
35
  this.saveState();
25
- // Start with side panel hidden
26
- this.sidePanel.classList.add('hidden');
27
36
  }
28
37
  bindToolbar() {
29
38
  document.querySelectorAll('[data-cmd]').forEach(btn => {
@@ -69,26 +78,29 @@ class Editor {
69
78
  document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
70
79
  document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
71
80
  document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
72
- this.sidePanel.classList.toggle('hidden');
81
+ this.sidePanel?.classList.toggle('hidden');
73
82
  this.syncViews();
74
83
  });
75
84
  // Code action buttons — matched by position within .code-actions
76
- const codeActions = document.querySelectorAll('.code-actions button');
77
- codeActions[0]?.addEventListener('click', () => {
78
- this.editable.innerHTML = this.sanitizeHTML(this.code.value);
79
- this.onContentChange();
80
- });
81
- codeActions[1]?.addEventListener('click', () => {
82
- this.code.value = this.sanitizeHTML(this.code.value);
83
- this.editable.innerHTML = this.code.value;
84
- this.onContentChange();
85
- });
86
- codeActions[2]?.addEventListener('click', () => {
87
- this.code.value = this.code.value
88
- .replace(/\n/g, '')
89
- .replace(/>\s+</g, '><')
90
- .trim();
91
- });
85
+ if (this.code) {
86
+ const code = this.code;
87
+ const codeActions = document.querySelectorAll('.code-actions button');
88
+ codeActions[0]?.addEventListener('click', () => {
89
+ this.editable.innerHTML = this.sanitizeHTML(code.value);
90
+ this.onContentChange();
91
+ });
92
+ codeActions[1]?.addEventListener('click', () => {
93
+ code.value = this.sanitizeHTML(code.value);
94
+ this.editable.innerHTML = code.value;
95
+ this.onContentChange();
96
+ });
97
+ codeActions[2]?.addEventListener('click', () => {
98
+ code.value = code.value
99
+ .replace(/\n/g, '')
100
+ .replace(/>\s+</g, '><')
101
+ .trim();
102
+ });
103
+ }
92
104
  const saveBtn = document.getElementById('saveBtn');
93
105
  saveBtn?.addEventListener('click', () => this.downloadHTML());
94
106
  document.getElementById('clearBtn')?.addEventListener('click', () => {
@@ -163,8 +175,10 @@ class Editor {
163
175
  this.syncViews();
164
176
  }
165
177
  syncViews() {
166
- this.code.value = this.editable.innerHTML.trim();
167
- this.preview.innerHTML = this.editable.innerHTML;
178
+ if (this.code)
179
+ this.code.value = this.editable.innerHTML.trim();
180
+ if (this.preview)
181
+ this.preview.innerHTML = this.editable.innerHTML;
168
182
  this.updateWordCount();
169
183
  }
170
184
  updateWordCount() {
package/js/editor.ts CHANGED
@@ -1,28 +1,47 @@
1
+ interface EditorOptions {
2
+ /** Hides the entire side panel (code/preview) permanently. Safe to use
3
+ * without #code, #preview, or #sidePanel in the DOM. */
4
+ simple?: boolean;
5
+ }
6
+
1
7
  class Editor {
2
8
  private readonly editable: HTMLElement;
3
- private readonly code: HTMLTextAreaElement;
4
- private readonly preview: HTMLElement;
5
- private readonly sidePanel: HTMLElement;
9
+ private readonly code: HTMLTextAreaElement | null;
10
+ private readonly preview: HTMLElement | null;
11
+ private readonly sidePanel: HTMLElement | null;
6
12
  private readonly wordCount: HTMLElement | null;
7
13
  private undoStack: string[] = [];
8
14
  private redoStack: string[] = [];
9
15
 
10
- constructor() {
16
+ constructor(options: EditorOptions = {}) {
11
17
  const editable = document.getElementById('editable');
12
- const code = document.getElementById('code') as HTMLTextAreaElement;
13
- const preview = document.getElementById('preview');
14
- const sidePanel = document.getElementById('sidePanel');
15
- const wordCount = document.getElementById('wordCount');
16
18
 
17
- if (!editable || !code || !preview || !sidePanel) {
18
- throw new Error('Editor: Required elements not found');
19
+ if (!editable) {
20
+ throw new Error('Editor: #editable element not found');
19
21
  }
20
22
 
21
- this.editable = editable;
22
- this.code = code;
23
- this.preview = preview;
24
- this.sidePanel = sidePanel;
25
- this.wordCount = wordCount;
23
+ this.editable = editable;
24
+ this.wordCount = document.getElementById('wordCount');
25
+
26
+ if (options.simple) {
27
+ this.code = null;
28
+ this.preview = null;
29
+ this.sidePanel = document.getElementById('sidePanel');
30
+ this.sidePanel?.classList.add('hidden');
31
+ } else {
32
+ const code = document.getElementById('code') as HTMLTextAreaElement;
33
+ const preview = document.getElementById('preview');
34
+ const sidePanel = document.getElementById('sidePanel');
35
+
36
+ if (!code || !preview || !sidePanel) {
37
+ throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
38
+ }
39
+
40
+ this.code = code;
41
+ this.preview = preview;
42
+ this.sidePanel = sidePanel;
43
+ this.sidePanel.classList.add('hidden');
44
+ }
26
45
 
27
46
  this.bindToolbar();
28
47
  this.bindActions();
@@ -31,9 +50,6 @@ class Editor {
31
50
  this.bindTabs();
32
51
  this.syncViews();
33
52
  this.saveState();
34
-
35
- // Start with side panel hidden
36
- this.sidePanel.classList.add('hidden');
37
53
  }
38
54
 
39
55
  private bindToolbar(): void {
@@ -82,27 +98,30 @@ class Editor {
82
98
  document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
83
99
 
84
100
  document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
85
- this.sidePanel.classList.toggle('hidden');
101
+ this.sidePanel?.classList.toggle('hidden');
86
102
  this.syncViews();
87
103
  });
88
104
 
89
105
  // Code action buttons — matched by position within .code-actions
90
- const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
91
- codeActions[0]?.addEventListener('click', () => {
92
- this.editable.innerHTML = this.sanitizeHTML(this.code.value);
93
- this.onContentChange();
94
- });
95
- codeActions[1]?.addEventListener('click', () => {
96
- this.code.value = this.sanitizeHTML(this.code.value);
97
- this.editable.innerHTML = this.code.value;
98
- this.onContentChange();
99
- });
100
- codeActions[2]?.addEventListener('click', () => {
101
- this.code.value = this.code.value
102
- .replace(/\n/g, '')
103
- .replace(/>\s+</g, '><')
104
- .trim();
105
- });
106
+ if (this.code) {
107
+ const code = this.code;
108
+ const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
109
+ codeActions[0]?.addEventListener('click', () => {
110
+ this.editable.innerHTML = this.sanitizeHTML(code.value);
111
+ this.onContentChange();
112
+ });
113
+ codeActions[1]?.addEventListener('click', () => {
114
+ code.value = this.sanitizeHTML(code.value);
115
+ this.editable.innerHTML = code.value;
116
+ this.onContentChange();
117
+ });
118
+ codeActions[2]?.addEventListener('click', () => {
119
+ code.value = code.value
120
+ .replace(/\n/g, '')
121
+ .replace(/>\s+</g, '><')
122
+ .trim();
123
+ });
124
+ }
106
125
 
107
126
  const saveBtn = document.getElementById('saveBtn');
108
127
  saveBtn?.addEventListener('click', () => this.downloadHTML());
@@ -171,8 +190,8 @@ class Editor {
171
190
  }
172
191
 
173
192
  private syncViews(): void {
174
- this.code.value = this.editable.innerHTML.trim();
175
- this.preview.innerHTML = this.editable.innerHTML;
193
+ if (this.code) this.code.value = this.editable.innerHTML.trim();
194
+ if (this.preview) this.preview.innerHTML = this.editable.innerHTML;
176
195
  this.updateWordCount();
177
196
  }
178
197
 
package/js/gallery.js CHANGED
@@ -13,6 +13,9 @@ class MasonryGallery {
13
13
  this.loadMoreImages();
14
14
  }
15
15
  };
16
+ this.fetchMockImages = () => {
17
+ throw Error("Method not implemented.");
18
+ };
16
19
  const container = document.getElementById(containerId);
17
20
  if (!container) {
18
21
  throw new Error(`Container with id "${containerId}" not found`);
@@ -85,7 +88,8 @@ class MasonryGallery {
85
88
  });
86
89
  }
87
90
  async loadMoreImages(isAutoFill = false) {
88
- if (!isAutoFill) this.reloaded++;
91
+ if (!isAutoFill)
92
+ this.reloaded++;
89
93
  if (this.options.reload > 0 && this.reloaded > this.options.reload) {
90
94
  console.warn("Maximum reload limit reached.");
91
95
  return;
@@ -119,9 +123,6 @@ class MasonryGallery {
119
123
  this.loader.classList.toggle("hidden", !show);
120
124
  }
121
125
  }
122
- fetchMockImages = () => {
123
- throw Error("Method not implemented.");
124
- };
125
126
  renderImages(imageDataList) {
126
127
  imageDataList.forEach((data) => {
127
128
  const item = this.createCard(data);
@@ -142,12 +143,12 @@ class MasonryGallery {
142
143
  createCard(data) {
143
144
  const div = document.createElement("div");
144
145
  div.className = "masonry-item";
145
- div.innerHTML = `
146
- <img src="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
147
- <div class="masonry-item-info">
148
- <h3 class="masonry-item-title">${this.escapeHtml(data.title)}</h3>
149
- <p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
150
- </div>
146
+ div.innerHTML = `
147
+ <img src="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
148
+ <div class="masonry-item-info">
149
+ <h3 class="masonry-item-title">${this.escapeHtml(data.title)}</h3>
150
+ <p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
151
+ </div>
151
152
  `;
152
153
  return div;
153
154
  }