@signality/core 0.0.1-alpha.2
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/README.md +60 -0
- package/browser/battery/index.d.ts +34 -0
- package/browser/bluetooth/index.d.ts +56 -0
- package/browser/breakpoints/index.d.ts +32 -0
- package/browser/broadcast-channel/index.d.ts +42 -0
- package/browser/browser-language/index.d.ts +34 -0
- package/browser/clipboard/index.d.ts +48 -0
- package/browser/device-posture/index.d.ts +18 -0
- package/browser/display-media/index.d.ts +80 -0
- package/browser/eye-dropper/index.d.ts +47 -0
- package/browser/favicon/index.d.ts +39 -0
- package/browser/fps/index.d.ts +46 -0
- package/browser/gamepad/index.d.ts +28 -0
- package/browser/geolocation/index.d.ts +64 -0
- package/browser/index.d.ts +29 -0
- package/browser/input-modality/index.d.ts +26 -0
- package/browser/listener/index.d.ts +61 -0
- package/browser/media-query/index.d.ts +36 -0
- package/browser/network/index.d.ts +44 -0
- package/browser/online/index.d.ts +27 -0
- package/browser/page-visibility/index.d.ts +27 -0
- package/browser/picture-in-picture/index.d.ts +42 -0
- package/browser/pointer-lock-element/index.d.ts +22 -0
- package/browser/screen-orientation/index.d.ts +29 -0
- package/browser/speech-recognition/index.d.ts +77 -0
- package/browser/speech-synthesis/index.d.ts +76 -0
- package/browser/storage/index.d.ts +142 -0
- package/browser/text-direction/index.d.ts +43 -0
- package/browser/vibration/index.d.ts +37 -0
- package/browser/wake-lock/index.d.ts +37 -0
- package/browser/web-notification/index.d.ts +58 -0
- package/browser/web-share/index.d.ts +42 -0
- package/browser/web-worker/index.d.ts +52 -0
- package/elements/active-element/index.d.ts +27 -0
- package/elements/dropzone/index.d.ts +61 -0
- package/elements/element-focus/index.d.ts +38 -0
- package/elements/element-focus-within/index.d.ts +29 -0
- package/elements/element-hover/index.d.ts +27 -0
- package/elements/element-size/index.d.ts +40 -0
- package/elements/element-visibility/index.d.ts +53 -0
- package/elements/index.d.ts +16 -0
- package/elements/mouse-position/index.d.ts +64 -0
- package/elements/on-click-outside/index.d.ts +42 -0
- package/elements/on-disconnect/index.d.ts +45 -0
- package/elements/on-long-press/index.d.ts +44 -0
- package/elements/pointer-swipe/index.d.ts +58 -0
- package/elements/scroll-position/index.d.ts +96 -0
- package/elements/swipe/index.d.ts +49 -0
- package/elements/text-selection/index.d.ts +39 -0
- package/elements/window-size/index.d.ts +46 -0
- package/fesm2022/signality-core-browser-battery.mjs +80 -0
- package/fesm2022/signality-core-browser-battery.mjs.map +1 -0
- package/fesm2022/signality-core-browser-bluetooth.mjs +112 -0
- package/fesm2022/signality-core-browser-bluetooth.mjs.map +1 -0
- package/fesm2022/signality-core-browser-breakpoints.mjs +51 -0
- package/fesm2022/signality-core-browser-breakpoints.mjs.map +1 -0
- package/fesm2022/signality-core-browser-broadcast-channel.mjs +74 -0
- package/fesm2022/signality-core-browser-broadcast-channel.mjs.map +1 -0
- package/fesm2022/signality-core-browser-browser-language.mjs +48 -0
- package/fesm2022/signality-core-browser-browser-language.mjs.map +1 -0
- package/fesm2022/signality-core-browser-clipboard.mjs +102 -0
- package/fesm2022/signality-core-browser-clipboard.mjs.map +1 -0
- package/fesm2022/signality-core-browser-device-posture.mjs +40 -0
- package/fesm2022/signality-core-browser-device-posture.mjs.map +1 -0
- package/fesm2022/signality-core-browser-display-media.mjs +121 -0
- package/fesm2022/signality-core-browser-display-media.mjs.map +1 -0
- package/fesm2022/signality-core-browser-eye-dropper.mjs +82 -0
- package/fesm2022/signality-core-browser-eye-dropper.mjs.map +1 -0
- package/fesm2022/signality-core-browser-favicon.mjs +100 -0
- package/fesm2022/signality-core-browser-favicon.mjs.map +1 -0
- package/fesm2022/signality-core-browser-fps.mjs +103 -0
- package/fesm2022/signality-core-browser-fps.mjs.map +1 -0
- package/fesm2022/signality-core-browser-gamepad.mjs +93 -0
- package/fesm2022/signality-core-browser-gamepad.mjs.map +1 -0
- package/fesm2022/signality-core-browser-geolocation.mjs +120 -0
- package/fesm2022/signality-core-browser-geolocation.mjs.map +1 -0
- package/fesm2022/signality-core-browser-input-modality.mjs +64 -0
- package/fesm2022/signality-core-browser-input-modality.mjs.map +1 -0
- package/fesm2022/signality-core-browser-listener.mjs +132 -0
- package/fesm2022/signality-core-browser-listener.mjs.map +1 -0
- package/fesm2022/signality-core-browser-media-query.mjs +55 -0
- package/fesm2022/signality-core-browser-media-query.mjs.map +1 -0
- package/fesm2022/signality-core-browser-network.mjs +76 -0
- package/fesm2022/signality-core-browser-network.mjs.map +1 -0
- package/fesm2022/signality-core-browser-online.mjs +49 -0
- package/fesm2022/signality-core-browser-online.mjs.map +1 -0
- package/fesm2022/signality-core-browser-page-visibility.mjs +47 -0
- package/fesm2022/signality-core-browser-page-visibility.mjs.map +1 -0
- package/fesm2022/signality-core-browser-picture-in-picture.mjs +93 -0
- package/fesm2022/signality-core-browser-picture-in-picture.mjs.map +1 -0
- package/fesm2022/signality-core-browser-pointer-lock-element.mjs +43 -0
- package/fesm2022/signality-core-browser-pointer-lock-element.mjs.map +1 -0
- package/fesm2022/signality-core-browser-screen-orientation.mjs +43 -0
- package/fesm2022/signality-core-browser-screen-orientation.mjs.map +1 -0
- package/fesm2022/signality-core-browser-speech-recognition.mjs +171 -0
- package/fesm2022/signality-core-browser-speech-recognition.mjs.map +1 -0
- package/fesm2022/signality-core-browser-speech-synthesis.mjs +146 -0
- package/fesm2022/signality-core-browser-speech-synthesis.mjs.map +1 -0
- package/fesm2022/signality-core-browser-storage.mjs +261 -0
- package/fesm2022/signality-core-browser-storage.mjs.map +1 -0
- package/fesm2022/signality-core-browser-text-direction.mjs +62 -0
- package/fesm2022/signality-core-browser-text-direction.mjs.map +1 -0
- package/fesm2022/signality-core-browser-vibration.mjs +94 -0
- package/fesm2022/signality-core-browser-vibration.mjs.map +1 -0
- package/fesm2022/signality-core-browser-wake-lock.mjs +149 -0
- package/fesm2022/signality-core-browser-wake-lock.mjs.map +1 -0
- package/fesm2022/signality-core-browser-web-notification.mjs +137 -0
- package/fesm2022/signality-core-browser-web-notification.mjs.map +1 -0
- package/fesm2022/signality-core-browser-web-share.mjs +92 -0
- package/fesm2022/signality-core-browser-web-share.mjs.map +1 -0
- package/fesm2022/signality-core-browser-web-worker.mjs +105 -0
- package/fesm2022/signality-core-browser-web-worker.mjs.map +1 -0
- package/fesm2022/signality-core-browser.mjs +34 -0
- package/fesm2022/signality-core-browser.mjs.map +1 -0
- package/fesm2022/signality-core-elements-active-element.mjs +88 -0
- package/fesm2022/signality-core-elements-active-element.mjs.map +1 -0
- package/fesm2022/signality-core-elements-dropzone.mjs +158 -0
- package/fesm2022/signality-core-elements-dropzone.mjs.map +1 -0
- package/fesm2022/signality-core-elements-element-focus-within.mjs +56 -0
- package/fesm2022/signality-core-elements-element-focus-within.mjs.map +1 -0
- package/fesm2022/signality-core-elements-element-focus.mjs +54 -0
- package/fesm2022/signality-core-elements-element-focus.mjs.map +1 -0
- package/fesm2022/signality-core-elements-element-hover.mjs +48 -0
- package/fesm2022/signality-core-elements-element-hover.mjs.map +1 -0
- package/fesm2022/signality-core-elements-element-size.mjs +73 -0
- package/fesm2022/signality-core-elements-element-size.mjs.map +1 -0
- package/fesm2022/signality-core-elements-element-visibility.mjs +76 -0
- package/fesm2022/signality-core-elements-element-visibility.mjs.map +1 -0
- package/fesm2022/signality-core-elements-mouse-position.mjs +109 -0
- package/fesm2022/signality-core-elements-mouse-position.mjs.map +1 -0
- package/fesm2022/signality-core-elements-on-click-outside.mjs +97 -0
- package/fesm2022/signality-core-elements-on-click-outside.mjs.map +1 -0
- package/fesm2022/signality-core-elements-on-disconnect.mjs +99 -0
- package/fesm2022/signality-core-elements-on-disconnect.mjs.map +1 -0
- package/fesm2022/signality-core-elements-on-long-press.mjs +84 -0
- package/fesm2022/signality-core-elements-on-long-press.mjs.map +1 -0
- package/fesm2022/signality-core-elements-pointer-swipe.mjs +116 -0
- package/fesm2022/signality-core-elements-pointer-swipe.mjs.map +1 -0
- package/fesm2022/signality-core-elements-scroll-position.mjs +175 -0
- package/fesm2022/signality-core-elements-scroll-position.mjs.map +1 -0
- package/fesm2022/signality-core-elements-swipe.mjs +107 -0
- package/fesm2022/signality-core-elements-swipe.mjs.map +1 -0
- package/fesm2022/signality-core-elements-text-selection.mjs +70 -0
- package/fesm2022/signality-core-elements-text-selection.mjs.map +1 -0
- package/fesm2022/signality-core-elements-window-size.mjs +81 -0
- package/fesm2022/signality-core-elements-window-size.mjs.map +1 -0
- package/fesm2022/signality-core-elements.mjs +21 -0
- package/fesm2022/signality-core-elements.mjs.map +1 -0
- package/fesm2022/signality-core-forms-cva.mjs +140 -0
- package/fesm2022/signality-core-forms-cva.mjs.map +1 -0
- package/fesm2022/signality-core-forms.mjs +6 -0
- package/fesm2022/signality-core-forms.mjs.map +1 -0
- package/fesm2022/signality-core-internal.mjs +268 -0
- package/fesm2022/signality-core-internal.mjs.map +1 -0
- package/fesm2022/signality-core-observers-intersection-observer.mjs +70 -0
- package/fesm2022/signality-core-observers-intersection-observer.mjs.map +1 -0
- package/fesm2022/signality-core-observers-mutation-observer.mjs +77 -0
- package/fesm2022/signality-core-observers-mutation-observer.mjs.map +1 -0
- package/fesm2022/signality-core-observers-performance-observer.mjs +84 -0
- package/fesm2022/signality-core-observers-performance-observer.mjs.map +1 -0
- package/fesm2022/signality-core-observers-resize-observer.mjs +69 -0
- package/fesm2022/signality-core-observers-resize-observer.mjs.map +1 -0
- package/fesm2022/signality-core-observers.mjs +9 -0
- package/fesm2022/signality-core-observers.mjs.map +1 -0
- package/fesm2022/signality-core-reactivity-debounced.mjs +27 -0
- package/fesm2022/signality-core-reactivity-debounced.mjs.map +1 -0
- package/fesm2022/signality-core-reactivity-throttled.mjs +27 -0
- package/fesm2022/signality-core-reactivity-throttled.mjs.map +1 -0
- package/fesm2022/signality-core-reactivity-watcher.mjs +36 -0
- package/fesm2022/signality-core-reactivity-watcher.mjs.map +1 -0
- package/fesm2022/signality-core-reactivity.mjs +8 -0
- package/fesm2022/signality-core-reactivity.mjs.map +1 -0
- package/fesm2022/signality-core-router-fragment.mjs +41 -0
- package/fesm2022/signality-core-router-fragment.mjs.map +1 -0
- package/fesm2022/signality-core-router-params.mjs +45 -0
- package/fesm2022/signality-core-router-params.mjs.map +1 -0
- package/fesm2022/signality-core-router-query-params.mjs +67 -0
- package/fesm2022/signality-core-router-query-params.mjs.map +1 -0
- package/fesm2022/signality-core-router-route-data.mjs +46 -0
- package/fesm2022/signality-core-router-route-data.mjs.map +1 -0
- package/fesm2022/signality-core-router-router-listener.mjs +50 -0
- package/fesm2022/signality-core-router-router-listener.mjs.map +1 -0
- package/fesm2022/signality-core-router-title.mjs +54 -0
- package/fesm2022/signality-core-router-title.mjs.map +1 -0
- package/fesm2022/signality-core-router-url.mjs +53 -0
- package/fesm2022/signality-core-router-url.mjs.map +1 -0
- package/fesm2022/signality-core-router.mjs +12 -0
- package/fesm2022/signality-core-router.mjs.map +1 -0
- package/fesm2022/signality-core-scheduling-debounce-callback.mjs +59 -0
- package/fesm2022/signality-core-scheduling-debounce-callback.mjs.map +1 -0
- package/fesm2022/signality-core-scheduling-interval.mjs +110 -0
- package/fesm2022/signality-core-scheduling-interval.mjs.map +1 -0
- package/fesm2022/signality-core-scheduling-throttle-callback.mjs +66 -0
- package/fesm2022/signality-core-scheduling-throttle-callback.mjs.map +1 -0
- package/fesm2022/signality-core-scheduling.mjs +8 -0
- package/fesm2022/signality-core-scheduling.mjs.map +1 -0
- package/fesm2022/signality-core-types.mjs +4 -0
- package/fesm2022/signality-core-types.mjs.map +1 -0
- package/fesm2022/signality-core.mjs +13 -0
- package/fesm2022/signality-core.mjs.map +1 -0
- package/forms/cva/index.d.ts +60 -0
- package/forms/index.d.ts +1 -0
- package/index.d.ts +8 -0
- package/internal/constants/index.d.ts +2 -0
- package/internal/constants/mobile-regex.d.ts +1 -0
- package/internal/constants/stubs.d.ts +32 -0
- package/internal/index.d.ts +4 -0
- package/internal/providers/index.d.ts +3 -0
- package/internal/providers/is-browser.d.ts +2 -0
- package/internal/providers/is-mobile.d.ts +2 -0
- package/internal/providers/is-server.d.ts +2 -0
- package/internal/types/index.d.ts +2 -0
- package/internal/types/timer.d.ts +1 -0
- package/internal/types/union.d.ts +1 -0
- package/internal/utils/bom/index.d.ts +1 -0
- package/internal/utils/bom/is-window.d.ts +1 -0
- package/internal/utils/const-signal.d.ts +10 -0
- package/internal/utils/context.d.ts +18 -0
- package/internal/utils/create-token.d.ts +8 -0
- package/internal/utils/dom/get-active-element.d.ts +1 -0
- package/internal/utils/dom/get-event-target.d.ts +1 -0
- package/internal/utils/dom/get-pip-element.d.ts +1 -0
- package/internal/utils/dom/get-shadow-root.d.ts +1 -0
- package/internal/utils/dom/index.d.ts +6 -0
- package/internal/utils/dom/is-element.d.ts +1 -0
- package/internal/utils/dom/is-node-within.d.ts +1 -0
- package/internal/utils/index.d.ts +10 -0
- package/internal/utils/is-plain-object.d.ts +1 -0
- package/internal/utils/is-query-signal.d.ts +10 -0
- package/internal/utils/proxy-signal.d.ts +18 -0
- package/internal/utils/to-element.d.ts +12 -0
- package/internal/utils/to-value.d.ts +6 -0
- package/observers/index.d.ts +4 -0
- package/observers/intersection-observer/index.d.ts +42 -0
- package/observers/mutation-observer/index.d.ts +45 -0
- package/observers/performance-observer/index.d.ts +58 -0
- package/observers/resize-observer/index.d.ts +40 -0
- package/package.json +343 -0
- package/reactivity/debounced/index.d.ts +50 -0
- package/reactivity/index.d.ts +3 -0
- package/reactivity/throttled/index.d.ts +53 -0
- package/reactivity/watcher/index.d.ts +68 -0
- package/router/fragment/index.d.ts +26 -0
- package/router/index.d.ts +7 -0
- package/router/params/index.d.ts +28 -0
- package/router/query-params/index.d.ts +80 -0
- package/router/route-data/index.d.ts +28 -0
- package/router/router-listener/index.d.ts +83 -0
- package/router/title/index.d.ts +29 -0
- package/router/url/index.d.ts +32 -0
- package/scheduling/debounce-callback/index.d.ts +28 -0
- package/scheduling/index.d.ts +3 -0
- package/scheduling/interval/index.d.ts +51 -0
- package/scheduling/throttle-callback/index.d.ts +30 -0
- package/types/index.d.ts +6 -0
- package/types/maybe-element-signal.d.ts +2 -0
- package/types/maybe-signal.d.ts +2 -0
- package/types/signal-value.d.ts +2 -0
- package/types/signal-values.d.ts +5 -0
- package/types/unref-element.d.ts +2 -0
- package/types/with-injector.d.ts +8 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { signal, untracked } from '@angular/core';
|
|
2
|
+
import { setupContext, NOOP_FN, constSignal, createToken } from '@signality/core/internal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reactive FPS monitor using requestAnimationFrame.
|
|
6
|
+
*
|
|
7
|
+
* @param options - Configuration options
|
|
8
|
+
* @returns An FpsRef with fps signal and control methods
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @Component({
|
|
13
|
+
* template: `
|
|
14
|
+
* <p>FPS: {{ fpsMonitor.fps() }}</p>
|
|
15
|
+
* <button (click)="fpsMonitor.stop()">Stop</button>
|
|
16
|
+
* <button (click)="fpsMonitor.start()">Start</button>
|
|
17
|
+
* `
|
|
18
|
+
* })
|
|
19
|
+
* class FpsComponent {
|
|
20
|
+
* readonly fpsMonitor = fps();
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function fps(options) {
|
|
25
|
+
const { runInContext } = setupContext(options?.injector, fps);
|
|
26
|
+
return runInContext(({ isServer, onCleanup }) => {
|
|
27
|
+
if (isServer) {
|
|
28
|
+
return {
|
|
29
|
+
fps: constSignal(0),
|
|
30
|
+
isRunning: constSignal(false),
|
|
31
|
+
start: NOOP_FN,
|
|
32
|
+
stop: NOOP_FN,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const immediate = options?.immediate ?? true;
|
|
36
|
+
const sampleSize = options?.sampleSize ?? 60;
|
|
37
|
+
const fpsValue = signal(0, ...(ngDevMode ? [{ debugName: "fpsValue" }] : []));
|
|
38
|
+
const isRunning = signal(false, ...(ngDevMode ? [{ debugName: "isRunning" }] : []));
|
|
39
|
+
let frameId;
|
|
40
|
+
let lastTime = performance.now();
|
|
41
|
+
let frameCount = 0;
|
|
42
|
+
let frameTimes = [];
|
|
43
|
+
const loop = (currentTime) => {
|
|
44
|
+
const delta = currentTime - lastTime;
|
|
45
|
+
lastTime = currentTime;
|
|
46
|
+
if (delta > 0) {
|
|
47
|
+
frameTimes.push(1000 / delta);
|
|
48
|
+
if (frameTimes.length > sampleSize) {
|
|
49
|
+
frameTimes.shift();
|
|
50
|
+
}
|
|
51
|
+
frameCount++;
|
|
52
|
+
if (frameCount >= 10) {
|
|
53
|
+
const avgFps = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
|
|
54
|
+
fpsValue.set(Math.round(avgFps));
|
|
55
|
+
frameCount = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (untracked(isRunning)) {
|
|
59
|
+
frameId = requestAnimationFrame(loop);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const start = () => {
|
|
63
|
+
const running = untracked(isRunning);
|
|
64
|
+
if (running) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
isRunning.set(true);
|
|
68
|
+
lastTime = performance.now();
|
|
69
|
+
frameCount = 0;
|
|
70
|
+
frameTimes = [];
|
|
71
|
+
frameId = requestAnimationFrame(loop);
|
|
72
|
+
};
|
|
73
|
+
const stop = () => {
|
|
74
|
+
const running = untracked(isRunning);
|
|
75
|
+
if (!running) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
isRunning.set(false);
|
|
79
|
+
if (frameId !== undefined) {
|
|
80
|
+
cancelAnimationFrame(frameId);
|
|
81
|
+
frameId = undefined;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
if (immediate) {
|
|
85
|
+
start();
|
|
86
|
+
}
|
|
87
|
+
onCleanup(stop);
|
|
88
|
+
return {
|
|
89
|
+
fps: fpsValue.asReadonly(),
|
|
90
|
+
isRunning: isRunning.asReadonly(),
|
|
91
|
+
start,
|
|
92
|
+
stop,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const FPS = /* @__PURE__ */ createToken(fps);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generated bundle index. Do not edit.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
export { FPS, fps };
|
|
103
|
+
//# sourceMappingURL=signality-core-browser-fps.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signality-core-browser-fps.mjs","sources":["../../../projects/core/browser/fps/index.ts","../../../projects/core/browser/fps/signality-core-browser-fps.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, createToken, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface FpsOptions extends WithInjector {\n /**\n * Start monitoring immediately.\n * @default true\n */\n readonly immediate?: boolean;\n\n /**\n * Number of frames to average.\n * @default 60\n */\n readonly sampleSize?: number;\n}\n\nexport interface FpsRef {\n /** Current frames per second */\n readonly fps: Signal<number>;\n\n /** Whether monitoring is active */\n readonly isRunning: Signal<boolean>;\n\n /** Start FPS monitoring */\n readonly start: () => void;\n\n /** Stop FPS monitoring */\n readonly stop: () => void;\n}\n\n/**\n * Reactive FPS monitor using requestAnimationFrame.\n *\n * @param options - Configuration options\n * @returns An FpsRef with fps signal and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <p>FPS: {{ fpsMonitor.fps() }}</p>\n * <button (click)=\"fpsMonitor.stop()\">Stop</button>\n * <button (click)=\"fpsMonitor.start()\">Start</button>\n * `\n * })\n * class FpsComponent {\n * readonly fpsMonitor = fps();\n * }\n * ```\n */\nexport function fps(options?: FpsOptions): FpsRef {\n const { runInContext } = setupContext(options?.injector, fps);\n\n return runInContext(({ isServer, onCleanup }) => {\n if (isServer) {\n return {\n fps: constSignal(0),\n isRunning: constSignal(false),\n start: NOOP_FN,\n stop: NOOP_FN,\n };\n }\n\n const immediate = options?.immediate ?? true;\n const sampleSize = options?.sampleSize ?? 60;\n\n const fpsValue = signal(0);\n const isRunning = signal(false);\n\n let frameId: number | undefined;\n let lastTime = performance.now();\n let frameCount = 0;\n let frameTimes: number[] = [];\n\n const loop = (currentTime: number) => {\n const delta = currentTime - lastTime;\n lastTime = currentTime;\n\n if (delta > 0) {\n frameTimes.push(1000 / delta);\n\n if (frameTimes.length > sampleSize) {\n frameTimes.shift();\n }\n\n frameCount++;\n\n if (frameCount >= 10) {\n const avgFps = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;\n fpsValue.set(Math.round(avgFps));\n frameCount = 0;\n }\n }\n\n if (untracked(isRunning)) {\n frameId = requestAnimationFrame(loop);\n }\n };\n\n const start = () => {\n const running = untracked(isRunning);\n\n if (running) {\n return;\n }\n\n isRunning.set(true);\n lastTime = performance.now();\n frameCount = 0;\n frameTimes = [];\n frameId = requestAnimationFrame(loop);\n };\n\n const stop = () => {\n const running = untracked(isRunning);\n\n if (!running) {\n return;\n }\n\n isRunning.set(false);\n\n if (frameId !== undefined) {\n cancelAnimationFrame(frameId);\n frameId = undefined;\n }\n };\n\n if (immediate) {\n start();\n }\n\n onCleanup(stop);\n\n return {\n fps: fpsValue.asReadonly(),\n isRunning: isRunning.asReadonly(),\n start,\n stop,\n };\n });\n}\n\nexport const FPS = /* @__PURE__ */ createToken(fps);\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { signal, computed } from '@angular/core';
|
|
2
|
+
import { setupContext, constSignal } from '@signality/core/internal';
|
|
3
|
+
import { setupSync, listener } from '@signality/core/browser/listener';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Signal-based wrapper around the Gamepad API.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Optional injector for DI context
|
|
9
|
+
* @returns A GamepadRef with gamepad state signals
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const gp = gamepad();
|
|
14
|
+
*
|
|
15
|
+
* // In template: @for (pad of gp.gamepads(); track pad?.index) { ... }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function gamepad(options) {
|
|
19
|
+
const { runInContext } = setupContext(options?.injector, gamepad);
|
|
20
|
+
return runInContext(({ onCleanup, isBrowser }) => {
|
|
21
|
+
const isSupported = constSignal(isBrowser && 'getGamepads' in navigator);
|
|
22
|
+
if (!isSupported()) {
|
|
23
|
+
return {
|
|
24
|
+
isSupported,
|
|
25
|
+
gamepads: constSignal([]),
|
|
26
|
+
activeGamepad: constSignal(undefined),
|
|
27
|
+
axes: constSignal([]),
|
|
28
|
+
buttons: constSignal([]),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const getGamepads = () => {
|
|
32
|
+
try {
|
|
33
|
+
const pads = navigator.getGamepads();
|
|
34
|
+
return [...pads];
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (ngDevMode) {
|
|
38
|
+
console.warn(`[gamepad] Failed to get gamepads.`, error);
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const gamepads = signal(getGamepads(), ...(ngDevMode ? [{ debugName: "gamepads" }] : []));
|
|
44
|
+
const activeGamepad = computed(() => gamepads().find(gp => gp !== null) ?? undefined, ...(ngDevMode ? [{ debugName: "activeGamepad" }] : []));
|
|
45
|
+
const axes = computed(() => activeGamepad()?.axes ?? [], ...(ngDevMode ? [{ debugName: "axes" }] : []));
|
|
46
|
+
const buttons = computed(() => activeGamepad()?.buttons ?? [], ...(ngDevMode ? [{ debugName: "buttons" }] : []));
|
|
47
|
+
let animationFrameId = null;
|
|
48
|
+
const pollGamepads = () => {
|
|
49
|
+
gamepads.set(getGamepads());
|
|
50
|
+
animationFrameId = requestAnimationFrame(pollGamepads);
|
|
51
|
+
};
|
|
52
|
+
const onGamepadConnected = () => {
|
|
53
|
+
gamepads.set(getGamepads());
|
|
54
|
+
if (animationFrameId === null) {
|
|
55
|
+
pollGamepads();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const onGamepadDisconnected = () => {
|
|
59
|
+
gamepads.set(getGamepads());
|
|
60
|
+
const hasGamepads = gamepads().some(gp => gp !== null);
|
|
61
|
+
if (!hasGamepads && animationFrameId !== null) {
|
|
62
|
+
cancelAnimationFrame(animationFrameId);
|
|
63
|
+
animationFrameId = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
setupSync(() => {
|
|
67
|
+
listener.passive(window, 'gamepadconnected', onGamepadConnected);
|
|
68
|
+
listener.passive(window, 'gamepaddisconnected', onGamepadDisconnected);
|
|
69
|
+
});
|
|
70
|
+
if (gamepads().some(gp => gp !== null)) {
|
|
71
|
+
pollGamepads();
|
|
72
|
+
}
|
|
73
|
+
onCleanup(() => {
|
|
74
|
+
if (animationFrameId !== null) {
|
|
75
|
+
cancelAnimationFrame(animationFrameId);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
isSupported,
|
|
80
|
+
gamepads: gamepads.asReadonly(),
|
|
81
|
+
activeGamepad,
|
|
82
|
+
axes,
|
|
83
|
+
buttons,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generated bundle index. Do not edit.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
export { gamepad };
|
|
93
|
+
//# sourceMappingURL=signality-core-browser-gamepad.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signality-core-browser-gamepad.mjs","sources":["../../../projects/core/browser/gamepad/index.ts","../../../projects/core/browser/gamepad/signality-core-browser-gamepad.ts"],"sourcesContent":["import { computed, signal, type Signal } from '@angular/core';\nimport { constSignal, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { listener, setupSync } from '@signality/core/browser/listener';\n\nexport interface GamepadRef {\n /** Whether Gamepad API is supported */\n readonly isSupported: Signal<boolean>;\n\n /** Array of connected gamepads */\n readonly gamepads: Signal<(Gamepad | null)[]>;\n\n /** First connected gamepad */\n readonly activeGamepad: Signal<Gamepad | undefined>;\n\n /** Axes values of active gamepad */\n readonly axes: Signal<readonly number[]>;\n\n /** Button states of active gamepad */\n readonly buttons: Signal<readonly GamepadButton[]>;\n}\n\n/**\n * Signal-based wrapper around the Gamepad API.\n *\n * @param options - Optional injector for DI context\n * @returns A GamepadRef with gamepad state signals\n *\n * @example\n * ```typescript\n * const gp = gamepad();\n *\n * // In template: @for (pad of gp.gamepads(); track pad?.index) { ... }\n * ```\n */\nexport function gamepad(options?: WithInjector): GamepadRef {\n const { runInContext } = setupContext(options?.injector, gamepad);\n\n return runInContext(({ onCleanup, isBrowser }) => {\n const isSupported = constSignal(isBrowser && 'getGamepads' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n gamepads: constSignal([]),\n activeGamepad: constSignal(undefined),\n axes: constSignal([]),\n buttons: constSignal([]),\n };\n }\n\n const getGamepads = () => {\n try {\n const pads = navigator.getGamepads();\n return [...pads];\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[gamepad] Failed to get gamepads.`, error);\n }\n return [];\n }\n };\n\n const gamepads = signal<(Gamepad | null)[]>(getGamepads());\n const activeGamepad = computed(() => gamepads().find(gp => gp !== null) ?? undefined);\n const axes = computed(() => activeGamepad()?.axes ?? []);\n const buttons = computed(() => activeGamepad()?.buttons ?? []);\n\n let animationFrameId: number | null = null;\n\n const pollGamepads = () => {\n gamepads.set(getGamepads());\n animationFrameId = requestAnimationFrame(pollGamepads);\n };\n\n const onGamepadConnected = () => {\n gamepads.set(getGamepads());\n\n if (animationFrameId === null) {\n pollGamepads();\n }\n };\n\n const onGamepadDisconnected = () => {\n gamepads.set(getGamepads());\n\n const hasGamepads = gamepads().some(gp => gp !== null);\n\n if (!hasGamepads && animationFrameId !== null) {\n cancelAnimationFrame(animationFrameId);\n animationFrameId = null;\n }\n };\n\n setupSync(() => {\n listener.passive(window, 'gamepadconnected', onGamepadConnected);\n listener.passive(window, 'gamepaddisconnected', onGamepadDisconnected);\n });\n\n if (gamepads().some(gp => gp !== null)) {\n pollGamepads();\n }\n\n onCleanup(() => {\n if (animationFrameId !== null) {\n cancelAnimationFrame(animationFrameId);\n }\n });\n\n return {\n isSupported,\n gamepads: gamepads.asReadonly(),\n activeGamepad,\n axes,\n buttons,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAsBA;;;;;;;;;;;;AAYG;AACG,SAAU,OAAO,CAAC,OAAsB,EAAA;AAC5C,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;IAEjE,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAI;QAC/C,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,aAAa,IAAI,SAAS,CAAC;AAExE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;AACzB,gBAAA,aAAa,EAAE,WAAW,CAAC,SAAS,CAAC;AACrC,gBAAA,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;AACrB,gBAAA,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;aACzB;QACH;QAEA,MAAM,WAAW,GAAG,MAAK;AACvB,YAAA,IAAI;AACF,gBAAA,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,EAAE;AACpC,gBAAA,OAAO,CAAC,GAAG,IAAI,CAAC;YAClB;YAAE,OAAO,KAAK,EAAE;gBACd,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,CAAC;gBAC1D;AACA,gBAAA,OAAO,EAAE;YACX;AACF,QAAA,CAAC;AAED,QAAA,MAAM,QAAQ,GAAG,MAAM,CAAqB,WAAW,EAAE,oDAAC;QAC1D,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,IAAI,SAAS,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,eAAA,EAAA,CAAA,GAAA,EAAA,CAAA,CAAC;AACrF,QAAA,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,aAAa,EAAE,EAAE,IAAI,IAAI,EAAE,gDAAC;AACxD,QAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,aAAa,EAAE,EAAE,OAAO,IAAI,EAAE,mDAAC;QAE9D,IAAI,gBAAgB,GAAkB,IAAI;QAE1C,MAAM,YAAY,GAAG,MAAK;AACxB,YAAA,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,YAAA,gBAAgB,GAAG,qBAAqB,CAAC,YAAY,CAAC;AACxD,QAAA,CAAC;QAED,MAAM,kBAAkB,GAAG,MAAK;AAC9B,YAAA,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;AAE3B,YAAA,IAAI,gBAAgB,KAAK,IAAI,EAAE;AAC7B,gBAAA,YAAY,EAAE;YAChB;AACF,QAAA,CAAC;QAED,MAAM,qBAAqB,GAAG,MAAK;AACjC,YAAA,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;AAE3B,YAAA,MAAM,WAAW,GAAG,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;AAEtD,YAAA,IAAI,CAAC,WAAW,IAAI,gBAAgB,KAAK,IAAI,EAAE;gBAC7C,oBAAoB,CAAC,gBAAgB,CAAC;gBACtC,gBAAgB,GAAG,IAAI;YACzB;AACF,QAAA,CAAC;QAED,SAAS,CAAC,MAAK;YACb,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,kBAAkB,EAAE,kBAAkB,CAAC;YAChE,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,qBAAqB,CAAC;AACxE,QAAA,CAAC,CAAC;AAEF,QAAA,IAAI,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE;AACtC,YAAA,YAAY,EAAE;QAChB;QAEA,SAAS,CAAC,MAAK;AACb,YAAA,IAAI,gBAAgB,KAAK,IAAI,EAAE;gBAC7B,oBAAoB,CAAC,gBAAgB,CAAC;YACxC;AACF,QAAA,CAAC,CAAC;QAEF,OAAO;YACL,WAAW;AACX,YAAA,QAAQ,EAAE,QAAQ,CAAC,UAAU,EAAE;YAC/B,aAAa;YACb,IAAI;YACJ,OAAO;SACR;AACH,IAAA,CAAC,CAAC;AACJ;;ACrHA;;AAEG;;;;"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { signal, untracked, computed } from '@angular/core';
|
|
2
|
+
import { setupContext, constSignal, NOOP_FN } from '@signality/core/internal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Signal-based wrapper around the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
|
|
6
|
+
*
|
|
7
|
+
* @param options - Configuration options
|
|
8
|
+
* @returns A GeolocationRef with location signals and control methods
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @Component({
|
|
13
|
+
* template: `
|
|
14
|
+
* @if (geo.isLoading()) {
|
|
15
|
+
* <p>Getting your location...</p>
|
|
16
|
+
* } @else if (geo.position()?.coords; as coords) {
|
|
17
|
+
* <p>{{ coords.latitude }}, {{ coords.longitude }}</p>
|
|
18
|
+
* }
|
|
19
|
+
* <button (click)="geo.stop()">Stop</button>
|
|
20
|
+
* <button (click)="geo.start()">Start</button>
|
|
21
|
+
* })
|
|
22
|
+
* class LocationComponent {
|
|
23
|
+
* readonly geo = geolocation();
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function geolocation(options) {
|
|
28
|
+
const { runInContext } = setupContext(options?.injector, geolocation);
|
|
29
|
+
return runInContext(({ isBrowser, onCleanup }) => {
|
|
30
|
+
const isSupported = constSignal(isBrowser && 'geolocation' in navigator);
|
|
31
|
+
if (!isSupported()) {
|
|
32
|
+
return {
|
|
33
|
+
isSupported,
|
|
34
|
+
position: constSignal(null),
|
|
35
|
+
error: constSignal(null),
|
|
36
|
+
isLoading: constSignal(false),
|
|
37
|
+
isActive: constSignal(false),
|
|
38
|
+
start: NOOP_FN,
|
|
39
|
+
stop: NOOP_FN,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const immediate = options?.immediate ?? true;
|
|
43
|
+
const positionOptions = {
|
|
44
|
+
enableHighAccuracy: options?.enableHighAccuracy ?? true,
|
|
45
|
+
maximumAge: options?.maximumAge ?? 0,
|
|
46
|
+
timeout: options?.timeout ?? Infinity,
|
|
47
|
+
};
|
|
48
|
+
const position = signal(null, ...(ngDevMode ? [{ debugName: "position" }] : []));
|
|
49
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
50
|
+
const isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
51
|
+
const watchId = signal(undefined, ...(ngDevMode ? [{ debugName: "watchId" }] : []));
|
|
52
|
+
const handleSuccess = (pos) => {
|
|
53
|
+
position.set(pos);
|
|
54
|
+
error.set(null);
|
|
55
|
+
isLoading.set(false);
|
|
56
|
+
};
|
|
57
|
+
const handleError = (err) => {
|
|
58
|
+
error.set(err);
|
|
59
|
+
isLoading.set(false);
|
|
60
|
+
};
|
|
61
|
+
const start = () => {
|
|
62
|
+
untracked(() => {
|
|
63
|
+
if (watchId() !== undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
isLoading.set(true);
|
|
67
|
+
const id = navigator.geolocation.watchPosition(handleSuccess, handleError, positionOptions);
|
|
68
|
+
watchId.set(id);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const stop = () => {
|
|
72
|
+
untracked(() => {
|
|
73
|
+
const currentId = watchId();
|
|
74
|
+
if (currentId !== undefined) {
|
|
75
|
+
navigator.geolocation.clearWatch(currentId);
|
|
76
|
+
watchId.set(undefined);
|
|
77
|
+
}
|
|
78
|
+
isLoading.set(false);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const abortController = new AbortController();
|
|
82
|
+
onCleanup(() => {
|
|
83
|
+
abortController.abort();
|
|
84
|
+
stop();
|
|
85
|
+
});
|
|
86
|
+
navigator.permissions.query({ name: 'geolocation' }).then(status => {
|
|
87
|
+
if (abortController.signal.aborted) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const check = () => {
|
|
91
|
+
if (status.state === 'denied') {
|
|
92
|
+
stop();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
check();
|
|
96
|
+
status.addEventListener('change', check, {
|
|
97
|
+
signal: abortController.signal,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
if (immediate) {
|
|
101
|
+
start();
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
isSupported,
|
|
105
|
+
position: position.asReadonly(),
|
|
106
|
+
error: error.asReadonly(),
|
|
107
|
+
isLoading: isLoading.asReadonly(),
|
|
108
|
+
isActive: computed(() => watchId() !== undefined),
|
|
109
|
+
start,
|
|
110
|
+
stop,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generated bundle index. Do not edit.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
export { geolocation };
|
|
120
|
+
//# sourceMappingURL=signality-core-browser-geolocation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signality-core-browser-geolocation.mjs","sources":["../../../projects/core/browser/geolocation/index.ts","../../../projects/core/browser/geolocation/signality-core-browser-geolocation.ts"],"sourcesContent":["import { computed, type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface GeolocationOptions extends WithInjector {\n /**\n * Start tracking immediately.\n * @default true\n */\n readonly immediate?: boolean;\n\n /**\n * Use GPS for better accuracy.\n * @default true\n */\n readonly enableHighAccuracy?: boolean;\n\n /**\n * Max age of cached position (ms).\n * @default 0\n */\n readonly maximumAge?: number;\n\n /**\n * Request timeout (ms).\n * @default Infinity\n */\n readonly timeout?: number;\n}\n\nexport interface GeolocationRef {\n /** Whether Geolocation is supported */\n readonly isSupported: Signal<boolean>;\n\n /** Full position object with timestamp */\n readonly position: Signal<GeolocationPosition | null>;\n\n /** Last error */\n readonly error: Signal<GeolocationPositionError | null>;\n\n /** Whether location tracking is currently active */\n readonly isActive: Signal<boolean>;\n\n /** Whether currently fetching location */\n readonly isLoading: Signal<boolean>;\n\n /** Start/resume watching position */\n readonly start: () => void;\n\n /** Stop watching position */\n readonly stop: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).\n *\n * @param options - Configuration options\n * @returns A GeolocationRef with location signals and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (geo.isLoading()) {\n * <p>Getting your location...</p>\n * } @else if (geo.position()?.coords; as coords) {\n * <p>{{ coords.latitude }}, {{ coords.longitude }}</p>\n * }\n * <button (click)=\"geo.stop()\">Stop</button>\n * <button (click)=\"geo.start()\">Start</button>\n * })\n * class LocationComponent {\n * readonly geo = geolocation();\n * }\n * ```\n */\nexport function geolocation(options?: GeolocationOptions): GeolocationRef {\n const { runInContext } = setupContext(options?.injector, geolocation);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'geolocation' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n position: constSignal(null),\n error: constSignal(null),\n isLoading: constSignal(false),\n isActive: constSignal(false),\n start: NOOP_FN,\n stop: NOOP_FN,\n };\n }\n\n const immediate = options?.immediate ?? true;\n\n const positionOptions: PositionOptions = {\n enableHighAccuracy: options?.enableHighAccuracy ?? true,\n maximumAge: options?.maximumAge ?? 0,\n timeout: options?.timeout ?? Infinity,\n };\n\n const position = signal<GeolocationPosition | null>(null);\n const error = signal<GeolocationPositionError | null>(null);\n const isLoading = signal(false);\n const watchId = signal<number | undefined>(undefined);\n\n const handleSuccess = (pos: GeolocationPosition) => {\n position.set(pos);\n error.set(null);\n isLoading.set(false);\n };\n\n const handleError = (err: GeolocationPositionError) => {\n error.set(err);\n isLoading.set(false);\n };\n\n const start = () => {\n untracked(() => {\n if (watchId() !== undefined) {\n return;\n }\n\n isLoading.set(true);\n const id = navigator.geolocation.watchPosition(handleSuccess, handleError, positionOptions);\n watchId.set(id);\n });\n };\n\n const stop = () => {\n untracked(() => {\n const currentId = watchId();\n\n if (currentId !== undefined) {\n navigator.geolocation.clearWatch(currentId);\n watchId.set(undefined);\n }\n\n isLoading.set(false);\n });\n };\n\n const abortController = new AbortController();\n\n onCleanup(() => {\n abortController.abort();\n stop();\n });\n\n navigator.permissions.query({ name: 'geolocation' }).then(status => {\n if (abortController.signal.aborted) {\n return;\n }\n\n const check = () => {\n if (status.state === 'denied') {\n stop();\n }\n };\n\n check();\n\n status.addEventListener('change', check, {\n signal: abortController.signal,\n });\n });\n\n if (immediate) {\n start();\n }\n\n return {\n isSupported,\n position: position.asReadonly(),\n error: error.asReadonly(),\n isLoading: isLoading.asReadonly(),\n isActive: computed(() => watchId() !== undefined),\n start,\n stop,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { setupContext, constSignal, createToken } from '@signality/core/internal';
|
|
3
|
+
import { setupSync, listener } from '@signality/core/browser/listener';
|
|
4
|
+
|
|
5
|
+
const TOUCH_BUFFER_MS = 650;
|
|
6
|
+
const MODIFIER_KEYS = ['Shift', 'Control', 'Alt', 'Meta', 'ContextMenu'];
|
|
7
|
+
/**
|
|
8
|
+
* Reactively track the user's current input method (keyboard, mouse, or touch).
|
|
9
|
+
*
|
|
10
|
+
* @param options - Optional configuration including injector
|
|
11
|
+
* @returns A signal containing the current input modality: `'keyboard'`, `'mouse'`, `'touch'`, or `null`
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* @Component({
|
|
16
|
+
* template: `
|
|
17
|
+
* <div [class.keyboard]="modality() === 'keyboard'">
|
|
18
|
+
* <p>Current input: {{ modality() ?? 'none' }}</p>
|
|
19
|
+
* <button>Button with conditional focus ring</button>
|
|
20
|
+
* </div>
|
|
21
|
+
* `
|
|
22
|
+
* })
|
|
23
|
+
* export class ModalityComponent {
|
|
24
|
+
* readonly modality = inputModality();
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function inputModality(options) {
|
|
29
|
+
const { runInContext } = setupContext(options?.injector, inputModality);
|
|
30
|
+
return runInContext(({ isServer }) => {
|
|
31
|
+
if (isServer) {
|
|
32
|
+
return constSignal(null);
|
|
33
|
+
}
|
|
34
|
+
const modality = signal(null, ...(ngDevMode ? [{ debugName: "modality" }] : []));
|
|
35
|
+
let lastTouchMs = 0;
|
|
36
|
+
setupSync(() => {
|
|
37
|
+
listener(document, 'keydown', (event) => {
|
|
38
|
+
if (MODIFIER_KEYS.includes(event.key)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
modality.set('keyboard');
|
|
42
|
+
});
|
|
43
|
+
listener(document, 'mousedown', () => {
|
|
44
|
+
if (Date.now() - lastTouchMs < TOUCH_BUFFER_MS) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
modality.set('mouse');
|
|
48
|
+
});
|
|
49
|
+
listener.passive(document, 'touchstart', () => {
|
|
50
|
+
lastTouchMs = Date.now();
|
|
51
|
+
modality.set('touch');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return modality.asReadonly();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const INPUT_MODALITY = /* @__PURE__ */ createToken(inputModality);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generated bundle index. Do not edit.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export { INPUT_MODALITY, inputModality };
|
|
64
|
+
//# sourceMappingURL=signality-core-browser-input-modality.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signality-core-browser-input-modality.mjs","sources":["../../../projects/core/browser/input-modality/index.ts","../../../projects/core/browser/input-modality/signality-core-browser-input-modality.ts"],"sourcesContent":["import { signal, type Signal } from '@angular/core';\nimport { constSignal, createToken, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { listener, setupSync } from '@signality/core/browser/listener';\n\nexport type InputModality = 'keyboard' | 'mouse' | 'touch' | null;\n\nconst TOUCH_BUFFER_MS = 650;\n\nconst MODIFIER_KEYS = ['Shift', 'Control', 'Alt', 'Meta', 'ContextMenu'];\n\n/**\n * Reactively track the user's current input method (keyboard, mouse, or touch).\n *\n * @param options - Optional configuration including injector\n * @returns A signal containing the current input modality: `'keyboard'`, `'mouse'`, `'touch'`, or `null`\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div [class.keyboard]=\"modality() === 'keyboard'\">\n * <p>Current input: {{ modality() ?? 'none' }}</p>\n * <button>Button with conditional focus ring</button>\n * </div>\n * `\n * })\n * export class ModalityComponent {\n * readonly modality = inputModality();\n * }\n * ```\n */\nexport function inputModality(options?: WithInjector): Signal<InputModality> {\n const { runInContext } = setupContext(options?.injector, inputModality);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(null);\n }\n\n const modality = signal<InputModality>(null);\n\n let lastTouchMs = 0;\n\n setupSync(() => {\n listener(document, 'keydown', (event: KeyboardEvent) => {\n if (MODIFIER_KEYS.includes(event.key)) {\n return;\n }\n modality.set('keyboard');\n });\n\n listener(document, 'mousedown', () => {\n if (Date.now() - lastTouchMs < TOUCH_BUFFER_MS) {\n return;\n }\n modality.set('mouse');\n });\n\n listener.passive(document, 'touchstart', () => {\n lastTouchMs = Date.now();\n modality.set('touch');\n });\n });\n\n return modality.asReadonly();\n });\n}\n\nexport const INPUT_MODALITY = /* @__PURE__ */ createToken(inputModality);\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAOA,MAAM,eAAe,GAAG,GAAG;AAE3B,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;AAoBG;AACG,SAAU,aAAa,CAAC,OAAsB,EAAA;AAClD,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC;AAEvE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,IAAI,CAAC;QAC1B;AAEA,QAAA,MAAM,QAAQ,GAAG,MAAM,CAAgB,IAAI,oDAAC;QAE5C,IAAI,WAAW,GAAG,CAAC;QAEnB,SAAS,CAAC,MAAK;YACb,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,KAAoB,KAAI;gBACrD,IAAI,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;oBACrC;gBACF;AACA,gBAAA,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;AAC1B,YAAA,CAAC,CAAC;AAEF,YAAA,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAK;gBACnC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,GAAG,eAAe,EAAE;oBAC9C;gBACF;AACA,gBAAA,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;AACvB,YAAA,CAAC,CAAC;YAEF,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAK;AAC5C,gBAAA,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE;AACxB,gBAAA,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;AACvB,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;AAEF,QAAA,OAAO,QAAQ,CAAC,UAAU,EAAE;AAC9B,IAAA,CAAC,CAAC;AACJ;AAEO,MAAM,cAAc,mBAAmB,WAAW,CAAC,aAAa;;ACrEvE;;AAEG;;;;"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterRenderEffect, effect } from '@angular/core';
|
|
2
|
+
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
3
|
+
import { setupContext, NOOP_EFFECT_REF, toElement, toValue } from '@signality/core/internal';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Signal-based wrapper around the [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) method.
|
|
7
|
+
*
|
|
8
|
+
* @param target - Event target
|
|
9
|
+
* @param event - Event type name
|
|
10
|
+
* @param handler - Event handler function
|
|
11
|
+
* @param options - Optional listener configuration
|
|
12
|
+
* @returns A ListenerRef that can be used to destroy the listener
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* @Component({
|
|
17
|
+
* template: `<button #btn>Click me</button>`,
|
|
18
|
+
* })
|
|
19
|
+
* class ListenerComponent {
|
|
20
|
+
* readonly btn = viewChild<ElementRef>('btn');
|
|
21
|
+
*
|
|
22
|
+
* constructor() {
|
|
23
|
+
* listener.capture.prevent(this.btn, 'click', event => {
|
|
24
|
+
* console.log('Button clicked!', event);
|
|
25
|
+
* });
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
const listener = createModifier({});
|
|
31
|
+
let isSyncSetupRequired = false;
|
|
32
|
+
/**
|
|
33
|
+
* By default, `listener()` registers event listeners after the render cycle completes
|
|
34
|
+
* to ensure DOM elements exist. However, global targets (window, document, navigator.*, etc.)
|
|
35
|
+
* are not tied to the render cycle. Use `setupSync()` to wrap listener calls when you need to prevent
|
|
36
|
+
* race conditions where a global event is dispatched before Angular completes its scheduled rendering tasks.
|
|
37
|
+
*/
|
|
38
|
+
function setupSync(listenerFactoryExecFn) {
|
|
39
|
+
isSyncSetupRequired = true;
|
|
40
|
+
try {
|
|
41
|
+
return listenerFactoryExecFn();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
isSyncSetupRequired = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function listenerImpl(applied, ...args) {
|
|
48
|
+
const options = args[3];
|
|
49
|
+
const { runInContext } = setupContext(options?.injector, listenerImpl);
|
|
50
|
+
return runInContext(({ isServer }) => {
|
|
51
|
+
if (isServer) {
|
|
52
|
+
return NOOP_EFFECT_REF;
|
|
53
|
+
}
|
|
54
|
+
const [maybeReactiveTarget, maybeReactiveEvent, rawHandler] = args;
|
|
55
|
+
const { stop, prevent, self, ...nativeOptions } = applied;
|
|
56
|
+
const hasModifiers = stop || prevent || self;
|
|
57
|
+
const handler = hasModifiers
|
|
58
|
+
? function (event) {
|
|
59
|
+
if (self && event.target !== event.currentTarget)
|
|
60
|
+
return;
|
|
61
|
+
if (prevent)
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
if (stop)
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
rawHandler.call(this, event);
|
|
66
|
+
}
|
|
67
|
+
: rawHandler;
|
|
68
|
+
const setupListener = (onCleanup) => {
|
|
69
|
+
const target = toElement(maybeReactiveTarget);
|
|
70
|
+
const event = toValue(maybeReactiveEvent);
|
|
71
|
+
if (!target) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
target.addEventListener(event, handler, nativeOptions);
|
|
75
|
+
onCleanup(() => {
|
|
76
|
+
target.removeEventListener(event, handler, nativeOptions);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
let effectRef;
|
|
80
|
+
if (isSyncSetupRequired) {
|
|
81
|
+
effectRef = syncEffect(setupListener);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
effectRef = afterRenderEffect({ read: setupListener });
|
|
85
|
+
}
|
|
86
|
+
return { destroy: () => effectRef.destroy() };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const MODIFIERS = new Set([
|
|
90
|
+
'capture',
|
|
91
|
+
'passive',
|
|
92
|
+
'once',
|
|
93
|
+
'stop',
|
|
94
|
+
'prevent',
|
|
95
|
+
'self',
|
|
96
|
+
]);
|
|
97
|
+
function createModifier(applied) {
|
|
98
|
+
const modifierFn = ((...args) => {
|
|
99
|
+
return listenerImpl(applied, ...args);
|
|
100
|
+
});
|
|
101
|
+
return new Proxy(modifierFn, {
|
|
102
|
+
get(target, prop) {
|
|
103
|
+
if (typeof prop !== 'string' || !MODIFIERS.has(prop)) {
|
|
104
|
+
return target[prop];
|
|
105
|
+
}
|
|
106
|
+
if (applied[prop]) {
|
|
107
|
+
return target;
|
|
108
|
+
}
|
|
109
|
+
return createModifier({ ...applied, [prop]: true });
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function syncEffect(effectFn, options) {
|
|
114
|
+
const effectRef = effect(effectFn, options);
|
|
115
|
+
const effectNode = effectRef[SIGNAL];
|
|
116
|
+
try {
|
|
117
|
+
effectNode.run();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (ngDevMode) {
|
|
121
|
+
console.warn('[syncEffect] Failed to run effectFn synchronously', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return effectRef;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generated bundle index. Do not edit.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
export { listener, setupSync };
|
|
132
|
+
//# sourceMappingURL=signality-core-browser-listener.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signality-core-browser-listener.mjs","sources":["../../../projects/core/browser/listener/index.ts","../../../projects/core/browser/listener/signality-core-browser-listener.ts"],"sourcesContent":["import {\n afterRenderEffect,\n type CreateEffectOptions,\n effect,\n type EffectCleanupRegisterFn,\n type EffectRef,\n} from '@angular/core';\nimport { type BaseEffectNode, SIGNAL } from '@angular/core/primitives/signals';\nimport { NOOP_EFFECT_REF, setupContext, toElement, toValue } from '@signality/core/internal';\nimport type { MaybeElementSignal, MaybeSignal, WithInjector } from '@signality/core/types';\n\nexport type ListenerOptions = WithInjector;\n\nexport interface ListenerRef {\n readonly destroy: () => void;\n}\n\nexport interface ListenerFunction {\n <E extends keyof WindowEventMap>(\n target: Window,\n event: MaybeSignal<E>,\n handler: (this: Window, e: WindowEventMap[E]) => any,\n options?: ListenerOptions\n ): ListenerRef;\n\n <E extends keyof DocumentEventMap>(\n target: Document,\n event: MaybeSignal<E>,\n handler: (this: Document, e: DocumentEventMap[E]) => any,\n options?: ListenerOptions\n ): ListenerRef;\n\n <E extends keyof ShadowRootEventMap>(\n target: MaybeSignal<ShadowRoot>,\n event: MaybeSignal<E>,\n handler: (this: ShadowRoot, e: ShadowRootEventMap[E]) => any,\n options?: ListenerOptions\n ): ListenerRef;\n\n <T extends HTMLElement, E extends keyof HTMLElementEventMap>(\n target: MaybeElementSignal<T>,\n event: MaybeSignal<E>,\n handler: (this: T, e: HTMLElementEventMap[E]) => any,\n options?: ListenerOptions\n ): ListenerRef;\n\n <T extends SVGElement, E extends keyof SVGElementEventMap>(\n target: MaybeElementSignal<T>,\n event: MaybeSignal<E>,\n handler: (this: T, e: SVGElementEventMap[E]) => any,\n options?: ListenerOptions\n ): ListenerRef;\n\n <Names extends string>(\n target: MaybeSignal<InferEventTarget<Names>>,\n event: MaybeSignal<Names>,\n handler: (e: Event) => void,\n options?: ListenerOptions\n ): ListenerRef;\n\n <EventType = Event>(\n target: MaybeSignal<EventTarget> | MaybeElementSignal<Element>,\n event: MaybeSignal<string>,\n handler: GeneralEventListener<EventType>,\n options?: ListenerOptions\n ): ListenerRef;\n\n readonly capture: ListenerFunction;\n readonly passive: ListenerFunction;\n readonly once: ListenerFunction;\n readonly stop: ListenerFunction;\n readonly prevent: ListenerFunction;\n readonly self: ListenerFunction;\n}\n\n/**\n * Signal-based wrapper around the [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) method.\n *\n * @param target - Event target\n * @param event - Event type name\n * @param handler - Event handler function\n * @param options - Optional listener configuration\n * @returns A ListenerRef that can be used to destroy the listener\n *\n * @example\n * ```typescript\n * @Component({\n * template: `<button #btn>Click me</button>`,\n * })\n * class ListenerComponent {\n * readonly btn = viewChild<ElementRef>('btn');\n *\n * constructor() {\n * listener.capture.prevent(this.btn, 'click', event => {\n * console.log('Button clicked!', event);\n * });\n * }\n * }\n * ```\n */\nexport const listener: ListenerFunction = createModifier({});\n\nlet isSyncSetupRequired = false;\n\n/**\n * By default, `listener()` registers event listeners after the render cycle completes\n * to ensure DOM elements exist. However, global targets (window, document, navigator.*, etc.)\n * are not tied to the render cycle. Use `setupSync()` to wrap listener calls when you need to prevent\n * race conditions where a global event is dispatched before Angular completes its scheduled rendering tasks.\n */\nexport function setupSync<T>(listenerFactoryExecFn: () => T): T {\n isSyncSetupRequired = true;\n\n try {\n return listenerFactoryExecFn();\n } finally {\n isSyncSetupRequired = false;\n }\n}\n\nfunction listenerImpl(applied: InternalListenerOptions, ...args: any[]): ListenerRef {\n const options = args[3] as ListenerOptions | undefined;\n\n const { runInContext } = setupContext(options?.injector, listenerImpl);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return NOOP_EFFECT_REF;\n }\n\n const [maybeReactiveTarget, maybeReactiveEvent, rawHandler] = args;\n const { stop, prevent, self, ...nativeOptions } = applied;\n const hasModifiers = stop || prevent || self;\n\n const handler = hasModifiers\n ? function (this: any, event: Event) {\n if (self && event.target !== event.currentTarget) return;\n if (prevent) event.preventDefault();\n if (stop) event.stopPropagation();\n rawHandler.call(this, event);\n }\n : rawHandler;\n\n const setupListener = (onCleanup: EffectCleanupRegisterFn) => {\n const target = toElement(maybeReactiveTarget);\n const event = toValue(maybeReactiveEvent);\n\n if (!target) {\n return;\n }\n\n target.addEventListener(event, handler, nativeOptions);\n\n onCleanup(() => {\n target.removeEventListener(event, handler, nativeOptions);\n });\n };\n\n let effectRef: EffectRef;\n\n if (isSyncSetupRequired) {\n effectRef = syncEffect(setupListener);\n } else {\n effectRef = afterRenderEffect({ read: setupListener });\n }\n\n return { destroy: () => effectRef.destroy() };\n });\n}\n\nconst MODIFIERS = new Set<keyof InternalListenerOptions>([\n 'capture',\n 'passive',\n 'once',\n 'stop',\n 'prevent',\n 'self',\n]);\n\nfunction createModifier(applied: InternalListenerOptions): ListenerFunction {\n const modifierFn = ((...args: any[]) => {\n return listenerImpl(applied, ...args);\n }) as ListenerFunction;\n\n return new Proxy(modifierFn, {\n get(target, prop) {\n if (typeof prop !== 'string' || !MODIFIERS.has(prop as any)) {\n return target[prop as keyof typeof target];\n }\n\n if (applied[prop as keyof InternalListenerOptions]) {\n return target;\n }\n\n return createModifier({ ...applied, [prop]: true });\n },\n });\n}\n\nfunction syncEffect(\n effectFn: (onCleanup: EffectCleanupRegisterFn) => void,\n options?: CreateEffectOptions\n): EffectRef {\n const effectRef = effect(effectFn, options);\n const effectNode: BaseEffectNode = (effectRef as any)[SIGNAL];\n try {\n effectNode.run();\n } catch (error) {\n if (ngDevMode) {\n console.warn('[syncEffect] Failed to run effectFn synchronously', error);\n }\n }\n return effectRef;\n}\n\ninterface InternalListenerOptions {\n readonly capture?: boolean;\n readonly passive?: boolean;\n readonly once?: boolean;\n readonly stop?: boolean;\n readonly prevent?: boolean;\n readonly self?: boolean;\n}\n\ninterface InferEventTarget<Events> {\n readonly addEventListener: (event: Events, fn?: any, options?: any) => any;\n readonly removeEventListener: (event: Events, fn?: any, options?: any) => any;\n}\n\ninterface GeneralEventListener<E = Event> {\n (e: E): void;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { signal, effect } from '@angular/core';
|
|
2
|
+
import { setupContext, constSignal, toValue } from '@signality/core/internal';
|
|
3
|
+
import { listener } from '@signality/core/browser/listener';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reactive wrapper around the [Window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) method.
|
|
7
|
+
*
|
|
8
|
+
* @param query - CSS media query string (can be a signal)
|
|
9
|
+
* @param options - Optional configuration
|
|
10
|
+
* @returns A signal that is true when the media query matches
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* @Component({
|
|
15
|
+
* template: `
|
|
16
|
+
* @if (prefersDark()) {
|
|
17
|
+
* <p>Dark mode is preferred</p>
|
|
18
|
+
* } @else {
|
|
19
|
+
* <p>Light mode is preferred</p>
|
|
20
|
+
* }
|
|
21
|
+
* `
|
|
22
|
+
* })
|
|
23
|
+
* class ThemeComponent {
|
|
24
|
+
* readonly prefersDark = mediaQuery('(prefers-color-scheme: dark)');
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function mediaQuery(query, options) {
|
|
29
|
+
const { runInContext } = setupContext(options?.injector, mediaQuery);
|
|
30
|
+
return runInContext(({ isServer, injector }) => {
|
|
31
|
+
if (isServer) {
|
|
32
|
+
return constSignal(!!options?.initialValue);
|
|
33
|
+
}
|
|
34
|
+
const matches = signal(!!options?.initialValue, options);
|
|
35
|
+
effect(onCleanup => {
|
|
36
|
+
const queryString = toValue(query);
|
|
37
|
+
if (!queryString) {
|
|
38
|
+
matches.set(false);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const mediaQueryList = window.matchMedia(queryString);
|
|
42
|
+
matches.set(mediaQueryList.matches);
|
|
43
|
+
const changeListener = listener(mediaQueryList, 'change', (e) => matches.set(e.matches), { injector });
|
|
44
|
+
onCleanup(changeListener.destroy);
|
|
45
|
+
});
|
|
46
|
+
return matches.asReadonly();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generated bundle index. Do not edit.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export { mediaQuery };
|
|
55
|
+
//# sourceMappingURL=signality-core-browser-media-query.mjs.map
|