windly-ui 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,30 +9,14 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
9
9
  type: BooleanConstructor;
10
10
  required: true;
11
11
  };
12
- title: {
12
+ width: {
13
13
  type: StringConstructor;
14
14
  default: string;
15
15
  };
16
- message: {
16
+ height: {
17
17
  type: StringConstructor;
18
18
  default: string;
19
19
  };
20
- confirmBtnText: {
21
- type: StringConstructor;
22
- default: string;
23
- };
24
- cancelBtnText: {
25
- type: StringConstructor;
26
- default: string;
27
- };
28
- showConfirmBtn: {
29
- type: BooleanConstructor;
30
- default: boolean;
31
- };
32
- showCloseIcon: {
33
- type: BooleanConstructor;
34
- default: boolean;
35
- };
36
20
  persistent: {
37
21
  type: BooleanConstructor;
38
22
  default: boolean;
@@ -40,40 +24,23 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
40
24
  position: {
41
25
  type: StringConstructor;
42
26
  default: string;
43
- validator: (val: unknown) => any;
27
+ validator: (value: unknown) => any;
44
28
  };
45
29
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
46
30
  "update:isOpen": (...args: any[]) => void;
47
- confirm: (...args: any[]) => void;
48
31
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
49
32
  isOpen: {
50
33
  type: BooleanConstructor;
51
34
  required: true;
52
35
  };
53
- title: {
36
+ width: {
54
37
  type: StringConstructor;
55
38
  default: string;
56
39
  };
57
- message: {
40
+ height: {
58
41
  type: StringConstructor;
59
42
  default: string;
60
43
  };
61
- confirmBtnText: {
62
- type: StringConstructor;
63
- default: string;
64
- };
65
- cancelBtnText: {
66
- type: StringConstructor;
67
- default: string;
68
- };
69
- showConfirmBtn: {
70
- type: BooleanConstructor;
71
- default: boolean;
72
- };
73
- showCloseIcon: {
74
- type: BooleanConstructor;
75
- default: boolean;
76
- };
77
44
  persistent: {
78
45
  type: BooleanConstructor;
79
46
  default: boolean;
@@ -81,23 +48,16 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
81
48
  position: {
82
49
  type: StringConstructor;
83
50
  default: string;
84
- validator: (val: unknown) => any;
51
+ validator: (value: unknown) => any;
85
52
  };
86
53
  }>> & Readonly<{
87
54
  "onUpdate:isOpen"?: ((...args: any[]) => any) | undefined;
88
- onConfirm?: ((...args: any[]) => any) | undefined;
89
55
  }>, {
90
- title: string;
91
56
  position: string;
92
- message: string;
93
- confirmBtnText: string;
94
- cancelBtnText: string;
95
- showConfirmBtn: boolean;
96
- showCloseIcon: boolean;
57
+ width: string;
58
+ height: string;
97
59
  persistent: boolean;
98
60
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
99
61
  type __VLS_Slots = {
100
- title?: ((props: {}) => any) | undefined;
101
- } & {
102
- message?: ((props: {}) => any) | undefined;
62
+ default?: ((props: {}) => any) | undefined;
103
63
  };
@@ -4,22 +4,26 @@ interface UploadedFile {
4
4
  size: number;
5
5
  type: string;
6
6
  }
7
- type Separator = 'solid' | 'dashed' | 'dotted';
8
- type Rounded = 'none' | 'sm' | 'md' | 'lg' | 'xl';
7
+ type Rounded = "none" | "sm" | "md" | "lg" | "xl" | "full";
8
+ type Separator = "solid" | "dashed" | "dotted";
9
+ type Size = "sm" | "md" | "lg";
9
10
  type __VLS_Props = {
10
11
  modelValue?: UploadedFile[];
11
- header?: string;
12
- subHeader?: string;
12
+ label?: string;
13
+ hint?: string;
13
14
  fileType?: string;
14
15
  multiple?: boolean;
15
- disabled?: boolean;
16
- bordered?: boolean;
17
- flat?: boolean;
16
+ size?: Size;
17
+ color?: string;
18
18
  rounded?: Rounded;
19
- borderColor?: string;
20
- bgColor?: string;
21
- separator?: Separator;
22
19
  dense?: boolean;
20
+ width?: string;
21
+ outlined?: boolean;
22
+ borderless?: boolean;
23
+ flat?: boolean;
24
+ separator?: Separator;
25
+ disabled?: boolean;
26
+ readonly?: boolean;
23
27
  fileRemoverIcon?: string;
24
28
  fileSizeLimitKb?: number;
25
29
  };
@@ -35,16 +39,19 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
35
39
  modelValue: UploadedFile[];
36
40
  disabled: boolean;
37
41
  dense: boolean;
38
- header: string;
42
+ label: string;
43
+ color: string;
39
44
  rounded: Rounded;
40
- borderColor: string;
45
+ size: Size;
41
46
  separator: Separator;
42
47
  flat: boolean;
43
- bordered: boolean;
44
- subHeader: string;
48
+ width: string;
49
+ readonly: boolean;
50
+ hint: string;
45
51
  fileType: string;
46
52
  multiple: boolean;
47
- bgColor: string;
53
+ outlined: boolean;
54
+ borderless: boolean;
48
55
  fileRemoverIcon: string;
49
56
  fileSizeLimitKb: number;
50
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,30 +1,32 @@
1
1
  <script setup>
2
- import { computed, ref } from "vue";
3
2
  defineOptions({
4
3
  name: "UIFileUploader"
5
4
  });
5
+ import { computed, ref } from "vue";
6
6
  const props = defineProps({
7
7
  modelValue: { type: Array, required: false, default: () => [] },
8
- header: { type: String, required: false, default: "Upload Files" },
9
- subHeader: { type: String, required: false, default: "Supports document uploads" },
8
+ label: { type: String, required: false, default: "" },
9
+ hint: { type: String, required: false, default: "" },
10
10
  fileType: { type: String, required: false, default: "*" },
11
11
  multiple: { type: Boolean, required: false, default: true },
12
- disabled: { type: Boolean, required: false, default: false },
13
- bordered: { type: Boolean, required: false, default: true },
12
+ size: { type: String, required: false, default: "md" },
13
+ color: { type: String, required: false, default: "" },
14
+ rounded: { type: String, required: false, default: "md" },
15
+ dense: { type: Boolean, required: false, default: false },
16
+ width: { type: String, required: false, default: "" },
17
+ outlined: { type: Boolean, required: false, default: true },
18
+ borderless: { type: Boolean, required: false, default: false },
14
19
  flat: { type: Boolean, required: false, default: false },
15
- rounded: { type: String, required: false, default: "xl" },
16
- borderColor: { type: String, required: false, default: "gray-300" },
17
- bgColor: { type: String, required: false, default: "white" },
18
20
  separator: { type: String, required: false, default: "dashed" },
19
- dense: { type: Boolean, required: false, default: false },
20
- fileRemoverIcon: { type: String, required: false, default: "delete" },
21
+ disabled: { type: Boolean, required: false, default: false },
22
+ readonly: { type: Boolean, required: false, default: false },
23
+ fileRemoverIcon: { type: String, required: false, default: "close" },
21
24
  fileSizeLimitKb: { type: Number, required: false, default: 2048 }
22
25
  });
23
26
  const emit = defineEmits(["update:modelValue"]);
24
- const fileInput = ref(
25
- null
26
- );
27
+ const fileInput = ref(null);
27
28
  const errorMessage = ref("");
29
+ const isDragging = ref(false);
28
30
  const files = computed({
29
31
  get: () => props.modelValue,
30
32
  set: (value) => emit(
@@ -33,56 +35,93 @@ const files = computed({
33
35
  )
34
36
  });
35
37
  const isHex = (val) => val.startsWith("#");
36
- const roundedClasses = computed(
37
- () => {
38
- const map = {
39
- none: "",
40
- sm: "rounded-sm",
41
- md: "rounded-md",
42
- lg: "rounded-lg",
43
- xl: "rounded-xl"
44
- };
45
- return map[props.rounded];
38
+ const roundedMap = {
39
+ none: "rounded-none",
40
+ sm: "rounded-sm",
41
+ md: "rounded-md",
42
+ lg: "rounded-lg",
43
+ xl: "rounded-xl",
44
+ full: "rounded-full"
45
+ };
46
+ const separatorClass = computed(() => {
47
+ switch (props.separator) {
48
+ case "solid":
49
+ return "border-solid";
50
+ case "dotted":
51
+ return "border-dotted";
52
+ default:
53
+ return "border-dashed";
46
54
  }
47
- );
48
- const separatorClasses = computed(
49
- () => {
50
- switch (props.separator) {
51
- case "solid":
52
- return "border-solid";
53
- case "dotted":
54
- return "border-dotted";
55
- case "dashed":
56
- default:
57
- return "border-dashed";
58
- }
55
+ });
56
+ const computedClass = computed(() => {
57
+ let classes = "input block transition-all duration-300 border ";
58
+ if (props.borderless) {
59
+ classes += " border-none bg-transparent";
60
+ } else if (props.outlined) {
61
+ classes += " border";
62
+ } else {
63
+ classes += " border-0 border-b";
59
64
  }
60
- );
61
- const uploaderClasses = computed(
62
- () => [
63
- "border-2 transition-all duration-300 flex flex-col items-center justify-center cursor-pointer",
64
- roundedClasses.value,
65
- separatorClasses.value,
66
- props.bordered ? !isHex(props.borderColor) ? `border-${props.borderColor}` : "" : "border-transparent",
67
- props.flat ? "shadow-none" : "shadow-md",
68
- !isHex(props.bgColor) ? `bg-${props.bgColor}` : "",
69
- props.dense ? "p-4" : "p-8",
70
- props.disabled ? "opacity-50 pointer-events-none" : "hover:shadow-lg"
71
- ]
72
- );
73
- const uploaderStyle = computed(
74
- () => {
75
- const style = {};
76
- if (isHex(props.borderColor)) {
77
- style.borderColor = props.borderColor;
78
- }
79
- if (isHex(props.bgColor)) {
80
- style.backgroundColor = props.bgColor;
81
- }
82
- return style;
65
+ if (!props.borderless) {
66
+ classes += ` ${separatorClass.value}`;
83
67
  }
84
- );
68
+ classes += ` ${roundedMap[props.rounded] ?? roundedMap.md}`;
69
+ if (!props.flat) {
70
+ classes += " shadow-sm";
71
+ }
72
+ if (props.disabled) {
73
+ classes += " opacity-50 cursor-not-allowed";
74
+ } else {
75
+ classes += " cursor-pointer";
76
+ }
77
+ if (props.readonly) {
78
+ classes += " pointer-events-none";
79
+ }
80
+ switch (props.size) {
81
+ case "sm":
82
+ classes += " text-sm";
83
+ break;
84
+ case "lg":
85
+ classes += " text-lg";
86
+ break;
87
+ default:
88
+ classes += " text-base";
89
+ }
90
+ if (props.color && !isHex(props.color)) {
91
+ classes += ` border-${props.color} focus:ring-${props.color}`;
92
+ }
93
+ if (props.width && !CSS.supports(
94
+ "width",
95
+ props.width
96
+ )) {
97
+ classes += ` w-${props.width}`;
98
+ }
99
+ if (!props.width) {
100
+ classes += " w-full";
101
+ }
102
+ if (isDragging.value) {
103
+ classes += " scale-[1.01]";
104
+ }
105
+ return classes;
106
+ });
107
+ const computedStyle = computed(() => {
108
+ const styles = {};
109
+ if (props.width && CSS.supports(
110
+ "width",
111
+ props.width
112
+ )) {
113
+ styles.width = props.width;
114
+ }
115
+ if (props.color && isHex(props.color)) {
116
+ styles.borderColor = props.color;
117
+ styles["--tw-ring-color"] = props.color;
118
+ }
119
+ return styles;
120
+ });
85
121
  const triggerFileInput = () => {
122
+ if (props.disabled || props.readonly) {
123
+ return;
124
+ }
86
125
  fileInput.value?.click();
87
126
  };
88
127
  const createFileObject = (file) => ({
@@ -91,112 +130,229 @@ const createFileObject = (file) => ({
91
130
  size: file.size,
92
131
  type: file.type
93
132
  });
94
- const handleFiles = (event) => {
95
- const selectedFiles = "dataTransfer" in event && event.dataTransfer ? Array.from(
96
- event.dataTransfer.files
97
- ) : Array.from(
98
- event.target.files || []
99
- );
100
- const validFiles = selectedFiles.filter((file) => {
101
- const fileSizeInKb = file.size / 1024;
102
- if (fileSizeInKb > props.fileSizeLimitKb) {
103
- errorMessage.value = `File "${file.name}" exceeds ${props.fileSizeLimitKb} KB`;
133
+ const validateFile = (file) => {
134
+ errorMessage.value = "";
135
+ const sizeKb = file.size / 1024;
136
+ if (sizeKb > props.fileSizeLimitKb) {
137
+ errorMessage.value = `"${file.name}" exceeds ${props.fileSizeLimitKb} KB.`;
138
+ return false;
139
+ }
140
+ if (props.fileType && props.fileType !== "*") {
141
+ const accepted = props.fileType.split(",").map((t) => t.trim());
142
+ const valid = accepted.some(
143
+ (type) => {
144
+ if (type.endsWith("/*")) {
145
+ return file.type.startsWith(
146
+ type.replace(
147
+ "/*",
148
+ "/"
149
+ )
150
+ );
151
+ }
152
+ if (type.startsWith(".")) {
153
+ return file.name.toLowerCase().endsWith(
154
+ type.toLowerCase()
155
+ );
156
+ }
157
+ return file.type === type;
158
+ }
159
+ );
160
+ if (!valid) {
161
+ errorMessage.value = `"${file.name}" is not a supported file type.`;
104
162
  return false;
105
163
  }
106
- return true;
107
- });
108
- const mapped = validFiles.map(
109
- createFileObject
110
- );
164
+ }
165
+ return true;
166
+ };
167
+ const addFiles = (selectedFiles) => {
168
+ const validFiles = selectedFiles.filter(validateFile).map(createFileObject);
169
+ if (!validFiles.length)
170
+ return;
111
171
  files.value = props.multiple ? [
112
172
  ...files.value,
113
- ...mapped
114
- ] : mapped;
115
- if (mapped.length > 0) {
116
- errorMessage.value = "";
173
+ ...validFiles
174
+ ] : validFiles;
175
+ errorMessage.value = "";
176
+ };
177
+ const handleFiles = (event) => {
178
+ const target = event.target;
179
+ if (!target.files)
180
+ return;
181
+ addFiles(
182
+ Array.from(target.files)
183
+ );
184
+ target.value = "";
185
+ };
186
+ const handleDrop = (event) => {
187
+ isDragging.value = false;
188
+ if (props.disabled || props.readonly) {
189
+ return;
190
+ }
191
+ if (!event.dataTransfer) {
192
+ return;
117
193
  }
194
+ addFiles(
195
+ Array.from(
196
+ event.dataTransfer.files
197
+ )
198
+ );
199
+ };
200
+ const handleDragOver = (event) => {
201
+ event.preventDefault();
202
+ if (props.disabled || props.readonly) {
203
+ return;
204
+ }
205
+ isDragging.value = true;
206
+ };
207
+ const handleDragLeave = () => {
208
+ isDragging.value = false;
118
209
  };
119
210
  const removeFile = (index) => {
120
211
  files.value = files.value.filter(
121
212
  (_, i) => i !== index
122
213
  );
123
214
  };
215
+ const formattedFiles = computed(
216
+ () => files.value.map(
217
+ (file) => ({
218
+ ...file,
219
+ formattedSize: file.size >= 1024 * 1024 ? `${(file.size / 1024 / 1024).toFixed(
220
+ 2
221
+ )} MB` : `${(file.size / 1024).toFixed(
222
+ 1
223
+ )} KB`
224
+ })
225
+ )
226
+ );
124
227
  </script>
125
228
 
126
229
  <template>
127
- <div class="max-w-lg mx-auto">
128
- <!-- Header -->
129
- <div class="mb-6 text-center">
130
- <h2 class="text-2xl font-bold text-gray-800 dark:text-white">
131
- {{ header }}
132
- </h2>
230
+ <div
231
+ :class="[
232
+ 'relative',
233
+ dense ? 'my-2.5' : 'mt-5 mb-2.5',
234
+ size && `input-${size}`
235
+ ]"
236
+ >
237
+ <!-- Hidden Input -->
238
+ <input
239
+ ref="fileInput"
240
+ type="file"
241
+ class="hidden"
242
+ :accept="fileType"
243
+ :multiple="multiple"
244
+ @change="handleFiles"
245
+ />
133
246
 
134
- <p class="text-sm text-gray-500">
135
- {{ subHeader }}
136
- </p>
137
- </div>
247
+ <!-- Floating Label -->
248
+ <label
249
+ class="absolute -top-6 px-1 pointer-events-none transition-all duration-200
250
+ dark:text-gray-300"
251
+ :class="color && !isHex(color) ? `text-${color}` : 'text-gray-400 input-' + size"
252
+ :style="
253
+ color && isHex(color) ? { color } : void 0
254
+ "
255
+ >
256
+ {{ label }}
257
+ </label>
138
258
 
139
- <!-- Hidden Input -->
140
- <input ref="fileInput" type="file" class="hidden" :accept="fileType" :multiple="multiple" @change="handleFiles">
259
+ <!-- Hint -->
260
+ <p
261
+ v-if="hint"
262
+ class="ml-1 text-[10px] text-gray-500 dark:text-gray-400"
263
+ >
264
+ {{ hint }}
265
+ </p>
141
266
 
142
267
  <!-- Dropzone -->
143
- <div :class="uploaderClasses" :style="uploaderStyle" @click="triggerFileInput" @dragover.prevent @drop.prevent="
144
- handleFiles($event)
145
- ">
268
+ <div
269
+ :class="computedClass"
270
+ :style="computedStyle"
271
+ class="mt-1 p-1 flex flex-col items-center justify-center
272
+ select-none text-center text-xs
273
+ transition-all duration-200
274
+ hover:bg-black/5
275
+ dark:hover:bg-white/5"
276
+ @click="triggerFileInput"
277
+ @dragover.prevent="handleDragOver"
278
+ @dragleave="handleDragLeave"
279
+ @drop.prevent="handleDrop"
280
+ >
146
281
  <slot name="content">
147
- <p class="text-gray-600 mb-4">
282
+ <p class="text-xs text-gray-500">
148
283
  {{
149
- multiple ? "Drag & drop files here" : "Drag & drop file here"
284
+ multiple ? "Drop files here or click to browse" : "Drop file here or click to browse"
150
285
  }}
151
286
  </p>
152
-
153
- <button type="button" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
154
- {{
155
- multiple ? "Browse Files" : "Browse File"
156
- }}
157
- </button>
287
+ <p
288
+ v-if="fileType && fileType !== '*'"
289
+ class="text-[10px] text-gray-500"
290
+ >
291
+ Accepted: {{ fileType }}
292
+ </p>
293
+ <p class="text-[10px] text-gray-500">
294
+ Maximum size: {{ fileSizeLimitKb }} KB
295
+ </p>
158
296
  </slot>
159
297
  </div>
160
-
161
298
  <!-- Uploaded Files -->
162
- <div v-if="files.length" class="mt-6">
163
- <h3 class="text-lg font-semibold mb-4">
164
- Uploaded Files
165
- </h3>
166
-
167
- <ul class="space-y-2">
168
- <li v-for="(file, index) in files" :key="index"
169
- class="flex items-center justify-between p-4 rounded-lg border border-gray-200 bg-gray-50">
170
- <div class="flex items-center gap-3">
171
- <span class="material-icons text-blue-500">
172
- description
173
- </span>
299
+ <div
300
+ v-if="formattedFiles.length"
301
+ class="mt-3 overflow-hidden rounded-lg border border-gray-200
302
+ dark:border-gray-700"
303
+ >
304
+ <div
305
+ v-for="(file, index) in formattedFiles"
306
+ :key="file.name"
307
+ class="flex items-start justify-between
308
+ border-b border-gray-100 p-1
309
+ last:border-b-0
310
+ dark:border-gray-700"
311
+ >
312
+ <div class="flex min-w-0 items-start gap-3">
313
+ <span class="material-icons text-base text-gray-500">
314
+ insert_drive_file
315
+ </span>
174
316
 
175
- <span>
317
+ <div class="min-w-0">
318
+ <div
319
+ class="truncate text-xs font-medium dark:text-gray-50"
320
+ >
176
321
  {{ file.name }}
177
- </span>
322
+ </div>
323
+
324
+ <div class="text-[10px] text-gray-400">
325
+ {{ file.formattedSize }}
326
+ </div>
178
327
  </div>
328
+ </div>
179
329
 
180
- <button type="button" class="text-red-500 hover:text-red-700" @click.stop="
181
- removeFile(index)
182
- ">
183
- <span class="material-icons">
184
- {{
185
- fileRemoverIcon
186
- }}
187
- </span>
188
- </button>
189
- </li>
190
- </ul>
330
+ <button
331
+ type="button"
332
+ class="flex h-5 w-5 items-center justify-center rounded-full
333
+ text-gray-400 transition-colors
334
+ hover:bg-red-500/10
335
+ hover:text-red-500
336
+ dark:hover:bg-red-500/20"
337
+ @click.stop="removeFile(index)"
338
+ >
339
+ <span class="material-icons text-xs">
340
+ {{ fileRemoverIcon }}
341
+ </span>
342
+ </button>
343
+ </div>
191
344
  </div>
192
345
 
193
346
  <!-- Error -->
194
- <div v-if="errorMessage" class="mt-4 text-sm text-red-500">
347
+ <p
348
+ v-if="errorMessage"
349
+ class="ml-1 mt-1 text-xs text-red-500"
350
+ >
195
351
  {{ errorMessage }}
196
- </div>
352
+ </p>
197
353
  </div>
198
354
  </template>
199
355
 
200
356
  <style scoped>
201
- .material-icons{font-size:20px}
357
+ .input-sm label{font-size:12px}.input-md label{font-size:14px}.input-lg label{font-size:16px}
202
358
  </style>