v-uixy 1.8.1 → 1.9.1
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/commands/add.js +43 -2
- package/package.json +7 -7
- package/templates/components/Button/Button.component.vue +32 -31
- package/templates/components/Button/Button.styles.ts +1 -1
- package/templates/components/DatePicker/DatePicker.component.vue +26 -5
- package/templates/components/DatePicker/DatePicker.types.ts +1 -0
- package/templates/components/DateRangePicker/DateRangePicker.component.vue +1 -0
- package/templates/components/Input/Input.styles.ts +4 -2
- package/templates/components/Modal/Modal.component.vue +26 -24
- package/templates/components/Modal/Modal.styles.ts +1 -1
- package/templates/components/Popover/Popover.component.vue +1 -0
- package/templates/components/Select/Select.component.vue +13 -1
- package/templates/components/Select/Select.styles.ts +1 -1
- package/templates/components/Sheet/Sheet.component.vue +15 -2
- package/templates/components/Sheet/Sheet.styles.ts +1 -1
- package/templates/components/Tooltip/Tooltip.component.vue +1 -0
- package/templates/components.json +7 -7
- package/templates/composables/useOverlayLayer.ts +131 -0
- package/templates/composables/usePosition.ts +11 -1
package/commands/add.js
CHANGED
|
@@ -72,7 +72,25 @@ async function copyComponent(name) {
|
|
|
72
72
|
console.log(chalk.green(`✔ Copied ${name} to app/components/ui/${name}`));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
async function
|
|
75
|
+
async function resolveComposableDeps(src, seen) {
|
|
76
|
+
const content = await fs.readFile(src, "utf8");
|
|
77
|
+
const deps = new Set();
|
|
78
|
+
|
|
79
|
+
for (const match of content.matchAll(
|
|
80
|
+
/(?:import|export)[^"']*from\s+["']\.\/([A-Za-z0-9_-]+)["']/g
|
|
81
|
+
)) {
|
|
82
|
+
deps.add(match[1]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const dep of deps) {
|
|
86
|
+
if (!seen.has(dep)) await copyComposable(dep, seen);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function copyComposable(name, seen = new Set()) {
|
|
91
|
+
if (seen.has(name)) return;
|
|
92
|
+
seen.add(name);
|
|
93
|
+
|
|
76
94
|
const src = path.join(templatesRoot, `composables/${name}.ts`);
|
|
77
95
|
const { composablesDir } = await getProjectPaths();
|
|
78
96
|
const dest = path.join(composablesDir, `${name}.ts`);
|
|
@@ -87,6 +105,8 @@ async function copyComposable(name) {
|
|
|
87
105
|
console.log(chalk.green(`✔ Copied composable: ${name}.ts`));
|
|
88
106
|
|
|
89
107
|
await updateComposablesIndex(name);
|
|
108
|
+
|
|
109
|
+
await resolveComposableDeps(src, seen);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
async function getExistingIndexComponents() {
|
|
@@ -212,12 +232,32 @@ async function ensureTsconfig() {
|
|
|
212
232
|
}
|
|
213
233
|
}
|
|
214
234
|
|
|
235
|
+
async function copyTypes() {
|
|
236
|
+
const typesSrcDir = path.join(templatesRoot, "types");
|
|
237
|
+
if (!(await fs.pathExists(typesSrcDir))) return;
|
|
238
|
+
|
|
239
|
+
const typesDestDir = path.join(projectRoot, "app/types");
|
|
240
|
+
await fs.ensureDir(typesDestDir);
|
|
241
|
+
|
|
242
|
+
const files = await fs.readdir(typesSrcDir);
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
await fs.copy(
|
|
245
|
+
path.join(typesSrcDir, file),
|
|
246
|
+
path.join(typesDestDir, file),
|
|
247
|
+
{ overwrite: true }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(chalk.green("✔ Synced type definitions to app/types"));
|
|
252
|
+
}
|
|
253
|
+
|
|
215
254
|
export default async function add(componentName) {
|
|
216
255
|
const registry = await loadRegistry();
|
|
217
256
|
let componentsToAdd = new Set();
|
|
218
257
|
|
|
219
258
|
await ensureTypeScriptInstalled();
|
|
220
259
|
await ensureTsconfig();
|
|
260
|
+
await copyTypes();
|
|
221
261
|
|
|
222
262
|
if (componentName.toLowerCase() === "all") {
|
|
223
263
|
componentsToAdd = new Set(registry.map((c) => c.name));
|
|
@@ -226,6 +266,7 @@ export default async function add(componentName) {
|
|
|
226
266
|
}
|
|
227
267
|
|
|
228
268
|
const copied = [];
|
|
269
|
+
const copiedComposables = new Set();
|
|
229
270
|
|
|
230
271
|
for (const name of componentsToAdd) {
|
|
231
272
|
await copyComponent(name);
|
|
@@ -233,7 +274,7 @@ export default async function add(componentName) {
|
|
|
233
274
|
|
|
234
275
|
const def = registry.find((c) => c.name === name);
|
|
235
276
|
for (const composable of def?.composables || []) {
|
|
236
|
-
await copyComposable(composable);
|
|
277
|
+
await copyComposable(composable, copiedComposables);
|
|
237
278
|
}
|
|
238
279
|
}
|
|
239
280
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "v-uixy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alan Haber",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"tailwindcss"
|
|
25
25
|
],
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=22.13.0"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"bin/",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"url": "git+https://github.com/haberalan/v-uixy.git"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"chalk": "^
|
|
43
|
-
"commander": "^
|
|
44
|
-
"execa": "^
|
|
45
|
-
"fs-extra": "^11.
|
|
46
|
-
"inquirer": "^
|
|
42
|
+
"chalk": "^6.0.0",
|
|
43
|
+
"commander": "^15.0.0",
|
|
44
|
+
"execa": "^10.0.1",
|
|
45
|
+
"fs-extra": "^11.4.0",
|
|
46
|
+
"inquirer": "^14.2.1"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<uixy-link
|
|
3
3
|
v-if="props.link"
|
|
4
|
-
:class="
|
|
4
|
+
:class="[
|
|
5
|
+
buttonStyles({ variant, size, rounded: !!props.rounded }, $attrs.class as string),
|
|
6
|
+
linkStyles({ disabled: !!props.disabled }),
|
|
7
|
+
]"
|
|
5
8
|
:tabindex="props.disabled ? -1 : 0"
|
|
6
9
|
v-bind="{ ...props.link, ...filteredAttrs }"
|
|
7
10
|
>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
</animate-presence>
|
|
11
|
+
<animate-presence :initial="false">
|
|
12
|
+
<motion.div
|
|
13
|
+
v-if="props.loading"
|
|
14
|
+
class="absolute top-1/2 flex size-6 -translate-y-1/2 items-center justify-center"
|
|
15
|
+
:initial="{ opacity: 0 }"
|
|
16
|
+
:animate="{ opacity: 1 }"
|
|
17
|
+
:exit="{ opacity: 0 }"
|
|
18
|
+
:transition="{ ease: 'easeInOut', duration: 0.15 }"
|
|
19
|
+
>
|
|
20
|
+
<uixy-loader size="sm" />
|
|
21
|
+
</motion.div>
|
|
22
|
+
</animate-presence>
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</div>
|
|
24
|
+
<animate-presence :initial="false">
|
|
25
|
+
<motion.div
|
|
26
|
+
class="flex items-center justify-center gap-1"
|
|
27
|
+
:initial="{ opacity: 0 }"
|
|
28
|
+
:animate="{ opacity: props.loading ? 0 : 1 }"
|
|
29
|
+
:transition="{ ease: 'easeInOut', duration: 0.15 }"
|
|
30
|
+
>
|
|
31
|
+
<uixy-icon
|
|
32
|
+
v-if="props.icon"
|
|
33
|
+
:name="props.icon"
|
|
34
|
+
:class="iconStyles({ position: props.iconPosition ?? 'left' })"
|
|
35
|
+
/>
|
|
36
|
+
<slot />
|
|
37
|
+
</motion.div>
|
|
38
|
+
</animate-presence>
|
|
38
39
|
</uixy-link>
|
|
39
40
|
|
|
40
41
|
<button
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
:transition="{ ease: 'easeInOut', duration: 0.15 }"
|
|
67
68
|
>
|
|
68
69
|
<uixy-icon
|
|
69
|
-
v-if="props.icon
|
|
70
|
+
v-if="props.icon"
|
|
70
71
|
:name="props.icon"
|
|
71
72
|
:class="iconStyles({ position: props.iconPosition ?? 'left' })"
|
|
72
73
|
/>
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex
|
|
3
|
-
<label v-if="props.label" :class="labelStyles({ status })">
|
|
2
|
+
<div :class="['flex flex-col gap-1', $slots.trigger ? 'w-fit' : 'w-full']">
|
|
3
|
+
<label v-if="props.label && !$slots.trigger" :class="labelStyles({ status })">
|
|
4
4
|
{{ props.label }}
|
|
5
5
|
</label>
|
|
6
6
|
|
|
7
7
|
<div
|
|
8
|
+
v-if="$slots.trigger"
|
|
9
|
+
ref="triggerRef"
|
|
10
|
+
class="inline-flex w-fit"
|
|
11
|
+
role="button"
|
|
12
|
+
:tabindex="props.disabled ? -1 : 0"
|
|
13
|
+
:aria-expanded="active"
|
|
14
|
+
@click="onTriggerClick"
|
|
15
|
+
@keydown.enter.prevent="onTriggerClick"
|
|
16
|
+
@keydown.space.prevent="onTriggerClick"
|
|
17
|
+
>
|
|
18
|
+
<slot name="trigger" :open="active" :value="model" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
v-else
|
|
8
23
|
ref="triggerRef"
|
|
9
24
|
role="button"
|
|
10
25
|
:tabindex="props.disabled ? -1 : 0"
|
|
@@ -61,6 +76,7 @@
|
|
|
61
76
|
<animate-presence>
|
|
62
77
|
<motion.div
|
|
63
78
|
v-if="active"
|
|
79
|
+
data-uixy-overlay
|
|
64
80
|
:initial="{ opacity: 0 }"
|
|
65
81
|
:animate="{ opacity: 1 }"
|
|
66
82
|
:exit="{ opacity: 0 }"
|
|
@@ -149,7 +165,12 @@
|
|
|
149
165
|
|
|
150
166
|
const triggerRef = ref<HTMLElement>();
|
|
151
167
|
|
|
152
|
-
const
|
|
168
|
+
const seedTime = (): UixyTimeParts =>
|
|
169
|
+
model.value
|
|
170
|
+
? timePartsOf(model.value)
|
|
171
|
+
: { hour: props.defaultHour ?? 0, minute: 0, second: 0 };
|
|
172
|
+
|
|
173
|
+
const draftTime = ref<UixyTimeParts>(seedTime());
|
|
153
174
|
|
|
154
175
|
const status = computed(() => {
|
|
155
176
|
if (props.disabled) return "disabled";
|
|
@@ -208,11 +229,11 @@
|
|
|
208
229
|
const clear = () => {
|
|
209
230
|
model.value = null;
|
|
210
231
|
|
|
211
|
-
draftTime.value = { hour: 0, minute: 0, second: 0 };
|
|
232
|
+
draftTime.value = { hour: props.defaultHour ?? 0, minute: 0, second: 0 };
|
|
212
233
|
};
|
|
213
234
|
|
|
214
235
|
watch(active, (isActive) => {
|
|
215
|
-
if (isActive) draftTime.value =
|
|
236
|
+
if (isActive) draftTime.value = seedTime();
|
|
216
237
|
});
|
|
217
238
|
|
|
218
239
|
const onDocClick = (event: MouseEvent) => {
|
|
@@ -18,14 +18,16 @@ export const inputStyles = styles(
|
|
|
18
18
|
xl: "px-3.5 py-2.5 text-base",
|
|
19
19
|
},
|
|
20
20
|
icon: {
|
|
21
|
-
left: "
|
|
22
|
-
right: "
|
|
21
|
+
left: "",
|
|
22
|
+
right: "",
|
|
23
23
|
none: "",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"size.sm+icon.left": "pl-7",
|
|
28
28
|
"size.sm+icon.right": "pr-7",
|
|
29
|
+
"size.md+icon.left": "pl-9",
|
|
30
|
+
"size.md+icon.right": "pr-9",
|
|
29
31
|
"size.xl+icon.left": "pl-9",
|
|
30
32
|
"size.xl+icon.right": "pr-9",
|
|
31
33
|
},
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
<animate-presence>
|
|
5
5
|
<motion.div
|
|
6
6
|
v-if="open"
|
|
7
|
+
ref="rootRef"
|
|
7
8
|
:initial="{ opacity: 0 }"
|
|
8
9
|
:animate="{ opacity: 1 }"
|
|
9
10
|
:exit="{ opacity: 0 }"
|
|
10
11
|
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
11
12
|
:class="modalStyles($attrs.class as string)"
|
|
12
|
-
|
|
13
|
+
:style="{ zIndex }"
|
|
14
|
+
:[modalAttr]="''"
|
|
15
|
+
:data-uixy-modal-id="id"
|
|
16
|
+
data-uixy-overlay
|
|
13
17
|
@click.self="handleClick"
|
|
14
18
|
>
|
|
15
19
|
<motion.div
|
|
@@ -32,6 +36,7 @@
|
|
|
32
36
|
import type { UixyModalProps } from "./Modal.types";
|
|
33
37
|
import { modalStyles } from "./Modal.styles";
|
|
34
38
|
import { AnimatePresence, motion } from "motion-v";
|
|
39
|
+
import { useModalLayer, MODAL_ATTR, getTopModalEl } from "~/composables";
|
|
35
40
|
|
|
36
41
|
const props = defineProps<UixyModalProps>();
|
|
37
42
|
|
|
@@ -41,6 +46,18 @@
|
|
|
41
46
|
|
|
42
47
|
const open = defineModel<boolean>();
|
|
43
48
|
|
|
49
|
+
const rootRef = ref<HTMLElement>();
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
id,
|
|
53
|
+
zIndex,
|
|
54
|
+
isTopModal,
|
|
55
|
+
open: openLayer,
|
|
56
|
+
close: closeLayer,
|
|
57
|
+
} = useModalLayer();
|
|
58
|
+
|
|
59
|
+
const modalAttr = computed(() => (open.value ? MODAL_ATTR : null));
|
|
60
|
+
|
|
44
61
|
const handleClick = () => {
|
|
45
62
|
if (props.persistent || props.loading) return;
|
|
46
63
|
|
|
@@ -48,6 +65,8 @@
|
|
|
48
65
|
};
|
|
49
66
|
|
|
50
67
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
68
|
+
if (!isTopModal.value) return;
|
|
69
|
+
|
|
51
70
|
if (e.key === "Escape") {
|
|
52
71
|
handleClick();
|
|
53
72
|
return;
|
|
@@ -55,39 +74,21 @@
|
|
|
55
74
|
if (e.key === "Enter") {
|
|
56
75
|
const target = e.target as HTMLElement;
|
|
57
76
|
if (target.tagName === "TEXTAREA" || target.tagName === "BUTTON") return;
|
|
58
|
-
const
|
|
59
|
-
|
|
77
|
+
const form =
|
|
78
|
+
rootRef.value?.querySelector("form") ??
|
|
79
|
+
getTopModalEl()?.querySelector("form");
|
|
60
80
|
form?.requestSubmit();
|
|
61
81
|
}
|
|
62
82
|
};
|
|
63
83
|
|
|
64
|
-
const toggleSiblingsInert = (enable?: boolean) => {
|
|
65
|
-
if (typeof document === "undefined") return;
|
|
66
|
-
|
|
67
|
-
const modalEl = document.querySelector(
|
|
68
|
-
"[data-modal]",
|
|
69
|
-
) as HTMLElement | null;
|
|
70
|
-
|
|
71
|
-
Array.from(document.body.children).forEach((el) => {
|
|
72
|
-
if (modalEl && (el === modalEl || el.contains(modalEl))) return;
|
|
73
|
-
|
|
74
|
-
if (enable) {
|
|
75
|
-
el.setAttribute("inert", "");
|
|
76
|
-
el.setAttribute("aria-hidden", "true");
|
|
77
|
-
} else {
|
|
78
|
-
el.removeAttribute("inert");
|
|
79
|
-
el.removeAttribute("aria-hidden");
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
|
|
84
84
|
watch(
|
|
85
85
|
open,
|
|
86
86
|
(isOpen) => {
|
|
87
|
-
toggleSiblingsInert(isOpen);
|
|
88
87
|
if (isOpen) {
|
|
88
|
+
openLayer();
|
|
89
89
|
window.addEventListener("keydown", handleKeyDown);
|
|
90
90
|
} else {
|
|
91
|
+
closeLayer();
|
|
91
92
|
window.removeEventListener("keydown", handleKeyDown);
|
|
92
93
|
}
|
|
93
94
|
},
|
|
@@ -96,5 +97,6 @@
|
|
|
96
97
|
|
|
97
98
|
onUnmounted(() => {
|
|
98
99
|
window.removeEventListener("keydown", handleKeyDown);
|
|
100
|
+
closeLayer();
|
|
99
101
|
});
|
|
100
102
|
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styles from "~/utils/styles";
|
|
2
2
|
|
|
3
3
|
const modalStyles = styles(
|
|
4
|
-
"fixed left-0 top-0
|
|
4
|
+
"fixed left-0 top-0 flex size-full items-center justify-center bg-white/5 backdrop-blur-[2px] dark:bg-gray-900/5"
|
|
5
5
|
);
|
|
6
6
|
|
|
7
7
|
export { modalStyles };
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
<animate-presence>
|
|
48
48
|
<motion.div
|
|
49
49
|
v-if="open && !props.disabled"
|
|
50
|
+
data-uixy-overlay
|
|
50
51
|
:class="dropdownPanelStyles({ size })"
|
|
51
|
-
:style="dropdownStyles"
|
|
52
|
+
:style="[dropdownStyles, { zIndex: dropdownZIndex }]"
|
|
52
53
|
:initial="{ opacity: 0, y: -4, scale: 0.98 }"
|
|
53
54
|
:animate="{ opacity: 1, y: 0, scale: 1 }"
|
|
54
55
|
:exit="{ opacity: 0, scale: 0.98 }"
|
|
@@ -203,6 +204,7 @@
|
|
|
203
204
|
import { UixyInput } from "../Input";
|
|
204
205
|
import { motion, AnimatePresence } from "motion-v";
|
|
205
206
|
import { useSelect } from "./composables";
|
|
207
|
+
import { useOverlayLayer } from "~/composables";
|
|
206
208
|
import SelectTreeItem from "./SelectTreeItem.vue";
|
|
207
209
|
|
|
208
210
|
const props = defineProps<UixySelectProps>();
|
|
@@ -234,6 +236,12 @@
|
|
|
234
236
|
width: "0px",
|
|
235
237
|
});
|
|
236
238
|
|
|
239
|
+
const {
|
|
240
|
+
zIndex: dropdownZIndex,
|
|
241
|
+
acquire: acquireLayer,
|
|
242
|
+
release: releaseLayer,
|
|
243
|
+
} = useOverlayLayer();
|
|
244
|
+
|
|
237
245
|
const {
|
|
238
246
|
refOptions,
|
|
239
247
|
open,
|
|
@@ -332,6 +340,8 @@
|
|
|
332
340
|
|
|
333
341
|
if (!isOpen) return;
|
|
334
342
|
|
|
343
|
+
acquireLayer();
|
|
344
|
+
|
|
335
345
|
void updateDropdownPosition();
|
|
336
346
|
|
|
337
347
|
if (props.search) {
|
|
@@ -351,6 +361,7 @@
|
|
|
351
361
|
}
|
|
352
362
|
|
|
353
363
|
onWatcherCleanup(() => {
|
|
364
|
+
releaseLayer();
|
|
354
365
|
window.removeEventListener("resize", onResize);
|
|
355
366
|
window.removeEventListener("scroll", onAnyScroll, true);
|
|
356
367
|
dropdownResizeObserver?.disconnect();
|
|
@@ -359,6 +370,7 @@
|
|
|
359
370
|
);
|
|
360
371
|
|
|
361
372
|
onUnmounted(() => {
|
|
373
|
+
releaseLayer();
|
|
362
374
|
dropdownResizeObserver?.disconnect();
|
|
363
375
|
});
|
|
364
376
|
|
|
@@ -88,7 +88,7 @@ export const listItemPaddingStyles = styles("", {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
export const dropdownPanelStyles = styles(
|
|
91
|
-
"scrollbar
|
|
91
|
+
"scrollbar max-h-60 overflow-y-auto rounded-1 border border-gray-300 bg-white p-1 shadow-sm dark:border-gray-900 dark:bg-gray-1000",
|
|
92
92
|
{
|
|
93
93
|
size: {
|
|
94
94
|
sm: "text-xs",
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
:animate="{ opacity: 1 }"
|
|
9
9
|
:exit="{ opacity: 0 }"
|
|
10
10
|
:transition="{ ease: 'easeInOut', duration: 0.15 }"
|
|
11
|
+
:style="{ zIndex }"
|
|
12
|
+
data-uixy-overlay
|
|
11
13
|
@click.self="handleClick"
|
|
12
|
-
class="fixed left-0 top-0
|
|
14
|
+
class="fixed left-0 top-0 size-full bg-white/5 backdrop-blur-[2px] dark:bg-gray-900/5"
|
|
13
15
|
/>
|
|
14
16
|
<motion.div
|
|
15
17
|
initial="initial"
|
|
@@ -17,6 +19,8 @@
|
|
|
17
19
|
exit="exit"
|
|
18
20
|
:variants="ANIMATIONS[props.direction ?? 'right']"
|
|
19
21
|
:transition="{ ease: 'easeInOut', duration: 0.3 }"
|
|
22
|
+
:style="{ zIndex }"
|
|
23
|
+
data-uixy-overlay
|
|
20
24
|
:class="
|
|
21
25
|
sheetStyles({ direction: props.direction ?? 'right' }, $attrs.class as string)
|
|
22
26
|
"
|
|
@@ -33,6 +37,7 @@
|
|
|
33
37
|
<script setup lang="ts">
|
|
34
38
|
import { AnimatePresence, motion } from "motion-v";
|
|
35
39
|
import { useScale } from "../Scale";
|
|
40
|
+
import { useOverlayLayer } from "~/composables";
|
|
36
41
|
import type { UixySheetProps } from "./Sheet.types";
|
|
37
42
|
import { sheetStyles } from "./Sheet.styles";
|
|
38
43
|
|
|
@@ -65,6 +70,8 @@
|
|
|
65
70
|
|
|
66
71
|
const { setScale } = useScale();
|
|
67
72
|
|
|
73
|
+
const { zIndex, acquire, release } = useOverlayLayer();
|
|
74
|
+
|
|
68
75
|
const open = defineModel<boolean>();
|
|
69
76
|
|
|
70
77
|
const handleClick = () => {
|
|
@@ -74,13 +81,19 @@
|
|
|
74
81
|
watch(
|
|
75
82
|
() => open.value,
|
|
76
83
|
(v) => {
|
|
77
|
-
if (v)
|
|
84
|
+
if (v) {
|
|
85
|
+
acquire();
|
|
86
|
+
return setScale(0.98);
|
|
87
|
+
}
|
|
78
88
|
|
|
89
|
+
release();
|
|
79
90
|
setScale(1);
|
|
80
91
|
}
|
|
81
92
|
);
|
|
82
93
|
|
|
83
94
|
onUnmounted(async () => {
|
|
95
|
+
release();
|
|
96
|
+
|
|
84
97
|
await nextTick();
|
|
85
98
|
|
|
86
99
|
setScale(1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styles from "~/utils/styles";
|
|
2
2
|
|
|
3
3
|
export const sheetStyles = styles(
|
|
4
|
-
"fixed top-0
|
|
4
|
+
"fixed top-0 h-full w-full max-w-[320px] border-gray-300 bg-gray-100 shadow-xl dark:border-gray-900 dark:bg-black",
|
|
5
5
|
{
|
|
6
6
|
direction: {
|
|
7
7
|
left: "left-0 border-r",
|
|
@@ -76,13 +76,13 @@
|
|
|
76
76
|
"name": "DatePicker",
|
|
77
77
|
"related-components": ["Icon", "Button", "Calendar"],
|
|
78
78
|
"packages": ["motion-v"],
|
|
79
|
-
"composables": ["usePosition"]
|
|
79
|
+
"composables": ["usePosition", "useOverlayLayer"]
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"name": "DateRangePicker",
|
|
83
83
|
"related-components": ["Icon", "Button", "Calendar", "DatePicker"],
|
|
84
84
|
"packages": ["motion-v"],
|
|
85
|
-
"composables": ["usePosition"]
|
|
85
|
+
"composables": ["usePosition", "useOverlayLayer"]
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"name": "FileUpload",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"name": "Modal",
|
|
131
131
|
"related-components": [],
|
|
132
132
|
"packages": ["motion-v"],
|
|
133
|
-
"composables": []
|
|
133
|
+
"composables": ["useOverlayLayer"]
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"name": "PieChart",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"name": "Popover",
|
|
143
143
|
"related-components": [],
|
|
144
144
|
"packages": ["motion-v", "uuid"],
|
|
145
|
-
"composables": ["usePosition"]
|
|
145
|
+
"composables": ["usePosition", "useOverlayLayer"]
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"name": "Radio",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"name": "Select",
|
|
167
167
|
"related-components": ["Badge", "Icon", "Input"],
|
|
168
168
|
"packages": ["motion-v"],
|
|
169
|
-
"composables": ["useInputValidation"]
|
|
169
|
+
"composables": ["useInputValidation", "useOverlayLayer"]
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
"name": "Separator",
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
"name": "Sheet",
|
|
179
179
|
"related-components": ["Scale"],
|
|
180
180
|
"packages": ["motion-v"],
|
|
181
|
-
"composables": []
|
|
181
|
+
"composables": ["useOverlayLayer"]
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
"name": "Skeleton",
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
"name": "Tooltip",
|
|
251
251
|
"related-components": [],
|
|
252
252
|
"packages": ["motion-v"],
|
|
253
|
-
"composables": ["usePosition"]
|
|
253
|
+
"composables": ["usePosition", "useOverlayLayer"]
|
|
254
254
|
}
|
|
255
255
|
]
|
|
256
256
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { computed, nextTick, onScopeDispose, ref, type ComputedRef } from "vue";
|
|
2
|
+
|
|
3
|
+
const BASE_Z = 60;
|
|
4
|
+
const STEP_Z = 10;
|
|
5
|
+
|
|
6
|
+
let seq = 0;
|
|
7
|
+
|
|
8
|
+
const layers = ref<number[]>([]);
|
|
9
|
+
|
|
10
|
+
export interface UseOverlayLayerReturn {
|
|
11
|
+
id: number;
|
|
12
|
+
zIndex: ComputedRef<number>;
|
|
13
|
+
isTop: ComputedRef<boolean>;
|
|
14
|
+
isActive: ComputedRef<boolean>;
|
|
15
|
+
acquire: () => void;
|
|
16
|
+
release: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useOverlayLayer(): UseOverlayLayerReturn {
|
|
20
|
+
const id = ++seq;
|
|
21
|
+
|
|
22
|
+
const index = computed(() => layers.value.indexOf(id));
|
|
23
|
+
const isActive = computed(() => index.value !== -1);
|
|
24
|
+
|
|
25
|
+
const zIndex = computed(() =>
|
|
26
|
+
isActive.value ? BASE_Z + (index.value + 1) * STEP_Z : BASE_Z + STEP_Z,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const isTop = computed(
|
|
30
|
+
() => isActive.value && index.value === layers.value.length - 1,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const acquire = () => {
|
|
34
|
+
if (!layers.value.includes(id)) layers.value = [...layers.value, id];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const release = () => {
|
|
38
|
+
if (layers.value.includes(id))
|
|
39
|
+
layers.value = layers.value.filter((x) => x !== id);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
onScopeDispose(release);
|
|
43
|
+
|
|
44
|
+
return { id, zIndex, isTop, isActive, acquire, release };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MANAGED_ATTR = "data-uixy-inert-managed";
|
|
48
|
+
const OVERLAY_ATTR = "data-uixy-overlay";
|
|
49
|
+
export const MODAL_ATTR = "data-uixy-modal";
|
|
50
|
+
export const MODAL_ID_ATTR = "data-uixy-modal-id";
|
|
51
|
+
|
|
52
|
+
const modalStack = ref<number[]>([]);
|
|
53
|
+
|
|
54
|
+
const zOf = (el: Element): number => {
|
|
55
|
+
const raw = window.getComputedStyle(el).zIndex;
|
|
56
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
57
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const getTopModalEl = (): HTMLElement | null => {
|
|
61
|
+
if (typeof document === "undefined") return null;
|
|
62
|
+
const els = Array.from(
|
|
63
|
+
document.querySelectorAll<HTMLElement>(`[${MODAL_ATTR}]`),
|
|
64
|
+
).filter((el) => {
|
|
65
|
+
const raw = el.getAttribute(MODAL_ID_ATTR);
|
|
66
|
+
if (raw === null) return true;
|
|
67
|
+
return modalStack.value.includes(Number(raw));
|
|
68
|
+
});
|
|
69
|
+
if (els.length === 0) return null;
|
|
70
|
+
return els.reduce((a, b) => (zOf(b) >= zOf(a) ? b : a));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function reconcileInert() {
|
|
74
|
+
if (typeof document === "undefined") return;
|
|
75
|
+
|
|
76
|
+
document.querySelectorAll(`[${MANAGED_ATTR}]`).forEach((el) => {
|
|
77
|
+
el.removeAttribute("inert");
|
|
78
|
+
el.removeAttribute("aria-hidden");
|
|
79
|
+
el.removeAttribute(MANAGED_ATTR);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const topEl = getTopModalEl();
|
|
83
|
+
if (!topEl) return;
|
|
84
|
+
|
|
85
|
+
const topZ = zOf(topEl);
|
|
86
|
+
|
|
87
|
+
Array.from(document.body.children).forEach((el) => {
|
|
88
|
+
if (!(el instanceof HTMLElement)) return;
|
|
89
|
+
if (el === topEl || el.contains(topEl)) return;
|
|
90
|
+
|
|
91
|
+
if (el.hasAttribute(OVERLAY_ATTR) && zOf(el) > topZ) return;
|
|
92
|
+
|
|
93
|
+
el.setAttribute("inert", "");
|
|
94
|
+
el.setAttribute("aria-hidden", "true");
|
|
95
|
+
el.setAttribute(MANAGED_ATTR, "");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface UseModalLayerReturn {
|
|
100
|
+
id: number;
|
|
101
|
+
zIndex: ComputedRef<number>;
|
|
102
|
+
isTopModal: ComputedRef<boolean>;
|
|
103
|
+
open: () => void;
|
|
104
|
+
close: () => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function useModalLayer(): UseModalLayerReturn {
|
|
108
|
+
const layer = useOverlayLayer();
|
|
109
|
+
|
|
110
|
+
const isTopModal = computed(
|
|
111
|
+
() => modalStack.value[modalStack.value.length - 1] === layer.id,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const open = () => {
|
|
115
|
+
layer.acquire();
|
|
116
|
+
if (!modalStack.value.includes(layer.id))
|
|
117
|
+
modalStack.value = [...modalStack.value, layer.id];
|
|
118
|
+
nextTick(reconcileInert);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const close = () => {
|
|
122
|
+
layer.release();
|
|
123
|
+
modalStack.value = modalStack.value.filter((x) => x !== layer.id);
|
|
124
|
+
reconcileInert();
|
|
125
|
+
nextTick(reconcileInert);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
onScopeDispose(close);
|
|
129
|
+
|
|
130
|
+
return { id: layer.id, zIndex: layer.zIndex, isTopModal, open, close };
|
|
131
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, computed, watch, onBeforeUnmount, nextTick, type Ref } from "vue";
|
|
2
|
+
import { useOverlayLayer } from "./useOverlayLayer";
|
|
2
3
|
|
|
3
4
|
type Direction = "top" | "bottom";
|
|
4
5
|
type Align = "center" | "start";
|
|
@@ -28,10 +29,12 @@ export function usePosition({
|
|
|
28
29
|
const active = ref(false);
|
|
29
30
|
const target = ref<HTMLElement | null>(null);
|
|
30
31
|
|
|
32
|
+
const { zIndex: layerZIndex, acquire, release } = useOverlayLayer();
|
|
33
|
+
|
|
31
34
|
const baseStyles = computed(() => ({
|
|
32
35
|
position: hasFixedParent(target.value) ? "fixed" : "absolute",
|
|
33
36
|
width: "max-content",
|
|
34
|
-
zIndex:
|
|
37
|
+
zIndex: layerZIndex.value,
|
|
35
38
|
}));
|
|
36
39
|
|
|
37
40
|
const styles = ref<Record<string, string | number>>({
|
|
@@ -115,10 +118,12 @@ export function usePosition({
|
|
|
115
118
|
[active, () => direction, () => align],
|
|
116
119
|
([isActive]) => {
|
|
117
120
|
if (isActive) {
|
|
121
|
+
acquire();
|
|
118
122
|
updatePosition();
|
|
119
123
|
observeResize();
|
|
120
124
|
window.addEventListener("resize", updatePosition);
|
|
121
125
|
} else {
|
|
126
|
+
release();
|
|
122
127
|
unobserveResize();
|
|
123
128
|
window.removeEventListener("resize", updatePosition);
|
|
124
129
|
}
|
|
@@ -126,7 +131,12 @@ export function usePosition({
|
|
|
126
131
|
{ flush: "post" },
|
|
127
132
|
);
|
|
128
133
|
|
|
134
|
+
watch(layerZIndex, (z) => {
|
|
135
|
+
styles.value = { ...styles.value, zIndex: z };
|
|
136
|
+
});
|
|
137
|
+
|
|
129
138
|
onBeforeUnmount(() => {
|
|
139
|
+
release();
|
|
130
140
|
unobserveResize();
|
|
131
141
|
window.removeEventListener("resize", updatePosition);
|
|
132
142
|
});
|