@signality/core 0.1.0 → 0.1.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.
@@ -81,3 +81,66 @@ export interface BluetoothRef {
81
81
  * ```
82
82
  */
83
83
  export declare function bluetooth(options?: BluetoothOptions): BluetoothRef;
84
+ /**
85
+ * Local type definitions for Web Bluetooth API.
86
+ *
87
+ * @remarks
88
+ * External `@types/web-bluetooth` package may conflict with user's other libraries
89
+ * or become outdated. For better DX, we define minimal required interfaces locally
90
+ * without polluting the global namespace with experimental APIs.
91
+ */
92
+ type BluetoothServiceUUID = number | string;
93
+ interface BluetoothDataFilter {
94
+ readonly dataPrefix?: BufferSource;
95
+ readonly mask?: BufferSource;
96
+ }
97
+ interface BluetoothManufacturerDataFilter extends BluetoothDataFilter {
98
+ readonly companyIdentifier: number;
99
+ }
100
+ interface BluetoothServiceDataFilter extends BluetoothDataFilter {
101
+ readonly service: BluetoothServiceUUID;
102
+ }
103
+ interface BluetoothLEScanFilter {
104
+ readonly name?: string;
105
+ readonly namePrefix?: string;
106
+ readonly services?: BluetoothServiceUUID[];
107
+ readonly manufacturerData?: BluetoothManufacturerDataFilter[];
108
+ readonly serviceData?: BluetoothServiceDataFilter[];
109
+ }
110
+ interface BluetoothRemoteGATTService extends EventTarget {
111
+ readonly device: BluetoothDevice;
112
+ readonly uuid: string;
113
+ readonly isPrimary: boolean;
114
+ getCharacteristic(characteristic: BluetoothCharacteristicUUID): Promise<BluetoothRemoteGATTCharacteristic>;
115
+ getCharacteristics(characteristic?: BluetoothCharacteristicUUID): Promise<BluetoothRemoteGATTCharacteristic[]>;
116
+ }
117
+ type BluetoothCharacteristicUUID = number | string;
118
+ interface BluetoothRemoteGATTCharacteristic extends EventTarget {
119
+ readonly service: BluetoothRemoteGATTService;
120
+ readonly uuid: string;
121
+ readonly value?: DataView;
122
+ readValue(): Promise<DataView>;
123
+ writeValue(value: BufferSource): Promise<void>;
124
+ startNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;
125
+ stopNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;
126
+ }
127
+ interface BluetoothRemoteGATTServer {
128
+ readonly device: BluetoothDevice;
129
+ readonly connected: boolean;
130
+ connect(): Promise<BluetoothRemoteGATTServer>;
131
+ disconnect(): void;
132
+ getPrimaryService(service: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService>;
133
+ getPrimaryServices(service?: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService[]>;
134
+ }
135
+ interface BluetoothDevice extends EventTarget {
136
+ readonly id: string;
137
+ readonly name?: string;
138
+ readonly gatt?: BluetoothRemoteGATTServer;
139
+ forget(): Promise<void>;
140
+ watchAdvertisements(options?: {
141
+ signal?: AbortSignal;
142
+ }): Promise<void>;
143
+ readonly watchingAdvertisements: boolean;
144
+ addEventListener(type: 'gattserverdisconnected', listener: (ev: Event) => void): void;
145
+ }
146
+ export {};
@@ -24,7 +24,10 @@ export interface FileDialogOptions extends WithInjector {
24
24
  readonly capture?: MaybeSignal<string>;
25
25
  /**
26
26
  * Whether to select directories instead of files.
27
- * Uses the non-standard `webkitdirectory` attribute.
27
+ *
28
+ * Uses the `webkitdirectory` attribute, widely supported across all modern browsers.
29
+ *
30
+ * @see [webkitdirectory on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)
28
31
  * @default false
29
32
  */
30
33
  readonly directory?: MaybeSignal<boolean>;
@@ -17,6 +17,10 @@ export interface PictureInPictureRef {
17
17
  /**
18
18
  * Enter Picture-in-Picture mode for the target video element.
19
19
  *
20
+ * @throws {DOMException} `'NotAllowedError'` — the document is not allowed to use PiP
21
+ * @throws {DOMException} `'InvalidStateError'` — the video element has `disablePictureInPicture` attribute
22
+ * @throws {DOMException} `'NotSupportedError'` — Picture-in-Picture is not supported
23
+ *
20
24
  * @see [HTMLVideoElement: requestPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture)
21
25
  */
22
26
  readonly enter: () => Promise<void>;
@@ -113,3 +113,17 @@ export interface SpeechRecognitionRef {
113
113
  * ```
114
114
  */
115
115
  export declare function speechRecognition(options?: SpeechRecognitionOptions): SpeechRecognitionRef;
116
+ /**
117
+ * Local type definitions for Web Speech API (Speech Recognition).
118
+ *
119
+ * @remarks
120
+ * External `@types/dom-speech-recognition` package may conflict with user's other libraries
121
+ * or become outdated. For better DX, we define minimal required interfaces locally
122
+ * without polluting the global namespace with experimental APIs.
123
+ */
124
+ type SpeechRecognitionErrorCode = 'aborted' | 'audio-capture' | 'bad-grammar' | 'language-not-supported' | 'network' | 'no-speech' | 'not-allowed' | 'service-not-allowed';
125
+ interface SpeechRecognitionErrorEvent extends Event {
126
+ readonly error: SpeechRecognitionErrorCode;
127
+ readonly message: string;
128
+ }
129
+ export {};
@@ -54,7 +54,7 @@ export interface StorageOptions<T> extends CreateSignalOptions<T>, WithInjector
54
54
  *
55
55
  * // Or with custom merge
56
56
  * const settings = storage('settings', defaultSettings, {
57
- * mergeResolver: (stored, initial) => deepMerge(stored, initial),
57
+ * mergeResolver: (stored, initial) => deepMerge(initial, stored),
58
58
  * });
59
59
  * ```
60
60
  */
@@ -1,4 +1,4 @@
1
- import { type CreateSignalOptions, type Signal } from '@angular/core';
1
+ import { type CreateSignalOptions, type WritableSignal } from '@angular/core';
2
2
  import type { MaybeElementSignal, WithInjector } from '@signality/core/types';
3
3
  export interface ElementFocusOptions extends CreateSignalOptions<boolean>, WithInjector {
4
4
  /**
@@ -10,23 +10,27 @@ export interface ElementFocusOptions extends CreateSignalOptions<boolean>, WithI
10
10
  * @default false
11
11
  */
12
12
  readonly focusVisible?: boolean;
13
+ /**
14
+ * Prevent scrolling to the element when it is focused.
15
+ * @default false
16
+ */
17
+ readonly preventScroll?: boolean;
13
18
  }
14
19
  /**
15
20
  * Reactive tracking of focus state on an element.
16
- * Detects when an element gains or loses focus.
21
+ * Detects when an element gains or loses focus, and allows programmatically setting focus.
17
22
  *
18
23
  * @param target - The element to track focus state on
19
- * @param options - Optional configuration including focusVisible mode and injector
20
- * @returns A signal that is `true` when the element has focus
24
+ * @param options - Optional configuration including focusVisible, preventScroll and injector
25
+ * @returns A writable signal that is `true` when the element has focus
21
26
  *
22
27
  * @example
23
28
  * ```typescript
24
29
  * @Component({
25
30
  * template: `
26
31
  * <input #input [class.focused]="isFocused()" />
27
- * @if (isFocused()) {
28
- * <p>Input is focused</p>
29
- * }
32
+ * <button (click)="isFocused.set(true)">Focus Input</button>
33
+ * <button (click)="isFocused.set(false)">Blur Input</button>
30
34
  * `
31
35
  * })
32
36
  * export class FocusDemo {
@@ -35,4 +39,4 @@ export interface ElementFocusOptions extends CreateSignalOptions<boolean>, WithI
35
39
  * }
36
40
  * ```
37
41
  */
38
- export declare function elementFocus(target: MaybeElementSignal<HTMLElement>, options?: ElementFocusOptions): Signal<boolean>;
42
+ export declare function elementFocus(target: MaybeElementSignal<HTMLElement>, options?: ElementFocusOptions): WritableSignal<boolean>;
@@ -74,7 +74,8 @@ function bluetooth(options) {
74
74
  isConnecting.set(true);
75
75
  error.set(null);
76
76
  try {
77
- const btDevice = await navigator.bluetooth.requestDevice(requestOptions);
77
+ const bt = navigator.bluetooth;
78
+ const btDevice = await bt.requestDevice(requestOptions);
78
79
  device.set(btDevice);
79
80
  disconnectListener = setupSync(() => listener(btDevice, 'gattserverdisconnected', disconnect, { injector }));
80
81
  const gattServer = await btDevice.gatt?.connect();
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-bluetooth.mjs","sources":["../../../projects/core/browser/bluetooth/index.ts","../../../projects/core/browser/bluetooth/signality-core-browser-bluetooth.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_ASYNC_FN, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { listener, ListenerRef, setupSync } from '@signality/core/browser/listener';\n\nexport interface BluetoothOptions extends WithInjector {\n /**\n * Accept any Bluetooth device without filtering.\n *\n * @default true\n * @see [requestDevice: acceptAllDevices on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#acceptalldevices)\n */\n readonly acceptAllDevices?: boolean;\n\n /**\n * Filters for device selection. Mutually exclusive with `acceptAllDevices`.\n *\n * @default undefined\n * @see [requestDevice: filters on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#filters)\n */\n readonly filters?: BluetoothLEScanFilter[];\n\n /**\n * Optional GATT services to access on the connected device.\n *\n * @default []\n * @see [requestDevice: optionalServices on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices)\n */\n readonly optionalServices?: BluetoothServiceUUID[];\n}\n\nexport interface BluetoothRef {\n /**\n * Whether Web Bluetooth API is supported in the current browser.\n *\n * @see [Web Bluetooth API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether a device is currently connected.\n */\n readonly isConnected: Signal<boolean>;\n\n /**\n * Whether a connection is in progress.\n */\n readonly isConnecting: Signal<boolean>;\n\n /**\n * Connected Bluetooth device.\n */\n readonly device: Signal<BluetoothDevice | null>;\n\n /**\n * GATT server of a connected device.\n */\n readonly server: Signal<BluetoothRemoteGATTServer | null>;\n\n /**\n * The last error that occurred.\n */\n readonly error: Signal<Error | null>;\n\n /**\n * Request device connection.\n */\n readonly request: () => Promise<void>;\n\n /**\n * Disconnect from a device.\n */\n readonly disconnect: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth).\n *\n * @param options - Optional configuration\n * @returns A BluetoothRef with connection state and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"bt.request()\">Connect</button>\n * @if (bt.isConnected()) {\n * <p>{{ bt.device()?.name }}</p>\n * }\n * `\n * })\n * export class BluetoothDemo {\n * readonly bt = bluetooth();\n * }\n * ```\n */\nexport function bluetooth(options?: BluetoothOptions): BluetoothRef {\n const { runInContext } = setupContext(options?.injector, bluetooth);\n\n return runInContext(({ isBrowser, injector, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'bluetooth' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isConnected: constSignal(false),\n isConnecting: constSignal(false),\n device: constSignal(null),\n server: constSignal(null),\n error: constSignal(null),\n request: NOOP_ASYNC_FN,\n disconnect: NOOP_FN,\n };\n }\n\n const requestOptions = {\n ...(options?.filters?.length\n ? { filters: options.filters }\n : { acceptAllDevices: options?.acceptAllDevices ?? true }),\n optionalServices: options?.optionalServices ?? [],\n };\n\n const isConnected = signal(false);\n const isConnecting = signal(false);\n const device = signal<BluetoothDevice | null>(null);\n const server = signal<BluetoothRemoteGATTServer | null>(null);\n const error = signal<Error | null>(null);\n\n let disconnectListener: ListenerRef | null = null;\n\n const disconnect = () => {\n if (disconnectListener) {\n disconnectListener?.destroy();\n disconnectListener = null;\n }\n\n const activeDevice = untracked(device);\n\n if (activeDevice?.gatt?.connected) {\n activeDevice.gatt.disconnect();\n }\n\n device.set(null);\n server.set(null);\n isConnected.set(false);\n };\n\n const request = async (): Promise<void> => {\n if (untracked(isConnecting)) {\n return;\n }\n\n if (untracked(isConnected)) {\n disconnect();\n }\n\n isConnecting.set(true);\n error.set(null);\n\n try {\n const btDevice = await navigator.bluetooth.requestDevice(requestOptions);\n\n device.set(btDevice);\n\n disconnectListener = setupSync(() =>\n listener(btDevice, 'gattserverdisconnected', disconnect, { injector })\n );\n\n const gattServer = await btDevice.gatt?.connect();\n\n if (gattServer) {\n server.set(gattServer);\n isConnected.set(true);\n }\n } catch (e) {\n error.set(e as Error);\n disconnect();\n } finally {\n isConnecting.set(false);\n }\n };\n\n onCleanup(disconnect);\n\n return {\n isSupported,\n isConnected: isConnected.asReadonly(),\n isConnecting: isConnecting.asReadonly(),\n device: device.asReadonly(),\n server: server.asReadonly(),\n error: error.asReadonly(),\n request,\n disconnect,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AA2EA;;;;;;;;;;;;;;;;;;;;AAoBG;AACG,SAAU,SAAS,CAAC,OAA0B,EAAA;AAClD,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;IAEnE,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAI;QACzD,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,WAAW,IAAI,SAAS,CAAC;AAEtE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC;AAC/B,gBAAA,YAAY,EAAE,WAAW,CAAC,KAAK,CAAC;AAChC,gBAAA,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC;AACzB,gBAAA,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC;AACzB,gBAAA,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC;AACxB,gBAAA,OAAO,EAAE,aAAa;AACtB,gBAAA,UAAU,EAAE,OAAO;aACpB;QACH;AAEA,QAAA,MAAM,cAAc,GAAG;AACrB,YAAA,IAAI,OAAO,EAAE,OAAO,EAAE;AACpB,kBAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO;kBAC1B,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,IAAI,IAAI,EAAE,CAAC;AAC5D,YAAA,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,IAAI,EAAE;SAClD;AAED,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,uDAAC;AACjC,QAAA,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,wDAAC;AAClC,QAAA,MAAM,MAAM,GAAG,MAAM,CAAyB,IAAI,kDAAC;AACnD,QAAA,MAAM,MAAM,GAAG,MAAM,CAAmC,IAAI,kDAAC;AAC7D,QAAA,MAAM,KAAK,GAAG,MAAM,CAAe,IAAI,iDAAC;QAExC,IAAI,kBAAkB,GAAuB,IAAI;QAEjD,MAAM,UAAU,GAAG,MAAK;YACtB,IAAI,kBAAkB,EAAE;gBACtB,kBAAkB,EAAE,OAAO,EAAE;gBAC7B,kBAAkB,GAAG,IAAI;YAC3B;AAEA,YAAA,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC;AAEtC,YAAA,IAAI,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;AACjC,gBAAA,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE;YAChC;AAEA,YAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;AAChB,YAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;AAChB,YAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;AACxB,QAAA,CAAC;AAED,QAAA,MAAM,OAAO,GAAG,YAA0B;AACxC,YAAA,IAAI,SAAS,CAAC,YAAY,CAAC,EAAE;gBAC3B;YACF;AAEA,YAAA,IAAI,SAAS,CAAC,WAAW,CAAC,EAAE;AAC1B,gBAAA,UAAU,EAAE;YACd;AAEA,YAAA,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;AACtB,YAAA,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;AAEf,YAAA,IAAI;gBACF,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC;AAExE,gBAAA,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;AAEpB,gBAAA,kBAAkB,GAAG,SAAS,CAAC,MAC7B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,CACvE;gBAED,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE;gBAEjD,IAAI,UAAU,EAAE;AACd,oBAAA,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;AACtB,oBAAA,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;gBACvB;YACF;YAAE,OAAO,CAAC,EAAE;AACV,gBAAA,KAAK,CAAC,GAAG,CAAC,CAAU,CAAC;AACrB,gBAAA,UAAU,EAAE;YACd;oBAAU;AACR,gBAAA,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;YACzB;AACF,QAAA,CAAC;QAED,SAAS,CAAC,UAAU,CAAC;QAErB,OAAO;YACL,WAAW;AACX,YAAA,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE;AACrC,YAAA,YAAY,EAAE,YAAY,CAAC,UAAU,EAAE;AACvC,YAAA,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;AAC3B,YAAA,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;AAC3B,YAAA,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE;YACzB,OAAO;YACP,UAAU;SACX;AACH,IAAA,CAAC,CAAC;AACJ;;ACnMA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-browser-bluetooth.mjs","sources":["../../../projects/core/browser/bluetooth/index.ts","../../../projects/core/browser/bluetooth/signality-core-browser-bluetooth.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_ASYNC_FN, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { listener, ListenerRef, setupSync } from '@signality/core/browser/listener';\n\nexport interface BluetoothOptions extends WithInjector {\n /**\n * Accept any Bluetooth device without filtering.\n *\n * @default true\n * @see [requestDevice: acceptAllDevices on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#acceptalldevices)\n */\n readonly acceptAllDevices?: boolean;\n\n /**\n * Filters for device selection. Mutually exclusive with `acceptAllDevices`.\n *\n * @default undefined\n * @see [requestDevice: filters on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#filters)\n */\n readonly filters?: BluetoothLEScanFilter[];\n\n /**\n * Optional GATT services to access on the connected device.\n *\n * @default []\n * @see [requestDevice: optionalServices on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices)\n */\n readonly optionalServices?: BluetoothServiceUUID[];\n}\n\nexport interface BluetoothRef {\n /**\n * Whether Web Bluetooth API is supported in the current browser.\n *\n * @see [Web Bluetooth API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether a device is currently connected.\n */\n readonly isConnected: Signal<boolean>;\n\n /**\n * Whether a connection is in progress.\n */\n readonly isConnecting: Signal<boolean>;\n\n /**\n * Connected Bluetooth device.\n */\n readonly device: Signal<BluetoothDevice | null>;\n\n /**\n * GATT server of a connected device.\n */\n readonly server: Signal<BluetoothRemoteGATTServer | null>;\n\n /**\n * The last error that occurred.\n */\n readonly error: Signal<Error | null>;\n\n /**\n * Request device connection.\n */\n readonly request: () => Promise<void>;\n\n /**\n * Disconnect from a device.\n */\n readonly disconnect: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth).\n *\n * @param options - Optional configuration\n * @returns A BluetoothRef with connection state and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"bt.request()\">Connect</button>\n * @if (bt.isConnected()) {\n * <p>{{ bt.device()?.name }}</p>\n * }\n * `\n * })\n * export class BluetoothDemo {\n * readonly bt = bluetooth();\n * }\n * ```\n */\nexport function bluetooth(options?: BluetoothOptions): BluetoothRef {\n const { runInContext } = setupContext(options?.injector, bluetooth);\n\n return runInContext(({ isBrowser, injector, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'bluetooth' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isConnected: constSignal(false),\n isConnecting: constSignal(false),\n device: constSignal(null),\n server: constSignal(null),\n error: constSignal(null),\n request: NOOP_ASYNC_FN,\n disconnect: NOOP_FN,\n };\n }\n\n const requestOptions = {\n ...(options?.filters?.length\n ? { filters: options.filters }\n : { acceptAllDevices: options?.acceptAllDevices ?? true }),\n optionalServices: options?.optionalServices ?? [],\n };\n\n const isConnected = signal(false);\n const isConnecting = signal(false);\n const device = signal<BluetoothDevice | null>(null);\n const server = signal<BluetoothRemoteGATTServer | null>(null);\n const error = signal<Error | null>(null);\n\n let disconnectListener: ListenerRef | null = null;\n\n const disconnect = () => {\n if (disconnectListener) {\n disconnectListener?.destroy();\n disconnectListener = null;\n }\n\n const activeDevice = untracked(device);\n\n if (activeDevice?.gatt?.connected) {\n activeDevice.gatt.disconnect();\n }\n\n device.set(null);\n server.set(null);\n isConnected.set(false);\n };\n\n const request = async (): Promise<void> => {\n if (untracked(isConnecting)) {\n return;\n }\n\n if (untracked(isConnected)) {\n disconnect();\n }\n\n isConnecting.set(true);\n error.set(null);\n\n try {\n const bt: Bluetooth = (navigator as any).bluetooth;\n const btDevice = await bt.requestDevice(requestOptions);\n\n device.set(btDevice);\n\n disconnectListener = setupSync(() =>\n listener(btDevice, 'gattserverdisconnected', disconnect, { injector })\n );\n\n const gattServer = await btDevice.gatt?.connect();\n\n if (gattServer) {\n server.set(gattServer);\n isConnected.set(true);\n }\n } catch (e) {\n error.set(e as Error);\n disconnect();\n } finally {\n isConnecting.set(false);\n }\n };\n\n onCleanup(disconnect);\n\n return {\n isSupported,\n isConnected: isConnected.asReadonly(),\n isConnecting: isConnecting.asReadonly(),\n device: device.asReadonly(),\n server: server.asReadonly(),\n error: error.asReadonly(),\n request,\n disconnect,\n };\n });\n}\n\n/**\n * Local type definitions for Web Bluetooth API.\n *\n * @remarks\n * External `@types/web-bluetooth` package may conflict with user's other libraries\n * or become outdated. For better DX, we define minimal required interfaces locally\n * without polluting the global namespace with experimental APIs.\n */\ntype BluetoothServiceUUID = number | string;\n\ninterface BluetoothDataFilter {\n readonly dataPrefix?: BufferSource;\n readonly mask?: BufferSource;\n}\n\ninterface BluetoothManufacturerDataFilter extends BluetoothDataFilter {\n readonly companyIdentifier: number;\n}\n\ninterface BluetoothServiceDataFilter extends BluetoothDataFilter {\n readonly service: BluetoothServiceUUID;\n}\n\ninterface BluetoothLEScanFilter {\n readonly name?: string;\n readonly namePrefix?: string;\n readonly services?: BluetoothServiceUUID[];\n readonly manufacturerData?: BluetoothManufacturerDataFilter[];\n readonly serviceData?: BluetoothServiceDataFilter[];\n}\n\ninterface BluetoothRemoteGATTService extends EventTarget {\n readonly device: BluetoothDevice;\n readonly uuid: string;\n readonly isPrimary: boolean;\n getCharacteristic(\n characteristic: BluetoothCharacteristicUUID\n ): Promise<BluetoothRemoteGATTCharacteristic>;\n getCharacteristics(\n characteristic?: BluetoothCharacteristicUUID\n ): Promise<BluetoothRemoteGATTCharacteristic[]>;\n}\n\ntype BluetoothCharacteristicUUID = number | string;\n\ninterface BluetoothRemoteGATTCharacteristic extends EventTarget {\n readonly service: BluetoothRemoteGATTService;\n readonly uuid: string;\n readonly value?: DataView;\n readValue(): Promise<DataView>;\n writeValue(value: BufferSource): Promise<void>;\n startNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;\n stopNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;\n}\n\ninterface BluetoothRemoteGATTServer {\n readonly device: BluetoothDevice;\n readonly connected: boolean;\n connect(): Promise<BluetoothRemoteGATTServer>;\n disconnect(): void;\n getPrimaryService(service: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService>;\n getPrimaryServices(service?: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService[]>;\n}\n\ninterface BluetoothDevice extends EventTarget {\n readonly id: string;\n readonly name?: string;\n readonly gatt?: BluetoothRemoteGATTServer;\n forget(): Promise<void>;\n watchAdvertisements(options?: { signal?: AbortSignal }): Promise<void>;\n readonly watchingAdvertisements: boolean;\n addEventListener(type: 'gattserverdisconnected', listener: (ev: Event) => void): void;\n}\n\ninterface Bluetooth {\n requestDevice(options?: {\n filters?: BluetoothLEScanFilter[];\n acceptAllDevices?: boolean;\n optionalServices?: BluetoothServiceUUID[];\n }): Promise<BluetoothDevice>;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -42,7 +42,7 @@ function favicon(options) {
42
42
  const appBaseHref = inject(APP_BASE_HREF, { optional: true });
43
43
  const baseUrl = options?.baseUrl ?? appBaseHref ?? '';
44
44
  const getLinkElement = () => {
45
- let link = document.querySelector('link[rel*="icon"]');
45
+ let link = document.querySelector('link[rel*="icon"]:not([rel*="apple-touch-icon"])');
46
46
  if (!link) {
47
47
  link = document.createElement('link');
48
48
  link.rel = 'icon';
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-favicon.mjs","sources":["../../../projects/core/browser/favicon/index.ts","../../../projects/core/browser/favicon/signality-core-browser-favicon.ts"],"sourcesContent":["import { inject, type Signal, signal, untracked } from '@angular/core';\nimport { APP_BASE_HREF } from '@angular/common';\nimport { constSignal, createToken, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface FaviconOptions extends WithInjector {\n /**\n * Base URL prepended to all favicon paths passed to `set()`.\n *\n * Resolution priority:\n * 1. Explicit `baseUrl` value\n * 2. [`APP_BASE_HREF`](https://angular.dev/api/common/APP_BASE_HREF) token value (if configured)\n * 3. Empty string `''`\n */\n readonly baseUrl?: string;\n}\n\nexport interface FaviconRef {\n /**\n * URL of the currently active favicon.\n */\n readonly current: Signal<string>;\n\n /**\n * URL of the favicon at the time the utility was initialized.\n */\n readonly original: Signal<string>;\n\n /**\n * Set the favicon to the given URL.\n *\n * @see [HTMLLinkElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement)\n */\n readonly set: (url: string) => void;\n\n /**\n * Render an emoji onto a canvas and use it as the favicon.\n *\n * @see [Canvas API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)\n */\n readonly setEmoji: (emoji: string) => void;\n\n /**\n * Reset the favicon to the original URL captured on initialization.\n */\n readonly reset: () => void;\n}\n\n/**\n * Reactive favicon manipulation using the [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement).\n * Dynamically change the page favicon based on application state.\n *\n * @param options - Optional configuration\n * @returns A FaviconRef with favicon control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"setNotification()\">Set Notification</button>\n * <button (click)=\"fav.reset()\">Reset Favicon</button>\n * <p>Current: {{ fav.current() }}</p>\n * `\n * })\n * export class FaviconDemo {\n * readonly fav = favicon();\n *\n * setNotification() {\n * this.fav.setEmoji('🔴');\n * }\n * }\n * ```\n */\nexport function favicon(options?: FaviconOptions): FaviconRef {\n const { runInContext } = setupContext(options?.injector, favicon);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return {\n current: constSignal(''),\n original: constSignal(''),\n set: NOOP_FN,\n setEmoji: NOOP_FN,\n reset: NOOP_FN,\n };\n }\n\n const appBaseHref = inject(APP_BASE_HREF, { optional: true });\n const baseUrl = options?.baseUrl ?? appBaseHref ?? '';\n\n const getLinkElement = (): HTMLLinkElement => {\n let link = document.querySelector<HTMLLinkElement>('link[rel*=\"icon\"]');\n\n if (!link) {\n link = document.createElement('link');\n link.rel = 'icon';\n document.head.appendChild(link);\n }\n\n return link;\n };\n\n const { href = '' } = getLinkElement();\n const current = signal(href);\n const original = signal(href);\n\n const set = (url: string) => {\n const fullUrl = baseUrl + url;\n const linkEl = getLinkElement();\n linkEl.href = fullUrl;\n current.set(fullUrl);\n };\n\n const setEmoji = (emoji: string) => {\n const canvas = document.createElement('canvas');\n canvas.width = 32;\n canvas.height = 32;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.font = '28px serif';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(emoji, 16, 18);\n\n const dataUrl = canvas.toDataURL('image/png');\n const linkEl = getLinkElement();\n linkEl.href = dataUrl;\n current.set(dataUrl);\n };\n\n const reset = () => {\n const linkEl = getLinkElement();\n const originalHref = untracked(original);\n linkEl.href = originalHref;\n current.set(originalHref);\n };\n\n return {\n current: current.asReadonly(),\n original: original.asReadonly(),\n set,\n setEmoji,\n reset,\n };\n });\n}\n\nexport const FAVICON = /* @__PURE__ */ createToken(favicon);\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signality-core-browser-favicon.mjs","sources":["../../../projects/core/browser/favicon/index.ts","../../../projects/core/browser/favicon/signality-core-browser-favicon.ts"],"sourcesContent":["import { inject, type Signal, signal, untracked } from '@angular/core';\nimport { APP_BASE_HREF } from '@angular/common';\nimport { constSignal, createToken, NOOP_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface FaviconOptions extends WithInjector {\n /**\n * Base URL prepended to all favicon paths passed to `set()`.\n *\n * Resolution priority:\n * 1. Explicit `baseUrl` value\n * 2. [`APP_BASE_HREF`](https://angular.dev/api/common/APP_BASE_HREF) token value (if configured)\n * 3. Empty string `''`\n */\n readonly baseUrl?: string;\n}\n\nexport interface FaviconRef {\n /**\n * URL of the currently active favicon.\n */\n readonly current: Signal<string>;\n\n /**\n * URL of the favicon at the time the utility was initialized.\n */\n readonly original: Signal<string>;\n\n /**\n * Set the favicon to the given URL.\n *\n * @see [HTMLLinkElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement)\n */\n readonly set: (url: string) => void;\n\n /**\n * Render an emoji onto a canvas and use it as the favicon.\n *\n * @see [Canvas API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)\n */\n readonly setEmoji: (emoji: string) => void;\n\n /**\n * Reset the favicon to the original URL captured on initialization.\n */\n readonly reset: () => void;\n}\n\n/**\n * Reactive favicon manipulation using the [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement).\n * Dynamically change the page favicon based on application state.\n *\n * @param options - Optional configuration\n * @returns A FaviconRef with favicon control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"setNotification()\">Set Notification</button>\n * <button (click)=\"fav.reset()\">Reset Favicon</button>\n * <p>Current: {{ fav.current() }}</p>\n * `\n * })\n * export class FaviconDemo {\n * readonly fav = favicon();\n *\n * setNotification() {\n * this.fav.setEmoji('🔴');\n * }\n * }\n * ```\n */\nexport function favicon(options?: FaviconOptions): FaviconRef {\n const { runInContext } = setupContext(options?.injector, favicon);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return {\n current: constSignal(''),\n original: constSignal(''),\n set: NOOP_FN,\n setEmoji: NOOP_FN,\n reset: NOOP_FN,\n };\n }\n\n const appBaseHref = inject(APP_BASE_HREF, { optional: true });\n const baseUrl = options?.baseUrl ?? appBaseHref ?? '';\n\n const getLinkElement = (): HTMLLinkElement => {\n let link = document.querySelector<HTMLLinkElement>(\n 'link[rel*=\"icon\"]:not([rel*=\"apple-touch-icon\"])'\n );\n\n if (!link) {\n link = document.createElement('link');\n link.rel = 'icon';\n document.head.appendChild(link);\n }\n\n return link;\n };\n\n const { href = '' } = getLinkElement();\n const current = signal(href);\n const original = signal(href);\n\n const set = (url: string) => {\n const fullUrl = baseUrl + url;\n const linkEl = getLinkElement();\n linkEl.href = fullUrl;\n current.set(fullUrl);\n };\n\n const setEmoji = (emoji: string) => {\n const canvas = document.createElement('canvas');\n canvas.width = 32;\n canvas.height = 32;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.font = '28px serif';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(emoji, 16, 18);\n\n const dataUrl = canvas.toDataURL('image/png');\n const linkEl = getLinkElement();\n linkEl.href = dataUrl;\n current.set(dataUrl);\n };\n\n const reset = () => {\n const linkEl = getLinkElement();\n const originalHref = untracked(original);\n linkEl.href = originalHref;\n current.set(originalHref);\n };\n\n return {\n current: current.asReadonly(),\n original: original.asReadonly(),\n set,\n setEmoji,\n reset,\n };\n });\n}\n\nexport const FAVICON = /* @__PURE__ */ createToken(favicon);\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -90,7 +90,7 @@ function fileDialog(options) {
90
90
  inputEl.click();
91
91
  });
92
92
  };
93
- const filters = [accept, multiple, validatorFn].filter(isSignal);
93
+ const filters = [accept, multiple].filter(isSignal);
94
94
  if (filters.length) {
95
95
  watcher(filters, () => processFiles(files()));
96
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-file-dialog.mjs","sources":["../../../projects/core/browser/file-dialog/index.ts","../../../projects/core/browser/file-dialog/signality-core-browser-file-dialog.ts"],"sourcesContent":["import { isSignal, type Signal, signal, untracked, type WritableSignal } from '@angular/core';\nimport { isAcceptedFile, NOOP_FN, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport interface FileDialogOptions extends WithInjector {\n /**\n * Whether to allow selecting multiple files.\n * When changed reactively, the current file list is re-filtered.\n * @default true\n */\n readonly multiple?: MaybeSignal<boolean>;\n\n /**\n * Comma-separated list of accepted file types, matching the native HTML\n * [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept) attribute format.\n * Supports MIME types (`'image/png'`), wildcards (`'image/*'`), and file extensions (`'.pdf'`).\n * When changed reactively, the current file list is re-filtered.\n *\n * @default '*'\n * @see [accept attribute on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept)\n */\n readonly accept?: MaybeSignal<string>;\n\n /**\n * Capture source for mobile devices: `'user'` (front camera) or `'environment'` (rear camera).\n * @see [capture attribute on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture)\n */\n readonly capture?: MaybeSignal<string>;\n\n /**\n * Whether to select directories instead of files.\n * Uses the non-standard `webkitdirectory` attribute.\n * @default false\n */\n readonly directory?: MaybeSignal<boolean>;\n\n /**\n * Custom validation predicate called for each selected file.\n * Return `true` to keep the file, `false` to reject it.\n *\n * When provided, the `accept` option is ignored — the validator\n * takes full responsibility for deciding which files are valid.\n *\n * @example\n * ```typescript\n * fileDialog({\n * validator: (file) => file.size <= 5 * 1024 * 1024, // max 5 MB\n * });\n * ```\n */\n readonly validator?: (file: File) => boolean;\n\n /**\n * Callback invoked with files that were rejected during selection.\n * Useful for showing toast notifications or validation errors.\n *\n * @example\n * ```typescript\n * fileDialog({\n * accept: 'image/*',\n * onReject: (rejected) => {\n * rejected.forEach(f => toast.error(`${f.name} is not valid`));\n * },\n * });\n * ```\n */\n readonly onReject?: (files: File[]) => void;\n}\n\nexport interface FileDialogRef {\n /**\n * List of files selected via the file dialog.\n * A `WritableSignal` — can be reset externally (e.g. `files.set([])`).\n * Re-filtered automatically when `accept` or `multiple` options change reactively.\n *\n * @see [File API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)\n */\n readonly files: WritableSignal<File[]>;\n\n /**\n * Open the native file picker dialog.\n */\n readonly open: () => void;\n}\n\n/**\n * Signal-based utility for programmatically opening the native file picker dialog.\n *\n * @param options - Optional configuration\n * @returns A {@link FileDialogRef} with `files` signal and `open` method\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"fd.open()\">Select Files</button>\n * @for (file of fd.files(); track file.name) {\n * <p>{{ file.name }} ({{ file.size }} bytes)</p>\n * }\n * `\n * })\n * export class FileUpload {\n * readonly fd = fileDialog({ accept: 'image/*' });\n * }\n * ```\n */\nexport function fileDialog(options?: FileDialogOptions): FileDialogRef {\n const { runInContext } = setupContext(options?.injector, fileDialog);\n\n return runInContext(({ isBrowser }) => {\n if (!isBrowser) {\n return {\n files: signal<File[]>([]),\n open: NOOP_FN,\n };\n }\n\n const accept = options?.accept ?? '*';\n const multiple = options?.multiple ?? true;\n const capture = options?.capture ?? '';\n const directory = options?.directory ?? false;\n const validatorFn = options?.validator;\n const onReject = options?.onReject;\n\n const files = signal<File[]>([]);\n\n let inputEl: HTMLInputElement | null = null;\n\n const processFiles = (raw: File[]) => {\n const accepted: File[] = [];\n const rejected: File[] = [];\n const acceptValue = toValue(accept);\n const multipleValue = toValue(multiple);\n\n const isAccepted = validatorFn\n ? validatorFn\n : (file: File) => isAcceptedFile(file, acceptValue);\n\n for (const file of raw) {\n if (isAccepted(file)) {\n accepted.push(file);\n if (!multipleValue) {\n break;\n }\n } else {\n rejected.push(file);\n }\n }\n\n if (onReject && rejected.length > 0) {\n onReject(rejected);\n }\n\n files.set(accepted);\n };\n\n const createInput = (): HTMLInputElement => {\n const el = document.createElement('input');\n el.type = 'file';\n el.onchange = e => {\n const fileList = (e.currentTarget as HTMLInputElement).files;\n processFiles(fileList ? Array.from(fileList) : []);\n };\n return el;\n };\n\n const open = (): void => {\n untracked(() => {\n inputEl ??= createInput();\n\n inputEl.value = '';\n inputEl.multiple = toValue(multiple);\n inputEl.accept = toValue(accept);\n\n const directoriesOnly = toValue(directory);\n if (directoriesOnly) {\n inputEl.webkitdirectory = true;\n }\n\n const captureValue = toValue(capture);\n if (captureValue) {\n inputEl.capture = captureValue;\n }\n\n inputEl.click();\n });\n };\n\n const filters = [accept, multiple, validatorFn].filter(isSignal) as Signal<any>[];\n\n if (filters.length) {\n watcher(filters, () => processFiles(files()));\n }\n\n return {\n files,\n open,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAsFA;;;;;;;;;;;;;;;;;;;;AAoBG;AACG,SAAU,UAAU,CAAC,OAA2B,EAAA;AACpD,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC;AAEpE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAI;QACpC,IAAI,CAAC,SAAS,EAAE;YACd,OAAO;AACL,gBAAA,KAAK,EAAE,MAAM,CAAS,EAAE,CAAC;AACzB,gBAAA,IAAI,EAAE,OAAO;aACd;QACH;AAEA,QAAA,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,GAAG;AACrC,QAAA,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,IAAI;AAC1C,QAAA,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,EAAE;AACtC,QAAA,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,KAAK;AAC7C,QAAA,MAAM,WAAW,GAAG,OAAO,EAAE,SAAS;AACtC,QAAA,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ;AAElC,QAAA,MAAM,KAAK,GAAG,MAAM,CAAS,EAAE,iDAAC;QAEhC,IAAI,OAAO,GAA4B,IAAI;AAE3C,QAAA,MAAM,YAAY,GAAG,CAAC,GAAW,KAAI;YACnC,MAAM,QAAQ,GAAW,EAAE;YAC3B,MAAM,QAAQ,GAAW,EAAE;AAC3B,YAAA,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;AACnC,YAAA,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC;YAEvC,MAAM,UAAU,GAAG;AACjB,kBAAE;AACF,kBAAE,CAAC,IAAU,KAAK,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC;AAErD,YAAA,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE;AACtB,gBAAA,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE;AACpB,oBAAA,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;oBACnB,IAAI,CAAC,aAAa,EAAE;wBAClB;oBACF;gBACF;qBAAO;AACL,oBAAA,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBACrB;YACF;YAEA,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE;gBACnC,QAAQ,CAAC,QAAQ,CAAC;YACpB;AAEA,YAAA,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;AACrB,QAAA,CAAC;QAED,MAAM,WAAW,GAAG,MAAuB;YACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC;AAC1C,YAAA,EAAE,CAAC,IAAI,GAAG,MAAM;AAChB,YAAA,EAAE,CAAC,QAAQ,GAAG,CAAC,IAAG;AAChB,gBAAA,MAAM,QAAQ,GAAI,CAAC,CAAC,aAAkC,CAAC,KAAK;AAC5D,gBAAA,YAAY,CAAC,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;AACpD,YAAA,CAAC;AACD,YAAA,OAAO,EAAE;AACX,QAAA,CAAC;QAED,MAAM,IAAI,GAAG,MAAW;YACtB,SAAS,CAAC,MAAK;gBACb,OAAO,KAAK,WAAW,EAAE;AAEzB,gBAAA,OAAO,CAAC,KAAK,GAAG,EAAE;AAClB,gBAAA,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACpC,gBAAA,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AAEhC,gBAAA,MAAM,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC;gBAC1C,IAAI,eAAe,EAAE;AACnB,oBAAA,OAAO,CAAC,eAAe,GAAG,IAAI;gBAChC;AAEA,gBAAA,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;gBACrC,IAAI,YAAY,EAAE;AAChB,oBAAA,OAAO,CAAC,OAAO,GAAG,YAAY;gBAChC;gBAEA,OAAO,CAAC,KAAK,EAAE;AACjB,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC;AAED,QAAA,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAkB;AAEjF,QAAA,IAAI,OAAO,CAAC,MAAM,EAAE;AAClB,YAAA,OAAO,CAAC,OAAO,EAAE,MAAM,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC;QAC/C;QAEA,OAAO;YACL,KAAK;YACL,IAAI;SACL;AACH,IAAA,CAAC,CAAC;AACJ;;ACxMA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-browser-file-dialog.mjs","sources":["../../../projects/core/browser/file-dialog/index.ts","../../../projects/core/browser/file-dialog/signality-core-browser-file-dialog.ts"],"sourcesContent":["import { isSignal, type Signal, signal, untracked, type WritableSignal } from '@angular/core';\nimport { isAcceptedFile, NOOP_FN, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport interface FileDialogOptions extends WithInjector {\n /**\n * Whether to allow selecting multiple files.\n * When changed reactively, the current file list is re-filtered.\n * @default true\n */\n readonly multiple?: MaybeSignal<boolean>;\n\n /**\n * Comma-separated list of accepted file types, matching the native HTML\n * [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept) attribute format.\n * Supports MIME types (`'image/png'`), wildcards (`'image/*'`), and file extensions (`'.pdf'`).\n * When changed reactively, the current file list is re-filtered.\n *\n * @default '*'\n * @see [accept attribute on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept)\n */\n readonly accept?: MaybeSignal<string>;\n\n /**\n * Capture source for mobile devices: `'user'` (front camera) or `'environment'` (rear camera).\n * @see [capture attribute on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture)\n */\n readonly capture?: MaybeSignal<string>;\n\n /**\n * Whether to select directories instead of files.\n *\n * Uses the `webkitdirectory` attribute, widely supported across all modern browsers.\n *\n * @see [webkitdirectory on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)\n * @default false\n */\n readonly directory?: MaybeSignal<boolean>;\n\n /**\n * Custom validation predicate called for each selected file.\n * Return `true` to keep the file, `false` to reject it.\n *\n * When provided, the `accept` option is ignored — the validator\n * takes full responsibility for deciding which files are valid.\n *\n * @example\n * ```typescript\n * fileDialog({\n * validator: (file) => file.size <= 5 * 1024 * 1024, // max 5 MB\n * });\n * ```\n */\n readonly validator?: (file: File) => boolean;\n\n /**\n * Callback invoked with files that were rejected during selection.\n * Useful for showing toast notifications or validation errors.\n *\n * @example\n * ```typescript\n * fileDialog({\n * accept: 'image/*',\n * onReject: (rejected) => {\n * rejected.forEach(f => toast.error(`${f.name} is not valid`));\n * },\n * });\n * ```\n */\n readonly onReject?: (files: File[]) => void;\n}\n\nexport interface FileDialogRef {\n /**\n * List of files selected via the file dialog.\n * A `WritableSignal` — can be reset externally (e.g. `files.set([])`).\n * Re-filtered automatically when `accept` or `multiple` options change reactively.\n *\n * @see [File API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)\n */\n readonly files: WritableSignal<File[]>;\n\n /**\n * Open the native file picker dialog.\n */\n readonly open: () => void;\n}\n\n/**\n * Signal-based utility for programmatically opening the native file picker dialog.\n *\n * @param options - Optional configuration\n * @returns A {@link FileDialogRef} with `files` signal and `open` method\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"fd.open()\">Select Files</button>\n * @for (file of fd.files(); track file.name) {\n * <p>{{ file.name }} ({{ file.size }} bytes)</p>\n * }\n * `\n * })\n * export class FileUpload {\n * readonly fd = fileDialog({ accept: 'image/*' });\n * }\n * ```\n */\nexport function fileDialog(options?: FileDialogOptions): FileDialogRef {\n const { runInContext } = setupContext(options?.injector, fileDialog);\n\n return runInContext(({ isBrowser }) => {\n if (!isBrowser) {\n return {\n files: signal<File[]>([]),\n open: NOOP_FN,\n };\n }\n\n const accept = options?.accept ?? '*';\n const multiple = options?.multiple ?? true;\n const capture = options?.capture ?? '';\n const directory = options?.directory ?? false;\n const validatorFn = options?.validator;\n const onReject = options?.onReject;\n\n const files = signal<File[]>([]);\n\n let inputEl: HTMLInputElement | null = null;\n\n const processFiles = (raw: File[]) => {\n const accepted: File[] = [];\n const rejected: File[] = [];\n const acceptValue = toValue(accept);\n const multipleValue = toValue(multiple);\n\n const isAccepted = validatorFn\n ? validatorFn\n : (file: File) => isAcceptedFile(file, acceptValue);\n\n for (const file of raw) {\n if (isAccepted(file)) {\n accepted.push(file);\n if (!multipleValue) {\n break;\n }\n } else {\n rejected.push(file);\n }\n }\n\n if (onReject && rejected.length > 0) {\n onReject(rejected);\n }\n\n files.set(accepted);\n };\n\n const createInput = (): HTMLInputElement => {\n const el = document.createElement('input');\n el.type = 'file';\n el.onchange = e => {\n const fileList = (e.currentTarget as HTMLInputElement).files;\n processFiles(fileList ? Array.from(fileList) : []);\n };\n return el;\n };\n\n const open = (): void => {\n untracked(() => {\n inputEl ??= createInput();\n inputEl.value = '';\n inputEl.multiple = toValue(multiple);\n inputEl.accept = toValue(accept);\n\n const directoriesOnly = toValue(directory);\n if (directoriesOnly) {\n inputEl.webkitdirectory = true;\n }\n\n const captureValue = toValue(capture);\n if (captureValue) {\n inputEl.capture = captureValue;\n }\n\n inputEl.click();\n });\n };\n\n const filters = [accept, multiple].filter(isSignal) as Signal<any>[];\n\n if (filters.length) {\n watcher(filters, () => processFiles(files()));\n }\n\n return {\n files,\n open,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { signal, untracked } from '@angular/core';
2
- import { setupContext, constSignal, NOOP_ASYNC_FN, toElement, getPipElement } from '@signality/core/internal';
2
+ import { setupContext, constSignal, NOOP_ASYNC_FN, toElement, assertElement, getPipElement } from '@signality/core/internal';
3
3
  import { listener } from '@signality/core/browser/listener';
4
4
  import { onDisconnect } from '@signality/core/elements/on-disconnect';
5
5
 
@@ -45,19 +45,13 @@ function pictureInPicture(target, options) {
45
45
  const isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
46
46
  const enter = async () => {
47
47
  const targetEl = toElement.untracked(target);
48
- try {
49
- await targetEl?.requestPictureInPicture();
50
- }
51
- catch (error) {
52
- if (ngDevMode) {
53
- console.warn(`[pictureInPicture] Failed to enter Picture-in-Picture mode.`, error);
54
- }
55
- }
48
+ assertElement(targetEl, 'pictureInPicture');
49
+ await targetEl.requestPictureInPicture();
56
50
  };
57
51
  const exit = async () => {
58
52
  const targetEl = toElement.untracked(target);
59
53
  const pipEl = getPipElement(document);
60
- if (targetEl && pipEl && targetEl === pipEl) {
54
+ if (targetEl === pipEl) {
61
55
  try {
62
56
  await document.exitPictureInPicture();
63
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-picture-in-picture.mjs","sources":["../../../projects/core/browser/picture-in-picture/index.ts","../../../projects/core/browser/picture-in-picture/signality-core-browser-picture-in-picture.ts"],"sourcesContent":["import { signal, type Signal, untracked } from '@angular/core';\nimport {\n constSignal,\n getPipElement,\n NOOP_ASYNC_FN,\n setupContext,\n toElement,\n} from '@signality/core/internal';\nimport type { MaybeElementSignal, WithInjector } from '@signality/core/types';\nimport { listener } from '@signality/core/browser/listener';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport type PictureInPictureOptions = WithInjector;\n\nexport interface PictureInPictureRef {\n /**\n * Whether the Picture-in-Picture API is supported in the current browser.\n *\n * @see [Picture-in-Picture API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the target video element is currently displayed in Picture-in-Picture mode.\n *\n * @see [Document: pictureInPictureElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/pictureInPictureElement)\n */\n readonly isActive: Signal<boolean>;\n\n /**\n * Enter Picture-in-Picture mode for the target video element.\n *\n * @see [HTMLVideoElement: requestPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture)\n */\n readonly enter: () => Promise<void>;\n\n /**\n * Exit Picture-in-Picture mode.\n *\n * @see [Document: exitPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/exitPictureInPicture)\n */\n readonly exit: () => Promise<void>;\n\n /**\n * Toggle Picture-in-Picture mode — enters if inactive, exits if active.\n */\n readonly toggle: () => Promise<void>;\n}\n\n/**\n * Signal-based wrapper around the [Picture-in-Picture API](https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API).\n *\n * Automatically exits Picture-in-Picture when the target element is disconnected from the DOM.\n *\n * @param target - Video element\n * @param options - Optional configuration\n * @returns A {@link PictureInPictureRef} with `isSupported`, `isActive` signals and `enter`/`exit`/`toggle` methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (pip.isSupported()) {\n * <video #video src=\"video.mp4\"></video>\n * <button (click)=\"pip.toggle()\">Toggle PiP</button>\n * <p>Active: {{ pip.isActive() }}</p>\n * }\n * `\n * })\n * export class PiPDemo {\n * readonly video = viewChild<HTMLVideoElement>('video');\n * readonly pip = pictureInPicture(this.video);\n * }\n * ```\n */\nexport function pictureInPicture(\n target: MaybeElementSignal<HTMLVideoElement>,\n options?: PictureInPictureOptions\n): PictureInPictureRef {\n const { runInContext } = setupContext(options?.injector, pictureInPicture);\n\n return runInContext(({ isBrowser }) => {\n const isSupported = constSignal(\n isBrowser && 'pictureInPictureEnabled' in document && document.pictureInPictureEnabled\n );\n\n if (!isSupported()) {\n return {\n isSupported,\n isActive: constSignal(false),\n enter: NOOP_ASYNC_FN,\n exit: NOOP_ASYNC_FN,\n toggle: NOOP_ASYNC_FN,\n };\n }\n\n const isActive = signal(false);\n\n const enter = async (): Promise<void> => {\n const targetEl = toElement.untracked(target);\n\n try {\n await targetEl?.requestPictureInPicture();\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[pictureInPicture] Failed to enter Picture-in-Picture mode.`, error);\n }\n }\n };\n\n const exit = async (): Promise<void> => {\n const targetEl = toElement.untracked(target);\n const pipEl = getPipElement(document);\n\n if (targetEl && pipEl && targetEl === pipEl) {\n try {\n await document.exitPictureInPicture();\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[pictureInPicture] Failed to exit Picture-in-Picture mode.`, error);\n }\n }\n }\n };\n\n const toggle = async (): Promise<void> => {\n if (untracked(isActive)) {\n await exit();\n } else {\n await enter();\n }\n };\n\n listener(target, 'enterpictureinpicture', () => isActive.set(true));\n listener(target, 'leavepictureinpicture', () => isActive.set(false));\n\n onDisconnect(target, async targetEl => {\n const pipEl = getPipElement(document);\n\n if (pipEl && targetEl === pipEl) {\n try {\n await document.exitPictureInPicture();\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[pictureInPicture] Failed to exit Picture-in-Picture mode on disconnect.`,\n error\n );\n }\n }\n isActive.set(false);\n }\n });\n\n return {\n isSupported,\n isActive,\n enter,\n exit,\n toggle,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAiDA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACG,SAAU,gBAAgB,CAC9B,MAA4C,EAC5C,OAAiC,EAAA;AAEjC,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,CAAC;AAE1E,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAI;AACpC,QAAA,MAAM,WAAW,GAAG,WAAW,CAC7B,SAAS,IAAI,yBAAyB,IAAI,QAAQ,IAAI,QAAQ,CAAC,uBAAuB,CACvF;AAED,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC;AAC5B,gBAAA,KAAK,EAAE,aAAa;AACpB,gBAAA,IAAI,EAAE,aAAa;AACnB,gBAAA,MAAM,EAAE,aAAa;aACtB;QACH;AAEA,QAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,oDAAC;AAE9B,QAAA,MAAM,KAAK,GAAG,YAA0B;YACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;AAE5C,YAAA,IAAI;AACF,gBAAA,MAAM,QAAQ,EAAE,uBAAuB,EAAE;YAC3C;YAAE,OAAO,KAAK,EAAE;gBACd,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,KAAK,CAAC;gBACpF;YACF;AACF,QAAA,CAAC;AAED,QAAA,MAAM,IAAI,GAAG,YAA0B;YACrC,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;AAC5C,YAAA,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC;YAErC,IAAI,QAAQ,IAAI,KAAK,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC3C,gBAAA,IAAI;AACF,oBAAA,MAAM,QAAQ,CAAC,oBAAoB,EAAE;gBACvC;gBAAE,OAAO,KAAK,EAAE;oBACd,IAAI,SAAS,EAAE;AACb,wBAAA,OAAO,CAAC,IAAI,CAAC,4DAA4D,EAAE,KAAK,CAAC;oBACnF;gBACF;YACF;AACF,QAAA,CAAC;AAED,QAAA,MAAM,MAAM,GAAG,YAA0B;AACvC,YAAA,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE;gBACvB,MAAM,IAAI,EAAE;YACd;iBAAO;gBACL,MAAM,KAAK,EAAE;YACf;AACF,QAAA,CAAC;AAED,QAAA,QAAQ,CAAC,MAAM,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACnE,QAAA,QAAQ,CAAC,MAAM,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAEpE,QAAA,YAAY,CAAC,MAAM,EAAE,OAAM,QAAQ,KAAG;AACpC,YAAA,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC;AAErC,YAAA,IAAI,KAAK,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC/B,gBAAA,IAAI;AACF,oBAAA,MAAM,QAAQ,CAAC,oBAAoB,EAAE;gBACvC;gBAAE,OAAO,KAAK,EAAE;oBACd,IAAI,SAAS,EAAE;AACb,wBAAA,OAAO,CAAC,IAAI,CACV,0EAA0E,EAC1E,KAAK,CACN;oBACH;gBACF;AACA,gBAAA,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YACrB;AACF,QAAA,CAAC,CAAC;QAEF,OAAO;YACL,WAAW;YACX,QAAQ;YACR,KAAK;YACL,IAAI;YACJ,MAAM;SACP;AACH,IAAA,CAAC,CAAC;AACJ;;AClKA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-browser-picture-in-picture.mjs","sources":["../../../projects/core/browser/picture-in-picture/index.ts","../../../projects/core/browser/picture-in-picture/signality-core-browser-picture-in-picture.ts"],"sourcesContent":["import { signal, type Signal, untracked } from '@angular/core';\nimport {\n assertElement,\n constSignal,\n getPipElement,\n NOOP_ASYNC_FN,\n setupContext,\n toElement,\n} from '@signality/core/internal';\nimport type { MaybeElementSignal, WithInjector } from '@signality/core/types';\nimport { listener } from '@signality/core/browser/listener';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport type PictureInPictureOptions = WithInjector;\n\nexport interface PictureInPictureRef {\n /**\n * Whether the Picture-in-Picture API is supported in the current browser.\n *\n * @see [Picture-in-Picture API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the target video element is currently displayed in Picture-in-Picture mode.\n *\n * @see [Document: pictureInPictureElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/pictureInPictureElement)\n */\n readonly isActive: Signal<boolean>;\n\n /**\n * Enter Picture-in-Picture mode for the target video element.\n *\n * @throws {DOMException} `'NotAllowedError'` — the document is not allowed to use PiP\n * @throws {DOMException} `'InvalidStateError'` — the video element has `disablePictureInPicture` attribute\n * @throws {DOMException} `'NotSupportedError'` — Picture-in-Picture is not supported\n *\n * @see [HTMLVideoElement: requestPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture)\n */\n readonly enter: () => Promise<void>;\n\n /**\n * Exit Picture-in-Picture mode.\n *\n * @see [Document: exitPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/exitPictureInPicture)\n */\n readonly exit: () => Promise<void>;\n\n /**\n * Toggle Picture-in-Picture mode — enters if inactive, exits if active.\n */\n readonly toggle: () => Promise<void>;\n}\n\n/**\n * Signal-based wrapper around the [Picture-in-Picture API](https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API).\n *\n * Automatically exits Picture-in-Picture when the target element is disconnected from the DOM.\n *\n * @param target - Video element\n * @param options - Optional configuration\n * @returns A {@link PictureInPictureRef} with `isSupported`, `isActive` signals and `enter`/`exit`/`toggle` methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (pip.isSupported()) {\n * <video #video src=\"video.mp4\"></video>\n * <button (click)=\"pip.toggle()\">Toggle PiP</button>\n * <p>Active: {{ pip.isActive() }}</p>\n * }\n * `\n * })\n * export class PiPDemo {\n * readonly video = viewChild<HTMLVideoElement>('video');\n * readonly pip = pictureInPicture(this.video);\n * }\n * ```\n */\nexport function pictureInPicture(\n target: MaybeElementSignal<HTMLVideoElement>,\n options?: PictureInPictureOptions\n): PictureInPictureRef {\n const { runInContext } = setupContext(options?.injector, pictureInPicture);\n\n return runInContext(({ isBrowser }) => {\n const isSupported = constSignal(\n isBrowser && 'pictureInPictureEnabled' in document && document.pictureInPictureEnabled\n );\n\n if (!isSupported()) {\n return {\n isSupported,\n isActive: constSignal(false),\n enter: NOOP_ASYNC_FN,\n exit: NOOP_ASYNC_FN,\n toggle: NOOP_ASYNC_FN,\n };\n }\n\n const isActive = signal(false);\n\n const enter = async (): Promise<void> => {\n const targetEl = toElement.untracked(target);\n assertElement(targetEl, 'pictureInPicture');\n await targetEl.requestPictureInPicture();\n };\n\n const exit = async (): Promise<void> => {\n const targetEl = toElement.untracked(target);\n const pipEl = getPipElement(document);\n\n if (targetEl === pipEl) {\n try {\n await document.exitPictureInPicture();\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[pictureInPicture] Failed to exit Picture-in-Picture mode.`, error);\n }\n }\n }\n };\n\n const toggle = async (): Promise<void> => {\n if (untracked(isActive)) {\n await exit();\n } else {\n await enter();\n }\n };\n\n listener(target, 'enterpictureinpicture', () => isActive.set(true));\n listener(target, 'leavepictureinpicture', () => isActive.set(false));\n\n onDisconnect(target, async targetEl => {\n const pipEl = getPipElement(document);\n\n if (pipEl && targetEl === pipEl) {\n try {\n await document.exitPictureInPicture();\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[pictureInPicture] Failed to exit Picture-in-Picture mode on disconnect.`,\n error\n );\n }\n }\n isActive.set(false);\n }\n });\n\n return {\n isSupported,\n isActive,\n enter,\n exit,\n toggle,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACG,SAAU,gBAAgB,CAC9B,MAA4C,EAC5C,OAAiC,EAAA;AAEjC,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,CAAC;AAE1E,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAI;AACpC,QAAA,MAAM,WAAW,GAAG,WAAW,CAC7B,SAAS,IAAI,yBAAyB,IAAI,QAAQ,IAAI,QAAQ,CAAC,uBAAuB,CACvF;AAED,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC;AAC5B,gBAAA,KAAK,EAAE,aAAa;AACpB,gBAAA,IAAI,EAAE,aAAa;AACnB,gBAAA,MAAM,EAAE,aAAa;aACtB;QACH;AAEA,QAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,oDAAC;AAE9B,QAAA,MAAM,KAAK,GAAG,YAA0B;YACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;AAC5C,YAAA,aAAa,CAAC,QAAQ,EAAE,kBAAkB,CAAC;AAC3C,YAAA,MAAM,QAAQ,CAAC,uBAAuB,EAAE;AAC1C,QAAA,CAAC;AAED,QAAA,MAAM,IAAI,GAAG,YAA0B;YACrC,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;AAC5C,YAAA,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC;AAErC,YAAA,IAAI,QAAQ,KAAK,KAAK,EAAE;AACtB,gBAAA,IAAI;AACF,oBAAA,MAAM,QAAQ,CAAC,oBAAoB,EAAE;gBACvC;gBAAE,OAAO,KAAK,EAAE;oBACd,IAAI,SAAS,EAAE;AACb,wBAAA,OAAO,CAAC,IAAI,CAAC,4DAA4D,EAAE,KAAK,CAAC;oBACnF;gBACF;YACF;AACF,QAAA,CAAC;AAED,QAAA,MAAM,MAAM,GAAG,YAA0B;AACvC,YAAA,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE;gBACvB,MAAM,IAAI,EAAE;YACd;iBAAO;gBACL,MAAM,KAAK,EAAE;YACf;AACF,QAAA,CAAC;AAED,QAAA,QAAQ,CAAC,MAAM,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACnE,QAAA,QAAQ,CAAC,MAAM,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAEpE,QAAA,YAAY,CAAC,MAAM,EAAE,OAAM,QAAQ,KAAG;AACpC,YAAA,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC;AAErC,YAAA,IAAI,KAAK,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC/B,gBAAA,IAAI;AACF,oBAAA,MAAM,QAAQ,CAAC,oBAAoB,EAAE;gBACvC;gBAAE,OAAO,KAAK,EAAE;oBACd,IAAI,SAAS,EAAE;AACb,wBAAA,OAAO,CAAC,IAAI,CACV,0EAA0E,EAC1E,KAAK,CACN;oBACH;gBACF;AACA,gBAAA,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YACrB;AACF,QAAA,CAAC,CAAC;QAEF,OAAO;YACL,WAAW;YACX,QAAQ;YACR,KAAK;YACL,IAAI;YACJ,MAAM;SACP;AACH,IAAA,CAAC,CAAC;AACJ;;ACjKA;;AAEG;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-speech-recognition.mjs","sources":["../../../projects/core/browser/speech-recognition/index.ts","../../../projects/core/browser/speech-recognition/signality-core-browser-speech-recognition.ts"],"sourcesContent":["import { isSignal, type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\nimport { permissionState } from '@signality/core/browser/permission-state';\n\nexport interface SpeechRecognitionOptions extends WithInjector {\n /**\n * BCP 47 language tag for recognition (e.g. `'en-US'`).\n *\n * @default 'en-US'\n * @see [SpeechRecognition: lang on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/lang)\n */\n readonly lang?: MaybeSignal<string>;\n\n /**\n * Whether to return interim (in-progress) results alongside final ones.\n *\n * @default false\n * @see [SpeechRecognition: interimResults on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/interimResults)\n */\n readonly interimResults?: boolean;\n\n /**\n * Whether recognition continues after the user stops speaking.\n *\n * @default false\n * @see [SpeechRecognition: continuous on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/continuous)\n */\n readonly continuous?: boolean;\n\n /**\n * Maximum number of alternative recognition results per utterance.\n *\n * @default 1\n * @see [SpeechRecognition: maxAlternatives on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/maxAlternatives)\n */\n readonly maxAlternatives?: number;\n}\n\nexport interface SpeechRecognitionRef {\n /**\n * Whether the Speech Recognition API is supported in the current browser.\n *\n * @see [SpeechRecognition browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether speech recognition is currently active and listening.\n */\n readonly isListening: Signal<boolean>;\n\n /**\n * Accumulated final transcript text.\n *\n * @see [SpeechRecognitionResult on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionResult)\n */\n readonly text: Signal<string>;\n\n /**\n * In-progress interim transcript. Only populated when `interimResults` is `true`.\n *\n * @see [SpeechRecognition: interimResults on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/interimResults)\n */\n readonly interimText: Signal<string>;\n\n /**\n * The last recognition error, or `null` if no error occurred.\n *\n * @see [SpeechRecognitionErrorEvent on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionErrorEvent)\n */\n readonly error: Signal<SpeechRecognitionErrorEvent | Error | null>;\n\n /**\n * Start listening for speech input.\n *\n * @see [SpeechRecognition: start() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/start)\n */\n readonly start: () => void;\n\n /**\n * Stop listening and return any remaining results.\n *\n * @see [SpeechRecognition: stop() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/stop)\n */\n readonly stop: () => void;\n\n /**\n * Abort recognition immediately without returning results.\n *\n * @see [SpeechRecognition: abort() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/abort)\n */\n readonly abort: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Speech Recognition API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).\n *\n * @param options - Optional configuration\n * @returns A SpeechRecognitionRef with isSupported, isListening, text, interimText, error signals and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (recognition.isSupported()) {\n * <button (click)=\"toggleRecognition()\">\n * {{ recognition.isListening() ? 'Stop' : 'Start' }} Recording\n * </button>\n * <p>{{ recognition.text() }}</p>\n * @if (recognition.interimText()) {\n * <p><em>{{ recognition.interimText() }}</em></p>\n * }\n * }\n * `\n * })\n * export class SpeechComponent {\n * readonly recognition = speechRecognition();\n *\n * toggleRecognition() {\n * if (this.recognition.isListening()) {\n * this.recognition.stop();\n * } else {\n * this.recognition.start();\n * }\n * }\n * }\n * ```\n */\nexport function speechRecognition(options?: SpeechRecognitionOptions): SpeechRecognitionRef {\n const { runInContext } = setupContext(options?.injector, speechRecognition);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(\n isBrowser && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)\n );\n\n if (!isSupported()) {\n return {\n isSupported,\n isListening: constSignal(false),\n text: constSignal(''),\n interimText: constSignal(''),\n error: constSignal(null),\n start: NOOP_FN,\n stop: NOOP_FN,\n abort: NOOP_FN,\n };\n }\n\n const SpeechRecognitionClass = window.SpeechRecognition || window.webkitSpeechRecognition;\n\n const recognition = new SpeechRecognitionClass();\n\n const {\n lang = 'en-US',\n interimResults = false,\n continuous = false,\n maxAlternatives = 1,\n } = options ?? {};\n\n recognition.lang = toValue(lang);\n recognition.continuous = continuous;\n recognition.interimResults = interimResults;\n recognition.maxAlternatives = maxAlternatives;\n\n const isListening = signal(false);\n const text = signal('');\n const interimText = signal('');\n const error = signal<SpeechRecognitionErrorEvent | Error | null>(null);\n\n const handleResult = (event: SpeechRecognitionEvent) => {\n let finalTranscript = '';\n let interimTranscript = '';\n\n for (let i = event.resultIndex; i < event.results.length; i++) {\n const result = event.results[i];\n const transcript = result[0]?.transcript || result.item(0)?.transcript || '';\n\n if (result.isFinal) {\n finalTranscript += transcript;\n } else {\n interimTranscript += transcript;\n }\n }\n\n if (finalTranscript) {\n text.update(t => (t ? t + ' ' : '') + finalTranscript);\n interimText.set('');\n } else if (interimTranscript) {\n interimText.set(interimTranscript);\n }\n };\n\n const handleError = (event: SpeechRecognitionErrorEvent) => {\n error.set(event);\n isListening.set(false);\n };\n\n const handleStart = () => {\n isListening.set(true);\n error.set(null);\n\n if (!continuous) {\n text.set('');\n interimText.set('');\n }\n };\n\n const handleEnd = () => {\n isListening.set(false);\n recognition.lang = toValue(lang);\n };\n\n recognition.onstart = handleStart;\n recognition.onend = handleEnd;\n recognition.onerror = handleError;\n recognition.onresult = handleResult;\n\n const start = () => {\n try {\n if (!untracked(isListening)) {\n recognition.start();\n }\n } catch (err) {\n error.set(err as Error);\n }\n };\n\n const stop = () => {\n if (untracked(isListening)) {\n recognition.stop();\n }\n };\n\n const abort = () => {\n if (untracked(isListening)) {\n recognition.abort();\n }\n };\n\n onCleanup(abort);\n\n if (isSignal(lang)) {\n watcher(lang, newLang => {\n if (!isListening()) {\n recognition.lang = newLang;\n }\n });\n }\n\n watcher(permissionState('microphone'), state => {\n if (state === 'denied') {\n abort();\n }\n });\n\n return {\n isSupported,\n isListening: isListening.asReadonly(),\n text: text.asReadonly(),\n interimText: interimText.asReadonly(),\n error: error.asReadonly(),\n start,\n stop,\n abort,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signality-core-browser-speech-recognition.mjs","sources":["../../../projects/core/browser/speech-recognition/index.ts","../../../projects/core/browser/speech-recognition/signality-core-browser-speech-recognition.ts"],"sourcesContent":["import { isSignal, type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\nimport { permissionState } from '@signality/core/browser/permission-state';\n\nexport interface SpeechRecognitionOptions extends WithInjector {\n /**\n * BCP 47 language tag for recognition (e.g. `'en-US'`).\n *\n * @default 'en-US'\n * @see [SpeechRecognition: lang on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/lang)\n */\n readonly lang?: MaybeSignal<string>;\n\n /**\n * Whether to return interim (in-progress) results alongside final ones.\n *\n * @default false\n * @see [SpeechRecognition: interimResults on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/interimResults)\n */\n readonly interimResults?: boolean;\n\n /**\n * Whether recognition continues after the user stops speaking.\n *\n * @default false\n * @see [SpeechRecognition: continuous on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/continuous)\n */\n readonly continuous?: boolean;\n\n /**\n * Maximum number of alternative recognition results per utterance.\n *\n * @default 1\n * @see [SpeechRecognition: maxAlternatives on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/maxAlternatives)\n */\n readonly maxAlternatives?: number;\n}\n\nexport interface SpeechRecognitionRef {\n /**\n * Whether the Speech Recognition API is supported in the current browser.\n *\n * @see [SpeechRecognition browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether speech recognition is currently active and listening.\n */\n readonly isListening: Signal<boolean>;\n\n /**\n * Accumulated final transcript text.\n *\n * @see [SpeechRecognitionResult on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionResult)\n */\n readonly text: Signal<string>;\n\n /**\n * In-progress interim transcript. Only populated when `interimResults` is `true`.\n *\n * @see [SpeechRecognition: interimResults on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/interimResults)\n */\n readonly interimText: Signal<string>;\n\n /**\n * The last recognition error, or `null` if no error occurred.\n *\n * @see [SpeechRecognitionErrorEvent on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionErrorEvent)\n */\n readonly error: Signal<SpeechRecognitionErrorEvent | Error | null>;\n\n /**\n * Start listening for speech input.\n *\n * @see [SpeechRecognition: start() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/start)\n */\n readonly start: () => void;\n\n /**\n * Stop listening and return any remaining results.\n *\n * @see [SpeechRecognition: stop() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/stop)\n */\n readonly stop: () => void;\n\n /**\n * Abort recognition immediately without returning results.\n *\n * @see [SpeechRecognition: abort() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/abort)\n */\n readonly abort: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Speech Recognition API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).\n *\n * @param options - Optional configuration\n * @returns A SpeechRecognitionRef with isSupported, isListening, text, interimText, error signals and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (recognition.isSupported()) {\n * <button (click)=\"toggleRecognition()\">\n * {{ recognition.isListening() ? 'Stop' : 'Start' }} Recording\n * </button>\n * <p>{{ recognition.text() }}</p>\n * @if (recognition.interimText()) {\n * <p><em>{{ recognition.interimText() }}</em></p>\n * }\n * }\n * `\n * })\n * export class SpeechComponent {\n * readonly recognition = speechRecognition();\n *\n * toggleRecognition() {\n * if (this.recognition.isListening()) {\n * this.recognition.stop();\n * } else {\n * this.recognition.start();\n * }\n * }\n * }\n * ```\n */\nexport function speechRecognition(options?: SpeechRecognitionOptions): SpeechRecognitionRef {\n const { runInContext } = setupContext(options?.injector, speechRecognition);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(\n isBrowser && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)\n );\n\n if (!isSupported()) {\n return {\n isSupported,\n isListening: constSignal(false),\n text: constSignal(''),\n interimText: constSignal(''),\n error: constSignal(null),\n start: NOOP_FN,\n stop: NOOP_FN,\n abort: NOOP_FN,\n };\n }\n\n const SpeechRecognitionClass =\n (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;\n\n const recognition: SpeechRecognition = new SpeechRecognitionClass();\n\n const {\n lang = 'en-US',\n interimResults = false,\n continuous = false,\n maxAlternatives = 1,\n } = options ?? {};\n\n recognition.lang = toValue(lang);\n recognition.continuous = continuous;\n recognition.interimResults = interimResults;\n recognition.maxAlternatives = maxAlternatives;\n\n const isListening = signal(false);\n const text = signal('');\n const interimText = signal('');\n const error = signal<SpeechRecognitionErrorEvent | Error | null>(null);\n\n const handleResult = (event: SpeechRecognitionEvent) => {\n let finalTranscript = '';\n let interimTranscript = '';\n\n for (let i = event.resultIndex; i < event.results.length; i++) {\n const result = event.results[i];\n const transcript = result[0]?.transcript || result.item(0)?.transcript || '';\n\n if (result.isFinal) {\n finalTranscript += transcript;\n } else {\n interimTranscript += transcript;\n }\n }\n\n if (finalTranscript) {\n text.update(t => (t ? t + ' ' : '') + finalTranscript);\n interimText.set('');\n } else if (interimTranscript) {\n interimText.set(interimTranscript);\n }\n };\n\n const handleError = (event: SpeechRecognitionErrorEvent) => {\n error.set(event);\n isListening.set(false);\n };\n\n const handleStart = () => {\n isListening.set(true);\n error.set(null);\n\n if (!continuous) {\n text.set('');\n interimText.set('');\n }\n };\n\n const handleEnd = () => {\n isListening.set(false);\n recognition.lang = toValue(lang);\n };\n\n recognition.onstart = handleStart;\n recognition.onend = handleEnd;\n recognition.onerror = handleError;\n recognition.onresult = handleResult;\n\n const start = () => {\n try {\n if (!untracked(isListening)) {\n recognition.start();\n }\n } catch (err) {\n error.set(err as Error);\n }\n };\n\n const stop = () => {\n if (untracked(isListening)) {\n recognition.stop();\n }\n };\n\n const abort = () => {\n if (untracked(isListening)) {\n recognition.abort();\n }\n };\n\n onCleanup(abort);\n\n if (isSignal(lang)) {\n watcher(lang, newLang => {\n if (!isListening()) {\n recognition.lang = newLang;\n }\n });\n }\n\n watcher(permissionState('microphone'), state => {\n if (state === 'denied') {\n abort();\n }\n });\n\n return {\n isSupported,\n isListening: isListening.asReadonly(),\n text: text.asReadonly(),\n interimText: interimText.asReadonly(),\n error: error.asReadonly(),\n start,\n stop,\n abort,\n };\n });\n}\n\n/**\n * Local type definitions for Web Speech API (Speech Recognition).\n *\n * @remarks\n * External `@types/dom-speech-recognition` package may conflict with user's other libraries\n * or become outdated. For better DX, we define minimal required interfaces locally\n * without polluting the global namespace with experimental APIs.\n */\ntype SpeechRecognitionErrorCode =\n | 'aborted'\n | 'audio-capture'\n | 'bad-grammar'\n | 'language-not-supported'\n | 'network'\n | 'no-speech'\n | 'not-allowed'\n | 'service-not-allowed';\n\ninterface SpeechRecognitionAlternative {\n readonly transcript: string;\n readonly confidence: number;\n}\n\ninterface SpeechRecognitionResult {\n readonly isFinal: boolean;\n readonly length: number;\n item(index: number): SpeechRecognitionAlternative;\n [index: number]: SpeechRecognitionAlternative;\n}\n\ninterface SpeechRecognitionResultList {\n readonly length: number;\n item(index: number): SpeechRecognitionResult;\n [index: number]: SpeechRecognitionResult;\n}\n\ninterface SpeechGrammar {\n src: string;\n weight: number;\n}\n\ninterface SpeechGrammarList {\n readonly length: number;\n addFromString(string: string, weight?: number): void;\n addFromURI(src: string, weight?: number): void;\n item(index: number): SpeechGrammar;\n [index: number]: SpeechGrammar;\n}\n\ninterface SpeechRecognitionEvent extends Event {\n readonly resultIndex: number;\n readonly results: SpeechRecognitionResultList;\n}\n\ninterface SpeechRecognitionErrorEvent extends Event {\n readonly error: SpeechRecognitionErrorCode;\n readonly message: string;\n}\n\ninterface SpeechRecognition extends EventTarget {\n continuous: boolean;\n grammars: SpeechGrammarList;\n interimResults: boolean;\n lang: string;\n maxAlternatives: number;\n onaudioend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onaudiostart: ((this: SpeechRecognition, ev: Event) => void) | null;\n onend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null;\n onnomatch: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null;\n onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null;\n onsoundend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onsoundstart: ((this: SpeechRecognition, ev: Event) => void) | null;\n onspeechend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onspeechstart: ((this: SpeechRecognition, ev: Event) => void) | null;\n onstart: ((this: SpeechRecognition, ev: Event) => void) | null;\n abort(): void;\n start(audioTrack?: MediaStreamTrack): void;\n stop(): void;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-storage.mjs","sources":["../../../projects/core/browser/storage/index.ts","../../../projects/core/browser/storage/signality-core-browser-storage.ts"],"sourcesContent":["import { type CreateSignalOptions, isSignal, signal, type WritableSignal } from '@angular/core';\nimport { isPlainObject, proxySignal, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { listener, setupSync } from '@signality/core/browser/listener';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport interface StorageOptions<T> extends CreateSignalOptions<T>, WithInjector {\n /**\n * Storage type to use.\n * @default 'local'\n */\n readonly type?: 'local' | 'session';\n\n /**\n * Custom serializer for read/write operations.\n *\n * If not provided, the serializer is automatically inferred from the initial value type:\n * - `string` → pass-through (no transformation)\n * - `number` → handles Infinity, -Infinity, NaN\n * - `boolean` → strict true/false conversion\n * - `bigint` → string representation\n * - `Date` → ISO 8601 format\n * - `Map` → JSON array of entries\n * - `Set` → JSON array\n * - `object/array` → JSON serialization\n *\n * @example\n * ```typescript\n * // Use built-in serializers\n * import { Serializers } from '@signality/core';\n *\n * const counter = storage('count', 0, {\n * serializer: Serializers.number,\n * });\n *\n * // or create a custom serializer\n * const userSettings = storage('settings', defaultSettings, {\n * serializer: {\n * write: (v) => JSON.stringify(v),\n * read: (s) => ({ ...defaultSettings, ...JSON.parse(s) }),\n * },\n * });\n * ```\n */\n readonly serializer?: Serializer<T>;\n\n /**\n * Merge resolver function when reading from storage.\n *\n * Receives stored value and default value, returns the final value.\n * Default: shallow merge for objects ({ ...initialValue, ...stored })\n *\n * Useful for handling schema migrations when default has new properties.\n *\n * @example\n * ```typescript\n * const settings = storage('settings', { theme: 'dark', fontSize: 14 }, {\n * mergeResolver: (stored, initial) => ({ ...initial, ...stored }),\n * });\n *\n * // Or with custom merge\n * const settings = storage('settings', defaultSettings, {\n * mergeResolver: (stored, initial) => deepMerge(stored, initial),\n * });\n * ```\n */\n readonly mergeResolver?: (storedValue: T, initialValue: T) => T;\n}\n\n/**\n * Serializer interface for converting values to/from strings for storage.\n */\nexport interface Serializer<T> {\n readonly write: (value: T) => string;\n readonly read: (raw: string) => T;\n}\n\n/**\n * Signal-based wrapper around the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) (localStorage/sessionStorage).\n *\n * @param key - Storage key (can be a signal for dynamic keys)\n * @param initialValue - Default value if key doesn't exist\n * @param options - Configuration options\n * @returns A WritableSignal that automatically syncs with storage\n *\n * @example\n * Basic usage with automatic serialization:\n * ```typescript\n * @Component({\n * template: '\n * <input [(ngModel)]=\"username\" />\n * <p>Count: {{ count() }}</p>\n * <button (click)=\"count.set(count() + 1)\">Increment</button>\n * '\n * })\n * export class UserPreview {\n * readonly username = storage('username', '');\n * readonly count = storage('counter', 0); // number serialization inferred\n * readonly lastVisit = storage('lastVisit', new Date()); // Date serialization inferred\n * }\n * ```\n *\n * @example\n * With options:\n * ```typescript\n * const preferences = storage('prefs', defaultPrefs, {\n * type: 'session',\n * mergeWithInitial: true,\n * });\n * ```\n */\nexport function storage<T>(\n key: MaybeSignal<string>,\n initialValue: T,\n options?: StorageOptions<T>\n): WritableSignal<T> {\n const { runInContext } = setupContext(options?.injector, storage);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return signal(initialValue, options);\n }\n\n const storageType = options?.type ?? 'local';\n const serializer = resolveSerializer(initialValue, options);\n\n const getStorage = (): Storage | null => {\n const type = storageType === 'local' ? 'localStorage' : 'sessionStorage';\n\n if (!storageAvailable(type)) {\n if (ngDevMode) {\n console.warn(`[storage] ${type} is not available or accessible`);\n }\n return null;\n }\n\n return window[type];\n };\n\n const mergeWithInitial = (storedValue: T) => {\n if (options?.mergeResolver) {\n return options.mergeResolver(storedValue, initialValue);\n }\n\n if (isPlainObject(initialValue)) {\n return { ...initialValue, ...storedValue };\n }\n\n return storedValue;\n };\n\n const readValue = (storageKey: string): T => {\n const storage = getStorage();\n\n if (storage === null) {\n return initialValue;\n }\n\n try {\n const raw = storage.getItem(storageKey);\n\n if (raw === null) {\n if (initialValue != null) {\n writeValue(initialValue);\n }\n return initialValue;\n }\n\n const parsed = serializer.read(raw);\n return mergeWithInitial(parsed);\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[storage] Failed to deserialize value for key \"${key}\"`, error);\n }\n\n return initialValue;\n }\n };\n\n const writeValue = (value: T): void => {\n const storage = getStorage();\n const storageKey = toValue(key);\n\n if (storage === null) {\n return;\n }\n\n try {\n if (value == null) {\n storage.removeItem(storageKey);\n } else {\n const serialized = serializer.write(value);\n storage.setItem(storageKey, serialized);\n }\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[storage] Failed to write value for key \"${storageKey}\". ` +\n `This may be due to storage quota exceeded or serialization error.`,\n error\n );\n }\n }\n };\n\n const state = signal<T>(readValue(toValue(key)), options);\n\n if (storageType === 'local') {\n setupSync(() => {\n listener(window, 'storage', event => {\n const currentKey = toValue(key);\n\n if (event.key === currentKey && event.storageArea === window.localStorage) {\n try {\n const newValue =\n event.newValue === null\n ? initialValue\n : mergeWithInitial(serializer.read(event.newValue));\n\n state.set(newValue);\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[storage] Failed to sync value from other tab for key \"${event.key}\"`,\n error\n );\n }\n }\n }\n });\n });\n }\n\n if (isSignal(key)) {\n watcher(key, newKey => state.set(readValue(newKey)));\n }\n\n return proxySignal(state, {\n set: (value: T) => {\n state.set(value);\n writeValue(value);\n },\n });\n });\n}\n\nexport const Serializers = {\n string: {\n read: (v: string): string => v,\n write: (v: string): string => v,\n } satisfies Serializer<string>,\n\n number: {\n read: (v: string): number => {\n if (v === 'Infinity') return Infinity;\n if (v === '-Infinity') return -Infinity;\n if (v === 'NaN') return NaN;\n return Number.parseFloat(v);\n },\n write: (v: number): string => {\n if (Number.isNaN(v)) return 'NaN';\n if (v === Infinity) return 'Infinity';\n if (v === -Infinity) return '-Infinity';\n return String(v);\n },\n } satisfies Serializer<number>,\n\n boolean: {\n read: (v: string): boolean => v === 'true',\n write: (v: boolean): string => (v ? 'true' : 'false'),\n } satisfies Serializer<boolean>,\n\n bigint: {\n read: (v: string): bigint => BigInt(v),\n write: (v: bigint): string => v.toString(),\n } satisfies Serializer<bigint>,\n\n /*\n * Date serializer - uses ISO 8601 format for maximum compatibility.\n */\n date: {\n read: (v: string): Date => new Date(v),\n write: (v: Date): string => v.toISOString(),\n } satisfies Serializer<Date>,\n\n object: {\n read: <T>(v: string): T => JSON.parse(v) as T,\n write: <T>(v: T): string => JSON.stringify(v),\n } satisfies Serializer<unknown>,\n\n map: {\n read: <K, V>(v: string): Map<K, V> => new Map(JSON.parse(v)),\n write: <K, V>(v: Map<K, V>): string => JSON.stringify([...v.entries()]),\n } satisfies Serializer<Map<unknown, unknown>>,\n\n set: {\n read: <T>(v: string): Set<T> => new Set(JSON.parse(v)),\n write: <T>(v: Set<T>): string => JSON.stringify([...v]),\n } satisfies Serializer<Set<unknown>>,\n\n /*\n * Any serializer - fallback that treats everything as string.\n */\n any: {\n read: <T>(v: string): T => v as T,\n write: (v: unknown): string => String(v),\n } satisfies Serializer<unknown>,\n} as const;\n\nfunction resolveSerializer<T>(initialValue: T, options?: StorageOptions<T>): Serializer<T> {\n if (options?.serializer) {\n return options.serializer;\n }\n const type = inferSerializerType(initialValue);\n return Serializers[type] as Serializer<T>;\n}\n\nfunction inferSerializerType<T>(value: T): keyof typeof Serializers {\n if (value === null || value === undefined) {\n return 'any';\n }\n\n if (value instanceof Map) {\n return 'map';\n }\n\n if (value instanceof Set) {\n return 'set';\n }\n\n if (value instanceof Date) {\n return 'date';\n }\n\n switch (typeof value) {\n case 'string':\n return 'string';\n case 'number':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'bigint':\n return 'bigint';\n case 'object':\n return 'object';\n default:\n return 'any';\n }\n}\n\nfunction storageAvailable(type: 'localStorage' | 'sessionStorage'): boolean {\n let storage: Storage | undefined;\n\n try {\n storage = window[type];\n const testKey = '__storage_test__';\n storage.setItem(testKey, testKey);\n storage.removeItem(testKey);\n return true;\n } catch (e) {\n return (\n e instanceof DOMException &&\n e.name === 'QuotaExceededError' &&\n storage !== undefined &&\n storage.length !== 0\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signality-core-browser-storage.mjs","sources":["../../../projects/core/browser/storage/index.ts","../../../projects/core/browser/storage/signality-core-browser-storage.ts"],"sourcesContent":["import { type CreateSignalOptions, isSignal, signal, type WritableSignal } from '@angular/core';\nimport { isPlainObject, proxySignal, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { listener, setupSync } from '@signality/core/browser/listener';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport interface StorageOptions<T> extends CreateSignalOptions<T>, WithInjector {\n /**\n * Storage type to use.\n * @default 'local'\n */\n readonly type?: 'local' | 'session';\n\n /**\n * Custom serializer for read/write operations.\n *\n * If not provided, the serializer is automatically inferred from the initial value type:\n * - `string` → pass-through (no transformation)\n * - `number` → handles Infinity, -Infinity, NaN\n * - `boolean` → strict true/false conversion\n * - `bigint` → string representation\n * - `Date` → ISO 8601 format\n * - `Map` → JSON array of entries\n * - `Set` → JSON array\n * - `object/array` → JSON serialization\n *\n * @example\n * ```typescript\n * // Use built-in serializers\n * import { Serializers } from '@signality/core';\n *\n * const counter = storage('count', 0, {\n * serializer: Serializers.number,\n * });\n *\n * // or create a custom serializer\n * const userSettings = storage('settings', defaultSettings, {\n * serializer: {\n * write: (v) => JSON.stringify(v),\n * read: (s) => ({ ...defaultSettings, ...JSON.parse(s) }),\n * },\n * });\n * ```\n */\n readonly serializer?: Serializer<T>;\n\n /**\n * Merge resolver function when reading from storage.\n *\n * Receives stored value and default value, returns the final value.\n * Default: shallow merge for objects ({ ...initialValue, ...stored })\n *\n * Useful for handling schema migrations when default has new properties.\n *\n * @example\n * ```typescript\n * const settings = storage('settings', { theme: 'dark', fontSize: 14 }, {\n * mergeResolver: (stored, initial) => ({ ...initial, ...stored }),\n * });\n *\n * // Or with custom merge\n * const settings = storage('settings', defaultSettings, {\n * mergeResolver: (stored, initial) => deepMerge(initial, stored),\n * });\n * ```\n */\n readonly mergeResolver?: (storedValue: T, initialValue: T) => T;\n}\n\n/**\n * Serializer interface for converting values to/from strings for storage.\n */\nexport interface Serializer<T> {\n readonly write: (value: T) => string;\n readonly read: (raw: string) => T;\n}\n\n/**\n * Signal-based wrapper around the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) (localStorage/sessionStorage).\n *\n * @param key - Storage key (can be a signal for dynamic keys)\n * @param initialValue - Default value if key doesn't exist\n * @param options - Configuration options\n * @returns A WritableSignal that automatically syncs with storage\n *\n * @example\n * Basic usage with automatic serialization:\n * ```typescript\n * @Component({\n * template: '\n * <input [(ngModel)]=\"username\" />\n * <p>Count: {{ count() }}</p>\n * <button (click)=\"count.set(count() + 1)\">Increment</button>\n * '\n * })\n * export class UserPreview {\n * readonly username = storage('username', '');\n * readonly count = storage('counter', 0); // number serialization inferred\n * readonly lastVisit = storage('lastVisit', new Date()); // Date serialization inferred\n * }\n * ```\n *\n * @example\n * With options:\n * ```typescript\n * const preferences = storage('prefs', defaultPrefs, {\n * type: 'session',\n * mergeWithInitial: true,\n * });\n * ```\n */\nexport function storage<T>(\n key: MaybeSignal<string>,\n initialValue: T,\n options?: StorageOptions<T>\n): WritableSignal<T> {\n const { runInContext } = setupContext(options?.injector, storage);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return signal(initialValue, options);\n }\n\n const storageType = options?.type ?? 'local';\n const serializer = resolveSerializer(initialValue, options);\n\n const getStorage = (): Storage | null => {\n const type = storageType === 'local' ? 'localStorage' : 'sessionStorage';\n\n if (!storageAvailable(type)) {\n if (ngDevMode) {\n console.warn(`[storage] ${type} is not available or accessible`);\n }\n return null;\n }\n\n return window[type];\n };\n\n const mergeWithInitial = (storedValue: T) => {\n if (options?.mergeResolver) {\n return options.mergeResolver(storedValue, initialValue);\n }\n\n if (isPlainObject(initialValue)) {\n return { ...initialValue, ...storedValue };\n }\n\n return storedValue;\n };\n\n const readValue = (storageKey: string): T => {\n const storage = getStorage();\n\n if (storage === null) {\n return initialValue;\n }\n\n try {\n const raw = storage.getItem(storageKey);\n\n if (raw === null) {\n if (initialValue != null) {\n writeValue(initialValue);\n }\n return initialValue;\n }\n\n const parsed = serializer.read(raw);\n return mergeWithInitial(parsed);\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[storage] Failed to deserialize value for key \"${key}\"`, error);\n }\n\n return initialValue;\n }\n };\n\n const writeValue = (value: T): void => {\n const storage = getStorage();\n const storageKey = toValue(key);\n\n if (storage === null) {\n return;\n }\n\n try {\n if (value == null) {\n storage.removeItem(storageKey);\n } else {\n const serialized = serializer.write(value);\n storage.setItem(storageKey, serialized);\n }\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[storage] Failed to write value for key \"${storageKey}\". ` +\n `This may be due to storage quota exceeded or serialization error.`,\n error\n );\n }\n }\n };\n\n const state = signal<T>(readValue(toValue(key)), options);\n\n if (storageType === 'local') {\n setupSync(() => {\n listener(window, 'storage', event => {\n const currentKey = toValue(key);\n\n if (event.key === currentKey && event.storageArea === window.localStorage) {\n try {\n const newValue =\n event.newValue === null\n ? initialValue\n : mergeWithInitial(serializer.read(event.newValue));\n\n state.set(newValue);\n } catch (error) {\n if (ngDevMode) {\n console.warn(\n `[storage] Failed to sync value from other tab for key \"${event.key}\"`,\n error\n );\n }\n }\n }\n });\n });\n }\n\n if (isSignal(key)) {\n watcher(key, newKey => state.set(readValue(newKey)));\n }\n\n return proxySignal(state, {\n set: (value: T) => {\n state.set(value);\n writeValue(value);\n },\n });\n });\n}\n\nexport const Serializers = {\n string: {\n read: (v: string): string => v,\n write: (v: string): string => v,\n } satisfies Serializer<string>,\n\n number: {\n read: (v: string): number => {\n if (v === 'Infinity') return Infinity;\n if (v === '-Infinity') return -Infinity;\n if (v === 'NaN') return NaN;\n return Number.parseFloat(v);\n },\n write: (v: number): string => {\n if (Number.isNaN(v)) return 'NaN';\n if (v === Infinity) return 'Infinity';\n if (v === -Infinity) return '-Infinity';\n return String(v);\n },\n } satisfies Serializer<number>,\n\n boolean: {\n read: (v: string): boolean => v === 'true',\n write: (v: boolean): string => (v ? 'true' : 'false'),\n } satisfies Serializer<boolean>,\n\n bigint: {\n read: (v: string): bigint => BigInt(v),\n write: (v: bigint): string => v.toString(),\n } satisfies Serializer<bigint>,\n\n /*\n * Date serializer - uses ISO 8601 format for maximum compatibility.\n */\n date: {\n read: (v: string): Date => new Date(v),\n write: (v: Date): string => v.toISOString(),\n } satisfies Serializer<Date>,\n\n object: {\n read: <T>(v: string): T => JSON.parse(v) as T,\n write: <T>(v: T): string => JSON.stringify(v),\n } satisfies Serializer<unknown>,\n\n map: {\n read: <K, V>(v: string): Map<K, V> => new Map(JSON.parse(v)),\n write: <K, V>(v: Map<K, V>): string => JSON.stringify([...v.entries()]),\n } satisfies Serializer<Map<unknown, unknown>>,\n\n set: {\n read: <T>(v: string): Set<T> => new Set(JSON.parse(v)),\n write: <T>(v: Set<T>): string => JSON.stringify([...v]),\n } satisfies Serializer<Set<unknown>>,\n\n /*\n * Any serializer - fallback that treats everything as string.\n */\n any: {\n read: <T>(v: string): T => v as T,\n write: (v: unknown): string => String(v),\n } satisfies Serializer<unknown>,\n} as const;\n\nfunction resolveSerializer<T>(initialValue: T, options?: StorageOptions<T>): Serializer<T> {\n if (options?.serializer) {\n return options.serializer;\n }\n const type = inferSerializerType(initialValue);\n return Serializers[type] as Serializer<T>;\n}\n\nfunction inferSerializerType<T>(value: T): keyof typeof Serializers {\n if (value === null || value === undefined) {\n return 'any';\n }\n\n if (value instanceof Map) {\n return 'map';\n }\n\n if (value instanceof Set) {\n return 'set';\n }\n\n if (value instanceof Date) {\n return 'date';\n }\n\n switch (typeof value) {\n case 'string':\n return 'string';\n case 'number':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'bigint':\n return 'bigint';\n case 'object':\n return 'object';\n default:\n return 'any';\n }\n}\n\nfunction storageAvailable(type: 'localStorage' | 'sessionStorage'): boolean {\n let storage: Storage | undefined;\n\n try {\n storage = window[type];\n const testKey = '__storage_test__';\n storage.setItem(testKey, testKey);\n storage.removeItem(testKey);\n return true;\n } catch (e) {\n return (\n e instanceof DOMException &&\n e.name === 'QuotaExceededError' &&\n storage !== undefined &&\n storage.length !== 0\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1,24 +1,23 @@
1
1
  import { signal } from '@angular/core';
2
- import { setupContext, constSignal } from '@signality/core/internal';
2
+ import { setupContext, proxySignal, toElement } from '@signality/core/internal';
3
3
  import { listener } from '@signality/core/browser/listener';
4
4
  import { onDisconnect } from '@signality/core/elements/on-disconnect';
5
5
 
6
6
  /**
7
7
  * Reactive tracking of focus state on an element.
8
- * Detects when an element gains or loses focus.
8
+ * Detects when an element gains or loses focus, and allows programmatically setting focus.
9
9
  *
10
10
  * @param target - The element to track focus state on
11
- * @param options - Optional configuration including focusVisible mode and injector
12
- * @returns A signal that is `true` when the element has focus
11
+ * @param options - Optional configuration including focusVisible, preventScroll and injector
12
+ * @returns A writable signal that is `true` when the element has focus
13
13
  *
14
14
  * @example
15
15
  * ```typescript
16
16
  * @Component({
17
17
  * template: `
18
18
  * <input #input [class.focused]="isFocused()" />
19
- * @if (isFocused()) {
20
- * <p>Input is focused</p>
21
- * }
19
+ * <button (click)="isFocused.set(true)">Focus Input</button>
20
+ * <button (click)="isFocused.set(false)">Blur Input</button>
22
21
  * `
23
22
  * })
24
23
  * export class FocusDemo {
@@ -31,9 +30,10 @@ function elementFocus(target, options) {
31
30
  const { runInContext } = setupContext(options?.injector, elementFocus);
32
31
  return runInContext(({ isServer }) => {
33
32
  if (isServer) {
34
- return constSignal(false);
33
+ return signal(false, options);
35
34
  }
36
35
  const focusVisible = options?.focusVisible ?? false;
36
+ const preventScroll = options?.preventScroll ?? false;
37
37
  const focused = signal(false, options);
38
38
  listener(target, 'focus', e => {
39
39
  focused.set(focusVisible ? e.target.matches(':focus-visible') : true);
@@ -41,8 +41,21 @@ function elementFocus(target, options) {
41
41
  listener(target, 'blur', () => {
42
42
  focused.set(false);
43
43
  });
44
- onDisconnect(target, () => focused.set(false));
45
- return focused.asReadonly();
44
+ onDisconnect(target, () => {
45
+ focused.set(false);
46
+ });
47
+ return proxySignal(focused, {
48
+ set: (value) => {
49
+ const el = toElement(target);
50
+ const hasFocus = el?.matches(':focus') ?? false;
51
+ if (value && !hasFocus) {
52
+ el?.focus({ preventScroll });
53
+ }
54
+ else if (!value && hasFocus) {
55
+ el?.blur();
56
+ }
57
+ },
58
+ });
46
59
  });
47
60
  }
48
61
 
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-elements-element-focus.mjs","sources":["../../../projects/core/elements/element-focus/index.ts","../../../projects/core/elements/element-focus/signality-core-elements-element-focus.ts"],"sourcesContent":["import { type CreateSignalOptions, signal, type Signal } from '@angular/core';\nimport { constSignal, setupContext } from '@signality/core/internal';\nimport type { MaybeElementSignal, WithInjector } from '@signality/core/types';\nimport { listener } from '@signality/core/browser/listener';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementFocusOptions extends CreateSignalOptions<boolean>, WithInjector {\n /**\n * Track focus using the `:focus-visible` pseudo-class.\n * The browser uses heuristics to determine when focus should be visually indicated\n * (e.g., keyboard navigation, programmatic focus, or when the element requires user attention).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible MDN: :focus-visible}\n * @default false\n */\n readonly focusVisible?: boolean;\n}\n\n/**\n * Reactive tracking of focus state on an element.\n * Detects when an element gains or loses focus.\n *\n * @param target - The element to track focus state on\n * @param options - Optional configuration including focusVisible mode and injector\n * @returns A signal that is `true` when the element has focus\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <input #input [class.focused]=\"isFocused()\" />\n * @if (isFocused()) {\n * <p>Input is focused</p>\n * }\n * `\n * })\n * export class FocusDemo {\n * readonly input = viewChild<ElementRef>('input');\n * readonly isFocused = elementFocus(this.input);\n * }\n * ```\n */\nexport function elementFocus(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementFocusOptions\n): Signal<boolean> {\n const { runInContext } = setupContext(options?.injector, elementFocus);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(false);\n }\n\n const focusVisible = options?.focusVisible ?? false;\n const focused = signal<boolean>(false, options);\n\n listener(target, 'focus', e => {\n focused.set(focusVisible ? (e.target as HTMLElement).matches(':focus-visible') : true);\n });\n\n listener(target, 'blur', () => {\n focused.set(false);\n });\n\n onDisconnect(target, () => focused.set(false));\n\n return focused.asReadonly();\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAkBA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,YAAY,CAC1B,MAAuC,EACvC,OAA6B,EAAA;AAE7B,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,CAAC;AAEtE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,KAAK,CAAC;QAC3B;AAEA,QAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,KAAK;QACnD,MAAM,OAAO,GAAG,MAAM,CAAU,KAAK,EAAE,OAAO,CAAC;AAE/C,QAAA,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,IAAG;YAC5B,OAAO,CAAC,GAAG,CAAC,YAAY,GAAI,CAAC,CAAC,MAAsB,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;AACxF,QAAA,CAAC,CAAC;AAEF,QAAA,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAK;AAC5B,YAAA,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACpB,QAAA,CAAC,CAAC;AAEF,QAAA,YAAY,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAE9C,QAAA,OAAO,OAAO,CAAC,UAAU,EAAE;AAC7B,IAAA,CAAC,CAAC;AACJ;;ACpEA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-elements-element-focus.mjs","sources":["../../../projects/core/elements/element-focus/index.ts","../../../projects/core/elements/element-focus/signality-core-elements-element-focus.ts"],"sourcesContent":["import { type CreateSignalOptions, signal, type WritableSignal } from '@angular/core';\nimport { proxySignal, setupContext, toElement } from '@signality/core/internal';\nimport type { MaybeElementSignal, WithInjector } from '@signality/core/types';\nimport { listener } from '@signality/core/browser/listener';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementFocusOptions extends CreateSignalOptions<boolean>, WithInjector {\n /**\n * Track focus using the `:focus-visible` pseudo-class.\n * The browser uses heuristics to determine when focus should be visually indicated\n * (e.g., keyboard navigation, programmatic focus, or when the element requires user attention).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible MDN: :focus-visible}\n * @default false\n */\n readonly focusVisible?: boolean;\n\n /**\n * Prevent scrolling to the element when it is focused.\n * @default false\n */\n readonly preventScroll?: boolean;\n}\n\n/**\n * Reactive tracking of focus state on an element.\n * Detects when an element gains or loses focus, and allows programmatically setting focus.\n *\n * @param target - The element to track focus state on\n * @param options - Optional configuration including focusVisible, preventScroll and injector\n * @returns A writable signal that is `true` when the element has focus\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <input #input [class.focused]=\"isFocused()\" />\n * <button (click)=\"isFocused.set(true)\">Focus Input</button>\n * <button (click)=\"isFocused.set(false)\">Blur Input</button>\n * `\n * })\n * export class FocusDemo {\n * readonly input = viewChild<ElementRef>('input');\n * readonly isFocused = elementFocus(this.input);\n * }\n * ```\n */\nexport function elementFocus(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementFocusOptions\n): WritableSignal<boolean> {\n const { runInContext } = setupContext(options?.injector, elementFocus);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return signal(false, options);\n }\n\n const focusVisible = options?.focusVisible ?? false;\n const preventScroll = options?.preventScroll ?? false;\n\n const focused = signal<boolean>(false, options);\n\n listener(target, 'focus', e => {\n focused.set(focusVisible ? (e.target as HTMLElement).matches(':focus-visible') : true);\n });\n\n listener(target, 'blur', () => {\n focused.set(false);\n });\n\n onDisconnect(target, () => {\n focused.set(false);\n });\n\n return proxySignal(focused, {\n set: (value: boolean) => {\n const el = toElement(target);\n const hasFocus = el?.matches(':focus') ?? false;\n\n if (value && !hasFocus) {\n el?.focus({ preventScroll });\n } else if (!value && hasFocus) {\n el?.blur();\n }\n },\n });\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAwBA;;;;;;;;;;;;;;;;;;;;;;AAsBG;AACG,SAAU,YAAY,CAC1B,MAAuC,EACvC,OAA6B,EAAA;AAE7B,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,CAAC;AAEtE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;QAC/B;AAEA,QAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,KAAK;AACnD,QAAA,MAAM,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,KAAK;QAErD,MAAM,OAAO,GAAG,MAAM,CAAU,KAAK,EAAE,OAAO,CAAC;AAE/C,QAAA,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,IAAG;YAC5B,OAAO,CAAC,GAAG,CAAC,YAAY,GAAI,CAAC,CAAC,MAAsB,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;AACxF,QAAA,CAAC,CAAC;AAEF,QAAA,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAK;AAC5B,YAAA,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACpB,QAAA,CAAC,CAAC;AAEF,QAAA,YAAY,CAAC,MAAM,EAAE,MAAK;AACxB,YAAA,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACpB,QAAA,CAAC,CAAC;QAEF,OAAO,WAAW,CAAC,OAAO,EAAE;AAC1B,YAAA,GAAG,EAAE,CAAC,KAAc,KAAI;AACtB,gBAAA,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;gBAC5B,MAAM,QAAQ,GAAG,EAAE,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK;AAE/C,gBAAA,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE;AACtB,oBAAA,EAAE,EAAE,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;gBAC9B;AAAO,qBAAA,IAAI,CAAC,KAAK,IAAI,QAAQ,EAAE;oBAC7B,EAAE,EAAE,IAAI,EAAE;gBACZ;YACF,CAAC;AACF,SAAA,CAAC;AACJ,IAAA,CAAC,CAAC;AACJ;;ACxFA;;AAEG;;;;"}
@@ -51,7 +51,7 @@ function elementSize(target, options) {
51
51
  }
52
52
  };
53
53
  resizeObserver(target, updateSize, options);
54
- onDisconnect(target, () => size.set(initialValue));
54
+ onDisconnect(target, () => size.set(DEFAULT_SIZE));
55
55
  return size;
56
56
  });
57
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-elements-element-size.mjs","sources":["../../../projects/core/elements/element-size/index.ts","../../../projects/core/elements/element-size/signality-core-elements-element-size.ts"],"sourcesContent":["import { type CreateSignalOptions, signal, type Signal } from '@angular/core';\nimport { constSignal, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeElementSignal, MaybeSignal, WithInjector } from '@signality/core/types';\nimport { resizeObserver } from '@signality/core/observers/resize-observer';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementSizeValue {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ElementSizeOptions extends CreateSignalOptions<ElementSizeValue>, WithInjector {\n /**\n * Which box model to observe. Can be a reactive signal.\n *\n * @default 'border-box'\n */\n readonly box?: MaybeSignal<ResizeObserverBoxOptions>;\n\n /**\n * Initial value for SSR and before the first measurement.\n *\n * @default { width: 0, height: 0 }\n */\n readonly initialValue?: ElementSizeValue;\n}\n\n/**\n * Signal-based wrapper around the [ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).\n *\n * @param target - The element to observe\n * @param options - Optional configuration including signal options (equal, debugName), box model, and injector\n * @returns A signal containing the current element dimensions\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div #box>\n * Size: {{ size().width }} × {{ size().height }}px\n * </div>\n * `\n * })\n * export class ElementSizeDemo {\n * readonly box = viewChild<ElementRef>('box');\n * readonly size = elementSize(this.box);\n * }\n * ```\n */\nexport function elementSize(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementSizeOptions\n): Signal<ElementSizeValue> {\n const { runInContext } = setupContext(options?.injector, elementSize);\n const initialValue = options?.initialValue ?? DEFAULT_SIZE;\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(initialValue);\n }\n\n const size = signal<ElementSizeValue>(initialValue, options);\n\n const updateSize = ([entry]: readonly ResizeObserverEntry[]) => {\n const box = toValue(options?.box) ?? 'border-box';\n\n if (box === 'content-box') {\n const contentBoxSize = entry.contentBoxSize?.[0];\n size.set({\n width: contentBoxSize?.inlineSize ?? entry.contentRect.width,\n height: contentBoxSize?.blockSize ?? entry.contentRect.height,\n });\n } else {\n const borderBoxSize = entry.borderBoxSize?.[0];\n size.set({\n width: borderBoxSize?.inlineSize ?? entry.contentRect.width,\n height: borderBoxSize?.blockSize ?? entry.contentRect.height,\n });\n }\n };\n\n resizeObserver(target, updateSize, options);\n\n onDisconnect(target, () => size.set(initialValue));\n\n return size;\n });\n}\n\nconst DEFAULT_SIZE: ElementSizeValue = {\n width: 0,\n height: 0,\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA2BA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACG,SAAU,WAAW,CACzB,MAAuC,EACvC,OAA4B,EAAA;AAE5B,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC;AACrE,IAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,YAAY;AAE1D,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,YAAY,CAAC;QAClC;QAEA,MAAM,IAAI,GAAG,MAAM,CAAmB,YAAY,EAAE,OAAO,CAAC;AAE5D,QAAA,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAiC,KAAI;YAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,YAAY;AAEjD,YAAA,IAAI,GAAG,KAAK,aAAa,EAAE;gBACzB,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;gBAChD,IAAI,CAAC,GAAG,CAAC;oBACP,KAAK,EAAE,cAAc,EAAE,UAAU,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK;oBAC5D,MAAM,EAAE,cAAc,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM;AAC9D,iBAAA,CAAC;YACJ;iBAAO;gBACL,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC;gBAC9C,IAAI,CAAC,GAAG,CAAC;oBACP,KAAK,EAAE,aAAa,EAAE,UAAU,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK;oBAC3D,MAAM,EAAE,aAAa,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM;AAC7D,iBAAA,CAAC;YACJ;AACF,QAAA,CAAC;AAED,QAAA,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC;AAE3C,QAAA,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAElD,QAAA,OAAO,IAAI;AACb,IAAA,CAAC,CAAC;AACJ;AAEA,MAAM,YAAY,GAAqB;AACrC,IAAA,KAAK,EAAE,CAAC;AACR,IAAA,MAAM,EAAE,CAAC;CACV;;AC5FD;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-elements-element-size.mjs","sources":["../../../projects/core/elements/element-size/index.ts","../../../projects/core/elements/element-size/signality-core-elements-element-size.ts"],"sourcesContent":["import { type CreateSignalOptions, signal, type Signal } from '@angular/core';\nimport { constSignal, setupContext, toValue } from '@signality/core/internal';\nimport type { MaybeElementSignal, MaybeSignal, WithInjector } from '@signality/core/types';\nimport { resizeObserver } from '@signality/core/observers/resize-observer';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementSizeValue {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ElementSizeOptions extends CreateSignalOptions<ElementSizeValue>, WithInjector {\n /**\n * Which box model to observe. Can be a reactive signal.\n *\n * @default 'border-box'\n */\n readonly box?: MaybeSignal<ResizeObserverBoxOptions>;\n\n /**\n * Initial value for SSR and before the first measurement.\n *\n * @default { width: 0, height: 0 }\n */\n readonly initialValue?: ElementSizeValue;\n}\n\n/**\n * Signal-based wrapper around the [ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).\n *\n * @param target - The element to observe\n * @param options - Optional configuration including signal options (equal, debugName), box model, and injector\n * @returns A signal containing the current element dimensions\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div #box>\n * Size: {{ size().width }} × {{ size().height }}px\n * </div>\n * `\n * })\n * export class ElementSizeDemo {\n * readonly box = viewChild<ElementRef>('box');\n * readonly size = elementSize(this.box);\n * }\n * ```\n */\nexport function elementSize(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementSizeOptions\n): Signal<ElementSizeValue> {\n const { runInContext } = setupContext(options?.injector, elementSize);\n const initialValue = options?.initialValue ?? DEFAULT_SIZE;\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(initialValue);\n }\n\n const size = signal<ElementSizeValue>(initialValue, options);\n\n const updateSize = ([entry]: readonly ResizeObserverEntry[]) => {\n const box = toValue(options?.box) ?? 'border-box';\n\n if (box === 'content-box') {\n const contentBoxSize = entry.contentBoxSize?.[0];\n size.set({\n width: contentBoxSize?.inlineSize ?? entry.contentRect.width,\n height: contentBoxSize?.blockSize ?? entry.contentRect.height,\n });\n } else {\n const borderBoxSize = entry.borderBoxSize?.[0];\n size.set({\n width: borderBoxSize?.inlineSize ?? entry.contentRect.width,\n height: borderBoxSize?.blockSize ?? entry.contentRect.height,\n });\n }\n };\n\n resizeObserver(target, updateSize, options);\n\n onDisconnect(target, () => size.set(DEFAULT_SIZE));\n\n return size;\n });\n}\n\nconst DEFAULT_SIZE: ElementSizeValue = {\n width: 0,\n height: 0,\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA2BA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACG,SAAU,WAAW,CACzB,MAAuC,EACvC,OAA4B,EAAA;AAE5B,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC;AACrE,IAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,YAAY;AAE1D,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,YAAY,CAAC;QAClC;QAEA,MAAM,IAAI,GAAG,MAAM,CAAmB,YAAY,EAAE,OAAO,CAAC;AAE5D,QAAA,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAiC,KAAI;YAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,YAAY;AAEjD,YAAA,IAAI,GAAG,KAAK,aAAa,EAAE;gBACzB,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;gBAChD,IAAI,CAAC,GAAG,CAAC;oBACP,KAAK,EAAE,cAAc,EAAE,UAAU,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK;oBAC5D,MAAM,EAAE,cAAc,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM;AAC9D,iBAAA,CAAC;YACJ;iBAAO;gBACL,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC;gBAC9C,IAAI,CAAC,GAAG,CAAC;oBACP,KAAK,EAAE,aAAa,EAAE,UAAU,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK;oBAC3D,MAAM,EAAE,aAAa,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM;AAC7D,iBAAA,CAAC;YACJ;AACF,QAAA,CAAC;AAED,QAAA,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC;AAE3C,QAAA,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAElD,QAAA,OAAO,IAAI;AACb,IAAA,CAAC,CAAC;AACJ;AAEA,MAAM,YAAY,GAAqB;AACrC,IAAA,KAAK,EAAE,CAAC;AACR,IAAA,MAAM,EAAE,CAAC;CACV;;AC5FD;;AAEG;;;;"}
@@ -27,7 +27,7 @@ import { onDisconnect } from '@signality/core/elements/on-disconnect';
27
27
  */
28
28
  function elementVisibility(target, options) {
29
29
  const { runInContext } = setupContext(options?.injector, elementVisibility);
30
- const initialValue = options?.initialValue ?? DEFAULT_VISIBILITY;
30
+ const initialValue = options?.initialValue ?? { isVisible: true, ratio: 1 };
31
31
  return runInContext(({ isServer }) => {
32
32
  if (isServer) {
33
33
  return constSignal(initialValue);
@@ -59,14 +59,10 @@ function elementVisibility(target, options) {
59
59
  });
60
60
  };
61
61
  intersectionObserver(target, update, { threshold, root, rootMargin });
62
- onDisconnect(target, () => visibility.set(initialValue));
62
+ onDisconnect(target, () => visibility.set({ isVisible: false, ratio: 0 }));
63
63
  return visibility;
64
64
  });
65
65
  }
66
- const DEFAULT_VISIBILITY = {
67
- isVisible: true,
68
- ratio: 1,
69
- };
70
66
 
71
67
  /**
72
68
  * Generated bundle index. Do not edit.
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-elements-element-visibility.mjs","sources":["../../../projects/core/elements/element-visibility/index.ts","../../../projects/core/elements/element-visibility/signality-core-elements-element-visibility.ts"],"sourcesContent":["import { type CreateSignalOptions, type Signal, signal } from '@angular/core';\nimport { constSignal, setupContext } from '@signality/core/internal';\nimport type { MaybeElementSignal, MaybeSignal, WithInjector } from '@signality/core/types';\nimport { intersectionObserver } from '@signality/core/observers/intersection-observer';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementVisibilityOptions\n extends CreateSignalOptions<ElementVisibilityValue>,\n WithInjector {\n /**\n * Fraction of the element that must be visible to trigger a change.\n * A single number or an array of thresholds, each between `0` and `1`.\n *\n * @default 0\n * @see [IntersectionObserver: thresholds on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds)\n */\n readonly threshold?: MaybeSignal<number | number[]>;\n\n /**\n * Scrollable ancestor used as the viewport for intersection checks.\n * `null` or `undefined` defaults to the browser viewport.\n *\n * @default undefined\n * @see [IntersectionObserver: root on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)\n */\n readonly root?: MaybeElementSignal<Element> | Document;\n\n /**\n * CSS margin applied around the root before computing intersections.\n * Accepts values in the same format as the CSS `margin` property (e.g. `'10px 0px'`).\n *\n * @default '0px'\n * @see [IntersectionObserver: rootMargin on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)\n */\n readonly rootMargin?: MaybeSignal<string>;\n\n /**\n * Initial value for SSR.\n *\n * @default { isVisible: true, ratio: 1 }\n */\n readonly initialValue?: ElementVisibilityValue;\n}\n\nexport interface ElementVisibilityValue {\n /**\n * Whether the element is currently intersecting the root (visible in the viewport).\n *\n * @see [IntersectionObserverEntry: isIntersecting on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting)\n */\n readonly isVisible: boolean;\n\n /**\n * Fraction of the element visible within the root, from `0.0` (not visible) to `1.0` (fully visible).\n *\n * @see [IntersectionObserverEntry: intersectionRatio on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/intersectionRatio)\n */\n readonly ratio: number;\n}\n\n/**\n * Signal-based wrapper around the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).\n *\n * @param target - The element to observe\n * @param options - Optional configuration\n * @returns A signal containing the current visibility state\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div #section [class.visible]=\"visibility().isVisible\">\n * Visibility: {{ visibility().ratio * 100 }}%\n * </div>\n * `\n * })\n * export class VisibilityDemo {\n * readonly section = viewChild<ElementRef>('section');\n * readonly visibility = elementVisibility(this.section);\n * }\n * ```\n */\nexport function elementVisibility(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementVisibilityOptions\n): Signal<ElementVisibilityValue> {\n const { runInContext } = setupContext(options?.injector, elementVisibility);\n const initialValue = options?.initialValue ?? DEFAULT_VISIBILITY;\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(initialValue);\n }\n\n const visibility = signal(initialValue, options);\n\n const threshold = options?.threshold ?? 0;\n const root = options?.root ?? undefined;\n const rootMargin = options?.rootMargin ?? '0px';\n\n const update = (entries: readonly IntersectionObserverEntry[]) => {\n if (entries.length === 0) {\n return;\n }\n\n // Find the entry with the latest time to ensure we use the most up-to-date state\n // IntersectionObserver may batch multiple changes and call the callback once\n // with multiple entries, and the order in the array doesn't guarantee\n // that the last entry is the most recent one\n let latestEntry = entries[0];\n let latestTime = entries[0].time;\n\n for (let i = 1; i < entries.length; i++) {\n const entry = entries[i];\n if (entry.time >= latestTime) {\n latestTime = entry.time;\n latestEntry = entry;\n }\n }\n\n visibility.set({\n isVisible: latestEntry.isIntersecting,\n ratio: latestEntry.intersectionRatio,\n });\n };\n\n intersectionObserver(target, update, { threshold, root, rootMargin });\n\n onDisconnect(target, () => visibility.set(initialValue));\n\n return visibility;\n });\n}\n\nconst DEFAULT_VISIBILITY: ElementVisibilityValue = {\n isVisible: true,\n ratio: 1,\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA4DA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACG,SAAU,iBAAiB,CAC/B,MAAuC,EACvC,OAAkC,EAAA;AAElC,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,CAAC;AAC3E,IAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,kBAAkB;AAEhE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,YAAY,CAAC;QAClC;QAEA,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC;AAEhD,QAAA,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,CAAC;AACzC,QAAA,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,SAAS;AACvC,QAAA,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,KAAK;AAE/C,QAAA,MAAM,MAAM,GAAG,CAAC,OAA6C,KAAI;AAC/D,YAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE;gBACxB;YACF;;;;;AAMA,YAAA,IAAI,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;YAC5B,IAAI,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;AAEhC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACvC,gBAAA,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC;AACxB,gBAAA,IAAI,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE;AAC5B,oBAAA,UAAU,GAAG,KAAK,CAAC,IAAI;oBACvB,WAAW,GAAG,KAAK;gBACrB;YACF;YAEA,UAAU,CAAC,GAAG,CAAC;gBACb,SAAS,EAAE,WAAW,CAAC,cAAc;gBACrC,KAAK,EAAE,WAAW,CAAC,iBAAiB;AACrC,aAAA,CAAC;AACJ,QAAA,CAAC;AAED,QAAA,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAErE,QAAA,YAAY,CAAC,MAAM,EAAE,MAAM,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAExD,QAAA,OAAO,UAAU;AACnB,IAAA,CAAC,CAAC;AACJ;AAEA,MAAM,kBAAkB,GAA2B;AACjD,IAAA,SAAS,EAAE,IAAI;AACf,IAAA,KAAK,EAAE,CAAC;CACT;;ACzID;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-elements-element-visibility.mjs","sources":["../../../projects/core/elements/element-visibility/index.ts","../../../projects/core/elements/element-visibility/signality-core-elements-element-visibility.ts"],"sourcesContent":["import { type CreateSignalOptions, type Signal, signal } from '@angular/core';\nimport { constSignal, setupContext } from '@signality/core/internal';\nimport type { MaybeElementSignal, MaybeSignal, WithInjector } from '@signality/core/types';\nimport { intersectionObserver } from '@signality/core/observers/intersection-observer';\nimport { onDisconnect } from '@signality/core/elements/on-disconnect';\n\nexport interface ElementVisibilityOptions\n extends CreateSignalOptions<ElementVisibilityValue>,\n WithInjector {\n /**\n * Fraction of the element that must be visible to trigger a change.\n * A single number or an array of thresholds, each between `0` and `1`.\n *\n * @default 0\n * @see [IntersectionObserver: thresholds on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds)\n */\n readonly threshold?: MaybeSignal<number | number[]>;\n\n /**\n * Scrollable ancestor used as the viewport for intersection checks.\n * `null` or `undefined` defaults to the browser viewport.\n *\n * @default undefined\n * @see [IntersectionObserver: root on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)\n */\n readonly root?: MaybeElementSignal<Element> | Document;\n\n /**\n * CSS margin applied around the root before computing intersections.\n * Accepts values in the same format as the CSS `margin` property (e.g. `'10px 0px'`).\n *\n * @default '0px'\n * @see [IntersectionObserver: rootMargin on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)\n */\n readonly rootMargin?: MaybeSignal<string>;\n\n /**\n * Initial value for SSR.\n *\n * @default { isVisible: true, ratio: 1 }\n */\n readonly initialValue?: ElementVisibilityValue;\n}\n\nexport interface ElementVisibilityValue {\n /**\n * Whether the element is currently intersecting the root (visible in the viewport).\n *\n * @see [IntersectionObserverEntry: isIntersecting on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting)\n */\n readonly isVisible: boolean;\n\n /**\n * Fraction of the element visible within the root, from `0.0` (not visible) to `1.0` (fully visible).\n *\n * @see [IntersectionObserverEntry: intersectionRatio on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/intersectionRatio)\n */\n readonly ratio: number;\n}\n\n/**\n * Signal-based wrapper around the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).\n *\n * @param target - The element to observe\n * @param options - Optional configuration\n * @returns A signal containing the current visibility state\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div #section [class.visible]=\"visibility().isVisible\">\n * Visibility: {{ visibility().ratio * 100 }}%\n * </div>\n * `\n * })\n * export class VisibilityDemo {\n * readonly section = viewChild<ElementRef>('section');\n * readonly visibility = elementVisibility(this.section);\n * }\n * ```\n */\nexport function elementVisibility(\n target: MaybeElementSignal<HTMLElement>,\n options?: ElementVisibilityOptions\n): Signal<ElementVisibilityValue> {\n const { runInContext } = setupContext(options?.injector, elementVisibility);\n const initialValue = options?.initialValue ?? { isVisible: true, ratio: 1 };\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return constSignal(initialValue);\n }\n\n const visibility = signal(initialValue, options);\n\n const threshold = options?.threshold ?? 0;\n const root = options?.root ?? undefined;\n const rootMargin = options?.rootMargin ?? '0px';\n\n const update = (entries: readonly IntersectionObserverEntry[]) => {\n if (entries.length === 0) {\n return;\n }\n\n // Find the entry with the latest time to ensure we use the most up-to-date state\n // IntersectionObserver may batch multiple changes and call the callback once\n // with multiple entries, and the order in the array doesn't guarantee\n // that the last entry is the most recent one\n let latestEntry = entries[0];\n let latestTime = entries[0].time;\n\n for (let i = 1; i < entries.length; i++) {\n const entry = entries[i];\n if (entry.time >= latestTime) {\n latestTime = entry.time;\n latestEntry = entry;\n }\n }\n\n visibility.set({\n isVisible: latestEntry.isIntersecting,\n ratio: latestEntry.intersectionRatio,\n });\n };\n\n intersectionObserver(target, update, { threshold, root, rootMargin });\n\n onDisconnect(target, () => visibility.set({ isVisible: false, ratio: 0 }));\n\n return visibility;\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA4DA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACG,SAAU,iBAAiB,CAC/B,MAAuC,EACvC,OAAkC,EAAA;AAElC,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,CAAC;AAC3E,IAAA,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE;AAE3E,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,YAAY,CAAC;QAClC;QAEA,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC;AAEhD,QAAA,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,CAAC;AACzC,QAAA,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,SAAS;AACvC,QAAA,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,KAAK;AAE/C,QAAA,MAAM,MAAM,GAAG,CAAC,OAA6C,KAAI;AAC/D,YAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE;gBACxB;YACF;;;;;AAMA,YAAA,IAAI,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;YAC5B,IAAI,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;AAEhC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACvC,gBAAA,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC;AACxB,gBAAA,IAAI,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE;AAC5B,oBAAA,UAAU,GAAG,KAAK,CAAC,IAAI;oBACvB,WAAW,GAAG,KAAK;gBACrB;YACF;YAEA,UAAU,CAAC,GAAG,CAAC;gBACb,SAAS,EAAE,WAAW,CAAC,cAAc;gBACrC,KAAK,EAAE,WAAW,CAAC,iBAAiB;AACrC,aAAA,CAAC;AACJ,QAAA,CAAC;AAED,QAAA,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAErE,YAAY,CAAC,MAAM,EAAE,MAAM,UAAU,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAE1E,QAAA,OAAO,UAAU;AACnB,IAAA,CAAC,CAAC;AACJ;;ACpIA;;AAEG;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signality/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "author": "Vyacheslav Borodin <https://github.com/vs-borodin>",
6
6
  "description": "A foundational toolkit for Angular Signals",
@@ -288,14 +288,14 @@
288
288
  "types": "./observers/resize-observer/index.d.ts",
289
289
  "default": "./fesm2022/signality-core-observers-resize-observer.mjs"
290
290
  },
291
- "./reactivity/debounced": {
292
- "types": "./reactivity/debounced/index.d.ts",
293
- "default": "./fesm2022/signality-core-reactivity-debounced.mjs"
294
- },
295
291
  "./reactivity/throttled": {
296
292
  "types": "./reactivity/throttled/index.d.ts",
297
293
  "default": "./fesm2022/signality-core-reactivity-throttled.mjs"
298
294
  },
295
+ "./reactivity/debounced": {
296
+ "types": "./reactivity/debounced/index.d.ts",
297
+ "default": "./fesm2022/signality-core-reactivity-debounced.mjs"
298
+ },
299
299
  "./reactivity/watcher": {
300
300
  "types": "./reactivity/watcher/index.d.ts",
301
301
  "default": "./fesm2022/signality-core-reactivity-watcher.mjs"