@vizejs/composable 0.343.0 → 0.347.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/abort-signal.d.mts +96 -0
  2. package/dist/abort-signal.mjs +203 -0
  3. package/dist/async-resource.d.mts +97 -0
  4. package/dist/async-resource.mjs +99 -0
  5. package/dist/capability-BNpkvSy5.d.mts +93 -0
  6. package/dist/capability.d.mts +2 -0
  7. package/dist/capability.mjs +37 -0
  8. package/dist/catalog-CuWhR-0q.d.mts +506 -0
  9. package/dist/catalog-DOXTbk4r.mjs +529 -0
  10. package/dist/catalog.d.mts +2 -0
  11. package/dist/catalog.mjs +2 -0
  12. package/dist/disposal-scope.d.mts +93 -0
  13. package/dist/disposal-scope.mjs +119 -0
  14. package/dist/event-listener.d.mts +85 -0
  15. package/dist/event-listener.mjs +55 -0
  16. package/dist/index.d.mts +19 -786
  17. package/dist/index.mjs +19 -751
  18. package/dist/locale.d.mts +65 -0
  19. package/dist/locale.mjs +72 -0
  20. package/dist/media-query.d.mts +56 -0
  21. package/dist/media-query.mjs +56 -0
  22. package/dist/retry-async.d.mts +91 -0
  23. package/dist/retry-async.mjs +136 -0
  24. package/dist/retry-delay.d.mts +73 -0
  25. package/dist/retry-delay.mjs +50 -0
  26. package/dist/scope.d.mts +18 -0
  27. package/dist/scope.mjs +23 -0
  28. package/dist/timeout-scheduler.d.mts +18 -0
  29. package/dist/timeout-scheduler.mjs +1 -0
  30. package/dist/use-counter.d.mts +91 -0
  31. package/dist/use-counter.mjs +67 -0
  32. package/dist/use-debounced.d.mts +87 -0
  33. package/dist/use-debounced.mjs +98 -0
  34. package/dist/use-history.d.mts +105 -0
  35. package/dist/use-history.mjs +137 -0
  36. package/dist/use-previous.d.mts +43 -0
  37. package/dist/use-previous.mjs +11 -0
  38. package/dist/use-throttled.d.mts +107 -0
  39. package/dist/use-throttled.mjs +124 -0
  40. package/dist/use-toggle.d.mts +45 -0
  41. package/dist/use-toggle.mjs +34 -0
  42. package/package.json +91 -1
@@ -0,0 +1,91 @@
1
+ import { ComputedRef, ShallowRef } from "vue";
2
+
3
+ //#region src/use-counter.d.ts
4
+ /** Options for {@link useCounter}. */
5
+ interface UseCounterOptions {
6
+ /**
7
+ * Inclusive lower bound applied to every value the counter takes.
8
+ *
9
+ * @default Number.NEGATIVE_INFINITY
10
+ */
11
+ readonly min?: number;
12
+ /**
13
+ * Inclusive upper bound applied to every value the counter takes.
14
+ *
15
+ * @default Number.POSITIVE_INFINITY
16
+ */
17
+ readonly max?: number;
18
+ }
19
+ /** Reactive controls returned by {@link useCounter}. */
20
+ interface CounterControls {
21
+ /** Current count. Changes only through the controls, never by assignment. */
22
+ readonly count: Readonly<ShallowRef<number>>;
23
+ /** Whether the count currently sits on the configured lower bound. */
24
+ readonly atMin: ComputedRef<boolean>;
25
+ /** Whether the count currently sits on the configured upper bound. */
26
+ readonly atMax: ComputedRef<boolean>;
27
+ /**
28
+ * Add `delta` (default `1`) to the count and clamp into the bounds.
29
+ *
30
+ * @param delta Amount added; may be negative or infinite.
31
+ * @returns The count after clamping.
32
+ */
33
+ readonly increment: (delta?: number) => number;
34
+ /**
35
+ * Subtract `delta` (default `1`) from the count and clamp into the bounds.
36
+ *
37
+ * @param delta Amount subtracted; may be negative or infinite.
38
+ * @returns The count after clamping.
39
+ */
40
+ readonly decrement: (delta?: number) => number;
41
+ /**
42
+ * Assign a value directly, clamped into the bounds.
43
+ *
44
+ * @returns The count after clamping.
45
+ */
46
+ readonly set: (value: number) => number;
47
+ /**
48
+ * Restore the reset baseline, or establish a new one.
49
+ *
50
+ * Without an argument the count returns to the creation-time initial value
51
+ * (after its original clamping). With an argument, the clamped value
52
+ * becomes both the new count and the baseline used by later `reset()`
53
+ * calls.
54
+ *
55
+ * @param value Replacement baseline.
56
+ * @returns The count after clamping.
57
+ */
58
+ readonly reset: (value?: number) => number;
59
+ }
60
+ /**
61
+ * Create a clamped counter whose every transition stays inside `[min, max]`.
62
+ *
63
+ * All operations clamp instead of failing, including the initial value, so
64
+ * the count is inside the bounds at every observable moment. Only `NaN` is
65
+ * rejected — silently corrupting the count is never an option. Purely
66
+ * synchronous state: safe during server rendering (no browser globals, no
67
+ * timers) and nothing to dispose, so it works inside and outside reactive
68
+ * scopes alike. Bounds are fixed at creation and not reactive.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
73
+ * increment(); // 10
74
+ * increment(); // 10 (clamped)
75
+ * atMax.value; // true
76
+ * ```
77
+ *
78
+ * @param initial Count before any operation, clamped into the bounds.
79
+ * @default initial 0
80
+ * @param options Inclusive bounds for every value the counter takes.
81
+ * @default options {}
82
+ * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
83
+ * bound is `NaN` or `min` exceeds `max`.
84
+ * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
85
+ * initial value, operand, or arithmetic result is `NaN` (for example
86
+ * incrementing `-Infinity` by `Infinity`); the count is left unchanged.
87
+ * @returns Reactive count, bound flags, and mutation controls.
88
+ */
89
+ declare function useCounter(initial?: number, options?: UseCounterOptions): CounterControls;
90
+ //#endregion
91
+ export { CounterControls, UseCounterOptions, useCounter };
@@ -0,0 +1,67 @@
1
+ import { computed, shallowRef } from "vue";
2
+ //#region src/use-counter.ts
3
+ /**
4
+ * Create a clamped counter whose every transition stays inside `[min, max]`.
5
+ *
6
+ * All operations clamp instead of failing, including the initial value, so
7
+ * the count is inside the bounds at every observable moment. Only `NaN` is
8
+ * rejected — silently corrupting the count is never an option. Purely
9
+ * synchronous state: safe during server rendering (no browser globals, no
10
+ * timers) and nothing to dispose, so it works inside and outside reactive
11
+ * scopes alike. Bounds are fixed at creation and not reactive.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
16
+ * increment(); // 10
17
+ * increment(); // 10 (clamped)
18
+ * atMax.value; // true
19
+ * ```
20
+ *
21
+ * @param initial Count before any operation, clamped into the bounds.
22
+ * @default initial 0
23
+ * @param options Inclusive bounds for every value the counter takes.
24
+ * @default options {}
25
+ * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
26
+ * bound is `NaN` or `min` exceeds `max`.
27
+ * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
28
+ * initial value, operand, or arithmetic result is `NaN` (for example
29
+ * incrementing `-Infinity` by `Infinity`); the count is left unchanged.
30
+ * @returns Reactive count, bound flags, and mutation controls.
31
+ */
32
+ function useCounter(initial = 0, options = {}) {
33
+ const min = requireBound(options.min ?? Number.NEGATIVE_INFINITY, "min");
34
+ const max = requireBound(options.max ?? Number.POSITIVE_INFINITY, "max");
35
+ if (min > max) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] min must not exceed max; received min ${String(min)} and max ${String(max)}`);
36
+ const clamp = (value) => Math.min(max, Math.max(min, value));
37
+ const count = shallowRef(clamp(requireValue(initial)));
38
+ let baseline = count.value;
39
+ const setClamped = (next) => {
40
+ count.value = clamp(requireValue(next));
41
+ return count.value;
42
+ };
43
+ const reset = (value) => {
44
+ const applied = setClamped(value ?? baseline);
45
+ if (value !== void 0) baseline = applied;
46
+ return applied;
47
+ };
48
+ return {
49
+ count,
50
+ atMin: computed(() => count.value === min),
51
+ atMax: computed(() => count.value === max),
52
+ increment: (delta = 1) => setClamped(count.value + requireValue(delta)),
53
+ decrement: (delta = 1) => setClamped(count.value - requireValue(delta)),
54
+ set: setClamped,
55
+ reset
56
+ };
57
+ }
58
+ function requireBound(value, label) {
59
+ if (Number.isNaN(value)) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] ${label} must not be NaN`);
60
+ return value;
61
+ }
62
+ function requireValue(value) {
63
+ if (Number.isNaN(value)) throw new RangeError("[VIZE_COMPOSE_COUNTER_INVALID_VALUE] the value is NaN; counter state was left unchanged");
64
+ return value;
65
+ }
66
+ //#endregion
67
+ export { useCounter };
@@ -0,0 +1,87 @@
1
+ import { TimeoutScheduler } from "./timeout-scheduler.mjs";
2
+ import { MaybeRefOrGetter, ShallowRef } from "vue";
3
+
4
+ //#region src/use-debounced.d.ts
5
+ /** Options for {@link useDebounced}. */
6
+ interface UseDebouncedOptions {
7
+ /**
8
+ * Applies the timing policy when no browser `window` is available.
9
+ *
10
+ * Keep this disabled during server rendering, where the debounced view
11
+ * mirrors the source synchronously instead of starting timers. Enable it
12
+ * for native, desktop, worker, and test runtimes whose scheduler is
13
+ * lifecycle-bound.
14
+ *
15
+ * @default false
16
+ */
17
+ readonly runOnServer?: boolean;
18
+ /**
19
+ * Owns the single-shot timer.
20
+ *
21
+ * @default globalThis timer functions
22
+ */
23
+ readonly scheduler?: TimeoutScheduler;
24
+ }
25
+ /** Reactive debounced view and controls returned by {@link useDebounced}. */
26
+ interface DebouncedControls<Value> {
27
+ /** Readonly view of the source that settles `waitMs` after the last change. */
28
+ readonly debounced: Readonly<ShallowRef<Value>>;
29
+ /** Whether a trailing update is currently scheduled. */
30
+ readonly pending: Readonly<ShallowRef<boolean>>;
31
+ /**
32
+ * Discard the scheduled trailing update and keep the last settled value.
33
+ * Later source changes debounce again as usual.
34
+ *
35
+ * @returns Whether a scheduled update was discarded.
36
+ */
37
+ readonly cancel: () => boolean;
38
+ /**
39
+ * Apply the current source value immediately instead of waiting out the
40
+ * delay.
41
+ *
42
+ * @returns Whether a scheduled update was applied.
43
+ */
44
+ readonly flush: () => boolean;
45
+ }
46
+ /**
47
+ * Create a readonly debounced view of a reactive source.
48
+ *
49
+ * The view starts at the current source value. Each source change (observed
50
+ * with `flush: "sync"`, so every synchronous write counts) restarts a
51
+ * single-shot timer of `waitMs` milliseconds; when it fires, the view takes
52
+ * the source value current at that moment. `waitMs` is reactive and is read
53
+ * when a timer is scheduled; changing it does not restart an already-pending
54
+ * timer. A wait of `0` still defers to the next scheduler tick.
55
+ *
56
+ * Server rendering is explicit: without a browser `window` (and with
57
+ * {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
58
+ * the view mirrors the source synchronously, so server-rendered output shows
59
+ * current values and nothing leaks. `pending` stays `false` and the controls
60
+ * report `false` in that mode.
61
+ *
62
+ * Cleanup rule: the watcher and any pending timer are released when the
63
+ * owning reactive scope stops; call inside an active scope. Outside one, the
64
+ * watcher lives as long as the source and `cancel()` only clears the pending
65
+ * timer.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const query = shallowRef("");
70
+ * const { debounced, flush } = useDebounced(query, 300);
71
+ * query.value = "vize"; // debounced.value still "" for 300ms
72
+ * flush(); // debounced.value === "vize" immediately
73
+ * ```
74
+ *
75
+ * @param source Reactive source to debounce.
76
+ * @param waitMs Reactive delay in milliseconds; must be finite and at least
77
+ * zero. Fractions are truncated.
78
+ * @param options Runtime scheduling overrides.
79
+ * @default options {}
80
+ * @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
81
+ * resolved wait is not finite or is negative, both synchronously at creation
82
+ * (even in mirror mode) and again for every scheduled delay.
83
+ * @returns Readonly debounced view, pending flag, and cancel/flush controls.
84
+ */
85
+ declare function useDebounced<Value>(source: MaybeRefOrGetter<Value>, waitMs: MaybeRefOrGetter<number>, options?: UseDebouncedOptions): DebouncedControls<Value>;
86
+ //#endregion
87
+ export { DebouncedControls, UseDebouncedOptions, useDebounced };
@@ -0,0 +1,98 @@
1
+ import { tryOnScopeDispose } from "./scope.mjs";
2
+ import { shallowRef, toValue, watch } from "vue";
3
+ //#region src/use-debounced.ts
4
+ const defaultScheduler = {
5
+ setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
6
+ clearTimeout: (handle) => {
7
+ globalThis.clearTimeout(handle);
8
+ }
9
+ };
10
+ /**
11
+ * Create a readonly debounced view of a reactive source.
12
+ *
13
+ * The view starts at the current source value. Each source change (observed
14
+ * with `flush: "sync"`, so every synchronous write counts) restarts a
15
+ * single-shot timer of `waitMs` milliseconds; when it fires, the view takes
16
+ * the source value current at that moment. `waitMs` is reactive and is read
17
+ * when a timer is scheduled; changing it does not restart an already-pending
18
+ * timer. A wait of `0` still defers to the next scheduler tick.
19
+ *
20
+ * Server rendering is explicit: without a browser `window` (and with
21
+ * {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
22
+ * the view mirrors the source synchronously, so server-rendered output shows
23
+ * current values and nothing leaks. `pending` stays `false` and the controls
24
+ * report `false` in that mode.
25
+ *
26
+ * Cleanup rule: the watcher and any pending timer are released when the
27
+ * owning reactive scope stops; call inside an active scope. Outside one, the
28
+ * watcher lives as long as the source and `cancel()` only clears the pending
29
+ * timer.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const query = shallowRef("");
34
+ * const { debounced, flush } = useDebounced(query, 300);
35
+ * query.value = "vize"; // debounced.value still "" for 300ms
36
+ * flush(); // debounced.value === "vize" immediately
37
+ * ```
38
+ *
39
+ * @param source Reactive source to debounce.
40
+ * @param waitMs Reactive delay in milliseconds; must be finite and at least
41
+ * zero. Fractions are truncated.
42
+ * @param options Runtime scheduling overrides.
43
+ * @default options {}
44
+ * @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
45
+ * resolved wait is not finite or is negative, both synchronously at creation
46
+ * (even in mirror mode) and again for every scheduled delay.
47
+ * @returns Readonly debounced view, pending flag, and cancel/flush controls.
48
+ */
49
+ function useDebounced(source, waitMs, options = {}) {
50
+ const scheduler = options.scheduler ?? defaultScheduler;
51
+ const debounced = shallowRef(toValue(source));
52
+ const pending = shallowRef(false);
53
+ let handle;
54
+ resolveWaitMs(toValue(waitMs));
55
+ const apply = () => {
56
+ handle = void 0;
57
+ pending.value = false;
58
+ debounced.value = toValue(source);
59
+ };
60
+ const cancel = () => {
61
+ if (!pending.value) return false;
62
+ scheduler.clearTimeout(handle);
63
+ handle = void 0;
64
+ pending.value = false;
65
+ return true;
66
+ };
67
+ const flush = () => {
68
+ if (!pending.value) return false;
69
+ scheduler.clearTimeout(handle);
70
+ apply();
71
+ return true;
72
+ };
73
+ watch(() => toValue(source), (next) => {
74
+ if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
75
+ debounced.value = next;
76
+ return;
77
+ }
78
+ const delayMs = resolveWaitMs(toValue(waitMs));
79
+ if (pending.value) scheduler.clearTimeout(handle);
80
+ pending.value = true;
81
+ handle = scheduler.setTimeout(apply, delayMs);
82
+ }, { flush: "sync" });
83
+ tryOnScopeDispose(() => {
84
+ cancel();
85
+ });
86
+ return {
87
+ debounced,
88
+ pending,
89
+ cancel,
90
+ flush
91
+ };
92
+ }
93
+ function resolveWaitMs(value) {
94
+ if (!Number.isFinite(value) || value < 0) throw new RangeError(`[VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT] waitMs must be finite and at least zero; received ${String(value)}`);
95
+ return Math.trunc(value);
96
+ }
97
+ //#endregion
98
+ export { useDebounced };
@@ -0,0 +1,105 @@
1
+ import { ComputedRef, Ref } from "vue";
2
+
3
+ //#region src/use-history.d.ts
4
+ /** Options for {@link useHistory}. */
5
+ interface UseHistoryOptions<Value> {
6
+ /**
7
+ * Maximum number of undo entries retained; recording a change beyond it
8
+ * drops the oldest entry. The redo stack is bounded by construction, since
9
+ * redo entries only ever come from undone changes. Must be an integer
10
+ * greater than zero and is fixed at creation.
11
+ *
12
+ * @default 100
13
+ */
14
+ readonly capacity?: number;
15
+ /**
16
+ * Clone applied to every value captured into history and to every value
17
+ * restored out of it, isolating snapshots from later in-place mutation.
18
+ *
19
+ * @default identity — values are stored and restored by reference
20
+ */
21
+ readonly clone?: (value: Value) => Value;
22
+ }
23
+ /** Reactive undo/redo controls returned by {@link useHistory}. */
24
+ interface HistoryControls {
25
+ /** Whether {@link HistoryControls.undo} currently has an entry to restore. */
26
+ readonly canUndo: ComputedRef<boolean>;
27
+ /** Whether {@link HistoryControls.redo} currently has an entry to restore. */
28
+ readonly canRedo: ComputedRef<boolean>;
29
+ /** Number of retained undo entries. */
30
+ readonly undoCount: ComputedRef<number>;
31
+ /** Number of retained redo entries. */
32
+ readonly redoCount: ComputedRef<number>;
33
+ /**
34
+ * Restore the newest undo entry and move the current value onto the redo
35
+ * stack. The restoring write is not recorded.
36
+ *
37
+ * @returns Whether an entry was restored.
38
+ */
39
+ readonly undo: () => boolean;
40
+ /**
41
+ * Restore the newest redo entry and move the current value back onto the
42
+ * undo stack. The restoring write is not recorded.
43
+ *
44
+ * @returns Whether an entry was restored.
45
+ */
46
+ readonly redo: () => boolean;
47
+ /**
48
+ * Group every source write inside `run` into at most one undo entry.
49
+ *
50
+ * The entry restores the value from just before the batch. It is committed
51
+ * only when the final value differs (`Object.is`) from the starting value,
52
+ * and it is committed even when `run` throws, so a partially applied batch
53
+ * stays undoable as one step. Nested calls collapse into the outermost
54
+ * batch. The callback's return value is passed through.
55
+ */
56
+ readonly batch: <Result>(run: () => Result) => Result;
57
+ /** Drop every undo and redo entry while keeping the current value. */
58
+ readonly clear: () => void;
59
+ }
60
+ /**
61
+ * Record bounded undo/redo history over the writes of a ref.
62
+ *
63
+ * Recording is shallow and identity-based, matching Vue's own change
64
+ * detection: assignments to `source.value` are recorded (observed with
65
+ * `flush: "sync"`, so every synchronous write counts), writes that are
66
+ * `Object.is`-equal to the current value are not changes, and in-place
67
+ * mutations of object values are invisible — pair mutable values with
68
+ * {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
69
+ * values through `clone` as well, so snapshots never share identity with the
70
+ * live value unless the default identity clone is kept. When a
71
+ * user-provided `clone` throws, the failed operation leaves history
72
+ * unchanged and the error propagates.
73
+ *
74
+ * Safe during server rendering: no browser globals are read and no timers
75
+ * start. Cleanup rule: when the owning reactive scope stops, recording stops
76
+ * and every retained snapshot is released, so `undo`/`redo` return `false`
77
+ * afterwards; call inside an active scope, or the watcher lives as long as
78
+ * the source.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const text = shallowRef("");
83
+ * const { undo, redo, batch } = useHistory(text);
84
+ * text.value = "a";
85
+ * batch(() => {
86
+ * text.value = "ab";
87
+ * text.value = "abc";
88
+ * });
89
+ * undo(); // text.value === "a" (the batch is one step)
90
+ * redo(); // text.value === "abc"
91
+ * ```
92
+ *
93
+ * @param source Ref whose writes are recorded.
94
+ * @param options Retention bound and snapshot cloning.
95
+ * @default options {}
96
+ * @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
97
+ * the capacity is not an integer greater than zero.
98
+ * @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
99
+ * `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
100
+ * stack movement would corrupt the pending group.
101
+ * @returns Reactive undo/redo state and controls.
102
+ */
103
+ declare function useHistory<Value>(source: Ref<Value>, options?: UseHistoryOptions<Value>): HistoryControls;
104
+ //#endregion
105
+ export { HistoryControls, UseHistoryOptions, useHistory };
@@ -0,0 +1,137 @@
1
+ import { tryOnScopeDispose } from "./scope.mjs";
2
+ import { computed, shallowRef, watch } from "vue";
3
+ //#region src/use-history.ts
4
+ /**
5
+ * Record bounded undo/redo history over the writes of a ref.
6
+ *
7
+ * Recording is shallow and identity-based, matching Vue's own change
8
+ * detection: assignments to `source.value` are recorded (observed with
9
+ * `flush: "sync"`, so every synchronous write counts), writes that are
10
+ * `Object.is`-equal to the current value are not changes, and in-place
11
+ * mutations of object values are invisible — pair mutable values with
12
+ * {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
13
+ * values through `clone` as well, so snapshots never share identity with the
14
+ * live value unless the default identity clone is kept. When a
15
+ * user-provided `clone` throws, the failed operation leaves history
16
+ * unchanged and the error propagates.
17
+ *
18
+ * Safe during server rendering: no browser globals are read and no timers
19
+ * start. Cleanup rule: when the owning reactive scope stops, recording stops
20
+ * and every retained snapshot is released, so `undo`/`redo` return `false`
21
+ * afterwards; call inside an active scope, or the watcher lives as long as
22
+ * the source.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const text = shallowRef("");
27
+ * const { undo, redo, batch } = useHistory(text);
28
+ * text.value = "a";
29
+ * batch(() => {
30
+ * text.value = "ab";
31
+ * text.value = "abc";
32
+ * });
33
+ * undo(); // text.value === "a" (the batch is one step)
34
+ * redo(); // text.value === "abc"
35
+ * ```
36
+ *
37
+ * @param source Ref whose writes are recorded.
38
+ * @param options Retention bound and snapshot cloning.
39
+ * @default options {}
40
+ * @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
41
+ * the capacity is not an integer greater than zero.
42
+ * @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
43
+ * `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
44
+ * stack movement would corrupt the pending group.
45
+ * @returns Reactive undo/redo state and controls.
46
+ */
47
+ function useHistory(source, options = {}) {
48
+ const capacity = options.capacity ?? 100;
49
+ if (!Number.isInteger(capacity) || capacity < 1) throw new RangeError(`[VIZE_COMPOSE_HISTORY_INVALID_CAPACITY] capacity must be an integer greater than zero; received ${String(capacity)}`);
50
+ const clone = options.clone ?? ((value) => value);
51
+ const undoStack = shallowRef([]);
52
+ const redoStack = shallowRef([]);
53
+ let restoring = false;
54
+ let batchDepth = 0;
55
+ let activeBatch;
56
+ const pushUndo = (entry) => {
57
+ const next = [...undoStack.value, entry];
58
+ undoStack.value = next.length > capacity ? next.slice(next.length - capacity) : next;
59
+ redoStack.value = [];
60
+ };
61
+ const writeSilently = (value) => {
62
+ restoring = true;
63
+ try {
64
+ source.value = value;
65
+ } finally {
66
+ restoring = false;
67
+ }
68
+ };
69
+ const requireOutsideBatch = (operation) => {
70
+ if (batchDepth > 0) throw new Error(`[VIZE_COMPOSE_HISTORY_IN_BATCH] ${operation}() is not available inside batch()`);
71
+ };
72
+ const undo = () => {
73
+ requireOutsideBatch("undo");
74
+ const entry = undoStack.value.at(-1);
75
+ if (entry === void 0) return false;
76
+ const restored = clone(entry.value);
77
+ const recorded = { value: clone(source.value) };
78
+ undoStack.value = undoStack.value.slice(0, -1);
79
+ redoStack.value = [...redoStack.value, recorded];
80
+ writeSilently(restored);
81
+ return true;
82
+ };
83
+ const redo = () => {
84
+ requireOutsideBatch("redo");
85
+ const entry = redoStack.value.at(-1);
86
+ if (entry === void 0) return false;
87
+ const restored = clone(entry.value);
88
+ const recorded = { value: clone(source.value) };
89
+ redoStack.value = redoStack.value.slice(0, -1);
90
+ undoStack.value = [...undoStack.value, recorded];
91
+ writeSilently(restored);
92
+ return true;
93
+ };
94
+ const batch = (run) => {
95
+ if (batchDepth === 0) activeBatch = {
96
+ raw: source.value,
97
+ entry: { value: clone(source.value) }
98
+ };
99
+ batchDepth += 1;
100
+ try {
101
+ return run();
102
+ } finally {
103
+ batchDepth -= 1;
104
+ if (batchDepth === 0 && activeBatch !== void 0) {
105
+ const finished = activeBatch;
106
+ activeBatch = void 0;
107
+ if (!Object.is(finished.raw, source.value)) pushUndo(finished.entry);
108
+ }
109
+ }
110
+ };
111
+ const clear = () => {
112
+ requireOutsideBatch("clear");
113
+ undoStack.value = [];
114
+ redoStack.value = [];
115
+ };
116
+ const handle = watch(source, (_next, replaced) => {
117
+ if (restoring || batchDepth > 0) return;
118
+ pushUndo({ value: clone(replaced) });
119
+ }, { flush: "sync" });
120
+ tryOnScopeDispose(() => {
121
+ handle.stop();
122
+ undoStack.value = [];
123
+ redoStack.value = [];
124
+ });
125
+ return {
126
+ canUndo: computed(() => undoStack.value.length > 0),
127
+ canRedo: computed(() => redoStack.value.length > 0),
128
+ undoCount: computed(() => undoStack.value.length),
129
+ redoCount: computed(() => redoStack.value.length),
130
+ undo,
131
+ redo,
132
+ batch,
133
+ clear
134
+ };
135
+ }
136
+ //#endregion
137
+ export { useHistory };
@@ -0,0 +1,43 @@
1
+ import { MaybeRefOrGetter, ShallowRef } from "vue";
2
+
3
+ //#region src/use-previous.d.ts
4
+ /**
5
+ * Track the value a reactive source held before its latest change.
6
+ *
7
+ * The semantics are precise and arity-based:
8
+ *
9
+ * - Without an `initial` argument the ref holds `undefined` until the source
10
+ * changes for the first time.
11
+ * - With an `initial` argument (including an explicit `undefined` when
12
+ * `Value` allows it) the ref holds that value until the first change, and
13
+ * the return type never widens with `undefined`.
14
+ * - Every synchronous write is observed (`flush: "sync"`), so a sequence of
15
+ * writes in one tick shifts the previous value step by step instead of
16
+ * collapsing into one batch.
17
+ * - Writes whose value is `Object.is`-equal to the current value do not
18
+ * count as changes, matching Vue's own change detection.
19
+ * - Tracking is shallow: reassignments are observed, in-place mutations of
20
+ * object values are not.
21
+ *
22
+ * Safe during server rendering: no browser globals are read and no timers
23
+ * start. The underlying watcher is bound to the current reactive scope and
24
+ * stops with it; call inside an active scope, or the watcher lives as long
25
+ * as the source. A plain non-reactive source never changes, so the ref stays
26
+ * at its initial value.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const route = shallowRef("/home");
31
+ * const previousRoute = usePrevious(route, "/");
32
+ * route.value = "/settings";
33
+ * previousRoute.value; // "/home"
34
+ * ```
35
+ *
36
+ * @param source Reactive source to observe.
37
+ * @param initial Value reported before the first change.
38
+ * @returns Readonly shallow ref holding the previous source value.
39
+ */
40
+ declare function usePrevious<Value>(source: MaybeRefOrGetter<Value>): Readonly<ShallowRef<Value | undefined>>;
41
+ declare function usePrevious<Value>(source: MaybeRefOrGetter<Value>, initial: Value): Readonly<ShallowRef<Value>>;
42
+ //#endregion
43
+ export { usePrevious };
@@ -0,0 +1,11 @@
1
+ import { shallowRef, toValue, watch } from "vue";
2
+ //#region src/use-previous.ts
3
+ function usePrevious(source, ...initial) {
4
+ const previous = shallowRef(initial.length === 1 ? initial[0] : void 0);
5
+ watch(() => toValue(source), (_next, replaced) => {
6
+ previous.value = replaced;
7
+ }, { flush: "sync" });
8
+ return previous;
9
+ }
10
+ //#endregion
11
+ export { usePrevious };