basecoat-cli 0.3.9 → 0.3.10-beta.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.
@@ -0,0 +1,192 @@
1
+ (() => {
2
+ const initCarousel = (carouselComponent) => {
3
+ const slidesContainer = carouselComponent.querySelector('.carousel-slides');
4
+ if (!slidesContainer) return;
5
+
6
+ const slides = Array.from(carouselComponent.querySelectorAll('.carousel-item'));
7
+ const prevButton = carouselComponent.querySelector('.carousel-prev');
8
+ const nextButton = carouselComponent.querySelector('.carousel-next');
9
+ const indicators = Array.from(carouselComponent.querySelectorAll('.carousel-indicators button'));
10
+
11
+ const loop = carouselComponent.dataset.carouselLoop === 'true';
12
+ const autoplayDelay = parseInt(carouselComponent.dataset.carouselAutoplay, 10);
13
+ const orientation = carouselComponent.dataset.orientation || 'horizontal';
14
+
15
+ let currentIndex = 0;
16
+ let autoplayInterval = null;
17
+
18
+ const getScrollAmount = () => {
19
+ if (slides.length === 0) return 0;
20
+ const firstSlide = slides[0];
21
+ return orientation === 'vertical'
22
+ ? firstSlide.offsetHeight + parseInt(getComputedStyle(slidesContainer).gap || 0)
23
+ : firstSlide.offsetWidth + parseInt(getComputedStyle(slidesContainer).gap || 0);
24
+ };
25
+
26
+ const scrollToIndex = (index) => {
27
+ const scrollAmount = getScrollAmount();
28
+ if (orientation === 'vertical') {
29
+ slidesContainer.scrollTo({ top: scrollAmount * index, behavior: 'smooth' });
30
+ } else {
31
+ slidesContainer.scrollTo({ left: scrollAmount * index, behavior: 'smooth' });
32
+ }
33
+ currentIndex = index;
34
+ updateIndicators();
35
+ updateButtonStates();
36
+ };
37
+
38
+ const updateIndicators = () => {
39
+ indicators.forEach((indicator, index) => {
40
+ const isActive = index === currentIndex;
41
+ indicator.setAttribute('aria-current', isActive ? 'true' : 'false');
42
+ indicator.setAttribute('aria-label', `Slide ${index + 1}${isActive ? ' (current)' : ''}`);
43
+ });
44
+
45
+ slides.forEach((slide, index) => {
46
+ slide.setAttribute('aria-hidden', index === currentIndex ? 'false' : 'true');
47
+ });
48
+ };
49
+
50
+ const updateButtonStates = () => {
51
+ if (!prevButton || !nextButton) return;
52
+
53
+ if (loop) {
54
+ prevButton.disabled = false;
55
+ nextButton.disabled = false;
56
+ } else {
57
+ prevButton.disabled = currentIndex === 0;
58
+ nextButton.disabled = currentIndex === slides.length - 1;
59
+ }
60
+ };
61
+
62
+ const goToPrevious = () => {
63
+ if (currentIndex > 0) {
64
+ scrollToIndex(currentIndex - 1);
65
+ } else if (loop) {
66
+ scrollToIndex(slides.length - 1);
67
+ }
68
+ };
69
+
70
+ const goToNext = () => {
71
+ if (currentIndex < slides.length - 1) {
72
+ scrollToIndex(currentIndex + 1);
73
+ } else if (loop) {
74
+ scrollToIndex(0);
75
+ }
76
+ };
77
+
78
+ const startAutoplay = () => {
79
+ if (!autoplayDelay || autoplayDelay <= 0) return;
80
+
81
+ autoplayInterval = setInterval(() => {
82
+ goToNext();
83
+ }, autoplayDelay);
84
+ };
85
+
86
+ const stopAutoplay = () => {
87
+ if (autoplayInterval) {
88
+ clearInterval(autoplayInterval);
89
+ autoplayInterval = null;
90
+ }
91
+ };
92
+
93
+ const detectCurrentSlide = () => {
94
+ const scrollPosition = orientation === 'vertical'
95
+ ? slidesContainer.scrollTop
96
+ : slidesContainer.scrollLeft;
97
+ const scrollAmount = getScrollAmount();
98
+ const newIndex = Math.round(scrollPosition / scrollAmount);
99
+
100
+ if (newIndex !== currentIndex && newIndex >= 0 && newIndex < slides.length) {
101
+ currentIndex = newIndex;
102
+ updateIndicators();
103
+ updateButtonStates();
104
+ }
105
+ };
106
+
107
+ // Previous/Next button handlers
108
+ if (prevButton) {
109
+ prevButton.addEventListener('click', () => {
110
+ stopAutoplay();
111
+ goToPrevious();
112
+ });
113
+ }
114
+
115
+ if (nextButton) {
116
+ nextButton.addEventListener('click', () => {
117
+ stopAutoplay();
118
+ goToNext();
119
+ });
120
+ }
121
+
122
+ // Indicator click handlers
123
+ indicators.forEach((indicator, index) => {
124
+ indicator.addEventListener('click', () => {
125
+ stopAutoplay();
126
+ scrollToIndex(index);
127
+ });
128
+ });
129
+
130
+ // Keyboard navigation
131
+ carouselComponent.addEventListener('keydown', (event) => {
132
+ const isVertical = orientation === 'vertical';
133
+ const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
134
+ const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
135
+
136
+ switch (event.key) {
137
+ case prevKey:
138
+ event.preventDefault();
139
+ stopAutoplay();
140
+ goToPrevious();
141
+ break;
142
+ case nextKey:
143
+ event.preventDefault();
144
+ stopAutoplay();
145
+ goToNext();
146
+ break;
147
+ case 'Home':
148
+ event.preventDefault();
149
+ stopAutoplay();
150
+ scrollToIndex(0);
151
+ break;
152
+ case 'End':
153
+ event.preventDefault();
154
+ stopAutoplay();
155
+ scrollToIndex(slides.length - 1);
156
+ break;
157
+ }
158
+ });
159
+
160
+ // Detect scroll position changes (for touch/manual scrolling)
161
+ let scrollTimeout;
162
+ slidesContainer.addEventListener('scroll', () => {
163
+ clearTimeout(scrollTimeout);
164
+ scrollTimeout = setTimeout(() => {
165
+ detectCurrentSlide();
166
+ }, 100);
167
+ });
168
+
169
+ // Pause autoplay on hover or focus
170
+ if (autoplayDelay) {
171
+ carouselComponent.addEventListener('mouseenter', stopAutoplay);
172
+ carouselComponent.addEventListener('mouseleave', startAutoplay);
173
+ carouselComponent.addEventListener('focusin', stopAutoplay);
174
+ carouselComponent.addEventListener('focusout', startAutoplay);
175
+ }
176
+
177
+ // Initialize
178
+ updateIndicators();
179
+ updateButtonStates();
180
+
181
+ if (autoplayDelay) {
182
+ startAutoplay();
183
+ }
184
+
185
+ carouselComponent.dataset.carouselInitialized = true;
186
+ carouselComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
187
+ };
188
+
189
+ if (window.basecoat) {
190
+ window.basecoat.register('carousel', '.carousel:not([data-carousel-initialized])', initCarousel);
191
+ }
192
+ })();
@@ -0,0 +1 @@
1
+ (()=>{const e=e=>{const t=e.querySelector(".carousel-slides");if(!t)return;const r=Array.from(e.querySelectorAll(".carousel-item")),a=e.querySelector(".carousel-prev"),o=e.querySelector(".carousel-next"),l=Array.from(e.querySelectorAll(".carousel-indicators button")),s="true"===e.dataset.carouselLoop,n=parseInt(e.dataset.carouselAutoplay,10),i=e.dataset.orientation||"horizontal";let c=0,d=null;const u=()=>{if(0===r.length)return 0;const e=r[0];return"vertical"===i?e.offsetHeight+parseInt(getComputedStyle(t).gap||0):e.offsetWidth+parseInt(getComputedStyle(t).gap||0)},v=e=>{const r=u();"vertical"===i?t.scrollTo({top:r*e,behavior:"smooth"}):t.scrollTo({left:r*e,behavior:"smooth"}),c=e,f(),h()},f=()=>{l.forEach(((e,t)=>{const r=t===c;e.setAttribute("aria-current",r?"true":"false"),e.setAttribute("aria-label",`Slide ${t+1}${r?" (current)":""}`)})),r.forEach(((e,t)=>{e.setAttribute("aria-hidden",t===c?"false":"true")}))},h=()=>{a&&o&&(s?(a.disabled=!1,o.disabled=!1):(a.disabled=0===c,o.disabled=c===r.length-1))},p=()=>{c>0?v(c-1):s&&v(r.length-1)},b=()=>{c<r.length-1?v(c+1):s&&v(0)},E=()=>{!n||n<=0||(d=setInterval((()=>{b()}),n))},g=()=>{d&&(clearInterval(d),d=null)};let m;a&&a.addEventListener("click",(()=>{g(),p()})),o&&o.addEventListener("click",(()=>{g(),b()})),l.forEach(((e,t)=>{e.addEventListener("click",(()=>{g(),v(t)}))})),e.addEventListener("keydown",(e=>{const t="vertical"===i,a=t?"ArrowUp":"ArrowLeft",o=t?"ArrowDown":"ArrowRight";switch(e.key){case a:e.preventDefault(),g(),p();break;case o:e.preventDefault(),g(),b();break;case"Home":e.preventDefault(),g(),v(0);break;case"End":e.preventDefault(),g(),v(r.length-1)}})),t.addEventListener("scroll",(()=>{clearTimeout(m),m=setTimeout((()=>{(()=>{const e="vertical"===i?t.scrollTop:t.scrollLeft,a=u(),o=Math.round(e/a);o!==c&&o>=0&&o<r.length&&(c=o,f(),h())})()}),100)})),n&&(e.addEventListener("mouseenter",g),e.addEventListener("mouseleave",E),e.addEventListener("focusin",g),e.addEventListener("focusout",E)),f(),h(),n&&E(),e.dataset.carouselInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("carousel",".carousel:not([data-carousel-initialized])",e)})();
@@ -3,23 +3,31 @@
3
3
  const trigger = selectComponent.querySelector(':scope > button');
4
4
  const selectedLabel = trigger.querySelector(':scope > span');
5
5
  const popover = selectComponent.querySelector(':scope > [data-popover]');
6
- const listbox = popover.querySelector('[role="listbox"]');
6
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
7
7
  const input = selectComponent.querySelector(':scope > input[type="hidden"]');
8
8
  const filter = selectComponent.querySelector('header input[type="text"]');
9
+
9
10
  if (!trigger || !popover || !listbox || !input) {
10
11
  const missing = [];
11
12
  if (!trigger) missing.push('trigger');
12
13
  if (!popover) missing.push('popover');
13
14
  if (!listbox) missing.push('listbox');
14
- if (!input) missing.push('input');
15
+ if (!input) missing.push('input');
15
16
  console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
16
17
  return;
17
18
  }
18
-
19
+
19
20
  const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
20
21
  const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
21
22
  let visibleOptions = [...options];
22
23
  let activeIndex = -1;
24
+ const isMultiple = listbox.getAttribute('aria-multiselectable') === 'true';
25
+ let selectedValues = isMultiple ? new Set() : null;
26
+ let placeholder = null;
27
+
28
+ if (isMultiple) {
29
+ placeholder = selectComponent.dataset.placeholder || '';
30
+ }
23
31
 
24
32
  const setActiveOption = (index) => {
25
33
  if (activeIndex > -1 && options[activeIndex]) {
@@ -46,23 +54,92 @@
46
54
  return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
47
55
  };
48
56
 
49
- const updateValue = (option, triggerEvent = true) => {
50
- if (option) {
57
+ const syncMultipleInputs = () => {
58
+ if (!isMultiple) return;
59
+ const values = Array.from(selectedValues);
60
+ const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
61
+ inputs.slice(1).forEach(inp => inp.remove());
62
+
63
+ if (values.length === 0) {
64
+ input.value = '';
65
+ } else {
66
+ input.value = values[0];
67
+ let insertAfter = input;
68
+ for (let i = 1; i < values.length; i++) {
69
+ const clone = input.cloneNode(true);
70
+ clone.removeAttribute('id');
71
+ clone.value = values[i];
72
+ insertAfter.after(clone);
73
+ insertAfter = clone;
74
+ }
75
+ }
76
+ };
77
+
78
+ const updateMultipleLabel = () => {
79
+ if (!isMultiple) return;
80
+ const selected = options.filter(opt => selectedValues.has(opt.dataset.value));
81
+ if (selected.length === 0) {
82
+ selectedLabel.textContent = placeholder;
83
+ selectedLabel.classList.add('text-muted-foreground');
84
+ } else {
85
+ selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
86
+ selectedLabel.classList.remove('text-muted-foreground');
87
+ }
88
+ };
89
+
90
+ const updateValue = (optionOrOptions, triggerEvent = true) => {
91
+ let value;
92
+
93
+ if (isMultiple) {
94
+ const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
95
+ selectedValues = new Set(opts.map(opt => opt.dataset.value));
96
+ options.forEach(opt => {
97
+ if (selectedValues.has(opt.dataset.value)) {
98
+ opt.setAttribute('aria-selected', 'true');
99
+ } else {
100
+ opt.removeAttribute('aria-selected');
101
+ }
102
+ });
103
+ updateMultipleLabel();
104
+ syncMultipleInputs();
105
+ value = Array.from(selectedValues);
106
+ } else {
107
+ const option = optionOrOptions;
108
+ if (!option) return;
51
109
  selectedLabel.innerHTML = option.innerHTML;
52
110
  input.value = option.dataset.value;
53
- listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
54
- option.setAttribute('aria-selected', 'true');
55
-
56
- if (triggerEvent) {
57
- const event = new CustomEvent('change', {
58
- detail: { value: option.dataset.value },
59
- bubbles: true
60
- });
61
- selectComponent.dispatchEvent(event);
62
- }
111
+ options.forEach(opt => {
112
+ if (opt === option) {
113
+ opt.setAttribute('aria-selected', 'true');
114
+ } else {
115
+ opt.removeAttribute('aria-selected');
116
+ }
117
+ });
118
+ value = option.dataset.value;
119
+ }
120
+
121
+ if (triggerEvent) {
122
+ selectComponent.dispatchEvent(new CustomEvent('change', {
123
+ detail: { value },
124
+ bubbles: true
125
+ }));
63
126
  }
64
127
  };
65
128
 
129
+ const toggleMultipleValue = (value, triggerEvent = true) => {
130
+ if (!isMultiple || value == null) return;
131
+
132
+ const newValues = new Set(selectedValues);
133
+ if (newValues.has(value)) {
134
+ newValues.delete(value);
135
+ } else {
136
+ newValues.add(value);
137
+ }
138
+
139
+ const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
140
+ updateValue(selectedOptions, triggerEvent);
141
+ };
142
+
66
143
  const closePopover = (focusOnTrigger = true) => {
67
144
  if (popover.getAttribute('aria-hidden') === 'true') return;
68
145
 
@@ -101,7 +178,27 @@
101
178
 
102
179
  const selectByValue = (value) => {
103
180
  const option = options.find(opt => opt.dataset.value === value);
104
- selectOption(option);
181
+ if (isMultiple) {
182
+ if (value != null && selectedValues.has(value)) return;
183
+ if (option && value != null) {
184
+ const newValues = new Set(selectedValues);
185
+ newValues.add(value);
186
+ const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
187
+ updateValue(selectedOptions);
188
+ }
189
+ } else {
190
+ selectOption(option);
191
+ }
192
+ };
193
+
194
+ const selectAll = () => {
195
+ if (!isMultiple) return;
196
+ updateValue(options.filter(opt => opt.dataset.value != null));
197
+ };
198
+
199
+ const selectNone = () => {
200
+ if (!isMultiple) return;
201
+ updateValue([]);
105
202
  };
106
203
 
107
204
  if (filter) {
@@ -137,13 +234,32 @@
137
234
  filter.addEventListener('input', filterOptions);
138
235
  }
139
236
 
140
- let initialOption = options.find(opt => opt.dataset.value === input.value);
141
-
142
- if (!initialOption) {
143
- initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
144
- }
237
+ if (isMultiple) {
238
+ const validValues = new Set(options.map(opt => opt.dataset.value).filter(v => v != null));
239
+ const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
240
+ const initialValues = inputs
241
+ .map(inp => inp.value)
242
+ .filter(v => v != null && validValues.has(v));
243
+
244
+ let initialOptions;
245
+ if (initialValues.length > 0) {
246
+ initialOptions = options.filter(opt => initialValues.includes(opt.dataset.value));
247
+ } else {
248
+ initialOptions = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
249
+ }
250
+
251
+ updateValue(initialOptions, false);
252
+ } else {
253
+ let initialOption = options.find(opt => opt.dataset.value === input.value);
145
254
 
146
- updateValue(initialOption, false);
255
+ if (!initialOption) {
256
+ initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
257
+ }
258
+
259
+ if (initialOption) {
260
+ updateValue(initialOption, false);
261
+ }
262
+ }
147
263
 
148
264
  const handleKeyNavigation = (event) => {
149
265
  const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
@@ -169,7 +285,11 @@
169
285
 
170
286
  if (event.key === 'Enter') {
171
287
  if (activeIndex > -1) {
172
- selectOption(options[activeIndex]);
288
+ if (isMultiple) {
289
+ toggleMultipleValue(options[activeIndex].dataset.value);
290
+ } else {
291
+ selectOption(options[activeIndex]);
292
+ }
173
293
  }
174
294
  return;
175
295
  }
@@ -268,7 +388,17 @@
268
388
  listbox.addEventListener('click', (event) => {
269
389
  const clickedOption = event.target.closest('[role="option"]');
270
390
  if (clickedOption) {
271
- selectOption(clickedOption);
391
+ if (isMultiple) {
392
+ toggleMultipleValue(clickedOption.dataset.value);
393
+ setActiveOption(options.indexOf(clickedOption));
394
+ if (filter) {
395
+ filter.focus();
396
+ } else {
397
+ trigger.focus();
398
+ }
399
+ } else {
400
+ selectOption(clickedOption);
401
+ }
272
402
  }
273
403
  });
274
404
 
@@ -285,8 +415,12 @@
285
415
  });
286
416
 
287
417
  popover.setAttribute('aria-hidden', 'true');
288
-
418
+
289
419
  selectComponent.selectByValue = selectByValue;
420
+ if (isMultiple) {
421
+ selectComponent.selectAll = selectAll;
422
+ selectComponent.selectNone = selectNone;
423
+ }
290
424
  selectComponent.dataset.selectInitialized = true;
291
425
  selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
292
426
  };
@@ -1 +1 @@
1
- (()=>{const e=e=>{const t=e.querySelector(":scope > button"),i=t.querySelector(":scope > span"),a=e.querySelector(":scope > [data-popover]"),r=a.querySelector('[role="listbox"]'),n=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&a&&r&&n)){const i=[];return t||i.push("trigger"),a||i.push("popover"),r||i.push("listbox"),n||i.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${i.join(", ")}`,e)}const o=Array.from(r.querySelectorAll('[role="option"]')),d=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let c=[...d],l=-1;const u=e=>{if(l>-1&&d[l]&&d[l].classList.remove("active"),l=e,l>-1){const e=d[l];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},v=()=>{const e=getComputedStyle(a);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},p=(t,a=!0)=>{if(t&&(i.innerHTML=t.innerHTML,n.value=t.dataset.value,r.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute("aria-selected"),t.setAttribute("aria-selected","true"),a)){const i=new CustomEvent("change",{detail:{value:t.dataset.value},bubbles:!0});e.dispatchEvent(i)}},f=(e=!0)=>{if("true"!==a.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",c=[...d],o.forEach((e=>e.setAttribute("aria-hidden","false")))};v()?a.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),a.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),u(-1)}},b=e=>{if(!e)return;const t=n.value,i=e.dataset.value;null!=i&&i!==t&&p(e),f()};if(s){const e=()=>{const e=s.value.trim().toLowerCase();u(-1),c=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(d.includes(t)&&c.push(t));const i=(t.dataset.filter||t.textContent).trim().toLowerCase(),a=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),r=i.includes(e)||a;t.setAttribute("aria-hidden",String(!r)),r&&d.includes(t)&&c.push(t)}))};s.addEventListener("input",e)}let h=d.find((e=>e.dataset.value===n.value));h||(h=d.find((e=>void 0!==e.dataset.value))??d[0]),p(h,!1);const E=e=>{const i="false"===a.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!i)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void f();if("Enter"===e.key)return void(l>-1&&b(d[l]));if(0===c.length)return;const r=l>-1?c.indexOf(d[l]):-1;let n=r;switch(e.key){case"ArrowDown":r<c.length-1&&(n=r+1);break;case"ArrowUp":r>0?n=r-1:-1===r&&(n=0);break;case"Home":n=0;break;case"End":n=c.length-1}if(n!==r){const e=c[n];u(d.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};r.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&c.includes(t)){const e=d.indexOf(t);e!==l&&u(e)}})),r.addEventListener("mouseleave",(()=>{const e=r.querySelector('[role="option"][aria-selected="true"]');u(e?d.indexOf(e):-1)})),t.addEventListener("keydown",E),s&&s.addEventListener("keydown",E);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?f():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(v()?a.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),a.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const i=r.querySelector('[role="option"][aria-selected="true"]');i&&(u(d.indexOf(i)),i.scrollIntoView({block:"nearest"}))})()})),r.addEventListener("click",(e=>{const t=e.target.closest('[role="option"]');t&&b(t)})),document.addEventListener("click",(t=>{e.contains(t.target)||f(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&f(!1)})),a.setAttribute("aria-hidden","true"),e.selectByValue=e=>{const t=d.find((t=>t.dataset.value===e));b(t)},e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
1
+ (()=>{const e=e=>{const t=e.querySelector(":scope > button"),a=t.querySelector(":scope > span"),r=e.querySelector(":scope > [data-popover]"),n=r?r.querySelector('[role="listbox"]'):null,i=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&r&&n&&i)){const a=[];return t||a.push("trigger"),r||a.push("popover"),n||a.push("listbox"),i||a.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${a.join(", ")}`,e)}const o=Array.from(n.querySelectorAll('[role="option"]')),l=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let d=[...l],u=-1;const c="true"===n.getAttribute("aria-multiselectable");let f=c?new Set:null,v=null;c&&(v=e.dataset.placeholder||"");const p=e=>{if(u>-1&&l[u]&&l[u].classList.remove("active"),u=e,u>-1){const e=l[u];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},h=()=>{const e=getComputedStyle(r);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},b=(t,r=!0)=>{let n;if(c){const r=Array.isArray(t)?t:[];f=new Set(r.map((e=>e.dataset.value))),l.forEach((e=>{f.has(e.dataset.value)?e.setAttribute("aria-selected","true"):e.removeAttribute("aria-selected")})),(()=>{if(!c)return;const e=l.filter((e=>f.has(e.dataset.value)));0===e.length?(a.textContent=v,a.classList.add("text-muted-foreground")):(a.textContent=e.map((e=>e.dataset.label||e.textContent.trim())).join(", "),a.classList.remove("text-muted-foreground"))})(),(()=>{if(!c)return;const t=Array.from(f);if(Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).slice(1).forEach((e=>e.remove())),0===t.length)i.value="";else{i.value=t[0];let e=i;for(let a=1;a<t.length;a++){const r=i.cloneNode(!0);r.removeAttribute("id"),r.value=t[a],e.after(r),e=r}}})(),n=Array.from(f)}else{const e=t;if(!e)return;a.innerHTML=e.innerHTML,i.value=e.dataset.value,l.forEach((t=>{t===e?t.setAttribute("aria-selected","true"):t.removeAttribute("aria-selected")})),n=e.dataset.value}r&&e.dispatchEvent(new CustomEvent("change",{detail:{value:n},bubbles:!0}))},m=(e,t=!0)=>{if(!c||null==e)return;const a=new Set(f);a.has(e)?a.delete(e):a.add(e);const r=l.filter((e=>a.has(e.dataset.value)));b(r,t)},A=(e=!0)=>{if("true"!==r.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",d=[...l],o.forEach((e=>e.setAttribute("aria-hidden","false")))};h()?r.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),r.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),p(-1)}},y=e=>{if(!e)return;const t=i.value,a=e.dataset.value;null!=a&&a!==t&&b(e),A()},E=()=>{c&&b(l.filter((e=>null!=e.dataset.value)))},w=()=>{c&&b([])};if(s){const e=()=>{const e=s.value.trim().toLowerCase();p(-1),d=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(l.includes(t)&&d.push(t));const a=(t.dataset.filter||t.textContent).trim().toLowerCase(),r=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),n=a.includes(e)||r;t.setAttribute("aria-hidden",String(!n)),n&&l.includes(t)&&d.push(t)}))};s.addEventListener("input",e)}if(c){const t=new Set(l.map((e=>e.dataset.value)).filter((e=>null!=e))),a=Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).map((e=>e.value)).filter((e=>null!=e&&t.has(e)));let r;r=a.length>0?l.filter((e=>a.includes(e.dataset.value))):l.filter((e=>"true"===e.getAttribute("aria-selected"))),b(r,!1)}else{let e=l.find((e=>e.dataset.value===i.value));e||(e=l.find((e=>void 0!==e.dataset.value))??l[0]),e&&b(e,!1)}const g=e=>{const a="false"===r.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!a)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void A();if("Enter"===e.key)return void(u>-1&&(c?m(l[u].dataset.value):y(l[u])));if(0===d.length)return;const n=u>-1?d.indexOf(l[u]):-1;let i=n;switch(e.key){case"ArrowDown":n<d.length-1&&(i=n+1);break;case"ArrowUp":n>0?i=n-1:-1===n&&(i=0);break;case"Home":i=0;break;case"End":i=d.length-1}if(i!==n){const e=d[i];p(l.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};n.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&d.includes(t)){const e=l.indexOf(t);e!==u&&p(e)}})),n.addEventListener("mouseleave",(()=>{const e=n.querySelector('[role="option"][aria-selected="true"]');p(e?l.indexOf(e):-1)})),t.addEventListener("keydown",g),s&&s.addEventListener("keydown",g);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?A():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(h()?r.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),r.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const a=n.querySelector('[role="option"][aria-selected="true"]');a&&(p(l.indexOf(a)),a.scrollIntoView({block:"nearest"}))})()})),n.addEventListener("click",(e=>{const a=e.target.closest('[role="option"]');a&&(c?(m(a.dataset.value),p(l.indexOf(a)),s?s.focus():t.focus()):y(a))})),document.addEventListener("click",(t=>{e.contains(t.target)||A(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&A(!1)})),r.setAttribute("aria-hidden","true"),e.selectByValue=e=>{const t=l.find((t=>t.dataset.value===e));if(c){if(null!=e&&f.has(e))return;if(t&&null!=e){const t=new Set(f);t.add(e);const a=l.filter((e=>t.has(e.dataset.value)));b(a)}}else y(t)},c&&(e.selectAll=E,e.selectNone=w),e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
@@ -0,0 +1,111 @@
1
+ {#
2
+ Renders a carousel component with navigation controls and optional indicators.
3
+
4
+ @param id {string} [optional] - Unique identifier for the carousel component. Auto-generated if not provided.
5
+ @param slides {array} - An array of objects representing carousel slides.
6
+ Each object should have:
7
+ - content {string}: HTML content for the slide.
8
+ - attrs {object} [optional]: Additional HTML attributes for the slide item.
9
+ @param loop {boolean} [optional] [default=false] - Enable continuous looping.
10
+ @param autoplay {number} [optional] [default=0] - Auto-advance delay in milliseconds (0 = disabled).
11
+ @param align {string} [optional] [default='start'] - Slide alignment ('start' or 'center').
12
+ @param orientation {string} [optional] [default='horizontal'] - Carousel orientation ('horizontal' or 'vertical').
13
+ @param show_controls {boolean} [optional] [default=true] - Show previous/next navigation buttons.
14
+ @param show_indicators {boolean} [optional] [default=true] - Show slide indicator dots.
15
+ @param main_attrs {object} [optional] - Additional HTML attributes for the main container.
16
+ @param viewport_attrs {object} [optional] - Additional HTML attributes for the viewport container.
17
+ #}
18
+ {% macro carousel(
19
+ id=None,
20
+ slides=[],
21
+ loop=false,
22
+ autoplay=0,
23
+ align='start',
24
+ orientation='horizontal',
25
+ show_controls=true,
26
+ show_indicators=true,
27
+ main_attrs={},
28
+ viewport_attrs={}
29
+ )
30
+ %}
31
+ {% set id = id or ("carousel-" + (range(100000, 999999) | random | string)) %}
32
+ <div
33
+ class="carousel {{ main_attrs.class }}"
34
+ id="{{ id }}"
35
+ data-carousel-loop="{{ 'true' if loop else 'false' }}"
36
+ {% if autoplay > 0 %}data-carousel-autoplay="{{ autoplay }}"{% endif %}
37
+ data-orientation="{{ orientation }}"
38
+ role="region"
39
+ aria-roledescription="carousel"
40
+ aria-label="Carousel"
41
+ tabindex="0"
42
+ {% for key, value in main_attrs %}
43
+ {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
44
+ {% endfor %}
45
+ >
46
+ <div
47
+ class="carousel-viewport {{ viewport_attrs.class }}"
48
+ {% for key, value in viewport_attrs %}
49
+ {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
50
+ {% endfor %}
51
+ >
52
+ <div
53
+ class="carousel-slides"
54
+ data-orientation="{{ orientation }}"
55
+ >
56
+ {% for slide in slides %}
57
+ <div
58
+ class="carousel-item"
59
+ role="group"
60
+ aria-roledescription="slide"
61
+ aria-label="{{ loop.index }} of {{ slides | length }}"
62
+ {% if align == 'center' %}data-align="center"{% endif %}
63
+ {% if slide.attrs %}
64
+ {% for key, value in slide.attrs %}
65
+ {{ key }}="{{ value }}"
66
+ {% endfor %}
67
+ {% endif %}
68
+ >
69
+ {{ slide.content | safe }}
70
+ </div>
71
+ {% endfor %}
72
+ </div>
73
+ </div>
74
+
75
+ {% if show_controls %}
76
+ <div class="carousel-controls">
77
+ <button
78
+ type="button"
79
+ class="carousel-prev"
80
+ aria-label="Previous slide"
81
+ >
82
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
83
+ <path d="m15 18-6-6 6-6"/>
84
+ </svg>
85
+ </button>
86
+ <button
87
+ type="button"
88
+ class="carousel-next"
89
+ aria-label="Next slide"
90
+ >
91
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
92
+ <path d="m9 18 6-6-6-6"/>
93
+ </svg>
94
+ </button>
95
+ </div>
96
+ {% endif %}
97
+
98
+ {% if show_indicators %}
99
+ <div class="carousel-indicators" role="tablist" aria-label="Slides">
100
+ {% for slide in slides %}
101
+ <button
102
+ type="button"
103
+ role="tab"
104
+ aria-label="Slide {{ loop.index }}"
105
+ {% if loop.index == 1 %}aria-current="true"{% else %}aria-current="false"{% endif %}
106
+ ></button>
107
+ {% endfor %}
108
+ </div>
109
+ {% endif %}
110
+ </div>
111
+ {% endmacro %}