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,18 @@
|
|
|
1
|
+
export type UixyChartSeries = {
|
|
2
|
+
label: string;
|
|
3
|
+
values: number[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type UixyChartData = {
|
|
7
|
+
label: string;
|
|
8
|
+
value: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface UixyChartProps {
|
|
12
|
+
series?: UixyChartSeries[];
|
|
13
|
+
data?: UixyChartData[];
|
|
14
|
+
color?: string;
|
|
15
|
+
baseColor?: string;
|
|
16
|
+
rows?: number;
|
|
17
|
+
animate?: boolean;
|
|
18
|
+
}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
<label :class="checkboxStyles($attrs.class as string)">
|
|
3
3
|
<div class="relative flex items-center justify-center">
|
|
4
4
|
<input
|
|
5
|
+
ref="inputRef"
|
|
5
6
|
type="checkbox"
|
|
6
7
|
:disabled="props.disabled"
|
|
7
8
|
v-model="checked"
|
|
8
|
-
class="
|
|
9
|
+
:class="inputStyles({ indeterminate: !!props.indeterminate })"
|
|
9
10
|
/>
|
|
10
11
|
<animate-presence>
|
|
11
12
|
<motion.div
|
|
12
|
-
v-if="checked"
|
|
13
|
+
v-if="checked || props.indeterminate"
|
|
13
14
|
:initial="{ opacity: 0 }"
|
|
14
15
|
:animate="{ opacity: 1 }"
|
|
15
16
|
:exit="{ opacity: 0 }"
|
|
@@ -17,11 +18,11 @@
|
|
|
17
18
|
class="pointer-events-none absolute"
|
|
18
19
|
>
|
|
19
20
|
<uixy-icon
|
|
20
|
-
name="check"
|
|
21
|
+
:name="props.indeterminate ? 'minus' : 'check'"
|
|
21
22
|
:class="
|
|
22
23
|
iconStyles({
|
|
23
24
|
disabled: !!props.disabled,
|
|
24
|
-
checked: checked,
|
|
25
|
+
checked: checked || props.indeterminate,
|
|
25
26
|
})
|
|
26
27
|
"
|
|
27
28
|
/>
|
|
@@ -41,9 +42,22 @@
|
|
|
41
42
|
import { motion, AnimatePresence } from "motion-v";
|
|
42
43
|
import { UixyIcon } from "../Icon";
|
|
43
44
|
import type { UixyCheckboxProps } from "./Checkbox.types";
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
checkboxStyles,
|
|
47
|
+
inputStyles,
|
|
48
|
+
iconStyles,
|
|
49
|
+
labelStyles,
|
|
50
|
+
} from "./Checkbox.styles";
|
|
45
51
|
|
|
46
52
|
const props = defineProps<UixyCheckboxProps>();
|
|
47
53
|
|
|
48
54
|
const checked = defineModel<boolean>("checked");
|
|
55
|
+
|
|
56
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
57
|
+
|
|
58
|
+
watchEffect(() => {
|
|
59
|
+
if (inputRef.value) {
|
|
60
|
+
inputRef.value.indeterminate = !!props.indeterminate;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
49
63
|
</script>
|
|
@@ -2,6 +2,16 @@ import styles from "~/utils/styles";
|
|
|
2
2
|
|
|
3
3
|
export const checkboxStyles = styles("flex items-center gap-2");
|
|
4
4
|
|
|
5
|
+
export const inputStyles = styles(
|
|
6
|
+
"size-4 cursor-pointer appearance-none rounded-1 border border-black transition-all duration-150 ease-in-out checked:bg-black hover:bg-gray-300 checked:hover:bg-gray-800 disabled:pointer-events-none disabled:border-gray-500 disabled:bg-gray-300 checked:disabled:bg-gray-500 dark:border-gray-100 dark:checked:bg-gray-100 dark:hover:bg-gray-800 dark:checked:hover:bg-gray-200 dark:disabled:border-gray-600 dark:disabled:bg-gray-900 dark:checked:disabled:bg-gray-600",
|
|
7
|
+
{
|
|
8
|
+
indeterminate: {
|
|
9
|
+
true: "bg-black hover:bg-gray-800 disabled:bg-gray-300 dark:bg-gray-100 dark:hover:bg-gray-200 dark:disabled:bg-gray-900",
|
|
10
|
+
false: "",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
5
15
|
export const iconStyles = styles("h-3 w-3", {
|
|
6
16
|
disabled: {
|
|
7
17
|
true: "text-gray-500",
|
|
@@ -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>
|