elements-kit 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ import { _ as signal } from "../signals-BHmWX6ox.mjs";
2
+ import { on } from "./event-listener.mjs";
3
+ //#region src/utilities/network.ts
4
+ function createOnline() {
5
+ const value = signal(typeof navigator !== "undefined" ? navigator.onLine : true);
6
+ const update = () => value(navigator.onLine);
7
+ on(window, "online", update);
8
+ on(window, "offline", update);
9
+ return value;
10
+ }
11
+ /**
12
+ * Singleton `Computed<boolean>` — `true` when `navigator.onLine` is true.
13
+ * Reacts to `online` / `offline` window events.
14
+ */
15
+ const online = createOnline();
16
+ //#endregion
17
+ export { online };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,29 @@
1
+ import { n as vi, o as describe, r as afterEach, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
2
+ import { online } from "./network.mjs";
3
+ //#region src/utilities/network.test.ts
4
+ afterEach(() => {
5
+ vi.restoreAllMocks();
6
+ });
7
+ describe("online", () => {
8
+ it("reflects navigator.onLine", () => {
9
+ globalExpect(online()).toBe(navigator.onLine);
10
+ });
11
+ it("becomes false on offline event", () => {
12
+ Object.defineProperty(navigator, "onLine", {
13
+ configurable: true,
14
+ get: () => false
15
+ });
16
+ window.dispatchEvent(new Event("offline"));
17
+ globalExpect(online()).toBe(false);
18
+ });
19
+ it("becomes true on online event", () => {
20
+ Object.defineProperty(navigator, "onLine", {
21
+ configurable: true,
22
+ get: () => true
23
+ });
24
+ window.dispatchEvent(new Event("online"));
25
+ globalExpect(online()).toBe(true);
26
+ });
27
+ });
28
+ //#endregion
29
+ export {};
@@ -0,0 +1,56 @@
1
+ import { t as Computed } from "../index-DUshSQ_6.mjs";
2
+
3
+ //#region src/utilities/promise.d.ts
4
+ /**
5
+ * A `Promise` subclass that exposes its state as reactive signals.
6
+ *
7
+ * Prefer the {@link promise} factory for most use cases — it returns a
8
+ * `ComputedPromise` that is both awaitable and callable as a signal.
9
+ * Use `ReactivePromise` directly when you need the lower-level class —
10
+ * for example, to wrap a promise and expose `.state`, `.value`, `.reason`,
11
+ * and `.result` without the `Computed` callable interface.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const rp = ReactivePromise.from(fetch("/api/data"));
16
+ *
17
+ * effect(() => {
18
+ * if (rp.state === "fulfilled") console.log(rp.value);
19
+ * });
20
+ * ```
21
+ */
22
+ declare class ReactivePromise<T, E = unknown> extends Promise<T> {
23
+ #private;
24
+ get state(): "pending" | "fulfilled" | "rejected";
25
+ get value(): T | undefined;
26
+ get reason(): E | undefined;
27
+ get result(): T | E | undefined;
28
+ constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void);
29
+ static from<T, E = unknown>(p: Promise<T>): ReactivePromise<T, E>;
30
+ }
31
+ type ComputedPromise<T, E = unknown> = ReactivePromise<T, E> & Computed<T | E | undefined>;
32
+ type Executor<T, E = unknown> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: E) => void) => void;
33
+ /**
34
+ * Wraps a promise, executor, or `ReactivePromise` into a `ComputedPromise` —
35
+ * an object that is both awaitable like a regular Promise and reactive like a
36
+ * `Computed` signal.
37
+ *
38
+ * **Awaitable:** `await promise(fetch(...))` resolves to the fulfilled value,
39
+ * or rejects with the rejection reason, just like a native Promise.
40
+ *
41
+ * **Reactive:** calling the returned value as a function (`cp()`) reads the
42
+ * current result inside an `effect` or `computed`, tracking it as a dependency.
43
+ * Equivalent to `.result` — returns `undefined` while pending, the fulfilled
44
+ * value when resolved, or the rejection reason when rejected.
45
+ *
46
+ * Reactive state is also accessible via:
47
+ * - `.state` — `"pending" | "fulfilled" | "rejected"`
48
+ * - `.value` — the resolved value (or `undefined` while pending)
49
+ * - `.reason` — the rejection reason (or `undefined` while pending/fulfilled)
50
+ * - `.result` — `T | E | undefined`; the resolved value, rejection reason, or `undefined` while pending
51
+ */
52
+ declare function promise<T, E = unknown>(p: ReactivePromise<T>): ComputedPromise<T, E>;
53
+ declare function promise<T, E = unknown>(p: Promise<T>): ComputedPromise<T, E>;
54
+ declare function promise<T, E = unknown>(executor: Executor<T, E>): ComputedPromise<T, E>;
55
+ //#endregion
56
+ export { ComputedPromise, ReactivePromise, promise };
@@ -0,0 +1,101 @@
1
+ import { _ as signal, c as batch, l as computed } from "../signals-BHmWX6ox.mjs";
2
+ //#region src/utilities/promise.ts
3
+ /**
4
+ * A `Promise` subclass that exposes its state as reactive signals.
5
+ *
6
+ * Prefer the {@link promise} factory for most use cases — it returns a
7
+ * `ComputedPromise` that is both awaitable and callable as a signal.
8
+ * Use `ReactivePromise` directly when you need the lower-level class —
9
+ * for example, to wrap a promise and expose `.state`, `.value`, `.reason`,
10
+ * and `.result` without the `Computed` callable interface.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const rp = ReactivePromise.from(fetch("/api/data"));
15
+ *
16
+ * effect(() => {
17
+ * if (rp.state === "fulfilled") console.log(rp.value);
18
+ * });
19
+ * ```
20
+ */
21
+ var ReactivePromise = class ReactivePromise extends Promise {
22
+ #state = signal("pending");
23
+ #value = signal(void 0);
24
+ #reason = signal(void 0);
25
+ #result = computed(() => {
26
+ const state = this.#state();
27
+ if (state === "fulfilled") return this.#value();
28
+ if (state === "rejected") return this.#reason();
29
+ });
30
+ get state() {
31
+ return this.#state();
32
+ }
33
+ get value() {
34
+ return this.#value();
35
+ }
36
+ get reason() {
37
+ return this.#reason();
38
+ }
39
+ get result() {
40
+ return this.#result();
41
+ }
42
+ constructor(executor) {
43
+ super((res, rej) => {
44
+ executor(async (value) => {
45
+ const resolved = await value;
46
+ batch(() => {
47
+ this.#state("fulfilled");
48
+ this.#value(resolved);
49
+ });
50
+ res(value);
51
+ }, async (_reason) => {
52
+ const reason = await _reason;
53
+ batch(() => {
54
+ this.#state("rejected");
55
+ this.#reason(reason);
56
+ });
57
+ rej(reason);
58
+ });
59
+ });
60
+ }
61
+ static from(p) {
62
+ return new ReactivePromise((resolve, reject) => {
63
+ p.then(resolve).catch(reject);
64
+ });
65
+ }
66
+ };
67
+ const PROMISE_KEYS = new Set([
68
+ "then",
69
+ "catch",
70
+ "finally",
71
+ "state",
72
+ "value",
73
+ "reason",
74
+ "result"
75
+ ]);
76
+ function resolvePromise(from) {
77
+ if (from instanceof ReactivePromise) return from;
78
+ if (from instanceof Promise) return ReactivePromise.from(from);
79
+ return new ReactivePromise(from);
80
+ }
81
+ function promise(from) {
82
+ const p = resolvePromise(from);
83
+ const $value = computed(() => p.result);
84
+ return new Proxy($value, {
85
+ apply() {
86
+ return $value();
87
+ },
88
+ get(target, prop, receiver) {
89
+ if (PROMISE_KEYS.has(prop)) {
90
+ const val = p[prop];
91
+ return typeof val === "function" ? val.bind(p) : val;
92
+ }
93
+ return Reflect.get(target, prop, receiver);
94
+ },
95
+ getPrototypeOf() {
96
+ return ReactivePromise.prototype;
97
+ }
98
+ });
99
+ }
100
+ //#endregion
101
+ export { ReactivePromise, promise };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,156 @@
1
+ import { u as effect } from "../signals-BHmWX6ox.mjs";
2
+ import { o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
3
+ import { ReactivePromise, promise } from "./promise.mjs";
4
+ //#region src/utilities/promise.test.ts
5
+ describe("ReactivePromise", () => {
6
+ it("starts in pending state", () => {
7
+ const rp = new ReactivePromise(() => {});
8
+ globalExpect(rp.state).toBe("pending");
9
+ globalExpect(rp.value).toBeUndefined();
10
+ globalExpect(rp.reason).toBeUndefined();
11
+ globalExpect(rp.result).toBeUndefined();
12
+ });
13
+ it("transitions to fulfilled", async () => {
14
+ const rp = new ReactivePromise((resolve) => resolve(42));
15
+ await rp;
16
+ globalExpect(rp.state).toBe("fulfilled");
17
+ globalExpect(rp.value).toBe(42);
18
+ globalExpect(rp.reason).toBeUndefined();
19
+ globalExpect(rp.result).toBe(42);
20
+ });
21
+ it("transitions to rejected", async () => {
22
+ const err = /* @__PURE__ */ new Error("fail");
23
+ const rp = new ReactivePromise((_, reject) => reject(err));
24
+ await rp.catch(() => {});
25
+ globalExpect(rp.state).toBe("rejected");
26
+ globalExpect(rp.reason).toBe(err);
27
+ globalExpect(rp.value).toBeUndefined();
28
+ globalExpect(rp.result).toBe(err);
29
+ });
30
+ it("unwraps a PromiseLike value", async () => {
31
+ const rp = new ReactivePromise((resolve) => resolve(Promise.resolve(99)));
32
+ await rp;
33
+ globalExpect(rp.value).toBe(99);
34
+ });
35
+ it("state and value update atomically — no inconsistent intermediate state", async () => {
36
+ const rp = new ReactivePromise((resolve) => resolve(7));
37
+ const snapshots = [];
38
+ const stop = effect(() => {
39
+ snapshots.push({
40
+ state: rp.state,
41
+ value: rp.value
42
+ });
43
+ });
44
+ await rp;
45
+ stop();
46
+ globalExpect(snapshots.find((s) => s.state === "fulfilled" && s.value === void 0)).toBeUndefined();
47
+ globalExpect(snapshots.at(-1)).toEqual({
48
+ state: "fulfilled",
49
+ value: 7
50
+ });
51
+ });
52
+ it("state and reason update atomically on rejection", async () => {
53
+ const err = /* @__PURE__ */ new Error("oops");
54
+ const rp = new ReactivePromise((_, reject) => reject(err));
55
+ const snapshots = [];
56
+ const stop = effect(() => {
57
+ snapshots.push({
58
+ state: rp.state,
59
+ reason: rp.reason
60
+ });
61
+ });
62
+ await rp.catch(() => {});
63
+ stop();
64
+ globalExpect(snapshots.find((s) => s.state === "rejected" && s.reason === void 0)).toBeUndefined();
65
+ globalExpect(snapshots.at(-1)).toEqual({
66
+ state: "rejected",
67
+ reason: err
68
+ });
69
+ });
70
+ it("ReactivePromise.from() wraps an existing Promise", async () => {
71
+ const rp = ReactivePromise.from(Promise.resolve("hello"));
72
+ await rp;
73
+ globalExpect(rp.state).toBe("fulfilled");
74
+ globalExpect(rp.value).toBe("hello");
75
+ });
76
+ });
77
+ describe("promise()", () => {
78
+ it("is instanceof ReactivePromise", () => {
79
+ globalExpect(promise(() => {}) instanceof ReactivePromise).toBe(true);
80
+ });
81
+ it("returns undefined while pending", () => {
82
+ const cp = promise(() => {});
83
+ globalExpect(cp()).toBeUndefined();
84
+ globalExpect(cp.state).toBe("pending");
85
+ });
86
+ it("returns the resolved value when fulfilled", async () => {
87
+ const cp = promise((resolve) => resolve(42));
88
+ await cp;
89
+ globalExpect(cp()).toBe(42);
90
+ });
91
+ it("returns the rejection reason when rejected", async () => {
92
+ const err = /* @__PURE__ */ new Error("fail");
93
+ const cp = promise((_, reject) => reject(err));
94
+ await cp.catch(() => {});
95
+ globalExpect(cp()).toBe(err);
96
+ });
97
+ it("wraps an existing Promise", async () => {
98
+ const cp = promise(Promise.resolve("hi"));
99
+ await cp;
100
+ globalExpect(cp()).toBe("hi");
101
+ });
102
+ it("passes through a ReactivePromise", async () => {
103
+ const cp = promise(new ReactivePromise((resolve) => resolve(5)));
104
+ await cp;
105
+ globalExpect(cp()).toBe(5);
106
+ });
107
+ it("is awaitable — resolves to the fulfilled value", async () => {
108
+ globalExpect(await promise((resolve) => resolve(100))).toBe(100);
109
+ });
110
+ it("is awaitable — rejects on failure", async () => {
111
+ const err = /* @__PURE__ */ new Error("bad");
112
+ const cp = promise((_, reject) => reject(err));
113
+ let caught;
114
+ await cp.catch((e) => {
115
+ caught = e;
116
+ });
117
+ globalExpect(caught).toBe(err);
118
+ });
119
+ it("delegates .then()", async () => {
120
+ globalExpect(await promise((resolve) => resolve(3)).then((v) => v * 2)).toBe(6);
121
+ });
122
+ it("delegates .catch()", async () => {
123
+ const err = /* @__PURE__ */ new Error("caught");
124
+ globalExpect(await promise((_, reject) => reject(err)).catch((e) => e)).toBe(err);
125
+ });
126
+ it("cp.catch handler runs on rejection", async () => {
127
+ const err = /* @__PURE__ */ new Error("caught");
128
+ const cp = promise((_, reject) => reject(err));
129
+ const caught = [];
130
+ await cp.catch((e) => caught.push(e));
131
+ globalExpect(caught).toEqual([err]);
132
+ });
133
+ it("is reactive — effect reruns on fulfillment", async () => {
134
+ const cp = promise((resolve) => resolve(1));
135
+ const values = [];
136
+ const stop = effect(() => {
137
+ values.push(cp.value);
138
+ });
139
+ await cp;
140
+ stop();
141
+ globalExpect(values).toEqual([void 0, 1]);
142
+ });
143
+ it("is reactive — effect reruns on rejection", async () => {
144
+ const err = /* @__PURE__ */ new Error("x");
145
+ const cp = promise((_, reject) => reject(err));
146
+ const results = [];
147
+ const stop = effect(() => {
148
+ results.push(cp());
149
+ });
150
+ await cp.catch(() => {});
151
+ stop();
152
+ globalExpect(results).toEqual([void 0, err]);
153
+ });
154
+ });
155
+ //#endregion
156
+ export {};
@@ -0,0 +1,10 @@
1
+ //#region src/utilities/retry.d.ts
2
+ type RetryDelay = number | ((attempt: number) => number);
3
+ /**
4
+ * Wraps `fn` to retry up to `attempts` times on failure.
5
+ * Delay (if provided) is inserted between failures only — not after the last.
6
+ * Each attempt runs in an effect scope so `onCleanup` inside `fn` fires before each retry.
7
+ */
8
+ declare function retry<T>(fn: () => Promise<T>, attempts: number, delay?: RetryDelay): () => Promise<T>;
9
+ //#endregion
10
+ export { retry };
@@ -0,0 +1,32 @@
1
+ import { u as effect, y as untracked } from "../signals-BHmWX6ox.mjs";
2
+ //#region src/utilities/retry.ts
3
+ /**
4
+ * Wraps `fn` to retry up to `attempts` times on failure.
5
+ * Delay (if provided) is inserted between failures only — not after the last.
6
+ * Each attempt runs in an effect scope so `onCleanup` inside `fn` fires before each retry.
7
+ */
8
+ function retry(fn, attempts, delay) {
9
+ return async () => {
10
+ let last;
11
+ for (let i = 0; i < attempts; i++) {
12
+ let stop = () => {};
13
+ try {
14
+ const result = await new Promise((res, rej) => {
15
+ stop = effect(() => untracked(() => fn().then(res, rej)));
16
+ });
17
+ stop();
18
+ return result;
19
+ } catch (err) {
20
+ stop();
21
+ last = err;
22
+ if (i < attempts - 1 && delay !== void 0) {
23
+ const ms = typeof delay === "function" ? delay(i) : delay;
24
+ await new Promise((r) => setTimeout(r, ms));
25
+ }
26
+ }
27
+ }
28
+ throw last;
29
+ };
30
+ }
31
+ //#endregion
32
+ export { retry };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,83 @@
1
+ import { g as onCleanup } from "../signals-BHmWX6ox.mjs";
2
+ import { n as vi, o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
3
+ import { retry } from "./retry.mjs";
4
+ //#region src/utilities/retry.test.ts
5
+ describe("retry", () => {
6
+ it("resolves immediately on first success", async () => {
7
+ globalExpect(await retry(() => Promise.resolve(42), 3)()).toBe(42);
8
+ });
9
+ it("retries after failure and resolves on later success", async () => {
10
+ let calls = 0;
11
+ globalExpect(await retry(() => {
12
+ calls++;
13
+ if (calls < 3) return Promise.reject(/* @__PURE__ */ new Error("fail"));
14
+ return Promise.resolve(calls);
15
+ }, 3)()).toBe(3);
16
+ globalExpect(calls).toBe(3);
17
+ });
18
+ it("rejects after exhausting all attempts", async () => {
19
+ const err = /* @__PURE__ */ new Error("always fails");
20
+ await globalExpect(retry(() => Promise.reject(err), 3)()).rejects.toBe(err);
21
+ });
22
+ it("rejects with the last error", async () => {
23
+ let i = 0;
24
+ const errors = [
25
+ /* @__PURE__ */ new Error("a"),
26
+ /* @__PURE__ */ new Error("b"),
27
+ /* @__PURE__ */ new Error("c")
28
+ ];
29
+ await globalExpect(retry(() => Promise.reject(errors[i++]), 3)()).rejects.toBe(errors[2]);
30
+ });
31
+ it("delays between failures but not after the last", async () => {
32
+ const delayFn = vi.fn().mockReturnValue(0);
33
+ await retry(() => Promise.reject(/* @__PURE__ */ new Error("x")), 3, delayFn)().catch(() => {});
34
+ globalExpect(delayFn).toHaveBeenCalledTimes(2);
35
+ });
36
+ it("supports dynamic delay via function — passes attempt index", async () => {
37
+ const delayFn = vi.fn().mockReturnValue(0);
38
+ await retry(() => Promise.reject(/* @__PURE__ */ new Error("x")), 3, delayFn)().catch(() => {});
39
+ globalExpect(delayFn).toHaveBeenNthCalledWith(1, 0);
40
+ globalExpect(delayFn).toHaveBeenNthCalledWith(2, 1);
41
+ });
42
+ it("onCleanup inside fn fires before each retry", async () => {
43
+ const cleaned = [];
44
+ let attempt = 0;
45
+ await retry(() => {
46
+ const i = attempt++;
47
+ onCleanup(() => cleaned.push(i));
48
+ if (i < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
49
+ return Promise.resolve(i);
50
+ }, 3)();
51
+ globalExpect(cleaned).toContain(0);
52
+ globalExpect(cleaned).toContain(1);
53
+ });
54
+ it("composes with async()", async () => {
55
+ const asyncOp = (await import("./async.mjs")).async;
56
+ let calls = 0;
57
+ const op = asyncOp(() => retry(() => {
58
+ calls++;
59
+ if (calls < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
60
+ return Promise.resolve(calls);
61
+ }, 3)());
62
+ op.start();
63
+ await op;
64
+ globalExpect(op.value).toBe(2);
65
+ op.stop();
66
+ });
67
+ it("AbortController cleanup fires between retries when composed with async()", async () => {
68
+ const asyncOp = (await import("./async.mjs")).async;
69
+ const aborted = [];
70
+ let attempt = 0;
71
+ const op = asyncOp(() => {
72
+ const i = attempt++;
73
+ onCleanup(() => aborted.push(i));
74
+ if (i < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
75
+ return Promise.resolve(i);
76
+ });
77
+ op.start();
78
+ await op.catch(() => {});
79
+ op.stop();
80
+ });
81
+ });
82
+ //#endregion
83
+ export {};
@@ -2,7 +2,7 @@ import { t as Computed } from "../index-DUshSQ_6.mjs";
2
2
 
3
3
  //#region src/utilities/timeout.d.ts
4
4
  type TimeoutResult = {
5
- isRunning: Computed<boolean>;
5
+ pending: Computed<boolean>;
6
6
  start(): void;
7
7
  stop(): void;
8
8
  reset(): void;
@@ -6,19 +6,19 @@ import { _ as signal, g as onCleanup } from "../signals-BHmWX6ox.mjs";
6
6
  * unless `immediate` is set to `false`.
7
7
  */
8
8
  function createTimeout(callback, delay, immediate = true) {
9
- const isRunning = signal(false);
9
+ const pending = signal(false);
10
10
  let id;
11
11
  const getDelay = typeof delay === "function" ? delay : () => delay;
12
12
  const stop = () => {
13
13
  clearTimeout(id);
14
14
  id = void 0;
15
- isRunning(false);
15
+ pending(false);
16
16
  };
17
17
  const start = () => {
18
- if (isRunning()) return;
19
- isRunning(true);
18
+ if (pending()) return;
19
+ pending(true);
20
20
  id = setTimeout(() => {
21
- isRunning(false);
21
+ pending(false);
22
22
  id = void 0;
23
23
  callback();
24
24
  }, getDelay());
@@ -31,7 +31,7 @@ function createTimeout(callback, delay, immediate = true) {
31
31
  const cleanup = () => stop();
32
32
  onCleanup(cleanup);
33
33
  return {
34
- isRunning,
34
+ pending,
35
35
  start,
36
36
  stop,
37
37
  reset,
@@ -14,7 +14,7 @@ describe("createTimeout", () => {
14
14
  effectScope(() => {
15
15
  t = createTimeout(cb, 500);
16
16
  });
17
- globalExpect(t.isRunning()).toBe(true);
17
+ globalExpect(t.pending()).toBe(true);
18
18
  });
19
19
  it("fires callback after delay", () => {
20
20
  vi.useFakeTimers();
@@ -25,14 +25,14 @@ describe("createTimeout", () => {
25
25
  vi.advanceTimersByTime(600);
26
26
  globalExpect(cb).toHaveBeenCalledOnce();
27
27
  });
28
- it("isRunning becomes false after firing", () => {
28
+ it("pending becomes false after firing", () => {
29
29
  vi.useFakeTimers();
30
30
  let t;
31
31
  effectScope(() => {
32
32
  t = createTimeout(vi.fn(), 200);
33
33
  });
34
34
  vi.advanceTimersByTime(300);
35
- globalExpect(t.isRunning()).toBe(false);
35
+ globalExpect(t.pending()).toBe(false);
36
36
  });
37
37
  it("stop() cancels the timeout", () => {
38
38
  vi.useFakeTimers();
@@ -59,6 +59,61 @@ describe("createTimeout", () => {
59
59
  vi.advanceTimersByTime(300);
60
60
  globalExpect(cb).toHaveBeenCalledOnce();
61
61
  });
62
+ it("immediate=false does not start automatically", () => {
63
+ vi.useFakeTimers();
64
+ const cb = vi.fn();
65
+ let t;
66
+ effectScope(() => {
67
+ t = createTimeout(cb, 200, false);
68
+ });
69
+ globalExpect(t.pending()).toBe(false);
70
+ vi.advanceTimersByTime(300);
71
+ globalExpect(cb).not.toHaveBeenCalled();
72
+ });
73
+ it("start() fires after delay when called manually", () => {
74
+ vi.useFakeTimers();
75
+ const cb = vi.fn();
76
+ let t;
77
+ effectScope(() => {
78
+ t = createTimeout(cb, 200, false);
79
+ });
80
+ t.start();
81
+ globalExpect(t.pending()).toBe(true);
82
+ vi.advanceTimersByTime(200);
83
+ globalExpect(cb).toHaveBeenCalledOnce();
84
+ globalExpect(t.pending()).toBe(false);
85
+ });
86
+ it("Symbol.dispose cancels the timeout", () => {
87
+ vi.useFakeTimers();
88
+ const cb = vi.fn();
89
+ let t;
90
+ effectScope(() => {
91
+ t = createTimeout(cb, 500);
92
+ });
93
+ t[Symbol.dispose]();
94
+ vi.advanceTimersByTime(1e3);
95
+ globalExpect(cb).not.toHaveBeenCalled();
96
+ });
97
+ it("stops when scope is disposed", () => {
98
+ vi.useFakeTimers();
99
+ const cb = vi.fn();
100
+ effectScope(() => {
101
+ createTimeout(cb, 200);
102
+ })();
103
+ vi.advanceTimersByTime(500);
104
+ globalExpect(cb).not.toHaveBeenCalled();
105
+ });
106
+ it("dynamic delay function is called at start", () => {
107
+ vi.useFakeTimers();
108
+ const cb = vi.fn();
109
+ const getDelay = vi.fn().mockReturnValue(300);
110
+ effectScope(() => {
111
+ createTimeout(cb, getDelay);
112
+ });
113
+ vi.advanceTimersByTime(300);
114
+ globalExpect(cb).toHaveBeenCalledOnce();
115
+ globalExpect(getDelay).toHaveBeenCalledOnce();
116
+ });
62
117
  });
63
118
  //#endregion
64
119
  export {};
@@ -0,0 +1,10 @@
1
+ import { t as Computed } from "../index-DUshSQ_6.mjs";
2
+
3
+ //#region src/utilities/window-focus.d.ts
4
+ /**
5
+ * Singleton `Computed<boolean>` — `true` while the browser window has focus.
6
+ * Reacts to `focus` / `blur` window events.
7
+ */
8
+ declare const windowFocused: Computed<boolean>;
9
+ //#endregion
10
+ export { windowFocused };
@@ -0,0 +1,16 @@
1
+ import { _ as signal } from "../signals-BHmWX6ox.mjs";
2
+ import { on } from "./event-listener.mjs";
3
+ //#region src/utilities/window-focus.ts
4
+ function createWindowFocused() {
5
+ const value = signal(typeof document !== "undefined" ? document.hasFocus() : true);
6
+ on(window, "focus", () => value(true));
7
+ on(window, "blur", () => value(false));
8
+ return value;
9
+ }
10
+ /**
11
+ * Singleton `Computed<boolean>` — `true` while the browser window has focus.
12
+ * Reacts to `focus` / `blur` window events.
13
+ */
14
+ const windowFocused = createWindowFocused();
15
+ //#endregion
16
+ export { windowFocused };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elements-kit",
3
3
  "type": "module",
4
- "version": "0.0.13",
4
+ "version": "0.0.15",
5
5
  "description": "A lightweight reactive UI library that transforms native HTMLElements into reactive components with signals. Ideal for framework-agnostic applications and web components.",
6
6
  "keywords": [
7
7
  "webcomponents",