v-uixy 1.9.1 → 1.9.3

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.
@@ -0,0 +1,79 @@
1
+ import { onMounted } from "vue";
2
+
3
+ const GPU_ATTRIBUTE = "data-uixy-gpu";
4
+ const IDLE_TIMEOUT_MS = 2000;
5
+ const FALLBACK_DELAY_MS = 200;
6
+
7
+ const SOFTWARE_RENDERER_PATTERN =
8
+ /swiftshader|llvmpipe|softpipe|software|basic render/i;
9
+
10
+ let cachedResult: boolean | null = null;
11
+ let isDetectionScheduled = false;
12
+
13
+ const createWebGlContext = (): WebGLRenderingContext | null => {
14
+ const canvas = document.createElement("canvas");
15
+ const options = { failIfMajorPerformanceCaveat: true };
16
+
17
+ return (canvas.getContext("webgl2", options) ??
18
+ canvas.getContext("webgl", options)) as WebGLRenderingContext | null;
19
+ };
20
+
21
+ const getRendererName = (gl: WebGLRenderingContext): string => {
22
+ const info = gl.getExtension("WEBGL_debug_renderer_info");
23
+
24
+ return info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "";
25
+ };
26
+
27
+ const checkGpuAcceleration = (): boolean => {
28
+ try {
29
+ const gl = createWebGlContext();
30
+
31
+ if (!gl) return false;
32
+
33
+ const isHardwareRenderer = !SOFTWARE_RENDERER_PATTERN.test(
34
+ getRendererName(gl),
35
+ );
36
+
37
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
38
+
39
+ return isHardwareRenderer;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ export const detectGpuAcceleration = (): boolean => {
46
+ if (typeof document === "undefined") return false;
47
+
48
+ cachedResult ??= checkGpuAcceleration();
49
+
50
+ return cachedResult;
51
+ };
52
+
53
+ const applyGpuAttribute = () => {
54
+ const value = detectGpuAcceleration() ? "on" : "off";
55
+
56
+ document.documentElement.setAttribute(GPU_ATTRIBUTE, value);
57
+ };
58
+
59
+ const runWhenIdle = (callback: () => void) => {
60
+ if ("requestIdleCallback" in window) {
61
+ window.requestIdleCallback(callback, { timeout: IDLE_TIMEOUT_MS });
62
+ return;
63
+ }
64
+
65
+ setTimeout(callback, FALLBACK_DELAY_MS);
66
+ };
67
+
68
+ export function useGpuAcceleration() {
69
+ onMounted(() => {
70
+ if (
71
+ isDetectionScheduled ||
72
+ document.documentElement.hasAttribute(GPU_ATTRIBUTE)
73
+ )
74
+ return;
75
+
76
+ isDetectionScheduled = true;
77
+ runWhenIdle(applyGpuAttribute);
78
+ });
79
+ }
@@ -1,73 +1,123 @@
1
1
  import { computed, nextTick, onScopeDispose, ref, type ComputedRef } from "vue";
2
2
 
3
+ const PAGE_ANCHORED_Z = 30;
3
4
  const BASE_Z = 60;
4
5
  const STEP_Z = 10;
5
6
 
7
+ const MANAGED_ATTR = "data-uixy-inert-managed";
8
+ const OVERLAY_ATTR = "data-uixy-overlay";
9
+ export const MODAL_ATTR = "data-uixy-modal";
10
+ export const MODAL_ID_ATTR = "data-uixy-modal-id";
11
+
6
12
  let seq = 0;
7
13
 
8
14
  const layers = ref<number[]>([]);
9
15
 
16
+ const modalStack = ref<number[]>([]);
17
+
18
+ const zOf = (el: Element): number => {
19
+ const raw = window.getComputedStyle(el).zIndex;
20
+ const parsed = Number.parseInt(raw ?? "", 10);
21
+
22
+ return Number.isFinite(parsed) ? parsed : 0;
23
+ };
24
+
25
+ const hasPinnedAncestor = (el: HTMLElement | null): boolean => {
26
+ while (el) {
27
+ const { position } = window.getComputedStyle(el);
28
+
29
+ if (position === "fixed" || position === "sticky") return true;
30
+
31
+ el = el.parentElement;
32
+ }
33
+
34
+ return false;
35
+ };
36
+
37
+ const isAnchoredToPage = (anchor: HTMLElement): boolean => {
38
+ const overlay = anchor.closest(`[${OVERLAY_ATTR}]`);
39
+
40
+ if (overlay && zOf(overlay) > PAGE_ANCHORED_Z) return false;
41
+
42
+ return !hasPinnedAncestor(anchor);
43
+ };
44
+
10
45
  export interface UseOverlayLayerReturn {
11
46
  id: number;
12
47
  zIndex: ComputedRef<number>;
13
48
  isTop: ComputedRef<boolean>;
14
49
  isActive: ComputedRef<boolean>;
15
- acquire: () => void;
50
+ acquire: (anchor?: HTMLElement | null) => void;
16
51
  release: () => void;
17
52
  }
18
53
 
19
54
  export function useOverlayLayer(): UseOverlayLayerReturn {
20
55
  const id = ++seq;
21
56
 
57
+ const pageAnchored = ref(false);
58
+
22
59
  const index = computed(() => layers.value.indexOf(id));
23
60
  const isActive = computed(() => index.value !== -1);
24
61
 
25
- const zIndex = computed(() =>
26
- isActive.value ? BASE_Z + (index.value + 1) * STEP_Z : BASE_Z + STEP_Z,
27
- );
62
+ const zIndex = computed(() => {
63
+ if (pageAnchored.value) return PAGE_ANCHORED_Z;
64
+
65
+ const position = isActive.value ? index.value + 1 : 1;
66
+
67
+ return BASE_Z + position * STEP_Z;
68
+ });
28
69
 
29
70
  const isTop = computed(
30
71
  () => isActive.value && index.value === layers.value.length - 1,
31
72
  );
32
73
 
33
- const acquire = () => {
34
- if (!layers.value.includes(id)) layers.value = [...layers.value, id];
35
- };
36
-
37
74
  const release = () => {
38
- if (layers.value.includes(id))
75
+ pageAnchored.value = false;
76
+
77
+ if (layers.value.includes(id)) {
39
78
  layers.value = layers.value.filter((x) => x !== id);
79
+ }
40
80
  };
41
81
 
42
- onScopeDispose(release);
82
+ const acquire = (anchor?: HTMLElement | null) => {
83
+ if (anchor && isAnchoredToPage(anchor)) {
84
+ release();
43
85
 
44
- return { id, zIndex, isTop, isActive, acquire, release };
45
- }
86
+ pageAnchored.value = true;
46
87
 
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";
88
+ return;
89
+ }
51
90
 
52
- const modalStack = ref<number[]>([]);
91
+ pageAnchored.value = false;
53
92
 
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
- };
93
+ if (!layers.value.includes(id)) {
94
+ layers.value = [...layers.value, id];
95
+ }
96
+ };
97
+
98
+ onScopeDispose(release);
99
+
100
+ return { id, zIndex, isTop, isActive, acquire, release };
101
+ }
59
102
 
60
103
  export const getTopModalEl = (): HTMLElement | null => {
61
104
  if (typeof document === "undefined") return null;
62
- const els = Array.from(
105
+
106
+ const modals = Array.from(
63
107
  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));
108
+ );
109
+
110
+ const openModals = modals.filter((el) => {
111
+ const rawId = el.getAttribute(MODAL_ID_ATTR);
112
+
113
+ if (rawId === null) return true;
114
+
115
+ return modalStack.value.includes(Number(rawId));
68
116
  });
69
- if (els.length === 0) return null;
70
- return els.reduce((a, b) => (zOf(b) >= zOf(a) ? b : a));
117
+
118
+ if (openModals.length === 0) return null;
119
+
120
+ return openModals.reduce((a, b) => (zOf(b) >= zOf(a) ? b : a));
71
121
  };
72
122
 
73
123
  function reconcileInert() {
@@ -80,6 +130,7 @@ function reconcileInert() {
80
130
  });
81
131
 
82
132
  const topEl = getTopModalEl();
133
+
83
134
  if (!topEl) return;
84
135
 
85
136
  const topZ = zOf(topEl);
@@ -87,7 +138,6 @@ function reconcileInert() {
87
138
  Array.from(document.body.children).forEach((el) => {
88
139
  if (!(el instanceof HTMLElement)) return;
89
140
  if (el === topEl || el.contains(topEl)) return;
90
-
91
141
  if (el.hasAttribute(OVERLAY_ATTR) && zOf(el) > topZ) return;
92
142
 
93
143
  el.setAttribute("inert", "");
@@ -113,14 +163,19 @@ export function useModalLayer(): UseModalLayerReturn {
113
163
 
114
164
  const open = () => {
115
165
  layer.acquire();
116
- if (!modalStack.value.includes(layer.id))
166
+
167
+ if (!modalStack.value.includes(layer.id)) {
117
168
  modalStack.value = [...modalStack.value, layer.id];
169
+ }
170
+
118
171
  nextTick(reconcileInert);
119
172
  };
120
173
 
121
174
  const close = () => {
122
175
  layer.release();
176
+
123
177
  modalStack.value = modalStack.value.filter((x) => x !== layer.id);
178
+
124
179
  reconcileInert();
125
180
  nextTick(reconcileInert);
126
181
  };
@@ -118,7 +118,7 @@ export function usePosition({
118
118
  [active, () => direction, () => align],
119
119
  ([isActive]) => {
120
120
  if (isActive) {
121
- acquire();
121
+ acquire(target.value);
122
122
  updatePosition();
123
123
  observeResize();
124
124
  window.addEventListener("resize", updatePosition);