v-uixy 1.4.0 → 1.6.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.
- package/package.json +1 -1
- package/templates/components/Alert/Alert.types.ts +4 -0
- package/templates/components/Alert/AlertItem.component.vue +2 -4
- package/templates/components/Alert/index.ts +6 -1
- package/templates/components/Breadcrumb/Breadcrumb.component.vue +116 -0
- package/templates/components/Breadcrumb/Breadcrumb.styles.ts +40 -0
- package/templates/components/Breadcrumb/Breadcrumb.types.ts +30 -0
- package/templates/components/Breadcrumb/index.ts +17 -0
- package/templates/components/Button/Button.styles.ts +4 -4
- package/templates/components/Calendar/Calendar.component.vue +50 -2
- package/templates/components/Calendar/Calendar.styles.ts +4 -0
- package/templates/components/Calendar/Calendar.types.ts +8 -0
- package/templates/components/Chart/Chart.component.vue +308 -0
- package/templates/components/Chart/Chart.types.ts +18 -0
- package/templates/components/Chart/index.ts +13 -0
- package/templates/components/Checkbox/Checkbox.component.vue +19 -5
- package/templates/components/Checkbox/Checkbox.styles.ts +10 -0
- package/templates/components/Checkbox/Checkbox.types.ts +1 -0
- package/templates/components/DatePicker/DatePicker.component.vue +219 -0
- package/templates/components/DatePicker/DatePicker.styles.ts +74 -0
- package/templates/components/DatePicker/DatePicker.types.ts +18 -0
- package/templates/components/DatePicker/DatePicker.utils.ts +46 -0
- package/templates/components/DatePicker/TimeWheel.vue +145 -0
- package/templates/components/DatePicker/TimeWheelColumn.vue +147 -0
- package/templates/components/DatePicker/index.ts +11 -0
- package/templates/components/DateRangePicker/DateRangePicker.component.vue +304 -0
- package/templates/components/DateRangePicker/DateRangePicker.types.ts +23 -0
- package/templates/components/DateRangePicker/index.ts +11 -0
- package/templates/components/FileUpload/FileUpload.component.vue +306 -0
- package/templates/components/FileUpload/FileUpload.styles.ts +74 -0
- package/templates/components/FileUpload/FileUpload.types.ts +31 -0
- package/templates/components/FileUpload/index.ts +15 -0
- package/templates/components/InputOTP/InputOTP.component.vue +4 -2
- package/templates/components/InputOTP/InputOTP.types.ts +1 -0
- package/templates/components/Popover/Popover.component.vue +1 -1
- package/templates/components/SegmentedControl/SegmentedControl.component.vue +85 -0
- package/templates/components/SegmentedControl/SegmentedControl.styles.ts +55 -0
- package/templates/components/SegmentedControl/SegmentedControl.types.ts +15 -0
- package/templates/components/SegmentedControl/index.ts +11 -0
- package/templates/components/Table/Table.component.vue +507 -0
- package/templates/components/Table/Table.styles.ts +140 -0
- package/templates/components/Table/Table.types.ts +98 -0
- package/templates/components/Table/index.ts +39 -0
- package/templates/components.json +42 -0
- package/templates/composables/usePosition.ts +6 -4
|
@@ -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>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import styles from "~/utils/styles";
|
|
2
|
+
|
|
3
|
+
export const dropzoneStyles = styles(
|
|
4
|
+
"relative flex w-full flex-col items-center justify-center gap-2 rounded-2 border border-dashed px-6 py-8 text-center transition-colors duration-150 ease-in-out outline-offset-2 outline-gray-300",
|
|
5
|
+
{
|
|
6
|
+
state: {
|
|
7
|
+
default:
|
|
8
|
+
"cursor-pointer border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-1000 dark:hover:border-gray-700 dark:hover:bg-gray-900",
|
|
9
|
+
dragging:
|
|
10
|
+
"cursor-copy border-gray-500 bg-gray-100 dark:border-gray-500 dark:bg-gray-900",
|
|
11
|
+
error:
|
|
12
|
+
"cursor-pointer border-error-700 bg-white hover:bg-gray-100 dark:border-error-900 dark:bg-gray-1000 dark:hover:bg-gray-900",
|
|
13
|
+
disabled:
|
|
14
|
+
"pointer-events-none cursor-not-allowed border-gray-300 bg-gray-200 dark:border-gray-900 dark:bg-gray-900",
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const dropzoneIconStyles = styles(
|
|
20
|
+
"h-6 w-6 transition-colors duration-150 ease-in-out",
|
|
21
|
+
{
|
|
22
|
+
state: {
|
|
23
|
+
default: "text-gray-400 dark:text-gray-600",
|
|
24
|
+
dragging: "text-gray-700 dark:text-gray-300",
|
|
25
|
+
error: "text-error-600 dark:text-error-500",
|
|
26
|
+
disabled: "text-gray-400 dark:text-gray-700",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const dropzoneLabelStyles = styles(
|
|
32
|
+
"text-sm font-500 text-gray-800 dark:text-gray-200",
|
|
33
|
+
{
|
|
34
|
+
state: {
|
|
35
|
+
default: "",
|
|
36
|
+
dragging: "",
|
|
37
|
+
error: "",
|
|
38
|
+
disabled: "text-gray-500 dark:text-gray-700",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const dropzoneHintStyles = styles(
|
|
44
|
+
"text-xs text-gray-500 dark:text-gray-600"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export const fileListStyles = styles("flex flex-col gap-2");
|
|
48
|
+
|
|
49
|
+
export const fileRowStyles = styles(
|
|
50
|
+
"flex items-center gap-3 rounded-1 border border-gray-300 bg-white px-3 py-2 dark:border-gray-900 dark:bg-gray-1000"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const fileIconStyles = styles(
|
|
54
|
+
"h-5 w-5 shrink-0 text-gray-500 dark:text-gray-500"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const fileNameStyles = styles(
|
|
58
|
+
"flex-1 min-w-0 truncate text-sm text-gray-800 dark:text-gray-200"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const fileSizeStyles = styles(
|
|
62
|
+
"shrink-0 text-xs text-gray-500 dark:text-gray-600"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const helperStyles = styles(
|
|
66
|
+
"transition-colors ease-in-out duration-150 text-xs",
|
|
67
|
+
{
|
|
68
|
+
status: {
|
|
69
|
+
default: "text-gray-600",
|
|
70
|
+
error: "text-error-600",
|
|
71
|
+
disabled: "text-gray-400 dark:text-gray-800",
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IconName } from "~/types/icons";
|
|
2
|
+
|
|
3
|
+
export type UixyFileRejectionReason = "type" | "size" | "count";
|
|
4
|
+
|
|
5
|
+
export interface UixyFileRejection {
|
|
6
|
+
file: File;
|
|
7
|
+
reason: UixyFileRejectionReason;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UixyFileUploadProps {
|
|
11
|
+
variant?: "dropzone" | "button";
|
|
12
|
+
buttonVariant?: "primary" | "secondary" | "tertiary" | "quaternary";
|
|
13
|
+
buttonSize?: "sm" | "md" | "lg";
|
|
14
|
+
accept?: string;
|
|
15
|
+
multiple?: boolean;
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
maxFiles?: number;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
icon?: IconName;
|
|
20
|
+
label?: string;
|
|
21
|
+
hint?: string;
|
|
22
|
+
status?: "default" | "error";
|
|
23
|
+
helperText?: string;
|
|
24
|
+
errorText?: string;
|
|
25
|
+
hideList?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UixyFileUploadEmits {
|
|
29
|
+
(event: "change", files: File[]): void;
|
|
30
|
+
(event: "reject", rejections: UixyFileRejection[]): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import UixyFileUpload from "./FileUpload.component.vue";
|
|
2
|
+
import {
|
|
3
|
+
type UixyFileUploadProps,
|
|
4
|
+
type UixyFileUploadEmits,
|
|
5
|
+
type UixyFileRejection,
|
|
6
|
+
type UixyFileRejectionReason,
|
|
7
|
+
} from "./FileUpload.types";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
UixyFileUpload,
|
|
11
|
+
type UixyFileUploadProps,
|
|
12
|
+
type UixyFileUploadEmits,
|
|
13
|
+
type UixyFileRejection,
|
|
14
|
+
type UixyFileRejectionReason,
|
|
15
|
+
};
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
import { inputOTPStyles } from "./InputOTP.styles";
|
|
46
46
|
import type { UixyInputOTPProps } from "./InputOTP.types";
|
|
47
47
|
|
|
48
|
-
const props = defineProps<UixyInputOTPProps>()
|
|
48
|
+
const props = withDefaults(defineProps<UixyInputOTPProps>(), {
|
|
49
|
+
autoFocus: true,
|
|
50
|
+
});
|
|
49
51
|
|
|
50
52
|
const INPUT_LENGTH = 6 as const;
|
|
51
53
|
const PLACES = [
|
|
@@ -153,6 +155,6 @@
|
|
|
153
155
|
});
|
|
154
156
|
|
|
155
157
|
onMounted(() => {
|
|
156
|
-
inputRefs[0].value?.focus();
|
|
158
|
+
if (props.autoFocus) inputRefs[0].value?.focus();
|
|
157
159
|
});
|
|
158
160
|
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
role="tablist"
|
|
4
|
+
:class="
|
|
5
|
+
segmentedTrackStyles({
|
|
6
|
+
fullWidth: props.fullWidth,
|
|
7
|
+
disabled: props.disabled,
|
|
8
|
+
})
|
|
9
|
+
"
|
|
10
|
+
>
|
|
11
|
+
<button
|
|
12
|
+
v-for="option in props.options"
|
|
13
|
+
:key="option.value"
|
|
14
|
+
type="button"
|
|
15
|
+
role="tab"
|
|
16
|
+
:aria-selected="model === option.value"
|
|
17
|
+
:disabled="props.disabled || option.disabled"
|
|
18
|
+
:class="
|
|
19
|
+
segmentStyles({
|
|
20
|
+
size: props.size ?? 'md',
|
|
21
|
+
selected: model === option.value,
|
|
22
|
+
disabled: !!option.disabled,
|
|
23
|
+
fullWidth: props.fullWidth,
|
|
24
|
+
})
|
|
25
|
+
"
|
|
26
|
+
@click="select(option)"
|
|
27
|
+
>
|
|
28
|
+
<animate-presence mode="wait" :initial="false">
|
|
29
|
+
<motion.div
|
|
30
|
+
v-if="model === option.value"
|
|
31
|
+
:layout-id="pillLayoutId"
|
|
32
|
+
:initial="{ opacity: 0 }"
|
|
33
|
+
:animate="{ opacity: 1 }"
|
|
34
|
+
:exit="{ opacity: 0 }"
|
|
35
|
+
:transition="{ duration: 0.2, ease: 'easeInOut' }"
|
|
36
|
+
:class="segmentPillStyles()"
|
|
37
|
+
/>
|
|
38
|
+
</animate-presence>
|
|
39
|
+
|
|
40
|
+
<span :class="segmentContentStyles()">
|
|
41
|
+
<uixy-icon
|
|
42
|
+
v-if="option.icon"
|
|
43
|
+
:name="option.icon"
|
|
44
|
+
:class="segmentIconStyles({ size: props.size ?? 'md' })"
|
|
45
|
+
/>
|
|
46
|
+
<span v-if="option.label">{{ option.label }}</span>
|
|
47
|
+
</span>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { motion, AnimatePresence } from "motion-v";
|
|
54
|
+
import { v4 as uuidv4 } from "uuid";
|
|
55
|
+
import { UixyIcon } from "../Icon";
|
|
56
|
+
import type {
|
|
57
|
+
UixySegmentedControlProps,
|
|
58
|
+
UixySegmentedOption,
|
|
59
|
+
} from "./SegmentedControl.types";
|
|
60
|
+
import {
|
|
61
|
+
segmentedTrackStyles,
|
|
62
|
+
segmentStyles,
|
|
63
|
+
segmentPillStyles,
|
|
64
|
+
segmentContentStyles,
|
|
65
|
+
segmentIconStyles,
|
|
66
|
+
} from "./SegmentedControl.styles";
|
|
67
|
+
|
|
68
|
+
const props = defineProps<UixySegmentedControlProps>();
|
|
69
|
+
|
|
70
|
+
const model = defineModel<string | number>();
|
|
71
|
+
|
|
72
|
+
const pillLayoutId = computed(() => `uixy-segmented-${uuidv4()}`);
|
|
73
|
+
|
|
74
|
+
const select = (option: UixySegmentedOption) => {
|
|
75
|
+
if (props.disabled || option.disabled) return;
|
|
76
|
+
|
|
77
|
+
model.value = option.value;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
onBeforeMount(() => {
|
|
81
|
+
if (model.value === undefined && props.options.length > 0) {
|
|
82
|
+
model.value = props.options[0]?.value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import styles from "~/utils/styles";
|
|
2
|
+
|
|
3
|
+
export const segmentedTrackStyles = styles(
|
|
4
|
+
"relative inline-flex items-center gap-1 rounded-1 bg-gray-200 p-0.5 dark:bg-gray-900",
|
|
5
|
+
{
|
|
6
|
+
fullWidth: {
|
|
7
|
+
true: "flex w-full",
|
|
8
|
+
false: "",
|
|
9
|
+
},
|
|
10
|
+
disabled: {
|
|
11
|
+
true: "pointer-events-none opacity-50",
|
|
12
|
+
false: "",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const segmentStyles = styles(
|
|
18
|
+
"relative flex items-center justify-center rounded-1 font-500 cursor-pointer select-none transition-colors duration-150 ease-in-out outline-offset-2 outline-gray-300",
|
|
19
|
+
{
|
|
20
|
+
size: {
|
|
21
|
+
sm: "px-2.5 py-1 text-xs",
|
|
22
|
+
md: "px-3 py-1 text-sm",
|
|
23
|
+
lg: "px-4 py-1.5 text-sm",
|
|
24
|
+
},
|
|
25
|
+
selected: {
|
|
26
|
+
true: "text-black dark:text-white",
|
|
27
|
+
false:
|
|
28
|
+
"text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-gray-100",
|
|
29
|
+
},
|
|
30
|
+
disabled: {
|
|
31
|
+
true: "cursor-not-allowed text-gray-400 hover:text-gray-400 dark:text-gray-700 dark:hover:text-gray-700",
|
|
32
|
+
false: "",
|
|
33
|
+
},
|
|
34
|
+
fullWidth: {
|
|
35
|
+
true: "flex-1",
|
|
36
|
+
false: "",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export const segmentPillStyles = styles(
|
|
42
|
+
"absolute inset-0 rounded-1 bg-white shadow-xs dark:bg-gray-700",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const segmentContentStyles = styles(
|
|
46
|
+
"relative z-10 flex items-center gap-1.5",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const segmentIconStyles = styles("shrink-0", {
|
|
50
|
+
size: {
|
|
51
|
+
sm: "h-3.5 w-3.5",
|
|
52
|
+
md: "h-4 w-4",
|
|
53
|
+
lg: "h-4 w-4",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IconName } from "~/types/icons";
|
|
2
|
+
|
|
3
|
+
export interface UixySegmentedOption {
|
|
4
|
+
label?: string;
|
|
5
|
+
value: string | number;
|
|
6
|
+
icon?: IconName;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UixySegmentedControlProps {
|
|
11
|
+
options: UixySegmentedOption[];
|
|
12
|
+
size?: "sm" | "md" | "lg";
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
fullWidth?: boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import UixySegmentedControl from "./SegmentedControl.component.vue";
|
|
2
|
+
import {
|
|
3
|
+
type UixySegmentedControlProps,
|
|
4
|
+
type UixySegmentedOption,
|
|
5
|
+
} from "./SegmentedControl.types";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
UixySegmentedControl,
|
|
9
|
+
type UixySegmentedControlProps,
|
|
10
|
+
type UixySegmentedOption,
|
|
11
|
+
};
|