fluid-ui-svelte 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +16 -5
  3. package/dist/app.css +284 -0
  4. package/dist/base/Button.svelte +35 -0
  5. package/dist/base/Button.svelte.d.ts +14 -0
  6. package/dist/base/Container.svelte +34 -0
  7. package/dist/base/Container.svelte.d.ts +14 -0
  8. package/dist/base/Image.svelte +21 -0
  9. package/dist/base/Image.svelte.d.ts +8 -0
  10. package/dist/base/InputField.svelte +33 -0
  11. package/dist/base/InputField.svelte.d.ts +10 -0
  12. package/dist/base/Link.svelte +23 -0
  13. package/dist/base/Link.svelte.d.ts +10 -0
  14. package/dist/base/List.svelte +39 -0
  15. package/dist/base/List.svelte.d.ts +33 -0
  16. package/dist/base/Table.svelte +80 -0
  17. package/dist/base/Table.svelte.d.ts +42 -0
  18. package/dist/base/Text.svelte +45 -0
  19. package/dist/base/Text.svelte.d.ts +11 -0
  20. package/dist/base/index.d.ts +8 -0
  21. package/dist/base/index.js +8 -0
  22. package/dist/components/Accordion.svelte +44 -1
  23. package/dist/components/Accordion.svelte.d.ts +14 -25
  24. package/dist/components/Calendar.svelte +62 -0
  25. package/dist/components/Calendar.svelte.d.ts +12 -25
  26. package/dist/components/Carousel.svelte +114 -0
  27. package/dist/components/Carousel.svelte.d.ts +37 -0
  28. package/dist/components/CodeBlock.svelte +33 -0
  29. package/dist/components/CodeBlock.svelte.d.ts +9 -0
  30. package/dist/components/Drawer.svelte +77 -22
  31. package/dist/components/Drawer.svelte.d.ts +21 -6
  32. package/dist/components/{Badge.svelte.d.ts → ImageCrop.svelte.d.ts} +3 -3
  33. package/dist/components/Page.svelte +26 -0
  34. package/dist/components/Page.svelte.d.ts +11 -0
  35. package/dist/components/Switch.svelte +22 -20
  36. package/dist/components/Switch.svelte.d.ts +9 -4
  37. package/dist/components/index.d.ts +8 -0
  38. package/dist/components/index.js +8 -0
  39. package/dist/index.d.ts +2 -33
  40. package/dist/index.js +2 -36
  41. package/dist/{community/components/index.js → utilities/accesibility.js} +1 -0
  42. package/dist/utilities/applyCharacterFilter.d.ts +1 -0
  43. package/dist/utilities/applyCharacterFilter.js +6 -0
  44. package/dist/utilities/calendar.d.ts +10 -0
  45. package/dist/utilities/calendar.js +112 -0
  46. package/dist/utilities/carousel.d.ts +2 -0
  47. package/dist/utilities/carousel.js +27 -0
  48. package/dist/utilities/codeBlockContents.d.ts +15 -0
  49. package/dist/utilities/codeBlockContents.js +298 -0
  50. package/dist/utilities/imageCrop.d.ts +33 -0
  51. package/dist/utilities/imageCrop.js +217 -0
  52. package/dist/utilities/mergeClasses.d.ts +1 -0
  53. package/dist/utilities/mergeClasses.js +3 -0
  54. package/package.json +50 -33
  55. package/dist/community/prebuilt/index.d.ts +0 -1
  56. package/dist/community/prebuilt/index.js +0 -1
  57. package/dist/components/Breadcrumb.svelte +0 -1
  58. package/dist/components/Breadcrumb.svelte.d.ts +0 -26
  59. package/dist/components/Dropdown.svelte +0 -59
  60. package/dist/components/Dropdown.svelte.d.ts +0 -20
  61. package/dist/components/Dropzone.svelte +0 -1
  62. package/dist/components/Dropzone.svelte.d.ts +0 -26
  63. package/dist/components/InteractiveScrollArea.svelte +0 -1
  64. package/dist/components/InteractiveScrollArea.svelte.d.ts +0 -26
  65. package/dist/components/Pagination.svelte +0 -1
  66. package/dist/components/Pagination.svelte.d.ts +0 -26
  67. package/dist/components/Progress.svelte +0 -1
  68. package/dist/components/Progress.svelte.d.ts +0 -26
  69. package/dist/components/Tabs.svelte +0 -1
  70. package/dist/components/Tabs.svelte.d.ts +0 -26
  71. package/dist/components/Tooltip.svelte +0 -1
  72. package/dist/components/Tooltip.svelte.d.ts +0 -26
  73. package/dist/components/charts/BatteryChart.svelte +0 -0
  74. package/dist/components/charts/BatteryChart.svelte.d.ts +0 -26
  75. package/dist/components/charts/LineChart.svelte +0 -0
  76. package/dist/components/charts/LineChart.svelte.d.ts +0 -26
  77. package/dist/components/charts/PieChart.svelte +0 -0
  78. package/dist/components/charts/PieChart.svelte.d.ts +0 -26
  79. package/dist/components/notification/NotificationArea.svelte +0 -21
  80. package/dist/components/notification/NotificationArea.svelte.d.ts +0 -7
  81. package/dist/prebuilt/FormBuilder.svelte +0 -1
  82. package/dist/prebuilt/FormBuilder.svelte.d.ts +0 -26
  83. package/dist/prebuilt/document/Document.svelte +0 -1
  84. package/dist/prebuilt/document/Document.svelte.d.ts +0 -26
  85. package/dist/prebuilt/document/DocumentBlock.svelte +0 -1
  86. package/dist/prebuilt/document/DocumentBlock.svelte.d.ts +0 -26
  87. package/dist/prebuilt/document/DocumentPage.svelte +0 -1
  88. package/dist/prebuilt/document/DocumentPage.svelte.d.ts +0 -26
  89. package/dist/prebuilt/timeline/Timeline.svelte +0 -0
  90. package/dist/prebuilt/timeline/Timeline.svelte.d.ts +0 -26
  91. package/dist/prebuilt/timeline/TimelineItem.svelte +0 -0
  92. package/dist/prebuilt/timeline/TimelineItem.svelte.d.ts +0 -26
  93. package/dist/primitives/Button.svelte +0 -53
  94. package/dist/primitives/Button.svelte.d.ts +0 -13
  95. package/dist/primitives/Container.svelte +0 -51
  96. package/dist/primitives/Container.svelte.d.ts +0 -10
  97. package/dist/primitives/Dialog.svelte +0 -39
  98. package/dist/primitives/Dialog.svelte.d.ts +0 -10
  99. package/dist/primitives/Divider.svelte +0 -14
  100. package/dist/primitives/Divider.svelte.d.ts +0 -6
  101. package/dist/primitives/Form.svelte +0 -18
  102. package/dist/primitives/Form.svelte.d.ts +0 -8
  103. package/dist/primitives/Image.svelte +0 -57
  104. package/dist/primitives/Image.svelte.d.ts +0 -12
  105. package/dist/primitives/Input.svelte +0 -23
  106. package/dist/primitives/Input.svelte.d.ts +0 -9
  107. package/dist/primitives/Label.svelte +0 -18
  108. package/dist/primitives/Label.svelte.d.ts +0 -8
  109. package/dist/primitives/Link.svelte +0 -18
  110. package/dist/primitives/Link.svelte.d.ts +0 -8
  111. package/dist/primitives/Table.svelte +0 -18
  112. package/dist/primitives/Table.svelte.d.ts +0 -8
  113. package/dist/primitives/TableBody.svelte +0 -17
  114. package/dist/primitives/TableBody.svelte.d.ts +0 -7
  115. package/dist/primitives/TableData.svelte +0 -18
  116. package/dist/primitives/TableData.svelte.d.ts +0 -8
  117. package/dist/primitives/TableFooter.svelte +0 -17
  118. package/dist/primitives/TableFooter.svelte.d.ts +0 -7
  119. package/dist/primitives/TableHead.svelte +0 -18
  120. package/dist/primitives/TableHead.svelte.d.ts +0 -8
  121. package/dist/primitives/TableHeader.svelte +0 -17
  122. package/dist/primitives/TableHeader.svelte.d.ts +0 -7
  123. package/dist/primitives/TableRow.svelte +0 -18
  124. package/dist/primitives/TableRow.svelte.d.ts +0 -8
  125. package/dist/primitives/Text.svelte +0 -46
  126. package/dist/primitives/Text.svelte.d.ts +0 -8
  127. package/dist/primitives/TextArea.svelte +0 -15
  128. package/dist/primitives/TextArea.svelte.d.ts +0 -7
  129. package/dist/types.d.ts +0 -4
  130. package/dist/types.js +0 -1
  131. /package/dist/components/{Badge.svelte → ImageCrop.svelte} +0 -0
  132. /package/dist/{community/components/index.d.ts → utilities/accesibility.d.ts} +0 -0
@@ -0,0 +1,298 @@
1
+ // Parsing errors happen in code editor when imports and script tags used inside code block component's contents.
2
+ // To prevent this we import usage examples from here.
3
+ const gettingStartedAppCss = `// src/routes/+layout.svelte
4
+ import '../app.css';`;
5
+ const gettingStartedUsage = `<script>
6
+ // Import a base element for custom implementation
7
+ import { Button } from 'fluid-ui-svelte/base';
8
+
9
+ // Import a pre-built component
10
+ import { Accordion } from 'fluid-ui-svelte/components';
11
+ </script>
12
+
13
+ <Button onclick={handleClick}>Submit</Button>
14
+
15
+ <Accordion>
16
+ <!-- Accordion content -->
17
+ </Accordion>`;
18
+ const drawerBasicUsage = `<script>
19
+ import { Drawer } from 'fluid-ui-svelte/components';
20
+ import { Button, Text } from 'fluid-ui-svelte/base';
21
+
22
+ let isBasicDrawerOpen = $state(false);
23
+ </script>
24
+
25
+ <Button onclick={async () => isBasicDrawerOpen = true}>Open Drawer</Button>
26
+
27
+ <Drawer bind:isOpen={isBasicDrawerOpen} position="left">
28
+ <div class="flex flex-col gap-4">
29
+ <Text type="h2">Drawer Content</Text>
30
+ <Text>This is some content inside the drawer.</Text>
31
+ <Button onclick={async () => isBasicDrawerOpen = false}>Close</Button>
32
+ </div>
33
+ </Drawer>`;
34
+ const drawerPositions = `<script>
35
+ import { Drawer } from 'fluid-ui-svelte/components';
36
+ let left = $state(false);
37
+ let right = $state(false);
38
+ let top = $state(false);
39
+ let bottom = $state(false);
40
+ </script>
41
+
42
+ <Button onclick={async () => left = true}>Left</Button>
43
+ <Button onclick={async () => right = true}>Right</Button>
44
+ <Button onclick={async () => top = true}>Top</Button>
45
+ <Button onclick={async () => bottom = true}>Bottom</Button>
46
+
47
+ <Drawer bind:isOpen={left} position="left">Left Drawer</Drawer>
48
+ <Drawer bind:isOpen={right} position="right">Right Drawer</Drawer>
49
+ <Drawer bind:isOpen={top} position="top">Top Drawer</Drawer>
50
+ <Drawer bind:isOpen={bottom} position="bottom">Bottom Drawer</Drawer>`;
51
+ const drawerAnimated = `<script>
52
+ import { Drawer } from 'fluid-ui-svelte/components';
53
+ import { fly, fade } from 'svelte/transition';
54
+
55
+ let isAnimatedDrawerOpen = $state(false);
56
+ </script>
57
+
58
+ <Button onclick={async () => isAnimatedDrawerOpen = true}>Open Animated</Button>
59
+
60
+ <Drawer
61
+ bind:isOpen={isAnimatedDrawerOpen}
62
+ position="right"
63
+ transitionFn={fly}
64
+ transitionParams={{ x: 500, duration: 500 }}
65
+ backdropTransitionFn={fade}
66
+ >
67
+ Animated Content
68
+ </Drawer>`;
69
+ const drawerFlyAnimation = `<script>
70
+ import { Drawer } from 'fluid-ui-svelte/components';
71
+ import { fly } from 'svelte/transition';
72
+
73
+ let isFlyDrawerOpen = $state(false);
74
+ </script>
75
+
76
+ <Button onclick={async () => isFlyDrawerOpen = true}>Open Fly Drawer</Button>
77
+
78
+ <Drawer
79
+ bind:isOpen={isFlyDrawerOpen}
80
+ position="bottom"
81
+ transitionFn={fly}
82
+ transitionParams={{ y: 200, duration: 800 }}
83
+ >
84
+ Fly Content
85
+ </Drawer>`;
86
+ const calendarSingle = `<script>
87
+ import { Calendar } from 'fluid-ui-svelte/components';
88
+ import { Button, Container } from 'fluid-ui-svelte/base';
89
+
90
+ let currentDate = $state(new Date().toISOString());
91
+
92
+ const changeMonth = (increment: number) => {
93
+ const date = new Date(currentDate);
94
+ date.setMonth(date.getMonth() + increment);
95
+ currentDate = date.toISOString();
96
+ };
97
+ </script>
98
+
99
+ <Container class="flex flex-col gap-4">
100
+ <Container class="flex gap-2">
101
+ <Button onclick={() => changeMonth(-1)}>Prev</Button>
102
+ <Button onclick={() => changeMonth(1)}>Next</Button>
103
+ </Container>
104
+ <Calendar bind:currentDate componentId="calendar-single" />
105
+ </Container>`;
106
+ const calendarDual = `<script>
107
+ import { Calendar } from 'fluid-ui-svelte/components';
108
+ import { Button, Container } from 'fluid-ui-svelte/base';
109
+
110
+ let baseDate = $state(new Date().toISOString());
111
+
112
+ // Helper to get next month relative to base
113
+ const getOffsetDate = (offset: number) => {
114
+ const d = new Date(baseDate);
115
+ d.setMonth(d.getMonth() + offset);
116
+ return d.toISOString();
117
+ };
118
+
119
+ const changeMonth = (increment: number) => {
120
+ const d = new Date(baseDate);
121
+ d.setMonth(d.getMonth() + increment);
122
+ baseDate = d.toISOString();
123
+ };
124
+ </script>
125
+
126
+ <Container class="flex flex-col gap-4">
127
+ <Container class="flex gap-2">
128
+ <Button onclick={() => changeMonth(-1)}>Prev</Button>
129
+ <Button onclick={() => changeMonth(1)}>Next</Button>
130
+ </Container>
131
+ <Container class="flex gap-8 flex-wrap">
132
+ <Calendar currentDate={baseDate} componentId="cal-1" />
133
+ <Calendar currentDate={getOffsetDate(1)} componentId="cal-2" />
134
+ </Container>
135
+ </Container>`;
136
+ const calendarSixMonth = `<script>
137
+ import { Calendar } from 'fluid-ui-svelte/components';
138
+ import { Button, Container } from 'fluid-ui-svelte/base';
139
+
140
+ let baseDate = $state(new Date().toISOString());
141
+
142
+ const getOffsetDate = (offset: number) => {
143
+ const d = new Date(baseDate);
144
+ d.setMonth(d.getMonth() + offset);
145
+ return d.toISOString();
146
+ };
147
+
148
+ const changeMonth = (increment: number) => {
149
+ const d = new Date(baseDate);
150
+ d.setMonth(d.getMonth() + increment);
151
+ baseDate = d.toISOString();
152
+ };
153
+ </script>
154
+
155
+ <Container class="flex flex-col gap-4">
156
+ <Container class="flex gap-2">
157
+ <Button onclick={() => changeMonth(-1)}>Prev</Button>
158
+ <Button onclick={() => changeMonth(1)}>Next</Button>
159
+ </Container>
160
+ <Container class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
161
+ {#each Array(6) as _, i}
162
+ <Calendar currentDate={getOffsetDate(i)} componentId="cal-{i}" />
163
+ {/each}
164
+ </Container>
165
+ </Container>`;
166
+ const calendarRange = `<script>
167
+ import { Calendar } from 'fluid-ui-svelte/components';
168
+ import { Button, Container, Text } from 'fluid-ui-svelte/base';
169
+
170
+ let baseDate = $state(new Date().toISOString());
171
+ let startDate = $state<string>();
172
+ let endDate = $state<string>();
173
+
174
+ const getOffsetDate = (offset: number) => {
175
+ const d = new Date(baseDate);
176
+ d.setMonth(d.getMonth() + offset);
177
+ return d.toISOString();
178
+ };
179
+
180
+ const changeMonth = (inc: number) => {
181
+ const d = new Date(baseDate);
182
+ d.setMonth(d.getMonth() + inc);
183
+ baseDate = d.toISOString();
184
+ };
185
+ </script>
186
+
187
+ <Container class="flex flex-col gap-4">
188
+ <Container class="flex justify-between items-center">
189
+ <Container class="flex gap-2">
190
+ <Button onclick={() => changeMonth(-1)}>Prev</Button>
191
+ <Button onclick={() => changeMonth(1)}>Next</Button>
192
+ </Container>
193
+ <Text class="text-sm font-mono">
194
+ {startDate || '...'} to {endDate || '...'}
195
+ </Text>
196
+ </Container>
197
+
198
+ <Container class="flex gap-8 flex-wrap">
199
+ <Calendar
200
+ currentDate={baseDate}
201
+ bind:startDate
202
+ bind:endDate
203
+ componentId="cal-range-1"
204
+ />
205
+ <Calendar
206
+ currentDate={getOffsetDate(1)}
207
+ bind:startDate
208
+ bind:endDate
209
+ componentId="cal-range-2"
210
+ />
211
+ </Container>
212
+ </Container>`;
213
+ const calendarMulti = `<script>
214
+ import { Calendar } from 'fluid-ui-svelte/components';
215
+ import { Button, Container } from 'fluid-ui-svelte/base';
216
+
217
+ const multiCalendarState = $state({
218
+ currentDate: new Date().toISOString(),
219
+ startDate: '',
220
+ endDate: ''
221
+ });
222
+
223
+ const changeMonthMulti = (inc: number) => {
224
+ const d = new Date(multiCalendarState.currentDate);
225
+ d.setMonth(d.getMonth() + inc);
226
+ multiCalendarState.currentDate = d.toISOString();
227
+ };
228
+ </script>
229
+
230
+ <Container class="flex flex-col gap-4">
231
+ <Container class="flex gap-2">
232
+ <Button onclick={() => changeMonthMulti(-1)}>Prev</Button>
233
+ <Button onclick={() => changeMonthMulti(1)}>Next</Button>
234
+ </Container>
235
+
236
+ <Container class="flex gap-8 flex-wrap">
237
+ <Calendar
238
+ bind:currentDate={multiCalendarState.currentDate}
239
+ bind:startDate={multiCalendarState.startDate}
240
+ bind:endDate={multiCalendarState.endDate}
241
+ />
242
+ <Calendar
243
+ currentDate={new Date(new Date(multiCalendarState.currentDate).setMonth(new Date(multiCalendarState.currentDate).getMonth() + 1)).toISOString()}
244
+ bind:startDate={multiCalendarState.startDate}
245
+ bind:endDate={multiCalendarState.endDate}
246
+ />
247
+ </Container>
248
+ </Container>`;
249
+ const carouselSlide = `<script>
250
+ import { Carousel } from 'fluid-ui-svelte/components';
251
+ import { Image } from 'fluid-ui-svelte/base';
252
+
253
+ const items = [
254
+ { src: 'https://via.placeholder.com/800x400?text=Slide+1', alt: 'Slide 1' },
255
+ { src: 'https://via.placeholder.com/800x400?text=Slide+2', alt: 'Slide 2' },
256
+ { src: 'https://via.placeholder.com/800x400?text=Slide+3', alt: 'Slide 3' }
257
+ ];
258
+ </script>
259
+
260
+ <Carousel {items} type="slide" loop>
261
+ {#snippet children(item)}
262
+ <Image src={item.src} alt={item.alt} class="w-full h-64 object-cover" />
263
+ {/snippet}
264
+ </Carousel>`;
265
+ const carouselFade = `<script>
266
+ import { Carousel } from 'fluid-ui-svelte/components';
267
+ import { Text } from 'fluid-ui-svelte/base';
268
+
269
+ const quotes = [
270
+ { text: "Innovation is key.", author: "Tech CEO" },
271
+ { text: "Design is intelligence made visible.", author: "Designer" },
272
+ { text: "Simplicity is the ultimate sophistication.", author: "Da Vinci" }
273
+ ];
274
+ </script>
275
+
276
+ <Carousel {items: quotes} type="fade" autoplay autoplayInterval={4000}>
277
+ {#snippet children(quote)}
278
+ <div class="h-64 flex flex-col items-center justify-center bg-neutral-100 dark:bg-neutral-800 p-8 text-center">
279
+ <Text type="h3" class="text-2xl font-bold mb-4">"{quote.text}"</Text>
280
+ <Text class="text-neutral-500">- {quote.author}</Text>
281
+ </div>
282
+ {/snippet}
283
+ </Carousel>`;
284
+ export const codeBlockContents = {
285
+ gettingStartedAppCss,
286
+ gettingStartedUsage,
287
+ drawerBasicUsage,
288
+ drawerPositions,
289
+ drawerAnimated,
290
+ drawerFlyAnimation,
291
+ calendarSingle,
292
+ calendarDual,
293
+ calendarSixMonth,
294
+ calendarRange,
295
+ calendarMulti,
296
+ carouselSlide,
297
+ carouselFade
298
+ };
@@ -0,0 +1,33 @@
1
+ export declare function generateCropAreaState(shape?: 'circle' | 'rectangle', aspectRatio?: {
2
+ w: number;
3
+ h: number;
4
+ }): {
5
+ cropArea: {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ };
11
+ shape: "circle" | "rectangle";
12
+ aspectRatio: {
13
+ w: number;
14
+ h: number;
15
+ };
16
+ activeAction: string;
17
+ lastMousePosition: {
18
+ x: number;
19
+ y: number;
20
+ };
21
+ };
22
+ export declare function draw(canvasElement: HTMLCanvasElement, image: HTMLImageElement, cropAreaState: ReturnType<typeof generateCropAreaState>): void;
23
+ export declare function handleMouseDown(event: PointerEvent | MouseEvent, cropAreaState: ReturnType<typeof generateCropAreaState>, canvasElement?: HTMLCanvasElement): void;
24
+ export declare function handleMouseMove(event: PointerEvent | MouseEvent, cropAreaState: ReturnType<typeof generateCropAreaState>, canvasElement?: HTMLCanvasElement): void;
25
+ export declare function handleMouseUp(cropAreaState: ReturnType<typeof generateCropAreaState>): void;
26
+ /**
27
+ * Export the currently selected crop as a PNG Blob.
28
+ * - If shape === 'rectangle' exports the rectangular region.
29
+ * - If shape === 'circle' exports a square canvas with a circular alpha mask (transparent outside circle).
30
+ *
31
+ * Returns Promise<Blob | null>.
32
+ */
33
+ export declare function exportSelectionAsPNG(canvasElement: HTMLCanvasElement, image: HTMLImageElement, cropAreaState: ReturnType<typeof generateCropAreaState>): Promise<Blob | null>;
@@ -0,0 +1,217 @@
1
+ export function generateCropAreaState(shape = 'rectangle', aspectRatio = { w: 1, h: 1 }) {
2
+ return {
3
+ cropArea: { x: 0, y: 0, width: 0, height: 0 },
4
+ shape,
5
+ aspectRatio,
6
+ activeAction: 'idle',
7
+ lastMousePosition: { x: 0, y: 0 }
8
+ };
9
+ }
10
+ export function draw(canvasElement, image, cropAreaState) {
11
+ if (!image || !canvasElement)
12
+ return;
13
+ const context = canvasElement.getContext('2d');
14
+ // 1. Draw the base image
15
+ context.drawImage(image, 0, 0);
16
+ // 2. Draw the semi-transparent mask
17
+ context.fillStyle = 'rgba(0, 0, 0, 0.2)';
18
+ context.fillRect(0, 0, canvasElement.width, canvasElement.height);
19
+ // 3. Cut a "hole" for the crop area
20
+ drawCropArea(context, cropAreaState);
21
+ // 4. Draw the resize handles
22
+ drawHandles(context, cropAreaState);
23
+ }
24
+ function drawCropArea(context, cropAreaState) {
25
+ context.save(); // Save the current state
26
+ context.globalCompositeOperation = 'destination-out';
27
+ if (cropAreaState.shape === 'circle') {
28
+ const { x, y, width, height } = cropAreaState.cropArea;
29
+ // Use the smaller dimension so the circle always fits inside the bounding box
30
+ const radius = Math.min(width, height) / 2;
31
+ const centerX = x + width / 2;
32
+ const centerY = y + height / 2;
33
+ context.beginPath();
34
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
35
+ context.fill();
36
+ }
37
+ else {
38
+ // Default to square/rectangle
39
+ context.fillRect(cropAreaState.cropArea.x, cropAreaState.cropArea.y, cropAreaState.cropArea.width, cropAreaState.cropArea.height);
40
+ }
41
+ context.restore(); // Restore to the previous state (resets globalCompositeOperation)
42
+ }
43
+ const HANDLE_SIZE = 24;
44
+ function getHandles(cropAreaState) {
45
+ const { x, y, width, height } = cropAreaState.cropArea;
46
+ // If the crop shape is a circle, place handles around the actual circle (inscribed),
47
+ // with a small padding so they sit slightly outside the circle's edge.
48
+ if (cropAreaState.shape === 'circle') {
49
+ const centerX = x + width / 2;
50
+ const centerY = y + height / 2;
51
+ const radius = Math.min(width, height) / 2;
52
+ const padding = radius * 0.05; // 5% padding
53
+ const r = radius + padding;
54
+ const diagOffset = r / Math.sqrt(2); // 45° corner offset
55
+ return [
56
+ // corners (45° around circle)
57
+ { x: centerX - diagOffset, y: centerY - diagOffset, name: 'tl' },
58
+ { x: centerX + diagOffset, y: centerY - diagOffset, name: 'tr' },
59
+ { x: centerX + diagOffset, y: centerY + diagOffset, name: 'br' },
60
+ { x: centerX - diagOffset, y: centerY + diagOffset, name: 'bl' },
61
+ // sides (cardinal points on circle)
62
+ { x: centerX, y: centerY - r, name: 't' },
63
+ { x: centerX + r, y: centerY, name: 'r' },
64
+ { x: centerX, y: centerY + r, name: 'b' },
65
+ { x: centerX - r, y: centerY, name: 'l' }
66
+ ];
67
+ }
68
+ // Default rectangle handles (unchanged)
69
+ return [
70
+ // corners
71
+ { x: x, y: y, name: 'tl' }, // top-left
72
+ { x: x + width, y: y, name: 'tr' }, // top-right
73
+ { x: x + width, y: y + height, name: 'br' }, // bottom-right
74
+ { x: x, y: y + height, name: 'bl' }, // bottom-left
75
+ // sides
76
+ { x: x + width / 2, y: y, name: 't' }, // top
77
+ { x: x + width, y: y + height / 2, name: 'r' }, // right
78
+ { x: x + width / 2, y: y + height, name: 'b' }, // bottom
79
+ { x: x, y: y + height / 2, name: 'l' } // left
80
+ ];
81
+ }
82
+ function drawHandles(context, cropAreaState) {
83
+ context.fillStyle = 'white';
84
+ const handles = getHandles(cropAreaState);
85
+ for (const handle of handles) {
86
+ context.fillRect(handle.x - HANDLE_SIZE / 2, handle.y - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE);
87
+ }
88
+ }
89
+ /**
90
+ * Map a PointerEvent/MouseEvent to canvas coordinate space (accounts for CSS scaling).
91
+ */
92
+ function getCanvasCoords(event, canvas) {
93
+ // Use client coordinates and canvas bounding box to map to drawing buffer pixels
94
+ const rect = canvas.getBoundingClientRect();
95
+ const clientX = event.clientX ?? event.clientX;
96
+ const clientY = event.clientY ?? event.clientY;
97
+ const scaleX = canvas.width / rect.width;
98
+ const scaleY = canvas.height / rect.height;
99
+ return {
100
+ x: (clientX - rect.left) * scaleX,
101
+ y: (clientY - rect.top) * scaleY
102
+ };
103
+ }
104
+ function clampCropToCanvas(cropArea, canvas, shape = 'rectangle', minSize = 20) {
105
+ // Ensure positive sizes
106
+ cropArea.width = Math.max(minSize, cropArea.width);
107
+ cropArea.height = Math.max(minSize, cropArea.height);
108
+ if (shape === 'circle') {
109
+ // Force a square bounding box that stays centered on the current center.
110
+ const centerX = cropArea.x + cropArea.width / 2;
111
+ const centerY = cropArea.y + cropArea.height / 2;
112
+ const size = Math.max(minSize, Math.min(cropArea.width, cropArea.height));
113
+ cropArea.width = size;
114
+ cropArea.height = size;
115
+ cropArea.x = centerX - size / 2;
116
+ cropArea.y = centerY - size / 2;
117
+ }
118
+ // Keep position inside canvas
119
+ cropArea.x = Math.max(0, Math.min(cropArea.x, canvas.width - cropArea.width));
120
+ cropArea.y = Math.max(0, Math.min(cropArea.y, canvas.height - cropArea.height));
121
+ }
122
+ export function handleMouseDown(event, cropAreaState, canvasElement) {
123
+ if (!canvasElement)
124
+ return;
125
+ const { x: offsetX, y: offsetY } = getCanvasCoords(event, canvasElement);
126
+ const handles = getHandles(cropAreaState);
127
+ // Check if a handle was clicked (handles are in canvas coordinates)
128
+ for (const handle of handles) {
129
+ const handleX = handle.x - HANDLE_SIZE / 2;
130
+ const handleY = handle.y - HANDLE_SIZE / 2;
131
+ if (offsetX >= handleX &&
132
+ offsetX <= handleX + HANDLE_SIZE &&
133
+ offsetY >= handleY &&
134
+ offsetY <= handleY + HANDLE_SIZE) {
135
+ cropAreaState.activeAction = `resizing-${handle.name}`;
136
+ cropAreaState.lastMousePosition = { x: offsetX, y: offsetY };
137
+ return;
138
+ }
139
+ }
140
+ // If no handle was clicked, check if the main rectangle was clicked
141
+ const { x, y, width, height } = cropAreaState.cropArea;
142
+ if (offsetX >= x && offsetX <= x + width && offsetY >= y && offsetY <= y + height) {
143
+ cropAreaState.activeAction = 'moving';
144
+ cropAreaState.lastMousePosition = { x: offsetX, y: offsetY };
145
+ return;
146
+ }
147
+ }
148
+ export function handleMouseMove(event, cropAreaState, canvasElement) {
149
+ if (cropAreaState.activeAction === 'idle')
150
+ return;
151
+ if (!canvasElement)
152
+ return;
153
+ const { x: offsetX, y: offsetY } = getCanvasCoords(event, canvasElement);
154
+ const deltaX = offsetX - cropAreaState.lastMousePosition.x;
155
+ const deltaY = offsetY - cropAreaState.lastMousePosition.y;
156
+ const { cropArea } = cropAreaState;
157
+ if (cropAreaState.activeAction === 'moving') {
158
+ cropArea.x += deltaX;
159
+ cropArea.y += deltaY;
160
+ }
161
+ else if (cropAreaState.activeAction.startsWith('resizing-')) {
162
+ const handleName = cropAreaState.activeAction.split('-')[1];
163
+ if (handleName.includes('r'))
164
+ cropArea.width += deltaX;
165
+ if (handleName.includes('l')) {
166
+ cropArea.x += deltaX;
167
+ cropArea.width -= deltaX;
168
+ }
169
+ if (handleName.includes('b'))
170
+ cropArea.height += deltaY;
171
+ if (handleName.includes('t')) {
172
+ cropArea.y += deltaY;
173
+ cropArea.height -= deltaY;
174
+ }
175
+ }
176
+ // Prevent negative/too-small sizes and keep crop within canvas
177
+ // pass the current shape so a circle forces a square bounding box
178
+ clampCropToCanvas(cropArea, canvasElement, cropAreaState.shape, 20);
179
+ cropAreaState.lastMousePosition = { x: offsetX, y: offsetY };
180
+ }
181
+ export function handleMouseUp(cropAreaState) {
182
+ cropAreaState.activeAction = 'idle';
183
+ }
184
+ /**
185
+ * Export the currently selected crop as a PNG Blob.
186
+ * - If shape === 'rectangle' exports the rectangular region.
187
+ * - If shape === 'circle' exports a square canvas with a circular alpha mask (transparent outside circle).
188
+ *
189
+ * Returns Promise<Blob | null>.
190
+ */
191
+ export async function exportSelectionAsPNG(canvasElement, image, cropAreaState) {
192
+ if (!canvasElement || !image)
193
+ return null;
194
+ const { x, y, width, height } = cropAreaState.cropArea;
195
+ if (width <= 0 || height <= 0)
196
+ return null;
197
+ // Create an offscreen canvas sized to the crop bounding box (square for circle)
198
+ const outCanvas = document.createElement('canvas');
199
+ outCanvas.width = Math.round(width);
200
+ outCanvas.height = Math.round(height);
201
+ const ctx = outCanvas.getContext('2d');
202
+ if (!ctx)
203
+ return null;
204
+ // Draw the corresponding portion of the source image
205
+ // coords are in canvas/image pixel space because the component sets canvas size to image.naturalWidth/Height
206
+ ctx.drawImage(image, x, y, width, height, 0, 0, outCanvas.width, outCanvas.height);
207
+ if (cropAreaState.shape === 'circle') {
208
+ // Mask to a circle (destination-in keeps the circular area)
209
+ ctx.globalCompositeOperation = 'destination-in';
210
+ ctx.beginPath();
211
+ const r = Math.min(outCanvas.width, outCanvas.height) / 2;
212
+ ctx.arc(outCanvas.width / 2, outCanvas.height / 2, r, 0, Math.PI * 2);
213
+ ctx.fill();
214
+ }
215
+ // Return a PNG Blob
216
+ return await new Promise((resolve) => outCanvas.toBlob((b) => resolve(b), 'image/png'));
217
+ }
@@ -0,0 +1 @@
1
+ export declare function mergeClasses(classA: string, classB: string): string;
@@ -0,0 +1,3 @@
1
+ export function mergeClasses(classA, classB) {
2
+ return [...new Set([...classA.split(' '), ...classB.split(' ')])].join(' ').trim();
3
+ }
package/package.json CHANGED
@@ -1,27 +1,27 @@
1
1
  {
2
2
  "name": "fluid-ui-svelte",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "author": {
5
5
  "name": "Emre Ayaz",
6
- "email": "emreayaz@frostium.io",
7
- "url": "https://emreayaz.com"
6
+ "email": "ayazthemre@gmail.com"
8
7
  },
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/ayazemre/fluid-ui-svelte",
9
10
  "repository": {
10
- "url": "https://github.com/ayazemre/fluid-ui"
11
+ "url": "git+https://github.com/ayazemre/fluid-ui-svelte.git"
11
12
  },
12
13
  "scripts": {
13
14
  "dev": "vite dev",
14
- "build": "vite build && npm run package",
15
+ "build": "vite build && npm run prepack",
15
16
  "preview": "vite preview",
16
- "package": "svelte-kit sync && svelte-package && publint",
17
- "prepublishOnly": "npm run package",
17
+ "prepare": "svelte-kit sync || echo ''",
18
+ "prepack": "svelte-kit sync && svelte-package && publint",
18
19
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
19
20
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
20
21
  "format": "prettier --write .",
21
22
  "lint": "prettier --check . && eslint .",
22
23
  "test:unit": "vitest",
23
- "test": "npm run test:unit -- --run && npm run test:e2e",
24
- "test:e2e": "playwright test"
24
+ "test": "npm run test:unit -- --run"
25
25
  },
26
26
  "files": [
27
27
  "dist",
@@ -38,34 +38,51 @@
38
38
  ".": {
39
39
  "types": "./dist/index.d.ts",
40
40
  "svelte": "./dist/index.js"
41
+ },
42
+ "./base": {
43
+ "types": "./dist/base/index.d.ts",
44
+ "svelte": "./dist/base/index.js"
45
+ },
46
+ "./components": {
47
+ "types": "./dist/components/index.d.ts",
48
+ "svelte": "./dist/components/index.js"
41
49
  }
42
50
  },
43
51
  "peerDependencies": {
44
52
  "svelte": "^5.0.0"
45
53
  },
46
54
  "devDependencies": {
47
- "@playwright/test": "^1.45.3",
48
- "@sveltejs/adapter-auto": "^3.0.0",
49
- "@sveltejs/kit": "^2.0.0",
50
- "@sveltejs/package": "^2.0.0",
51
- "@sveltejs/vite-plugin-svelte": "^4.0.0",
52
- "@types/eslint": "^9.6.0",
53
- "eslint": "^9.7.0",
54
- "eslint-config-prettier": "^9.1.0",
55
- "eslint-plugin-svelte": "^2.36.0",
56
- "globals": "^15.0.0",
57
- "prettier": "^3.3.2",
58
- "prettier-plugin-svelte": "^3.2.6",
59
- "publint": "^0.2.0",
60
- "svelte": "^5.0.0",
61
- "svelte-check": "^4.0.0",
62
- "typescript": "^5.0.0",
63
- "typescript-eslint": "^8.0.0",
64
- "vite": "^5.0.11",
65
- "vitest": "^2.0.4"
55
+ "@eslint/compat": "^1.4.0",
56
+ "@eslint/js": "^9.38.0",
57
+ "@iconify/svelte": "^5.1.0",
58
+ "@sveltejs/adapter-auto": "^7.0.0",
59
+ "@sveltejs/adapter-node": "^5.4.0",
60
+ "@sveltejs/kit": "^2.47.1",
61
+ "@sveltejs/package": "^2.5.4",
62
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
63
+ "@tailwindcss/vite": "^4.1.14",
64
+ "@types/node": "^22",
65
+ "@vitest/browser": "^3.2.4",
66
+ "@vitest/ui": "^3.2.4",
67
+ "eslint": "^9.38.0",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "eslint-plugin-svelte": "^3.12.4",
70
+ "globals": "^16.4.0",
71
+ "playwright": "^1.56.1",
72
+ "prettier": "^3.6.2",
73
+ "prettier-plugin-svelte": "^3.4.0",
74
+ "prettier-plugin-tailwindcss": "^0.7.1",
75
+ "publint": "^0.3.14",
76
+ "svelte": "^5.41.0",
77
+ "svelte-check": "^4.3.3",
78
+ "tailwindcss": "^4.1.14",
79
+ "typescript": "^5.9.3",
80
+ "typescript-eslint": "^8.46.1",
81
+ "vite": "^7.1.10",
82
+ "vitest": "^3.2.4",
83
+ "vitest-browser-svelte": "^1.1.0"
66
84
  },
67
- "dependencies": {
68
- "@tailwindcss/vite": "^4.0.0-alpha.31",
69
- "tailwindcss": "^4.0.0-alpha.31"
70
- }
71
- }
85
+ "keywords": [
86
+ "svelte"
87
+ ]
88
+ }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- "use strict";
@@ -1 +0,0 @@
1
- <!-- TODO -->