@getmicdrop/svelte-components 5.5.1 → 5.5.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.
Files changed (214) hide show
  1. package/dist/calendar/AboutShow/AboutShow.svelte +172 -172
  2. package/dist/calendar/Calendar/MiniMonthCalendar.svelte +782 -782
  3. package/dist/calendar/FAQs/FAQs.svelte +75 -75
  4. package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte +126 -126
  5. package/dist/calendar/OrderSummary/OrderSummary.svelte +367 -367
  6. package/dist/calendar/PublicCard/PublicCard.svelte +134 -134
  7. package/dist/calendar/ShowCard/ShowCard.svelte +157 -157
  8. package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte +61 -61
  9. package/dist/components/Layout/Grid.svelte +4 -4
  10. package/dist/components/Layout/Section.svelte +80 -80
  11. package/dist/components/Layout/Sidebar.svelte +108 -108
  12. package/dist/components/Layout/Stack.svelte +6 -6
  13. package/dist/constants/validation.js +91 -91
  14. package/dist/constants/validation.spec.js +64 -64
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +223 -222
  17. package/dist/patterns/data/DataGrid.svelte +45 -45
  18. package/dist/patterns/data/DataList.svelte +24 -24
  19. package/dist/patterns/data/DataTable.svelte +36 -36
  20. package/dist/patterns/forms/FormActions.spec.js +88 -88
  21. package/dist/patterns/forms/FormActions.stories.svelte +97 -97
  22. package/dist/patterns/forms/FormActions.svelte +46 -46
  23. package/dist/patterns/forms/FormGrid.svelte +33 -33
  24. package/dist/patterns/forms/FormSection.svelte +32 -32
  25. package/dist/patterns/forms/FormValidationSummary.stories.svelte +83 -83
  26. package/dist/patterns/forms/FormValidationSummary.svelte +33 -33
  27. package/dist/patterns/layout/Sidebar.svelte +39 -39
  28. package/dist/patterns/navigation/BottomNav.stories.svelte +117 -117
  29. package/dist/patterns/navigation/BottomNav.svelte +20 -20
  30. package/dist/patterns/navigation/Header.stories.svelte +77 -77
  31. package/dist/patterns/navigation/Header.svelte +193 -193
  32. package/dist/patterns/page/PageHeader.svelte +18 -18
  33. package/dist/patterns/page/PageLayout.svelte +40 -40
  34. package/dist/patterns/page/PageLoader.spec.js +54 -54
  35. package/dist/patterns/page/PageLoader.stories.svelte +137 -137
  36. package/dist/patterns/page/PageLoader.svelte +24 -24
  37. package/dist/patterns/page/SectionHeader.svelte +29 -29
  38. package/dist/presets/badges.js +112 -112
  39. package/dist/presets/buttons.js +76 -76
  40. package/dist/presets/index.js +9 -9
  41. package/dist/primitives/Accordion/Accordion.stories.svelte +75 -75
  42. package/dist/primitives/Accordion/Accordion.svelte +42 -42
  43. package/dist/primitives/Accordion/AccordionItem.svelte +95 -95
  44. package/dist/primitives/Alert/Alert.spec.js +170 -170
  45. package/dist/primitives/Alert/Alert.stories.svelte +88 -88
  46. package/dist/primitives/Alert/Alert.svelte +27 -27
  47. package/dist/primitives/Avatar/Avatar.stories.svelte +94 -94
  48. package/dist/primitives/Avatar/Avatar.svelte +66 -66
  49. package/dist/primitives/Badges/Badge.spec.js +103 -103
  50. package/dist/primitives/Badges/Badge.stories.svelte +86 -86
  51. package/dist/primitives/Badges/Badge.svelte +79 -79
  52. package/dist/primitives/BottomSheet/BottomSheet.spec.js +127 -127
  53. package/dist/primitives/BottomSheet/BottomSheet.stories.svelte +83 -83
  54. package/dist/primitives/BottomSheet/BottomSheet.svelte +100 -100
  55. package/dist/primitives/Breadcrumb/Breadcrumb.spec.js +120 -120
  56. package/dist/primitives/Breadcrumb/Breadcrumb.stories.svelte +23 -23
  57. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +89 -89
  58. package/dist/primitives/Button/Button.spec.js +211 -211
  59. package/dist/primitives/Button/Button.stories.svelte +76 -76
  60. package/dist/primitives/Button/Button.svelte +270 -270
  61. package/dist/primitives/Button/ButtonSaveDemo.spec.js +48 -48
  62. package/dist/primitives/Button/ButtonSaveDemo.svelte +25 -25
  63. package/dist/primitives/Button/ButtonVariantShowcase.svelte +129 -129
  64. package/dist/primitives/Card.spec.js +49 -49
  65. package/dist/primitives/Card.stories.svelte +22 -22
  66. package/dist/primitives/Card.svelte +28 -28
  67. package/dist/primitives/Checkbox/Checkbox.stories.svelte +84 -84
  68. package/dist/primitives/Checkbox/Checkbox.svelte +88 -88
  69. package/dist/primitives/DarkModeToggle.spec.js +357 -357
  70. package/dist/primitives/DarkModeToggle.stories.svelte +57 -57
  71. package/dist/primitives/DarkModeToggle.svelte +136 -136
  72. package/dist/primitives/Drawer/Drawer.stories.svelte +80 -80
  73. package/dist/primitives/Drawer/Drawer.svelte +120 -120
  74. package/dist/primitives/Dropdown/Dropdown.stories.svelte +137 -137
  75. package/dist/primitives/Dropdown/Dropdown.svelte +14 -14
  76. package/dist/primitives/Dropdown/DropdownItem.svelte +80 -80
  77. package/dist/primitives/Icons/ArrowLeft.svelte +8 -8
  78. package/dist/primitives/Icons/ArrowRight.svelte +8 -8
  79. package/dist/primitives/Icons/Availability.svelte +14 -14
  80. package/dist/primitives/Icons/Back.svelte +14 -14
  81. package/dist/primitives/Icons/CheckCircle.svelte +6 -6
  82. package/dist/primitives/Icons/CheckCircleOutline.svelte +15 -15
  83. package/dist/primitives/Icons/ChevronLeft.svelte +4 -4
  84. package/dist/primitives/Icons/ChevronRight.svelte +4 -4
  85. package/dist/primitives/Icons/Copy.svelte +15 -15
  86. package/dist/primitives/Icons/Cross.svelte +5 -5
  87. package/dist/primitives/Icons/DownArrow.svelte +8 -8
  88. package/dist/primitives/Icons/ErrorCircle.svelte +6 -6
  89. package/dist/primitives/Icons/FacebookIcon.svelte +2 -2
  90. package/dist/primitives/Icons/Home.svelte +15 -15
  91. package/dist/primitives/Icons/Icon.spec.js +169 -169
  92. package/dist/primitives/Icons/Icon.stories.svelte +100 -100
  93. package/dist/primitives/Icons/Icon.svelte +52 -52
  94. package/dist/primitives/Icons/IconGallery.stories.svelte +235 -235
  95. package/dist/primitives/Icons/Info.svelte +7 -7
  96. package/dist/primitives/Icons/InstagramIcon.svelte +4 -4
  97. package/dist/primitives/Icons/LogoInstagram.svelte +2 -2
  98. package/dist/primitives/Icons/Message.svelte +15 -15
  99. package/dist/primitives/Icons/MoonIcon.svelte +5 -5
  100. package/dist/primitives/Icons/More.svelte +21 -21
  101. package/dist/primitives/Icons/MoreHori.spec.js +61 -61
  102. package/dist/primitives/Icons/MoreHori.svelte +22 -22
  103. package/dist/primitives/Icons/Notification.svelte +14 -14
  104. package/dist/primitives/Icons/Payment.svelte +14 -14
  105. package/dist/primitives/Icons/Profile.svelte +21 -21
  106. package/dist/primitives/Icons/Reload.svelte +29 -29
  107. package/dist/primitives/Icons/Shows.svelte +21 -21
  108. package/dist/primitives/Icons/Signout.svelte +21 -21
  109. package/dist/primitives/Icons/SunIcon.svelte +8 -8
  110. package/dist/primitives/Icons/TiktokIcon.svelte +2 -2
  111. package/dist/primitives/Icons/TwitterIcon.svelte +2 -2
  112. package/dist/primitives/Icons/WarningIcon.spec.js +18 -18
  113. package/dist/primitives/Icons/WarningIcon.svelte +5 -5
  114. package/dist/primitives/Input/Input.spec.js +573 -573
  115. package/dist/primitives/Input/Input.stories.svelte +139 -139
  116. package/dist/primitives/Input/Input.svelte +392 -392
  117. package/dist/primitives/Input/Select.spec.js +218 -218
  118. package/dist/primitives/Input/Select.stories.svelte +112 -112
  119. package/dist/primitives/Input/Select.svelte +128 -128
  120. package/dist/primitives/Input/Textarea.stories.svelte +137 -137
  121. package/dist/primitives/Input/Textarea.svelte +35 -35
  122. package/dist/primitives/Label/Label.svelte +37 -37
  123. package/dist/primitives/Modal/Modal.spec.js +95 -95
  124. package/dist/primitives/Modal/Modal.stories.svelte +86 -86
  125. package/dist/primitives/Modal/Modal.svelte +158 -158
  126. package/dist/primitives/NumberInput/NumberInput.svelte +106 -106
  127. package/dist/primitives/Pagination/Pagination.stories.svelte +76 -76
  128. package/dist/primitives/Pagination/Pagination.svelte +261 -261
  129. package/dist/primitives/Radio/Radio.stories.svelte +80 -80
  130. package/dist/primitives/Radio/Radio.svelte +67 -67
  131. package/dist/primitives/Skeleton/CardPlaceholder.svelte +87 -87
  132. package/dist/primitives/Skeleton/ImagePlaceholder.svelte +59 -59
  133. package/dist/primitives/Skeleton/ListPlaceholder.svelte +76 -76
  134. package/dist/primitives/Skeleton/Skeleton.stories.svelte +151 -151
  135. package/dist/primitives/Skeleton/Skeleton.svelte +26 -26
  136. package/dist/primitives/Spinner/Spinner.spec.js +75 -75
  137. package/dist/primitives/Spinner/Spinner.stories.svelte +29 -29
  138. package/dist/primitives/Spinner/Spinner.svelte +20 -20
  139. package/dist/primitives/Tabs/TabItem.svelte +49 -49
  140. package/dist/primitives/Tabs/Tabs.stories.svelte +112 -112
  141. package/dist/primitives/Tabs/Tabs.svelte +123 -123
  142. package/dist/primitives/Toggle.spec.js +127 -127
  143. package/dist/primitives/Toggle.stories.svelte +92 -92
  144. package/dist/primitives/Toggle.svelte +71 -71
  145. package/dist/primitives/Typography/Typography.svelte +53 -53
  146. package/dist/primitives/ValidationError.spec.js +103 -103
  147. package/dist/primitives/ValidationError.stories.svelte +69 -69
  148. package/dist/primitives/ValidationError.svelte +29 -29
  149. package/dist/recipes/CropImage/CropImage.spec.js +216 -216
  150. package/dist/recipes/CropImage/CropImage.stories.svelte +104 -104
  151. package/dist/recipes/CropImage/CropImage.svelte +238 -238
  152. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte +125 -125
  153. package/dist/recipes/ImageUploader/ImageUploader.svelte +804 -804
  154. package/dist/recipes/SuperLogin/SuperLogin.svelte +7 -6
  155. package/dist/recipes/SuperLogin/SuperLogin.svelte.d.ts.map +1 -1
  156. package/dist/recipes/Toaster/Toaster.stories.svelte +62 -62
  157. package/dist/recipes/feedback/EmptyState/EmptyState.svelte +1 -1
  158. package/dist/recipes/feedback/ErrorDisplay.spec.js +69 -69
  159. package/dist/recipes/feedback/ErrorDisplay.stories.svelte +101 -101
  160. package/dist/recipes/feedback/ErrorDisplay.svelte +1 -1
  161. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.spec.js +129 -129
  162. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.svelte +157 -157
  163. package/dist/recipes/fields/CheckboxField.svelte +85 -85
  164. package/dist/recipes/fields/FormField.svelte +58 -58
  165. package/dist/recipes/fields/RadioGroup.svelte +95 -95
  166. package/dist/recipes/fields/SelectField.svelte +80 -80
  167. package/dist/recipes/fields/TextareaField.svelte +97 -97
  168. package/dist/recipes/fields/ToggleField.svelte +60 -60
  169. package/dist/recipes/fields/index.js +7 -7
  170. package/dist/recipes/inputs/MultiSelect.spec.js +257 -257
  171. package/dist/recipes/inputs/MultiSelect.stories.svelte +133 -133
  172. package/dist/recipes/inputs/MultiSelect.svelte +249 -249
  173. package/dist/recipes/inputs/OTPInput.spec.js +238 -238
  174. package/dist/recipes/inputs/OTPInput.stories.svelte +162 -162
  175. package/dist/recipes/inputs/OTPInput.svelte +29 -29
  176. package/dist/recipes/inputs/PasswordInput.svelte +22 -22
  177. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.spec.js +173 -173
  178. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte +43 -43
  179. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.spec.js +300 -300
  180. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.stories.svelte +123 -123
  181. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte +326 -326
  182. package/dist/recipes/inputs/Search.svelte +37 -37
  183. package/dist/recipes/inputs/SelectDropdown.svelte +57 -57
  184. package/dist/recipes/modals/AlertModal.svelte +130 -130
  185. package/dist/recipes/modals/ConfirmationModal.spec.js +191 -191
  186. package/dist/recipes/modals/ConfirmationModal.stories.svelte +119 -119
  187. package/dist/recipes/modals/ConfirmationModal.svelte +152 -152
  188. package/dist/recipes/modals/InputModal.svelte +182 -182
  189. package/dist/recipes/modals/ModalStateManager.spec.js +100 -100
  190. package/dist/recipes/modals/ModalStateManager.svelte +77 -77
  191. package/dist/recipes/modals/ModalTestWrapper.svelte +65 -65
  192. package/dist/recipes/modals/StatusModal.svelte +206 -206
  193. package/dist/services/EventService.js +75 -75
  194. package/dist/services/EventService.spec.js +217 -217
  195. package/dist/services/ShowService.spec.js +342 -342
  196. package/dist/stores/auth.js +36 -36
  197. package/dist/stores/auth.spec.js +139 -139
  198. package/dist/stores/toaster.js +13 -13
  199. package/dist/stories/ButtonAuditReview.stories.svelte +14 -14
  200. package/dist/stories/ButtonAuditReview.svelte +427 -427
  201. package/dist/stories/PatternsGallery.stories.svelte +19 -19
  202. package/dist/stories/PatternsGallery.svelte +206 -206
  203. package/dist/stories/PrimitivesGallery.stories.svelte +19 -19
  204. package/dist/stories/PrimitivesGallery.svelte +725 -725
  205. package/dist/stories/RecipesGallery.stories.svelte +19 -19
  206. package/dist/stories/RecipesGallery.svelte +271 -271
  207. package/dist/stories/button-audit-manifest.json +11186 -11186
  208. package/dist/tailwind/preset.cjs +82 -82
  209. package/dist/tokens/tokens.css +87 -87
  210. package/dist/utils/transitions.d.ts +24 -0
  211. package/dist/utils/transitions.d.ts.map +1 -0
  212. package/dist/utils/transitions.js +62 -0
  213. package/dist/utils/utils.js +354 -354
  214. package/package.json +283 -283
@@ -1,49 +1,49 @@
1
- <script module>
2
- import { defineMeta } from "@storybook/addon-svelte-csf";
3
-
4
- import ValidationError from './ValidationError.svelte';
5
-
6
- const { Story } = defineMeta({
7
- title: 'Components/ValidationError',
8
- component: ValidationError,
9
- tags: ['autodocs'],
10
- argTypes: {
11
- error: { control: 'text' },
12
- show: { control: 'boolean' },
13
- id: { control: 'text' },
14
- },
15
- });
16
-
17
- </script>
18
-
19
- <script>
20
- </script>
21
-
22
-
23
- <Story
24
- name="Default"
25
- args={{
26
- error: 'This field is required',
27
- show: true,
28
- }}
29
- />
30
-
31
- <Story
32
- name="Email Validation"
33
- args={{
34
- error: 'Please enter a valid email address',
35
- show: true,
36
- }}
37
- />
38
-
39
- <Story
40
- name="Password Validation"
41
- args={{
42
- error: 'Password must be at least 8 characters',
43
- show: true,
44
- }}
45
- />
46
-
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+
4
+ import ValidationError from './ValidationError.svelte';
5
+
6
+ const { Story } = defineMeta({
7
+ title: 'Components/ValidationError',
8
+ component: ValidationError,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ error: { control: 'text' },
12
+ show: { control: 'boolean' },
13
+ id: { control: 'text' },
14
+ },
15
+ });
16
+
17
+ </script>
18
+
19
+ <script>
20
+ </script>
21
+
22
+
23
+ <Story
24
+ name="Default"
25
+ args={{
26
+ error: 'This field is required',
27
+ show: true,
28
+ }}
29
+ />
30
+
31
+ <Story
32
+ name="Email Validation"
33
+ args={{
34
+ error: 'Please enter a valid email address',
35
+ show: true,
36
+ }}
37
+ />
38
+
39
+ <Story
40
+ name="Password Validation"
41
+ args={{
42
+ error: 'Password must be at least 8 characters',
43
+ show: true,
44
+ }}
45
+ />
46
+
47
47
  <Story name="With Input Field">
48
48
  <div class="max-w-md">
49
49
  <label for="email-field" class="block text-sm font-medium mb-1">Email Address</label>
@@ -87,26 +87,26 @@
87
87
  <p class="text-sm text-gray-500 dark:text-gray-400 mt-2">Validation error is hidden until field is touched</p>
88
88
  </div>
89
89
  </Story>
90
-
91
- <Story name="Accessible with aria-describedby">
92
- <div class="max-w-md">
93
- <label for="accessible-email" class="block text-sm font-medium mb-1">Email Address</label>
94
- <input
95
- id="accessible-email"
96
- type="email"
97
- aria-describedby="email-error"
98
- aria-invalid="true"
99
- class="w-full px-3 py-2 border border-red-300 rounded focus:ring-red-500 focus:border-red-500"
100
- value="invalid-email"
101
- />
102
- <ValidationError
103
- id="email-error"
104
- error="Please enter a valid email address"
105
- show={true}
106
- />
107
- <p class="text-sm text-gray-500 mt-4">
108
- This example shows proper accessibility: the input references the error via aria-describedby,
109
- and the error has aria-live="assertive" for screen reader announcements.
110
- </p>
111
- </div>
112
- </Story>
90
+
91
+ <Story name="Accessible with aria-describedby">
92
+ <div class="max-w-md">
93
+ <label for="accessible-email" class="block text-sm font-medium mb-1">Email Address</label>
94
+ <input
95
+ id="accessible-email"
96
+ type="email"
97
+ aria-describedby="email-error"
98
+ aria-invalid="true"
99
+ class="w-full px-3 py-2 border border-red-300 rounded focus:ring-red-500 focus:border-red-500"
100
+ value="invalid-email"
101
+ />
102
+ <ValidationError
103
+ id="email-error"
104
+ error="Please enter a valid email address"
105
+ show={true}
106
+ />
107
+ <p class="text-sm text-gray-500 mt-4">
108
+ This example shows proper accessibility: the input references the error via aria-describedby,
109
+ and the error has aria-live="assertive" for screen reader announcements.
110
+ </p>
111
+ </div>
112
+ </Story>
@@ -1,29 +1,29 @@
1
- <script lang="ts">
2
- import { slide } from "svelte/transition";
3
- import { cubicOut } from "svelte/easing";
4
- import WarningIcon from "./Icons/WarningIcon.svelte";
5
-
6
- interface Props {
7
- /** Error message to display */
8
- error?: string;
9
- /** Whether to show the error */
10
- show?: boolean;
11
- /** Optional ID for aria-describedby linking from input fields */
12
- id?: string;
13
- }
14
-
15
- let { error = '', show = true, id }: Props = $props();
16
- </script>
17
-
18
- {#if error && show}
19
- <div
20
- transition:slide={{ duration: 300, easing: cubicOut }}
21
- class="flex items-start gap-1.5 text-red-600 dark:text-red-500 mt-2"
22
- role="alert"
23
- aria-live="assertive"
24
- {id}
25
- >
26
- <WarningIcon class="shrink-0 mt-[1px]" />
27
- <p class="text-sm leading-tight m-0">{error}</p>
28
- </div>
29
- {/if}
1
+ <script lang="ts">
2
+ import { safeSlide } from "../utils/transitions.js";
3
+ import { cubicOut } from "svelte/easing";
4
+ import WarningIcon from "./Icons/WarningIcon.svelte";
5
+
6
+ interface Props {
7
+ /** Error message to display */
8
+ error?: string;
9
+ /** Whether to show the error */
10
+ show?: boolean;
11
+ /** Optional ID for aria-describedby linking from input fields */
12
+ id?: string;
13
+ }
14
+
15
+ let { error = '', show = true, id }: Props = $props();
16
+ </script>
17
+
18
+ {#if error && show}
19
+ <div
20
+ transition:safeSlide={{ duration: 300, easing: cubicOut }}
21
+ class="flex items-start gap-1.5 text-red-600 dark:text-red-500 mt-2"
22
+ role="alert"
23
+ aria-live="assertive"
24
+ {id}
25
+ >
26
+ <WarningIcon class="shrink-0 mt-[1px]" />
27
+ <p class="text-sm leading-tight m-0">{error}</p>
28
+ </div>
29
+ {/if}
@@ -1,216 +1,216 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
3
-
4
- // Mock svelte-easy-crop with the mock Svelte component
5
- vi.mock('svelte-easy-crop', async () => {
6
- const MockCropper = await import('../../__mocks__/svelte-easy-crop.svelte');
7
- return { default: MockCropper.default };
8
- });
9
-
10
- // Import after mock is set up
11
- import CropImage from './CropImage.svelte';
12
-
13
- describe('CropImage', () => {
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
-
18
- describe('when showModal is false', () => {
19
- it('does not render modal content', () => {
20
- const { container } = render(CropImage, {
21
- props: { showModal: false, imageSrc: '' },
22
- });
23
- expect(container.querySelector('.fixed')).toBeNull();
24
- });
25
- });
26
-
27
- describe('when showModal is true', () => {
28
- const defaultProps = {
29
- showModal: true,
30
- imageSrc: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
31
- onSave: vi.fn(),
32
- onCancel: vi.fn(),
33
- };
34
-
35
- it('renders the modal', () => {
36
- const { container } = render(CropImage, { props: defaultProps });
37
- expect(container.querySelector('.fixed')).toBeInTheDocument();
38
- });
39
-
40
- it('renders the header with title', () => {
41
- render(CropImage, { props: defaultProps });
42
- expect(screen.getByText('Crop image')).toBeInTheDocument();
43
- });
44
-
45
- it('renders cancel button', () => {
46
- render(CropImage, { props: defaultProps });
47
- expect(screen.getByText('Cancel')).toBeInTheDocument();
48
- });
49
-
50
- it('renders save button', () => {
51
- render(CropImage, { props: defaultProps });
52
- expect(screen.getByText('Save')).toBeInTheDocument();
53
- });
54
-
55
- it('renders instructions text', () => {
56
- render(CropImage, { props: defaultProps });
57
- expect(screen.getByText(/Drag to position/)).toBeInTheDocument();
58
- });
59
-
60
- it('renders zoom controls', () => {
61
- render(CropImage, { props: defaultProps });
62
- expect(screen.getByLabelText('Zoom out')).toBeInTheDocument();
63
- expect(screen.getByLabelText('Zoom in')).toBeInTheDocument();
64
- expect(screen.getByLabelText('Zoom level')).toBeInTheDocument();
65
- });
66
-
67
- it('shows loading state initially', () => {
68
- render(CropImage, { props: defaultProps });
69
- expect(screen.getByText('Loading...')).toBeInTheDocument();
70
- });
71
-
72
- it('calls onCancel when cancel button clicked', async () => {
73
- const onCancel = vi.fn();
74
- render(CropImage, { props: { ...defaultProps, onCancel } });
75
-
76
- await fireEvent.click(screen.getByText('Cancel'));
77
- expect(onCancel).toHaveBeenCalled();
78
- });
79
-
80
- it('calls onCancel when close button clicked', async () => {
81
- const onCancel = vi.fn();
82
- render(CropImage, { props: { ...defaultProps, onCancel } });
83
-
84
- await fireEvent.click(screen.getByLabelText('Close'));
85
- expect(onCancel).toHaveBeenCalled();
86
- });
87
-
88
- it('calls onCancel when clicking backdrop', async () => {
89
- const onCancel = vi.fn();
90
- const { container } = render(CropImage, { props: { ...defaultProps, onCancel } });
91
-
92
- const backdrop = container.querySelector('.fixed');
93
- await fireEvent.click(backdrop);
94
- expect(onCancel).toHaveBeenCalled();
95
- });
96
-
97
- it('zoom out button decreases zoom', async () => {
98
- render(CropImage, { props: defaultProps });
99
- const zoomOutBtn = screen.getByLabelText('Zoom out');
100
- const slider = screen.getByLabelText('Zoom level');
101
-
102
- // Initial zoom is 1
103
- expect(slider.value).toBe('1');
104
-
105
- // Click zoom out (should stay at 1 since that's min)
106
- await fireEvent.click(zoomOutBtn);
107
- expect(slider.value).toBe('1');
108
- });
109
-
110
- it('zoom in button increases zoom', async () => {
111
- render(CropImage, { props: defaultProps });
112
- const zoomInBtn = screen.getByLabelText('Zoom in');
113
- const slider = screen.getByLabelText('Zoom level');
114
-
115
- await fireEvent.click(zoomInBtn);
116
- expect(parseFloat(slider.value)).toBeGreaterThan(1);
117
- });
118
-
119
- it('hides loading state after crop complete', async () => {
120
- render(CropImage, { props: defaultProps });
121
-
122
- // Initially loading
123
- expect(screen.getByText('Loading...')).toBeInTheDocument();
124
-
125
- // Wait for the mock cropper to emit cropcomplete
126
- await waitFor(() => {
127
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
128
- }, { timeout: 100 });
129
- });
130
-
131
- it('slider controls zoom value', async () => {
132
- render(CropImage, { props: defaultProps });
133
- const slider = screen.getByLabelText('Zoom level');
134
-
135
- await fireEvent.input(slider, { target: { value: '2' } });
136
- expect(slider.value).toBe('2');
137
- });
138
-
139
- it('handles save button click when not loading', async () => {
140
- const onSave = vi.fn();
141
- render(CropImage, { props: { ...defaultProps, onSave } });
142
-
143
- // Wait for loading to complete
144
- await waitFor(() => {
145
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
146
- }, { timeout: 200 });
147
-
148
- // Try clicking save button
149
- const saveButton = screen.getByText('Save');
150
- await fireEvent.click(saveButton);
151
- });
152
-
153
- it('disables save button while loading', () => {
154
- render(CropImage, { props: defaultProps });
155
- const saveButton = screen.getByText('Save');
156
- // Save button should be disabled during loading
157
- expect(saveButton.closest('button')).toBeDefined();
158
- });
159
-
160
- it('resets state when modal reopens with new image', async () => {
161
- const { rerender } = render(CropImage, { props: defaultProps });
162
-
163
- // Change imageSrc to trigger reset
164
- await rerender({
165
- showModal: true,
166
- imageSrc: 'data:image/png;base64,differentImage',
167
- onSave: vi.fn(),
168
- onCancel: vi.fn(),
169
- });
170
-
171
- expect(screen.getByText('Crop image')).toBeInTheDocument();
172
- });
173
-
174
- it('updates zoom progress based on zoom value', async () => {
175
- render(CropImage, { props: defaultProps });
176
- const slider = screen.getByLabelText('Zoom level');
177
-
178
- // Set zoom to middle value
179
- await fireEvent.input(slider, { target: { value: '2' } });
180
- expect(slider.value).toBe('2');
181
-
182
- // Set zoom to max
183
- await fireEvent.input(slider, { target: { value: '3' } });
184
- expect(slider.value).toBe('3');
185
- });
186
-
187
- it('zoom value stays within bounds', async () => {
188
- render(CropImage, { props: defaultProps });
189
- const slider = screen.getByLabelText('Zoom level');
190
- const zoomOutBtn = screen.getByLabelText('Zoom out');
191
- const zoomInBtn = screen.getByLabelText('Zoom in');
192
-
193
- // Spam zoom out - should not go below 1
194
- for (let i = 0; i < 10; i++) {
195
- await fireEvent.click(zoomOutBtn);
196
- }
197
- expect(parseFloat(slider.value)).toBe(1);
198
-
199
- // Set to max and spam zoom in
200
- await fireEvent.input(slider, { target: { value: '3' } });
201
- for (let i = 0; i < 10; i++) {
202
- await fireEvent.click(zoomInBtn);
203
- }
204
- expect(parseFloat(slider.value)).toBeLessThanOrEqual(3);
205
- });
206
-
207
- it('renders cropper component with correct props', async () => {
208
- const { container } = render(CropImage, { props: defaultProps });
209
-
210
- // Wait for render
211
- await waitFor(() => {
212
- expect(container.querySelector('.cropper-container')).toBeInTheDocument();
213
- });
214
- });
215
- });
216
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
3
+
4
+ // Mock svelte-easy-crop with the mock Svelte component
5
+ vi.mock('svelte-easy-crop', async () => {
6
+ const MockCropper = await import('../../__mocks__/svelte-easy-crop.svelte');
7
+ return { default: MockCropper.default };
8
+ });
9
+
10
+ // Import after mock is set up
11
+ import CropImage from './CropImage.svelte';
12
+
13
+ describe('CropImage', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('when showModal is false', () => {
19
+ it('does not render modal content', () => {
20
+ const { container } = render(CropImage, {
21
+ props: { showModal: false, imageSrc: '' },
22
+ });
23
+ expect(container.querySelector('.fixed')).toBeNull();
24
+ });
25
+ });
26
+
27
+ describe('when showModal is true', () => {
28
+ const defaultProps = {
29
+ showModal: true,
30
+ imageSrc: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
31
+ onSave: vi.fn(),
32
+ onCancel: vi.fn(),
33
+ };
34
+
35
+ it('renders the modal', () => {
36
+ const { container } = render(CropImage, { props: defaultProps });
37
+ expect(container.querySelector('.fixed')).toBeInTheDocument();
38
+ });
39
+
40
+ it('renders the header with title', () => {
41
+ render(CropImage, { props: defaultProps });
42
+ expect(screen.getByText('Crop image')).toBeInTheDocument();
43
+ });
44
+
45
+ it('renders cancel button', () => {
46
+ render(CropImage, { props: defaultProps });
47
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
48
+ });
49
+
50
+ it('renders save button', () => {
51
+ render(CropImage, { props: defaultProps });
52
+ expect(screen.getByText('Save')).toBeInTheDocument();
53
+ });
54
+
55
+ it('renders instructions text', () => {
56
+ render(CropImage, { props: defaultProps });
57
+ expect(screen.getByText(/Drag to position/)).toBeInTheDocument();
58
+ });
59
+
60
+ it('renders zoom controls', () => {
61
+ render(CropImage, { props: defaultProps });
62
+ expect(screen.getByLabelText('Zoom out')).toBeInTheDocument();
63
+ expect(screen.getByLabelText('Zoom in')).toBeInTheDocument();
64
+ expect(screen.getByLabelText('Zoom level')).toBeInTheDocument();
65
+ });
66
+
67
+ it('shows loading state initially', () => {
68
+ render(CropImage, { props: defaultProps });
69
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
70
+ });
71
+
72
+ it('calls onCancel when cancel button clicked', async () => {
73
+ const onCancel = vi.fn();
74
+ render(CropImage, { props: { ...defaultProps, onCancel } });
75
+
76
+ await fireEvent.click(screen.getByText('Cancel'));
77
+ expect(onCancel).toHaveBeenCalled();
78
+ });
79
+
80
+ it('calls onCancel when close button clicked', async () => {
81
+ const onCancel = vi.fn();
82
+ render(CropImage, { props: { ...defaultProps, onCancel } });
83
+
84
+ await fireEvent.click(screen.getByLabelText('Close'));
85
+ expect(onCancel).toHaveBeenCalled();
86
+ });
87
+
88
+ it('calls onCancel when clicking backdrop', async () => {
89
+ const onCancel = vi.fn();
90
+ const { container } = render(CropImage, { props: { ...defaultProps, onCancel } });
91
+
92
+ const backdrop = container.querySelector('.fixed');
93
+ await fireEvent.click(backdrop);
94
+ expect(onCancel).toHaveBeenCalled();
95
+ });
96
+
97
+ it('zoom out button decreases zoom', async () => {
98
+ render(CropImage, { props: defaultProps });
99
+ const zoomOutBtn = screen.getByLabelText('Zoom out');
100
+ const slider = screen.getByLabelText('Zoom level');
101
+
102
+ // Initial zoom is 1
103
+ expect(slider.value).toBe('1');
104
+
105
+ // Click zoom out (should stay at 1 since that's min)
106
+ await fireEvent.click(zoomOutBtn);
107
+ expect(slider.value).toBe('1');
108
+ });
109
+
110
+ it('zoom in button increases zoom', async () => {
111
+ render(CropImage, { props: defaultProps });
112
+ const zoomInBtn = screen.getByLabelText('Zoom in');
113
+ const slider = screen.getByLabelText('Zoom level');
114
+
115
+ await fireEvent.click(zoomInBtn);
116
+ expect(parseFloat(slider.value)).toBeGreaterThan(1);
117
+ });
118
+
119
+ it('hides loading state after crop complete', async () => {
120
+ render(CropImage, { props: defaultProps });
121
+
122
+ // Initially loading
123
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
124
+
125
+ // Wait for the mock cropper to emit cropcomplete
126
+ await waitFor(() => {
127
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
128
+ }, { timeout: 100 });
129
+ });
130
+
131
+ it('slider controls zoom value', async () => {
132
+ render(CropImage, { props: defaultProps });
133
+ const slider = screen.getByLabelText('Zoom level');
134
+
135
+ await fireEvent.input(slider, { target: { value: '2' } });
136
+ expect(slider.value).toBe('2');
137
+ });
138
+
139
+ it('handles save button click when not loading', async () => {
140
+ const onSave = vi.fn();
141
+ render(CropImage, { props: { ...defaultProps, onSave } });
142
+
143
+ // Wait for loading to complete
144
+ await waitFor(() => {
145
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
146
+ }, { timeout: 200 });
147
+
148
+ // Try clicking save button
149
+ const saveButton = screen.getByText('Save');
150
+ await fireEvent.click(saveButton);
151
+ });
152
+
153
+ it('disables save button while loading', () => {
154
+ render(CropImage, { props: defaultProps });
155
+ const saveButton = screen.getByText('Save');
156
+ // Save button should be disabled during loading
157
+ expect(saveButton.closest('button')).toBeDefined();
158
+ });
159
+
160
+ it('resets state when modal reopens with new image', async () => {
161
+ const { rerender } = render(CropImage, { props: defaultProps });
162
+
163
+ // Change imageSrc to trigger reset
164
+ await rerender({
165
+ showModal: true,
166
+ imageSrc: 'data:image/png;base64,differentImage',
167
+ onSave: vi.fn(),
168
+ onCancel: vi.fn(),
169
+ });
170
+
171
+ expect(screen.getByText('Crop image')).toBeInTheDocument();
172
+ });
173
+
174
+ it('updates zoom progress based on zoom value', async () => {
175
+ render(CropImage, { props: defaultProps });
176
+ const slider = screen.getByLabelText('Zoom level');
177
+
178
+ // Set zoom to middle value
179
+ await fireEvent.input(slider, { target: { value: '2' } });
180
+ expect(slider.value).toBe('2');
181
+
182
+ // Set zoom to max
183
+ await fireEvent.input(slider, { target: { value: '3' } });
184
+ expect(slider.value).toBe('3');
185
+ });
186
+
187
+ it('zoom value stays within bounds', async () => {
188
+ render(CropImage, { props: defaultProps });
189
+ const slider = screen.getByLabelText('Zoom level');
190
+ const zoomOutBtn = screen.getByLabelText('Zoom out');
191
+ const zoomInBtn = screen.getByLabelText('Zoom in');
192
+
193
+ // Spam zoom out - should not go below 1
194
+ for (let i = 0; i < 10; i++) {
195
+ await fireEvent.click(zoomOutBtn);
196
+ }
197
+ expect(parseFloat(slider.value)).toBe(1);
198
+
199
+ // Set to max and spam zoom in
200
+ await fireEvent.input(slider, { target: { value: '3' } });
201
+ for (let i = 0; i < 10; i++) {
202
+ await fireEvent.click(zoomInBtn);
203
+ }
204
+ expect(parseFloat(slider.value)).toBeLessThanOrEqual(3);
205
+ });
206
+
207
+ it('renders cropper component with correct props', async () => {
208
+ const { container } = render(CropImage, { props: defaultProps });
209
+
210
+ // Wait for render
211
+ await waitFor(() => {
212
+ expect(container.querySelector('.cropper-container')).toBeInTheDocument();
213
+ });
214
+ });
215
+ });
216
+ });