v-uixy 1.4.0 → 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/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/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/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/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.json +36 -0
|
@@ -0,0 +1,219 @@
|
|
|
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="
|
|
23
|
+
triggerValueStyles({ placeholder: !model, disabled: props.disabled })
|
|
24
|
+
"
|
|
25
|
+
>
|
|
26
|
+
{{ displayValue }}
|
|
27
|
+
</span>
|
|
28
|
+
<uixy-icon
|
|
29
|
+
v-if="model && !props.disabled"
|
|
30
|
+
name="close"
|
|
31
|
+
class="h-4 w-4 shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white"
|
|
32
|
+
@click.stop="clear"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div v-if="!props.hideHelper && helperText" class="h-4">
|
|
37
|
+
<p :class="helperStyles({ status })">{{ helperText }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<teleport to="body">
|
|
41
|
+
<animate-presence>
|
|
42
|
+
<motion.div
|
|
43
|
+
v-if="active"
|
|
44
|
+
:initial="{ opacity: 0 }"
|
|
45
|
+
:animate="{ opacity: 1 }"
|
|
46
|
+
:exit="{ opacity: 0 }"
|
|
47
|
+
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
48
|
+
:style="positionStyles"
|
|
49
|
+
>
|
|
50
|
+
<div ref="refElement">
|
|
51
|
+
<motion.div
|
|
52
|
+
:initial="{ y: 4, scale: 0.96, opacity: 0 }"
|
|
53
|
+
:animate="{ y: 0, scale: 1, opacity: 1 }"
|
|
54
|
+
:exit="{ opacity: 0, scale: 0.96 }"
|
|
55
|
+
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
56
|
+
:class="panelStyles()"
|
|
57
|
+
>
|
|
58
|
+
<div class="flex items-stretch gap-3">
|
|
59
|
+
<uixy-calendar
|
|
60
|
+
:model-value="model"
|
|
61
|
+
:year="viewYear"
|
|
62
|
+
:month="viewMonth"
|
|
63
|
+
:start-of-week="props.startOfWeek"
|
|
64
|
+
:is-date-disabled="props.isDateDisabled"
|
|
65
|
+
@update:model-value="onDayPick"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<div v-if="props.withTime" class="flex items-center">
|
|
69
|
+
<uixy-time-wheel
|
|
70
|
+
v-model="timeParts"
|
|
71
|
+
:hour-cycle="24"
|
|
72
|
+
:with-seconds="props.withSeconds"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div :class="footerStyles()">
|
|
78
|
+
<uixy-button variant="tertiary" size="sm" @click="clear">
|
|
79
|
+
Clear
|
|
80
|
+
</uixy-button>
|
|
81
|
+
<uixy-button variant="primary" size="sm" @click="handleLeave">
|
|
82
|
+
Done
|
|
83
|
+
</uixy-button>
|
|
84
|
+
</div>
|
|
85
|
+
</motion.div>
|
|
86
|
+
</div>
|
|
87
|
+
</motion.div>
|
|
88
|
+
</animate-presence>
|
|
89
|
+
</teleport>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import { AnimatePresence, motion } from "motion-v";
|
|
95
|
+
import { usePosition } from "~/composables";
|
|
96
|
+
import { UixyIcon } from "../Icon";
|
|
97
|
+
import { UixyButton } from "../Button";
|
|
98
|
+
import { UixyCalendar } from "../Calendar";
|
|
99
|
+
import UixyTimeWheel from "./TimeWheel.vue";
|
|
100
|
+
import type { UixyDatePickerProps } from "./DatePicker.types";
|
|
101
|
+
import {
|
|
102
|
+
triggerStyles,
|
|
103
|
+
triggerValueStyles,
|
|
104
|
+
triggerIconStyles,
|
|
105
|
+
labelStyles,
|
|
106
|
+
helperStyles,
|
|
107
|
+
panelStyles,
|
|
108
|
+
footerStyles,
|
|
109
|
+
} from "./DatePicker.styles";
|
|
110
|
+
import {
|
|
111
|
+
formatDate,
|
|
112
|
+
timePartsOf,
|
|
113
|
+
withTimeParts,
|
|
114
|
+
type UixyTimeParts,
|
|
115
|
+
} from "./DatePicker.utils";
|
|
116
|
+
|
|
117
|
+
const props = defineProps<UixyDatePickerProps>();
|
|
118
|
+
|
|
119
|
+
const model = defineModel<Date | null>();
|
|
120
|
+
|
|
121
|
+
const {
|
|
122
|
+
active,
|
|
123
|
+
styles: positionStyles,
|
|
124
|
+
refElement,
|
|
125
|
+
handleOpen,
|
|
126
|
+
handleLeave,
|
|
127
|
+
} = usePosition({ direction: props.direction ?? "bottom" });
|
|
128
|
+
|
|
129
|
+
const triggerRef = ref<HTMLElement>();
|
|
130
|
+
|
|
131
|
+
const draftTime = ref<UixyTimeParts>(timePartsOf(model.value));
|
|
132
|
+
|
|
133
|
+
const status = computed(() => {
|
|
134
|
+
if (props.disabled) return "disabled";
|
|
135
|
+
|
|
136
|
+
return props.status ?? "default";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const helperText = computed(() => {
|
|
140
|
+
if (status.value === "error") return props.errorText ?? "";
|
|
141
|
+
|
|
142
|
+
return props.helperText ?? "";
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const displayValue = computed(() => {
|
|
146
|
+
if (!model.value) return props.placeholder ?? "Select date";
|
|
147
|
+
|
|
148
|
+
return formatDate(model.value, {
|
|
149
|
+
withTime: props.withTime,
|
|
150
|
+
withSeconds: props.withSeconds,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const viewBase = computed(() => model.value ?? new Date());
|
|
155
|
+
|
|
156
|
+
const viewYear = computed(() => viewBase.value.getFullYear());
|
|
157
|
+
|
|
158
|
+
const viewMonth = computed(() => viewBase.value.getMonth());
|
|
159
|
+
|
|
160
|
+
const timeParts = computed<UixyTimeParts>({
|
|
161
|
+
get: () => (model.value ? timePartsOf(model.value) : draftTime.value),
|
|
162
|
+
set: (parts) => {
|
|
163
|
+
draftTime.value = parts;
|
|
164
|
+
|
|
165
|
+
if (!model.value) return;
|
|
166
|
+
|
|
167
|
+
model.value = withTimeParts(model.value, parts);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const onTriggerClick = (event: MouseEvent | KeyboardEvent) => {
|
|
172
|
+
if (props.disabled) return;
|
|
173
|
+
|
|
174
|
+
handleOpen(event as MouseEvent);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onDayPick = (date: Date | null | undefined) => {
|
|
178
|
+
if (!date) return;
|
|
179
|
+
|
|
180
|
+
model.value = withTimeParts(date, draftTime.value);
|
|
181
|
+
|
|
182
|
+
if (!props.withTime && props.closeOnSelect) handleLeave();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const clear = () => {
|
|
186
|
+
model.value = null;
|
|
187
|
+
|
|
188
|
+
draftTime.value = { hour: 0, minute: 0, second: 0 };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
watch(active, (isActive) => {
|
|
192
|
+
if (isActive) draftTime.value = timePartsOf(model.value);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const onDocClick = (event: MouseEvent) => {
|
|
196
|
+
if (!active.value) return;
|
|
197
|
+
|
|
198
|
+
const target = event.target as Node;
|
|
199
|
+
|
|
200
|
+
const clickedInside =
|
|
201
|
+
refElement.value?.contains(target) || triggerRef.value?.contains(target);
|
|
202
|
+
|
|
203
|
+
if (!clickedInside) handleLeave();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
207
|
+
if (active.value && event.key === "Escape") handleLeave();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
onMounted(() => {
|
|
211
|
+
window.addEventListener("click", onDocClick);
|
|
212
|
+
window.addEventListener("keydown", onKeyDown);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
onUnmounted(() => {
|
|
216
|
+
window.removeEventListener("click", onDocClick);
|
|
217
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
218
|
+
});
|
|
219
|
+
</script>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import styles from "~/utils/styles";
|
|
2
|
+
|
|
3
|
+
export const triggerStyles = styles(
|
|
4
|
+
"relative flex w-full items-center gap-2 cursor-pointer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 pl-3 pr-3 py-2 text-sm min-h-[38px] outline-offset-1 outline-gray-300 dark:outline-gray-800 select-none",
|
|
5
|
+
{
|
|
6
|
+
status: {
|
|
7
|
+
default:
|
|
8
|
+
"bg-white dark:bg-gray-1000 hover:border-gray-400 dark:hover:border-gray-700",
|
|
9
|
+
error:
|
|
10
|
+
"bg-white dark:bg-gray-1000 border-error-700 dark:border-error-900 text-error-600 dark:text-error-500",
|
|
11
|
+
disabled:
|
|
12
|
+
"bg-gray-200 text-gray-400 dark:bg-gray-900 dark:text-gray-700 pointer-events-none cursor-not-allowed",
|
|
13
|
+
},
|
|
14
|
+
open: {
|
|
15
|
+
true: "border-gray-500 dark:border-gray-600",
|
|
16
|
+
false: "",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const triggerValueStyles = styles("flex-1 truncate text-left", {
|
|
22
|
+
placeholder: {
|
|
23
|
+
true: "text-gray-500 dark:text-gray-700",
|
|
24
|
+
false: "text-gray-900 dark:text-gray-100",
|
|
25
|
+
},
|
|
26
|
+
disabled: {
|
|
27
|
+
true: "text-gray-400 dark:text-gray-700",
|
|
28
|
+
false: "",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const triggerIconStyles = styles(
|
|
33
|
+
"h-4 w-4 shrink-0 text-gray-400 dark:text-gray-600",
|
|
34
|
+
{
|
|
35
|
+
disabled: {
|
|
36
|
+
true: "dark:text-gray-700",
|
|
37
|
+
false: "",
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const labelStyles = styles(
|
|
43
|
+
"transition-colors ease-in-out duration-150 text-sm font-500",
|
|
44
|
+
{
|
|
45
|
+
status: {
|
|
46
|
+
default: "",
|
|
47
|
+
error: "text-error-600",
|
|
48
|
+
disabled: "text-gray-500 dark:text-gray-700",
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const helperStyles = styles(
|
|
54
|
+
"transition-colors ease-in-out duration-150 text-xs",
|
|
55
|
+
{
|
|
56
|
+
status: {
|
|
57
|
+
default: "text-gray-600",
|
|
58
|
+
error: "text-error-600",
|
|
59
|
+
disabled: "text-gray-400 dark:text-gray-800",
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const panelStyles = styles(
|
|
65
|
+
"flex flex-col gap-3 rounded-2 border border-gray-300 bg-gray-100 p-3 shadow-lg dark:border-gray-900 dark:bg-black"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const sectionLabelStyles = styles(
|
|
69
|
+
"text-xs font-500 text-gray-500 dark:text-gray-600"
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const footerStyles = styles(
|
|
73
|
+
"flex items-center justify-between gap-2"
|
|
74
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IconName } from "~/types/icons";
|
|
2
|
+
|
|
3
|
+
export interface UixyDatePickerProps {
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
helperText?: string;
|
|
7
|
+
errorText?: string;
|
|
8
|
+
status?: "default" | "error";
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
icon?: IconName;
|
|
11
|
+
withTime?: boolean;
|
|
12
|
+
withSeconds?: boolean;
|
|
13
|
+
startOfWeek?: "Sunday" | "Monday";
|
|
14
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
15
|
+
direction?: "top" | "bottom";
|
|
16
|
+
hideHelper?: boolean;
|
|
17
|
+
closeOnSelect?: boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const pad2 = (n: number) => (n < 10 ? `0${n}` : `${n}`);
|
|
2
|
+
|
|
3
|
+
export const startOfDay = (date: Date) =>
|
|
4
|
+
new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
5
|
+
|
|
6
|
+
export interface UixyTimeParts {
|
|
7
|
+
hour: number;
|
|
8
|
+
minute: number;
|
|
9
|
+
second: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const timePartsOf = (date?: Date | null): UixyTimeParts => ({
|
|
13
|
+
hour: date?.getHours() ?? 0,
|
|
14
|
+
minute: date?.getMinutes() ?? 0,
|
|
15
|
+
second: date?.getSeconds() ?? 0,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const withTimeParts = (day: Date, time: UixyTimeParts) =>
|
|
19
|
+
new Date(
|
|
20
|
+
day.getFullYear(),
|
|
21
|
+
day.getMonth(),
|
|
22
|
+
day.getDate(),
|
|
23
|
+
time.hour,
|
|
24
|
+
time.minute,
|
|
25
|
+
time.second
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export interface UixyDateFormatOptions {
|
|
29
|
+
withTime?: boolean;
|
|
30
|
+
withSeconds?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const formatDate = (
|
|
34
|
+
date?: Date | null,
|
|
35
|
+
options: UixyDateFormatOptions = {}
|
|
36
|
+
) => {
|
|
37
|
+
if (!date) return "";
|
|
38
|
+
|
|
39
|
+
const { withTime = false, withSeconds = false } = options;
|
|
40
|
+
const datePart = `${pad2(date.getDate())}-${pad2(date.getMonth() + 1)}-${date.getFullYear()}`;
|
|
41
|
+
|
|
42
|
+
if (!withTime) return datePart;
|
|
43
|
+
|
|
44
|
+
const tail = withSeconds ? `:${pad2(date.getSeconds())}` : "";
|
|
45
|
+
return `${datePart} ${pad2(date.getHours())}:${pad2(date.getMinutes())}${tail}`;
|
|
46
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="relative mx-auto w-fit"
|
|
4
|
+
:style="{ height: `${ITEM_HEIGHT * VISIBLE}px` }"
|
|
5
|
+
>
|
|
6
|
+
<div
|
|
7
|
+
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
8
|
+
>
|
|
9
|
+
<div
|
|
10
|
+
class="w-full rounded-1 bg-gray-200/80 dark:bg-gray-900/80"
|
|
11
|
+
:style="{ height: `${ITEM_HEIGHT}px` }"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="pointer-events-none absolute inset-x-0 top-0 z-10 bg-gradient-to-b from-gray-100 to-transparent dark:from-black"
|
|
17
|
+
:style="{ height: `${ITEM_HEIGHT * 1.6}px` }"
|
|
18
|
+
/>
|
|
19
|
+
<div
|
|
20
|
+
class="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-gray-100 to-transparent dark:from-black"
|
|
21
|
+
:style="{ height: `${ITEM_HEIGHT * 1.6}px` }"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<div class="relative flex items-stretch gap-1 text-sm">
|
|
25
|
+
<div class="w-11">
|
|
26
|
+
<time-wheel-column
|
|
27
|
+
v-model="hour"
|
|
28
|
+
:values="hourValues"
|
|
29
|
+
:format="pad"
|
|
30
|
+
:item-height="ITEM_HEIGHT"
|
|
31
|
+
:visible-count="VISIBLE"
|
|
32
|
+
:disabled="props.disabled"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="w-11">
|
|
36
|
+
<time-wheel-column
|
|
37
|
+
v-model="minute"
|
|
38
|
+
:values="minuteValues"
|
|
39
|
+
:format="pad"
|
|
40
|
+
:item-height="ITEM_HEIGHT"
|
|
41
|
+
:visible-count="VISIBLE"
|
|
42
|
+
:disabled="props.disabled"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div v-if="props.withSeconds" class="w-11">
|
|
46
|
+
<time-wheel-column
|
|
47
|
+
v-model="second"
|
|
48
|
+
:values="minuteValues"
|
|
49
|
+
:format="pad"
|
|
50
|
+
:item-height="ITEM_HEIGHT"
|
|
51
|
+
:visible-count="VISIBLE"
|
|
52
|
+
:disabled="props.disabled"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-if="is12" class="w-12">
|
|
56
|
+
<time-wheel-column
|
|
57
|
+
v-model="period"
|
|
58
|
+
:values="periodValues"
|
|
59
|
+
:item-height="ITEM_HEIGHT"
|
|
60
|
+
:visible-count="VISIBLE"
|
|
61
|
+
:disabled="props.disabled"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import TimeWheelColumn from "./TimeWheelColumn.vue";
|
|
70
|
+
import { pad2, type UixyTimeParts } from "./DatePicker.utils";
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(
|
|
73
|
+
defineProps<{
|
|
74
|
+
hourCycle?: 12 | 24;
|
|
75
|
+
withSeconds?: boolean;
|
|
76
|
+
disabled?: boolean;
|
|
77
|
+
}>(),
|
|
78
|
+
{ hourCycle: 12 },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const model = defineModel<UixyTimeParts>({
|
|
82
|
+
default: () => ({ hour: 0, minute: 0, second: 0 }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const ITEM_HEIGHT = 34;
|
|
86
|
+
const VISIBLE = 5;
|
|
87
|
+
|
|
88
|
+
const pad = (value: number | string) => pad2(Number(value));
|
|
89
|
+
|
|
90
|
+
const is12 = computed(() => props.hourCycle === 12);
|
|
91
|
+
|
|
92
|
+
const hourValues = computed(() =>
|
|
93
|
+
is12.value
|
|
94
|
+
? Array.from({ length: 12 }, (_, i) => i + 1)
|
|
95
|
+
: Array.from({ length: 24 }, (_, i) => i),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const minuteValues = Array.from({ length: 60 }, (_, i) => i);
|
|
99
|
+
|
|
100
|
+
const periodValues = ["AM", "PM"];
|
|
101
|
+
|
|
102
|
+
const period = computed<string>({
|
|
103
|
+
get: () => (model.value.hour < 12 ? "AM" : "PM"),
|
|
104
|
+
set: (value) => {
|
|
105
|
+
const base = model.value.hour % 12;
|
|
106
|
+
let hour = base;
|
|
107
|
+
|
|
108
|
+
switch (value) {
|
|
109
|
+
case "PM":
|
|
110
|
+
hour = base + 12;
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
hour = base;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
model.value = { ...model.value, hour };
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const hour = computed<number>({
|
|
121
|
+
get: () =>
|
|
122
|
+
is12.value ? ((model.value.hour + 11) % 12) + 1 : model.value.hour,
|
|
123
|
+
set: (value) => {
|
|
124
|
+
const next = Number(value);
|
|
125
|
+
|
|
126
|
+
if (!is12.value) {
|
|
127
|
+
model.value = { ...model.value, hour: next };
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hour24 = (next % 12) + (period.value === "PM" ? 12 : 0);
|
|
132
|
+
model.value = { ...model.value, hour: hour24 };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const minute = computed<number>({
|
|
137
|
+
get: () => model.value.minute,
|
|
138
|
+
set: (value) => (model.value = { ...model.value, minute: Number(value) }),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const second = computed<number>({
|
|
142
|
+
get: () => model.value.second,
|
|
143
|
+
set: (value) => (model.value = { ...model.value, second: Number(value) }),
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="containerRef"
|
|
4
|
+
class="relative overflow-y-scroll [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
5
|
+
:class="props.disabled ? 'pointer-events-none opacity-40' : ''"
|
|
6
|
+
:style="{ height: `${itemHeight * visibleCount}px`, scrollSnapType: 'y mandatory' }"
|
|
7
|
+
@scroll="onScroll"
|
|
8
|
+
>
|
|
9
|
+
<div :style="{ height: `${spacer}px` }" aria-hidden="true" />
|
|
10
|
+
<button
|
|
11
|
+
v-for="(value, index) in props.values"
|
|
12
|
+
:key="index"
|
|
13
|
+
type="button"
|
|
14
|
+
class="flex w-full items-center justify-center font-500 tabular-nums select-none transition-colors"
|
|
15
|
+
style="scroll-snap-align: center"
|
|
16
|
+
:style="itemStyle(index)"
|
|
17
|
+
@click="selectIndex(index)"
|
|
18
|
+
>
|
|
19
|
+
{{ props.format ? props.format(value) : value }}
|
|
20
|
+
</button>
|
|
21
|
+
<div :style="{ height: `${spacer}px` }" aria-hidden="true" />
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const props = withDefaults(
|
|
27
|
+
defineProps<{
|
|
28
|
+
values: (number | string)[];
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
itemHeight?: number;
|
|
31
|
+
visibleCount?: number;
|
|
32
|
+
format?: (value: number | string) => string;
|
|
33
|
+
}>(),
|
|
34
|
+
{
|
|
35
|
+
itemHeight: 34,
|
|
36
|
+
visibleCount: 5,
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const model = defineModel<number | string>();
|
|
41
|
+
|
|
42
|
+
const containerRef = ref<HTMLElement>();
|
|
43
|
+
|
|
44
|
+
const scrollTop = ref(0);
|
|
45
|
+
|
|
46
|
+
const itemHeight = computed(() => props.itemHeight);
|
|
47
|
+
|
|
48
|
+
const visibleCount = computed(() => props.visibleCount);
|
|
49
|
+
|
|
50
|
+
const spacer = computed(
|
|
51
|
+
() => (itemHeight.value * (visibleCount.value - 1)) / 2
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const currentIndex = computed(() => {
|
|
55
|
+
const idx = props.values.findIndex((v) => v === model.value);
|
|
56
|
+
return idx === -1 ? 0 : idx;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const itemStyle = (index: number) => {
|
|
60
|
+
const distance =
|
|
61
|
+
Math.abs(index * itemHeight.value - scrollTop.value) / itemHeight.value;
|
|
62
|
+
const opacity = Math.max(0.25, 1 - distance * 0.32);
|
|
63
|
+
const scale = Math.max(0.78, 1 - distance * 0.12);
|
|
64
|
+
return {
|
|
65
|
+
height: `${itemHeight.value}px`,
|
|
66
|
+
lineHeight: `${itemHeight.value}px`,
|
|
67
|
+
opacity,
|
|
68
|
+
transform: `scale(${scale})`,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let settleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
73
|
+
let rafId = 0;
|
|
74
|
+
|
|
75
|
+
const onScroll = () => {
|
|
76
|
+
const el = containerRef.value;
|
|
77
|
+
if (!el) return;
|
|
78
|
+
|
|
79
|
+
if (!rafId) {
|
|
80
|
+
rafId = requestAnimationFrame(() => {
|
|
81
|
+
scrollTop.value = el.scrollTop;
|
|
82
|
+
rafId = 0;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
87
|
+
settleTimer = setTimeout(settle, 110);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const settle = () => {
|
|
91
|
+
const el = containerRef.value;
|
|
92
|
+
if (!el) return;
|
|
93
|
+
|
|
94
|
+
const index = Math.max(
|
|
95
|
+
0,
|
|
96
|
+
Math.min(props.values.length - 1, Math.round(el.scrollTop / itemHeight.value))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const value = props.values[index];
|
|
100
|
+
if (value !== undefined && value !== model.value) model.value = value;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const scrollToIndex = (index: number) => {
|
|
104
|
+
const el = containerRef.value;
|
|
105
|
+
if (!el) return;
|
|
106
|
+
|
|
107
|
+
const top = index * itemHeight.value;
|
|
108
|
+
if (Math.abs(el.scrollTop - top) < 1) return;
|
|
109
|
+
|
|
110
|
+
el.scrollTo({ top, behavior: "smooth" });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const selectIndex = (index: number) => {
|
|
114
|
+
if (props.disabled) return;
|
|
115
|
+
|
|
116
|
+
const value = props.values[index];
|
|
117
|
+
if (value !== undefined && value !== model.value) model.value = value;
|
|
118
|
+
|
|
119
|
+
scrollToIndex(index);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
onMounted(() => {
|
|
123
|
+
nextTick(() => {
|
|
124
|
+
const el = containerRef.value;
|
|
125
|
+
if (el) {
|
|
126
|
+
el.scrollTop = currentIndex.value * itemHeight.value;
|
|
127
|
+
scrollTop.value = el.scrollTop;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
watch(
|
|
133
|
+
() => model.value,
|
|
134
|
+
() => {
|
|
135
|
+
const el = containerRef.value;
|
|
136
|
+
if (!el) return;
|
|
137
|
+
|
|
138
|
+
const target = currentIndex.value * itemHeight.value;
|
|
139
|
+
if (Math.abs(el.scrollTop - target) > 1) scrollToIndex(currentIndex.value);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
onUnmounted(() => {
|
|
144
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
145
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
146
|
+
});
|
|
147
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import UixyDatePicker from "./DatePicker.component.vue";
|
|
2
|
+
import UixyTimeWheel from "./TimeWheel.vue";
|
|
3
|
+
import { type UixyDatePickerProps } from "./DatePicker.types";
|
|
4
|
+
import { type UixyTimeParts } from "./DatePicker.utils";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
UixyDatePicker,
|
|
8
|
+
UixyTimeWheel,
|
|
9
|
+
type UixyDatePickerProps,
|
|
10
|
+
type UixyTimeParts,
|
|
11
|
+
};
|