@getmicdrop/svelte-components 5.9.4 → 5.10.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 (55) hide show
  1. package/dist/calendar/Calendar/MiniMonthCalendar.svelte +3 -3
  2. package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte +3 -3
  3. package/dist/calendar/PublicCard/PublicCard.svelte +2 -2
  4. package/dist/calendar/ShowCard/ShowCard.svelte +1 -1
  5. package/dist/components/Layout/ContentSection.svelte +1 -1
  6. package/dist/patterns/forms/FormValidationSummary.svelte +1 -1
  7. package/dist/patterns/layout/Sidebar.svelte +4 -4
  8. package/dist/patterns/navigation/Header.svelte +1 -1
  9. package/dist/patterns/page/PageLayout.svelte +1 -1
  10. package/dist/primitives/Accordion/AccordionItem.spec.js +2 -2
  11. package/dist/primitives/Accordion/AccordionItem.svelte +1 -1
  12. package/dist/primitives/Badges/Badge.svelte +1 -1
  13. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +11 -2
  14. package/dist/primitives/Breadcrumb/Breadcrumb.svelte.d.ts.map +1 -1
  15. package/dist/primitives/Button/Button.svelte +3 -3
  16. package/dist/primitives/Button/ButtonGroup.svelte +1 -1
  17. package/dist/primitives/Checkbox/Checkbox.spec.js +2 -2
  18. package/dist/primitives/Checkbox/Checkbox.svelte +1 -1
  19. package/dist/primitives/DarkModeToggle.spec.js +1 -1
  20. package/dist/primitives/Dropdown/Dropdown.svelte +10 -0
  21. package/dist/primitives/Dropdown/Dropdown.svelte.d.ts.map +1 -1
  22. package/dist/primitives/Input/Input.svelte +7 -17
  23. package/dist/primitives/Input/Select.spec.js +6 -0
  24. package/dist/primitives/Input/Select.svelte +4 -1
  25. package/dist/primitives/Input/Select.svelte.d.ts.map +1 -1
  26. package/dist/primitives/Input/Textarea.svelte +1 -1
  27. package/dist/primitives/Modal/Modal.svelte +1 -1
  28. package/dist/primitives/Radio/Radio.spec.js +2 -2
  29. package/dist/primitives/Radio/Radio.svelte +1 -1
  30. package/dist/primitives/Tabs/Tabs.svelte +1 -1
  31. package/dist/primitives/Toggle.spec.js +9 -6
  32. package/dist/primitives/Toggle.svelte +84 -22
  33. package/dist/primitives/Toggle.svelte.d.ts.map +1 -1
  34. package/dist/primitives/Tooltip/Tooltip.svelte +1 -1
  35. package/dist/recipes/CropImage/CropImage.svelte +2 -2
  36. package/dist/recipes/SuperLogin/SuperLogin.svelte +1 -1
  37. package/dist/recipes/inputs/MultiSelect.spec.js +6 -0
  38. package/dist/recipes/inputs/MultiSelect.svelte +5 -3
  39. package/dist/recipes/inputs/MultiSelect.svelte.d.ts.map +1 -1
  40. package/dist/recipes/inputs/OTPInput.svelte +1 -1
  41. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.spec.js +6 -0
  42. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte +4 -2
  43. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte.d.ts.map +1 -1
  44. package/dist/recipes/inputs/Search.svelte +1 -1
  45. package/dist/recipes/inputs/SelectDropdown.spec.js +6 -0
  46. package/dist/recipes/inputs/SelectDropdown.svelte +3 -1
  47. package/dist/recipes/inputs/SelectDropdown.svelte.d.ts.map +1 -1
  48. package/dist/recipes/modals/InputModal.svelte +1 -1
  49. package/dist/recipes/modals/ModalTestWrapper.svelte +26 -26
  50. package/dist/stories/ButtonAuditDashboard.spec.js +1 -1
  51. package/dist/stories/ButtonAuditDashboard.svelte +25 -25
  52. package/dist/stories/ButtonGridView.svelte +1 -1
  53. package/dist/stories/RecipesGallery.svelte +3 -3
  54. package/dist/tokens/utilities.css +5 -5
  55. package/package.json +1 -1
@@ -644,7 +644,7 @@
644
644
 
645
645
  <div class="flex items-center gap-2">
646
646
  <button
647
- class="p-2 flex items-center justify-center border-0 rounded-lg bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-all duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
647
+ class="p-2 flex items-center justify-center border-0 rounded-lg bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-all duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
648
648
  class:scale-90={prevPressed}
649
649
  class:bg-gray-100={prevPressed}
650
650
  class:dark:bg-gray-700={prevPressed}
@@ -663,7 +663,7 @@
663
663
 
664
664
  {#if showTodayButton}
665
665
  <button
666
- class="text-sm font-medium px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg bg-transparent text-gray-900 dark:text-white cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-blue-700 hover:text-white hover:border-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-40 disabled:cursor-not-allowed"
666
+ class="text-sm font-medium px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg bg-transparent text-gray-900 dark:text-white cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-blue-700 hover:text-white hover:border-blue-700 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-40 disabled:cursor-not-allowed"
667
667
  class:scale-95={todayPressed}
668
668
  class:bg-blue-700={todayPressed}
669
669
  class:text-white={todayPressed}
@@ -682,7 +682,7 @@
682
682
  {/if}
683
683
 
684
684
  <button
685
- class="p-2 flex items-center justify-center border-0 rounded-lg bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-all duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
685
+ class="p-2 flex items-center justify-center border-0 rounded-lg bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-all duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
686
686
  class:scale-90={nextPressed}
687
687
  class:bg-gray-100={nextPressed}
688
688
  class:dark:bg-gray-700={nextPressed}
@@ -69,7 +69,7 @@
69
69
  </h2>
70
70
  <div class="flex items-center gap-2">
71
71
  <button
72
- class="p-3 -m-1.5 flex items-center justify-center border-0 rounded-full bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
72
+ class="p-3 -m-1.5 flex items-center justify-center border-0 rounded-full bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
73
73
  class:scale-90={prevPressed}
74
74
  class:bg-gray-100={prevPressed}
75
75
  class:dark:bg-gray-700={prevPressed}
@@ -88,7 +88,7 @@
88
88
  <ChevronLeftOutline class="w-5 h-5" />
89
89
  </button>
90
90
  <button
91
- class="{`${typography.label} px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg bg-transparent cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-blue-700 hover:text-white hover:border-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-40 disabled:cursor-not-allowed disabled:text-gray-500 dark:disabled:text-gray-400`}"
91
+ class="{`${typography.label} px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg bg-transparent cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-blue-700 hover:text-white hover:border-blue-700 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:opacity-40 disabled:cursor-not-allowed disabled:text-gray-500 dark:disabled:text-gray-400`}"
92
92
  class:scale-95={todayPressed}
93
93
  class:bg-blue-700={todayPressed}
94
94
  class:text-white={todayPressed}
@@ -105,7 +105,7 @@
105
105
  Today
106
106
  </button>
107
107
  <button
108
- class="p-3 -m-1.5 flex items-center justify-center border-0 rounded-full bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
108
+ class="p-3 -m-1.5 flex items-center justify-center border-0 rounded-full bg-transparent text-gray-500 dark:text-gray-400 cursor-pointer select-none transition-transform duration-100 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
109
109
  class:scale-90={nextPressed}
110
110
  class:bg-gray-100={nextPressed}
111
111
  class:dark:bg-gray-700={nextPressed}
@@ -73,7 +73,7 @@
73
73
  {#each events as event}
74
74
  <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
75
75
  <article
76
- class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 cursor-pointer overflow-hidden transition-all duration-200 hover:shadow-lg hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 relative {view === 'col'
76
+ class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 cursor-pointer overflow-hidden transition-all duration-200 hover:shadow-lg hover:border-gray-300 dark:hover:border-gray-600 focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 relative {view === 'col'
77
77
  ? 'flex flex-col'
78
78
  : 'grid grid-cols-[100px_1fr] sm:grid-cols-[160px_1fr] gap-3 sm:gap-4 p-3 sm:p-4'}"
79
79
  onclick={() => handleEventClick(event)}
@@ -85,7 +85,7 @@
85
85
  <div
86
86
  class="bg-gray-100 dark:bg-gray-700 flex items-center justify-center overflow-hidden {view === 'col'
87
87
  ? 'w-full aspect-[4/3] rounded-t-lg'
88
- : 'w-24 h-24 sm:w-40 sm:h-32 rounded-lg flex-shrink-0'}"
88
+ : 'w-24 h-24 sm:w-40 sm:h-32 rounded-lg shrink-0'}"
89
89
  >
90
90
  <img
91
91
  src={event.image || placeholder}
@@ -83,7 +83,7 @@
83
83
  {event.name || ''}
84
84
  </h2>
85
85
  <button
86
- class="flex-shrink-0 p-2 rounded-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 relative"
86
+ class="shrink-0 p-2 rounded-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 relative"
87
87
  aria-label="Share event"
88
88
  onclick={handleShare}
89
89
  >
@@ -67,7 +67,7 @@
67
67
  {/if}
68
68
  </div>
69
69
  {#if actions}
70
- <div class="flex items-center gap-3 flex-shrink-0">
70
+ <div class="flex items-center gap-3 shrink-0">
71
71
  {@render actions()}
72
72
  </div>
73
73
  {/if}
@@ -60,7 +60,7 @@
60
60
  <Button
61
61
  variant="link"
62
62
  size="sm"
63
- class="!text-red-600 dark:!text-red-500 hover:!text-red-900 dark:hover:!text-red-400"
63
+ class="text-red-600! dark:text-red-500! hover:text-red-900! dark:hover:text-red-400!"
64
64
  onclick={() => scrollToField(field.elementId)}
65
65
  >
66
66
  {field.label}
@@ -22,17 +22,17 @@
22
22
 
23
23
  <div class="flex flex-col lg:flex-row gap-6 {className}">
24
24
  {#if sidebarPosition === 'left'}
25
- <aside class="w-full {widthClasses[sidebarWidth]} flex-shrink-0">
25
+ <aside class="w-full {widthClasses[sidebarWidth]} shrink-0">
26
26
  {@render sidebar?.()}
27
27
  </aside>
28
- <main class="w-full {mainWidthClasses[sidebarWidth]} flex-grow min-w-0">
28
+ <main class="w-full {mainWidthClasses[sidebarWidth]} grow min-w-0">
29
29
  {@render children?.()}
30
30
  </main>
31
31
  {:else}
32
- <main class="w-full {mainWidthClasses[sidebarWidth]} flex-grow min-w-0">
32
+ <main class="w-full {mainWidthClasses[sidebarWidth]} grow min-w-0">
33
33
  {@render children?.()}
34
34
  </main>
35
- <aside class="w-full {widthClasses[sidebarWidth]} flex-shrink-0">
35
+ <aside class="w-full {widthClasses[sidebarWidth]} shrink-0">
36
36
  {@render sidebar?.()}
37
37
  </aside>
38
38
  {/if}
@@ -217,7 +217,7 @@
217
217
  transition:fly={{ y: 300, duration: 300, easing: cubicOut }}
218
218
  >
219
219
  <div class="flex justify-center pt-3 pb-2">
220
- <div class="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-sm"></div>
220
+ <div class="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-xs"></div>
221
221
  </div>
222
222
 
223
223
  <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
@@ -21,7 +21,7 @@
21
21
  <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
22
22
  <Breadcrumb data={breadcrumb} {title} {subtitle} />
23
23
  {#if actions}
24
- <div class="flex items-center gap-3 flex-shrink-0">
24
+ <div class="flex items-center gap-3 shrink-0">
25
25
  {@render actions()}
26
26
  </div>
27
27
  {/if}
@@ -233,10 +233,10 @@ describe('AccordionItem - Accessibility', () => {
233
233
  });
234
234
  });
235
235
 
236
- test('button has focus:outline-none for custom focus styling', () => {
236
+ test('button has focus:outline-hidden for custom focus styling', () => {
237
237
  const { container } = render(AccordionItemWrapper);
238
238
  const button = container.querySelector('button');
239
- expect(button).toHaveClass('focus:outline-none');
239
+ expect(button).toHaveClass('focus:outline-hidden');
240
240
  });
241
241
 
242
242
  test('button is keyboard accessible', async () => {
@@ -61,7 +61,7 @@
61
61
  typography.smMuted,
62
62
  "bg-gray-100 dark:bg-gray-800",
63
63
  "hover:bg-gray-200 dark:hover:bg-gray-700",
64
- "focus:outline-none",
64
+ "focus:outline-hidden",
65
65
  "gap-3",
66
66
  isOpen ? "bg-gray-100 dark:bg-gray-800" : "",
67
67
  ].join(" "));
@@ -92,7 +92,7 @@ let variantClasses = $derived(
92
92
  onclick={(e) => onclick?.(e)}
93
93
  >
94
94
  {#if showDot}
95
- <span class="w-1.5 h-1.5 rounded-full bg-current flex-shrink-0"></span>
95
+ <span class="w-1.5 h-1.5 rounded-full bg-current shrink-0"></span>
96
96
  {/if}
97
97
  {@render leftIcon?.()}
98
98
  {@render children?.()}
@@ -45,11 +45,14 @@
45
45
  {#if index > 0}
46
46
  <ChevronRightOutline class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1" />
47
47
  {/if}
48
- {#if index === data.length - 1}
49
- <span class={`ms-1 ${typography.smMuted} font-medium md:ms-2 max-w-48 truncate`} title={crumb.name}>
48
+ {#if index === 0 && showHomeIcon && data.length === 1}
49
+ <!-- Single item with home icon - show as non-clickable label -->
50
+ <span class="{typography.smMuted} inline-flex items-center font-medium">
51
+ <HomeSolid class="w-3 h-3 me-2.5" />
50
52
  {crumb.name}
51
53
  </span>
52
54
  {:else if index === 0 && showHomeIcon}
55
+ <!-- First item with home icon - clickable link -->
53
56
  <a
54
57
  href={crumb.href}
55
58
  onclick={() => handleClick(crumb)}
@@ -58,7 +61,13 @@
58
61
  <HomeSolid class="w-3 h-3 me-2.5" />
59
62
  {crumb.name}
60
63
  </a>
64
+ {:else if index === data.length - 1}
65
+ <!-- Last item - non-clickable -->
66
+ <span class={`ms-1 ${typography.smMuted} font-medium md:ms-2 max-w-48 truncate`} title={crumb.name}>
67
+ {crumb.name}
68
+ </span>
61
69
  {:else}
70
+ <!-- Middle items - clickable links -->
62
71
  <a
63
72
  href={crumb.href}
64
73
  onclick={() => handleClick(crumb)}
@@ -1 +1 @@
1
- {"version":3,"file":"Breadcrumb.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Breadcrumb/Breadcrumb.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,KAAK;IACb,6BAA6B;IAC7B,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;IACxB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAkEH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"Breadcrumb.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Breadcrumb/Breadcrumb.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,KAAK;IACb,6BAA6B;IAC7B,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;IACxB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AA2EH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -118,8 +118,8 @@
118
118
  // Chart row - for leaderboard/chart list items with progress bars
119
119
  "chart-row": "w-full text-left text-gray-900 bg-transparent border-transparent hover:bg-gray-50 dark:text-white dark:hover:bg-gray-800",
120
120
  // Landing page hero buttons - prominent CTAs with shadow
121
- landing: "text-white bg-blue-600 border border-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:border-blue-600 dark:hover:bg-blue-700 no-underline hover:no-underline shadow-sm hover:shadow-md",
122
- "landing-secondary": "text-gray-700 bg-white border border-gray-200 hover:border-gray-400 hover:text-gray-900 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:text-gray-100 no-underline hover:no-underline shadow-sm hover:shadow-md",
121
+ landing: "text-white bg-blue-600 border border-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:border-blue-600 dark:hover:bg-blue-700 no-underline hover:no-underline shadow hover:shadow-md",
122
+ "landing-secondary": "text-gray-700 bg-white border border-gray-200 hover:border-gray-400 hover:text-gray-900 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:text-gray-100 no-underline hover:no-underline shadow hover:shadow-md",
123
123
  };
124
124
 
125
125
  // Active state classes for ghost, toggle, menu-item, and nav
@@ -188,7 +188,7 @@ let sizeClass = $derived((() => {
188
188
  : "inline-flex items-center justify-center",
189
189
  roundedClass,
190
190
  "font-medium leading-none",
191
- "focus:outline-none",
191
+ "focus:outline-hidden",
192
192
  // Apple-style micro-interactions (only when not disabled)
193
193
  "transition-all duration-150 ease-out",
194
194
  effectiveDisabled ? "" : "active:scale-[0.97] active:opacity-90",
@@ -21,7 +21,7 @@
21
21
  </script>
22
22
 
23
23
  <div
24
- class="inline-flex rounded-lg shadow-sm {className}"
24
+ class="inline-flex rounded-lg shadow {className}"
25
25
  role="group"
26
26
  >
27
27
  {#if children}
@@ -140,10 +140,10 @@ describe('Checkbox Input Styling', () => {
140
140
  expect(input).toHaveClass('dark:border-gray-600');
141
141
  });
142
142
 
143
- test('has focus:outline-none', () => {
143
+ test('has focus:outline-hidden', () => {
144
144
  const { container } = render(Checkbox);
145
145
  const input = container.querySelector('input');
146
- expect(input).toHaveClass('focus:outline-none');
146
+ expect(input).toHaveClass('focus:outline-hidden');
147
147
  });
148
148
 
149
149
  test('has cursor-pointer when not disabled', () => {
@@ -56,7 +56,7 @@
56
56
  "w-4 h-4 rounded",
57
57
  "bg-gray-100 border-gray-300",
58
58
  "dark:bg-gray-700 dark:border-gray-600",
59
- "focus:outline-none",
59
+ "focus:outline-hidden",
60
60
  colorClass,
61
61
  disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
62
62
  ].join(" "));
@@ -112,7 +112,7 @@ describe("DarkModeToggle Component Tests", () => {
112
112
  test("Button has focus outline removed", () => {
113
113
  setupTest();
114
114
  const button = screen.getByRole("button");
115
- expect(button).toHaveClass("focus:outline-none");
115
+ expect(button).toHaveClass("focus:outline-hidden");
116
116
  });
117
117
 
118
118
  test("Button click handler is called on interaction", async () => {
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount, onDestroy, tick, setContext } from "svelte";
3
3
  import type { Snippet } from "svelte";
4
+ import { bloom } from "../../utils/transitions.js";
4
5
 
5
6
  interface Props {
6
7
  open?: boolean;
@@ -143,6 +144,14 @@
143
144
  left: "right-full top-0 mr-1",
144
145
  right: "left-full top-0 ml-1"
145
146
  };
147
+
148
+ // Map placement to transform origin for bloom animation
149
+ const placementOrigins: Record<string, string> = {
150
+ bottom: "top left",
151
+ top: "bottom left",
152
+ left: "top right",
153
+ right: "top left"
154
+ };
146
155
  </script>
147
156
 
148
157
  {#if open}
@@ -151,6 +160,7 @@
151
160
  class="absolute z-10 bg-white divide-y divide-gray-100 rounded-lg shadow-lg w-44 dark:bg-gray-700 dark:divide-gray-600 {className}"
152
161
  role="menu"
153
162
  aria-label={ariaLabel}
163
+ transition:bloom={{ origin: placementOrigins[placement] || "top left" }}
154
164
  {...restProps}
155
165
  >
156
166
  <ul bind:this={dropdownRef} class="py-2 text-sm text-gray-700 dark:text-gray-200">
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGpC,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAoJH,QAAA,MAAM,QAAQ,+CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA6JH,QAAA,MAAM,QAAQ,+CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -293,7 +293,7 @@
293
293
  onfocus={handleFocus}
294
294
  {maxlength}
295
295
  {minlength}
296
- class="{typography.sm} w-full px-3 py-2 bg-gray-50 dark:bg-gray-800 border rounded-lg font-medium placeholder-gray-500 dark:placeholder-gray-400 transition-all focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 resize-y {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {getContentFloatClass()} {getTextareaSizeClass()} {shouldAnimate ? 'focus:scale-[1.01]' : ''}"
296
+ class="{typography.sm} w-full px-3 py-2 bg-gray-50 dark:bg-gray-800 border rounded-lg font-medium placeholder-gray-500 dark:placeholder-gray-400 transition-all focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-300 resize-y {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {getContentFloatClass()} {getTextareaSizeClass()} {shouldAnimate ? 'focus:scale-[1.01]' : ''}"
297
297
  required={false}
298
298
  {disabled}
299
299
  {readonly}
@@ -301,7 +301,7 @@
301
301
  aria-required={required}
302
302
  ></textarea>
303
303
  {:else if type === "password" && showPasswordToggle}
304
- <div class="flex items-center w-full bg-gray-50 dark:bg-gray-800 border rounded-lg transition-all outline-none focus-within:ring-4 focus-within:ring-blue-300 dark:focus-within:ring-blue-800 {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus-within:border-blue-500'} {shouldAnimate ? 'focus-within:scale-[1.01]' : ''}">
304
+ <div class="relative flex items-center w-full">
305
305
  <!-- svelte-ignore a11y_autofocus -->
306
306
  <input
307
307
  bind:this={inputElement}
@@ -316,7 +316,7 @@
316
316
  onfocus={handleFocus}
317
317
  {maxlength}
318
318
  {minlength}
319
- class="password-input-inner {typography.sm} flex-1 w-full {sizeClass} bg-transparent border-0 font-medium placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-0 {getContentFloatClass()}"
319
+ class="{typography.body} w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg font-medium placeholder-gray-500 dark:placeholder-gray-400 transition-all focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-300 {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} pr-10 {getContentFloatClass()} {shouldAnimate ? 'focus:scale-[1.01]' : ''} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
320
320
  required={false}
321
321
  {disabled}
322
322
  {readonly}
@@ -328,7 +328,7 @@
328
328
  <button
329
329
  type="button"
330
330
  onclick={togglePasswordVisibility}
331
- class="flex items-center justify-center px-3 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none"
331
+ class="absolute right-3 inset-y-0 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-hidden"
332
332
  tabindex="-1"
333
333
  aria-label={isPasswordVisible ? "Hide password" : "Show password"}
334
334
  >
@@ -361,7 +361,7 @@
361
361
  onkeydown={handleSearchKeyDown}
362
362
  {maxlength}
363
363
  {minlength}
364
- class="{typography.body} w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg font-medium placeholder-gray-500 dark:placeholder-gray-400 transition-all focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {icon || (showClearButton && inputValue) ? 'pr-10' : ''} {leftIcon ? 'pl-10' : ''} {getContentFloatClass()} {shouldAnimate ? 'focus:scale-[1.01]' : ''} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
364
+ class="{typography.body} w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg font-medium placeholder-gray-500 dark:placeholder-gray-400 transition-all focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-300 {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {icon || (showClearButton && inputValue) ? 'pr-10' : ''} {leftIcon ? 'pl-10' : ''} {getContentFloatClass()} {shouldAnimate ? 'focus:scale-[1.01]' : ''} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
365
365
  required={false}
366
366
  {disabled}
367
367
  {readonly}
@@ -374,7 +374,7 @@
374
374
  <button
375
375
  type="button"
376
376
  onclick={clearInput}
377
- class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
377
+ class="absolute right-3 inset-y-0 flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
378
378
  aria-label="Clear input"
379
379
  tabindex="-1"
380
380
  >
@@ -419,15 +419,5 @@
419
419
  </div>
420
420
 
421
421
  <style>
422
- /*
423
- * Override Tailwind v4 preflight which sets input { border: 1px solid currentColor }
424
- * in @layer base. The base layer's element selector has higher specificity than
425
- * utility class selectors like border-0 or border-none, so we need scoped overrides.
426
- *
427
- * This affects the password input which is wrapped in a container div that provides
428
- * the visible border - the inner input should have no border.
429
- */
430
- :global(.password-input-inner) {
431
- border: none !important;
432
- }
422
+ /* No custom styles needed - password input now uses same structure as email input */
433
423
  </style>
@@ -3,6 +3,12 @@ import userEvent from "@testing-library/user-event";
3
3
  import { expect, describe, test, vi } from "vitest";
4
4
  import Select from "./Select.svelte";
5
5
 
6
+ // Mock transitions to be instant for testing
7
+ vi.mock('../../utils/transitions.js', () => ({
8
+ bloom: () => ({ duration: 0, delay: 0, css: () => '' }),
9
+ safeSlide: () => ({ duration: 0, delay: 0, css: () => '' }),
10
+ }));
11
+
6
12
  const sampleItems = [
7
13
  { name: "Option 1", value: "opt1" },
8
14
  { name: "Option 2", value: "opt2" },
@@ -4,6 +4,7 @@
4
4
  import { portal as portalAction } from "../../utils/portal.js";
5
5
  import { typography } from "../../tokens/typography";
6
6
  import { formControlSizes } from "../../tokens/sizing";
7
+ import { bloom } from "../../utils/transitions.js";
7
8
 
8
9
  interface SelectItem {
9
10
  value: string;
@@ -180,7 +181,7 @@
180
181
  bind:this={triggerElement}
181
182
  {id}
182
183
  {name}
183
- class="flex items-center justify-between w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg cursor-pointer transition-colors text-left focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 {error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-500'} {disabled ? 'opacity-50 cursor-not-allowed' : ''} {!selectedItem ? `${typography.textMuted}` : `${typography.body}`}"
184
+ class="flex items-center justify-between w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg cursor-pointer transition-colors text-left focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 {error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-500'} {disabled ? 'opacity-50 cursor-not-allowed' : ''} {!selectedItem ? `${typography.textMuted}` : `${typography.body}`}"
184
185
  {disabled}
185
186
  aria-haspopup="listbox"
186
187
  aria-expanded={isOpen}
@@ -198,6 +199,7 @@
198
199
  class="absolute top-full left-0 right-0 z-50 mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto py-1"
199
200
  role="listbox"
200
201
  tabindex="-1"
202
+ transition:bloom={{ origin: "top left" }}
201
203
  >
202
204
  {#each items as item, index}
203
205
  <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -231,6 +233,7 @@
231
233
  style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px; width: {dropdownPosition.width}px;"
232
234
  role="listbox"
233
235
  tabindex="-1"
236
+ transition:bloom={{ origin: "top left" }}
234
237
  >
235
238
  {#each items as item, index}
236
239
  <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -1 +1 @@
1
- {"version":3,"file":"Select.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Input/Select.svelte.ts"],"names":[],"mappings":"AAUE,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,KAAK,IAAI,CAAC;IACjE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqMH,QAAA,MAAM,MAAM,gDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Select.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Input/Select.svelte.ts"],"names":[],"mappings":"AAWE,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,KAAK,IAAI,CAAC;IACjE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAsMH,QAAA,MAAM,MAAM,gDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
@@ -85,7 +85,7 @@
85
85
  {readonly}
86
86
  {maxlength}
87
87
  {minlength}
88
- class="{typography.sm} w-full p-2.5 bg-gray-50 dark:bg-gray-800 leading-normal border rounded-lg resize-y transition-colors focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 placeholder-gray-500 dark:placeholder-gray-400 {error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {disabled ? 'opacity-50 cursor-not-allowed' : ''} {className}"
88
+ class="{typography.sm} w-full p-2.5 bg-gray-50 dark:bg-gray-800 leading-normal border rounded-lg resize-y transition-colors focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 placeholder-gray-500 dark:placeholder-gray-400 {error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 focus:border-blue-500'} {disabled ? 'opacity-50 cursor-not-allowed' : ''} {className}"
89
89
  bind:value
90
90
  oninput={handleInput}
91
91
  onchange={handleChange}
@@ -127,7 +127,7 @@
127
127
  >
128
128
  <!-- Handle bar -->
129
129
  <div class="flex justify-center pt-3 pb-1 shrink-0">
130
- <div class="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-sm"></div>
130
+ <div class="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-xs"></div>
131
131
  </div>
132
132
 
133
133
  <div class="p-6 pb-[calc(5rem+env(safe-area-inset-bottom,0px))] overflow-y-auto flex-1">
@@ -134,10 +134,10 @@ describe('Radio Input Styling', () => {
134
134
  expect(input).toHaveClass('dark:border-gray-600');
135
135
  });
136
136
 
137
- test('has focus:outline-none', () => {
137
+ test('has focus:outline-hidden', () => {
138
138
  const { container } = render(Radio);
139
139
  const input = container.querySelector('input');
140
- expect(input).toHaveClass('focus:outline-none');
140
+ expect(input).toHaveClass('focus:outline-hidden');
141
141
  });
142
142
  });
143
143
 
@@ -57,7 +57,7 @@
57
57
  {disabled}
58
58
  checked={isChecked}
59
59
  onchange={handleChange}
60
- class="w-4 h-4 bg-gray-100 border-gray-300 focus:outline-none dark:bg-gray-700 dark:border-gray-600 {colorClass}"
60
+ class="w-4 h-4 bg-gray-100 border-gray-300 focus:outline-hidden dark:bg-gray-700 dark:border-gray-600 {colorClass}"
61
61
  />
62
62
  {#if children}
63
63
  <span class="text-sm font-medium text-gray-900 dark:text-gray-300">
@@ -91,7 +91,7 @@
91
91
 
92
92
  // Tab button classes by style
93
93
  function getTabClasses(isActive: boolean) {
94
- const base = "inline-flex items-center justify-center whitespace-nowrap transition-colors focus:outline-none";
94
+ const base = "inline-flex items-center justify-center whitespace-nowrap transition-colors focus:outline-hidden";
95
95
 
96
96
  switch (tabStyle) {
97
97
  case 'underline':
@@ -97,22 +97,25 @@ describe('Toggle Component', () => {
97
97
  it('has default md size track dimensions', () => {
98
98
  const { container } = render(Toggle, { size: 'md' });
99
99
  const switchEl = container.querySelector('[role="switch"]');
100
- expect(switchEl).toHaveClass('w-11');
101
- expect(switchEl).toHaveClass('h-6');
100
+ // Uses CSS classes instead of Tailwind for Tailwind v4 compatibility
101
+ expect(switchEl).toHaveClass('toggle-track');
102
+ expect(switchEl).toHaveClass('toggle-md');
102
103
  });
103
104
 
104
105
  it('has sm size track dimensions', () => {
105
106
  const { container } = render(Toggle, { size: 'sm' });
106
107
  const switchEl = container.querySelector('[role="switch"]');
107
- expect(switchEl).toHaveClass('w-9');
108
- expect(switchEl).toHaveClass('h-5');
108
+ // Uses CSS classes instead of Tailwind for Tailwind v4 compatibility
109
+ expect(switchEl).toHaveClass('toggle-track');
110
+ expect(switchEl).toHaveClass('toggle-sm');
109
111
  });
110
112
 
111
113
  it('has lg size track dimensions', () => {
112
114
  const { container } = render(Toggle, { size: 'lg' });
113
115
  const switchEl = container.querySelector('[role="switch"]');
114
- expect(switchEl).toHaveClass('w-14');
115
- expect(switchEl).toHaveClass('h-7');
116
+ // Uses CSS classes instead of Tailwind for Tailwind v4 compatibility
117
+ expect(switchEl).toHaveClass('toggle-track');
118
+ expect(switchEl).toHaveClass('toggle-lg');
116
119
  });
117
120
 
118
121
  it('has dark mode classes', () => {
@@ -2,6 +2,10 @@
2
2
  /**
3
3
  * Toggle Component - Flowbite Native
4
4
  * Migrated to Svelte 5 runes
5
+ *
6
+ * Note: Uses CSS style block for pseudo-element styling instead of Tailwind
7
+ * after: classes, because Tailwind v4 doesn't generate after: classes from
8
+ * node_modules when this component is consumed by other apps.
5
9
  */
6
10
 
7
11
  /** @type {{
@@ -26,27 +30,6 @@
26
30
  checked = event.target.checked;
27
31
  onchange?.({ checked });
28
32
  }
29
-
30
- // Flowbite toggle sizes - thumb uses after: pseudo-element
31
- const sizes = {
32
- sm: {
33
- track: 'w-9 h-5',
34
- thumb: 'after:h-4 after:w-4 after:top-0.5 after:start-0.5',
35
- translate: 'peer-checked:after:translate-x-full'
36
- },
37
- md: {
38
- track: 'w-11 h-6',
39
- thumb: 'after:h-5 after:w-5 after:top-0.5 after:start-0.5',
40
- translate: 'peer-checked:after:translate-x-full'
41
- },
42
- lg: {
43
- track: 'w-14 h-7',
44
- thumb: 'after:h-6 after:w-6 after:top-0.5 after:start-0.5',
45
- translate: 'peer-checked:after:translate-x-full'
46
- }
47
- };
48
-
49
- let sizeConfig = $derived(sizes[size] || sizes.md);
50
33
  </script>
51
34
 
52
35
  <label class="inline-flex items-center {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} {className}" {...restProps}>
@@ -58,9 +41,10 @@
58
41
  class="sr-only peer"
59
42
  />
60
43
  <div
61
- class="relative {sizeConfig.track} bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute {sizeConfig.thumb} after:bg-white after:border-gray-300 after:border after:rounded-full after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
44
+ class="toggle-track toggle-{size} relative bg-gray-200 peer-focus:outline-hidden peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:bg-blue-600"
62
45
  role="switch"
63
46
  aria-checked={checked}
47
+ style="--toggle-translate: {sizeConfig.translatePx}"
64
48
  ></div>
65
49
  {#if children}
66
50
  <span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">
@@ -68,3 +52,81 @@
68
52
  </span>
69
53
  {/if}
70
54
  </label>
55
+
56
+ <style>
57
+ /*
58
+ * Toggle track sizes (replaces Tailwind w-* h-* classes)
59
+ * Using CSS instead of Tailwind because these need to work in consuming apps
60
+ */
61
+ .toggle-sm {
62
+ width: 2.25rem; /* w-9 = 36px */
63
+ height: 1.25rem; /* h-5 = 20px */
64
+ }
65
+ .toggle-md {
66
+ width: 2.75rem; /* w-11 = 44px */
67
+ height: 1.5rem; /* h-6 = 24px */
68
+ }
69
+ .toggle-lg {
70
+ width: 3.5rem; /* w-14 = 56px */
71
+ height: 1.75rem; /* h-7 = 28px */
72
+ }
73
+
74
+ /*
75
+ * Toggle thumb (the white circle) - uses ::after pseudo-element
76
+ * Tailwind v4 doesn't generate after: classes from node_modules,
77
+ * so we use explicit CSS here.
78
+ */
79
+ .toggle-track::after {
80
+ content: '';
81
+ position: absolute;
82
+ background-color: white;
83
+ border: 1px solid #d1d5db; /* gray-300 */
84
+ border-radius: 9999px;
85
+ transition: all 150ms;
86
+ }
87
+
88
+ /* Thumb sizes for each toggle size */
89
+ .toggle-sm::after {
90
+ width: 1rem; /* 16px */
91
+ height: 1rem;
92
+ top: 0.125rem; /* 2px */
93
+ inset-inline-start: 0.125rem;
94
+ }
95
+ .toggle-md::after {
96
+ width: 1.25rem; /* 20px */
97
+ height: 1.25rem;
98
+ top: 0.125rem;
99
+ inset-inline-start: 0.125rem;
100
+ }
101
+ .toggle-lg::after {
102
+ width: 1.5rem; /* 24px */
103
+ height: 1.5rem;
104
+ top: 0.125rem;
105
+ inset-inline-start: 0.125rem;
106
+ }
107
+
108
+ /* Checked state - move thumb to the right */
109
+ :global(input.peer:checked) + .toggle-sm::after {
110
+ transform: translateX(1rem); /* 16px */
111
+ border-color: white;
112
+ }
113
+ :global(input.peer:checked) + .toggle-md::after {
114
+ transform: translateX(1.25rem); /* 20px */
115
+ border-color: white;
116
+ }
117
+ :global(input.peer:checked) + .toggle-lg::after {
118
+ transform: translateX(1.5rem); /* 24px */
119
+ border-color: white;
120
+ }
121
+
122
+ /* RTL support - translate in opposite direction */
123
+ :global([dir="rtl"]) :global(input.peer:checked) + .toggle-sm::after {
124
+ transform: translateX(-1rem);
125
+ }
126
+ :global([dir="rtl"]) :global(input.peer:checked) + .toggle-md::after {
127
+ transform: translateX(-1.25rem);
128
+ }
129
+ :global([dir="rtl"]) :global(input.peer:checked) + .toggle-lg::after {
130
+ transform: translateX(-1.5rem);
131
+ }
132
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"Toggle.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/primitives/Toggle.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;AAyEA;cAPc,OAAO;eACN,OAAO;WACX,IAAI,GAAG,IAAI,GAAG,IAAI;YACjB,MAAM;eACH,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI;eACtC,OAAO,QAAQ,EAAE,OAAO;kBAEc"}
1
+ {"version":3,"file":"Toggle.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/primitives/Toggle.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;AA0DA;cAPc,OAAO;eACN,OAAO;WACX,IAAI,GAAG,IAAI,GAAG,IAAI;YACjB,MAAM;eACH,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI;eACtC,OAAO,QAAQ,EAAE,OAAO;kBAEc"}
@@ -72,7 +72,7 @@
72
72
 
73
73
  {#if visible && content}
74
74
  <div
75
- class="absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded-lg shadow-sm whitespace-nowrap pointer-events-none dark:bg-gray-700 {placementClasses[placement]} {className}"
75
+ class="absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded-lg shadow whitespace-nowrap pointer-events-none dark:bg-gray-700 {placementClasses[placement]} {className}"
76
76
  transition:fly={{ y: placement === 'top' ? 5 : placement === 'bottom' ? -5 : 0, x: placement === 'left' ? 5 : placement === 'right' ? -5 : 0, duration: 150 }}
77
77
  >
78
78
  {content}
@@ -159,7 +159,7 @@
159
159
  <Button
160
160
  variant="outline"
161
161
  size="sm"
162
- class="!rounded-full !w-9 !h-9 !p-0 shrink-0"
162
+ class="rounded-full! w-9! h-9! p-0! shrink-0"
163
163
  onclick={() => (zoom = Math.max(1, zoom - 0.2))}
164
164
  aria-label="Zoom out"
165
165
  >
@@ -179,7 +179,7 @@
179
179
  <Button
180
180
  variant="outline"
181
181
  size="sm"
182
- class="!rounded-full !w-9 !h-9 !p-0 shrink-0"
182
+ class="rounded-full! w-9! h-9! p-0! shrink-0"
183
183
  onclick={() => (zoom = Math.min(3, zoom + 0.2))}
184
184
  aria-label="Zoom in"
185
185
  >
@@ -802,7 +802,7 @@
802
802
  onclick={() => handleAccountSelectInternal(account)}
803
803
  >
804
804
  <div
805
- class="h-10 w-10 rounded-md bg-gray-200 flex-shrink-0 overflow-hidden mr-4"
805
+ class="h-10 w-10 rounded-md bg-gray-200 shrink-0 overflow-hidden mr-4"
806
806
  >
807
807
  {#if account.performerProfile?.profileImage}
808
808
  <img
@@ -3,6 +3,12 @@ import userEvent from "@testing-library/user-event";
3
3
  import { expect, describe, test, vi } from "vitest";
4
4
  import MultiSelect from "./MultiSelect.svelte";
5
5
 
6
+ // Mock transitions to be instant for testing
7
+ vi.mock('../../utils/transitions.js', () => ({
8
+ bloom: () => ({ duration: 0, delay: 0, css: () => '' }),
9
+ safeSlide: () => ({ duration: 0, delay: 0, css: () => '' }),
10
+ }));
11
+
6
12
  const sampleItems = [
7
13
  { name: "Option 1", value: "opt1" },
8
14
  { name: "Option 2", value: "opt2" },
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import { CloseOutline, ChevronDownOutline } from "../../primitives/Icons";
3
3
  import { typography } from '../../tokens/typography';
4
+ import { bloom } from "../../utils/transitions.js";
4
5
 
5
6
 
6
7
  /**
@@ -183,7 +184,7 @@
183
184
  <div
184
185
  bind:this={triggerElement}
185
186
  {id}
186
- class="flex items-center justify-between gap-2 w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg cursor-pointer text-left transition-all focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800
187
+ class="flex items-center justify-between gap-2 w-full {sizeClass} bg-gray-50 dark:bg-gray-800 border rounded-lg cursor-pointer text-left transition-all focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800
187
188
  {error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-500 focus:border-blue-500 dark:focus:border-blue-500'}
188
189
  {disabled ? 'opacity-50 cursor-not-allowed' : ''}
189
190
  {animateFocus ? 'multiselect-animate-focus' : ''}"
@@ -206,7 +207,7 @@
206
207
  <span class={`${typography.sm} overflow-hidden text-ellipsis whitespace-nowrap`} style="color-scheme: light dark;">{item.name}</span>
207
208
  <button
208
209
  type="button"
209
- class="inline-flex items-center p-0.5 ms-1.5 text-sm text-blue-400 bg-transparent rounded-sm hover:bg-blue-200 hover:text-blue-900 dark:hover:bg-blue-800 dark:hover:text-blue-300"
210
+ class="inline-flex items-center p-0.5 ms-1.5 text-sm text-blue-400 bg-transparent rounded-xs hover:bg-blue-200 hover:text-blue-900 dark:hover:bg-blue-800 dark:hover:text-blue-300"
210
211
  onclick={(e) => removeItem(item, e)}
211
212
  aria-label="Remove {item.name}"
212
213
  >
@@ -220,7 +221,7 @@
220
221
  {/if}
221
222
  </div>
222
223
 
223
- <div class="flex items-center gap-1 flex-shrink-0">
224
+ <div class="flex items-center gap-1 shrink-0">
224
225
  {#if hasSelection && !hideClear}
225
226
  <button
226
227
  type="button"
@@ -244,6 +245,7 @@
244
245
  role="listbox"
245
246
  tabindex="-1"
246
247
  aria-multiselectable="true"
248
+ transition:bloom={{ origin: "top left" }}
247
249
  aria-activedescendant={focusedIndex >= 0
248
250
  ? `${id || name}-option-${focusedIndex}`
249
251
  : undefined}
@@ -1 +1 @@
1
- {"version":3,"file":"MultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/inputs/MultiSelect.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsQA;YAhBY,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE;YACnB;;;;cA1OI,MAAM;;;;eACN,MAAM,GAAG,MAAM;OAyOP;kBACN,MAAM;YACZ,MAAM;eACH,OAAO;eACP,OAAO;YACV,MAAM;gBACF,OAAO;qBACF,OAAO;mBACT,OAAO;WACf,MAAM;SACR,MAAM;WACJ,IAAI,GAAG,IAAI,GAAG,IAAI;mBACV,OAAO;eACX,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;QAAC,KAAK,EAAE;;;;kBAvP7C,MAAM;;;;mBACN,MAAM,GAAG,MAAM;WAsP0C,CAAA;KAAE,KAAK,IAAI;gBAE1B;;;;;UAzP1C,MAAM;;;;WACN,MAAM,GAAG,MAAM"}
1
+ {"version":3,"file":"MultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/inputs/MultiSelect.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwQA;YAhBY,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE;YACnB;;;;cA1OI,MAAM;;;;eACN,MAAM,GAAG,MAAM;OAyOP;kBACN,MAAM;YACZ,MAAM;eACH,OAAO;eACP,OAAO;YACV,MAAM;gBACF,OAAO;qBACF,OAAO;mBACT,OAAO;WACf,MAAM;SACR,MAAM;WACJ,IAAI,GAAG,IAAI,GAAG,IAAI;mBACV,OAAO;eACX,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;QAAC,KAAK,EAAE;;;;kBAvP7C,MAAM;;;;mBACN,MAAM,GAAG,MAAM;WAsP0C,CAAA;KAAE,KAAK,IAAI;gBAE1B;;;;;UAzP1C,MAAM;;;;WACN,MAAM,GAAG,MAAM"}
@@ -108,7 +108,7 @@
108
108
  {disabled}
109
109
  aria-label="Digit {index + 1} of {length}"
110
110
  aria-describedby="otp-instructions"
111
- class="h-12 w-12 border-2 border-gray-300 bg-gray-50 p-0 text-center text-xl font-semibold leading-none text-gray-900 transition-colors focus:border-blue-500 focus:outline-none focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500 dark:disabled:bg-gray-600 md:h-14 md:w-14 md:text-2xl rounded-lg"
111
+ class="h-12 w-12 border-2 border-gray-300 bg-gray-50 p-0 text-center text-xl font-semibold leading-none text-gray-900 transition-colors focus:border-blue-500 focus:outline-hidden focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500 dark:disabled:bg-gray-600 md:h-14 md:w-14 md:text-2xl rounded-lg"
112
112
  oninput={(e) => handleInput(index, e)}
113
113
  onkeydown={(e) => handleKeyDown(index, e)}
114
114
  onpaste={handlePaste}
@@ -32,6 +32,12 @@ vi.mock('$lib/config.js', () => ({
32
32
  PUBLIC_GOOGLE_MAPS_API_KEY: 'test-api-key',
33
33
  }));
34
34
 
35
+ // Mock transitions to be instant for testing
36
+ vi.mock('../../../utils/transitions.js', () => ({
37
+ bloom: () => ({ duration: 0, delay: 0, css: () => '' }),
38
+ safeSlide: () => ({ duration: 0, delay: 0, css: () => '' }),
39
+ }));
40
+
35
41
  function setupTest(props = {}) {
36
42
  const user = userEvent.setup();
37
43
  const result = render(PlaceAutocomplete, { props });
@@ -3,6 +3,7 @@
3
3
  import * as GMaps from '@googlemaps/js-api-loader';
4
4
  import { PUBLIC_GOOGLE_MAPS_API_KEY } from '../../../config.js';
5
5
  import { typography } from '../../../tokens/typography';
6
+ import { bloom } from '../../../utils/transitions.js';
6
7
  const { Loader } = GMaps;
7
8
  /** Google Places API address component */ interface AddressComponent { longText: string; shortText: string; types: string[]; } /** Google Places API response data */ interface PlaceData { formattedAddress?: string; addressComponents?: AddressComponent[]; text?: string; [key: string]: unknown; } /** Google Places autocomplete suggestion */ interface AutocompleteSuggestion { placePrediction: { types: string[]; text: { toString(): string }; toPlace(): PlaceObject; }; } /** Google Places place object */ interface PlaceObject { fetchFields(options: { fields: string[] }): Promise<void>; toJSON(): PlaceData; } /** Google Places API interface */ interface PlacesApiInterface { AutocompleteSessionToken: new () => AutocompleteSessionToken; AutocompleteSuggestion: { fetchAutocompleteSuggestions(request: AutocompleteRequest): Promise<{ suggestions: AutocompleteSuggestion[] }>; }; } /** Google Places autocomplete session token */ interface AutocompleteSessionToken {} /** Autocomplete request parameters */ interface AutocompleteRequest { input: string; language: string; region: string; sessionToken: AutocompleteSessionToken | string; } /** Search result item */ interface SearchResult { to_place: PlaceObject; text: string; originalText?: string; }
8
9
 
@@ -291,7 +292,7 @@
291
292
  type="text"
292
293
  name="location"
293
294
  class="block w-full h-10 pl-10 pr-2.5 py-2.5 bg-gray-50 border border-gray-300 rounded-lg {typography.label}
294
- focus:ring-blue-500 focus:border-blue-500 focus:outline-none
295
+ focus:ring-blue-500 focus:border-blue-500 focus:outline-hidden
295
296
  hover:border-blue-500
296
297
  disabled:opacity-50 disabled:cursor-not-allowed
297
298
  placeholder:text-gray-500
@@ -318,6 +319,7 @@
318
319
  class="absolute top-full left-0 right-0 z-50 mt-1 py-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto
319
320
  dark:bg-gray-800 dark:border-gray-600"
320
321
  id="options"
322
+ transition:bloom={{ origin: "top left" }}
321
323
  >
322
324
  {#each results as place, i}
323
325
  <li
@@ -328,7 +330,7 @@
328
330
  <button
329
331
  type="button"
330
332
  class="block w-full text-left px-4 py-3 bg-transparent border-none cursor-pointer {typography.sm}
331
- focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
333
+ focus:outline-hidden focus:bg-gray-100 dark:focus:bg-gray-700"
332
334
  tabindex={i + 1}
333
335
  onclick={() => onPlaceSelected(place.to_place, place.text)}
334
336
  >
@@ -1 +1 @@
1
- {"version":3,"file":"PlaceAutocomplete.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte.ts"],"names":[],"mappings":"AAQA,0CAA0C,CAAE,UAAU,gBAAgB;IAAM,QAAQ,EAAE,MAAM,CAAC;IAAI,SAAS,EAAE,MAAM,CAAC;IAAI,KAAK,EAAE,MAAM,EAAE,CAAC;CAAG;AACzI,sCAAsC,CAAE,UAAU,SAAS;IAAM,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAI,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAAI,IAAI,CAAC,EAAE,MAAM,CAAC;IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAAG;AAMrL,UAAU,KAAK;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqTH,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"PlaceAutocomplete.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte.ts"],"names":[],"mappings":"AASA,0CAA0C,CAAE,UAAU,gBAAgB;IAAM,QAAQ,EAAE,MAAM,CAAC;IAAI,SAAS,EAAE,MAAM,CAAC;IAAI,KAAK,EAAE,MAAM,EAAE,CAAC;CAAG;AACzI,sCAAsC,CAAE,UAAU,SAAS;IAAM,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAI,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAAI,IAAI,CAAC,EAAE,MAAM,CAAC;IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAAG;AAMrL,UAAU,KAAK;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsTH,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -84,7 +84,7 @@
84
84
  {name}
85
85
  {placeholder}
86
86
  {disabled}
87
- class="w-full pr-3 bg-gray-50 dark:bg-gray-700 font-medium border border-gray-300 dark:border-gray-600 rounded-lg transition-colors focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 focus:border-blue-500 hover:border-blue-500 placeholder-gray-500 dark:placeholder-gray-400 [&::-webkit-search-cancel-button]:appearance-none {sizeConfig.input} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
87
+ class="w-full pr-3 bg-gray-50 dark:bg-gray-700 font-medium border border-gray-300 dark:border-gray-600 rounded-lg transition-colors focus:outline-hidden focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 focus:border-blue-500 hover:border-blue-500 placeholder-gray-500 dark:placeholder-gray-400 [&::-webkit-search-cancel-button]:appearance-none {sizeConfig.input} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
88
88
  bind:value
89
89
  oninput={handleInput}
90
90
  onchange={handleChange}
@@ -3,6 +3,12 @@ import userEvent from "@testing-library/user-event";
3
3
  import { expect, describe, test, vi, beforeEach } from "vitest";
4
4
  import SelectDropdown from "./SelectDropdown.svelte";
5
5
 
6
+ // Mock transitions to be instant for testing
7
+ vi.mock('../../utils/transitions.js', () => ({
8
+ bloom: () => ({ duration: 0, delay: 0, css: () => '' }),
9
+ safeSlide: () => ({ duration: 0, delay: 0, css: () => '' }),
10
+ }));
11
+
6
12
  const sampleOptions = [
7
13
  { label: "Option 1", value: "opt1" },
8
14
  { label: "Option 2", value: "opt2" },
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { tick } from "svelte";
3
3
  import { ChevronDownOutline } from "../../primitives/Icons";
4
+ import { bloom } from "../../utils/transitions.js";
4
5
 
5
6
  interface SelectOption {
6
7
  label: string;
@@ -129,7 +130,7 @@
129
130
  <button
130
131
  bind:this={triggerRef}
131
132
  type="button"
132
- class="inline-flex items-center justify-between w-full px-4 py-2.5 text-sm font-medium text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-blue-800 {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
133
+ class="inline-flex items-center justify-between w-full px-4 py-2.5 text-sm font-medium text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-hidden focus:ring-blue-300 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-blue-800 {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
133
134
  onclick={toggleDropdown}
134
135
  aria-haspopup="listbox"
135
136
  aria-expanded={isOpen}
@@ -148,6 +149,7 @@
148
149
  class="absolute z-10 mt-1 w-full bg-white divide-y divide-gray-100 rounded-lg shadow-lg dark:bg-gray-700 dark:divide-gray-600 max-h-60 overflow-y-auto"
149
150
  role="listbox"
150
151
  aria-label={placeholder}
152
+ transition:bloom={{ origin: "top left" }}
151
153
  >
152
154
  <ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
153
155
  {#each options as option}
@@ -1 +1 @@
1
- {"version":3,"file":"SelectDropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/inputs/SelectDropdown.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AA6IH,QAAA,MAAM,cAAc,mDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"SelectDropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/inputs/SelectDropdown.svelte.ts"],"names":[],"mappings":"AAQE,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AA8IH,QAAA,MAAM,cAAc,mDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -118,7 +118,7 @@
118
118
  bind:value={inputValue}
119
119
  placeholder={inputPlaceholder}
120
120
  rows={inputRows}
121
- class="w-full px-3 py-2 border {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-gray-50 dark:bg-gray-700 {typography.body} rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
121
+ class="w-full px-3 py-2 border {hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-gray-50 dark:bg-gray-700 {typography.body} rounded-lg focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
122
122
  disabled={disabled || loading}
123
123
  ></textarea>
124
124
  {:else}
@@ -34,32 +34,32 @@
34
34
  }: Props = $props();
35
35
  </script>
36
36
 
37
- <Modal bind:show oncancel={oncancel}>
38
- {#snippet headerSnippet()}
39
- {#if header}
40
- {@render header()}
41
- {:else}
42
- {title}
43
- {/if}
44
- {/snippet}
37
+ {#snippet headerContent()}
38
+ {#if header}
39
+ {@render header()}
40
+ {:else}
41
+ {title}
42
+ {/if}
43
+ {/snippet}
45
44
 
46
- {#snippet bodySnippet()}
47
- {#if body}
48
- {@render body()}
49
- {:else}
50
- {description}
51
- {#if warningText}
52
- <br /> {warningText}
53
- {/if}
45
+ {#snippet bodyContent()}
46
+ {#if body}
47
+ {@render body()}
48
+ {:else}
49
+ {description}
50
+ {#if warningText}
51
+ <br /> {warningText}
54
52
  {/if}
55
- {/snippet}
53
+ {/if}
54
+ {/snippet}
56
55
 
57
- {#snippet footerSnippet()}
58
- {#if footer}
59
- {@render footer()}
60
- {:else}
61
- <Button variant="alternative">Cancel</Button>
62
- <Button variant="default">Confirm</Button>
63
- {/if}
64
- {/snippet}
65
- </Modal>
56
+ {#snippet footerContent()}
57
+ {#if footer}
58
+ {@render footer()}
59
+ {:else}
60
+ <Button variant="alternative">Cancel</Button>
61
+ <Button variant="default">Confirm</Button>
62
+ {/if}
63
+ {/snippet}
64
+
65
+ <Modal bind:show {oncancel} header={headerContent} body={bodyContent} footer={footerContent} />
@@ -727,7 +727,7 @@ describe('ButtonAuditDashboard', () => {
727
727
 
728
728
  it('should have proper border and shadow styling', () => {
729
729
  const { container } = render(ButtonAuditDashboard);
730
- const header = container.querySelector('.border-b.shadow-sm');
730
+ const header = container.querySelector('.border-b.shadow');
731
731
  expect(header).toBeInTheDocument();
732
732
  });
733
733
  });
@@ -161,7 +161,7 @@
161
161
 
162
162
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
163
163
  <!-- Sticky Header -->
164
- <div class="sticky top-0 z-50 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm">
164
+ <div class="sticky top-0 z-50 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow">
165
165
  <div class="max-w-7xl mx-auto px-4 py-4">
166
166
  <div class="flex flex-wrap items-center justify-between gap-4">
167
167
  <div>
@@ -292,21 +292,21 @@
292
292
  📂 Open
293
293
  </a>
294
294
  </div>
295
- <div class="flex items-center gap-2 flex-shrink-0 ml-2">
296
- {#if isPreviewable(route.file)}
297
- <!-- svelte-ignore a11y_click_events_have_key_events -->
298
- <!-- svelte-ignore a11y_no_static_element_interactions -->
299
- <span
300
- role="button"
301
- tabindex="0"
302
- onclick={(e) => { e.stopPropagation(); openPreview(route.route, route.file); }}
303
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); openPreview(route.route, route.file); }}}
304
- class="px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-300 dark:hover:bg-green-800 cursor-pointer"
305
- title="Preview this page"
306
- >
307
- 👁️ Preview
308
- </span>
309
- {/if}
295
+ <div class="flex items-center gap-2 shrink-0 ml-2">
296
+ {#if isPreviewable(route.file)}
297
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
298
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
299
+ <span
300
+ role="button"
301
+ tabindex="0"
302
+ onclick={(e) => { e.stopPropagation(); openPreview(route.route, route.file); }}
303
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); openPreview(route.route, route.file); }}}
304
+ class="px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-300 dark:hover:bg-green-800 cursor-pointer"
305
+ title="Preview this page"
306
+ >
307
+ 👁️ Preview
308
+ </span>
309
+ {/if}
310
310
  <span class="text-xs text-gray-500 dark:text-gray-400">
311
311
  {routeVisibleButtons.length} buttons
312
312
  </span>
@@ -404,7 +404,7 @@
404
404
 
405
405
  <!-- Preview Modal -->
406
406
  {#if previewModal.open}
407
- <div class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
407
+ <div class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs">
408
408
  <div class="relative w-full max-w-6xl h-[85vh] bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden flex flex-col">
409
409
  <!-- Modal Header -->
410
410
  <div class="flex items-center justify-between px-4 py-3 bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
@@ -430,14 +430,14 @@
430
430
  </div>
431
431
  </div>
432
432
 
433
- <!-- Iframe Content -->
434
- <div class="flex-1 relative">
435
- <iframe
436
- src={previewModal.url}
437
- class="absolute inset-0 w-full h-full border-0"
438
- title="Page preview"
439
- ></iframe>
440
- </div>
433
+ <!-- Iframe Content -->
434
+ <div class="flex-1 relative">
435
+ <iframe
436
+ src={previewModal.url}
437
+ class="absolute inset-0 w-full h-full border-0"
438
+ title="Page preview"
439
+ ></iframe>
440
+ </div>
441
441
 
442
442
  <!-- Modal Footer -->
443
443
  <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400">
@@ -74,7 +74,7 @@
74
74
 
75
75
  <div class="min-h-screen bg-gray-100 dark:bg-gray-900 p-4">
76
76
  <!-- Header -->
77
- <div class="sticky top-0 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 mb-4">
77
+ <div class="sticky top-0 z-50 bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-4">
78
78
  <div class="flex flex-wrap items-center justify-between gap-4">
79
79
  <div>
80
80
  <h1 class="text-xl font-bold text-gray-900 dark:text-white">Button Grid View</h1>
@@ -192,7 +192,7 @@
192
192
  type="text"
193
193
  placeholder="John Doe"
194
194
  bind:value={formFieldValue}
195
- class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
195
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
196
196
  />
197
197
  {/snippet}
198
198
  </FormField>
@@ -208,7 +208,7 @@
208
208
  {id}
209
209
  type="email"
210
210
  placeholder="email@example.com"
211
- class="w-full px-3 py-2 border {error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
211
+ class="w-full px-3 py-2 border {error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
212
212
  />
213
213
  {/snippet}
214
214
  </FormField>
@@ -423,7 +423,7 @@
423
423
  {id}
424
424
  type="email"
425
425
  placeholder="email@example.com"
426
- class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
426
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
427
427
  />
428
428
  {/snippet}
429
429
  </FormField>
@@ -173,7 +173,7 @@
173
173
 
174
174
  /* Elevated card with shadow */
175
175
  .card-elevated {
176
- @apply bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm;
176
+ @apply bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow;
177
177
  }
178
178
 
179
179
  /* Compact card with less padding */
@@ -258,22 +258,22 @@
258
258
 
259
259
  /* Base input styling */
260
260
  .input-base {
261
- @apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
261
+ @apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent;
262
262
  }
263
263
 
264
264
  /* Base button styling */
265
265
  .btn-base {
266
- @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
266
+ @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
267
267
  }
268
268
 
269
269
  /* Primary button */
270
270
  .btn-primary {
271
- @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
271
+ @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
272
272
  }
273
273
 
274
274
  /* Secondary/outline button */
275
275
  .btn-secondary {
276
- @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-gray-500;
276
+ @apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-gray-500;
277
277
  }
278
278
 
279
279
  /* ==========================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmicdrop/svelte-components",
3
- "version": "5.9.4",
3
+ "version": "5.10.0",
4
4
  "description": "Shared component library for Micdrop applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",