@code-collective/booking-widget 1.0.3 → 1.0.4

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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { CartItem, CartUnitItem, CheckoutProductDto, CheckoutCartItemDetailDto, CheckoutOptionDto, CheckoutAvailabilityDto, CheckoutPickupLocationDto, CheckoutAvailabilityCalendarDto } from './client-types';
3
3
  import type { BookingApi } from './api';
4
- import type { WizardPages, WizardWidgetType } from './config';
4
+ import type { WizardPages, WizardPageConfig, WizardWidgetType } from './config';
5
5
  import { formatCurrency } from './currency';
6
6
  import { dateKey, formatTime, parseDateFromAvailabilityId } from './utils';
7
7
  import OptionCard from './OptionCard.svelte';
@@ -15,13 +15,14 @@
15
15
  product: CheckoutProductDto;
16
16
  api: BookingApi;
17
17
  wizardPages: WizardPages;
18
+ editPages?: WizardPages;
18
19
  editItem?: CheckoutCartItemDetailDto;
19
20
  autoSelectSingleTimeSlot?: boolean;
20
21
  onComplete: (item: CartItem) => void;
21
22
  onCancel?: () => void;
22
23
  }
23
24
  let {
24
- product, api, wizardPages,
25
+ product, api, wizardPages, editPages = wizardPages,
25
26
  editItem, autoSelectSingleTimeSlot = false, onComplete, onCancel,
26
27
  }: Props = $props();
27
28
 
@@ -43,10 +44,27 @@
43
44
  let loadingTimeSlots = $state(false);
44
45
  let dirty = $state(false);
45
46
  let showConfirmDialog = $state(false);
47
+ let expandedSection = $state(0);
48
+
49
+ // Widgets whose previously-made selection was invalidated by a later change (e.g. a
50
+ // capacity increase that no longer fits the selected slot) and now needs to be redone.
51
+ // Surfaced on the edit accordion as a red border + "needs new configuration" summary,
52
+ // even while collapsed, so the user knows exactly what to revisit instead of everything.
53
+ let invalidatedWidgets = $state<Set<WizardWidgetType>>(new Set());
54
+
55
+ function markInvalidated(...types: WizardWidgetType[]) {
56
+ invalidatedWidgets = new Set([...invalidatedWidgets, ...types]);
57
+ }
58
+
59
+ function clearInvalidated(...types: WizardWidgetType[]) {
60
+ if (types.every(t => !invalidatedWidgets.has(t))) return;
61
+ invalidatedWidgets = new Set([...invalidatedWidgets].filter(w => !types.includes(w)));
62
+ }
46
63
 
47
64
  let hasQuantities = $derived(Object.values(unitQuantities).some(q => q > 0));
48
65
  let hasAllDaySlot = $derived(timeSlots.length === 1 && timeSlots[0].allDay);
49
66
  let hasSingleSlot = $derived(timeSlots.length === 1);
67
+ let hasNoTimeSlots = $derived(!loadingTimeSlots && timeSlots.length === 0);
50
68
  let hideTimePicker = $derived(hasAllDaySlot || (autoSelectSingleTimeSlot && hasSingleSlot));
51
69
 
52
70
  // -- Flow detection --
@@ -55,8 +73,8 @@
55
73
  // same unit types, so we can show units and fetch availability before an
56
74
  // option is selected by using the first option as a stand-in.
57
75
  let isAgeFirstFlow = $derived(() => {
58
- const agePage = wizardPages.findIndex(p => p.includes('age-category'));
59
- const optPage = wizardPages.findIndex(p => p.includes('option'));
76
+ const agePage = wizardPages.findIndex(p => p.widgets.includes('age-category'));
77
+ const optPage = wizardPages.findIndex(p => p.widgets.includes('option'));
60
78
  return agePage >= 0 && (optPage < 0 || agePage < optPage);
61
79
  });
62
80
 
@@ -132,7 +150,7 @@
132
150
  case 'option': return !lockedOptionId;
133
151
  case 'age-category': return selectedOption != null;
134
152
  case 'date': return hasQuantities;
135
- case 'time': return selectedDate != null && !hideTimePicker;
153
+ case 'time': return selectedDate != null && !hideTimePicker && !hasNoTimeSlots;
136
154
  case 'pickup': return selectedTimeSlot != null && selectedOption?.pickupAvailable === true;
137
155
  case 'addon': return false;
138
156
  }
@@ -143,7 +161,7 @@
143
161
  switch (type) {
144
162
  case 'age-category': return availableUnits().length > 0;
145
163
  case 'date': return hasQuantities;
146
- case 'time': return selectedDate != null && !hideTimePicker;
164
+ case 'time': return selectedDate != null && !hideTimePicker && !hasNoTimeSlots;
147
165
  case 'option': return !lockedOptionId && (selectedTimeSlot != null || (selectedDate != null && hideTimePicker));
148
166
  case 'pickup': return selectedOption != null && selectedOption.pickupAvailable === true && selectedTimeSlot != null;
149
167
  case 'addon': return false;
@@ -151,18 +169,18 @@
151
169
  }
152
170
 
153
171
  let visiblePages = $derived(() => {
154
- const result: { index: number; widgets: WizardWidgetType[] }[] = [];
172
+ const result: { index: number; title: string; widgets: WizardWidgetType[] }[] = [];
155
173
  for (let i = 0; i < wizardPages.length; i++) {
156
- const visible = wizardPages[i].filter(isWidgetVisible);
157
- if (visible.length > 0) result.push({ index: i, widgets: visible });
174
+ const visible = wizardPages[i].widgets.filter(isWidgetVisible);
175
+ if (visible.length > 0) result.push({ index: i, title: wizardPages[i].title, widgets: visible });
158
176
  }
159
177
  return result;
160
178
  });
161
179
 
162
180
  let allVisibleWidgets = $derived(() => {
163
181
  const result: WizardWidgetType[] = [];
164
- for (const page of wizardPages) {
165
- for (const w of page) {
182
+ for (const page of editPages) {
183
+ for (const w of page.widgets) {
166
184
  if (isWidgetVisible(w) && !result.includes(w)) result.push(w);
167
185
  }
168
186
  }
@@ -177,7 +195,53 @@
177
195
 
178
196
  let isLastPage = $derived(() => pageIndex >= visiblePages().length - 1);
179
197
 
198
+ let pageTitle = $derived(() => currentPage()?.title ?? '');
199
+
200
+ function widgetSummary(type: WizardWidgetType): string {
201
+ switch (type) {
202
+ case 'option':
203
+ return selectedOption?.title ?? 'Not selected';
204
+ case 'age-category': {
205
+ const parts: string[] = [];
206
+ for (const unit of availableUnits()) {
207
+ const qty = unitQuantities[unit.id] ?? 0;
208
+ if (qty > 0) parts.push(`${qty} ${unit.title}`);
209
+ }
210
+ return parts.length > 0 ? parts.join(', ') : 'Not selected';
211
+ }
212
+ case 'date':
213
+ if (!selectedDate) return 'Not selected';
214
+ return selectedDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
215
+ case 'time':
216
+ return selectedTime ?? (hideTimePicker ? 'All day' : 'Not selected');
217
+ case 'pickup':
218
+ return selectedPickupPoint?.title ?? selectedPickupPoint?.id ?? 'Not selected';
219
+ default:
220
+ return '';
221
+ }
222
+ }
223
+
224
+ function sectionSummary(widgets: WizardWidgetType[]): string {
225
+ return widgets.map(widgetSummary).filter(s => s && s !== 'Not selected').join(' \u00B7 ') || 'Not selected';
226
+ }
227
+
228
+ let editSections = $derived(() => {
229
+ const result: { title: string; widgets: WizardWidgetType[] }[] = [];
230
+ for (const page of editPages) {
231
+ const visible = page.widgets.filter(isWidgetVisible);
232
+ if (visible.length > 0) {
233
+ result.push({ title: page.title, widgets: visible });
234
+ }
235
+ }
236
+ return result;
237
+ });
238
+
180
239
  let pageComplete = $derived(() => {
240
+ // A date with confirmed zero slots can never be completed - 'time' is hidden entirely in
241
+ // this case (see isWidgetVisible), so without this the widget-list check below would never
242
+ // even look at it and could wrongly treat the page as done.
243
+ if (selectedDate != null && hasNoTimeSlots) return false;
244
+
181
245
  const widgets = editMode ? allVisibleWidgets() : (currentPage()?.widgets ?? []);
182
246
  if (widgets.length === 0) return false;
183
247
  for (const w of widgets) {
@@ -207,10 +271,14 @@
207
271
 
208
272
  function selectOption(option: CheckoutOptionDto) {
209
273
  // In age-first flow the user already picked quantities, date, and time
210
- // before reaching the option step — keep those selections intact.
274
+ // before reaching the option step — keep those selections intact. Pickup only
275
+ // needs to be redone if the new option doesn't offer the point already chosen.
211
276
  if (isAgeFirstFlow()) {
212
277
  selectedOption = option;
213
- selectedPickupPoint = null;
278
+ if (selectedPickupPoint && !option.pickupLocations?.some(p => p.id === selectedPickupPoint!.id)) {
279
+ selectedPickupPoint = null;
280
+ markInvalidated('pickup');
281
+ }
214
282
  dirty = true;
215
283
  return;
216
284
  }
@@ -232,6 +300,22 @@
232
300
  if (hasQuantities && Object.keys(availabilityByDate).length === 0) {
233
301
  fetchAvailability();
234
302
  }
303
+ invalidateDateIfCapacityExceedsSlot();
304
+ }
305
+
306
+ // A capacity increase can outgrow the already-selected slot's vacancy - if so, that slot
307
+ // no longer covers the booking and needs to be reselected. If it still has room (or has no
308
+ // vacancy limit at all), the date/time picked earlier is still perfectly valid.
309
+ function invalidateDateIfCapacityExceedsSlot() {
310
+ if (!selectedTimeSlot || selectedTimeSlot.vacancies == null) return;
311
+ const totalQty = Object.values(unitQuantities).reduce((sum, q) => sum + q, 0);
312
+ if (totalQty <= selectedTimeSlot.vacancies) return;
313
+
314
+ selectedDate = null;
315
+ selectedTime = null;
316
+ selectedTimeSlot = null;
317
+ timeSlots = [];
318
+ markInvalidated('date', 'time');
235
319
  }
236
320
 
237
321
  async function fetchAvailability() {
@@ -245,13 +329,15 @@
245
329
  async function onDateSelected(date: Date) {
246
330
  const optionId = effectiveOptionId();
247
331
  if (!optionId) return;
332
+ // Pickup points are a property of the option, not the date/time, so an already-chosen
333
+ // one stays valid across a date change and doesn't need to be picked again.
248
334
  selectedDate = date;
249
335
  selectedTime = null;
250
336
  selectedTimeSlot = null;
251
- selectedPickupPoint = null;
252
337
  timeSlots = [];
253
338
  dirty = true;
254
339
  loadingTimeSlots = true;
340
+ clearInvalidated('date', 'time');
255
341
 
256
342
  const slots = await api.getAvailability(product.id, optionId, dateKey(date));
257
343
  timeSlots = slots;
@@ -270,13 +356,14 @@
270
356
  if (!slot) return;
271
357
  selectedTime = time;
272
358
  selectedTimeSlot = slot;
273
- selectedPickupPoint = null;
274
359
  dirty = true;
360
+ clearInvalidated('time');
275
361
  }
276
362
 
277
363
  function onPickupSelected(point: CheckoutPickupLocationDto) {
278
364
  selectedPickupPoint = point;
279
365
  dirty = true;
366
+ clearInvalidated('pickup');
280
367
  }
281
368
 
282
369
  function goNext() {
@@ -359,100 +446,205 @@
359
446
  </div>
360
447
  {/if}
361
448
 
362
- <div class="wizard-body">
363
- {#each editMode ? allVisibleWidgets() : (currentPage()?.widgets ?? []) as widgetType}
364
- {#if widgetType === 'option' && !lockedOptionId}
365
- <div class="section">
366
- <h4 class="section-title">Options</h4>
367
- <div class="option-list">
368
- {#each product.options as option}
369
- <OptionCard
370
- {option}
371
- selected={selectedOption?.id === option.id}
372
- onSelect={() => selectOption(option)}
373
- />
374
- {/each}
375
- </div>
449
+ {#if !editMode}
450
+ <div class="page-title-row">
451
+ <h3 class="page-title">{pageTitle()}</h3>
452
+ {#if visiblePages().length > 1}
453
+ <div class="page-dots">
454
+ {#each visiblePages() as _, i}
455
+ <span class="dot" class:active={i === pageIndex}></span>
456
+ {/each}
376
457
  </div>
377
458
  {/if}
459
+ </div>
460
+ {/if}
378
461
 
379
- {#if widgetType === 'age-category' && availableUnits().length > 0}
380
- <div class="section">
381
- <h4 class="section-title">How many tickets?</h4>
382
- <div class="unit-list">
383
- {#each availableUnits() as unit}
384
- <UnitCounter
385
- {unit}
386
- quantity={unitQuantities[unit.id] ?? 0}
387
- onChange={(qty) => onUnitChanged(unit.id, qty)}
388
- />
389
- {/each}
390
- </div>
462
+ {#if editMode}
463
+ <div class="wizard-body">
464
+ {#each editSections() as section, sectionIndex}
465
+ {@const sectionInvalidated = section.widgets.some(w => invalidatedWidgets.has(w))}
466
+ <div class="accordion-section" class:expanded={expandedSection === sectionIndex} class:invalidated={sectionInvalidated}>
467
+ <button class="accordion-header" onclick={() => { expandedSection = expandedSection === sectionIndex ? -1 : sectionIndex; }}>
468
+ <div class="accordion-header-text">
469
+ <span class="accordion-title">{section.title.toUpperCase()}</span>
470
+ {#if expandedSection !== sectionIndex}
471
+ <span class="accordion-summary" class:invalidated={sectionInvalidated}>
472
+ {sectionInvalidated ? 'Changes made require new configuration' : sectionSummary(section.widgets)}
473
+ </span>
474
+ {/if}
475
+ </div>
476
+ <span class="accordion-chevron" class:open={expandedSection === sectionIndex}></span>
477
+ </button>
478
+ {#if expandedSection === sectionIndex}
479
+ <div class="accordion-body">
480
+ {#each section.widgets as widgetType}
481
+ {#if widgetType === 'option' && !lockedOptionId}
482
+ <div class="section">
483
+ <div class="option-list">
484
+ {#each product.options as option}
485
+ <OptionCard
486
+ {option}
487
+ selected={selectedOption?.id === option.id}
488
+ onSelect={() => selectOption(option)}
489
+ />
490
+ {/each}
491
+ </div>
492
+ </div>
493
+ {/if}
494
+
495
+ {#if widgetType === 'age-category' && availableUnits().length > 0}
496
+ <div class="section">
497
+ <div class="unit-list">
498
+ {#each availableUnits() as unit}
499
+ <UnitCounter
500
+ {unit}
501
+ quantity={unitQuantities[unit.id] ?? 0}
502
+ onChange={(qty) => onUnitChanged(unit.id, qty)}
503
+ />
504
+ {/each}
505
+ </div>
506
+ </div>
507
+ {/if}
508
+
509
+ {#if widgetType === 'date' && hasQuantities}
510
+ <div class="section">
511
+ <AvailabilityCalendar
512
+ {availabilityByDate}
513
+ {selectedDate}
514
+ onDateSelected={onDateSelected}
515
+ loading={loadingAvailability}
516
+ />
517
+ </div>
518
+ {/if}
519
+
520
+ {#if widgetType === 'time' && selectedDate && !hideTimePicker}
521
+ <div class="section">
522
+ <TimeSlotPicker
523
+ {timeSlots}
524
+ {selectedTime}
525
+ loading={loadingTimeSlots}
526
+ onTimeSelected={onTimeSelected}
527
+ />
528
+ </div>
529
+ {/if}
530
+
531
+ {#if widgetType === 'pickup' && selectedOption?.pickupAvailable && selectedOption.pickupLocations}
532
+ <div class="section">
533
+ <div class="pickup-list">
534
+ {#each selectedOption.pickupLocations as point}
535
+ <SelectableCard
536
+ selected={selectedPickupPoint?.id === point.id}
537
+ onSelect={() => onPickupSelected(point)}
538
+ heading={point.title ?? point.id}
539
+ body={point.address}
540
+ />
541
+ {/each}
542
+ </div>
543
+ </div>
544
+ {/if}
545
+ {/each}
546
+ </div>
547
+ {/if}
391
548
  </div>
392
- {/if}
549
+ {/each}
550
+ </div>
393
551
 
394
- {#if widgetType === 'date' && hasQuantities}
395
- <div class="section">
396
- <h4 class="section-title">Choose a date</h4>
397
- <AvailabilityCalendar
398
- {availabilityByDate}
399
- {selectedDate}
400
- onDateSelected={onDateSelected}
401
- loading={loadingAvailability}
402
- />
403
- </div>
404
- {/if}
552
+ <div class="action-bar">
553
+ <button class="btn btn-secondary" onclick={handleCancel}>
554
+ Return to Cart
555
+ </button>
556
+ <button class="btn btn-primary" onclick={completeWizard} disabled={!pageComplete()}>
557
+ Save &amp; Update
558
+ </button>
559
+ </div>
405
560
 
406
- {#if widgetType === 'time' && selectedDate && !hideTimePicker}
407
- <div class="section">
408
- <h4 class="section-title">Select a time</h4>
409
- <TimeSlotPicker
410
- {timeSlots}
411
- {selectedTime}
412
- loading={loadingTimeSlots}
413
- onTimeSelected={onTimeSelected}
414
- />
415
- </div>
416
- {/if}
561
+ {:else}
562
+ <div class="wizard-body">
563
+ {#each currentPage()?.widgets ?? [] as widgetType}
564
+ {#if widgetType === 'option' && !lockedOptionId}
565
+ <div class="section">
566
+ <h4 class="section-title">Options</h4>
567
+ <div class="option-list">
568
+ {#each product.options as option}
569
+ <OptionCard
570
+ {option}
571
+ selected={selectedOption?.id === option.id}
572
+ onSelect={() => selectOption(option)}
573
+ />
574
+ {/each}
575
+ </div>
576
+ </div>
577
+ {/if}
417
578
 
418
- {#if widgetType === 'pickup' && selectedOption?.pickupAvailable && selectedOption.pickupLocations}
419
- <div class="section">
420
- <h4 class="section-title">Select pickup point</h4>
421
- <div class="pickup-list">
422
- {#each selectedOption.pickupLocations as point}
423
- <SelectableCard
424
- selected={selectedPickupPoint?.id === point.id}
425
- onSelect={() => onPickupSelected(point)}
426
- heading={point.title ?? point.id}
427
- body={point.address}
428
- />
429
- {/each}
579
+ {#if widgetType === 'age-category' && availableUnits().length > 0}
580
+ <div class="section">
581
+ <h4 class="section-title">How many tickets?</h4>
582
+ <div class="unit-list">
583
+ {#each availableUnits() as unit}
584
+ <UnitCounter
585
+ {unit}
586
+ quantity={unitQuantities[unit.id] ?? 0}
587
+ onChange={(qty) => onUnitChanged(unit.id, qty)}
588
+ />
589
+ {/each}
590
+ </div>
430
591
  </div>
431
- </div>
432
- {/if}
433
- {/each}
592
+ {/if}
434
593
 
435
- <div class="action-bar">
436
- {#if editMode}
437
- <button class="btn btn-secondary" onclick={handleCancel}>
438
- Return to Cart
439
- </button>
440
- <button class="btn btn-primary" onclick={completeWizard} disabled={!pageComplete()}>
441
- Save &amp; Update
442
- </button>
443
- {:else}
444
- {#if pageIndex > 0 || onCancel}
445
- <button class="btn btn-secondary" onclick={goBack}>
446
- {pageIndex > 0 ? 'Back' : 'Cancel'}
447
- </button>
594
+ {#if widgetType === 'date' && hasQuantities}
595
+ <div class="section">
596
+ <h4 class="section-title">Choose a date</h4>
597
+ <AvailabilityCalendar
598
+ {availabilityByDate}
599
+ {selectedDate}
600
+ onDateSelected={onDateSelected}
601
+ loading={loadingAvailability}
602
+ />
603
+ </div>
604
+ {/if}
605
+
606
+ {#if widgetType === 'time' && selectedDate && !hideTimePicker}
607
+ <div class="section">
608
+ <h4 class="section-title">Select a time</h4>
609
+ <TimeSlotPicker
610
+ {timeSlots}
611
+ {selectedTime}
612
+ loading={loadingTimeSlots}
613
+ onTimeSelected={onTimeSelected}
614
+ />
615
+ </div>
448
616
  {/if}
449
- <button class="btn btn-primary" onclick={goNext} disabled={!pageComplete()}>
450
- {isLastPage() ? 'Add to Cart' : 'Next'}
451
- <span class="arrow">&rarr;</span>
617
+
618
+ {#if widgetType === 'pickup' && selectedOption?.pickupAvailable && selectedOption.pickupLocations}
619
+ <div class="section">
620
+ <h4 class="section-title">Select pickup point</h4>
621
+ <div class="pickup-list">
622
+ {#each selectedOption.pickupLocations as point}
623
+ <SelectableCard
624
+ selected={selectedPickupPoint?.id === point.id}
625
+ onSelect={() => onPickupSelected(point)}
626
+ heading={point.title ?? point.id}
627
+ body={point.address}
628
+ />
629
+ {/each}
630
+ </div>
631
+ </div>
632
+ {/if}
633
+ {/each}
634
+ </div>
635
+
636
+ <div class="action-bar">
637
+ {#if pageIndex > 0 || onCancel}
638
+ <button class="btn btn-secondary" onclick={goBack}>
639
+ {pageIndex > 0 ? 'Back' : 'Cancel'}
452
640
  </button>
453
641
  {/if}
642
+ <button class="btn btn-primary" onclick={goNext} disabled={!pageComplete()}>
643
+ {isLastPage() ? 'Add to Cart' : 'Next'}
644
+ <span class="arrow">&rarr;</span>
645
+ </button>
454
646
  </div>
455
- </div>
647
+ {/if}
456
648
  </div>
457
649
 
458
650
  {#if showConfirmDialog}
@@ -484,6 +676,89 @@
484
676
  font-size: 20px;
485
677
  font-weight: 800;
486
678
  }
679
+ .page-title-row {
680
+ display: flex;
681
+ align-items: center;
682
+ justify-content: space-between;
683
+ padding: 12px 16px 0;
684
+ }
685
+ .page-title {
686
+ font-size: 16px;
687
+ font-weight: 700;
688
+ color: var(--bw-color-text);
689
+ }
690
+ .page-dots {
691
+ display: flex;
692
+ gap: 6px;
693
+ }
694
+ .dot {
695
+ width: 8px;
696
+ height: 8px;
697
+ border-radius: 50%;
698
+ background: var(--bw-color-border);
699
+ }
700
+ .dot.active {
701
+ background: var(--bw-color-text);
702
+ }
703
+ .accordion-section {
704
+ border: 1px solid var(--bw-color-border);
705
+ border-radius: var(--bw-radius-lg);
706
+ margin-bottom: 12px;
707
+ overflow: hidden;
708
+ background: var(--bw-color-bg);
709
+ }
710
+ .accordion-section.invalidated {
711
+ border-color: var(--bw-color-error);
712
+ }
713
+ .accordion-header {
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: space-between;
717
+ width: 100%;
718
+ padding: 16px 20px;
719
+ background: none;
720
+ border: none;
721
+ cursor: pointer;
722
+ text-align: left;
723
+ font-family: inherit;
724
+ }
725
+ .accordion-header-text {
726
+ display: flex;
727
+ flex-direction: column;
728
+ gap: 4px;
729
+ min-width: 0;
730
+ flex: 1;
731
+ }
732
+ .accordion-title {
733
+ font-size: 11px;
734
+ font-weight: 700;
735
+ letter-spacing: 0.08em;
736
+ color: var(--bw-color-text-secondary);
737
+ }
738
+ .accordion-summary {
739
+ font-size: 15px;
740
+ font-weight: 600;
741
+ color: var(--bw-color-text);
742
+ }
743
+ .accordion-summary.invalidated {
744
+ color: var(--bw-color-error);
745
+ }
746
+ .accordion-chevron {
747
+ flex-shrink: 0;
748
+ margin-left: 12px;
749
+ width: 12px;
750
+ height: 12px;
751
+ border-right: 2px solid var(--bw-color-text-secondary);
752
+ border-bottom: 2px solid var(--bw-color-text-secondary);
753
+ transform: rotate(45deg);
754
+ transition: transform 0.15s ease;
755
+ }
756
+ .accordion-chevron.open {
757
+ transform: rotate(-135deg);
758
+ }
759
+ .accordion-body {
760
+ padding: 0 20px 20px;
761
+ }
487
762
  .wizard-body {
488
763
  flex: 1;
489
764
  overflow-y: auto;