@ansh-ui/vue 0.2.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/LICENSE +21 -0
- package/dist/chunk-2VSROZ2N.js +1281 -0
- package/dist/chunk-2VSROZ2N.js.map +1 -0
- package/dist/chunk-5BQEZYG6.cjs +1294 -0
- package/dist/chunk-5BQEZYG6.cjs.map +1 -0
- package/dist/chunk-6732H4BQ.cjs +28 -0
- package/dist/chunk-6732H4BQ.cjs.map +1 -0
- package/dist/chunk-6ABPGOJP.js +722 -0
- package/dist/chunk-6ABPGOJP.js.map +1 -0
- package/dist/chunk-6XJOB6LE.js +62 -0
- package/dist/chunk-6XJOB6LE.js.map +1 -0
- package/dist/chunk-774WBLDR.cjs +24 -0
- package/dist/chunk-774WBLDR.cjs.map +1 -0
- package/dist/chunk-B6OXKW5Q.js +567 -0
- package/dist/chunk-B6OXKW5Q.js.map +1 -0
- package/dist/chunk-DMPNT3MY.cjs +64 -0
- package/dist/chunk-DMPNT3MY.cjs.map +1 -0
- package/dist/chunk-EISHOEB6.cjs +158 -0
- package/dist/chunk-EISHOEB6.cjs.map +1 -0
- package/dist/chunk-EVO23GUS.js +223 -0
- package/dist/chunk-EVO23GUS.js.map +1 -0
- package/dist/chunk-FVKLCGA4.js +1035 -0
- package/dist/chunk-FVKLCGA4.js.map +1 -0
- package/dist/chunk-IOXGBPOV.cjs +3681 -0
- package/dist/chunk-IOXGBPOV.cjs.map +1 -0
- package/dist/chunk-J2LA4GRP.cjs +1051 -0
- package/dist/chunk-J2LA4GRP.cjs.map +1 -0
- package/dist/chunk-JQ4YLXXM.cjs +580 -0
- package/dist/chunk-JQ4YLXXM.cjs.map +1 -0
- package/dist/chunk-KCA7RNJ4.cjs +781 -0
- package/dist/chunk-KCA7RNJ4.cjs.map +1 -0
- package/dist/chunk-NJDDNO4C.js +23 -0
- package/dist/chunk-NJDDNO4C.js.map +1 -0
- package/dist/chunk-OAQ7MDYX.js +22 -0
- package/dist/chunk-OAQ7MDYX.js.map +1 -0
- package/dist/chunk-PP6OYURM.cjs +238 -0
- package/dist/chunk-PP6OYURM.cjs.map +1 -0
- package/dist/chunk-SHX2JGID.cjs +269 -0
- package/dist/chunk-SHX2JGID.cjs.map +1 -0
- package/dist/chunk-SOYRICHU.js +3645 -0
- package/dist/chunk-SOYRICHU.js.map +1 -0
- package/dist/chunk-T425FIC7.js +773 -0
- package/dist/chunk-T425FIC7.js.map +1 -0
- package/dist/chunk-TMG3M6TK.js +152 -0
- package/dist/chunk-TMG3M6TK.js.map +1 -0
- package/dist/chunk-XLLSS464.cjs +286 -0
- package/dist/chunk-XLLSS464.cjs.map +1 -0
- package/dist/chunk-XZ5ZFLZM.js +280 -0
- package/dist/chunk-XZ5ZFLZM.js.map +1 -0
- package/dist/chunk-Z7XOFKM4.cjs +733 -0
- package/dist/chunk-Z7XOFKM4.cjs.map +1 -0
- package/dist/chunk-ZGMXOMCB.js +266 -0
- package/dist/chunk-ZGMXOMCB.js.map +1 -0
- package/dist/data.cjs +70 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.cts +963 -0
- package/dist/data.d.ts +963 -0
- package/dist/data.js +5 -0
- package/dist/data.js.map +1 -0
- package/dist/form.cjs +151 -0
- package/dist/form.cjs.map +1 -0
- package/dist/form.d.cts +2639 -0
- package/dist/form.d.ts +2639 -0
- package/dist/form.js +6 -0
- package/dist/form.js.map +1 -0
- package/dist/index.cjs +1158 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +439 -0
- package/dist/index.d.ts +439 -0
- package/dist/index.js +547 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.cjs +67 -0
- package/dist/layout.cjs.map +1 -0
- package/dist/layout.d.cts +729 -0
- package/dist/layout.d.ts +729 -0
- package/dist/layout.js +6 -0
- package/dist/layout.js.map +1 -0
- package/dist/motion.cjs +50 -0
- package/dist/motion.cjs.map +1 -0
- package/dist/motion.d.cts +681 -0
- package/dist/motion.d.ts +681 -0
- package/dist/motion.js +5 -0
- package/dist/motion.js.map +1 -0
- package/dist/navigation.cjs +48 -0
- package/dist/navigation.cjs.map +1 -0
- package/dist/navigation.d.cts +515 -0
- package/dist/navigation.d.ts +515 -0
- package/dist/navigation.js +7 -0
- package/dist/navigation.js.map +1 -0
- package/dist/overlay.cjs +61 -0
- package/dist/overlay.cjs.map +1 -0
- package/dist/overlay.d.cts +772 -0
- package/dist/overlay.d.ts +772 -0
- package/dist/overlay.js +8 -0
- package/dist/overlay.js.map +1 -0
- package/dist/position-Cn7qP1pn.d.cts +50 -0
- package/dist/position-Cn7qP1pn.d.ts +50 -0
- package/dist/primitives.cjs +129 -0
- package/dist/primitives.cjs.map +1 -0
- package/dist/primitives.d.cts +300 -0
- package/dist/primitives.d.ts +300 -0
- package/dist/primitives.js +8 -0
- package/dist/primitives.js.map +1 -0
- package/dist/sidebar-CQqEmpid.d.cts +124 -0
- package/dist/sidebar-CTZ-Ckko.d.ts +124 -0
- package/dist/styles.css +5593 -0
- package/dist/utils-IKPG3zcC.d.cts +46 -0
- package/dist/utils-IKPG3zcC.d.ts +46 -0
- package/package.json +91 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var vue = require('vue');
|
|
4
|
+
var core = require('@ansh-ui/core');
|
|
5
|
+
|
|
6
|
+
// src/primitives/presence.ts
|
|
7
|
+
function usePresence(open, target) {
|
|
8
|
+
const mounted = vue.ref(open.value);
|
|
9
|
+
const state = vue.ref(open.value ? "open" : "closed");
|
|
10
|
+
let frame = null;
|
|
11
|
+
let timeout = null;
|
|
12
|
+
let node = null;
|
|
13
|
+
let onEnd = null;
|
|
14
|
+
const clear = () => {
|
|
15
|
+
if (node && onEnd) {
|
|
16
|
+
node.removeEventListener("animationend", onEnd);
|
|
17
|
+
node.removeEventListener("transitionend", onEnd);
|
|
18
|
+
}
|
|
19
|
+
node = null;
|
|
20
|
+
onEnd = null;
|
|
21
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
22
|
+
timeout = null;
|
|
23
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
24
|
+
frame = null;
|
|
25
|
+
};
|
|
26
|
+
vue.watch(
|
|
27
|
+
open,
|
|
28
|
+
(isOpen) => {
|
|
29
|
+
clear();
|
|
30
|
+
if (isOpen) {
|
|
31
|
+
mounted.value = true;
|
|
32
|
+
frame = requestAnimationFrame(() => {
|
|
33
|
+
frame = null;
|
|
34
|
+
state.value = "open";
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!mounted.value) return;
|
|
39
|
+
state.value = "closed";
|
|
40
|
+
const element = target.value;
|
|
41
|
+
if (!element || core.prefersReducedMotion()) {
|
|
42
|
+
mounted.value = false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
node = element;
|
|
46
|
+
onEnd = (event) => {
|
|
47
|
+
if (event.target !== node) return;
|
|
48
|
+
mounted.value = false;
|
|
49
|
+
};
|
|
50
|
+
node.addEventListener("animationend", onEnd);
|
|
51
|
+
node.addEventListener("transitionend", onEnd);
|
|
52
|
+
timeout = setTimeout(() => {
|
|
53
|
+
mounted.value = false;
|
|
54
|
+
}, 400);
|
|
55
|
+
},
|
|
56
|
+
{ immediate: true, flush: "post" }
|
|
57
|
+
);
|
|
58
|
+
if (vue.getCurrentScope()) vue.onScopeDispose(clear);
|
|
59
|
+
return { mounted, state };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
exports.usePresence = usePresence;
|
|
63
|
+
//# sourceMappingURL=chunk-DMPNT3MY.cjs.map
|
|
64
|
+
//# sourceMappingURL=chunk-DMPNT3MY.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/primitives/presence.ts"],"names":["ref","watch","prefersReducedMotion","getCurrentScope","onScopeDispose"],"mappings":";;;;;;AAmBO,SAAS,WAAA,CACd,MACA,MAAA,EAC0D;AAC1D,EAAA,MAAM,OAAA,GAAUA,OAAA,CAAI,IAAA,CAAK,KAAK,CAAA;AAC9B,EAAA,MAAM,KAAA,GAAQA,OAAA,CAAuB,IAAA,CAAK,KAAA,GAAQ,SAAS,QAAQ,CAAA;AAEnE,EAAA,IAAI,KAAA,GAAuB,IAAA;AAC3B,EAAA,IAAI,OAAA,GAAgD,IAAA;AACpD,EAAA,IAAI,IAAA,GAA2B,IAAA;AAC/B,EAAA,IAAI,KAAA,GAAoE,IAAA;AAExE,EAAA,MAAM,QAAQ,MAAY;AACxB,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,IAAA,CAAK,mBAAA,CAAoB,gBAAgB,KAAK,CAAA;AAC9C,MAAA,IAAA,CAAK,mBAAA,CAAoB,iBAAiB,KAAK,CAAA;AAAA,IACjD;AACA,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,IAAI,OAAA,KAAY,IAAA,EAAM,YAAA,CAAa,OAAO,CAAA;AAC1C,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,IAAI,KAAA,KAAU,IAAA,EAAM,oBAAA,CAAqB,KAAK,CAAA;AAC9C,IAAA,KAAA,GAAQ,IAAA;AAAA,EACV,CAAA;AAEA,EAAAC,SAAA;AAAA,IACE,IAAA;AAAA,IACA,CAAC,MAAA,KAAW;AACV,MAAA,KAAA,EAAM;AAEN,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAIhB,QAAA,KAAA,GAAQ,sBAAsB,MAAM;AAClC,UAAA,KAAA,GAAQ,IAAA;AACR,UAAA,KAAA,CAAM,KAAA,GAAQ,MAAA;AAAA,QAChB,CAAC,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,QAAQ,KAAA,EAAO;AACpB,MAAA,KAAA,CAAM,KAAA,GAAQ,QAAA;AAEd,MAAA,MAAM,UAAU,MAAA,CAAO,KAAA;AACvB,MAAA,IAAI,CAAC,OAAA,IAAWC,yBAAA,EAAqB,EAAG;AACtC,QAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAChB,QAAA;AAAA,MACF;AAIA,MAAA,IAAA,GAAO,OAAA;AACP,MAAA,KAAA,GAAQ,CAAC,KAAA,KAAU;AACjB,QAAA,IAAI,KAAA,CAAM,WAAW,IAAA,EAAM;AAC3B,QAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,MAClB,CAAA;AACA,MAAA,IAAA,CAAK,gBAAA,CAAiB,gBAAgB,KAAK,CAAA;AAC3C,MAAA,IAAA,CAAK,gBAAA,CAAiB,iBAAiB,KAAK,CAAA;AAI5C,MAAA,OAAA,GAAU,WAAW,MAAM;AACzB,QAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,MAClB,GAAG,GAAG,CAAA;AAAA,IACR,CAAA;AAAA,IACA,EAAE,SAAA,EAAW,IAAA,EAAM,KAAA,EAAO,MAAA;AAAO,GACnC;AAEA,EAAA,IAAIC,mBAAA,EAAgB,EAAGC,kBAAA,CAAe,KAAK,CAAA;AAE3C,EAAA,OAAO,EAAE,SAAS,KAAA,EAAM;AAC1B","file":"chunk-DMPNT3MY.cjs","sourcesContent":["import { type Ref, getCurrentScope, onScopeDispose, ref, watch } from 'vue'\nimport { prefersReducedMotion } from '@ansh-ui/core'\nimport { type ReadonlyRef } from './utils'\n\n/**\n * Keep an element mounted while it animates out — the Vue counterpart of `@ansh-ui/react`'s\n * `usePresence`.\n *\n * The problem: `v-if=\"open\"` cannot animate on close, because Vue removes the node the instant\n * `open` flips (Vue's own `<Transition>` solves this for simple cases, but our overlay components\n * need the resolved `mounted`/`state` pair to drive `data-state` for CSS *and* to gate focus\n * trapping and scroll locking off the same signal). The fix is the same as React's: keep rendering\n * it and unmount when the exit animation actually finishes.\n *\n * Finishing is detected from `animationend`/`transitionend` rather than a duration prop, which would\n * have to be kept in sync with the CSS by hand and drifts the moment someone retimes the animation\n * in the stylesheet. The timeout is only a safety net for the case where no animation is declared at\n * all and no event ever fires.\n */\nexport function usePresence(\n open: ReadonlyRef<boolean>,\n target: Ref<HTMLElement | null | undefined>,\n): { mounted: Ref<boolean>; state: Ref<'open' | 'closed'> } {\n const mounted = ref(open.value)\n const state = ref<'open' | 'closed'>(open.value ? 'open' : 'closed')\n\n let frame: number | null = null\n let timeout: ReturnType<typeof setTimeout> | null = null\n let node: HTMLElement | null = null\n let onEnd: ((event: AnimationEvent | TransitionEvent) => void) | null = null\n\n const clear = (): void => {\n if (node && onEnd) {\n node.removeEventListener('animationend', onEnd)\n node.removeEventListener('transitionend', onEnd)\n }\n node = null\n onEnd = null\n if (timeout !== null) clearTimeout(timeout)\n timeout = null\n if (frame !== null) cancelAnimationFrame(frame)\n frame = null\n }\n\n watch(\n open,\n (isOpen) => {\n clear()\n\n if (isOpen) {\n mounted.value = true\n // Next frame, so the element mounts in its closed state and *then* transitions. Setting\n // both in the same tick means the browser never sees the start state and no transition\n // runs — the classic \"enter animation doesn't play\" bug.\n frame = requestAnimationFrame(() => {\n frame = null\n state.value = 'open'\n })\n return\n }\n\n if (!mounted.value) return\n state.value = 'closed'\n\n const element = target.value\n if (!element || prefersReducedMotion()) {\n mounted.value = false\n return\n }\n\n // Only the element's *own* animations count. A child's animation ending would unmount the\n // surface while it is still fading.\n node = element\n onEnd = (event) => {\n if (event.target !== node) return\n mounted.value = false\n }\n node.addEventListener('animationend', onEnd)\n node.addEventListener('transitionend', onEnd)\n\n // Safety net: if no animation is declared, neither event ever fires and the surface would\n // stay mounted forever, invisible and intercepting pointer events.\n timeout = setTimeout(() => {\n mounted.value = false\n }, 400)\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) onScopeDispose(clear)\n\n return { mounted, state }\n}\n"]}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var vue = require('vue');
|
|
4
|
+
|
|
5
|
+
// src/primitives/focus.ts
|
|
6
|
+
var FOCUSABLE_SELECTOR = [
|
|
7
|
+
"a[href]",
|
|
8
|
+
"button:not([disabled])",
|
|
9
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
10
|
+
"select:not([disabled])",
|
|
11
|
+
"textarea:not([disabled])",
|
|
12
|
+
"audio[controls]",
|
|
13
|
+
"video[controls]",
|
|
14
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
15
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
16
|
+
].join(",");
|
|
17
|
+
function isVisible(element) {
|
|
18
|
+
if (element.hasAttribute("inert")) return false;
|
|
19
|
+
if (element.offsetParent !== null) return true;
|
|
20
|
+
const rect = element.getBoundingClientRect();
|
|
21
|
+
return rect.width > 0 || rect.height > 0;
|
|
22
|
+
}
|
|
23
|
+
function getTabbable(container) {
|
|
24
|
+
if (!container) return [];
|
|
25
|
+
const nodes = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
26
|
+
return nodes.filter((node) => {
|
|
27
|
+
if (node.getAttribute("aria-hidden") === "true") return false;
|
|
28
|
+
if (node.closest("[inert]")) return false;
|
|
29
|
+
return isVisible(node);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function useFocusTrap(target, active) {
|
|
33
|
+
let detach = null;
|
|
34
|
+
const attach = () => {
|
|
35
|
+
const container = target.value;
|
|
36
|
+
if (!container || detach) return;
|
|
37
|
+
const onKeyDown = (event) => {
|
|
38
|
+
if (event.key !== "Tab") return;
|
|
39
|
+
const tabbable = getTabbable(container);
|
|
40
|
+
if (tabbable.length === 0) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const first = tabbable[0];
|
|
45
|
+
const last = tabbable[tabbable.length - 1];
|
|
46
|
+
const activeElement = document.activeElement;
|
|
47
|
+
if (event.shiftKey && (activeElement === first || activeElement === container)) {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
last.focus();
|
|
50
|
+
} else if (!event.shiftKey && activeElement === last) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
first.focus();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
container.addEventListener("keydown", onKeyDown);
|
|
56
|
+
detach = () => {
|
|
57
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
58
|
+
detach = null;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
vue.watch([active, target], () => active.value ? attach() : detach?.(), {
|
|
62
|
+
immediate: true,
|
|
63
|
+
flush: "post"
|
|
64
|
+
});
|
|
65
|
+
if (vue.getCurrentScope()) vue.onScopeDispose(() => detach?.());
|
|
66
|
+
}
|
|
67
|
+
function useFocusRestore(active) {
|
|
68
|
+
let previous = null;
|
|
69
|
+
vue.watch(
|
|
70
|
+
active,
|
|
71
|
+
(isActive) => {
|
|
72
|
+
if (isActive) {
|
|
73
|
+
previous = document.activeElement;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (previous && previous.isConnected) previous.focus?.({ preventScroll: true });
|
|
77
|
+
previous = null;
|
|
78
|
+
},
|
|
79
|
+
{ flush: "post" }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function useAutoFocus(target, active, initialFocus) {
|
|
83
|
+
let frame = null;
|
|
84
|
+
vue.watch(
|
|
85
|
+
[active, target],
|
|
86
|
+
() => {
|
|
87
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
88
|
+
if (!active.value) return;
|
|
89
|
+
const container = target.value;
|
|
90
|
+
if (!container) return;
|
|
91
|
+
frame = requestAnimationFrame(() => {
|
|
92
|
+
frame = null;
|
|
93
|
+
const explicit = initialFocus?.value;
|
|
94
|
+
if (explicit?.isConnected) {
|
|
95
|
+
explicit.focus({ preventScroll: true });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const target2 = getTabbable(container)[0] ?? container;
|
|
99
|
+
target2.focus({ preventScroll: true });
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
{ immediate: true, flush: "post" }
|
|
103
|
+
);
|
|
104
|
+
if (vue.getCurrentScope()) {
|
|
105
|
+
vue.onScopeDispose(() => {
|
|
106
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function useRovingFocus(options) {
|
|
111
|
+
const { items, active, onActiveChange, orientation = "horizontal", loop = true } = options;
|
|
112
|
+
return (event) => {
|
|
113
|
+
const nodes = items.value.filter((item) => item !== null);
|
|
114
|
+
if (nodes.length === 0) return;
|
|
115
|
+
const horizontal = orientation === "horizontal" || orientation === "both";
|
|
116
|
+
const vertical = orientation === "vertical" || orientation === "both";
|
|
117
|
+
let next = active();
|
|
118
|
+
switch (event.key) {
|
|
119
|
+
case "ArrowRight":
|
|
120
|
+
if (!horizontal) return;
|
|
121
|
+
next = active() + 1;
|
|
122
|
+
break;
|
|
123
|
+
case "ArrowLeft":
|
|
124
|
+
if (!horizontal) return;
|
|
125
|
+
next = active() - 1;
|
|
126
|
+
break;
|
|
127
|
+
case "ArrowDown":
|
|
128
|
+
if (!vertical) return;
|
|
129
|
+
next = active() + 1;
|
|
130
|
+
break;
|
|
131
|
+
case "ArrowUp":
|
|
132
|
+
if (!vertical) return;
|
|
133
|
+
next = active() - 1;
|
|
134
|
+
break;
|
|
135
|
+
case "Home":
|
|
136
|
+
next = 0;
|
|
137
|
+
break;
|
|
138
|
+
case "End":
|
|
139
|
+
next = nodes.length - 1;
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
if (next < 0) next = loop ? nodes.length - 1 : 0;
|
|
146
|
+
if (next >= nodes.length) next = loop ? 0 : nodes.length - 1;
|
|
147
|
+
onActiveChange(next);
|
|
148
|
+
nodes[next]?.focus();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
exports.getTabbable = getTabbable;
|
|
153
|
+
exports.useAutoFocus = useAutoFocus;
|
|
154
|
+
exports.useFocusRestore = useFocusRestore;
|
|
155
|
+
exports.useFocusTrap = useFocusTrap;
|
|
156
|
+
exports.useRovingFocus = useRovingFocus;
|
|
157
|
+
//# sourceMappingURL=chunk-EISHOEB6.cjs.map
|
|
158
|
+
//# sourceMappingURL=chunk-EISHOEB6.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/primitives/focus.ts"],"names":["watch","getCurrentScope","onScopeDispose"],"mappings":"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","file":"chunk-EISHOEB6.cjs","sourcesContent":["import { type Ref, getCurrentScope, onScopeDispose, watch } from 'vue'\nimport { type ReadonlyRef } from './utils'\n\n/**\n * Focus management for overlay surfaces — the Vue counterpart of `@ansh-ui/react`'s `focus.ts`.\n *\n * Three separate concerns that are usually conflated:\n *\n * 1. **Finding what can be focused.** Harder than it looks — `disabled`, `hidden`, `inert`,\n * zero-size elements and negative tabindex all disqualify a node, and CSS-hidden ancestors\n * disqualify their whole subtree.\n * 2. **Trapping.** Tab must cycle within the surface. Required by WCAG 2.4.3 for modals.\n * 3. **Restoring.** Focus must return to the trigger on close, or a keyboard user is dumped at the\n * top of the document and has to navigate all the way back.\n */\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',')\n\nfunction isVisible(element: HTMLElement): boolean {\n if (element.hasAttribute('inert')) return false\n if (element.offsetParent !== null) return true\n const rect = element.getBoundingClientRect()\n return rect.width > 0 || rect.height > 0\n}\n\n/** Every tabbable descendant, in tab order. */\nexport function getTabbable(container: HTMLElement | null | undefined): HTMLElement[] {\n if (!container) return []\n const nodes = Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n return nodes.filter((node) => {\n if (node.getAttribute('aria-hidden') === 'true') return false\n if (node.closest('[inert]')) return false\n return isVisible(node)\n })\n}\n\n/**\n * Trap Tab within a container while `active`.\n *\n * Implemented on `keydown` at the container rather than with sentinel elements at either end. The\n * sentinel approach adds two focusable nodes to the accessibility tree that screen readers can land\n * on and announce as blank; intercepting the key does not.\n */\nexport function useFocusTrap(\n target: Ref<HTMLElement | null | undefined>,\n active: ReadonlyRef<boolean>,\n): void {\n let detach: (() => void) | null = null\n\n const attach = (): void => {\n const container = target.value\n if (!container || detach) return\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Tab') return\n\n const tabbable = getTabbable(container)\n if (tabbable.length === 0) {\n event.preventDefault()\n return\n }\n\n const first = tabbable[0]!\n const last = tabbable[tabbable.length - 1]!\n const activeElement = document.activeElement as HTMLElement | null\n\n if (event.shiftKey && (activeElement === first || activeElement === container)) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n }\n\n container.addEventListener('keydown', onKeyDown)\n detach = () => {\n container.removeEventListener('keydown', onKeyDown)\n detach = null\n }\n }\n\n watch([active, target], () => (active.value ? attach() : detach?.()), {\n immediate: true,\n flush: 'post',\n })\n\n if (getCurrentScope()) onScopeDispose(() => detach?.())\n}\n\n/**\n * Restore focus to whatever was focused before `active` became true.\n *\n * The element is captured on activation, not on mount, because a component can be mounted long\n * before it opens.\n */\nexport function useFocusRestore(active: ReadonlyRef<boolean>): void {\n let previous: HTMLElement | null = null\n\n watch(\n active,\n (isActive) => {\n if (isActive) {\n previous = document.activeElement as HTMLElement | null\n return\n }\n // Guard against restoring to a node that has since been removed from the document —\n // focusing a detached element silently moves focus to `<body>`, losing the user's place.\n if (previous && previous.isConnected) previous.focus?.({ preventScroll: true })\n previous = null\n },\n { flush: 'post' },\n )\n}\n\n/**\n * Move focus into a surface when it opens.\n *\n * Prefers an explicitly marked element, then the first tabbable one, then the container itself. The\n * container fallback matters for a dialog whose only content is text: focus must land inside it so a\n * screen reader announces the dialog rather than continuing from the page behind.\n */\nexport function useAutoFocus(\n target: Ref<HTMLElement | null | undefined>,\n active: ReadonlyRef<boolean>,\n initialFocus?: Ref<HTMLElement | null | undefined>,\n): void {\n let frame: number | null = null\n\n watch(\n [active, target],\n () => {\n if (frame !== null) cancelAnimationFrame(frame)\n if (!active.value) return\n const container = target.value\n if (!container) return\n\n // After paint: the surface may still be animating in, and focusing a node with\n // `visibility: hidden` is a no-op that silently leaves focus on the body.\n frame = requestAnimationFrame(() => {\n frame = null\n const explicit = initialFocus?.value\n if (explicit?.isConnected) {\n explicit.focus({ preventScroll: true })\n return\n }\n const target2 = getTabbable(container)[0] ?? container\n target2.focus({ preventScroll: true })\n })\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n if (frame !== null) cancelAnimationFrame(frame)\n })\n }\n}\n\n/**\n * Roving tabindex for a composite widget (toolbar, tab list, menu).\n *\n * The ARIA authoring practice: a composite has exactly **one** tab stop, and arrow keys move within\n * it. Returns a keydown handler to bind on the composite's container.\n */\nexport function useRovingFocus(options: {\n /** Items in DOM order. */\n items: Ref<Array<HTMLElement | null>>\n /** Index that currently owns the tab stop. */\n active: () => number\n onActiveChange: (index: number) => void\n orientation?: 'horizontal' | 'vertical' | 'both'\n loop?: boolean\n}): (event: KeyboardEvent) => void {\n const { items, active, onActiveChange, orientation = 'horizontal', loop = true } = options\n\n return (event: KeyboardEvent) => {\n const nodes = items.value.filter((item): item is HTMLElement => item !== null)\n if (nodes.length === 0) return\n\n const horizontal = orientation === 'horizontal' || orientation === 'both'\n const vertical = orientation === 'vertical' || orientation === 'both'\n\n let next = active()\n switch (event.key) {\n case 'ArrowRight':\n if (!horizontal) return\n next = active() + 1\n break\n case 'ArrowLeft':\n if (!horizontal) return\n next = active() - 1\n break\n case 'ArrowDown':\n if (!vertical) return\n next = active() + 1\n break\n case 'ArrowUp':\n if (!vertical) return\n next = active() - 1\n break\n case 'Home':\n next = 0\n break\n case 'End':\n next = nodes.length - 1\n break\n default:\n return\n }\n\n event.preventDefault()\n\n if (next < 0) next = loop ? nodes.length - 1 : 0\n if (next >= nodes.length) next = loop ? 0 : nodes.length - 1\n\n onActiveChange(next)\n nodes[next]?.focus()\n }\n}\n"]}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, shallowRef, readonly, watch, ref } from 'vue';
|
|
2
|
+
import { motionValue, springTo, animate, observeIntersection, observeResize, trackScroll, trackContainerScroll, trackPointer, onReducedMotionChange, getTicker } from '@ansh-ui/core';
|
|
3
|
+
|
|
4
|
+
// src/composables/use-motion.ts
|
|
5
|
+
function useMotionValue(initial) {
|
|
6
|
+
const value = motionValue(initial);
|
|
7
|
+
if (getCurrentScope()) onScopeDispose(() => value.destroy());
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function useMotionRef(value) {
|
|
11
|
+
const state = shallowRef(value.get());
|
|
12
|
+
const stop = value.on((next) => {
|
|
13
|
+
state.value = next;
|
|
14
|
+
});
|
|
15
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
16
|
+
return readonly(state);
|
|
17
|
+
}
|
|
18
|
+
function useMotionStyle(value, format, property = "transform") {
|
|
19
|
+
const element = shallowRef(null);
|
|
20
|
+
const write = (next) => {
|
|
21
|
+
element.value?.style.setProperty(kebab(property), format(next));
|
|
22
|
+
};
|
|
23
|
+
const stop = value.onRender(write);
|
|
24
|
+
watch(element, (node) => {
|
|
25
|
+
if (node) write(value.get());
|
|
26
|
+
});
|
|
27
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
28
|
+
return element;
|
|
29
|
+
}
|
|
30
|
+
function useSpringValue(initial, options = {}) {
|
|
31
|
+
const value = useMotionValue(initial);
|
|
32
|
+
let target = initial;
|
|
33
|
+
const stop = springTo(value, () => target, options);
|
|
34
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
35
|
+
return {
|
|
36
|
+
value,
|
|
37
|
+
set(next) {
|
|
38
|
+
target = next;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function useAnimate() {
|
|
43
|
+
let controls = null;
|
|
44
|
+
if (getCurrentScope()) {
|
|
45
|
+
onScopeDispose(() => {
|
|
46
|
+
controls?.stop();
|
|
47
|
+
controls = null;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return function start(from, to, options) {
|
|
51
|
+
controls?.stop();
|
|
52
|
+
controls = animate(from, to, options);
|
|
53
|
+
return controls;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function useAnimatedFrom(source, target, map, options) {
|
|
57
|
+
let controls = null;
|
|
58
|
+
const stop = watch(
|
|
59
|
+
source,
|
|
60
|
+
(next) => {
|
|
61
|
+
controls?.stop();
|
|
62
|
+
controls = animate(target, map(next), options);
|
|
63
|
+
},
|
|
64
|
+
{ flush: "post" }
|
|
65
|
+
);
|
|
66
|
+
if (getCurrentScope()) {
|
|
67
|
+
onScopeDispose(() => {
|
|
68
|
+
controls?.stop();
|
|
69
|
+
stop();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function kebab(property) {
|
|
74
|
+
return property.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
75
|
+
}
|
|
76
|
+
function useTicker(callback, options = {}) {
|
|
77
|
+
const { phase = "write", priority = 0, enabled = true } = options;
|
|
78
|
+
let stop = null;
|
|
79
|
+
const attach = () => {
|
|
80
|
+
if (stop) return;
|
|
81
|
+
stop = getTicker().add(callback, { phase, priority, name: "useTicker" });
|
|
82
|
+
};
|
|
83
|
+
const detach = () => {
|
|
84
|
+
stop?.();
|
|
85
|
+
stop = null;
|
|
86
|
+
};
|
|
87
|
+
if (typeof enabled === "boolean") {
|
|
88
|
+
if (enabled) attach();
|
|
89
|
+
} else {
|
|
90
|
+
watch(enabled, (value) => value ? attach() : detach(), { immediate: true });
|
|
91
|
+
}
|
|
92
|
+
if (getCurrentScope()) onScopeDispose(detach);
|
|
93
|
+
return detach;
|
|
94
|
+
}
|
|
95
|
+
function useInView(target, options = {}) {
|
|
96
|
+
const inView = ref(false);
|
|
97
|
+
const { threshold = 0, rootMargin = "0px", once = false } = options;
|
|
98
|
+
let stop = null;
|
|
99
|
+
watch(
|
|
100
|
+
target,
|
|
101
|
+
(element) => {
|
|
102
|
+
stop?.();
|
|
103
|
+
stop = null;
|
|
104
|
+
if (!element) return;
|
|
105
|
+
stop = observeIntersection(
|
|
106
|
+
element,
|
|
107
|
+
(entry) => {
|
|
108
|
+
inView.value = entry.isIntersecting;
|
|
109
|
+
if (entry.isIntersecting && once) {
|
|
110
|
+
queueMicrotask(() => {
|
|
111
|
+
stop?.();
|
|
112
|
+
stop = null;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ threshold, rootMargin }
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
{ immediate: true, flush: "post" }
|
|
120
|
+
);
|
|
121
|
+
if (getCurrentScope()) onScopeDispose(() => stop?.());
|
|
122
|
+
return inView;
|
|
123
|
+
}
|
|
124
|
+
function useElementSize(target) {
|
|
125
|
+
const size = ref({ width: 0, height: 0 });
|
|
126
|
+
let stop = null;
|
|
127
|
+
watch(
|
|
128
|
+
target,
|
|
129
|
+
(element) => {
|
|
130
|
+
stop?.();
|
|
131
|
+
stop = null;
|
|
132
|
+
if (!element) return;
|
|
133
|
+
stop = observeResize(element, ({ width, height }) => {
|
|
134
|
+
if (size.value.width !== width || size.value.height !== height) {
|
|
135
|
+
size.value = { width, height };
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
{ immediate: true, flush: "post" }
|
|
140
|
+
);
|
|
141
|
+
if (getCurrentScope()) onScopeDispose(() => stop?.());
|
|
142
|
+
return size;
|
|
143
|
+
}
|
|
144
|
+
function useScrollProgress(target, options = {}) {
|
|
145
|
+
const progress = shallowRef({
|
|
146
|
+
progress: 0,
|
|
147
|
+
velocity: 0,
|
|
148
|
+
scroll: 0,
|
|
149
|
+
direction: 0,
|
|
150
|
+
inView: false
|
|
151
|
+
});
|
|
152
|
+
let stop = null;
|
|
153
|
+
watch(
|
|
154
|
+
target,
|
|
155
|
+
(element) => {
|
|
156
|
+
stop?.();
|
|
157
|
+
stop = null;
|
|
158
|
+
if (!element) return;
|
|
159
|
+
stop = trackScroll(
|
|
160
|
+
element,
|
|
161
|
+
(next) => {
|
|
162
|
+
progress.value = next;
|
|
163
|
+
options.onChange?.(next);
|
|
164
|
+
},
|
|
165
|
+
options
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
{ immediate: true, flush: "post" }
|
|
169
|
+
);
|
|
170
|
+
if (getCurrentScope()) onScopeDispose(() => stop?.());
|
|
171
|
+
return progress;
|
|
172
|
+
}
|
|
173
|
+
function useWindowScroll(onChange, options = {}) {
|
|
174
|
+
const state = shallowRef({ progress: 0, scroll: 0, velocity: 0, max: 1 });
|
|
175
|
+
const stop = trackContainerScroll((info) => {
|
|
176
|
+
state.value = info;
|
|
177
|
+
onChange?.(info);
|
|
178
|
+
}, options);
|
|
179
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
function usePointer(target, options = {}) {
|
|
183
|
+
const state = shallowRef(null);
|
|
184
|
+
let tracker = null;
|
|
185
|
+
watch(
|
|
186
|
+
target,
|
|
187
|
+
(element) => {
|
|
188
|
+
tracker?.destroy();
|
|
189
|
+
tracker = null;
|
|
190
|
+
state.value = null;
|
|
191
|
+
if (!element) return;
|
|
192
|
+
tracker = trackPointer(element, options);
|
|
193
|
+
state.value = tracker.state;
|
|
194
|
+
},
|
|
195
|
+
{ immediate: true, flush: "post" }
|
|
196
|
+
);
|
|
197
|
+
if (getCurrentScope()) onScopeDispose(() => tracker?.destroy());
|
|
198
|
+
return state;
|
|
199
|
+
}
|
|
200
|
+
function useReducedMotion() {
|
|
201
|
+
const reduced = ref(false);
|
|
202
|
+
const stop = onReducedMotionChange((value) => {
|
|
203
|
+
reduced.value = value;
|
|
204
|
+
});
|
|
205
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
206
|
+
return reduced;
|
|
207
|
+
}
|
|
208
|
+
function useFrameBudget() {
|
|
209
|
+
const report = ref(null);
|
|
210
|
+
let last = 0;
|
|
211
|
+
const stop = getTicker().onBudget((next) => {
|
|
212
|
+
const now = performance.now();
|
|
213
|
+
if (now - last < 250) return;
|
|
214
|
+
last = now;
|
|
215
|
+
report.value = next;
|
|
216
|
+
});
|
|
217
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
218
|
+
return report;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { useAnimate, useAnimatedFrom, useElementSize, useFrameBudget, useInView, useMotionRef, useMotionStyle, useMotionValue, usePointer, useReducedMotion, useScrollProgress, useSpringValue, useTicker, useWindowScroll };
|
|
222
|
+
//# sourceMappingURL=chunk-EVO23GUS.js.map
|
|
223
|
+
//# sourceMappingURL=chunk-EVO23GUS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/composables/use-motion.ts","../src/composables/use-observers.ts"],"names":["watch","getCurrentScope","onScopeDispose","shallowRef"],"mappings":"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","file":"chunk-EVO23GUS.js","sourcesContent":["import {\n type Ref,\n type ShallowRef,\n getCurrentScope,\n onScopeDispose,\n readonly,\n shallowRef,\n watch,\n} from 'vue'\nimport {\n type AnimationOptions,\n type MotionValue,\n type SpringOptions,\n animate,\n motionValue,\n springTo,\n} from '@ansh-ui/core'\n\n/**\n * Vue motion composables.\n *\n * The design principle mirrors the React package: animated values live **outside** Vue's\n * reactivity. A `ref` that updates 60 times a second triggers 60 reactive effect flushes and, if\n * anything in a template reads it, 60 patch cycles — which is precisely the cost that makes\n * \"animation in a framework\" slow.\n *\n * So {@link useMotionValue} returns the engine's `MotionValue` directly and writes to the DOM. Where\n * a value genuinely belongs in a template, {@link useMotionRef} bridges it into a `shallowRef` and\n * says plainly what that costs.\n *\n * Cleanup uses `onScopeDispose` rather than `onBeforeUnmount`, so these compose correctly inside\n * `effectScope()` — including in Pinia stores and outside a component instance entirely.\n */\n\n/**\n * Create a {@link MotionValue} bound to the current effect scope.\n *\n * Does not trigger re-renders. Pass it to `useMotionStyle` or read it in a `useTicker` callback.\n */\nexport function useMotionValue<T>(initial: T): MotionValue<T> {\n const value = motionValue(initial)\n if (getCurrentScope()) onScopeDispose(() => value.destroy())\n return value\n}\n\n/**\n * Bridge a MotionValue into a Vue ref.\n *\n * **This makes the value reactive, which means a component update on every change.** For a value\n * being animated continuously that is 60 updates per second. Use it for values that change at\n * interaction rate — a settled spring, a toggle — not for anything mid-animation.\n *\n * A `shallowRef` rather than `ref` because the value is replaced wholesale, never mutated, so deep\n * reactivity would be pure overhead.\n */\nexport function useMotionRef<T>(value: MotionValue<T>): Readonly<ShallowRef<T>> {\n const state = shallowRef(value.get()) as ShallowRef<T>\n const stop = value.on((next) => {\n state.value = next\n })\n if (getCurrentScope()) onScopeDispose(stop)\n return readonly(state) as Readonly<ShallowRef<T>>\n}\n\n/**\n * Write a MotionValue straight to an element's style property, bypassing Vue's renderer.\n *\n * This is the mechanism for smooth animation in Vue. Bind the returned ref to an element:\n *\n * ```vue\n * <script setup>\n * const x = useMotionValue(0)\n * const el = useMotionStyle(x, (v) => `translateX(${v}px)`)\n * </script>\n * <template><div :ref=\"el\" /></template>\n * ```\n */\nexport function useMotionStyle<T, El extends HTMLElement = HTMLElement>(\n value: MotionValue<T>,\n format: (value: T) => string,\n property = 'transform',\n): ShallowRef<El | null> {\n // Cast because Vue's `shallowRef` overloads resolve to a union for a generic element type; the\n // runtime value is unambiguously `El | null`.\n const element = shallowRef(null) as ShallowRef<El | null>\n\n const write = (next: T): void => {\n element.value?.style.setProperty(kebab(property), format(next))\n }\n\n const stop = value.onRender(write)\n\n // Apply immediately when the element arrives, so there is no frame at the un-styled default.\n watch(element, (node) => {\n if (node) write(value.get())\n })\n\n if (getCurrentScope()) onScopeDispose(stop)\n\n return element\n}\n\n/**\n * A spring that continuously chases a target.\n *\n * The target is a plain setter rather than a reactive ref, deliberately: reading a ref inside the\n * spring's per-frame callback would register a reactive dependency and flush an effect every\n * frame, which defeats the purpose of keeping the animation outside the reactivity system.\n */\nexport function useSpringValue(\n initial: number,\n options: SpringOptions = {},\n): { value: MotionValue<number>; set: (target: number) => void } {\n const value = useMotionValue(initial)\n let target = initial\n\n const stop = springTo(value, () => target, options)\n if (getCurrentScope()) onScopeDispose(stop)\n\n return {\n value,\n set(next: number) {\n target = next\n },\n }\n}\n\n/**\n * Scope-bound imperative animation.\n *\n * Any in-flight animation is stopped when the scope disposes, and when `start` is called again —\n * which is the behaviour you want, since two animations driving one value fight each other.\n */\nexport function useAnimate() {\n let controls: ReturnType<typeof animate> | null = null\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n controls?.stop()\n controls = null\n })\n }\n\n return function start<T>(\n from: T | MotionValue<T>,\n to: T | readonly T[],\n options?: AnimationOptions<T>,\n ) {\n controls?.stop()\n controls = animate(from, to, options)\n return controls\n }\n}\n\n/**\n * Drive a MotionValue from a reactive source.\n *\n * The bridge in the other direction: when a `ref` in your component changes, animate toward the new\n * value. This is the idiomatic way to connect Vue state to Ansh motion.\n *\n * ```ts\n * const open = ref(false)\n * const height = useMotionValue(0)\n * useAnimatedFrom(open, height, (isOpen) => (isOpen ? 320 : 0), { spring: 'snappy' })\n * ```\n */\nexport function useAnimatedFrom<Source, T>(\n source: Ref<Source>,\n target: MotionValue<T>,\n map: (value: Source) => T,\n options?: AnimationOptions<T>,\n): void {\n let controls: ReturnType<typeof animate> | null = null\n\n const stop = watch(\n source,\n (next) => {\n controls?.stop()\n controls = animate(target, map(next), options)\n },\n { flush: 'post' },\n )\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n controls?.stop()\n stop()\n })\n }\n}\n\nfunction kebab(property: string): string {\n return property.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)\n}\n","import {\n type Ref,\n type ShallowRef,\n getCurrentScope,\n onScopeDispose,\n ref,\n shallowRef,\n watch,\n} from 'vue'\nimport {\n type BudgetReport,\n type FrameState,\n type PointerState,\n type ScrollProgress,\n type ScrollTrackOptions,\n getTicker,\n observeIntersection,\n observeResize,\n onReducedMotionChange,\n trackContainerScroll,\n trackPointer,\n trackScroll,\n} from '@ansh-ui/core'\n\n/**\n * Observer and lifecycle composables.\n *\n * A consistent pattern throughout: composables that take an element accept a **ref you bind in the\n * template**, and watch it rather than reading it once. This matters because a template ref is\n * `null` during `setup()` and only populated after mount — and stays null across a `v-if` toggle,\n * then repopulates. Watching handles all of that; reading once in `onMounted` silently fails for\n * conditionally-rendered elements, which is the most common bug in hand-written Vue composables.\n */\n\n/** Subscribe to the shared ticker for the current scope's lifetime. */\nexport function useTicker(\n callback: (state: FrameState) => void,\n options: {\n phase?: 'read' | 'compute' | 'write' | 'post'\n priority?: number\n enabled?: Ref<boolean> | boolean\n } = {},\n): () => void {\n const { phase = 'write', priority = 0, enabled = true } = options\n let stop: (() => void) | null = null\n\n const attach = (): void => {\n if (stop) return\n stop = getTicker().add(callback, { phase, priority, name: 'useTicker' })\n }\n\n const detach = (): void => {\n stop?.()\n stop = null\n }\n\n if (typeof enabled === 'boolean') {\n if (enabled) attach()\n } else {\n watch(enabled, (value) => (value ? attach() : detach()), { immediate: true })\n }\n\n if (getCurrentScope()) onScopeDispose(detach)\n return detach\n}\n\nexport interface UseInViewOptions {\n threshold?: number\n rootMargin?: string\n once?: boolean\n}\n\n/**\n * Is the element in view?\n *\n * @param target A template ref. Watched, so it works with `v-if` and with elements that mount late.\n */\nexport function useInView(\n target: Ref<HTMLElement | null | undefined>,\n options: UseInViewOptions = {},\n): Ref<boolean> {\n const inView = ref(false)\n const { threshold = 0, rootMargin = '0px', once = false } = options\n\n let stop: (() => void) | null = null\n\n watch(\n target,\n (element) => {\n stop?.()\n stop = null\n if (!element) return\n\n stop = observeIntersection(\n element,\n (entry) => {\n inView.value = entry.isIntersecting\n if (entry.isIntersecting && once) {\n // Deferred: unsubscribing inside the observer callback would mutate the pool's\n // callback set while it is being iterated.\n queueMicrotask(() => {\n stop?.()\n stop = null\n })\n }\n },\n { threshold, rootMargin },\n )\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) onScopeDispose(() => stop?.())\n return inView\n}\n\n/** Track an element's size as a reactive value. */\nexport function useElementSize(\n target: Ref<HTMLElement | null | undefined>,\n): Ref<{ width: number; height: number }> {\n const size = ref({ width: 0, height: 0 })\n let stop: (() => void) | null = null\n\n watch(\n target,\n (element) => {\n stop?.()\n stop = null\n if (!element) return\n stop = observeResize(element, ({ width, height }) => {\n // Guard identical values: ResizeObserver fires on sub-pixel changes during animation, and\n // every assignment here triggers a reactive flush.\n if (size.value.width !== width || size.value.height !== height) {\n size.value = { width, height }\n }\n })\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) onScopeDispose(() => stop?.())\n return size\n}\n\n/**\n * Track an element's scroll progress.\n *\n * Returns a **shallowRef holding a mutable state object**, not a reactive number. Scroll progress\n * changes every frame; making it reactive would flush an effect per frame and undo the phase\n * separation the kernel provides. Read `progress.value.progress` inside a `useTicker` callback, or\n * pass `onChange` and write to the DOM directly.\n */\nexport function useScrollProgress(\n target: Ref<HTMLElement | null | undefined>,\n options: ScrollTrackOptions & { onChange?: (progress: ScrollProgress) => void } = {},\n): ShallowRef<ScrollProgress> {\n const progress = shallowRef<ScrollProgress>({\n progress: 0,\n velocity: 0,\n scroll: 0,\n direction: 0,\n inView: false,\n })\n\n let stop: (() => void) | null = null\n\n watch(\n target,\n (element) => {\n stop?.()\n stop = null\n if (!element) return\n\n stop = trackScroll(\n element,\n (next) => {\n // Assigned rather than mutated so a caller who *does* want reactivity gets it, while\n // callers reading it per frame pay only a property write.\n progress.value = next\n options.onChange?.(next)\n },\n options,\n )\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) onScopeDispose(() => stop?.())\n return progress\n}\n\n/** Track the window's (or a container's) overall scroll. */\nexport function useWindowScroll(\n onChange?: (info: { progress: number; scroll: number; velocity: number; max: number }) => void,\n options: { axis?: 'y' | 'x'; container?: HTMLElement | null } = {},\n): ShallowRef<{ progress: number; scroll: number; velocity: number; max: number }> {\n const state = shallowRef({ progress: 0, scroll: 0, velocity: 0, max: 1 })\n\n const stop = trackContainerScroll((info) => {\n state.value = info\n onChange?.(info)\n }, options)\n\n if (getCurrentScope()) onScopeDispose(stop)\n return state\n}\n\n/**\n * Track pointer position over an element.\n *\n * The returned object is mutated in place by the tracker each frame and is deliberately **not**\n * reactive — reading it in a template would flush on every pointer move.\n */\nexport function usePointer(\n target: Ref<HTMLElement | null | undefined>,\n options: { smoothing?: number; trackOutside?: boolean } = {},\n): ShallowRef<Readonly<PointerState> | null> {\n const state = shallowRef<Readonly<PointerState> | null>(null)\n let tracker: ReturnType<typeof trackPointer> | null = null\n\n watch(\n target,\n (element) => {\n tracker?.destroy()\n tracker = null\n state.value = null\n if (!element) return\n tracker = trackPointer(element, options)\n state.value = tracker.state\n },\n { immediate: true, flush: 'post' },\n )\n\n if (getCurrentScope()) onScopeDispose(() => tracker?.destroy())\n return state\n}\n\n/**\n * Does the user prefer reduced motion?\n *\n * Returns `false` during SSR, which is the hydration-safe default: the server cannot know, and\n * differing between server and first client render would be a mismatch.\n */\nexport function useReducedMotion(): Ref<boolean> {\n const reduced = ref(false)\n const stop = onReducedMotionChange((value) => {\n reduced.value = value\n })\n if (getCurrentScope()) onScopeDispose(stop)\n return reduced\n}\n\n/**\n * Live frame-budget telemetry for a development overlay.\n *\n * Sampled at 4 Hz rather than per frame: a number changing 60 times a second is unreadable, and\n * rendering it that often distorts the measurement it is reporting.\n */\nexport function useFrameBudget(): Ref<BudgetReport | null> {\n const report = ref<BudgetReport | null>(null)\n let last = 0\n\n const stop = getTicker().onBudget((next) => {\n const now = performance.now()\n if (now - last < 250) return\n last = now\n report.value = next\n })\n\n if (getCurrentScope()) onScopeDispose(stop)\n return report\n}\n"]}
|