v-uixy 1.3.1 → 1.5.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 (64) hide show
  1. package/package.json +1 -1
  2. package/templates/assets/css/main.css +4 -0
  3. package/templates/components/Alert/Alert.types.ts +4 -0
  4. package/templates/components/Alert/AlertItem.component.vue +2 -4
  5. package/templates/components/Alert/index.ts +6 -1
  6. package/templates/components/Avatar/Avatar.component.vue +7 -3
  7. package/templates/components/Avatar/Avatar.styles.ts +3 -3
  8. package/templates/components/Breadcrumb/Breadcrumb.component.vue +116 -0
  9. package/templates/components/Breadcrumb/Breadcrumb.styles.ts +40 -0
  10. package/templates/components/Breadcrumb/Breadcrumb.types.ts +30 -0
  11. package/templates/components/Breadcrumb/index.ts +17 -0
  12. package/templates/components/Button/Button.styles.ts +3 -3
  13. package/templates/components/Calendar/Calendar.component.vue +76 -17
  14. package/templates/components/Calendar/Calendar.styles.ts +15 -3
  15. package/templates/components/Calendar/Calendar.types.ts +9 -0
  16. package/templates/components/Chart/Chart.component.vue +308 -0
  17. package/templates/components/Chart/Chart.types.ts +18 -0
  18. package/templates/components/Chart/index.ts +13 -0
  19. package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
  20. package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
  21. package/templates/components/Command/Command.component.vue +6 -6
  22. package/templates/components/Command/Command.types.ts +1 -0
  23. package/templates/components/DatePicker/DatePicker.component.vue +219 -0
  24. package/templates/components/DatePicker/DatePicker.styles.ts +74 -0
  25. package/templates/components/DatePicker/DatePicker.types.ts +18 -0
  26. package/templates/components/DatePicker/DatePicker.utils.ts +46 -0
  27. package/templates/components/DatePicker/TimeWheel.vue +145 -0
  28. package/templates/components/DatePicker/TimeWheelColumn.vue +147 -0
  29. package/templates/components/DatePicker/index.ts +11 -0
  30. package/templates/components/DateRangePicker/DateRangePicker.component.vue +304 -0
  31. package/templates/components/DateRangePicker/DateRangePicker.types.ts +23 -0
  32. package/templates/components/DateRangePicker/index.ts +11 -0
  33. package/templates/components/FileUpload/FileUpload.component.vue +306 -0
  34. package/templates/components/FileUpload/FileUpload.styles.ts +74 -0
  35. package/templates/components/FileUpload/FileUpload.types.ts +31 -0
  36. package/templates/components/FileUpload/index.ts +15 -0
  37. package/templates/components/Input/Input.component.vue +27 -27
  38. package/templates/components/Input/Input.styles.ts +5 -5
  39. package/templates/components/Input/Input.types.ts +1 -0
  40. package/templates/components/InputOTP/InputOTP.component.vue +4 -2
  41. package/templates/components/InputOTP/InputOTP.types.ts +1 -0
  42. package/templates/components/Modal/Modal.component.vue +28 -4
  43. package/templates/components/Modal/Modal.types.ts +3 -1
  44. package/templates/components/Popover/Popover.component.vue +33 -2
  45. package/templates/components/Popover/Popover.types.ts +1 -0
  46. package/templates/components/SegmentedControl/SegmentedControl.component.vue +85 -0
  47. package/templates/components/SegmentedControl/SegmentedControl.styles.ts +55 -0
  48. package/templates/components/SegmentedControl/SegmentedControl.types.ts +15 -0
  49. package/templates/components/SegmentedControl/index.ts +11 -0
  50. package/templates/components/Select/Select.component.vue +253 -108
  51. package/templates/components/Select/Select.styles.ts +6 -6
  52. package/templates/components/Select/Select.types.ts +12 -0
  53. package/templates/components/Select/SelectTreeItem.vue +154 -0
  54. package/templates/components/Select/composables/useSelect.composable.ts +260 -42
  55. package/templates/components/Tabs/Tabs.component.vue +11 -2
  56. package/templates/components/Tabs/Tabs.types.ts +1 -0
  57. package/templates/components/Textarea/Textarea.component.vue +23 -32
  58. package/templates/components/Textarea/Textarea.styles.ts +4 -4
  59. package/templates/components/Textarea/Textarea.types.ts +1 -0
  60. package/templates/components/TimePicker/TimePicker.component.vue +21 -18
  61. package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
  62. package/templates/components/TimePicker/TimePicker.types.ts +1 -0
  63. package/templates/components.json +37 -1
  64. package/templates/composables/usePosition.ts +17 -2
@@ -0,0 +1,304 @@
1
+ <template>
2
+ <div class="flex w-full flex-col gap-1">
3
+ <label v-if="props.label" :class="labelStyles({ status })">
4
+ {{ props.label }}
5
+ </label>
6
+
7
+ <div
8
+ ref="triggerRef"
9
+ role="button"
10
+ :tabindex="props.disabled ? -1 : 0"
11
+ :aria-expanded="active"
12
+ :class="triggerStyles({ status, open: active })"
13
+ @click="onTriggerClick"
14
+ @keydown.enter.prevent="onTriggerClick"
15
+ @keydown.space.prevent="onTriggerClick"
16
+ >
17
+ <uixy-icon
18
+ :name="props.icon ?? 'calendar'"
19
+ :class="triggerIconStyles({ disabled: props.disabled })"
20
+ />
21
+ <span
22
+ :class="triggerValueStyles({ placeholder: !hasValue, disabled: props.disabled })"
23
+ >
24
+ {{ displayValue }}
25
+ </span>
26
+ <uixy-icon
27
+ v-if="hasValue && !props.disabled"
28
+ name="close"
29
+ class="h-4 w-4 shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white"
30
+ @click.stop="clear"
31
+ />
32
+ </div>
33
+
34
+ <div v-if="!props.hideHelper && helperText" class="h-4">
35
+ <p :class="helperStyles({ status })">{{ helperText }}</p>
36
+ </div>
37
+
38
+ <teleport to="body">
39
+ <animate-presence>
40
+ <motion.div
41
+ v-if="active"
42
+ :initial="{ opacity: 0 }"
43
+ :animate="{ opacity: 1 }"
44
+ :exit="{ opacity: 0 }"
45
+ :transition="{ duration: 0.15, ease: 'easeInOut' }"
46
+ :style="positionStyles"
47
+ >
48
+ <div ref="refElement">
49
+ <motion.div
50
+ :initial="{ y: 4, scale: 0.96, opacity: 0 }"
51
+ :animate="{ y: 0, scale: 1, opacity: 1 }"
52
+ :exit="{ opacity: 0, scale: 0.96 }"
53
+ :transition="{ duration: 0.15, ease: 'easeInOut' }"
54
+ :class="panelStyles()"
55
+ >
56
+ <div class="flex items-stretch gap-3">
57
+ <uixy-calendar
58
+ :year="viewYear"
59
+ :month="viewMonth"
60
+ :start-of-week="props.startOfWeek"
61
+ :is-date-disabled="props.isDateDisabled"
62
+ :range-start="model.start"
63
+ :range-end="model.end"
64
+ :range-hover="rangeHover"
65
+ @update:model-value="onDayPick"
66
+ @day-hover="onDayHover"
67
+ @day-leave="rangeHover = null"
68
+ />
69
+
70
+ <div v-if="props.withTime" class="flex gap-3">
71
+ <div class="flex flex-col items-center gap-1">
72
+ <span :class="sectionLabelStyles()">Start</span>
73
+ <uixy-time-wheel
74
+ v-model="startTime"
75
+ :hour-cycle="24"
76
+ :with-seconds="props.withSeconds"
77
+ :disabled="!model.start"
78
+ />
79
+ </div>
80
+ <div class="flex flex-col items-center gap-1">
81
+ <span :class="sectionLabelStyles()">End</span>
82
+ <uixy-time-wheel
83
+ v-model="endTime"
84
+ :hour-cycle="24"
85
+ :with-seconds="props.withSeconds"
86
+ :disabled="!model.end"
87
+ />
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div :class="footerStyles()">
93
+ <uixy-button variant="tertiary" size="sm" @click="clear">
94
+ Clear
95
+ </uixy-button>
96
+ <uixy-button variant="primary" size="sm" @click="handleLeave">
97
+ Done
98
+ </uixy-button>
99
+ </div>
100
+ </motion.div>
101
+ </div>
102
+ </motion.div>
103
+ </animate-presence>
104
+ </teleport>
105
+ </div>
106
+ </template>
107
+
108
+ <script setup lang="ts">
109
+ import { AnimatePresence, motion } from "motion-v";
110
+ import { usePosition } from "~/composables";
111
+ import { UixyIcon } from "../Icon";
112
+ import { UixyButton } from "../Button";
113
+ import { UixyCalendar } from "../Calendar";
114
+ import { UixyTimeWheel } from "../DatePicker";
115
+ import type {
116
+ UixyDateRange,
117
+ UixyDateRangePickerProps,
118
+ } from "./DateRangePicker.types";
119
+ import {
120
+ triggerStyles,
121
+ triggerValueStyles,
122
+ triggerIconStyles,
123
+ labelStyles,
124
+ helperStyles,
125
+ panelStyles,
126
+ sectionLabelStyles,
127
+ footerStyles,
128
+ } from "../DatePicker/DatePicker.styles";
129
+ import {
130
+ formatDate,
131
+ timePartsOf,
132
+ withTimeParts,
133
+ startOfDay,
134
+ type UixyTimeParts,
135
+ } from "../DatePicker/DatePicker.utils";
136
+
137
+ const props = defineProps<UixyDateRangePickerProps>();
138
+
139
+ const model = defineModel<UixyDateRange>({
140
+ default: () => ({ start: null, end: null }),
141
+ });
142
+
143
+ const {
144
+ active,
145
+ styles: positionStyles,
146
+ refElement,
147
+ handleOpen,
148
+ handleLeave,
149
+ } = usePosition({ direction: props.direction ?? "bottom" });
150
+
151
+ const triggerRef = ref<HTMLElement>();
152
+
153
+ const rangeHover = ref<Date | null>(null);
154
+
155
+ const draftStartTime = ref<UixyTimeParts>(timePartsOf(model.value.start));
156
+
157
+ const draftEndTime = ref<UixyTimeParts>(timePartsOf(model.value.end));
158
+
159
+ const status = computed(() => {
160
+ if (props.disabled) return "disabled";
161
+
162
+ return props.status ?? "default";
163
+ });
164
+
165
+ const helperText = computed(() => {
166
+ if (status.value === "error") return props.errorText ?? "";
167
+
168
+ return props.helperText ?? "";
169
+ });
170
+
171
+ const hasValue = computed(() => !!model.value.start || !!model.value.end);
172
+
173
+ const formatBound = (date: Date | null) =>
174
+ formatDate(date, {
175
+ withTime: props.withTime,
176
+ withSeconds: props.withSeconds,
177
+ });
178
+
179
+ const displayValue = computed(() => {
180
+ const { start, end } = model.value;
181
+
182
+ if (start && end) return `${formatBound(start)} – ${formatBound(end)}`;
183
+ if (start) return `${formatBound(start)} – …`;
184
+
185
+ return props.placeholder ?? "Select range";
186
+ });
187
+
188
+ const viewBase = computed(
189
+ () => model.value.start ?? model.value.end ?? new Date(),
190
+ );
191
+
192
+ const viewYear = computed(() => viewBase.value.getFullYear());
193
+
194
+ const viewMonth = computed(() => viewBase.value.getMonth());
195
+
196
+ const startTime = computed<UixyTimeParts>({
197
+ get: () =>
198
+ model.value.start ? timePartsOf(model.value.start) : draftStartTime.value,
199
+ set: (parts) => {
200
+ draftStartTime.value = parts;
201
+
202
+ if (!model.value.start) return;
203
+
204
+ model.value = {
205
+ ...model.value,
206
+ start: withTimeParts(model.value.start, parts),
207
+ };
208
+ },
209
+ });
210
+
211
+ const endTime = computed<UixyTimeParts>({
212
+ get: () =>
213
+ model.value.end ? timePartsOf(model.value.end) : draftEndTime.value,
214
+ set: (parts) => {
215
+ draftEndTime.value = parts;
216
+
217
+ if (!model.value.end) return;
218
+
219
+ model.value = {
220
+ ...model.value,
221
+ end: withTimeParts(model.value.end, parts),
222
+ };
223
+ },
224
+ });
225
+
226
+ const onDayPick = (date: Date | null | undefined) => {
227
+ if (!date) return;
228
+
229
+ const { start, end } = model.value;
230
+
231
+ if (!start || end) {
232
+ model.value = {
233
+ start: withTimeParts(date, draftStartTime.value),
234
+ end: null,
235
+ };
236
+ return;
237
+ }
238
+
239
+ const dateIsEarlier = startOfDay(date) <= startOfDay(start);
240
+ const earlier = dateIsEarlier ? date : start;
241
+ const later = dateIsEarlier ? start : date;
242
+
243
+ model.value = {
244
+ start: withTimeParts(earlier, draftStartTime.value),
245
+ end: withTimeParts(later, draftEndTime.value),
246
+ };
247
+
248
+ rangeHover.value = null;
249
+
250
+ if (!props.withTime && props.closeOnSelect) handleLeave();
251
+ };
252
+
253
+ const onDayHover = (date: Date) => {
254
+ if (model.value.start && !model.value.end) rangeHover.value = date;
255
+ };
256
+
257
+ const onTriggerClick = (event: MouseEvent | KeyboardEvent) => {
258
+ if (props.disabled) return;
259
+
260
+ handleOpen(event as MouseEvent);
261
+ };
262
+
263
+ const clear = () => {
264
+ model.value = { start: null, end: null };
265
+ rangeHover.value = null;
266
+
267
+ draftStartTime.value = { hour: 0, minute: 0, second: 0 };
268
+ draftEndTime.value = { hour: 0, minute: 0, second: 0 };
269
+ };
270
+
271
+ watch(active, (isActive) => {
272
+ if (isActive) {
273
+ draftStartTime.value = timePartsOf(model.value.start);
274
+ draftEndTime.value = timePartsOf(model.value.end);
275
+ } else {
276
+ rangeHover.value = null;
277
+ }
278
+ });
279
+
280
+ const onDocClick = (event: MouseEvent) => {
281
+ if (!active.value) return;
282
+
283
+ const target = event.target as Node;
284
+
285
+ const clickedInside =
286
+ refElement.value?.contains(target) || triggerRef.value?.contains(target);
287
+
288
+ if (!clickedInside) handleLeave();
289
+ };
290
+
291
+ const onKeyDown = (event: KeyboardEvent) => {
292
+ if (active.value && event.key === "Escape") handleLeave();
293
+ };
294
+
295
+ onMounted(() => {
296
+ window.addEventListener("click", onDocClick);
297
+ window.addEventListener("keydown", onKeyDown);
298
+ });
299
+
300
+ onUnmounted(() => {
301
+ window.removeEventListener("click", onDocClick);
302
+ window.removeEventListener("keydown", onKeyDown);
303
+ });
304
+ </script>
@@ -0,0 +1,23 @@
1
+ import type { IconName } from "~/types/icons";
2
+
3
+ export interface UixyDateRange {
4
+ start: Date | null;
5
+ end: Date | null;
6
+ }
7
+
8
+ export interface UixyDateRangePickerProps {
9
+ placeholder?: string;
10
+ label?: string;
11
+ helperText?: string;
12
+ errorText?: string;
13
+ status?: "default" | "error";
14
+ disabled?: boolean;
15
+ icon?: IconName;
16
+ withTime?: boolean;
17
+ withSeconds?: boolean;
18
+ startOfWeek?: "Sunday" | "Monday";
19
+ isDateDisabled?: (date: Date) => boolean;
20
+ direction?: "top" | "bottom";
21
+ hideHelper?: boolean;
22
+ closeOnSelect?: boolean;
23
+ }
@@ -0,0 +1,11 @@
1
+ import UixyDateRangePicker from "./DateRangePicker.component.vue";
2
+ import {
3
+ type UixyDateRangePickerProps,
4
+ type UixyDateRange,
5
+ } from "./DateRangePicker.types";
6
+
7
+ export {
8
+ UixyDateRangePicker,
9
+ type UixyDateRangePickerProps,
10
+ type UixyDateRange,
11
+ };
@@ -0,0 +1,306 @@
1
+ <template>
2
+ <div class="flex w-full flex-col gap-2">
3
+ <input
4
+ ref="inputRef"
5
+ type="file"
6
+ class="hidden"
7
+ :accept="props.accept"
8
+ :multiple="props.multiple"
9
+ :disabled="props.disabled"
10
+ @change="onInputChange"
11
+ />
12
+
13
+ <div
14
+ v-if="props.variant === 'button'"
15
+ class="flex flex-wrap items-center gap-3"
16
+ >
17
+ <uixy-button
18
+ :variant="props.buttonVariant ?? 'secondary'"
19
+ :size="props.buttonSize ?? 'md'"
20
+ :icon="props.icon ?? 'upload'"
21
+ :disabled="props.disabled"
22
+ type="button"
23
+ @click="open"
24
+ >
25
+ {{ props.label ?? "Upload file" }}
26
+ </uixy-button>
27
+ <span v-if="props.hint" :class="dropzoneHintStyles()">{{
28
+ props.hint
29
+ }}</span>
30
+ </div>
31
+
32
+ <div
33
+ v-else
34
+ role="button"
35
+ :tabindex="props.disabled ? -1 : 0"
36
+ :aria-disabled="props.disabled || undefined"
37
+ :class="dropzoneStyles({ state })"
38
+ @click="open"
39
+ @keydown.enter.prevent="open"
40
+ @keydown.space.prevent="open"
41
+ @dragenter.prevent="onDragEnter"
42
+ @dragover.prevent
43
+ @dragleave.prevent="onDragLeave"
44
+ @drop.prevent="onDrop"
45
+ >
46
+ <uixy-icon
47
+ :name="props.icon ?? 'cloud-upload'"
48
+ :class="dropzoneIconStyles({ state })"
49
+ />
50
+
51
+ <div class="flex flex-col gap-0.5">
52
+ <span :class="dropzoneLabelStyles({ state })">
53
+ {{ props.label ?? "Click to upload or drag and drop" }}
54
+ </span>
55
+ <span v-if="props.hint" :class="dropzoneHintStyles()">{{
56
+ props.hint
57
+ }}</span>
58
+ </div>
59
+ </div>
60
+
61
+ <div v-if="!props.hideList && model.length" :class="fileListStyles()">
62
+ <div
63
+ v-for="(file, index) in model"
64
+ :key="fileKey(file)"
65
+ :class="fileRowStyles()"
66
+ >
67
+ <uixy-icon :name="fileIconFor(file)" :class="fileIconStyles()" />
68
+ <span :class="fileNameStyles()">{{ file.name }}</span>
69
+ <span :class="fileSizeStyles()">{{ formatSize(file.size) }}</span>
70
+ <uixy-icon-button
71
+ icon="close"
72
+ variant="tertiary"
73
+ size="sm"
74
+ :disabled="props.disabled"
75
+ aria-label="Remove file"
76
+ @click="removeFile(index)"
77
+ />
78
+ </div>
79
+ </div>
80
+
81
+ <div v-if="text" class="h-4">
82
+ <animate-presence mode="wait" :initial="false">
83
+ <motion.div
84
+ :key="status"
85
+ :initial="{ opacity: 0, y: '-4px' }"
86
+ :animate="{ opacity: 1, y: 0 }"
87
+ :exit="{ opacity: 0, y: '-4px' }"
88
+ :transition="{ duration: 0.125 }"
89
+ >
90
+ <p :class="helperStyles({ status })">{{ text }}</p>
91
+ </motion.div>
92
+ </animate-presence>
93
+ </div>
94
+ </div>
95
+ </template>
96
+
97
+ <script setup lang="ts">
98
+ import { UixyIcon } from "../Icon";
99
+ import { UixyButton } from "../Button";
100
+ import { UixyIconButton } from "../IconButton";
101
+ import { AnimatePresence, motion } from "motion-v";
102
+ import type { IconName } from "~/types/icons";
103
+ import type {
104
+ UixyFileUploadProps,
105
+ UixyFileUploadEmits,
106
+ UixyFileRejection,
107
+ } from "./FileUpload.types";
108
+ import {
109
+ dropzoneStyles,
110
+ dropzoneIconStyles,
111
+ dropzoneLabelStyles,
112
+ dropzoneHintStyles,
113
+ fileListStyles,
114
+ fileRowStyles,
115
+ fileIconStyles,
116
+ fileNameStyles,
117
+ fileSizeStyles,
118
+ helperStyles,
119
+ } from "./FileUpload.styles";
120
+
121
+ const props = defineProps<UixyFileUploadProps>();
122
+
123
+ const emits = defineEmits<UixyFileUploadEmits>();
124
+
125
+ const model = defineModel<File[]>({ default: () => [] });
126
+
127
+ const inputRef = ref<HTMLInputElement>();
128
+
129
+ const dragDepth = ref(0);
130
+
131
+ const lastError = ref<string | null>(null);
132
+
133
+ const isDragging = computed(() => dragDepth.value > 0);
134
+
135
+ const status = computed(() => {
136
+ if (props.disabled) return "disabled";
137
+
138
+ if (props.status === "error" || lastError.value) return "error";
139
+
140
+ return "default";
141
+ });
142
+
143
+ const state = computed(() => {
144
+ if (props.disabled) return "disabled";
145
+
146
+ if (isDragging.value) return "dragging";
147
+
148
+ if (props.status === "error" || lastError.value) return "error";
149
+
150
+ return "default";
151
+ });
152
+
153
+ const text = computed(() => {
154
+ if (status.value === "error")
155
+ return props.errorText ?? lastError.value ?? "";
156
+
157
+ if (status.value === "default") return props.helperText ?? "";
158
+
159
+ return "";
160
+ });
161
+
162
+ const fileIconFor = (file: File): IconName => {
163
+ if (file.type.startsWith("image/")) return "image";
164
+
165
+ return "file";
166
+ };
167
+
168
+ const fileKey = (file: File) =>
169
+ `${file.name}-${file.size}-${file.lastModified}`;
170
+
171
+ const formatSize = (bytes: number) => {
172
+ if (bytes === 0) return "0 B";
173
+
174
+ const units = ["B", "KB", "MB", "GB"];
175
+ const i = Math.min(
176
+ units.length - 1,
177
+ Math.floor(Math.log(bytes) / Math.log(1024)),
178
+ );
179
+
180
+ const value = bytes / Math.pow(1024, i);
181
+
182
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[i]}`;
183
+ };
184
+
185
+ const matchesAccept = (file: File) => {
186
+ const accept = props.accept?.trim();
187
+ if (!accept) return true;
188
+
189
+ const name = file.name.toLowerCase();
190
+ const type = file.type.toLowerCase();
191
+
192
+ return accept
193
+ .split(",")
194
+ .map((token) => token.trim().toLowerCase())
195
+ .filter(Boolean)
196
+ .some((token) => {
197
+ if (token.startsWith(".")) return name.endsWith(token);
198
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
199
+ return type === token;
200
+ });
201
+ };
202
+
203
+ const rejectionMessage = (rejections: UixyFileRejection[]) => {
204
+ const reasons = new Set(rejections.map((r) => r.reason));
205
+ const parts: string[] = [];
206
+
207
+ if (reasons.has("type")) parts.push("unsupported type");
208
+ if (reasons.has("size") && props.maxSize)
209
+ parts.push(`larger than ${formatSize(props.maxSize)}`);
210
+ if (reasons.has("count")) parts.push("over the file limit");
211
+
212
+ const count = rejections.length;
213
+
214
+ return `${count} file${count > 1 ? "s" : ""} rejected${
215
+ parts.length ? ` — ${parts.join(", ")}` : ""
216
+ }.`;
217
+ };
218
+
219
+ const open = () => {
220
+ if (props.disabled) return;
221
+
222
+ inputRef.value?.click();
223
+ };
224
+
225
+ const handleFiles = (fileList: FileList | null) => {
226
+ if (props.disabled || !fileList) return;
227
+
228
+ const limit = props.multiple ? props.maxFiles : 1;
229
+ const current = props.multiple ? model.value.slice() : [];
230
+ const accepted: File[] = [];
231
+ const rejections: UixyFileRejection[] = [];
232
+
233
+ for (const file of Array.from(fileList)) {
234
+ if (!matchesAccept(file)) {
235
+ rejections.push({ file, reason: "type" });
236
+ } else if (props.maxSize !== undefined && file.size > props.maxSize) {
237
+ rejections.push({ file, reason: "size" });
238
+ } else if (
239
+ limit !== undefined &&
240
+ current.length + accepted.length >= limit
241
+ ) {
242
+ rejections.push({ file, reason: "count" });
243
+ } else {
244
+ accepted.push(file);
245
+ }
246
+ }
247
+
248
+ if (accepted.length) {
249
+ const next = props.multiple ? [...current, ...accepted] : [accepted[0]!];
250
+ model.value = next;
251
+ lastError.value = null;
252
+ emits("change", next);
253
+ }
254
+
255
+ if (rejections.length) {
256
+ lastError.value = rejectionMessage(rejections);
257
+ emits("reject", rejections);
258
+ }
259
+
260
+ if (inputRef.value) inputRef.value.value = "";
261
+ };
262
+
263
+ const onInputChange = (event: Event) => {
264
+ handleFiles((event.target as HTMLInputElement).files);
265
+ };
266
+
267
+ const onDrop = (event: DragEvent) => {
268
+ dragDepth.value = 0;
269
+ if (props.disabled) return;
270
+
271
+ handleFiles(event.dataTransfer?.files ?? null);
272
+ };
273
+
274
+ const onDragEnter = () => {
275
+ if (props.disabled) return;
276
+
277
+ dragDepth.value += 1;
278
+ };
279
+
280
+ const onDragLeave = () => {
281
+ if (props.disabled) return;
282
+
283
+ dragDepth.value = Math.max(0, dragDepth.value - 1);
284
+ };
285
+
286
+ const removeFile = (index: number) => {
287
+ if (props.disabled) return;
288
+
289
+ const next = model.value.slice();
290
+ next.splice(index, 1);
291
+
292
+ model.value = next;
293
+ lastError.value = null;
294
+
295
+ emits("change", next);
296
+ };
297
+
298
+ const clear = () => {
299
+ model.value = [];
300
+ lastError.value = null;
301
+
302
+ emits("change", []);
303
+ };
304
+
305
+ defineExpose({ open, clear });
306
+ </script>