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.
- package/package.json +1 -1
- package/templates/assets/css/main.css +4 -0
- 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/Avatar/Avatar.component.vue +7 -3
- package/templates/components/Avatar/Avatar.styles.ts +3 -3
- 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 +3 -3
- package/templates/components/Calendar/Calendar.component.vue +76 -17
- package/templates/components/Calendar/Calendar.styles.ts +15 -3
- package/templates/components/Calendar/Calendar.types.ts +9 -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.styles.ts +1 -1
- package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
- package/templates/components/Command/Command.component.vue +6 -6
- package/templates/components/Command/Command.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/Input/Input.component.vue +27 -27
- package/templates/components/Input/Input.styles.ts +5 -5
- package/templates/components/Input/Input.types.ts +1 -0
- package/templates/components/InputOTP/InputOTP.component.vue +4 -2
- package/templates/components/InputOTP/InputOTP.types.ts +1 -0
- package/templates/components/Modal/Modal.component.vue +28 -4
- package/templates/components/Modal/Modal.types.ts +3 -1
- package/templates/components/Popover/Popover.component.vue +33 -2
- package/templates/components/Popover/Popover.types.ts +1 -0
- 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/Select/Select.component.vue +253 -108
- package/templates/components/Select/Select.styles.ts +6 -6
- package/templates/components/Select/Select.types.ts +12 -0
- package/templates/components/Select/SelectTreeItem.vue +154 -0
- package/templates/components/Select/composables/useSelect.composable.ts +260 -42
- package/templates/components/Tabs/Tabs.component.vue +11 -2
- package/templates/components/Tabs/Tabs.types.ts +1 -0
- package/templates/components/Textarea/Textarea.component.vue +23 -32
- package/templates/components/Textarea/Textarea.styles.ts +4 -4
- package/templates/components/Textarea/Textarea.types.ts +1 -0
- package/templates/components/TimePicker/TimePicker.component.vue +21 -18
- package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
- package/templates/components/TimePicker/TimePicker.types.ts +1 -0
- package/templates/components.json +37 -1
- package/templates/composables/usePosition.ts +17 -2
|
@@ -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
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
<div class="flex flex-col gap-1">
|
|
3
3
|
<div class="relative flex flex-col-reverse gap-1">
|
|
4
4
|
<input
|
|
5
|
+
ref="inputRef"
|
|
5
6
|
v-model="model"
|
|
6
7
|
:type="props.type ?? 'text'"
|
|
7
8
|
:id
|
|
8
9
|
:disabled="props.disabled"
|
|
9
10
|
:placeholder="props.placeholder"
|
|
10
|
-
:auto-focus="props.autoFocus"
|
|
11
11
|
:class="inputStyles({ status, icon }, $attrs.class as string)"
|
|
12
12
|
v-bind="filteredAttrs"
|
|
13
13
|
/>
|
|
@@ -27,29 +27,19 @@
|
|
|
27
27
|
{{ props.label }}
|
|
28
28
|
</label>
|
|
29
29
|
</div>
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
:initial="{ opacity: 0, y: -4 }"
|
|
44
|
-
:animate="{ opacity: 1, y: 0 }"
|
|
45
|
-
:exit="{ opacity: 0, y: 4 }"
|
|
46
|
-
:transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
|
|
47
|
-
>
|
|
48
|
-
{{ text }}
|
|
49
|
-
</motion.p>
|
|
50
|
-
</animate-presence>
|
|
51
|
-
</motion.div>
|
|
52
|
-
</animate-presence>
|
|
30
|
+
<div v-if="!props.hideHelper" class="h-4">
|
|
31
|
+
<animate-presence mode="wait" :initial="false">
|
|
32
|
+
<motion.div
|
|
33
|
+
:initial="{ opacity: 0, y: '-4px' }"
|
|
34
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
35
|
+
:exit="{ opacity: 0, y: '-4px' }"
|
|
36
|
+
:transition="{ duration: 0.125 }"
|
|
37
|
+
:key="status"
|
|
38
|
+
>
|
|
39
|
+
<p :class="helperStyles({ status })">{{ text }}</p>
|
|
40
|
+
</motion.div>
|
|
41
|
+
</animate-presence>
|
|
42
|
+
</div>
|
|
53
43
|
</div>
|
|
54
44
|
</template>
|
|
55
45
|
|
|
@@ -80,6 +70,8 @@
|
|
|
80
70
|
return rest;
|
|
81
71
|
});
|
|
82
72
|
|
|
73
|
+
const inputRef = ref<HTMLInputElement>();
|
|
74
|
+
|
|
83
75
|
const model = defineModel<string>();
|
|
84
76
|
|
|
85
77
|
const instance = getCurrentInstance();
|
|
@@ -89,7 +81,7 @@
|
|
|
89
81
|
const hasIconClickEmit = computed(() => !!instance?.vnode.props?.onIconClick);
|
|
90
82
|
|
|
91
83
|
const status = computed(() =>
|
|
92
|
-
props.disabled ? "disabled" : props.status ?? "default"
|
|
84
|
+
props.disabled ? "disabled" : (props.status ?? "default"),
|
|
93
85
|
);
|
|
94
86
|
|
|
95
87
|
const text = computed(
|
|
@@ -99,10 +91,18 @@
|
|
|
99
91
|
default: props.helperText,
|
|
100
92
|
disabled: "",
|
|
101
93
|
valid: "",
|
|
102
|
-
}[props.status ?? "default"]
|
|
94
|
+
})[props.status ?? "default"],
|
|
103
95
|
);
|
|
104
96
|
|
|
105
97
|
const icon = computed(() =>
|
|
106
|
-
props.icon ? props.iconPositon ?? "right" : "none"
|
|
98
|
+
props.icon ? (props.iconPositon ?? "right") : "none",
|
|
107
99
|
);
|
|
100
|
+
|
|
101
|
+
onMounted(() => {
|
|
102
|
+
if (props.autoFocus) {
|
|
103
|
+
nextTick(() => inputRef.value?.focus({ preventScroll: true }));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
defineExpose({ focus: () => inputRef.value?.focus({ preventScroll: true }) });
|
|
108
108
|
</script>
|
|
@@ -17,7 +17,7 @@ export const inputStyles = styles(
|
|
|
17
17
|
right: "pr-8",
|
|
18
18
|
none: "",
|
|
19
19
|
},
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
export const iconStyles = styles(
|
|
@@ -41,11 +41,11 @@ export const iconStyles = styles(
|
|
|
41
41
|
true: "pointer-events-auto cursor-pointer",
|
|
42
42
|
false: "",
|
|
43
43
|
},
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
export const labelStyles = styles(
|
|
48
|
-
"transition-colors ease-in-out duration-150 text-
|
|
48
|
+
"transition-colors ease-in-out duration-150 text-sm font-500",
|
|
49
49
|
{
|
|
50
50
|
status: {
|
|
51
51
|
default: "",
|
|
@@ -53,7 +53,7 @@ export const labelStyles = styles(
|
|
|
53
53
|
disabled: "text-gray-500 dark:text-gray-700",
|
|
54
54
|
valid: "",
|
|
55
55
|
},
|
|
56
|
-
}
|
|
56
|
+
},
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
export const helperStyles = styles(
|
|
@@ -65,5 +65,5 @@ export const helperStyles = styles(
|
|
|
65
65
|
disabled: "text-gray-400 dark:text-gray-800",
|
|
66
66
|
valid: "text-gray-600",
|
|
67
67
|
},
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
69
|
);
|
|
@@ -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>
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
:class="modalStyles($attrs.class as string)"
|
|
12
12
|
data-modal
|
|
13
13
|
@click.self="handleClick"
|
|
14
|
-
@keydown{esc}="handleClick"
|
|
15
14
|
>
|
|
16
15
|
<motion.div
|
|
17
16
|
:initial="{ scale: 0.8 }"
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
import { modalStyles } from "./Modal.styles";
|
|
33
32
|
import { AnimatePresence, motion } from "motion-v";
|
|
34
33
|
|
|
35
|
-
defineProps<UixyModalProps>();
|
|
34
|
+
const props = defineProps<UixyModalProps>();
|
|
36
35
|
|
|
37
36
|
defineOptions({
|
|
38
37
|
inheritAttrs: false,
|
|
@@ -41,14 +40,30 @@
|
|
|
41
40
|
const open = defineModel<boolean>();
|
|
42
41
|
|
|
43
42
|
const handleClick = () => {
|
|
43
|
+
if (props.persistent) return;
|
|
44
|
+
|
|
44
45
|
open.value = false;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
49
|
+
if (e.key === "Escape") {
|
|
50
|
+
handleClick();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (e.key === "Enter") {
|
|
54
|
+
const target = e.target as HTMLElement;
|
|
55
|
+
if (target.tagName === "TEXTAREA" || target.tagName === "BUTTON") return;
|
|
56
|
+
const modalEl = document.querySelector("[data-modal]");
|
|
57
|
+
const form = modalEl?.querySelector("form");
|
|
58
|
+
form?.requestSubmit();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
47
62
|
const toggleSiblingsInert = (enable?: boolean) => {
|
|
48
63
|
if (typeof document === "undefined") return;
|
|
49
64
|
|
|
50
65
|
const modalEl = document.querySelector(
|
|
51
|
-
"[data-modal]"
|
|
66
|
+
"[data-modal]",
|
|
52
67
|
) as HTMLElement | null;
|
|
53
68
|
|
|
54
69
|
Array.from(document.body.children).forEach((el) => {
|
|
@@ -68,7 +83,16 @@
|
|
|
68
83
|
open,
|
|
69
84
|
(isOpen) => {
|
|
70
85
|
toggleSiblingsInert(isOpen);
|
|
86
|
+
if (isOpen) {
|
|
87
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
88
|
+
} else {
|
|
89
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
90
|
+
}
|
|
71
91
|
},
|
|
72
|
-
{ immediate: true }
|
|
92
|
+
{ immediate: true },
|
|
73
93
|
);
|
|
94
|
+
|
|
95
|
+
onUnmounted(() => {
|
|
96
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
97
|
+
});
|
|
74
98
|
</script>
|
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
|
|
28
28
|
<script setup lang="ts">
|
|
29
29
|
import { AnimatePresence, motion } from "motion-v";
|
|
30
|
+
import { v4 as uuidv4 } from "uuid";
|
|
30
31
|
import { usePosition } from "~/composables";
|
|
31
32
|
import { onWatcherCleanup } from "vue";
|
|
32
33
|
import type { UixyPopoverProps } from "./Popover.types";
|
|
33
34
|
|
|
34
35
|
const props = defineProps<UixyPopoverProps>();
|
|
35
36
|
|
|
37
|
+
const POPOVER_GROUP_EVENT = "uixy:popover-group-open";
|
|
38
|
+
const instanceId = uuidv4();
|
|
39
|
+
|
|
36
40
|
const { active, handleOpen, handleLeave, styles, refElement } = usePosition({
|
|
37
41
|
direction: props.direction ?? "bottom",
|
|
38
42
|
});
|
|
@@ -45,22 +49,49 @@
|
|
|
45
49
|
handleLeave();
|
|
46
50
|
};
|
|
47
51
|
|
|
52
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
53
|
+
if (e.key === "Escape") handleLeave();
|
|
54
|
+
};
|
|
55
|
+
|
|
48
56
|
watch(
|
|
49
57
|
() => active.value,
|
|
50
|
-
(
|
|
58
|
+
(isActive, prevVal) => {
|
|
51
59
|
if (prevVal) return;
|
|
52
60
|
|
|
61
|
+
if (isActive && props.group) {
|
|
62
|
+
window.dispatchEvent(
|
|
63
|
+
new CustomEvent(POPOVER_GROUP_EVENT, { detail: { id: instanceId } }),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
requestAnimationFrame(() => {
|
|
54
68
|
window.addEventListener("click", handleClickOutside);
|
|
69
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
55
70
|
});
|
|
56
71
|
|
|
57
72
|
onWatcherCleanup(() => {
|
|
58
73
|
window.removeEventListener("click", handleClickOutside);
|
|
74
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
59
75
|
});
|
|
60
|
-
}
|
|
76
|
+
},
|
|
61
77
|
);
|
|
62
78
|
|
|
79
|
+
const handleGroupOpen = (e: Event) => {
|
|
80
|
+
const event = e as CustomEvent<{ id: string }>;
|
|
81
|
+
if (event.detail.id !== instanceId) handleLeave();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
onMounted(() => {
|
|
85
|
+
if (props.group) {
|
|
86
|
+
window.addEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
63
90
|
onUnmounted(() => {
|
|
64
91
|
window.removeEventListener("click", handleClickOutside);
|
|
92
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
93
|
+
if (props.group) {
|
|
94
|
+
window.removeEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
|
|
95
|
+
}
|
|
65
96
|
});
|
|
66
97
|
</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
|
+
};
|