@dodlhuat/basix 1.3.4 → 1.4.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 (82) hide show
  1. package/README.md +160 -18
  2. package/css/checkbox.scss +15 -2
  3. package/css/color-picker.scss +278 -0
  4. package/css/datepicker.scss +5 -3
  5. package/css/push-menu.scss +7 -0
  6. package/css/stepper.scss +1 -1
  7. package/css/style.css +291 -6
  8. package/css/style.css.map +1 -1
  9. package/css/style.min.css +1 -1
  10. package/css/style.min.css.map +1 -1
  11. package/css/style.scss +1 -0
  12. package/css/table.scss +33 -0
  13. package/js/bottom-sheet.d.ts +0 -2
  14. package/js/bottom-sheet.js +0 -2
  15. package/js/calendar.d.ts +9 -17
  16. package/js/calendar.js +4 -7
  17. package/js/carousel.d.ts +0 -2
  18. package/js/carousel.js +1 -3
  19. package/js/chart.d.ts +6 -14
  20. package/js/chart.js +3 -5
  21. package/js/code-viewer.d.ts +2 -1
  22. package/js/code-viewer.js +8 -2
  23. package/js/color-picker.d.ts +50 -0
  24. package/js/color-picker.js +291 -0
  25. package/js/context-menu.d.ts +0 -3
  26. package/js/context-menu.js +0 -3
  27. package/js/datepicker.d.ts +0 -4
  28. package/js/datepicker.js +0 -1
  29. package/js/docs-nav.js +1 -0
  30. package/js/dropdown.d.ts +1 -4
  31. package/js/dropdown.js +0 -1
  32. package/js/editor.d.ts +4 -3
  33. package/js/editor.js +50 -35
  34. package/js/file-uploader.d.ts +0 -4
  35. package/js/file-uploader.js +0 -1
  36. package/js/flyout-menu.d.ts +0 -2
  37. package/js/flyout-menu.js +0 -1
  38. package/js/gallery.d.ts +1 -4
  39. package/js/gallery.js +18 -23
  40. package/js/group-picker.d.ts +0 -5
  41. package/js/group-picker.js +6 -8
  42. package/js/lightbox.d.ts +1 -4
  43. package/js/lightbox.js +6 -10
  44. package/js/modal.d.ts +0 -2
  45. package/js/modal.js +0 -1
  46. package/js/popover.d.ts +0 -3
  47. package/js/popover.js +0 -2
  48. package/js/position.d.ts +0 -13
  49. package/js/position.js +0 -12
  50. package/js/push-menu.d.ts +0 -3
  51. package/js/push-menu.js +6 -11
  52. package/js/range-slider.d.ts +0 -1
  53. package/js/range-slider.js +2 -3
  54. package/js/scroll.d.ts +0 -2
  55. package/js/scroll.js +5 -6
  56. package/js/scrollbar.d.ts +2 -10
  57. package/js/scrollbar.js +25 -42
  58. package/js/select.d.ts +2 -3
  59. package/js/select.js +6 -9
  60. package/js/sidebar-nav.d.ts +4 -15
  61. package/js/sidebar-nav.js +21 -35
  62. package/js/stepper.d.ts +5 -2
  63. package/js/stepper.js +42 -6
  64. package/js/table.d.ts +1 -56
  65. package/js/table.js +4 -56
  66. package/js/tabs.d.ts +1 -32
  67. package/js/tabs.js +7 -39
  68. package/js/theme.d.ts +0 -44
  69. package/js/theme.js +0 -44
  70. package/js/timepicker.d.ts +1 -4
  71. package/js/timepicker.js +1 -2
  72. package/js/toast.d.ts +0 -2
  73. package/js/toast.js +0 -1
  74. package/js/tooltip.d.ts +0 -4
  75. package/js/tooltip.js +0 -1
  76. package/js/tree.d.ts +0 -3
  77. package/js/tree.js +9 -11
  78. package/js/utils.d.ts +0 -10
  79. package/js/utils.js +2 -42
  80. package/js/virtual-dropdown.d.ts +3 -5
  81. package/js/virtual-dropdown.js +17 -66
  82. package/package.json +1 -1
@@ -0,0 +1,291 @@
1
+ class ColorPicker {
2
+ container;
3
+ onChange;
4
+ canvas;
5
+ ctx;
6
+ cursor;
7
+ hueSlider;
8
+ hexInput;
9
+ rInput;
10
+ gInput;
11
+ bInput;
12
+ preview;
13
+ hue = 0;
14
+ saturation = 100;
15
+ brightness = 100;
16
+ isDragging = false;
17
+ abortController = new AbortController();
18
+ ro;
19
+ constructor(elementOrSelector, options = {}) {
20
+ const el = typeof elementOrSelector === 'string'
21
+ ? document.querySelector(elementOrSelector)
22
+ : elementOrSelector;
23
+ if (!el)
24
+ throw new Error(`ColorPicker: element not found for "${elementOrSelector}"`);
25
+ this.container = el;
26
+ this.onChange = options.onChange;
27
+ this.build();
28
+ this.bindEvents();
29
+ this.ro = new ResizeObserver(() => this.resizeCanvas());
30
+ this.ro.observe(this.canvas.parentElement);
31
+ this.resizeCanvas();
32
+ if (options.value) {
33
+ this.setValue(options.value);
34
+ }
35
+ else {
36
+ this.updateFromHSB();
37
+ }
38
+ }
39
+ build() {
40
+ this.container.classList.add('color-picker');
41
+ this.container.innerHTML = `
42
+ <div class="color-picker__field">
43
+ <canvas class="color-picker__canvas"></canvas>
44
+ <div class="color-picker__cursor" aria-hidden="true"></div>
45
+ </div>
46
+ <div class="color-picker__controls">
47
+ <div class="color-picker__hue-track">
48
+ <input type="range" class="color-picker__hue-slider" min="0" max="360" value="0" aria-label="Hue">
49
+ </div>
50
+ <div class="color-picker__preview" aria-hidden="true"></div>
51
+ </div>
52
+ <div class="color-picker__inputs">
53
+ <div class="color-picker__input-group color-picker__input-group--hex">
54
+ <label>HEX</label>
55
+ <input type="text" class="color-picker__hex" value="#FF0000" spellcheck="false" autocomplete="off" maxlength="7">
56
+ </div>
57
+ <div class="color-picker__input-group">
58
+ <label>R</label>
59
+ <input type="number" class="color-picker__r" min="0" max="255" value="255">
60
+ </div>
61
+ <div class="color-picker__input-group">
62
+ <label>G</label>
63
+ <input type="number" class="color-picker__g" min="0" max="255" value="0">
64
+ </div>
65
+ <div class="color-picker__input-group">
66
+ <label>B</label>
67
+ <input type="number" class="color-picker__b" min="0" max="255" value="0">
68
+ </div>
69
+ </div>
70
+ `;
71
+ const canvas = this.container.querySelector('.color-picker__canvas');
72
+ const ctx = canvas?.getContext('2d');
73
+ if (!canvas || !ctx)
74
+ throw new Error('ColorPicker: canvas context unavailable');
75
+ this.canvas = canvas;
76
+ this.ctx = ctx;
77
+ this.cursor = this.container.querySelector('.color-picker__cursor');
78
+ this.hueSlider = this.container.querySelector('.color-picker__hue-slider');
79
+ this.hexInput = this.container.querySelector('.color-picker__hex');
80
+ this.rInput = this.container.querySelector('.color-picker__r');
81
+ this.gInput = this.container.querySelector('.color-picker__g');
82
+ this.bInput = this.container.querySelector('.color-picker__b');
83
+ this.preview = this.container.querySelector('.color-picker__preview');
84
+ }
85
+ bindEvents() {
86
+ const sig = { signal: this.abortController.signal };
87
+ this.canvas.addEventListener('pointerdown', (e) => {
88
+ e.preventDefault();
89
+ this.canvas.setPointerCapture(e.pointerId);
90
+ this.isDragging = true;
91
+ this.handleFieldInteraction(e);
92
+ }, sig);
93
+ this.canvas.addEventListener('pointermove', (e) => {
94
+ if (!this.isDragging)
95
+ return;
96
+ this.handleFieldInteraction(e);
97
+ }, sig);
98
+ this.canvas.addEventListener('pointerup', () => {
99
+ this.isDragging = false;
100
+ }, sig);
101
+ this.canvas.addEventListener('pointercancel', () => {
102
+ this.isDragging = false;
103
+ }, sig);
104
+ this.hueSlider.addEventListener('input', () => {
105
+ this.hue = +this.hueSlider.value;
106
+ this.drawField();
107
+ this.updateFromHSB();
108
+ }, sig);
109
+ this.hexInput.addEventListener('change', () => this.handleHexInput(), sig);
110
+ this.rInput.addEventListener('change', () => this.handleRGBInput(), sig);
111
+ this.gInput.addEventListener('change', () => this.handleRGBInput(), sig);
112
+ this.bInput.addEventListener('change', () => this.handleRGBInput(), sig);
113
+ }
114
+ resizeCanvas() {
115
+ const w = this.canvas.offsetWidth;
116
+ const h = this.canvas.offsetHeight;
117
+ if (w === 0 || h === 0)
118
+ return;
119
+ if (this.canvas.width === w && this.canvas.height === h)
120
+ return;
121
+ this.canvas.width = w;
122
+ this.canvas.height = h;
123
+ this.drawField();
124
+ this.updateCursor();
125
+ }
126
+ drawField() {
127
+ const { width, height } = this.canvas;
128
+ if (width === 0 || height === 0)
129
+ return;
130
+ const gradH = this.ctx.createLinearGradient(0, 0, width, 0);
131
+ gradH.addColorStop(0, '#fff');
132
+ gradH.addColorStop(1, `hsl(${this.hue}, 100%, 50%)`);
133
+ this.ctx.fillStyle = gradH;
134
+ this.ctx.fillRect(0, 0, width, height);
135
+ const gradV = this.ctx.createLinearGradient(0, 0, 0, height);
136
+ gradV.addColorStop(0, 'rgba(0,0,0,0)');
137
+ gradV.addColorStop(1, '#000');
138
+ this.ctx.fillStyle = gradV;
139
+ this.ctx.fillRect(0, 0, width, height);
140
+ }
141
+ handleFieldInteraction(e) {
142
+ const rect = this.canvas.getBoundingClientRect();
143
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
144
+ const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
145
+ this.saturation = (x / rect.width) * 100;
146
+ this.brightness = 100 - (y / rect.height) * 100;
147
+ this.updateCursor();
148
+ this.updateFromHSB();
149
+ }
150
+ updateCursor() {
151
+ const rect = this.canvas.getBoundingClientRect();
152
+ const x = (this.saturation / 100) * rect.width;
153
+ const y = (1 - this.brightness / 100) * rect.height;
154
+ this.cursor.style.left = `${x}px`;
155
+ this.cursor.style.top = `${y}px`;
156
+ this.cursor.classList.toggle('color-picker__cursor--dark', this.brightness > 50 && this.saturation < 80);
157
+ }
158
+ updateFromHSB() {
159
+ const rgb = this.hsbToRgb(this.hue, this.saturation, this.brightness);
160
+ const hex = this.rgbToHex(rgb);
161
+ this.syncInputs(rgb, hex);
162
+ this.syncPreview(rgb);
163
+ this.onChange?.(hex, rgb);
164
+ }
165
+ syncInputs(rgb, hex) {
166
+ this.hexInput.value = hex;
167
+ this.rInput.value = String(rgb.r);
168
+ this.gInput.value = String(rgb.g);
169
+ this.bInput.value = String(rgb.b);
170
+ }
171
+ syncPreview(rgb) {
172
+ this.preview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
173
+ }
174
+ handleHexInput() {
175
+ const hex = this.hexInput.value.trim();
176
+ if (/^#[0-9a-f]{6}$/i.test(hex)) {
177
+ this.setFromRGB(this.hexToRgb(hex));
178
+ }
179
+ else {
180
+ this.hexInput.value = this.rgbToHex(this.hsbToRgb(this.hue, this.saturation, this.brightness));
181
+ }
182
+ }
183
+ handleRGBInput() {
184
+ const clamp = (v) => Math.max(0, Math.min(255, isNaN(v) ? 0 : v));
185
+ this.setFromRGB({
186
+ r: clamp(+this.rInput.value),
187
+ g: clamp(+this.gInput.value),
188
+ b: clamp(+this.bInput.value),
189
+ });
190
+ }
191
+ setFromRGB(rgb) {
192
+ const { h, s, v } = this.rgbToHsb(rgb);
193
+ this.hue = h;
194
+ this.saturation = s;
195
+ this.brightness = v;
196
+ this.hueSlider.value = String(this.hue);
197
+ this.drawField();
198
+ this.updateCursor();
199
+ this.updateFromHSB();
200
+ }
201
+ getValue() {
202
+ return this.rgbToHex(this.hsbToRgb(this.hue, this.saturation, this.brightness));
203
+ }
204
+ setValue(hex) {
205
+ if (!/^#[0-9a-f]{6}$/i.test(hex))
206
+ return;
207
+ this.setFromRGB(this.hexToRgb(hex));
208
+ }
209
+ destroy() {
210
+ this.abortController.abort();
211
+ this.ro.disconnect();
212
+ this.container.classList.remove('color-picker');
213
+ this.container.innerHTML = '';
214
+ }
215
+ hsbToRgb(h, s, v) {
216
+ s /= 100;
217
+ v /= 100;
218
+ const c = v * s;
219
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
220
+ const m = v - c;
221
+ let r = 0, g = 0, b = 0;
222
+ if (h < 60) {
223
+ r = c;
224
+ g = x;
225
+ b = 0;
226
+ }
227
+ else if (h < 120) {
228
+ r = x;
229
+ g = c;
230
+ b = 0;
231
+ }
232
+ else if (h < 180) {
233
+ r = 0;
234
+ g = c;
235
+ b = x;
236
+ }
237
+ else if (h < 240) {
238
+ r = 0;
239
+ g = x;
240
+ b = c;
241
+ }
242
+ else if (h < 300) {
243
+ r = x;
244
+ g = 0;
245
+ b = c;
246
+ }
247
+ else {
248
+ r = c;
249
+ g = 0;
250
+ b = x;
251
+ }
252
+ return {
253
+ r: Math.round((r + m) * 255),
254
+ g: Math.round((g + m) * 255),
255
+ b: Math.round((b + m) * 255),
256
+ };
257
+ }
258
+ rgbToHsb({ r, g, b }) {
259
+ r /= 255;
260
+ g /= 255;
261
+ b /= 255;
262
+ const max = Math.max(r, g, b);
263
+ const min = Math.min(r, g, b);
264
+ const d = max - min;
265
+ let h = 0;
266
+ if (d !== 0) {
267
+ if (max === r)
268
+ h = ((g - b) / d) % 6;
269
+ else if (max === g)
270
+ h = (b - r) / d + 2;
271
+ else
272
+ h = (r - g) / d + 4;
273
+ }
274
+ h = Math.round(h * 60);
275
+ if (h < 0)
276
+ h += 360;
277
+ return {
278
+ h,
279
+ s: max === 0 ? 0 : Math.round((d / max) * 100),
280
+ v: Math.round(max * 100),
281
+ };
282
+ }
283
+ rgbToHex({ r, g, b }) {
284
+ return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
285
+ }
286
+ hexToRgb(hex) {
287
+ const m = hex.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
288
+ return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
289
+ }
290
+ }
291
+ export { ColorPicker };
@@ -1,4 +1,3 @@
1
- /** Definition for a single context menu item including optional submenu. */
2
1
  interface ContextMenuItemDef {
3
2
  label: string;
4
3
  icon?: string;
@@ -12,10 +11,8 @@ type ContextMenuInput = ContextMenuItemDef | 'separator' | {
12
11
  group: string;
13
12
  };
14
13
  interface ContextMenuOptions {
15
- /** Path to the SVG sprite file, e.g. `'svg-icons/icons.svg'`. Required to render icons. */
16
14
  spritePath?: string;
17
15
  }
18
- /** Right-click context menu with keyboard navigation and nested submenu support. */
19
16
  declare class ContextMenu {
20
17
  private items;
21
18
  private targets;
@@ -1,4 +1,3 @@
1
- /** Right-click context menu with keyboard navigation and nested submenu support. */
2
1
  class ContextMenu {
3
2
  items;
4
3
  targets;
@@ -148,8 +147,6 @@ class ContextMenu {
148
147
  const rect = li.getBoundingClientRect();
149
148
  return rect.right + submenuEl.offsetWidth > window.innerWidth;
150
149
  };
151
- // Delay timer prevents the submenu closing when mouse travels from
152
- // item → submenu (mouseleave fires before mouseenter on the submenu)
153
150
  let closeTimer = null;
154
151
  const openSub = () => {
155
152
  if (closeTimer) {
@@ -1,9 +1,7 @@
1
- /** Localised day and month names for the DatePicker. */
2
1
  interface DatePickerLocales {
3
2
  days: string[];
4
3
  months: string[];
5
4
  }
6
- /** Configuration options for the DatePicker. */
7
5
  interface DatePickerOptions {
8
6
  mode?: 'single' | 'range';
9
7
  startDay?: number;
@@ -12,12 +10,10 @@ interface DatePickerOptions {
12
10
  format?: (date: Date) => string;
13
11
  onSelect?: (date: Date | DateRange) => void;
14
12
  }
15
- /** A date range with optional start and end dates. */
16
13
  interface DateRange {
17
14
  start: Date | null;
18
15
  end: Date | null;
19
16
  }
20
- /** Calendar-based date (or date-range) picker that attaches to an input element. */
21
17
  declare class DatePicker {
22
18
  private input;
23
19
  private options;
package/js/datepicker.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { computePosition } from './position.js';
2
- /** Calendar-based date (or date-range) picker that attaches to an input element. */
3
2
  class DatePicker {
4
3
  input;
5
4
  options;
package/js/docs-nav.js CHANGED
@@ -16,6 +16,7 @@ const NAV = [
16
16
  items: [
17
17
  { label: 'Inputs & Forms', href: 'forms/inputs.html' },
18
18
  { label: 'Date Picker', href: 'forms/datepicker.html' },
19
+ { label: 'Color Picker', href: 'forms/color-picker.html' },
19
20
  { label: 'Range Slider', href: 'forms/range-slider.html' },
20
21
  { label: 'File Uploader', href: 'forms/file-uploader.html' },
21
22
  ],
package/js/dropdown.d.ts CHANGED
@@ -1,14 +1,11 @@
1
- /** Configuration options for a Dropdown instance. */
2
1
  interface DropdownOptions {
3
2
  closeOnSelect?: boolean;
4
3
  allowMultipleOpen?: boolean;
5
4
  }
6
- /** Event detail payload for the `dropdown-select` custom event. */
7
5
  interface DropdownSelectDetail {
8
6
  text: string;
9
7
  element: HTMLElement;
10
8
  }
11
- /** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
12
9
  declare class Dropdown {
13
10
  private container;
14
11
  private trigger;
@@ -28,4 +25,4 @@ declare class Dropdown {
28
25
  private handleSelection;
29
26
  destroy(): void;
30
27
  }
31
- export { Dropdown, DropdownSelectDetail };
28
+ export { Dropdown, type DropdownSelectDetail };
package/js/dropdown.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { bestPlacement } from './position.js';
2
- /** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
3
2
  class Dropdown {
4
3
  container;
5
4
  trigger;
package/js/editor.d.ts CHANGED
@@ -1,10 +1,9 @@
1
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
2
  simple?: boolean;
3
+ root?: string | HTMLElement;
5
4
  }
6
- /** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
7
5
  declare class Editor {
6
+ private readonly root;
8
7
  private readonly editable;
9
8
  private readonly code;
10
9
  private readonly preview;
@@ -14,6 +13,8 @@ declare class Editor {
14
13
  private redoStack;
15
14
  private abortController;
16
15
  constructor(options?: EditorOptions);
16
+ private q;
17
+ private qAll;
17
18
  private bindToolbar;
18
19
  private bindActions;
19
20
  private bindKeyboard;
package/js/editor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { sanitizeHtml } from './utils.js';
2
- /** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
3
2
  class Editor {
3
+ root;
4
4
  editable;
5
5
  code;
6
6
  preview;
@@ -10,24 +10,35 @@ class Editor {
10
10
  redoStack = [];
11
11
  abortController = new AbortController();
12
12
  constructor(options = {}) {
13
- const editable = document.getElementById('editable');
14
- if (!editable) {
15
- throw new Error('Editor: #editable element not found');
13
+ if (options.root instanceof HTMLElement) {
14
+ this.root = options.root;
16
15
  }
16
+ else if (typeof options.root === 'string') {
17
+ const el = document.querySelector(options.root);
18
+ if (!el)
19
+ throw new Error(`Editor: root "${options.root}" not found`);
20
+ this.root = el;
21
+ }
22
+ else {
23
+ this.root = document.body;
24
+ }
25
+ const editable = this.q('[data-editor="editable"]');
26
+ if (!editable)
27
+ throw new Error('Editor: [data-editor="editable"] element not found');
17
28
  this.editable = editable;
18
- this.wordCount = document.getElementById('wordCount');
29
+ this.wordCount = this.q('[data-editor="wordcount"]');
19
30
  if (options.simple) {
20
31
  this.code = null;
21
32
  this.preview = null;
22
- this.sidePanel = document.getElementById('sidePanel');
33
+ this.sidePanel = this.q('[data-editor="side-panel"]');
23
34
  this.sidePanel?.classList.add('hidden');
24
35
  }
25
36
  else {
26
- const code = document.getElementById('code');
27
- const preview = document.getElementById('preview');
28
- const sidePanel = document.getElementById('sidePanel');
37
+ const code = this.q('[data-editor="code"]');
38
+ const preview = this.q('[data-editor="preview"]');
39
+ const sidePanel = this.q('[data-editor="side-panel"]');
29
40
  if (!code || !preview || !sidePanel) {
30
- throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
41
+ throw new Error('Editor: [data-editor="code"], [data-editor="preview"] and [data-editor="side-panel"] are required unless simple: true');
31
42
  }
32
43
  this.code = code;
33
44
  this.preview = preview;
@@ -42,9 +53,15 @@ class Editor {
42
53
  this.syncViews();
43
54
  this.saveState();
44
55
  }
56
+ q(selector) {
57
+ return this.root.querySelector(selector);
58
+ }
59
+ qAll(selector) {
60
+ return this.root.querySelectorAll(selector);
61
+ }
45
62
  bindToolbar() {
46
63
  const sig = { signal: this.abortController.signal };
47
- document.querySelectorAll('[data-cmd]').forEach(btn => {
64
+ this.qAll('[data-cmd]').forEach(btn => {
48
65
  btn.addEventListener('click', () => {
49
66
  const cmd = btn.dataset.cmd;
50
67
  const val = btn.dataset.value ?? null;
@@ -55,13 +72,13 @@ class Editor {
55
72
  }
56
73
  bindActions() {
57
74
  const sig = { signal: this.abortController.signal };
58
- document.getElementById('linkBtn')?.addEventListener('click', () => {
75
+ this.q('[data-editor-action="link"]')?.addEventListener('click', () => {
59
76
  const url = prompt('Enter URL:', 'https://');
60
77
  if (url)
61
78
  this.exec('createLink', url);
62
79
  }, sig);
63
- const imageFile = document.getElementById('imageFile');
64
- document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click(), sig);
80
+ const imageFile = this.q('[data-editor="image-file"]');
81
+ this.q('[data-editor-action="image"]')?.addEventListener('click', () => imageFile?.click(), sig);
65
82
  imageFile?.addEventListener('change', () => {
66
83
  const file = imageFile.files?.[0];
67
84
  if (!file)
@@ -75,7 +92,7 @@ class Editor {
75
92
  reader.readAsDataURL(file);
76
93
  imageFile.value = '';
77
94
  }, sig);
78
- document.getElementById('cleanBtn')?.addEventListener('click', () => {
95
+ this.q('[data-editor-action="clean"]')?.addEventListener('click', () => {
79
96
  const sel = window.getSelection();
80
97
  if (!sel || sel.rangeCount === 0)
81
98
  return;
@@ -85,35 +102,32 @@ class Editor {
85
102
  range.insertNode(document.createTextNode(text));
86
103
  this.onContentChange();
87
104
  }, sig);
88
- document.getElementById('undoBtn')?.addEventListener('click', () => this.undo(), sig);
89
- document.getElementById('redoBtn')?.addEventListener('click', () => this.redo(), sig);
90
- document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
105
+ this.q('[data-editor-action="undo"]')?.addEventListener('click', () => this.undo(), sig);
106
+ this.q('[data-editor-action="redo"]')?.addEventListener('click', () => this.redo(), sig);
107
+ this.q('[data-editor-action="toggle-code"]')?.addEventListener('click', () => {
91
108
  this.sidePanel?.classList.toggle('hidden');
92
109
  this.syncViews();
93
110
  }, sig);
94
- // Code action buttons — matched by position within .code-actions
95
111
  if (this.code) {
96
112
  const code = this.code;
97
- const codeActions = document.querySelectorAll('.code-actions button');
98
- codeActions[0]?.addEventListener('click', () => {
113
+ this.q('[data-editor-action="apply-code"]')?.addEventListener('click', () => {
99
114
  this.editable.innerHTML = sanitizeHtml(code.value);
100
115
  this.onContentChange();
101
116
  }, sig);
102
- codeActions[1]?.addEventListener('click', () => {
117
+ this.q('[data-editor-action="sanitize-code"]')?.addEventListener('click', () => {
103
118
  code.value = sanitizeHtml(code.value);
104
119
  this.editable.innerHTML = code.value;
105
120
  this.onContentChange();
106
121
  }, sig);
107
- codeActions[2]?.addEventListener('click', () => {
122
+ this.q('[data-editor-action="minify-code"]')?.addEventListener('click', () => {
108
123
  code.value = code.value
109
124
  .replace(/\n/g, '')
110
125
  .replace(/>\s+</g, '><')
111
126
  .trim();
112
127
  }, sig);
113
128
  }
114
- const saveBtn = document.getElementById('saveBtn');
115
- saveBtn?.addEventListener('click', () => this.downloadHTML(), sig);
116
- document.getElementById('clearBtn')?.addEventListener('click', () => {
129
+ this.q('[data-editor-action="save"]')?.addEventListener('click', () => this.downloadHTML(), sig);
130
+ this.q('[data-editor-action="clear"]')?.addEventListener('click', () => {
117
131
  if (confirm('Clear all content?')) {
118
132
  this.editable.innerHTML = '';
119
133
  this.onContentChange();
@@ -121,8 +135,9 @@ class Editor {
121
135
  }, sig);
122
136
  }
123
137
  bindKeyboard() {
124
- const saveBtn = document.getElementById('saveBtn');
125
138
  window.addEventListener('keydown', (e) => {
139
+ if (!this.root.contains(document.activeElement))
140
+ return;
126
141
  const mod = e.ctrlKey || e.metaKey;
127
142
  if (!mod)
128
143
  return;
@@ -147,7 +162,7 @@ class Editor {
147
162
  }
148
163
  else if (key === 's') {
149
164
  e.preventDefault();
150
- saveBtn?.click();
165
+ this.q('[data-editor-action="save"]')?.click();
151
166
  }
152
167
  else if (key === 'z' && !e.shiftKey) {
153
168
  e.preventDefault();
@@ -172,13 +187,13 @@ class Editor {
172
187
  }
173
188
  bindTabs() {
174
189
  const sig = { signal: this.abortController.signal };
175
- document.querySelectorAll('.side-tab[data-tab]').forEach(tab => {
190
+ this.qAll('.side-tab[data-tab]').forEach(tab => {
176
191
  tab.addEventListener('click', () => {
177
- const targetId = tab.dataset.tab;
178
- document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
179
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
192
+ const target = tab.dataset.tab;
193
+ this.qAll('.side-tab').forEach(t => t.classList.remove('active'));
194
+ this.qAll('.side-panel').forEach(p => p.classList.remove('active'));
180
195
  tab.classList.add('active');
181
- document.getElementById(targetId)?.classList.add('active');
196
+ this.q(`[data-editor="${target}"]`)?.classList.add('active');
182
197
  }, sig);
183
198
  });
184
199
  }
@@ -365,7 +380,7 @@ class Editor {
365
380
  const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
366
381
  for (const line of lines) {
367
382
  const li = document.createElement('li');
368
- li.textContent = line.trim() || '\u200B';
383
+ li.textContent = line.trim() || '';
369
384
  list.appendChild(li);
370
385
  }
371
386
  range.deleteContents();
@@ -439,7 +454,7 @@ ${content}
439
454
  const element = container.nodeType === Node.TEXT_NODE
440
455
  ? container.parentElement
441
456
  : container;
442
- document.querySelectorAll('[data-cmd]').forEach(btn => {
457
+ this.qAll('[data-cmd]').forEach(btn => {
443
458
  const cmd = btn.dataset.cmd;
444
459
  let active = false;
445
460
  let current = element;
@@ -1,21 +1,17 @@
1
- /** Event detail payload for the `upload-completed` custom event. */
2
1
  interface UploadCompletedDetail {
3
2
  fileCount: number;
4
3
  files: File[];
5
4
  results: PromiseSettledResult<unknown>[];
6
5
  }
7
- /** Event detail payload for the `file-validation-error` custom event. */
8
6
  interface FileValidationErrorDetail {
9
7
  file: File;
10
8
  reason: 'size' | 'type';
11
9
  }
12
- /** Configuration options for the FileUploader. */
13
10
  interface FileUploaderConfig {
14
11
  uploadUrl?: string;
15
12
  maxFileSize?: number;
16
13
  allowedTypes?: string[];
17
14
  }
18
- /** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
19
15
  declare class FileUploader {
20
16
  private container;
21
17
  private dropZone;
@@ -1,5 +1,4 @@
1
1
  import { escapeHtml } from './utils.js';
2
- /** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
3
2
  class FileUploader {
4
3
  container;
5
4
  dropZone;
@@ -1,4 +1,3 @@
1
- /** Configuration options for the FlyoutMenu. */
2
1
  interface FlyoutMenuOptions {
3
2
  triggerSelector?: string;
4
3
  menuSelector?: string;
@@ -12,7 +11,6 @@ interface FlyoutMenuOptions {
12
11
  enableHeader?: boolean;
13
12
  enableFooter?: boolean;
14
13
  }
15
- /** Off-canvas flyout navigation with nested submenu support. */
16
14
  declare class FlyoutMenu {
17
15
  private options;
18
16
  private menuTrigger;
package/js/flyout-menu.js CHANGED
@@ -1,4 +1,3 @@
1
- /** Off-canvas flyout navigation with nested submenu support. */
2
1
  class FlyoutMenu {
3
2
  options;
4
3
  menuTrigger;