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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/templates/components/Alert/Alert.types.ts +4 -0
  3. package/templates/components/Alert/AlertItem.component.vue +2 -4
  4. package/templates/components/Alert/index.ts +6 -1
  5. package/templates/components/Breadcrumb/Breadcrumb.component.vue +116 -0
  6. package/templates/components/Breadcrumb/Breadcrumb.styles.ts +40 -0
  7. package/templates/components/Breadcrumb/Breadcrumb.types.ts +30 -0
  8. package/templates/components/Breadcrumb/index.ts +17 -0
  9. package/templates/components/Button/Button.styles.ts +4 -4
  10. package/templates/components/Calendar/Calendar.component.vue +50 -2
  11. package/templates/components/Calendar/Calendar.styles.ts +4 -0
  12. package/templates/components/Calendar/Calendar.types.ts +8 -0
  13. package/templates/components/Chart/Chart.component.vue +308 -0
  14. package/templates/components/Chart/Chart.types.ts +18 -0
  15. package/templates/components/Chart/index.ts +13 -0
  16. package/templates/components/Checkbox/Checkbox.component.vue +19 -5
  17. package/templates/components/Checkbox/Checkbox.styles.ts +10 -0
  18. package/templates/components/Checkbox/Checkbox.types.ts +1 -0
  19. package/templates/components/DatePicker/DatePicker.component.vue +219 -0
  20. package/templates/components/DatePicker/DatePicker.styles.ts +74 -0
  21. package/templates/components/DatePicker/DatePicker.types.ts +18 -0
  22. package/templates/components/DatePicker/DatePicker.utils.ts +46 -0
  23. package/templates/components/DatePicker/TimeWheel.vue +145 -0
  24. package/templates/components/DatePicker/TimeWheelColumn.vue +147 -0
  25. package/templates/components/DatePicker/index.ts +11 -0
  26. package/templates/components/DateRangePicker/DateRangePicker.component.vue +304 -0
  27. package/templates/components/DateRangePicker/DateRangePicker.types.ts +23 -0
  28. package/templates/components/DateRangePicker/index.ts +11 -0
  29. package/templates/components/FileUpload/FileUpload.component.vue +306 -0
  30. package/templates/components/FileUpload/FileUpload.styles.ts +74 -0
  31. package/templates/components/FileUpload/FileUpload.types.ts +31 -0
  32. package/templates/components/FileUpload/index.ts +15 -0
  33. package/templates/components/InputOTP/InputOTP.component.vue +4 -2
  34. package/templates/components/InputOTP/InputOTP.types.ts +1 -0
  35. package/templates/components/Popover/Popover.component.vue +1 -1
  36. package/templates/components/SegmentedControl/SegmentedControl.component.vue +85 -0
  37. package/templates/components/SegmentedControl/SegmentedControl.styles.ts +55 -0
  38. package/templates/components/SegmentedControl/SegmentedControl.types.ts +15 -0
  39. package/templates/components/SegmentedControl/index.ts +11 -0
  40. package/templates/components/Table/Table.component.vue +507 -0
  41. package/templates/components/Table/Table.styles.ts +140 -0
  42. package/templates/components/Table/Table.types.ts +98 -0
  43. package/templates/components/Table/index.ts +39 -0
  44. package/templates/components.json +42 -0
  45. 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>
@@ -1,4 +1,5 @@
1
1
  export interface UixyInputOTPProps {
2
2
  disabled?: boolean;
3
+ autoFocus?: boolean;
3
4
  filled?: (code: string) => Promise<boolean>;
4
5
  }
@@ -17,7 +17,7 @@
17
17
  :exit="{ opacity: 0, scale: 0.8 }"
18
18
  :transition="{ duration: 0.15, ease: 'easeInOut' }"
19
19
  >
20
- <slot />
20
+ <slot :close="handleLeave" />
21
21
  </motion.div>
22
22
  </div>
23
23
  </motion.div>
@@ -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
+ };