v-uixy 1.3.1 → 1.4.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/Avatar/Avatar.component.vue +7 -3
- package/templates/components/Avatar/Avatar.styles.ts +3 -3
- package/templates/components/Button/Button.styles.ts +3 -3
- package/templates/components/Calendar/Calendar.component.vue +26 -15
- package/templates/components/Calendar/Calendar.styles.ts +11 -3
- package/templates/components/Calendar/Calendar.types.ts +1 -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/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/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/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 +1 -1
- package/templates/composables/usePosition.ts +17 -2
|
@@ -1,33 +1,238 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
onWatcherCleanup,
|
|
3
|
+
watch,
|
|
4
|
+
computed,
|
|
5
|
+
ref,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from "vue";
|
|
9
|
+
import type { UixySelectOptionType, UixySelectProps } from "../Select.types";
|
|
3
10
|
|
|
4
|
-
|
|
11
|
+
const normalizeForSearch = (str: string): string =>
|
|
12
|
+
str.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
13
|
+
|
|
14
|
+
const getValue = (option: UixySelectOptionType): string =>
|
|
15
|
+
option.value ?? option.label;
|
|
16
|
+
|
|
17
|
+
const collectLeafValues = (option: UixySelectOptionType): string[] => {
|
|
18
|
+
if (!option.children || option.children.length === 0)
|
|
19
|
+
return [getValue(option)];
|
|
20
|
+
return option.children.flatMap(collectLeafValues);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const findInTree = (
|
|
5
24
|
options: UixySelectOptionType[],
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
value: string,
|
|
26
|
+
): UixySelectOptionType | null => {
|
|
27
|
+
for (const opt of options) {
|
|
28
|
+
if (getValue(opt) === value) return opt;
|
|
29
|
+
if (opt.children?.length) {
|
|
30
|
+
const found = findInTree(opt.children, value);
|
|
31
|
+
if (found) return found;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const useSelect = <T extends string | string[]>(
|
|
38
|
+
props: UixySelectProps,
|
|
39
|
+
model: Ref<T>,
|
|
40
|
+
emit: (event: "change", v: string) => void,
|
|
41
|
+
dropdownRef?: Ref<HTMLElement | undefined>,
|
|
8
42
|
) => {
|
|
9
43
|
const open = ref(false);
|
|
10
44
|
const search = ref("");
|
|
11
45
|
const refOptions = ref<HTMLDivElement>();
|
|
46
|
+
const lastScrollAt = ref(0);
|
|
47
|
+
const expandedItems = ref<string[]>([]);
|
|
12
48
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
49
|
+
const filterTreeOptions = (
|
|
50
|
+
items: UixySelectOptionType[],
|
|
51
|
+
searchTerm: string,
|
|
52
|
+
): UixySelectOptionType[] => {
|
|
53
|
+
if (!searchTerm) return items;
|
|
54
|
+
const normSearch = normalizeForSearch(searchTerm);
|
|
55
|
+
const filterNode = (
|
|
56
|
+
node: UixySelectOptionType,
|
|
57
|
+
): UixySelectOptionType | null => {
|
|
58
|
+
const matches = normalizeForSearch(node.label).includes(normSearch);
|
|
59
|
+
let filteredChildren: UixySelectOptionType[] = [];
|
|
60
|
+
if (node.children?.length) {
|
|
61
|
+
filteredChildren = node.children
|
|
62
|
+
.map(filterNode)
|
|
63
|
+
.filter((n): n is UixySelectOptionType => n !== null);
|
|
64
|
+
}
|
|
65
|
+
if (matches || filteredChildren.length > 0) {
|
|
66
|
+
return { ...node, children: filteredChildren };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
return items
|
|
71
|
+
.map(filterNode)
|
|
72
|
+
.filter((n): n is UixySelectOptionType => n !== null);
|
|
73
|
+
};
|
|
18
74
|
|
|
19
|
-
const
|
|
20
|
-
|
|
75
|
+
const filteredOptions = computed(() => {
|
|
76
|
+
const list = props.options ?? [];
|
|
77
|
+
if (props.tree) return filterTreeOptions(list, search.value);
|
|
78
|
+
return list.filter((option) =>
|
|
79
|
+
normalizeForSearch(option.label).includes(
|
|
80
|
+
normalizeForSearch(search.value),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const collectParentsWithMatchingDescendants = (
|
|
86
|
+
items: UixySelectOptionType[],
|
|
87
|
+
searchTerm: string,
|
|
88
|
+
): string[] => {
|
|
89
|
+
if (!searchTerm) return [];
|
|
90
|
+
const lowerSearch = searchTerm.toLowerCase();
|
|
91
|
+
const parentsToExpand: string[] = [];
|
|
92
|
+
const checkNode = (node: UixySelectOptionType): boolean => {
|
|
93
|
+
const nodeMatches = node.label.toLowerCase().includes(lowerSearch);
|
|
94
|
+
let hasMatchingDescendant = false;
|
|
95
|
+
if (node.children?.length) {
|
|
96
|
+
for (const child of node.children) {
|
|
97
|
+
if (checkNode(child)) hasMatchingDescendant = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hasMatchingDescendant) parentsToExpand.push(getValue(node));
|
|
101
|
+
return nodeMatches || hasMatchingDescendant;
|
|
102
|
+
};
|
|
103
|
+
for (const item of items) checkNode(item);
|
|
104
|
+
return parentsToExpand;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const effectiveExpandedItems = computed(() => {
|
|
108
|
+
if (!search.value) return expandedItems.value;
|
|
109
|
+
const autoExpanded = collectParentsWithMatchingDescendants(
|
|
110
|
+
props.options,
|
|
111
|
+
search.value,
|
|
112
|
+
);
|
|
113
|
+
return Array.from(new Set([...expandedItems.value, ...autoExpanded]));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const toggleExpand = (value: string) => {
|
|
117
|
+
const idx = expandedItems.value.indexOf(value);
|
|
118
|
+
if (idx >= 0) expandedItems.value.splice(idx, 1);
|
|
119
|
+
else expandedItems.value.push(value);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const isSelected = (option: UixySelectOptionType): boolean => {
|
|
123
|
+
const v = getValue(option);
|
|
124
|
+
if (props.tree && props.multiple && option.children?.length) {
|
|
125
|
+
const leaves = collectLeafValues(option);
|
|
126
|
+
const arr = (model.value as string[]) ?? [];
|
|
127
|
+
return leaves.length > 0 && leaves.every((l) => arr.includes(l));
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(model.value)
|
|
130
|
+
? (model.value as string[]).includes(v)
|
|
131
|
+
: (model.value as string) === v;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const isPartiallySelected = (option: UixySelectOptionType): boolean => {
|
|
135
|
+
if (!props.tree || !props.multiple || !option.children?.length)
|
|
136
|
+
return false;
|
|
137
|
+
const leaves = collectLeafValues(option);
|
|
138
|
+
const arr = (model.value as string[]) ?? [];
|
|
139
|
+
const selected = leaves.filter((l) => arr.includes(l)).length;
|
|
140
|
+
return selected > 0 && selected < leaves.length;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const setMultiValues = (next: string[]) => {
|
|
144
|
+
(model.value as string[]) = next;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const toggleMultiValue = (v: string) => {
|
|
148
|
+
const prev = Array.isArray(model.value) ? (model.value as string[]) : [];
|
|
149
|
+
if (prev.includes(v)) {
|
|
150
|
+
setMultiValues(prev.filter((item) => item !== v));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (props.max && prev.length >= props.max) {
|
|
154
|
+
setMultiValues([...prev.slice(1), v]);
|
|
21
155
|
return;
|
|
156
|
+
}
|
|
157
|
+
setMultiValues([...prev, v]);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const cascadeMultiTree = (option: UixySelectOptionType) => {
|
|
161
|
+
const leaves = collectLeafValues(option);
|
|
162
|
+
const prev = Array.isArray(model.value) ? (model.value as string[]) : [];
|
|
163
|
+
const allSelected = leaves.every((l) => prev.includes(l));
|
|
164
|
+
if (allSelected) {
|
|
165
|
+
setMultiValues(prev.filter((v) => !leaves.includes(v)));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const merged = Array.from(new Set([...prev, ...leaves]));
|
|
169
|
+
if (props.max && merged.length > props.max) {
|
|
170
|
+
setMultiValues(merged.slice(merged.length - props.max));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setMultiValues(merged);
|
|
174
|
+
};
|
|
22
175
|
|
|
176
|
+
const applyChange = (v: string) => {
|
|
177
|
+
if (!props.multiple) {
|
|
178
|
+
(model.value as string) = v;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (props.tree) {
|
|
182
|
+
const node = findInTree(props.options, v);
|
|
183
|
+
if (node?.children?.length) {
|
|
184
|
+
cascadeMultiTree(node);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
toggleMultiValue(v);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleClick = (value: string) => {
|
|
192
|
+
applyChange(value);
|
|
193
|
+
emit("change", value);
|
|
194
|
+
if (props.clearSearchOnSelect) search.value = "";
|
|
195
|
+
if (!props.multiple) open.value = false;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleClickSelectedOption = (item: string) => {
|
|
199
|
+
applyChange(item);
|
|
200
|
+
emit("change", item);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleDeselect = () => {
|
|
204
|
+
(model.value as string) = "";
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const getLabel = (value: string): string => {
|
|
208
|
+
if (!value) return "";
|
|
209
|
+
return findInTree(props.options, value)?.label ?? value;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const customAddVisible = computed(() => {
|
|
213
|
+
if (!props.allowCustom || !search.value) return false;
|
|
214
|
+
const lowerSearch = search.value.toLowerCase();
|
|
215
|
+
return !filteredOptions.value.some(
|
|
216
|
+
(opt) => opt.label.toLowerCase() === lowerSearch,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const handleCustomAdd = () => {
|
|
221
|
+
handleClick(search.value);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleOpen = (e?: MouseEvent) => {
|
|
225
|
+
if (
|
|
226
|
+
props.disabled ||
|
|
227
|
+
(e && "badge" in (e?.target as HTMLDivElement).dataset)
|
|
228
|
+
)
|
|
229
|
+
return;
|
|
23
230
|
open.value = !open.value;
|
|
24
231
|
};
|
|
25
232
|
|
|
26
233
|
const handleKeydownSelect = (e: KeyboardEvent) => {
|
|
27
234
|
const key = e.key;
|
|
28
|
-
|
|
29
235
|
if (key === "Tab") return;
|
|
30
|
-
|
|
31
236
|
if (key === "ArrowDown" || key === "Enter" || key === " ") {
|
|
32
237
|
e.preventDefault();
|
|
33
238
|
open.value = !open.value;
|
|
@@ -36,73 +241,76 @@ export const useSelect = <T>(
|
|
|
36
241
|
|
|
37
242
|
const handleKeydownOption = (
|
|
38
243
|
e: KeyboardEvent,
|
|
39
|
-
option: UixySelectOptionType
|
|
244
|
+
option: UixySelectOptionType,
|
|
40
245
|
) => {
|
|
41
246
|
const key = e.key;
|
|
42
|
-
|
|
43
247
|
if ((key === "Enter" || key === " ") && !option.disabled) {
|
|
44
248
|
e.preventDefault();
|
|
45
|
-
return
|
|
249
|
+
return handleClick(getValue(option));
|
|
46
250
|
}
|
|
47
|
-
|
|
48
251
|
if (key !== "ArrowDown" && key !== "ArrowUp") return;
|
|
49
|
-
|
|
50
|
-
const container = refOptions.value;
|
|
51
|
-
|
|
252
|
+
const container = dropdownRef?.value ?? refOptions.value;
|
|
52
253
|
if (!container) return;
|
|
53
|
-
|
|
54
254
|
const focusables = Array.from(
|
|
55
255
|
container.querySelectorAll<HTMLElement>(
|
|
56
|
-
"[select-dropdown] input, [select-dropdown] li"
|
|
57
|
-
)
|
|
256
|
+
"[select-dropdown] input, [select-dropdown] li",
|
|
257
|
+
),
|
|
58
258
|
).filter(
|
|
59
259
|
(el) =>
|
|
60
260
|
el.tabIndex >= 0 &&
|
|
61
261
|
el.getAttribute("aria-disabled") !== "true" &&
|
|
62
|
-
!el.hasAttribute("disabled")
|
|
262
|
+
!el.hasAttribute("disabled"),
|
|
63
263
|
);
|
|
64
|
-
|
|
65
264
|
if (!focusables.length) return;
|
|
66
|
-
|
|
67
265
|
const currentEl = (e.currentTarget as HTMLElement) || null;
|
|
68
266
|
let index = currentEl ? focusables.indexOf(currentEl) : -1;
|
|
69
|
-
|
|
70
267
|
e.preventDefault();
|
|
71
|
-
|
|
72
268
|
const val = key === "ArrowDown" ? 1 : -1;
|
|
73
|
-
|
|
74
269
|
index = (index + val + focusables.length) % focusables.length;
|
|
75
|
-
|
|
76
|
-
const nextEl = focusables[index];
|
|
77
|
-
nextEl?.focus();
|
|
270
|
+
focusables[index]?.focus();
|
|
78
271
|
};
|
|
79
272
|
|
|
80
|
-
const
|
|
81
|
-
|
|
273
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
274
|
+
if (Date.now() - lastScrollAt.value < 250) return;
|
|
275
|
+
const target = e.target as Node;
|
|
276
|
+
if (refOptions.value?.contains(target)) return;
|
|
277
|
+
if (dropdownRef?.value?.contains(target)) return;
|
|
278
|
+
const el = target as HTMLElement;
|
|
279
|
+
if (el.closest?.("[select-dropdown]")) return;
|
|
280
|
+
open.value = false;
|
|
82
281
|
};
|
|
83
282
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
open.value = false;
|
|
283
|
+
const markScroll = () => {
|
|
284
|
+
lastScrollAt.value = Date.now();
|
|
87
285
|
};
|
|
88
286
|
|
|
89
287
|
watch(
|
|
90
288
|
() => open.value,
|
|
91
|
-
(
|
|
289
|
+
(isOpen, prevVal) => {
|
|
290
|
+
if (!isOpen) {
|
|
291
|
+
expandedItems.value = [];
|
|
292
|
+
}
|
|
92
293
|
if (prevVal) return;
|
|
93
|
-
|
|
94
294
|
requestAnimationFrame(() => {
|
|
95
295
|
window.addEventListener("click", handleClickOutside);
|
|
296
|
+
window.addEventListener("scroll", markScroll, true);
|
|
297
|
+
window.addEventListener("wheel", markScroll, { passive: true });
|
|
298
|
+
window.addEventListener("touchmove", markScroll, { passive: true });
|
|
96
299
|
});
|
|
97
|
-
|
|
98
300
|
onWatcherCleanup(() => {
|
|
99
301
|
window.removeEventListener("click", handleClickOutside);
|
|
302
|
+
window.removeEventListener("scroll", markScroll, true);
|
|
303
|
+
window.removeEventListener("wheel", markScroll as any);
|
|
304
|
+
window.removeEventListener("touchmove", markScroll as any);
|
|
100
305
|
});
|
|
101
|
-
}
|
|
306
|
+
},
|
|
102
307
|
);
|
|
103
308
|
|
|
104
309
|
onUnmounted(() => {
|
|
105
310
|
window.removeEventListener("click", handleClickOutside);
|
|
311
|
+
window.removeEventListener("scroll", markScroll, true);
|
|
312
|
+
window.removeEventListener("wheel", markScroll as any);
|
|
313
|
+
window.removeEventListener("touchmove", markScroll as any);
|
|
106
314
|
});
|
|
107
315
|
|
|
108
316
|
return {
|
|
@@ -110,9 +318,19 @@ export const useSelect = <T>(
|
|
|
110
318
|
search,
|
|
111
319
|
refOptions,
|
|
112
320
|
filteredOptions,
|
|
321
|
+
expandedItems,
|
|
322
|
+
effectiveExpandedItems,
|
|
323
|
+
customAddVisible,
|
|
324
|
+
isSelected,
|
|
325
|
+
isPartiallySelected,
|
|
326
|
+
getLabel,
|
|
327
|
+
toggleExpand,
|
|
113
328
|
handleOpen,
|
|
114
329
|
handleKeydownSelect,
|
|
115
330
|
handleKeydownOption,
|
|
116
331
|
handleClickSelectedOption,
|
|
332
|
+
handleClick,
|
|
333
|
+
handleDeselect,
|
|
334
|
+
handleCustomAdd,
|
|
117
335
|
};
|
|
118
336
|
};
|
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
<slot name="nav-extra" />
|
|
25
25
|
<div>
|
|
26
|
-
<
|
|
26
|
+
<template v-if="props.keepAlive">
|
|
27
|
+
<div
|
|
28
|
+
v-for="tab in props.tabs"
|
|
29
|
+
:key="tab.value"
|
|
30
|
+
v-show="active === tab.value"
|
|
31
|
+
>
|
|
32
|
+
<slot :name="tab.value" />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
<animate-presence v-else mode="wait" :initial="false">
|
|
27
36
|
<motion.div
|
|
28
37
|
:key="active"
|
|
29
38
|
:initial="{ opacity: 0, y: 4 }"
|
|
@@ -55,6 +64,6 @@
|
|
|
55
64
|
const layoutId = computed(() => `uixy-tabs-${uuidv4()}`);
|
|
56
65
|
|
|
57
66
|
onBeforeMount(() => {
|
|
58
|
-
if (props.tabs.length > 0) active.value = props.tabs[0]
|
|
67
|
+
if (props.tabs.length > 0) active.value = props.tabs[0]?.value;
|
|
59
68
|
});
|
|
60
69
|
</script>
|
|
@@ -9,44 +9,35 @@
|
|
|
9
9
|
:auto-focus="props.autoFocus"
|
|
10
10
|
:rows="props.rows ?? 3"
|
|
11
11
|
:max-length="props.maxLength"
|
|
12
|
-
:class="
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
:class="
|
|
13
|
+
textareaStyles(
|
|
14
|
+
{ status, noResize: !!props.noResize },
|
|
15
|
+
$attrs.class as string,
|
|
16
|
+
)
|
|
17
|
+
"
|
|
16
18
|
v-bind="filteredAttrs"
|
|
17
19
|
/>
|
|
18
20
|
<label v-if="props.label" :for="id" :class="labelStyles({ status })">
|
|
19
21
|
{{ props.label }}
|
|
20
22
|
</label>
|
|
21
23
|
</div>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
:key="status + text"
|
|
34
|
-
:class="
|
|
35
|
-
helperStyles({
|
|
36
|
-
status,
|
|
37
|
-
align: props.alignText ?? 'left',
|
|
38
|
-
})
|
|
39
|
-
"
|
|
40
|
-
:initial="{ opacity: 0, y: -4 }"
|
|
41
|
-
:animate="{ opacity: 1, y: 0 }"
|
|
42
|
-
:exit="{ opacity: 0, y: 4 }"
|
|
43
|
-
:transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
|
|
24
|
+
<div v-if="!props.hideHelper" class="h-4">
|
|
25
|
+
<animate-presence mode="wait" :initial="false">
|
|
26
|
+
<motion.div
|
|
27
|
+
:initial="{ opacity: 0, y: '-4px' }"
|
|
28
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
29
|
+
:exit="{ opacity: 0, y: '-4px' }"
|
|
30
|
+
:transition="{ duration: 0.125 }"
|
|
31
|
+
:key="status"
|
|
32
|
+
>
|
|
33
|
+
<p
|
|
34
|
+
:class="helperStyles({ status, align: props.alignText ?? 'left' })"
|
|
44
35
|
>
|
|
45
36
|
{{ text }}
|
|
46
|
-
</
|
|
47
|
-
</
|
|
48
|
-
</
|
|
49
|
-
</
|
|
37
|
+
</p>
|
|
38
|
+
</motion.div>
|
|
39
|
+
</animate-presence>
|
|
40
|
+
</div>
|
|
50
41
|
</div>
|
|
51
42
|
</template>
|
|
52
43
|
|
|
@@ -74,7 +65,7 @@
|
|
|
74
65
|
const id = useId();
|
|
75
66
|
|
|
76
67
|
const status = computed(() =>
|
|
77
|
-
props.disabled ? "disabled" : props.status ?? "default"
|
|
68
|
+
props.disabled ? "disabled" : (props.status ?? "default"),
|
|
78
69
|
);
|
|
79
70
|
|
|
80
71
|
const text = computed(
|
|
@@ -84,6 +75,6 @@
|
|
|
84
75
|
default: props.helperText,
|
|
85
76
|
disabled: "",
|
|
86
77
|
valid: "",
|
|
87
|
-
}[props.status ?? "default"]
|
|
78
|
+
})[props.status ?? "default"],
|
|
88
79
|
);
|
|
89
80
|
</script>
|
|
@@ -17,11 +17,11 @@ export const textareaStyles = styles(
|
|
|
17
17
|
true: "resize-none",
|
|
18
18
|
false: "",
|
|
19
19
|
},
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
export const labelStyles = styles(
|
|
24
|
-
"transition-colors ease-in-out duration-150 text-
|
|
24
|
+
"transition-colors ease-in-out duration-150 text-sm font-500",
|
|
25
25
|
{
|
|
26
26
|
status: {
|
|
27
27
|
default: "",
|
|
@@ -29,7 +29,7 @@ export const labelStyles = styles(
|
|
|
29
29
|
disabled: "text-gray-500 dark:text-gray-700",
|
|
30
30
|
valid: "",
|
|
31
31
|
},
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
export const helperStyles = styles(
|
|
@@ -46,5 +46,5 @@ export const helperStyles = styles(
|
|
|
46
46
|
right: "text-right",
|
|
47
47
|
center: "text-center",
|
|
48
48
|
},
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
50
|
);
|
|
@@ -62,29 +62,34 @@
|
|
|
62
62
|
|
|
63
63
|
const validateAndFormatTime = (
|
|
64
64
|
value: string,
|
|
65
|
-
type: "hour" | "minute" | "second"
|
|
66
|
-
) => {
|
|
65
|
+
type: "hour" | "minute" | "second",
|
|
66
|
+
): string => {
|
|
67
67
|
const num = parseInt(value, 10);
|
|
68
68
|
if (isNaN(num)) return "00";
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (type === "hour") validatedNum = Math.min(23, Math.max(0, num));
|
|
73
|
-
else if (type === "minute") validatedNum = Math.min(59, Math.max(0, num));
|
|
74
|
-
else validatedNum = Math.min(59, Math.max(0, num));
|
|
75
|
-
|
|
76
|
-
return validatedNum < 10 ? `0${validatedNum}` : `${validatedNum}`;
|
|
69
|
+
const max = type === "hour" ? 23 : 59;
|
|
70
|
+
const clamped = Math.max(0, Math.min(max, num));
|
|
71
|
+
return clamped < 10 ? `0${clamped}` : `${clamped}`;
|
|
77
72
|
};
|
|
78
73
|
|
|
79
74
|
const handleChange = (event: Event, type: "hour" | "minute" | "second") => {
|
|
80
75
|
const target = event.target as HTMLInputElement;
|
|
81
|
-
const
|
|
82
|
-
|
|
76
|
+
const value = target.value.replace(/\D/g, "");
|
|
77
|
+
|
|
78
|
+
if (value === "") return;
|
|
79
|
+
|
|
80
|
+
let num = parseInt(value, 10);
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
if (type === "hour") {
|
|
83
|
+
if (props.allowMidnight && num >= 24) {
|
|
84
|
+
num = 0;
|
|
85
|
+
} else if (num > 23) {
|
|
86
|
+
num = 23;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
if (num > 59) num = 59;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
model.value = { ...model.value, [type]: num };
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
const displaySeparator = (order: "first" | "second") => {
|
|
@@ -92,7 +97,6 @@
|
|
|
92
97
|
|
|
93
98
|
if (order === "first") {
|
|
94
99
|
if (!model.value?.hasOwnProperty("hour")) return false;
|
|
95
|
-
|
|
96
100
|
if (
|
|
97
101
|
model.value?.hasOwnProperty("minute") ||
|
|
98
102
|
model.value?.hasOwnProperty("second")
|
|
@@ -102,7 +106,6 @@
|
|
|
102
106
|
|
|
103
107
|
if (order === "second") {
|
|
104
108
|
if (!model.value?.hasOwnProperty("second")) return false;
|
|
105
|
-
|
|
106
109
|
if (
|
|
107
110
|
model.value?.hasOwnProperty("hour") ||
|
|
108
111
|
model.value?.hasOwnProperty("minute")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import styles from "~/utils/styles";
|
|
2
2
|
|
|
3
3
|
export const timePickerStyles = styles(
|
|
4
|
-
"flex items-center justify-
|
|
4
|
+
"flex items-center justify-start gap-2 rounded-1 border border-gray-300 bg-white dark:bg-gray-1000 px-2 py-0.5 min-h-[38px] text-sm transition-colors ease-in-out duration-150 shadow-xs dark:border-gray-900",
|
|
5
5
|
);
|
|
@@ -39,12 +39,27 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
|
|
|
39
39
|
const tooltipRect = refElement.value.getBoundingClientRect();
|
|
40
40
|
const targetRect = target.value.getBoundingClientRect();
|
|
41
41
|
const winW = window.innerWidth;
|
|
42
|
+
const winH = window.innerHeight;
|
|
42
43
|
|
|
43
44
|
let top: number;
|
|
44
45
|
const scrollY = hasFixedParent(target.value) ? 0 : window.scrollY;
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const spaceBelow = winH - targetRect.bottom;
|
|
48
|
+
const spaceAbove = targetRect.top;
|
|
49
|
+
const fitsBelow = spaceBelow >= tooltipRect.height + 6;
|
|
50
|
+
const fitsAbove = spaceAbove >= tooltipRect.height + 6;
|
|
51
|
+
|
|
52
|
+
const effectiveDirection =
|
|
47
53
|
direction === "top"
|
|
54
|
+
? fitsAbove || !fitsBelow
|
|
55
|
+
? "top"
|
|
56
|
+
: "bottom"
|
|
57
|
+
: fitsBelow || !fitsAbove
|
|
58
|
+
? "bottom"
|
|
59
|
+
: "top";
|
|
60
|
+
|
|
61
|
+
top =
|
|
62
|
+
effectiveDirection === "top"
|
|
48
63
|
? targetRect.top - tooltipRect.height - 6 + scrollY
|
|
49
64
|
: targetRect.bottom + 6 + scrollY;
|
|
50
65
|
|
|
@@ -80,7 +95,7 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
|
|
|
80
95
|
window.removeEventListener("resize", updatePosition);
|
|
81
96
|
}
|
|
82
97
|
},
|
|
83
|
-
{ flush: "post" }
|
|
98
|
+
{ flush: "post" },
|
|
84
99
|
);
|
|
85
100
|
|
|
86
101
|
onBeforeUnmount(() => {
|