@dodlhuat/basix 1.0.0 → 1.1.1

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 (142) hide show
  1. package/README.md +1 -1
  2. package/css/accordion.scss +31 -22
  3. package/css/alert.scss +79 -27
  4. package/css/button.scss +151 -102
  5. package/css/card.scss +11 -12
  6. package/css/carousel.scss +123 -87
  7. package/css/chart.scss +9 -11
  8. package/css/chat-bubbles.scss +2 -2
  9. package/css/checkbox.scss +72 -55
  10. package/css/chips.scss +52 -52
  11. package/css/code-viewer.scss +73 -98
  12. package/css/datepicker.scss +20 -0
  13. package/css/dropdown.scss +151 -137
  14. package/css/editor.scss +9 -6
  15. package/css/file-uploader.scss +187 -195
  16. package/css/flyout-menu.scss +20 -13
  17. package/css/form.scss +168 -115
  18. package/css/gallery.scss +62 -63
  19. package/css/grid.scss +0 -1
  20. package/css/modal.scss +117 -72
  21. package/css/placeholder.scss +17 -12
  22. package/css/properties.scss +6 -0
  23. package/css/push-menu.scss +70 -23
  24. package/css/radiobutton.scss +86 -64
  25. package/css/range-slider.scss +136 -0
  26. package/css/scrollbar.scss +69 -69
  27. package/css/spinner.scss +41 -66
  28. package/css/style.css +4351 -3735
  29. package/css/style.css.map +1 -1
  30. package/css/style.scss +2 -1
  31. package/css/switch.scss +43 -42
  32. package/css/table.scss +61 -40
  33. package/css/tabs.scss +12 -7
  34. package/css/timeline.scss +72 -69
  35. package/css/timepicker.scss +151 -72
  36. package/css/toast.scss +49 -48
  37. package/css/tooltip.scss +112 -122
  38. package/css/tree.scss +135 -192
  39. package/css/typography.scss +70 -9
  40. package/css/virtual-dropdown.scss +201 -142
  41. package/js/carousel.js +45 -18
  42. package/js/carousel.ts +217 -173
  43. package/js/datepicker.js +505 -497
  44. package/js/datepicker.ts +9 -0
  45. package/js/editor.js +398 -415
  46. package/js/file-uploader.js +142 -128
  47. package/js/file-uploader.ts +364 -350
  48. package/js/gallery.js +22 -15
  49. package/js/gallery.ts +17 -12
  50. package/js/index.js +718 -720
  51. package/js/index.ts +7 -8
  52. package/js/push-menu.js +113 -101
  53. package/js/push-menu.ts +17 -2
  54. package/js/range-slider.js +26 -0
  55. package/js/range-slider.ts +33 -0
  56. package/js/timepicker.js +144 -98
  57. package/js/timepicker.ts +194 -131
  58. package/js/tree.js +56 -28
  59. package/js/tree.ts +239 -218
  60. package/package.json +3 -2
  61. package/css/accordion.css +0 -109
  62. package/css/accordion.css.map +0 -1
  63. package/css/alert.css +0 -57
  64. package/css/alert.css.map +0 -1
  65. package/css/button.css +0 -69
  66. package/css/button.css.map +0 -1
  67. package/css/card.css +0 -144
  68. package/css/card.css.map +0 -1
  69. package/css/carousel.css +0 -118
  70. package/css/carousel.css.map +0 -1
  71. package/css/chart.css +0 -159
  72. package/css/chart.css.map +0 -1
  73. package/css/chat-bubbles.css +0 -97
  74. package/css/chat-bubbles.css.map +0 -1
  75. package/css/checkbox.css +0 -77
  76. package/css/checkbox.css.map +0 -1
  77. package/css/chips.css +0 -72
  78. package/css/chips.css.map +0 -1
  79. package/css/code-viewer.css +0 -97
  80. package/css/code-viewer.css.map +0 -1
  81. package/css/colors.css +0 -63
  82. package/css/colors.css.map +0 -1
  83. package/css/datepicker.css +0 -264
  84. package/css/datepicker.css.map +0 -1
  85. package/css/defaults.css +0 -118
  86. package/css/defaults.css.map +0 -1
  87. package/css/dropdown.css +0 -146
  88. package/css/dropdown.css.map +0 -1
  89. package/css/editor.css +0 -413
  90. package/css/file-uploader.css +0 -194
  91. package/css/file-uploader.css.map +0 -1
  92. package/css/flyout-menu.css +0 -345
  93. package/css/flyout-menu.css.map +0 -1
  94. package/css/form-builder.css +0 -9
  95. package/css/form-builder.css.map +0 -1
  96. package/css/form-builder.scss +0 -11
  97. package/css/form.css +0 -130
  98. package/css/form.css.map +0 -1
  99. package/css/gallery.css +0 -91
  100. package/css/gallery.css.map +0 -1
  101. package/css/grid.css +0 -44
  102. package/css/grid.css.map +0 -1
  103. package/css/icons.css +0 -327
  104. package/css/icons.css.map +0 -1
  105. package/css/modal.css +0 -97
  106. package/css/modal.css.map +0 -1
  107. package/css/parameters.css +0 -1
  108. package/css/parameters.css.map +0 -1
  109. package/css/placeholder.css +0 -50
  110. package/css/placeholder.css.map +0 -1
  111. package/css/progress.css +0 -51
  112. package/css/progress.css.map +0 -1
  113. package/css/properties.css +0 -31
  114. package/css/properties.css.map +0 -1
  115. package/css/push-menu.css +0 -145
  116. package/css/push-menu.css.map +0 -1
  117. package/css/radiobutton.css +0 -91
  118. package/css/radiobutton.css.map +0 -1
  119. package/css/reset.css +0 -46
  120. package/css/reset.css.map +0 -1
  121. package/css/scrollbar.css +0 -91
  122. package/css/scrollbar.css.map +0 -1
  123. package/css/spinner.css +0 -118
  124. package/css/spinner.css.map +0 -1
  125. package/css/switch.css +0 -66
  126. package/css/switch.css.map +0 -1
  127. package/css/table.css +0 -201
  128. package/css/table.css.map +0 -1
  129. package/css/tabs.css +0 -135
  130. package/css/tabs.css.map +0 -1
  131. package/css/timeline.css +0 -69
  132. package/css/timeline.css.map +0 -1
  133. package/css/toast.css +0 -98
  134. package/css/toast.css.map +0 -1
  135. package/css/tooltip.css +0 -151
  136. package/css/tooltip.css.map +0 -1
  137. package/css/tree.css +0 -199
  138. package/css/tree.css.map +0 -1
  139. package/css/typography.css +0 -137
  140. package/css/typography.css.map +0 -1
  141. package/css/virtual-dropdown.css +0 -149
  142. package/css/virtual-dropdown.css.map +0 -1
package/js/index.ts CHANGED
@@ -17,6 +17,7 @@ import {Tooltip} from "./tooltip.js";
17
17
  import {Dropdown, DropdownSelectDetail} from "./dropdown.js";
18
18
  import {VirtualDropdown} from "./virtual-dropdown.js";
19
19
  import {TimeSpanPicker} from "./timepicker.js";
20
+ import {RangeSlider} from "./range-slider.js";
20
21
 
21
22
  // Generate sample table data
22
23
  const generateData = (count: number): TableRow[] => {
@@ -349,6 +350,9 @@ utils.ready(() => {
349
350
  },
350
351
  });
351
352
 
353
+ // Initialize range sliders
354
+ RangeSlider.initAll();
355
+
352
356
  Tooltip.initializeAll();
353
357
 
354
358
  const timeSpanPicker = new TimeSpanPicker('timespan-1', {
@@ -397,14 +401,9 @@ new CodeViewer(
397
401
  );
398
402
  new CodeViewer(
399
403
  "#usage-slider-demo",
400
- `<label for="range-slider" class="hidden">Slider</label>
401
- <input
402
- type="range"
403
- min="1"
404
- max="100"
405
- value="50"
406
- id="range-slider-demo"
407
- />`,
404
+ `<div class="range-slider">
405
+ <input type="range" min="1" max="100" value="50" />
406
+ </div>`,
408
407
  "html",
409
408
  );
410
409
  new CodeViewer(
package/js/push-menu.js CHANGED
@@ -1,101 +1,113 @@
1
- class PushMenu {
2
- static init() {
3
- if (this.initialized) {
4
- console.warn('PushMenu: Already initialized');
5
- return;
6
- }
7
- this.refresh();
8
- if (!this.elements.navigation || !this.elements.content) {
9
- throw new Error('PushMenu: Required elements not found (.navigation, .push-content)');
10
- }
11
- this.elements.navigation.addEventListener('change', this.handleNavigationChange.bind(this));
12
- this.initialized = true;
13
- }
14
- static handleNavigationChange() {
15
- const isPushed = this.elements.content?.classList.contains('pushed') ?? false;
16
- if (!isPushed) {
17
- this.elements.content?.addEventListener('click', this.clickNav);
18
- }
19
- else {
20
- this.elements.content?.removeEventListener('click', this.clickNav);
21
- }
22
- this.pushToggle();
23
- }
24
- static pushToggle() {
25
- if (!this.elements.content || !this.elements.menu) {
26
- throw new Error('PushMenu: Required elements not found (.push-content, .push-menu)');
27
- }
28
- const isPushed = this.elements.content.classList.contains('pushed');
29
- this.toggleClass(this.elements.content, 'pushed', !isPushed);
30
- this.toggleClass(this.elements.menu, 'pushed', !isPushed);
31
- this.toggleClass(this.elements.header, 'pushed', !isPushed);
32
- if (this.elements.controlIcon) {
33
- if (isPushed) {
34
- this.elements.controlIcon.classList.remove('icon-menu_open');
35
- this.elements.controlIcon.classList.add('icon-menu');
36
- }
37
- else {
38
- this.elements.controlIcon.classList.add('icon-menu_open');
39
- this.elements.controlIcon.classList.remove('icon-menu');
40
- }
41
- }
42
- }
43
- static toggleClass(element, className, add) {
44
- if (!element)
45
- return;
46
- if (add) {
47
- element.classList.add(className);
48
- }
49
- else {
50
- element.classList.remove(className);
51
- }
52
- }
53
- static open() {
54
- if (!this.elements.content?.classList.contains('pushed')) {
55
- this.pushToggle();
56
- }
57
- }
58
- static close() {
59
- if (this.elements.content?.classList.contains('pushed')) {
60
- this.pushToggle();
61
- }
62
- }
63
- static isOpen() {
64
- return this.elements.content?.classList.contains('pushed') ?? false;
65
- }
66
- static destroy() {
67
- if (!this.initialized)
68
- return;
69
- this.elements.navigation?.removeEventListener('change', this.handleNavigationChange);
70
- this.elements.content?.removeEventListener('click', this.clickNav);
71
- this.close();
72
- this.elements = {
73
- navigation: null,
74
- content: null,
75
- menu: null,
76
- header: null,
77
- controlIcon: null
78
- };
79
- this.initialized = false;
80
- }
81
- static refresh() {
82
- this.elements.navigation = document.querySelector('.navigation');
83
- this.elements.content = document.querySelector('.push-content');
84
- this.elements.menu = document.querySelector('.push-menu');
85
- this.elements.header = document.querySelector('.main-header');
86
- this.elements.controlIcon = document.querySelector('.navigation-controls .icon');
87
- }
88
- }
89
- PushMenu.elements = {
90
- navigation: null,
91
- content: null,
92
- menu: null,
93
- header: null,
94
- controlIcon: null
95
- };
96
- PushMenu.initialized = false;
97
- PushMenu.clickNav = () => {
98
- const navigation = PushMenu.elements.navigation;
99
- navigation?.click();
100
- };
101
- export { PushMenu };
1
+ class PushMenu {
2
+ static init() {
3
+ if (this.initialized) {
4
+ console.warn('PushMenu: Already initialized');
5
+ return;
6
+ }
7
+ this.refresh();
8
+ if (!this.elements.navigation || !this.elements.content) {
9
+ throw new Error('PushMenu: Required elements not found (.navigation, .push-content)');
10
+ }
11
+ this.elements.navigation.addEventListener('change', this.handleNavigationChange.bind(this));
12
+ this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
13
+ this.initialized = true;
14
+ }
15
+ static handleNavigationChange() {
16
+ const isPushed = this.elements.content?.classList.contains('pushed') ?? false;
17
+ if (!isPushed) {
18
+ this.elements.content?.addEventListener('click', this.clickNav);
19
+ }
20
+ else {
21
+ this.elements.content?.removeEventListener('click', this.clickNav);
22
+ }
23
+ this.pushToggle();
24
+ }
25
+ static pushToggle() {
26
+ if (!this.elements.content || !this.elements.menu) {
27
+ throw new Error('PushMenu: Required elements not found (.push-content, .push-menu)');
28
+ }
29
+ const isPushed = this.elements.content.classList.contains('pushed');
30
+ this.toggleClass(this.elements.content, 'pushed', !isPushed);
31
+ this.toggleClass(this.elements.menu, 'pushed', !isPushed);
32
+ this.toggleClass(this.elements.header, 'pushed', !isPushed);
33
+ this.toggleClass(this.elements.backdrop, 'pushed', !isPushed);
34
+ if (this.elements.controlIcon) {
35
+ if (isPushed) {
36
+ this.elements.controlIcon.classList.remove('icon-menu_open');
37
+ this.elements.controlIcon.classList.add('icon-menu');
38
+ }
39
+ else {
40
+ this.elements.controlIcon.classList.add('icon-menu_open');
41
+ this.elements.controlIcon.classList.remove('icon-menu');
42
+ }
43
+ }
44
+ }
45
+ static toggleClass(element, className, add) {
46
+ if (!element)
47
+ return;
48
+ if (add) {
49
+ element.classList.add(className);
50
+ }
51
+ else {
52
+ element.classList.remove(className);
53
+ }
54
+ }
55
+ static open() {
56
+ if (!this.elements.content?.classList.contains('pushed')) {
57
+ this.pushToggle();
58
+ }
59
+ }
60
+ static close() {
61
+ if (this.elements.content?.classList.contains('pushed')) {
62
+ this.pushToggle();
63
+ }
64
+ }
65
+ static isOpen() {
66
+ return this.elements.content?.classList.contains('pushed') ?? false;
67
+ }
68
+ static destroy() {
69
+ if (!this.initialized)
70
+ return;
71
+ this.elements.navigation?.removeEventListener('change', this.handleNavigationChange);
72
+ this.elements.content?.removeEventListener('click', this.clickNav);
73
+ this.elements.backdrop?.removeEventListener('click', this.handleBackdropClick);
74
+ this.close();
75
+ this.elements = {
76
+ navigation: null,
77
+ content: null,
78
+ menu: null,
79
+ header: null,
80
+ controlIcon: null,
81
+ backdrop: null
82
+ };
83
+ this.initialized = false;
84
+ }
85
+ static refresh() {
86
+ this.elements.navigation = document.querySelector('.navigation');
87
+ this.elements.content = document.querySelector('.push-content');
88
+ this.elements.menu = document.querySelector('.push-menu');
89
+ this.elements.header = document.querySelector('.main-header');
90
+ this.elements.controlIcon = document.querySelector('.navigation-controls .icon');
91
+ this.elements.backdrop = document.querySelector('.push-menu-backdrop');
92
+ }
93
+ }
94
+ PushMenu.elements = {
95
+ navigation: null,
96
+ content: null,
97
+ menu: null,
98
+ header: null,
99
+ controlIcon: null,
100
+ backdrop: null
101
+ };
102
+ PushMenu.initialized = false;
103
+ PushMenu.clickNav = () => {
104
+ const navigation = PushMenu.elements.navigation;
105
+ navigation?.click();
106
+ };
107
+ PushMenu.handleBackdropClick = () => {
108
+ if (PushMenu.isOpen()) {
109
+ const navigation = PushMenu.elements.navigation;
110
+ navigation?.click();
111
+ }
112
+ };
113
+ export { PushMenu };
package/js/push-menu.ts CHANGED
@@ -4,6 +4,7 @@ interface PushMenuElements {
4
4
  menu: HTMLElement | null;
5
5
  header: HTMLElement | null;
6
6
  controlIcon: HTMLElement | null;
7
+ backdrop: HTMLElement | null;
7
8
  }
8
9
 
9
10
  class PushMenu {
@@ -12,7 +13,8 @@ class PushMenu {
12
13
  content: null,
13
14
  menu: null,
14
15
  header: null,
15
- controlIcon: null
16
+ controlIcon: null,
17
+ backdrop: null
16
18
  };
17
19
 
18
20
  private static initialized = false;
@@ -31,6 +33,8 @@ class PushMenu {
31
33
 
32
34
  this.elements.navigation.addEventListener('change', this.handleNavigationChange.bind(this));
33
35
 
36
+ this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
37
+
34
38
  this.initialized = true;
35
39
  }
36
40
 
@@ -56,6 +60,7 @@ class PushMenu {
56
60
  this.toggleClass(this.elements.content, 'pushed', !isPushed);
57
61
  this.toggleClass(this.elements.menu, 'pushed', !isPushed);
58
62
  this.toggleClass(this.elements.header, 'pushed', !isPushed);
63
+ this.toggleClass(this.elements.backdrop, 'pushed', !isPushed);
59
64
 
60
65
  if (this.elements.controlIcon) {
61
66
  if (isPushed) {
@@ -83,6 +88,13 @@ class PushMenu {
83
88
  navigation?.click();
84
89
  };
85
90
 
91
+ private static handleBackdropClick = (): void => {
92
+ if (PushMenu.isOpen()) {
93
+ const navigation = PushMenu.elements.navigation as HTMLElement;
94
+ navigation?.click();
95
+ }
96
+ };
97
+
86
98
  public static open(): void {
87
99
  if (!this.elements.content?.classList.contains('pushed')) {
88
100
  this.pushToggle();
@@ -104,6 +116,7 @@ class PushMenu {
104
116
 
105
117
  this.elements.navigation?.removeEventListener('change', this.handleNavigationChange);
106
118
  this.elements.content?.removeEventListener('click', this.clickNav);
119
+ this.elements.backdrop?.removeEventListener('click', this.handleBackdropClick);
107
120
 
108
121
  this.close();
109
122
 
@@ -112,7 +125,8 @@ class PushMenu {
112
125
  content: null,
113
126
  menu: null,
114
127
  header: null,
115
- controlIcon: null
128
+ controlIcon: null,
129
+ backdrop: null
116
130
  };
117
131
 
118
132
  this.initialized = false;
@@ -124,6 +138,7 @@ class PushMenu {
124
138
  this.elements.menu = document.querySelector('.push-menu');
125
139
  this.elements.header = document.querySelector('.main-header');
126
140
  this.elements.controlIcon = document.querySelector('.navigation-controls .icon');
141
+ this.elements.backdrop = document.querySelector('.push-menu-backdrop');
127
142
  }
128
143
  }
129
144
 
@@ -0,0 +1,26 @@
1
+ class RangeSlider {
2
+ constructor(input) {
3
+ this.handleInput = () => {
4
+ this.update();
5
+ };
6
+ this.input = input;
7
+ this.update();
8
+ this.input.addEventListener('input', this.handleInput);
9
+ }
10
+ static initAll(selector = '.range-slider input[type="range"]') {
11
+ document.querySelectorAll(selector).forEach(input => {
12
+ new RangeSlider(input);
13
+ });
14
+ }
15
+ update() {
16
+ const min = +this.input.min || 0;
17
+ const max = +this.input.max || 100;
18
+ const pct = ((+this.input.value - min) / (max - min)) * 100;
19
+ this.input.style.setProperty('--range-fill', `${pct}%`);
20
+ }
21
+ destroy() {
22
+ this.input.removeEventListener('input', this.handleInput);
23
+ this.input.style.removeProperty('--range-fill');
24
+ }
25
+ }
26
+ export { RangeSlider };
@@ -0,0 +1,33 @@
1
+ class RangeSlider {
2
+ private readonly input: HTMLInputElement;
3
+
4
+ constructor(input: HTMLInputElement) {
5
+ this.input = input;
6
+ this.update();
7
+ this.input.addEventListener('input', this.handleInput);
8
+ }
9
+
10
+ public static initAll(selector: string = '.range-slider input[type="range"]'): void {
11
+ document.querySelectorAll<HTMLInputElement>(selector).forEach(input => {
12
+ new RangeSlider(input);
13
+ });
14
+ }
15
+
16
+ private update(): void {
17
+ const min = +this.input.min || 0;
18
+ const max = +this.input.max || 100;
19
+ const pct = ((+this.input.value - min) / (max - min)) * 100;
20
+ this.input.style.setProperty('--range-fill', `${pct}%`);
21
+ }
22
+
23
+ private handleInput = (): void => {
24
+ this.update();
25
+ };
26
+
27
+ public destroy(): void {
28
+ this.input.removeEventListener('input', this.handleInput);
29
+ this.input.style.removeProperty('--range-fill');
30
+ }
31
+ }
32
+
33
+ export { RangeSlider };
package/js/timepicker.js CHANGED
@@ -1,98 +1,144 @@
1
- class TimeSpanPicker {
2
- constructor(containerId, options) {
3
- const element = document.getElementById(containerId);
4
- if (!element) {
5
- throw new Error(`Container with id "${containerId}" not found`);
6
- }
7
- this.container = element;
8
- this.onChange = options?.onChange;
9
- this.render();
10
- // Query the inputs after rendering
11
- this.startTimeInput = this.queryInput('.timespan-start');
12
- this.endTimeInput = this.queryInput('.timespan-end');
13
- // Set default values if provided
14
- if (options?.defaultStart) {
15
- this.startTimeInput.value = options.defaultStart;
16
- }
17
- if (options?.defaultEnd) {
18
- this.endTimeInput.value = options.defaultEnd;
19
- }
20
- this.attachEventListeners();
21
- }
22
- queryInput(selector) {
23
- const input = this.container.querySelector(selector);
24
- if (!input) {
25
- throw new Error(`Input with selector "${selector}" not found`);
26
- }
27
- return input;
28
- }
29
- render() {
30
- this.container.innerHTML = `
31
- <div class="timespan-picker">
32
- <div class="timespan-field">
33
- <label for="timespan-start">from</label>
34
- <input
35
- type="time"
36
- class="timespan-start"
37
- id="timespan-start"
38
- />
39
- </div>
40
-
41
- <div class="timespan-separator">-</div>
42
-
43
- <div class="timespan-field">
44
- <label for="timespan-end">to</label>
45
- <input
46
- type="time"
47
- class="timespan-end"
48
- id="timespan-end"
49
- />
50
- </div>
51
- </div>
52
- `;
53
- }
54
- attachEventListeners() {
55
- this.startTimeInput.addEventListener('change', () => this.handleChange());
56
- this.endTimeInput.addEventListener('change', () => this.handleChange());
57
- }
58
- handleChange() {
59
- const start = this.startTimeInput.value;
60
- const end = this.endTimeInput.value;
61
- // Validate that end time is after start time
62
- if (start && end && start >= end) {
63
- this.endTimeInput.setCustomValidity('Endzeit muss nach Startzeit liegen');
64
- }
65
- else {
66
- this.endTimeInput.setCustomValidity('');
67
- }
68
- // Trigger onChange callback if both values are present
69
- if (this.onChange && start && end) {
70
- this.onChange(start, end);
71
- }
72
- }
73
- getValue() {
74
- return {
75
- start: this.startTimeInput.value,
76
- end: this.endTimeInput.value
77
- };
78
- }
79
- setValue(start, end) {
80
- this.startTimeInput.value = start;
81
- this.endTimeInput.value = end;
82
- this.handleChange();
83
- }
84
- reset() {
85
- this.startTimeInput.value = '';
86
- this.endTimeInput.value = '';
87
- this.endTimeInput.setCustomValidity('');
88
- }
89
- isValid() {
90
- const { start, end } = this.getValue();
91
- return !!(start && end && start < end);
92
- }
93
- destroy() {
94
- this.startTimeInput.removeEventListener('change', () => this.handleChange());
95
- this.endTimeInput.removeEventListener('change', () => this.handleChange());
96
- }
97
- }
98
- export { TimeSpanPicker };
1
+ class TimeSpanPicker {
2
+ constructor(containerId, options) {
3
+ const element = document.getElementById(containerId);
4
+ if (!element) {
5
+ throw new Error(`Container with id "${containerId}" not found`);
6
+ }
7
+ this.container = element;
8
+ this.onChange = options?.onChange;
9
+ this.render();
10
+ this.startTimeInput = this.queryInput('.timespan-start');
11
+ this.endTimeInput = this.queryInput('.timespan-end');
12
+ if (options?.defaultStart) {
13
+ this.startTimeInput.value = options.defaultStart;
14
+ }
15
+ if (options?.defaultEnd) {
16
+ this.endTimeInput.value = options.defaultEnd;
17
+ }
18
+ this.attachEventListeners();
19
+ if (options?.defaultStart || options?.defaultEnd) {
20
+ this.updateUI();
21
+ }
22
+ }
23
+ queryInput(selector) {
24
+ const input = this.container.querySelector(selector);
25
+ if (!input) {
26
+ throw new Error(`Input with selector "${selector}" not found`);
27
+ }
28
+ return input;
29
+ }
30
+ render() {
31
+ this.container.innerHTML = `
32
+ <div class="timespan-picker">
33
+ <div class="timespan-field timespan-field-start">
34
+ <label for="timespan-start">From</label>
35
+ <input
36
+ type="time"
37
+ class="timespan-start"
38
+ id="timespan-start"
39
+ />
40
+ </div>
41
+
42
+ <div class="timespan-center">
43
+ <span class="timespan-arrow">→</span>
44
+ <span class="timespan-duration"></span>
45
+ </div>
46
+
47
+ <div class="timespan-field timespan-field-end">
48
+ <label for="timespan-end">To</label>
49
+ <input
50
+ type="time"
51
+ class="timespan-end"
52
+ id="timespan-end"
53
+ />
54
+ </div>
55
+ </div>
56
+ <div class="timespan-bar" aria-hidden="true">
57
+ <div class="timespan-bar-fill"></div>
58
+ </div>
59
+ `;
60
+ }
61
+ attachEventListeners() {
62
+ this.startTimeInput.addEventListener('change', () => this.handleChange());
63
+ this.endTimeInput.addEventListener('change', () => this.handleChange());
64
+ }
65
+ toMinutes(time) {
66
+ const [h, m] = time.split(':').map(Number);
67
+ return h * 60 + m;
68
+ }
69
+ formatDuration(minutes) {
70
+ const h = Math.floor(minutes / 60);
71
+ const m = minutes % 60;
72
+ if (h && m) return `${h}h ${m}m`;
73
+ if (h) return `${h}h`;
74
+ return `${m}m`;
75
+ }
76
+ updateUI() {
77
+ const picker = this.container.querySelector('.timespan-picker');
78
+ const durationEl = this.container.querySelector('.timespan-duration');
79
+ const barFill = this.container.querySelector('.timespan-bar-fill');
80
+ const start = this.startTimeInput.value;
81
+ const end = this.endTimeInput.value;
82
+ const isError = !!(start && end && start >= end);
83
+ picker?.classList.toggle('is-error', isError);
84
+ if (isError) {
85
+ this.endTimeInput.setCustomValidity('End time must be after start time');
86
+ if (durationEl) durationEl.textContent = '!';
87
+ return;
88
+ }
89
+ this.endTimeInput.setCustomValidity('');
90
+ if (start && end && durationEl && barFill) {
91
+ const startMins = this.toMinutes(start);
92
+ const endMins = this.toMinutes(end);
93
+ const duration = endMins - startMins;
94
+ durationEl.textContent = this.formatDuration(duration);
95
+ const startPct = ((startMins / 1440) * 100).toFixed(2);
96
+ const widthPct = ((duration / 1440) * 100).toFixed(2);
97
+ barFill.style.left = `${startPct}%`;
98
+ barFill.style.width = `${widthPct}%`;
99
+ } else {
100
+ if (durationEl) durationEl.textContent = '';
101
+ if (barFill) { barFill.style.left = '0'; barFill.style.width = '0'; }
102
+ }
103
+ }
104
+ handleChange() {
105
+ this.updateUI();
106
+ const { start, end } = this.getValue();
107
+ if (this.onChange && start && end) {
108
+ this.onChange(start, end);
109
+ }
110
+ }
111
+ getValue() {
112
+ return {
113
+ start: this.startTimeInput.value,
114
+ end: this.endTimeInput.value
115
+ };
116
+ }
117
+ setValue(start, end) {
118
+ this.startTimeInput.value = start;
119
+ this.endTimeInput.value = end;
120
+ this.handleChange();
121
+ }
122
+ reset() {
123
+ this.startTimeInput.value = '';
124
+ this.endTimeInput.value = '';
125
+ this.endTimeInput.setCustomValidity('');
126
+ const picker = this.container.querySelector('.timespan-picker');
127
+ const durationEl = this.container.querySelector('.timespan-duration');
128
+ const barFill = this.container.querySelector('.timespan-bar-fill');
129
+ picker?.classList.remove('is-error');
130
+ if (durationEl) durationEl.textContent = '';
131
+ if (barFill) { barFill.style.left = '0'; barFill.style.width = '0'; }
132
+ }
133
+ isValid() {
134
+ const { start, end } = this.getValue();
135
+ return !!(start && end && start < end);
136
+ }
137
+ destroy() {
138
+ const startHandler = () => this.handleChange();
139
+ const endHandler = () => this.handleChange();
140
+ this.startTimeInput.removeEventListener('change', startHandler);
141
+ this.endTimeInput.removeEventListener('change', endHandler);
142
+ }
143
+ }
144
+ export { TimeSpanPicker };