@signality/core 0.1.1 → 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.
- package/browser/file-dialog/index.d.ts +4 -1
- package/browser/storage/index.d.ts +1 -1
- package/elements/element-focus/index.d.ts +12 -8
- package/fesm2022/signality-core-browser-favicon.mjs +1 -1
- package/fesm2022/signality-core-browser-favicon.mjs.map +1 -1
- package/fesm2022/signality-core-browser-file-dialog.mjs +1 -1
- package/fesm2022/signality-core-browser-file-dialog.mjs.map +1 -1
- package/fesm2022/signality-core-browser-storage.mjs.map +1 -1
- package/fesm2022/signality-core-elements-element-focus.mjs +23 -10
- package/fesm2022/signality-core-elements-element-focus.mjs.map +1 -1
- package/fesm2022/signality-core-elements-element-size.mjs +1 -1
- package/fesm2022/signality-core-elements-element-size.mjs.map +1 -1
- package/fesm2022/signality-core-elements-element-visibility.mjs +2 -6
- package/fesm2022/signality-core-elements-element-visibility.mjs.map +1 -1
- package/package.json +9 -9
|
@@ -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
|
-
*
|
|
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>;
|
|
@@ -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(
|
|
57
|
+
* mergeResolver: (stored, initial) => deepMerge(initial, stored),
|
|
58
58
|
* });
|
|
59
59
|
* ```
|
|
60
60
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CreateSignalOptions, type
|
|
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
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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):
|
|
42
|
+
export declare function elementFocus(target: MaybeElementSignal<HTMLElement>, options?: ElementFocusOptions): WritableSignal<boolean>;
|
|
@@ -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":";;;;AAgDA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACG,SAAU,OAAO,CAAC,OAAwB,EAAA;AAC9C,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;AAEjE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;YACZ,OAAO;AACL,gBAAA,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;AACxB,gBAAA,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;AACzB,gBAAA,GAAG,EAAE,OAAO;AACZ,gBAAA,QAAQ,EAAE,OAAO;AACjB,gBAAA,KAAK,EAAE,OAAO;aACf;QACH;AAEA,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC7D,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,WAAW,IAAI,EAAE;QAErD,MAAM,cAAc,GAAG,MAAsB;YAC3C,IAAI,IAAI,GAAG,QAAQ,CAAC,aAAa,
|
|
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
|
|
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
|
|
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 +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,
|
|
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
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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, () =>
|
|
45
|
-
|
|
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
|
|
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;;;;"}
|
|
@@ -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(
|
|
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 ??
|
|
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(
|
|
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 ??
|
|
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.
|
|
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,26 +288,26 @@
|
|
|
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"
|
|
302
302
|
},
|
|
303
|
-
"./router/params": {
|
|
304
|
-
"types": "./router/params/index.d.ts",
|
|
305
|
-
"default": "./fesm2022/signality-core-router-params.mjs"
|
|
306
|
-
},
|
|
307
303
|
"./router/fragment": {
|
|
308
304
|
"types": "./router/fragment/index.d.ts",
|
|
309
305
|
"default": "./fesm2022/signality-core-router-fragment.mjs"
|
|
310
306
|
},
|
|
307
|
+
"./router/params": {
|
|
308
|
+
"types": "./router/params/index.d.ts",
|
|
309
|
+
"default": "./fesm2022/signality-core-router-params.mjs"
|
|
310
|
+
},
|
|
311
311
|
"./router/query-params": {
|
|
312
312
|
"types": "./router/query-params/index.d.ts",
|
|
313
313
|
"default": "./fesm2022/signality-core-router-query-params.mjs"
|