@vizejs/composable 0.302.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +228 -0
- package/dist/index.mjs +244 -0
- package/dist/temporal.d.mts +90 -0
- package/dist/temporal.mjs +61 -0
- package/package.json +69 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { ComputedRef, MaybeRefOrGetter, Ref, ShallowRef } from "vue";
|
|
2
|
+
|
|
3
|
+
//#region src/async-resource.d.ts
|
|
4
|
+
/** Lifecycle state of an asynchronous resource. */
|
|
5
|
+
type AsyncResourceStatus = "idle" | "pending" | "success" | "error" | "cancelled";
|
|
6
|
+
/** Context supplied to an asynchronous resource loader. */
|
|
7
|
+
interface AsyncResourceContext {
|
|
8
|
+
/** Signal aborted by cancellation, reset, scope disposal, or a newer execution. */
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/** Explicit result of one asynchronous resource execution. */
|
|
12
|
+
type AsyncResourceExecution<Data, Failure> = {
|
|
13
|
+
readonly status: "success";
|
|
14
|
+
readonly data: Data;
|
|
15
|
+
} | {
|
|
16
|
+
readonly status: "error";
|
|
17
|
+
readonly error: Failure;
|
|
18
|
+
} | {
|
|
19
|
+
readonly status: "cancelled";
|
|
20
|
+
readonly reason: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
readonly status: "superseded";
|
|
23
|
+
};
|
|
24
|
+
/** Options for {@link useAsyncResource}. */
|
|
25
|
+
interface UseAsyncResourceOptions<Data> {
|
|
26
|
+
/**
|
|
27
|
+
* Initial data restored by {@link AsyncResource.reset}.
|
|
28
|
+
*
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
readonly initialData?: Data;
|
|
32
|
+
/**
|
|
33
|
+
* Abort the active execution when a newer execution starts.
|
|
34
|
+
*
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
readonly cancelPrevious?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Retain the current data while a new execution is pending.
|
|
40
|
+
*
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
readonly keepData?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Cancel an active execution when the current reactive scope is disposed.
|
|
46
|
+
*
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
readonly scope?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Reactive state and controls for an asynchronous loader. */
|
|
52
|
+
interface AsyncResource<Data, Arguments extends readonly unknown[], Failure> {
|
|
53
|
+
readonly data: Readonly<ShallowRef<Data | undefined>>;
|
|
54
|
+
readonly error: Readonly<ShallowRef<Failure | undefined>>;
|
|
55
|
+
readonly status: Readonly<Ref<AsyncResourceStatus>>;
|
|
56
|
+
readonly pending: ComputedRef<boolean>;
|
|
57
|
+
readonly execute: (...arguments_: Arguments) => Promise<AsyncResourceExecution<Data, Failure>>;
|
|
58
|
+
readonly cancel: (reason?: unknown) => boolean;
|
|
59
|
+
readonly reset: () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create a scoped, abortable asynchronous resource with latest-result-wins
|
|
63
|
+
* state. Every execution returns a discriminated result, so cancellation,
|
|
64
|
+
* supersession, loader failure, and successful `undefined` data stay distinct.
|
|
65
|
+
*/
|
|
66
|
+
declare function useAsyncResource<Data, Arguments extends readonly unknown[], Failure = unknown>(loader: (context: AsyncResourceContext, ...arguments_: Arguments) => Promise<Data>, options?: UseAsyncResourceOptions<Data>): AsyncResource<Data, Arguments, Failure>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/event-listener.d.ts
|
|
69
|
+
/** Options for {@link useEventListener}. */
|
|
70
|
+
interface UseEventListenerOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Invoke the listener during the capture phase.
|
|
73
|
+
*
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
readonly capture?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Stop listening after the first event.
|
|
79
|
+
*
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
readonly once?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Declare that the listener does not cancel the event's default action.
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
readonly passive?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Stop listening when this signal is aborted.
|
|
91
|
+
*
|
|
92
|
+
* @default undefined
|
|
93
|
+
*/
|
|
94
|
+
readonly signal?: AbortSignal;
|
|
95
|
+
/**
|
|
96
|
+
* Start listening during composable creation.
|
|
97
|
+
*
|
|
98
|
+
* @default true
|
|
99
|
+
*/
|
|
100
|
+
readonly immediate?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Reactive target update timing.
|
|
103
|
+
*
|
|
104
|
+
* @default "pre"
|
|
105
|
+
*/
|
|
106
|
+
readonly flush?: "pre" | "post" | "sync";
|
|
107
|
+
}
|
|
108
|
+
/** Reactive controls returned by {@link useEventListener}. */
|
|
109
|
+
interface EventListenerControls {
|
|
110
|
+
/** Whether a concrete target currently owns the listener. */
|
|
111
|
+
readonly isListening: Readonly<Ref<boolean>>;
|
|
112
|
+
/**
|
|
113
|
+
* Begin listening.
|
|
114
|
+
*
|
|
115
|
+
* @returns Whether a new reactive listener was started.
|
|
116
|
+
*/
|
|
117
|
+
readonly start: () => boolean;
|
|
118
|
+
/** Stop listening. Repeated calls are safe. */
|
|
119
|
+
readonly stop: () => void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Attach an event listener to a reactive target and clean it up with the
|
|
123
|
+
* current reactive scope. Missing targets are valid during server rendering.
|
|
124
|
+
*
|
|
125
|
+
* @param target Reactive event target.
|
|
126
|
+
* @param event Event name.
|
|
127
|
+
* @param listener Typed event listener.
|
|
128
|
+
* @param options Listener lifecycle and scheduling options.
|
|
129
|
+
* @default options {}
|
|
130
|
+
*/
|
|
131
|
+
declare function useEventListener<Key extends keyof WindowEventMap>(target: MaybeRefOrGetter<Window | null | undefined>, event: Key, listener: (event: WindowEventMap[Key]) => void, options?: UseEventListenerOptions): EventListenerControls;
|
|
132
|
+
declare function useEventListener<Key extends keyof DocumentEventMap>(target: MaybeRefOrGetter<Document | null | undefined>, event: Key, listener: (event: DocumentEventMap[Key]) => void, options?: UseEventListenerOptions): EventListenerControls;
|
|
133
|
+
declare function useEventListener<Key extends keyof HTMLElementEventMap>(target: MaybeRefOrGetter<HTMLElement | null | undefined>, event: Key, listener: (event: HTMLElementEventMap[Key]) => void, options?: UseEventListenerOptions): EventListenerControls;
|
|
134
|
+
declare function useEventListener(target: MaybeRefOrGetter<EventTarget | null | undefined>, event: string, listener: EventListener, options?: UseEventListenerOptions): EventListenerControls;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/locale.d.ts
|
|
137
|
+
/** Text flow reported by the internationalization runtime. */
|
|
138
|
+
type TextDirection = "ltr" | "rtl";
|
|
139
|
+
/** Locale selection options for {@link useLocale}. */
|
|
140
|
+
interface UseLocaleOptions {
|
|
141
|
+
/**
|
|
142
|
+
* Locale detector used when the reactive source has no value.
|
|
143
|
+
*
|
|
144
|
+
* @default navigator.language when available; otherwise undefined
|
|
145
|
+
*/
|
|
146
|
+
readonly detect?: () => Intl.Locale | string | null | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Locale used when neither the source nor detector provides one.
|
|
149
|
+
*
|
|
150
|
+
* @default "en"
|
|
151
|
+
*/
|
|
152
|
+
readonly fallback?: Intl.Locale | string;
|
|
153
|
+
}
|
|
154
|
+
/** Reactive locale metadata and cached formatter factories. */
|
|
155
|
+
interface LocaleControls {
|
|
156
|
+
/** Canonical Unicode locale identifier. */
|
|
157
|
+
readonly locale: ComputedRef<string>;
|
|
158
|
+
/** Parsed locale details supplied by the internationalization runtime. */
|
|
159
|
+
readonly details: ComputedRef<Intl.Locale>;
|
|
160
|
+
/** Native writing direction for the active locale. */
|
|
161
|
+
readonly direction: ComputedRef<TextDirection>;
|
|
162
|
+
/** Return a cached number formatter for the active locale and options. */
|
|
163
|
+
readonly number: (options?: Intl.NumberFormatOptions) => Intl.NumberFormat;
|
|
164
|
+
/** Return a cached date and time formatter for the active locale and options. */
|
|
165
|
+
readonly dateTime: (options?: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
|
|
166
|
+
/** Return a cached list formatter for the active locale and options. */
|
|
167
|
+
readonly list: (options?: Intl.ListFormatOptions) => Intl.ListFormat;
|
|
168
|
+
/** Return a cached relative-time formatter for the active locale and options. */
|
|
169
|
+
readonly relativeTime: (options?: Intl.RelativeTimeFormatOptions) => Intl.RelativeTimeFormat;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Create reactive locale metadata and platform-native formatter factories.
|
|
173
|
+
*
|
|
174
|
+
* Equivalent formatter options reuse instances. The bounded cache follows the
|
|
175
|
+
* active locale automatically and prevents repeated constructor overhead in
|
|
176
|
+
* reactive render paths.
|
|
177
|
+
*/
|
|
178
|
+
declare function useLocale(source?: MaybeRefOrGetter<Intl.Locale | string | null | undefined>, options?: UseLocaleOptions): LocaleControls;
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/media-query.d.ts
|
|
181
|
+
/** Capability required to evaluate media queries. */
|
|
182
|
+
interface MediaQueryHost {
|
|
183
|
+
/** Create an observable result for a media query. */
|
|
184
|
+
readonly matchMedia: (query: string) => MediaQueryList;
|
|
185
|
+
}
|
|
186
|
+
/** Options for {@link useMediaQuery}. */
|
|
187
|
+
interface UseMediaQueryOptions {
|
|
188
|
+
/**
|
|
189
|
+
* Value exposed when no media-query capability is available.
|
|
190
|
+
*
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
readonly ssrValue?: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Reactive media-query capability for alternate runtimes and tests.
|
|
196
|
+
*
|
|
197
|
+
* @default globalThis.window when available
|
|
198
|
+
*/
|
|
199
|
+
readonly host?: MaybeRefOrGetter<MediaQueryHost | null | undefined>;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Evaluate a reactive media query without requiring browser globals.
|
|
203
|
+
*
|
|
204
|
+
* @param query Reactive media-query source.
|
|
205
|
+
* @param options Runtime capability and server-rendered fallback.
|
|
206
|
+
* @default options {}
|
|
207
|
+
*/
|
|
208
|
+
declare function useMediaQuery(query: MaybeRefOrGetter<string>, options?: UseMediaQueryOptions): Readonly<Ref<boolean>>;
|
|
209
|
+
/** User motion preference exposed by {@link useReducedMotion}. */
|
|
210
|
+
type MotionPreference = "reduce" | "no-preference";
|
|
211
|
+
/**
|
|
212
|
+
* Return the reactive user motion preference.
|
|
213
|
+
*
|
|
214
|
+
* @param options Runtime capability and server-rendered fallback.
|
|
215
|
+
* @default options {}
|
|
216
|
+
*/
|
|
217
|
+
declare function useReducedMotion(options?: UseMediaQueryOptions): ComputedRef<MotionPreference>;
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/scope.d.ts
|
|
220
|
+
/**
|
|
221
|
+
* Register cleanup in the active reactive scope when one exists.
|
|
222
|
+
*
|
|
223
|
+
* @param cleanup Cleanup invoked exactly once when the scope is disposed.
|
|
224
|
+
* @returns Whether the cleanup was registered.
|
|
225
|
+
*/
|
|
226
|
+
declare function tryOnScopeDispose(cleanup: () => void): boolean;
|
|
227
|
+
//#endregion
|
|
228
|
+
export { AsyncResource, AsyncResourceContext, AsyncResourceExecution, AsyncResourceStatus, EventListenerControls, LocaleControls, MediaQueryHost, MotionPreference, TextDirection, UseAsyncResourceOptions, UseEventListenerOptions, UseLocaleOptions, UseMediaQueryOptions, tryOnScopeDispose, useAsyncResource, useEventListener, useLocale, useMediaQuery, useReducedMotion };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { computed, getCurrentScope, onScopeDispose, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
|
|
2
|
+
//#region src/scope.ts
|
|
3
|
+
/**
|
|
4
|
+
* Register cleanup in the active reactive scope when one exists.
|
|
5
|
+
*
|
|
6
|
+
* @param cleanup Cleanup invoked exactly once when the scope is disposed.
|
|
7
|
+
* @returns Whether the cleanup was registered.
|
|
8
|
+
*/
|
|
9
|
+
function tryOnScopeDispose(cleanup) {
|
|
10
|
+
if (!getCurrentScope()) return false;
|
|
11
|
+
onScopeDispose(cleanup);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/async-resource.ts
|
|
16
|
+
/**
|
|
17
|
+
* Create a scoped, abortable asynchronous resource with latest-result-wins
|
|
18
|
+
* state. Every execution returns a discriminated result, so cancellation,
|
|
19
|
+
* supersession, loader failure, and successful `undefined` data stay distinct.
|
|
20
|
+
*/
|
|
21
|
+
function useAsyncResource(loader, options = {}) {
|
|
22
|
+
const data = shallowRef(options.initialData);
|
|
23
|
+
const error = shallowRef(void 0);
|
|
24
|
+
const status = shallowRef("idle");
|
|
25
|
+
const pending = computed(() => status.value === "pending");
|
|
26
|
+
let generation = 0;
|
|
27
|
+
let active;
|
|
28
|
+
const cancel = (reason = createAbortReason("The execution was cancelled.")) => {
|
|
29
|
+
if (active === void 0) return false;
|
|
30
|
+
generation += 1;
|
|
31
|
+
active.controller.abort(reason);
|
|
32
|
+
active = void 0;
|
|
33
|
+
status.value = "cancelled";
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
const execute = async (...arguments_) => {
|
|
37
|
+
if ((options.cancelPrevious ?? true) && active !== void 0) {
|
|
38
|
+
active.superseded = true;
|
|
39
|
+
active.controller.abort(createAbortReason("A newer execution started."));
|
|
40
|
+
}
|
|
41
|
+
const record = {
|
|
42
|
+
generation: ++generation,
|
|
43
|
+
controller: new AbortController(),
|
|
44
|
+
superseded: false
|
|
45
|
+
};
|
|
46
|
+
active = record;
|
|
47
|
+
error.value = void 0;
|
|
48
|
+
status.value = "pending";
|
|
49
|
+
if (!(options.keepData ?? true)) data.value = void 0;
|
|
50
|
+
try {
|
|
51
|
+
const result = await loader({ signal: record.controller.signal }, ...arguments_);
|
|
52
|
+
if (record.generation !== generation) return executionAfterInvalidation(record);
|
|
53
|
+
data.value = result;
|
|
54
|
+
status.value = "success";
|
|
55
|
+
return {
|
|
56
|
+
status: "success",
|
|
57
|
+
data: result
|
|
58
|
+
};
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
if (record.generation !== generation || record.controller.signal.aborted) return executionAfterInvalidation(record);
|
|
61
|
+
error.value = cause;
|
|
62
|
+
status.value = "error";
|
|
63
|
+
return {
|
|
64
|
+
status: "error",
|
|
65
|
+
error: cause
|
|
66
|
+
};
|
|
67
|
+
} finally {
|
|
68
|
+
if (active === record) active = void 0;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const reset = () => {
|
|
72
|
+
cancel(createAbortReason("The resource was reset."));
|
|
73
|
+
data.value = options.initialData;
|
|
74
|
+
error.value = void 0;
|
|
75
|
+
status.value = "idle";
|
|
76
|
+
};
|
|
77
|
+
if (options.scope ?? true) tryOnScopeDispose(() => cancel(createAbortReason("The reactive scope was disposed.")));
|
|
78
|
+
return {
|
|
79
|
+
data,
|
|
80
|
+
error,
|
|
81
|
+
status,
|
|
82
|
+
pending,
|
|
83
|
+
execute,
|
|
84
|
+
cancel,
|
|
85
|
+
reset
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function executionAfterInvalidation(execution) {
|
|
89
|
+
if (execution.superseded || !execution.controller.signal.aborted) return { status: "superseded" };
|
|
90
|
+
return {
|
|
91
|
+
status: "cancelled",
|
|
92
|
+
reason: execution.controller.signal.reason
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createAbortReason(message) {
|
|
96
|
+
return new DOMException(message, "AbortError");
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/event-listener.ts
|
|
100
|
+
function useEventListener(target, event, listener, options = {}) {
|
|
101
|
+
const { capture = false, once = false, passive = false, signal, immediate = true, flush = "pre" } = options;
|
|
102
|
+
const isListening = ref(false);
|
|
103
|
+
const eventOptions = {
|
|
104
|
+
capture,
|
|
105
|
+
passive,
|
|
106
|
+
...signal ? { signal } : {}
|
|
107
|
+
};
|
|
108
|
+
let stopWatch;
|
|
109
|
+
const stop = () => {
|
|
110
|
+
stopWatch?.stop();
|
|
111
|
+
stopWatch = void 0;
|
|
112
|
+
isListening.value = false;
|
|
113
|
+
};
|
|
114
|
+
const start = () => {
|
|
115
|
+
if (stopWatch || signal?.aborted) return false;
|
|
116
|
+
stopWatch = watch(() => toValue(target), (next, _previous, onCleanup) => {
|
|
117
|
+
isListening.value = false;
|
|
118
|
+
if (!next || signal?.aborted) return;
|
|
119
|
+
const invoke = (nativeEvent) => {
|
|
120
|
+
if (once) stop();
|
|
121
|
+
listener(nativeEvent);
|
|
122
|
+
};
|
|
123
|
+
const onAbort = () => stop();
|
|
124
|
+
next.addEventListener(event, invoke, eventOptions);
|
|
125
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
126
|
+
isListening.value = true;
|
|
127
|
+
onCleanup(() => {
|
|
128
|
+
next.removeEventListener(event, invoke, capture);
|
|
129
|
+
signal?.removeEventListener("abort", onAbort);
|
|
130
|
+
isListening.value = false;
|
|
131
|
+
});
|
|
132
|
+
}, {
|
|
133
|
+
flush,
|
|
134
|
+
immediate: true
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
tryOnScopeDispose(stop);
|
|
139
|
+
if (immediate) start();
|
|
140
|
+
return {
|
|
141
|
+
isListening: readonly(isListening),
|
|
142
|
+
start,
|
|
143
|
+
stop
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/locale.ts
|
|
148
|
+
const FORMATTER_CACHE_LIMIT = 32;
|
|
149
|
+
/**
|
|
150
|
+
* Create reactive locale metadata and platform-native formatter factories.
|
|
151
|
+
*
|
|
152
|
+
* Equivalent formatter options reuse instances. The bounded cache follows the
|
|
153
|
+
* active locale automatically and prevents repeated constructor overhead in
|
|
154
|
+
* reactive render paths.
|
|
155
|
+
*/
|
|
156
|
+
function useLocale(source, options = {}) {
|
|
157
|
+
const locale = computed(() => {
|
|
158
|
+
const candidate = (source === void 0 ? void 0 : toValue(source)) ?? (options.detect ?? detectBrowserLocale)() ?? options.fallback ?? "en";
|
|
159
|
+
return candidate instanceof Intl.Locale ? candidate.toString() : new Intl.Locale(candidate).toString();
|
|
160
|
+
});
|
|
161
|
+
const details = computed(() => new Intl.Locale(locale.value));
|
|
162
|
+
const direction = computed(() => details.value.getTextInfo().direction);
|
|
163
|
+
const number = createFormatterCache((activeLocale, formatOptions) => new Intl.NumberFormat(activeLocale, formatOptions));
|
|
164
|
+
const dateTime = createFormatterCache((activeLocale, formatOptions) => new Intl.DateTimeFormat(activeLocale, formatOptions));
|
|
165
|
+
const list = createFormatterCache((activeLocale, formatOptions) => new Intl.ListFormat(activeLocale, formatOptions));
|
|
166
|
+
const relativeTime = createFormatterCache((activeLocale, formatOptions) => new Intl.RelativeTimeFormat(activeLocale, formatOptions));
|
|
167
|
+
return {
|
|
168
|
+
locale,
|
|
169
|
+
details,
|
|
170
|
+
direction,
|
|
171
|
+
number: (formatOptions) => number(locale.value, formatOptions),
|
|
172
|
+
dateTime: (formatOptions) => dateTime(locale.value, formatOptions),
|
|
173
|
+
list: (formatOptions) => list(locale.value, formatOptions),
|
|
174
|
+
relativeTime: (formatOptions) => relativeTime(locale.value, formatOptions)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function detectBrowserLocale() {
|
|
178
|
+
return typeof navigator === "undefined" ? void 0 : navigator.language;
|
|
179
|
+
}
|
|
180
|
+
function createFormatterCache(create) {
|
|
181
|
+
const cache = /* @__PURE__ */ new Map();
|
|
182
|
+
return (locale, options) => {
|
|
183
|
+
const key = `${locale}\u0000${serializeOptions(options)}`;
|
|
184
|
+
const cached = cache.get(key);
|
|
185
|
+
if (cached !== void 0) {
|
|
186
|
+
cache.delete(key);
|
|
187
|
+
cache.set(key, cached);
|
|
188
|
+
return cached;
|
|
189
|
+
}
|
|
190
|
+
const formatter = create(locale, options);
|
|
191
|
+
if (cache.size >= FORMATTER_CACHE_LIMIT) {
|
|
192
|
+
const oldest = cache.keys().next().value;
|
|
193
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
194
|
+
}
|
|
195
|
+
cache.set(key, formatter);
|
|
196
|
+
return formatter;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function serializeOptions(options) {
|
|
200
|
+
if (options === void 0) return "";
|
|
201
|
+
return JSON.stringify(Object.entries(options).sort(([left], [right]) => left.localeCompare(right)));
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/media-query.ts
|
|
205
|
+
/**
|
|
206
|
+
* Evaluate a reactive media query without requiring browser globals.
|
|
207
|
+
*
|
|
208
|
+
* @param query Reactive media-query source.
|
|
209
|
+
* @param options Runtime capability and server-rendered fallback.
|
|
210
|
+
* @default options {}
|
|
211
|
+
*/
|
|
212
|
+
function useMediaQuery(query, options = {}) {
|
|
213
|
+
const matches = ref(options.ssrValue ?? false);
|
|
214
|
+
watchEffect((onCleanup) => {
|
|
215
|
+
const host = options.host === void 0 ? browserMediaQueryHost() : toValue(options.host);
|
|
216
|
+
if (!host) {
|
|
217
|
+
matches.value = options.ssrValue ?? false;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const media = host.matchMedia(toValue(query));
|
|
221
|
+
const update = () => {
|
|
222
|
+
matches.value = media.matches;
|
|
223
|
+
};
|
|
224
|
+
update();
|
|
225
|
+
media.addEventListener("change", update);
|
|
226
|
+
onCleanup(() => media.removeEventListener("change", update));
|
|
227
|
+
});
|
|
228
|
+
return readonly(matches);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Return the reactive user motion preference.
|
|
232
|
+
*
|
|
233
|
+
* @param options Runtime capability and server-rendered fallback.
|
|
234
|
+
* @default options {}
|
|
235
|
+
*/
|
|
236
|
+
function useReducedMotion(options = {}) {
|
|
237
|
+
const reduced = useMediaQuery("(prefers-reduced-motion: reduce)", options);
|
|
238
|
+
return computed(() => reduced.value ? "reduce" : "no-preference");
|
|
239
|
+
}
|
|
240
|
+
function browserMediaQueryHost() {
|
|
241
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window : void 0;
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { tryOnScopeDispose, useAsyncResource, useEventListener, useLocale, useMediaQuery, useReducedMotion };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ComputedRef, MaybeRefOrGetter, Ref } from "vue";
|
|
2
|
+
import { Intl as TemporalIntl, Temporal } from "temporal-polyfill-lite";
|
|
3
|
+
|
|
4
|
+
//#region src/temporal.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Timer host used by {@link useTemporalNow}.
|
|
7
|
+
*
|
|
8
|
+
* Implement this interface to integrate a deterministic clock, a native
|
|
9
|
+
* runtime timer, or an application-owned scheduler.
|
|
10
|
+
*/
|
|
11
|
+
interface TemporalScheduler {
|
|
12
|
+
/** Starts a repeating callback and returns its opaque cancellation handle. */
|
|
13
|
+
readonly setInterval: (callback: () => void, intervalMs: number) => unknown;
|
|
14
|
+
/** Cancels a handle previously returned by {@link TemporalScheduler.setInterval}. */
|
|
15
|
+
readonly clearInterval: (handle: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
/** Options for {@link useTemporalNow}. */
|
|
18
|
+
interface UseTemporalNowOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Clock update interval in milliseconds.
|
|
21
|
+
*
|
|
22
|
+
* Reactive changes replace the active timer. Values must be finite and
|
|
23
|
+
* greater than zero.
|
|
24
|
+
*
|
|
25
|
+
* @default 1000
|
|
26
|
+
*/
|
|
27
|
+
readonly intervalMs?: MaybeRefOrGetter<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Pauses periodic updates while preserving the current instant.
|
|
30
|
+
* Manual calls to {@link TemporalClock.refresh} continue to work.
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
readonly paused?: MaybeRefOrGetter<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Starts the timer when no browser `window` is available.
|
|
37
|
+
*
|
|
38
|
+
* Keep this disabled during server rendering. Enable it for native,
|
|
39
|
+
* desktop, worker, and test runtimes whose scheduler is lifecycle-bound.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
readonly runOnServer?: MaybeRefOrGetter<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Produces the current instant.
|
|
46
|
+
*
|
|
47
|
+
* @default Temporal.Now.instant
|
|
48
|
+
*/
|
|
49
|
+
readonly now?: () => Temporal.Instant;
|
|
50
|
+
/**
|
|
51
|
+
* Owns the repeating timer.
|
|
52
|
+
*
|
|
53
|
+
* @default globalThis timer functions
|
|
54
|
+
*/
|
|
55
|
+
readonly scheduler?: TemporalScheduler;
|
|
56
|
+
}
|
|
57
|
+
/** Reactive controls returned by {@link useTemporalNow}. */
|
|
58
|
+
interface TemporalClock {
|
|
59
|
+
/** The latest instant. This ref is readonly to consumers. */
|
|
60
|
+
readonly instant: Readonly<Ref<Temporal.Instant>>;
|
|
61
|
+
/** Reads the clock source immediately, stores the value, and returns it. */
|
|
62
|
+
readonly refresh: () => Temporal.Instant;
|
|
63
|
+
}
|
|
64
|
+
/** Options for {@link useTemporalZonedDateTime}. */
|
|
65
|
+
interface UseTemporalZonedDateTimeOptions extends UseTemporalNowOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Time-zone identifier or zoned date-time accepted by Temporal.
|
|
68
|
+
*
|
|
69
|
+
* @default Temporal.Now.timeZoneId()
|
|
70
|
+
*/
|
|
71
|
+
readonly timeZone?: MaybeRefOrGetter<Temporal.TimeZoneLike>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Creates a pauseable Temporal clock whose timer follows the current Vue
|
|
75
|
+
* effect scope.
|
|
76
|
+
*
|
|
77
|
+
* The timer is replaced when reactive options change and is always cancelled
|
|
78
|
+
* when the owning scope stops. During server rendering, no timer starts unless
|
|
79
|
+
* {@link UseTemporalNowOptions.runOnServer} is explicitly enabled.
|
|
80
|
+
*/
|
|
81
|
+
declare function useTemporalNow(options?: UseTemporalNowOptions): TemporalClock;
|
|
82
|
+
/**
|
|
83
|
+
* Creates a reactive zoned date-time derived from a scoped Temporal clock.
|
|
84
|
+
*
|
|
85
|
+
* Changes to {@link UseTemporalZonedDateTimeOptions.timeZone} are reflected
|
|
86
|
+
* without replacing the underlying timer.
|
|
87
|
+
*/
|
|
88
|
+
declare function useTemporalZonedDateTime(options?: UseTemporalZonedDateTimeOptions): ComputedRef<Temporal.ZonedDateTime>;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { Temporal, TemporalClock, TemporalIntl, TemporalScheduler, UseTemporalNowOptions, UseTemporalZonedDateTimeOptions, useTemporalNow, useTemporalZonedDateTime };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { computed, readonly, shallowRef, toValue, watchEffect } from "vue";
|
|
2
|
+
import { Intl as TemporalIntl, Temporal } from "temporal-polyfill-lite";
|
|
3
|
+
//#region src/temporal.ts
|
|
4
|
+
const defaultScheduler = {
|
|
5
|
+
setInterval: (callback, intervalMs) => globalThis.setInterval(callback, intervalMs),
|
|
6
|
+
clearInterval: (handle) => {
|
|
7
|
+
globalThis.clearInterval(handle);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function resolveIntervalMs(value) {
|
|
11
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`[VIZE_COMPOSE_TEMPORAL_INVALID_INTERVAL] intervalMs must be finite and greater than zero; received ${String(value)}`);
|
|
12
|
+
return Math.max(1, Math.trunc(value));
|
|
13
|
+
}
|
|
14
|
+
function shouldSchedule(options) {
|
|
15
|
+
if (toValue(options.paused ?? false)) return false;
|
|
16
|
+
return typeof window !== "undefined" || toValue(options.runOnServer ?? false);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a pauseable Temporal clock whose timer follows the current Vue
|
|
20
|
+
* effect scope.
|
|
21
|
+
*
|
|
22
|
+
* The timer is replaced when reactive options change and is always cancelled
|
|
23
|
+
* when the owning scope stops. During server rendering, no timer starts unless
|
|
24
|
+
* {@link UseTemporalNowOptions.runOnServer} is explicitly enabled.
|
|
25
|
+
*/
|
|
26
|
+
function useTemporalNow(options = {}) {
|
|
27
|
+
const readNow = options.now ?? Temporal.Now.instant;
|
|
28
|
+
const instant = shallowRef(readNow());
|
|
29
|
+
if (shouldSchedule(options)) resolveIntervalMs(toValue(options.intervalMs ?? 1e3));
|
|
30
|
+
const refresh = () => {
|
|
31
|
+
const nextInstant = readNow();
|
|
32
|
+
instant.value = nextInstant;
|
|
33
|
+
return nextInstant;
|
|
34
|
+
};
|
|
35
|
+
watchEffect((onCleanup) => {
|
|
36
|
+
if (!shouldSchedule(options)) return;
|
|
37
|
+
const intervalMs = resolveIntervalMs(toValue(options.intervalMs ?? 1e3));
|
|
38
|
+
const scheduler = options.scheduler ?? defaultScheduler;
|
|
39
|
+
const handle = scheduler.setInterval(refresh, intervalMs);
|
|
40
|
+
onCleanup(() => scheduler.clearInterval(handle));
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
instant: readonly(instant),
|
|
44
|
+
refresh
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a reactive zoned date-time derived from a scoped Temporal clock.
|
|
49
|
+
*
|
|
50
|
+
* Changes to {@link UseTemporalZonedDateTimeOptions.timeZone} are reflected
|
|
51
|
+
* without replacing the underlying timer.
|
|
52
|
+
*/
|
|
53
|
+
function useTemporalZonedDateTime(options = {}) {
|
|
54
|
+
const clock = useTemporalNow(options);
|
|
55
|
+
return computed(() => {
|
|
56
|
+
const timeZone = options.timeZone === void 0 ? Temporal.Now.timeZoneId() : toValue(options.timeZone);
|
|
57
|
+
return clock.instant.value.toZonedDateTimeISO(timeZone);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { Temporal, TemporalIntl, useTemporalNow, useTemporalZonedDateTime };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vizejs/composable",
|
|
3
|
+
"version": "0.302.0",
|
|
4
|
+
"description": "Lifecycle-safe composable foundations for Vize applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"composables",
|
|
7
|
+
"lifecycle",
|
|
8
|
+
"type-safe",
|
|
9
|
+
"vize",
|
|
10
|
+
"vue"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/ubugeeei-prod/vize",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ubugeeei-prod/vize/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/ubugeeei-prod/vize.git",
|
|
20
|
+
"directory": "npm/compose/core"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.mjs",
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"import": "./dist/index.mjs",
|
|
33
|
+
"default": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./temporal": {
|
|
36
|
+
"types": "./dist/temporal.d.mts",
|
|
37
|
+
"import": "./dist/temporal.mjs",
|
|
38
|
+
"default": "./dist/temporal.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "vp pack",
|
|
46
|
+
"dev": "vp pack --watch",
|
|
47
|
+
"pretest": "vp pack && pnpm check:size",
|
|
48
|
+
"test": "vp exec node --test 'src/**/*.test.ts'",
|
|
49
|
+
"check": "vp check src scripts vite.config.ts",
|
|
50
|
+
"check:fix": "vp check --fix src scripts vite.config.ts",
|
|
51
|
+
"check:size": "node scripts/check-size.mjs",
|
|
52
|
+
"fmt": "vp fmt --write src scripts vite.config.ts"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"temporal-polyfill-lite": "0.4.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "catalog:typescript",
|
|
59
|
+
"typescript": "catalog:typescript",
|
|
60
|
+
"vite-plus": "catalog:vite-stack",
|
|
61
|
+
"vue": "catalog:vue-stable"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"vue": "^3.5.0"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=24"
|
|
68
|
+
}
|
|
69
|
+
}
|