@signality/core 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/browser/device-pixel-ratio/index.d.ts +30 -0
  2. package/browser/index.d.ts +1 -0
  3. package/browser/listener/index.d.ts +1 -0
  4. package/browser/picture-in-picture/index.d.ts +1 -5
  5. package/browser/web-notification/index.d.ts +3 -3
  6. package/fesm2022/signality-core-browser-clipboard.mjs +11 -29
  7. package/fesm2022/signality-core-browser-clipboard.mjs.map +1 -1
  8. package/fesm2022/signality-core-browser-device-pixel-ratio.mjs +45 -0
  9. package/fesm2022/signality-core-browser-device-pixel-ratio.mjs.map +1 -0
  10. package/fesm2022/signality-core-browser-eye-dropper.mjs +2 -3
  11. package/fesm2022/signality-core-browser-eye-dropper.mjs.map +1 -1
  12. package/fesm2022/signality-core-browser-fullscreen.mjs +7 -19
  13. package/fesm2022/signality-core-browser-fullscreen.mjs.map +1 -1
  14. package/fesm2022/signality-core-browser-gamepad.mjs +2 -10
  15. package/fesm2022/signality-core-browser-gamepad.mjs.map +1 -1
  16. package/fesm2022/signality-core-browser-listener.mjs.map +1 -1
  17. package/fesm2022/signality-core-browser-picture-in-picture.mjs +8 -12
  18. package/fesm2022/signality-core-browser-picture-in-picture.mjs.map +1 -1
  19. package/fesm2022/signality-core-browser-storage.mjs +19 -44
  20. package/fesm2022/signality-core-browser-storage.mjs.map +1 -1
  21. package/fesm2022/signality-core-browser-vibration.mjs +14 -29
  22. package/fesm2022/signality-core-browser-vibration.mjs.map +1 -1
  23. package/fesm2022/signality-core-browser-web-notification.mjs +32 -53
  24. package/fesm2022/signality-core-browser-web-notification.mjs.map +1 -1
  25. package/fesm2022/signality-core-browser-web-share.mjs +3 -11
  26. package/fesm2022/signality-core-browser-web-share.mjs.map +1 -1
  27. package/fesm2022/signality-core-browser.mjs +1 -0
  28. package/fesm2022/signality-core-browser.mjs.map +1 -1
  29. package/fesm2022/signality-core-forms-cva.mjs +13 -3
  30. package/fesm2022/signality-core-forms-cva.mjs.map +1 -1
  31. package/fesm2022/signality-core-internal.mjs +34 -2
  32. package/fesm2022/signality-core-internal.mjs.map +1 -1
  33. package/forms/cva/index.d.ts +10 -5
  34. package/internal/utils/index.d.ts +1 -0
  35. package/internal/utils/wait-for-value.d.ts +6 -0
  36. package/package.json +5 -1
@@ -57,22 +57,14 @@ function storage(key, initialValue, options) {
57
57
  return storedValue;
58
58
  };
59
59
  const readValue = (storageKey) => {
60
- try {
61
- const raw = targetStorage.getItem(storageKey);
62
- if (raw === null) {
63
- if (initialValue != null) {
64
- writeValue(initialValue);
65
- }
66
- return initialValue;
67
- }
68
- return processValue(serializer.read(raw));
69
- }
70
- catch (error) {
71
- if (ngDevMode) {
72
- console.warn(`[storage] Failed to deserialize value for key "${key}"`, error);
60
+ const raw = targetStorage.getItem(storageKey);
61
+ if (raw === null) {
62
+ if (initialValue != null) {
63
+ writeValue(initialValue);
73
64
  }
74
65
  return initialValue;
75
66
  }
67
+ return processValue(serializer.read(raw));
76
68
  };
77
69
  const dispatchStorageEvent = (key, oldValue, newValue) => {
78
70
  window.dispatchEvent(new StorageEvent('storage', {
@@ -85,43 +77,26 @@ function storage(key, initialValue, options) {
85
77
  };
86
78
  const writeValue = (value) => {
87
79
  const storageKey = toValue(key);
88
- try {
89
- const oldValue = targetStorage.getItem(storageKey);
90
- if (value == null) {
91
- targetStorage.removeItem(storageKey);
92
- dispatchStorageEvent(storageKey, oldValue, null);
93
- }
94
- else {
95
- const serialized = serializer.write(value);
96
- if (oldValue !== serialized) {
97
- targetStorage.setItem(storageKey, serialized);
98
- dispatchStorageEvent(storageKey, oldValue, serialized);
99
- }
100
- }
80
+ const oldValue = targetStorage.getItem(storageKey);
81
+ if (value == null) {
82
+ targetStorage.removeItem(storageKey);
83
+ dispatchStorageEvent(storageKey, oldValue, null);
101
84
  }
102
- catch (error) {
103
- if (ngDevMode) {
104
- console.warn(`[storage] Failed to write value for key "${storageKey}". ` +
105
- `This may be due to storage quota exceeded or serialization error.`, error);
85
+ else {
86
+ const serialized = serializer.write(value);
87
+ if (oldValue !== serialized) {
88
+ targetStorage.setItem(storageKey, serialized);
89
+ dispatchStorageEvent(storageKey, oldValue, serialized);
106
90
  }
107
91
  }
108
92
  };
109
93
  const state = signal(readValue(toValue(key)), options);
110
94
  setupSync(() => {
111
- listener(window, 'storage', event => {
112
- const currentKey = toValue(key);
113
- if (event.key === currentKey && event.storageArea === targetStorage) {
114
- try {
115
- const newValue = event.newValue === null
116
- ? initialValue
117
- : processValue(serializer.read(event.newValue));
118
- state.set(newValue);
119
- }
120
- catch (error) {
121
- if (ngDevMode) {
122
- console.warn(`[storage] Failed to sync value from other tab for key "${event.key}"`, error);
123
- }
124
- }
95
+ listener(window, 'storage', e => {
96
+ const currKey = toValue(key);
97
+ if (e.key === currKey && e.storageArea === targetStorage) {
98
+ const newValue = e.newValue === null ? initialValue : processValue(serializer.read(e.newValue));
99
+ state.set(newValue);
125
100
  }
126
101
  });
127
102
  });
@@ -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 } from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\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 const type = options?.type ?? 'local';\n\n if (isServer || !storageAvailable(type)) {\n return signal(initialValue, options);\n }\n\n const targetStorage = type === 'local' ? window.localStorage : window.sessionStorage;\n const serializer = resolveSerializer(initialValue, options);\n\n const processValue = (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 try {\n const raw = targetStorage.getItem(storageKey);\n\n if (raw === null) {\n if (initialValue != null) {\n writeValue(initialValue);\n }\n return initialValue;\n }\n\n return processValue(serializer.read(raw));\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[storage] Failed to deserialize value for key \"${key}\"`, error);\n }\n return initialValue;\n }\n };\n\n const dispatchStorageEvent = (\n key: string,\n oldValue: string | null,\n newValue: string | null\n ) => {\n window.dispatchEvent(\n new StorageEvent('storage', {\n key,\n oldValue,\n newValue,\n storageArea: targetStorage,\n url: window.location.href,\n })\n );\n };\n\n const writeValue = (value: T): void => {\n const storageKey = toValue(key);\n\n try {\n const oldValue = targetStorage.getItem(storageKey);\n\n if (value == null) {\n targetStorage.removeItem(storageKey);\n dispatchStorageEvent(storageKey, oldValue, null);\n } else {\n const serialized = serializer.write(value);\n if (oldValue !== serialized) {\n targetStorage.setItem(storageKey, serialized);\n dispatchStorageEvent(storageKey, oldValue, serialized);\n }\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 setupSync(() => {\n listener(window, 'storage', event => {\n const currentKey = toValue(key);\n\n if (event.key === currentKey && event.storageArea === targetStorage) {\n try {\n const newValue =\n event.newValue === null\n ? initialValue\n : processValue(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 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: 'local' | 'session'): boolean {\n let storage: Storage | undefined;\n\n try {\n storage = window[`${type}Storage`];\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 } from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\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 const type = options?.type ?? 'local';\n\n if (isServer || !storageAvailable(type)) {\n return signal(initialValue, options);\n }\n\n const targetStorage = type === 'local' ? window.localStorage : window.sessionStorage;\n const serializer = resolveSerializer(initialValue, options);\n\n const processValue = (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 raw = targetStorage.getItem(storageKey);\n\n if (raw === null) {\n if (initialValue != null) {\n writeValue(initialValue);\n }\n return initialValue;\n }\n\n return processValue(serializer.read(raw));\n };\n\n const dispatchStorageEvent = (\n key: string,\n oldValue: string | null,\n newValue: string | null\n ) => {\n window.dispatchEvent(\n new StorageEvent('storage', {\n key,\n oldValue,\n newValue,\n storageArea: targetStorage,\n url: window.location.href,\n })\n );\n };\n\n const writeValue = (value: T): void => {\n const storageKey = toValue(key);\n const oldValue = targetStorage.getItem(storageKey);\n\n if (value == null) {\n targetStorage.removeItem(storageKey);\n dispatchStorageEvent(storageKey, oldValue, null);\n } else {\n const serialized = serializer.write(value);\n if (oldValue !== serialized) {\n targetStorage.setItem(storageKey, serialized);\n dispatchStorageEvent(storageKey, oldValue, serialized);\n }\n }\n };\n\n const state = signal<T>(readValue(toValue(key)), options);\n\n setupSync(() => {\n listener(window, 'storage', e => {\n const currKey = toValue(key);\n\n if (e.key === currKey && e.storageArea === targetStorage) {\n const newValue =\n e.newValue === null ? initialValue : processValue(serializer.read(e.newValue));\n\n state.set(newValue);\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: 'local' | 'session'): boolean {\n let storage: Storage | undefined;\n\n try {\n storage = window[`${type}Storage`];\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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"}
@@ -48,39 +48,24 @@ function vibration(options) {
48
48
  let vibrateTimeout;
49
49
  const vibrate = (vibratePattern) => {
50
50
  const resolvedPattern = vibratePattern ?? toValue.untracked(options?.pattern) ?? 200;
51
- try {
52
- const result = navigator.vibrate(resolvedPattern);
53
- isVibrating.set(result);
54
- const duration = Array.isArray(resolvedPattern)
55
- ? resolvedPattern.reduce((a, b) => a + b, 0)
56
- : resolvedPattern;
57
- if (result && duration > 0) {
58
- if (vibrateTimeout) {
59
- clearTimeout(vibrateTimeout);
60
- }
61
- vibrateTimeout = setTimeout(() => {
62
- isVibrating.set(false);
63
- vibrateTimeout = undefined;
64
- }, duration);
65
- }
66
- }
67
- catch (error) {
68
- isVibrating.set(false);
69
- if (ngDevMode) {
70
- console.warn(`[vibration] Failed to vibrate device.`, error);
51
+ const result = navigator.vibrate(resolvedPattern);
52
+ isVibrating.set(result);
53
+ const duration = Array.isArray(resolvedPattern)
54
+ ? resolvedPattern.reduce((a, b) => a + b, 0)
55
+ : resolvedPattern;
56
+ if (result && duration > 0) {
57
+ if (vibrateTimeout) {
58
+ clearTimeout(vibrateTimeout);
71
59
  }
60
+ vibrateTimeout = setTimeout(() => {
61
+ isVibrating.set(false);
62
+ vibrateTimeout = undefined;
63
+ }, duration);
72
64
  }
73
65
  };
74
66
  const stop = () => {
75
- try {
76
- navigator.vibrate(0);
77
- isVibrating.set(false);
78
- }
79
- catch (error) {
80
- if (ngDevMode) {
81
- console.warn(`[vibration] Failed to stop vibration.`, error);
82
- }
83
- }
67
+ navigator.vibrate(0);
68
+ isVibrating.set(false);
84
69
  if (vibrateTimeout) {
85
70
  clearTimeout(vibrateTimeout);
86
71
  vibrateTimeout = undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-vibration.mjs","sources":["../../../projects/core/browser/vibration/index.ts","../../../projects/core/browser/vibration/signality-core-browser-vibration.ts"],"sourcesContent":["import { type Signal, signal } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext, type Timer } from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\n\nexport interface VibrationOptions extends WithInjector {\n /**\n * Default vibration pattern in milliseconds. A single number vibrates for that duration;\n * an array alternates between vibration and pause durations.\n *\n * @see [Navigator: vibrate() pattern on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate#pattern)\n */\n readonly pattern?: MaybeSignal<number | number[]>;\n}\n\nexport interface VibrationRef {\n /**\n * Whether the Vibration API is supported in the current browser.\n *\n * @see [Vibration API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the device is currently vibrating.\n */\n readonly isVibrating: Signal<boolean>;\n\n /**\n * Trigger vibration with an optional pattern. Falls back to the default pattern from options, then `200ms`.\n *\n * @see [Navigator: vibrate() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)\n */\n readonly vibrate: (pattern?: number | number[]) => void;\n\n /**\n * Stop any active vibration by calling `navigator.vibrate(0)`.\n *\n * @see [Navigator: vibrate() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)\n */\n readonly stop: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API).\n *\n * @param options - Optional configuration including default pattern and injector\n * @returns A VibrationRef with vibration control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (vib.isSupported()) {\n * <button (click)=\"vib.vibrate(100)\">Vibrate 100ms</button>\n * <button (click)=\"vib.vibrate([100, 50, 100])\">Pattern</button>\n * <button (click)=\"vib.stop()\">Stop</button>\n * }\n * `\n * })\n * export class VibrationDemo {\n * readonly vib = vibration();\n * }\n * ```\n *\n * @example\n * ```typescript\n * // With a default pattern\n * const vib = vibration({ pattern: 200 });\n *\n * vib.vibrate(); // Uses default 200ms pattern\n * ```\n */\nexport function vibration(options?: VibrationOptions): VibrationRef {\n const { runInContext } = setupContext(options?.injector, vibration);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'vibrate' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isVibrating: constSignal(false),\n vibrate: NOOP_FN,\n stop: NOOP_FN,\n };\n }\n\n const isVibrating = signal(false);\n\n let vibrateTimeout: Timer;\n\n const vibrate = (vibratePattern?: number | number[]) => {\n const resolvedPattern = vibratePattern ?? toValue.untracked(options?.pattern) ?? 200;\n\n try {\n const result = navigator.vibrate(resolvedPattern);\n\n isVibrating.set(result);\n\n const duration = Array.isArray(resolvedPattern)\n ? resolvedPattern.reduce((a, b) => a + b, 0)\n : resolvedPattern;\n\n if (result && duration > 0) {\n if (vibrateTimeout) {\n clearTimeout(vibrateTimeout);\n }\n\n vibrateTimeout = setTimeout(() => {\n isVibrating.set(false);\n vibrateTimeout = undefined;\n }, duration);\n }\n } catch (error) {\n isVibrating.set(false);\n if (ngDevMode) {\n console.warn(`[vibration] Failed to vibrate device.`, error);\n }\n }\n };\n\n const stop = () => {\n try {\n navigator.vibrate(0);\n isVibrating.set(false);\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[vibration] Failed to stop vibration.`, error);\n }\n }\n\n if (vibrateTimeout) {\n clearTimeout(vibrateTimeout);\n vibrateTimeout = undefined;\n }\n };\n\n onCleanup(stop);\n\n return {\n isSupported,\n isVibrating: isVibrating.asReadonly(),\n vibrate,\n stop,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AA2CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BG;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,SAAS,EAAE,KAAI;QAC/C,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,SAAS,IAAI,SAAS,CAAC;AAEpE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC;AAC/B,gBAAA,OAAO,EAAE,OAAO;AAChB,gBAAA,IAAI,EAAE,OAAO;aACd;QACH;AAEA,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,uDAAC;AAEjC,QAAA,IAAI,cAAqB;AAEzB,QAAA,MAAM,OAAO,GAAG,CAAC,cAAkC,KAAI;AACrD,YAAA,MAAM,eAAe,GAAG,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,GAAG;AAEpF,YAAA,IAAI;gBACF,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,CAAC;AAEjD,gBAAA,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC;AAEvB,gBAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe;AAC5C,sBAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;sBACzC,eAAe;AAEnB,gBAAA,IAAI,MAAM,IAAI,QAAQ,GAAG,CAAC,EAAE;oBAC1B,IAAI,cAAc,EAAE;wBAClB,YAAY,CAAC,cAAc,CAAC;oBAC9B;AAEA,oBAAA,cAAc,GAAG,UAAU,CAAC,MAAK;AAC/B,wBAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;wBACtB,cAAc,GAAG,SAAS;oBAC5B,CAAC,EAAE,QAAQ,CAAC;gBACd;YACF;YAAE,OAAO,KAAK,EAAE;AACd,gBAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;gBACtB,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,KAAK,CAAC;gBAC9D;YACF;AACF,QAAA,CAAC;QAED,MAAM,IAAI,GAAG,MAAK;AAChB,YAAA,IAAI;AACF,gBAAA,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;AACpB,gBAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;YACxB;YAAE,OAAO,KAAK,EAAE;gBACd,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,KAAK,CAAC;gBAC9D;YACF;YAEA,IAAI,cAAc,EAAE;gBAClB,YAAY,CAAC,cAAc,CAAC;gBAC5B,cAAc,GAAG,SAAS;YAC5B;AACF,QAAA,CAAC;QAED,SAAS,CAAC,IAAI,CAAC;QAEf,OAAO;YACL,WAAW;AACX,YAAA,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE;YACrC,OAAO;YACP,IAAI;SACL;AACH,IAAA,CAAC,CAAC;AACJ;;ACnJA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-browser-vibration.mjs","sources":["../../../projects/core/browser/vibration/index.ts","../../../projects/core/browser/vibration/signality-core-browser-vibration.ts"],"sourcesContent":["import { type Signal, signal } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext, type Timer } from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\n\nexport interface VibrationOptions extends WithInjector {\n /**\n * Default vibration pattern in milliseconds. A single number vibrates for that duration;\n * an array alternates between vibration and pause durations.\n *\n * @see [Navigator: vibrate() pattern on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate#pattern)\n */\n readonly pattern?: MaybeSignal<number | number[]>;\n}\n\nexport interface VibrationRef {\n /**\n * Whether the Vibration API is supported in the current browser.\n *\n * @see [Vibration API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the device is currently vibrating.\n */\n readonly isVibrating: Signal<boolean>;\n\n /**\n * Trigger vibration with an optional pattern. Falls back to the default pattern from options, then `200ms`.\n *\n * @see [Navigator: vibrate() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)\n */\n readonly vibrate: (pattern?: number | number[]) => void;\n\n /**\n * Stop any active vibration by calling `navigator.vibrate(0)`.\n *\n * @see [Navigator: vibrate() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)\n */\n readonly stop: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API).\n *\n * @param options - Optional configuration including default pattern and injector\n * @returns A VibrationRef with vibration control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (vib.isSupported()) {\n * <button (click)=\"vib.vibrate(100)\">Vibrate 100ms</button>\n * <button (click)=\"vib.vibrate([100, 50, 100])\">Pattern</button>\n * <button (click)=\"vib.stop()\">Stop</button>\n * }\n * `\n * })\n * export class VibrationDemo {\n * readonly vib = vibration();\n * }\n * ```\n *\n * @example\n * ```typescript\n * // With a default pattern\n * const vib = vibration({ pattern: 200 });\n *\n * vib.vibrate(); // Uses default 200ms pattern\n * ```\n */\nexport function vibration(options?: VibrationOptions): VibrationRef {\n const { runInContext } = setupContext(options?.injector, vibration);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'vibrate' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isVibrating: constSignal(false),\n vibrate: NOOP_FN,\n stop: NOOP_FN,\n };\n }\n\n const isVibrating = signal(false);\n\n let vibrateTimeout: Timer;\n\n const vibrate = (vibratePattern?: number | number[]) => {\n const resolvedPattern = vibratePattern ?? toValue.untracked(options?.pattern) ?? 200;\n\n const result = navigator.vibrate(resolvedPattern);\n\n isVibrating.set(result);\n\n const duration = Array.isArray(resolvedPattern)\n ? resolvedPattern.reduce((a, b) => a + b, 0)\n : resolvedPattern;\n\n if (result && duration > 0) {\n if (vibrateTimeout) {\n clearTimeout(vibrateTimeout);\n }\n\n vibrateTimeout = setTimeout(() => {\n isVibrating.set(false);\n vibrateTimeout = undefined;\n }, duration);\n }\n };\n\n const stop = () => {\n navigator.vibrate(0);\n isVibrating.set(false);\n\n if (vibrateTimeout) {\n clearTimeout(vibrateTimeout);\n vibrateTimeout = undefined;\n }\n };\n\n onCleanup(stop);\n\n return {\n isSupported,\n isVibrating: isVibrating.asReadonly(),\n vibrate,\n stop,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AA2CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BG;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,SAAS,EAAE,KAAI;QAC/C,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,SAAS,IAAI,SAAS,CAAC;AAEpE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC;AAC/B,gBAAA,OAAO,EAAE,OAAO;AAChB,gBAAA,IAAI,EAAE,OAAO;aACd;QACH;AAEA,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,uDAAC;AAEjC,QAAA,IAAI,cAAqB;AAEzB,QAAA,MAAM,OAAO,GAAG,CAAC,cAAkC,KAAI;AACrD,YAAA,MAAM,eAAe,GAAG,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,GAAG;YAEpF,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,CAAC;AAEjD,YAAA,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC;AAEvB,YAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe;AAC5C,kBAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;kBACzC,eAAe;AAEnB,YAAA,IAAI,MAAM,IAAI,QAAQ,GAAG,CAAC,EAAE;gBAC1B,IAAI,cAAc,EAAE;oBAClB,YAAY,CAAC,cAAc,CAAC;gBAC9B;AAEA,gBAAA,cAAc,GAAG,UAAU,CAAC,MAAK;AAC/B,oBAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;oBACtB,cAAc,GAAG,SAAS;gBAC5B,CAAC,EAAE,QAAQ,CAAC;YACd;AACF,QAAA,CAAC;QAED,MAAM,IAAI,GAAG,MAAK;AAChB,YAAA,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;AACpB,YAAA,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;YAEtB,IAAI,cAAc,EAAE;gBAClB,YAAY,CAAC,cAAc,CAAC;gBAC5B,cAAc,GAAG,SAAS;YAC5B;AACF,QAAA,CAAC;QAED,SAAS,CAAC,IAAI,CAAC;QAEf,OAAO;YACL,WAAW;AACX,YAAA,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE;YACrC,OAAO;YACP,IAAI;SACL;AACH,IAAA,CAAC,CAAC;AACJ;;ACtIA;;AAEG;;;;"}
@@ -1,6 +1,8 @@
1
1
  import { signal, untracked } from '@angular/core';
2
- import { setupContext, constSignal, NOOP_FN } from '@signality/core/internal';
2
+ import { setupContext, constSignal, NOOP_FN, NOOP_ASYNC_FN } from '@signality/core/internal';
3
3
  import { toValue } from '@signality/core/utilities';
4
+ import { watcher } from '@signality/core/reactivity';
5
+ import { permissionState } from '@signality/core/browser/permission-state';
4
6
 
5
7
  /**
6
8
  * Signal-based wrapper around the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API).
@@ -46,77 +48,54 @@ function webNotification(options) {
46
48
  isSupported,
47
49
  permission: constSignal('denied'),
48
50
  notification: constSignal(null),
49
- requestPermission: async () => 'denied',
50
- show: () => undefined,
51
+ requestPermission: NOOP_ASYNC_FN,
52
+ show: NOOP_FN,
51
53
  close: NOOP_FN,
52
54
  };
53
55
  }
54
56
  const permission = signal(Notification.permission, ...(ngDevMode ? [{ debugName: "permission" }] : []));
55
57
  const notification = signal(null, ...(ngDevMode ? [{ debugName: "notification" }] : []));
56
58
  let autoCloseTimeout;
57
- const clearAutoClose = () => {
59
+ const cancelAutoClose = () => {
58
60
  clearTimeout(autoCloseTimeout);
59
61
  autoCloseTimeout = undefined;
60
62
  };
61
63
  const requestPermission = async () => {
62
- try {
63
- const result = await Notification.requestPermission();
64
- permission.set(result);
65
- return result;
66
- }
67
- catch (error) {
68
- if (ngDevMode) {
69
- console.warn(`[webNotification] Failed to request notification permission.`, error);
70
- }
71
- return permission();
72
- }
64
+ const result = await Notification.requestPermission();
65
+ permission.set(result);
73
66
  };
74
67
  const close = () => {
75
- clearAutoClose();
76
- const current = notification();
68
+ cancelAutoClose();
69
+ const current = untracked(notification);
77
70
  if (current) {
78
- try {
79
- current.onclose = null;
80
- current.close();
81
- }
82
- catch (error) {
83
- if (ngDevMode) {
84
- console.warn(`[webNotification] Failed to close notification.`, error);
85
- }
86
- }
71
+ current.onclose = null;
72
+ current.close();
87
73
  notification.set(null);
88
74
  }
89
75
  };
90
76
  const show = (title, overrides) => {
91
- return untracked(() => {
92
- if (permission() !== 'granted') {
93
- return undefined;
94
- }
95
- close();
96
- try {
97
- const { autoClose, ...defaults } = options ?? {};
98
- const mergedOptions = { ...defaults, ...overrides };
99
- const instance = new Notification(title, mergedOptions);
100
- instance.onclose = () => {
101
- clearAutoClose();
102
- notification.set(null);
103
- };
104
- notification.set(instance);
105
- const autoCloseMs = toValue(autoClose);
106
- if (autoCloseMs && autoCloseMs > 0) {
107
- autoCloseTimeout = setTimeout(close, autoCloseMs);
108
- }
109
- return instance;
110
- }
111
- catch (error) {
112
- if (ngDevMode) {
113
- console.warn(`[webNotification] Failed to show notification.`, error);
114
- }
115
- return undefined;
116
- }
117
- });
77
+ if (untracked(permission) !== 'granted') {
78
+ return;
79
+ }
80
+ const { autoClose, ...defaultOptions } = options ?? {};
81
+ const instance = new Notification(title, { ...defaultOptions, ...overrides });
82
+ instance.onclose = () => {
83
+ cancelAutoClose();
84
+ notification.set(null);
85
+ };
86
+ notification.set(instance);
87
+ const autoCloseMs = toValue(autoClose);
88
+ if (autoCloseMs && autoCloseMs > 0) {
89
+ autoCloseTimeout = setTimeout(close, autoCloseMs);
90
+ }
118
91
  };
119
92
  onCleanup(close);
93
+ watcher(permissionState('notifications'), state => {
94
+ permission.set(state === 'prompt' ? 'default' : state);
95
+ if (state === 'denied') {
96
+ close();
97
+ }
98
+ });
120
99
  return {
121
100
  isSupported,
122
101
  permission: permission.asReadonly(),
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-web-notification.mjs","sources":["../../../projects/core/browser/web-notification/index.ts","../../../projects/core/browser/web-notification/signality-core-browser-web-notification.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_FN, setupContext, type Timer } from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\n\nexport interface WebNotificationOptions extends NotificationOptions, WithInjector {\n /**\n * Auto-close the notification after the specified number of milliseconds.\n *\n * @see [Notification: close() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/close)\n */\n readonly autoClose?: MaybeSignal<number>;\n}\n\nexport interface WebNotificationRef {\n /**\n * Whether the Notifications API is supported in the current browser.\n *\n * @see [Notifications API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Current notification permission state: `'granted'`, `'denied'`, or `'default'`.\n *\n * @see [Notification: permission static property on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/permission_static)\n */\n readonly permission: Signal<NotificationPermission>;\n\n /**\n * The currently active notification instance, or `null` if none is shown.\n *\n * @see [Notification on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification)\n */\n readonly notification: Signal<Notification | null>;\n\n /**\n * Request permission to show notifications.\n *\n * @see [Notification: requestPermission() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission_static)\n */\n readonly requestPermission: () => Promise<NotificationPermission>;\n\n /**\n * Show a notification. Automatically closes the previous one. Per-call options override constructor defaults.\n *\n * @see [Notification() constructor on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification)\n */\n readonly show: (title: string, options?: NotificationOptions) => Notification | undefined;\n\n /**\n * Close the currently active notification.\n *\n * @see [Notification: close() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/close)\n */\n readonly close: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API).\n *\n * Tracks the current notification as a signal. Calling `show()` auto-closes\n * the previous notification. Calling `close()` closes the current one.\n *\n * @param options - Optional configuration and default notification options\n * @returns A WebNotificationRef with notification state and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"requestPermission()\">Request Permission</button>\n * <button (click)=\"showNotification()\" [disabled]=\"notif.permission() !== 'granted'\">\n * Show Notification\n * </button>\n * @if (notif.notification()) {\n * <button (click)=\"notif.close()\">Close</button>\n * }\n * `\n * })\n * export class NotificationDemo {\n * readonly notif = webNotification({ icon: '/icon.png' });\n *\n * async requestPermission() {\n * await this.notif.requestPermission();\n * }\n *\n * showNotification() {\n * this.notif.show('Hello!', { body: 'This is a notification' });\n * }\n * }\n * ```\n */\nexport function webNotification(options?: WebNotificationOptions): WebNotificationRef {\n const { runInContext } = setupContext(options?.injector, webNotification);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'Notification' in window);\n\n if (!isSupported()) {\n return {\n isSupported,\n permission: constSignal('denied'),\n notification: constSignal(null),\n requestPermission: async () => 'denied',\n show: () => undefined,\n close: NOOP_FN,\n };\n }\n\n const permission = signal(Notification.permission);\n const notification = signal<Notification | null>(null);\n\n let autoCloseTimeout: Timer;\n\n const clearAutoClose = () => {\n clearTimeout(autoCloseTimeout);\n autoCloseTimeout = undefined;\n };\n\n const requestPermission = async (): Promise<NotificationPermission> => {\n try {\n const result = await Notification.requestPermission();\n permission.set(result);\n return result;\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[webNotification] Failed to request notification permission.`, error);\n }\n return permission();\n }\n };\n\n const close = (): void => {\n clearAutoClose();\n\n const current = notification();\n\n if (current) {\n try {\n current.onclose = null;\n current.close();\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[webNotification] Failed to close notification.`, error);\n }\n }\n notification.set(null);\n }\n };\n\n const show = (title: string, overrides?: NotificationOptions): Notification | undefined => {\n return untracked(() => {\n if (permission() !== 'granted') {\n return undefined;\n }\n\n close();\n\n try {\n const { autoClose, ...defaults } = options ?? {};\n const mergedOptions = { ...defaults, ...overrides };\n\n const instance = new Notification(title, mergedOptions);\n\n instance.onclose = () => {\n clearAutoClose();\n notification.set(null);\n };\n\n notification.set(instance);\n\n const autoCloseMs = toValue(autoClose);\n if (autoCloseMs && autoCloseMs > 0) {\n autoCloseTimeout = setTimeout(close, autoCloseMs);\n }\n\n return instance;\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[webNotification] Failed to show notification.`, error);\n }\n return undefined;\n }\n });\n };\n\n onCleanup(close);\n\n return {\n isSupported,\n permission: permission.asReadonly(),\n notification: notification.asReadonly(),\n requestPermission,\n show,\n close,\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-web-notification.mjs","sources":["../../../projects/core/browser/web-notification/index.ts","../../../projects/core/browser/web-notification/signality-core-browser-web-notification.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport {\n constSignal,\n NOOP_ASYNC_FN,\n NOOP_FN,\n setupContext,\n type Timer,\n} from '@signality/core/internal';\nimport { toValue } from '@signality/core/utilities';\nimport type { MaybeSignal, WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity';\nimport { permissionState } from '@signality/core/browser/permission-state';\n\nexport interface WebNotificationOptions extends NotificationOptions, WithInjector {\n /**\n * Auto-close the notification after the specified number of milliseconds.\n *\n * @see [Notification: close() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/close)\n */\n readonly autoClose?: MaybeSignal<number>;\n}\n\nexport interface WebNotificationRef {\n /**\n * Whether the Notifications API is supported in the current browser.\n *\n * @see [Notifications API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Current notification permission state: `'granted'`, `'denied'`, or `'default'`.\n *\n * @see [Notification: permission static property on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/permission_static)\n */\n readonly permission: Signal<NotificationPermission>;\n\n /**\n * The currently active notification instance, or `null` if none is shown.\n *\n * @see [Notification on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification)\n */\n readonly notification: Signal<Notification | null>;\n\n /**\n * Request permission to show notifications.\n *\n * @see [Notification: requestPermission() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission_static)\n */\n readonly requestPermission: () => Promise<void>;\n\n /**\n * Show a notification.\n *\n * @see [Notification() constructor on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification)\n */\n readonly show: (title: string, options?: NotificationOptions) => void;\n\n /**\n * Close the currently active notification.\n *\n * @see [Notification: close() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Notification/close)\n */\n readonly close: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API).\n *\n * Tracks the current notification as a signal. Calling `show()` auto-closes\n * the previous notification. Calling `close()` closes the current one.\n *\n * @param options - Optional configuration and default notification options\n * @returns A WebNotificationRef with notification state and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button (click)=\"requestPermission()\">Request Permission</button>\n * <button (click)=\"showNotification()\" [disabled]=\"notif.permission() !== 'granted'\">\n * Show Notification\n * </button>\n * @if (notif.notification()) {\n * <button (click)=\"notif.close()\">Close</button>\n * }\n * `\n * })\n * export class NotificationDemo {\n * readonly notif = webNotification({ icon: '/icon.png' });\n *\n * async requestPermission() {\n * await this.notif.requestPermission();\n * }\n *\n * showNotification() {\n * this.notif.show('Hello!', { body: 'This is a notification' });\n * }\n * }\n * ```\n */\nexport function webNotification(options?: WebNotificationOptions): WebNotificationRef {\n const { runInContext } = setupContext(options?.injector, webNotification);\n\n return runInContext(({ isBrowser, onCleanup }) => {\n const isSupported = constSignal(isBrowser && 'Notification' in window);\n\n if (!isSupported()) {\n return {\n isSupported,\n permission: constSignal('denied'),\n notification: constSignal(null),\n requestPermission: NOOP_ASYNC_FN,\n show: NOOP_FN,\n close: NOOP_FN,\n };\n }\n\n const permission = signal(Notification.permission);\n const notification = signal<Notification | null>(null);\n\n let autoCloseTimeout: Timer;\n\n const cancelAutoClose = () => {\n clearTimeout(autoCloseTimeout);\n autoCloseTimeout = undefined;\n };\n\n const requestPermission = async (): Promise<void> => {\n const result = await Notification.requestPermission();\n permission.set(result);\n };\n\n const close = () => {\n cancelAutoClose();\n\n const current = untracked(notification);\n\n if (current) {\n current.onclose = null;\n current.close();\n notification.set(null);\n }\n };\n\n const show = (title: string, overrides?: NotificationOptions) => {\n if (untracked(permission) !== 'granted') {\n return;\n }\n\n const { autoClose, ...defaultOptions } = options ?? {};\n const instance = new Notification(title, { ...defaultOptions, ...overrides });\n\n instance.onclose = () => {\n cancelAutoClose();\n notification.set(null);\n };\n\n notification.set(instance);\n\n const autoCloseMs = toValue(autoClose);\n if (autoCloseMs && autoCloseMs > 0) {\n autoCloseTimeout = setTimeout(close, autoCloseMs);\n }\n };\n\n onCleanup(close);\n\n watcher(permissionState('notifications'), state => {\n permission.set(state === 'prompt' ? 'default' : state);\n\n if (state === 'denied') {\n close();\n }\n });\n\n return {\n isSupported,\n permission: permission.asReadonly(),\n notification: notification.asReadonly(),\n requestPermission,\n show,\n close,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -54,8 +54,8 @@ function webShare(options) {
54
54
  catch (error) {
55
55
  // user canceled, or share failed
56
56
  // AbortError is expected when a user cancels
57
- if (ngDevMode && error.name !== 'AbortError') {
58
- console.warn(`[webShare] Failed to share content.`, error);
57
+ if (error.name !== 'AbortError') {
58
+ throw error;
59
59
  }
60
60
  }
61
61
  finally {
@@ -63,15 +63,7 @@ function webShare(options) {
63
63
  }
64
64
  };
65
65
  const canShare = (data) => {
66
- try {
67
- return navigator.canShare(data);
68
- }
69
- catch (error) {
70
- if (ngDevMode) {
71
- console.warn(`[webShare] Failed to check if content can be shared.`, error);
72
- }
73
- return false;
74
- }
66
+ return navigator.canShare(data);
75
67
  };
76
68
  return {
77
69
  isSupported,
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser-web-share.mjs","sources":["../../../projects/core/browser/web-share/index.ts","../../../projects/core/browser/web-share/signality-core-browser-web-share.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_ASYNC_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface WebShareRef {\n /**\n * Whether the Web Share API is supported in the current browser.\n *\n * @see [Web Share API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the native share dialog is currently open.\n */\n readonly isSharing: Signal<boolean>;\n\n /**\n * Open the native share dialog with the provided data.\n *\n * @see [Navigator: share() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)\n */\n readonly share: (data: ShareData) => Promise<void>;\n\n /**\n * Check whether the given data can be shared via the Web Share API.\n *\n * @see [Navigator: canShare() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare)\n */\n readonly canShare: (data?: ShareData) => boolean;\n}\n\n/**\n * Signal-based wrapper around the [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API).\n *\n * @param options - Optional injector for DI context\n * @returns A WebShareRef with share methods and state signals\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (webShare.isSupported()) {\n * <button (click)=\"shareContent()\" [disabled]=\"webShare.isSharing()\">\n * Share This Page\n * </button>\n * }\n * `\n * })\n * export class WebShareDemo {\n * readonly webShare = webShare();\n *\n * async shareContent() {\n * await this.webShare.share({\n * title: 'Check this out!',\n * url: window.location.href,\n * });\n * }\n * }\n * ```\n */\nexport function webShare(options?: WithInjector): WebShareRef {\n const { runInContext } = setupContext(options?.injector, webShare);\n\n return runInContext(({ isBrowser }) => {\n const isSupported = constSignal(isBrowser && 'share' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isSharing: constSignal(false),\n canShare: () => false,\n share: NOOP_ASYNC_FN,\n };\n }\n\n const isSharing = signal(false);\n\n const share = async (data: ShareData): Promise<void> => {\n if (untracked(isSharing)) {\n return;\n }\n\n try {\n isSharing.set(true);\n await navigator.share(data);\n } catch (error) {\n // user canceled, or share failed\n // AbortError is expected when a user cancels\n if (ngDevMode && (error as Error).name !== 'AbortError') {\n console.warn(`[webShare] Failed to share content.`, error);\n }\n } finally {\n isSharing.set(false);\n }\n };\n\n const canShare = (data?: ShareData): boolean => {\n try {\n return navigator.canShare(data);\n } catch (error) {\n if (ngDevMode) {\n console.warn(`[webShare] Failed to check if content can be shared.`, error);\n }\n return false;\n }\n };\n\n return {\n isSupported,\n isSharing: isSharing.asReadonly(),\n canShare,\n share,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BG;AACG,SAAU,QAAQ,CAAC,OAAsB,EAAA;AAC7C,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAElE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAI;QACpC,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,OAAO,IAAI,SAAS,CAAC;AAElE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC;AAC7B,gBAAA,QAAQ,EAAE,MAAM,KAAK;AACrB,gBAAA,KAAK,EAAE,aAAa;aACrB;QACH;AAEA,QAAA,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,qDAAC;AAE/B,QAAA,MAAM,KAAK,GAAG,OAAO,IAAe,KAAmB;AACrD,YAAA,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE;gBACxB;YACF;AAEA,YAAA,IAAI;AACF,gBAAA,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;AACnB,gBAAA,MAAM,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;YAC7B;YAAE,OAAO,KAAK,EAAE;;;gBAGd,IAAI,SAAS,IAAK,KAAe,CAAC,IAAI,KAAK,YAAY,EAAE;AACvD,oBAAA,OAAO,CAAC,IAAI,CAAC,qCAAqC,EAAE,KAAK,CAAC;gBAC5D;YACF;oBAAU;AACR,gBAAA,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YACtB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,QAAQ,GAAG,CAAC,IAAgB,KAAa;AAC7C,YAAA,IAAI;AACF,gBAAA,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjC;YAAE,OAAO,KAAK,EAAE;gBACd,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,sDAAsD,EAAE,KAAK,CAAC;gBAC7E;AACA,gBAAA,OAAO,KAAK;YACd;AACF,QAAA,CAAC;QAED,OAAO;YACL,WAAW;AACX,YAAA,SAAS,EAAE,SAAS,CAAC,UAAU,EAAE;YACjC,QAAQ;YACR,KAAK;SACN;AACH,IAAA,CAAC,CAAC;AACJ;;ACnHA;;AAEG;;;;"}
1
+ {"version":3,"file":"signality-core-browser-web-share.mjs","sources":["../../../projects/core/browser/web-share/index.ts","../../../projects/core/browser/web-share/signality-core-browser-web-share.ts"],"sourcesContent":["import { type Signal, signal, untracked } from '@angular/core';\nimport { constSignal, NOOP_ASYNC_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\n\nexport interface WebShareRef {\n /**\n * Whether the Web Share API is supported in the current browser.\n *\n * @see [Web Share API browser compatibility on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#browser_compatibility)\n */\n readonly isSupported: Signal<boolean>;\n\n /**\n * Whether the native share dialog is currently open.\n */\n readonly isSharing: Signal<boolean>;\n\n /**\n * Open the native share dialog with the provided data.\n *\n * @see [Navigator: share() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)\n */\n readonly share: (data: ShareData) => Promise<void>;\n\n /**\n * Check whether the given data can be shared via the Web Share API.\n *\n * @see [Navigator: canShare() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare)\n */\n readonly canShare: (data?: ShareData) => boolean;\n}\n\n/**\n * Signal-based wrapper around the [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API).\n *\n * @param options - Optional injector for DI context\n * @returns A WebShareRef with share methods and state signals\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * @if (webShare.isSupported()) {\n * <button (click)=\"shareContent()\" [disabled]=\"webShare.isSharing()\">\n * Share This Page\n * </button>\n * }\n * `\n * })\n * export class WebShareDemo {\n * readonly webShare = webShare();\n *\n * async shareContent() {\n * await this.webShare.share({\n * title: 'Check this out!',\n * url: window.location.href,\n * });\n * }\n * }\n * ```\n */\nexport function webShare(options?: WithInjector): WebShareRef {\n const { runInContext } = setupContext(options?.injector, webShare);\n\n return runInContext(({ isBrowser }) => {\n const isSupported = constSignal(isBrowser && 'share' in navigator);\n\n if (!isSupported()) {\n return {\n isSupported,\n isSharing: constSignal(false),\n canShare: () => false,\n share: NOOP_ASYNC_FN,\n };\n }\n\n const isSharing = signal(false);\n\n const share = async (data: ShareData): Promise<void> => {\n if (untracked(isSharing)) {\n return;\n }\n\n try {\n isSharing.set(true);\n await navigator.share(data);\n } catch (error) {\n // user canceled, or share failed\n // AbortError is expected when a user cancels\n if ((error as Error).name !== 'AbortError') {\n throw error;\n }\n } finally {\n isSharing.set(false);\n }\n };\n\n const canShare = (data?: ShareData): boolean => {\n return navigator.canShare(data);\n };\n\n return {\n isSupported,\n isSharing: isSharing.asReadonly(),\n canShare,\n share,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BG;AACG,SAAU,QAAQ,CAAC,OAAsB,EAAA;AAC7C,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAElE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAI;QACpC,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,IAAI,OAAO,IAAI,SAAS,CAAC;AAElE,QAAA,IAAI,CAAC,WAAW,EAAE,EAAE;YAClB,OAAO;gBACL,WAAW;AACX,gBAAA,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC;AAC7B,gBAAA,QAAQ,EAAE,MAAM,KAAK;AACrB,gBAAA,KAAK,EAAE,aAAa;aACrB;QACH;AAEA,QAAA,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,qDAAC;AAE/B,QAAA,MAAM,KAAK,GAAG,OAAO,IAAe,KAAmB;AACrD,YAAA,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE;gBACxB;YACF;AAEA,YAAA,IAAI;AACF,gBAAA,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;AACnB,gBAAA,MAAM,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;YAC7B;YAAE,OAAO,KAAK,EAAE;;;AAGd,gBAAA,IAAK,KAAe,CAAC,IAAI,KAAK,YAAY,EAAE;AAC1C,oBAAA,MAAM,KAAK;gBACb;YACF;oBAAU;AACR,gBAAA,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YACtB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,QAAQ,GAAG,CAAC,IAAgB,KAAa;AAC7C,YAAA,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;AACjC,QAAA,CAAC;QAED,OAAO;YACL,WAAW;AACX,YAAA,SAAS,EAAE,SAAS,CAAC,UAAU,EAAE;YACjC,QAAQ;YACR,KAAK;SACN;AACH,IAAA,CAAC,CAAC;AACJ;;AC5GA;;AAEG;;;;"}
@@ -4,6 +4,7 @@ export * from '@signality/core/browser/breakpoints';
4
4
  export * from '@signality/core/browser/broadcast-channel';
5
5
  export * from '@signality/core/browser/browser-language';
6
6
  export * from '@signality/core/browser/clipboard';
7
+ export * from '@signality/core/browser/device-pixel-ratio';
7
8
  export * from '@signality/core/browser/device-posture';
8
9
  export * from '@signality/core/browser/display-media';
9
10
  export * from '@signality/core/browser/eye-dropper';
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-browser.mjs","sources":["../../../projects/core/browser/signality-core-browser.ts"],"sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;AAEG"}
1
+ {"version":3,"file":"signality-core-browser.mjs","sources":["../../../projects/core/browser/signality-core-browser.ts"],"sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;AAEG"}
@@ -2,7 +2,7 @@ import { inject, signal, afterNextRender } from '@angular/core';
2
2
  import { NgControl, RequiredValidator, Validators, NgModel } from '@angular/forms';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { timer, switchMap, EMPTY, startWith, map } from 'rxjs';
5
- import { setupContext, ALWAYS_FALSE_FN } from '@signality/core/internal';
5
+ import { setupContext, ALWAYS_FALSE_FN, waitForValue } from '@signality/core/internal';
6
6
  import { watcher } from '@signality/core/reactivity/watcher';
7
7
 
8
8
  /**
@@ -50,13 +50,14 @@ function cva(options) {
50
50
  return runInContext(({ injector }) => {
51
51
  const ngControl = inject(NgControl, { self: true, optional: true });
52
52
  const ngModelRequired = inject(RequiredValidator, { self: true, optional: true });
53
- const initialValue = options.value();
54
53
  const { value,
55
54
  // Use ALWAYS_FALSE_FN to ensure touched signal always triggers watcher updates.
56
55
  // This is critical for controls with `updateOn: 'blur'` - when blur occurs, the control
57
56
  // may update its value, but control.touched remains true. Without forcing signal updates,
58
57
  // the watcher won't fire and the control won't properly synchronize its value after blur.
59
58
  touched = signal(false, { equal: ALWAYS_FALSE_FN }), disabled = signal(false), required = signal(false), invalid = signal(false), pending = signal(false), dirty = signal(false), errors = signal(null), } = options;
59
+ let initialValue = UNSET_VALUE;
60
+ waitForValue(value).then(val => (initialValue = val));
60
61
  const cvaRef = {
61
62
  value,
62
63
  touched,
@@ -66,7 +67,15 @@ function cva(options) {
66
67
  pending: pending.asReadonly(),
67
68
  dirty: dirty.asReadonly(),
68
69
  errors: errors.asReadonly(),
69
- reset: () => value.set(initialValue),
70
+ reset: () => {
71
+ if (initialValue === UNSET_VALUE) {
72
+ throw new Error(ngDevMode
73
+ ? '[cva] Cannot reset before the initial value is resolved. ' +
74
+ 'Avoid calling `reset()` synchronously during initialization.'
75
+ : '');
76
+ }
77
+ value.set(initialValue);
78
+ },
70
79
  };
71
80
  if (!ngControl) {
72
81
  return cvaRef;
@@ -131,6 +140,7 @@ function cva(options) {
131
140
  return cvaRef;
132
141
  });
133
142
  }
143
+ const UNSET_VALUE = Symbol('Cva#UNSET');
134
144
 
135
145
  /**
136
146
  * Generated bundle index. Do not edit.
@@ -1 +1 @@
1
- {"version":3,"file":"signality-core-forms-cva.mjs","sources":["../../../projects/core/forms/cva/index.ts","../../../projects/core/forms/cva/signality-core-forms-cva.ts"],"sourcesContent":["import {\n afterNextRender,\n type AfterRenderRef,\n inject,\n type Signal,\n signal,\n type WritableSignal,\n} from '@angular/core';\nimport {\n NgControl,\n NgModel,\n RequiredValidator,\n type ValidationErrors,\n Validators,\n} from '@angular/forms';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EMPTY, map, startWith, switchMap, timer } from 'rxjs';\nimport { ALWAYS_FALSE_FN, setupContext } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport type CvaOptions<T> = Omit<Partial<MakeWritable<CvaRef<T>>>, 'value'> &\n Pick<CvaRef<T>, 'value'> &\n WithInjector;\n\nexport interface CvaRef<T> {\n readonly value: WritableSignal<T>;\n readonly touched: WritableSignal<boolean>;\n readonly disabled: Signal<boolean>;\n readonly required: Signal<boolean>;\n readonly invalid: Signal<boolean>;\n readonly pending: Signal<boolean>;\n readonly dirty: Signal<boolean>;\n readonly errors: Signal<ValidationErrors | null>;\n readonly reset: () => void;\n}\n\n/**\n * Reactive wrapper around the [Angular Forms](https://angular.dev/guide/forms) Control Value Accessor (CVA) pattern.\n * Integrates seamlessly with both template-driven and reactive forms.\n *\n * @param options - Configuration options including the value signal\n * @returns A CvaRef with reactive form control state signals\n *\n * @example\n * ```typescript\n * @Component({\n * selector: 'app-currency-input',\n * template: `\n * <input\n * type=\"text\"\n * [value]=\"displayValue()\"\n * [required]=\"cva.required()\"\n * (input)=\"handleInput($any($event.target).value)\"\n * (blur)=\"cva.touched.set(true)\"\n * />\n * `,\n * })\n * export class CurrencyInput {\n * readonly value = model(0);\n * readonly cva = cva({ value: this.value });\n *\n * readonly displayValue = computed(() => {\n * return this.value()\n * .toFixed(2)\n * .replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); // Shows \"1,234.56\"\n * });\n *\n * handleInput(input: string) {\n * const num = parseFloat(input.replace(/[^0-9.]/g, ''));\n * if (!isNaN(num)) {\n * this.value.set(num);\n * }\n * }\n * }\n * ```\n */\nexport function cva<T>(options: CvaOptions<T>): CvaRef<T> {\n const { runInContext } = setupContext(options?.injector, cva);\n\n return runInContext(({ injector }) => {\n const ngControl = inject(NgControl, { self: true, optional: true });\n const ngModelRequired = inject(RequiredValidator, { self: true, optional: true });\n\n const initialValue = options.value();\n\n const {\n value,\n // Use ALWAYS_FALSE_FN to ensure touched signal always triggers watcher updates.\n // This is critical for controls with `updateOn: 'blur'` - when blur occurs, the control\n // may update its value, but control.touched remains true. Without forcing signal updates,\n // the watcher won't fire and the control won't properly synchronize its value after blur.\n touched = signal(false, { equal: ALWAYS_FALSE_FN }),\n disabled = signal(false),\n required = signal(false),\n invalid = signal(false),\n pending = signal(false),\n dirty = signal(false),\n errors = signal(null),\n } = options;\n\n const cvaRef: CvaRef<T> = {\n value,\n touched,\n disabled: disabled.asReadonly(),\n required: required.asReadonly(),\n invalid: invalid.asReadonly(),\n pending: pending.asReadonly(),\n dirty: dirty.asReadonly(),\n errors: errors.asReadonly(),\n reset: () => value.set(initialValue),\n };\n\n if (!ngControl) {\n return cvaRef;\n }\n\n let touchedFn: () => void;\n let updateFn: (v: T) => void;\n let scheduledModelUpdate: AfterRenderRef | null;\n\n const runModelUpdate = (fn: () => void) => {\n scheduledModelUpdate?.destroy();\n\n fn();\n\n scheduledModelUpdate = afterNextRender(\n () => {\n scheduledModelUpdate = null;\n },\n { injector }\n );\n };\n\n watcher(touched, isTouched => {\n if (isTouched) {\n touchedFn?.();\n }\n });\n\n watcher(value, v => {\n if (scheduledModelUpdate) {\n scheduledModelUpdate.destroy();\n scheduledModelUpdate = null;\n } else {\n updateFn?.(v);\n }\n });\n\n // the control instance isn't available immediately inside FormControl or FormControlName,\n // because they depend on [inputs]. That's why we schedule the subscription asynchronously.\n timer(0)\n .pipe(\n switchMap(() => {\n const { control } = ngControl;\n\n if (!control) {\n return EMPTY;\n }\n\n return control.events.pipe(\n startWith(null),\n map(() => control)\n );\n }),\n takeUntilDestroyed()\n )\n .subscribe(control => {\n required.set(\n control.hasValidator(Validators.required) ||\n // cannot compare references because `RequiredValidator` wraps `requiredValidator` fn\n // (https://github.com/angular/angular/blob/19.1.0/packages/forms/src/directives/validators.ts#L398)\n !!ngModelRequired?.required\n );\n touched.set(control.touched);\n invalid.set(control.invalid);\n pending.set(control.pending);\n errors.set(control.errors);\n dirty.set(control.dirty);\n });\n\n ngControl.valueAccessor = {\n writeValue: (rawValue: T | null) => {\n runModelUpdate(() => {\n // fix (https://github.com/angular/angular/issues/14988)\n const modelValue = ngControl instanceof NgModel ? ngControl.model : rawValue;\n value.set(modelValue);\n });\n },\n registerOnChange: fn => (updateFn = fn),\n registerOnTouched: fn => (touchedFn = fn),\n setDisabledState: isDisabled => disabled?.set(isDisabled),\n };\n\n return cvaRef;\n });\n}\n\ntype MakeWritable<T extends object> = {\n [K in keyof T]: T[K] extends Signal<infer U> ? WritableSignal<U> : never;\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-forms-cva.mjs","sources":["../../../projects/core/forms/cva/index.ts","../../../projects/core/forms/cva/signality-core-forms-cva.ts"],"sourcesContent":["import {\n afterNextRender,\n type AfterRenderRef,\n inject,\n type Signal,\n signal,\n type WritableSignal,\n} from '@angular/core';\nimport {\n NgControl,\n NgModel,\n RequiredValidator,\n type ValidationErrors,\n Validators,\n} from '@angular/forms';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EMPTY, map, startWith, switchMap, timer } from 'rxjs';\nimport { ALWAYS_FALSE_FN, setupContext, waitForValue } from '@signality/core/internal';\nimport type { WithInjector } from '@signality/core/types';\nimport { watcher } from '@signality/core/reactivity/watcher';\n\nexport interface CvaOptions<T> extends WithInjector {\n readonly value: WritableSignal<T>;\n readonly touched?: WritableSignal<boolean>;\n readonly disabled?: WritableSignal<boolean>;\n readonly required?: WritableSignal<boolean>;\n readonly invalid?: WritableSignal<boolean>;\n readonly pending?: WritableSignal<boolean>;\n readonly dirty?: WritableSignal<boolean>;\n readonly errors?: WritableSignal<ValidationErrors | null>;\n}\n\nexport interface CvaRef<T> {\n readonly value: WritableSignal<T>;\n readonly touched: WritableSignal<boolean>;\n readonly disabled: Signal<boolean>;\n readonly required: Signal<boolean>;\n readonly invalid: Signal<boolean>;\n readonly pending: Signal<boolean>;\n readonly dirty: Signal<boolean>;\n readonly errors: Signal<ValidationErrors | null>;\n readonly reset: () => void;\n}\n\n/**\n * Reactive wrapper around the [Angular Forms](https://angular.dev/guide/forms) Control Value Accessor (CVA) pattern.\n * Integrates seamlessly with both template-driven and reactive forms.\n *\n * @param options - Configuration options including the value signal\n * @returns A CvaRef with reactive form control state signals\n *\n * @example\n * ```typescript\n * @Component({\n * selector: 'app-currency-input',\n * template: `\n * <input\n * type=\"text\"\n * [value]=\"displayValue()\"\n * [required]=\"cva.required()\"\n * (input)=\"handleInput($any($event.target).value)\"\n * (blur)=\"cva.touched.set(true)\"\n * />\n * `,\n * })\n * export class CurrencyInput {\n * readonly value = model(0);\n * readonly cva = cva({ value: this.value });\n *\n * readonly displayValue = computed(() => {\n * return this.value()\n * .toFixed(2)\n * .replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); // Shows \"1,234.56\"\n * });\n *\n * handleInput(input: string) {\n * const num = parseFloat(input.replace(/[^0-9.]/g, ''));\n * if (!isNaN(num)) {\n * this.value.set(num);\n * }\n * }\n * }\n * ```\n */\nexport function cva<T>(options: CvaOptions<T>): CvaRef<T> {\n const { runInContext } = setupContext(options?.injector, cva);\n\n return runInContext(({ injector }) => {\n const ngControl = inject(NgControl, { self: true, optional: true });\n const ngModelRequired = inject(RequiredValidator, { self: true, optional: true });\n\n const {\n value,\n // Use ALWAYS_FALSE_FN to ensure touched signal always triggers watcher updates.\n // This is critical for controls with `updateOn: 'blur'` - when blur occurs, the control\n // may update its value, but control.touched remains true. Without forcing signal updates,\n // the watcher won't fire and the control won't properly synchronize its value after blur.\n touched = signal(false, { equal: ALWAYS_FALSE_FN }),\n disabled = signal(false),\n required = signal(false),\n invalid = signal(false),\n pending = signal(false),\n dirty = signal(false),\n errors = signal(null),\n } = options;\n\n let initialValue: T = UNSET_VALUE as never;\n\n waitForValue(value).then(val => (initialValue = val));\n\n const cvaRef: CvaRef<T> = {\n value,\n touched,\n disabled: disabled.asReadonly(),\n required: required.asReadonly(),\n invalid: invalid.asReadonly(),\n pending: pending.asReadonly(),\n dirty: dirty.asReadonly(),\n errors: errors.asReadonly(),\n reset: () => {\n if (initialValue === UNSET_VALUE) {\n throw new Error(\n ngDevMode\n ? '[cva] Cannot reset before the initial value is resolved. ' +\n 'Avoid calling `reset()` synchronously during initialization.'\n : ''\n );\n }\n value.set(initialValue);\n },\n };\n\n if (!ngControl) {\n return cvaRef;\n }\n\n let touchedFn: () => void;\n let updateFn: (v: T) => void;\n let scheduledModelUpdate: AfterRenderRef | null;\n\n const runModelUpdate = (fn: () => void) => {\n scheduledModelUpdate?.destroy();\n\n fn();\n\n scheduledModelUpdate = afterNextRender(\n () => {\n scheduledModelUpdate = null;\n },\n { injector }\n );\n };\n\n watcher(touched, isTouched => {\n if (isTouched) {\n touchedFn?.();\n }\n });\n\n watcher(value, v => {\n if (scheduledModelUpdate) {\n scheduledModelUpdate.destroy();\n scheduledModelUpdate = null;\n } else {\n updateFn?.(v);\n }\n });\n\n // the control instance isn't available immediately inside FormControl or FormControlName,\n // because they depend on [inputs]. That's why we schedule the subscription asynchronously.\n timer(0)\n .pipe(\n switchMap(() => {\n const { control } = ngControl;\n\n if (!control) {\n return EMPTY;\n }\n\n return control.events.pipe(\n startWith(null),\n map(() => control)\n );\n }),\n takeUntilDestroyed()\n )\n .subscribe(control => {\n required.set(\n control.hasValidator(Validators.required) ||\n // cannot compare references because `RequiredValidator` wraps `requiredValidator` fn\n // (https://github.com/angular/angular/blob/19.1.0/packages/forms/src/directives/validators.ts#L398)\n !!ngModelRequired?.required\n );\n touched.set(control.touched);\n invalid.set(control.invalid);\n pending.set(control.pending);\n errors.set(control.errors);\n dirty.set(control.dirty);\n });\n\n ngControl.valueAccessor = {\n writeValue: (rawValue: T | null) => {\n runModelUpdate(() => {\n // fix (https://github.com/angular/angular/issues/14988)\n const modelValue = ngControl instanceof NgModel ? ngControl.model : rawValue;\n value.set(modelValue);\n });\n },\n registerOnChange: fn => (updateFn = fn),\n registerOnTouched: fn => (touchedFn = fn),\n setDisabledState: isDisabled => disabled?.set(isDisabled),\n };\n\n return cvaRef;\n });\n}\n\nconst UNSET_VALUE: unique symbol = Symbol('Cva#UNSET');\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}