@bquery/bquery 1.4.0 → 1.6.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/README.md +586 -527
- package/dist/component/component.d.ts +13 -5
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/html.d.ts +40 -3
- package/dist/component/html.d.ts.map +1 -1
- package/dist/component/index.d.ts +4 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts +34 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/types.d.ts +132 -13
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-BEQgt5hl.js +600 -0
- package/dist/component-BEQgt5hl.js.map +1 -0
- package/dist/component.es.mjs +7 -184
- package/dist/config-DRmZZno3.js +40 -0
- package/dist/config-DRmZZno3.js.map +1 -0
- package/dist/core-BGQJVw0-.js +35 -0
- package/dist/core-BGQJVw0-.js.map +1 -0
- package/dist/core-CCEabVHl.js +648 -0
- package/dist/core-CCEabVHl.js.map +1 -0
- package/dist/core.es.mjs +45 -1261
- package/dist/effect-AFRW_Plg.js +84 -0
- package/dist/effect-AFRW_Plg.js.map +1 -0
- package/dist/full.d.ts +8 -8
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +101 -91
- package/dist/full.iife.js +173 -3
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +173 -3
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +147 -139
- package/dist/motion/transition.d.ts +1 -1
- package/dist/motion/transition.d.ts.map +1 -1
- package/dist/motion/types.d.ts +11 -1
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion-D9TcHxOF.js +415 -0
- package/dist/motion-D9TcHxOF.js.map +1 -0
- package/dist/motion.es.mjs +25 -361
- package/dist/object-qGpWr6-J.js +38 -0
- package/dist/object-qGpWr6-J.js.map +1 -0
- package/dist/platform/announcer.d.ts +59 -0
- package/dist/platform/announcer.d.ts.map +1 -0
- package/dist/platform/config.d.ts +92 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/cookies.d.ts +45 -0
- package/dist/platform/cookies.d.ts.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/meta.d.ts +62 -0
- package/dist/platform/meta.d.ts.map +1 -0
- package/dist/platform-Dr9b6fsq.js +362 -0
- package/dist/platform-Dr9b6fsq.js.map +1 -0
- package/dist/platform.es.mjs +11 -248
- package/dist/reactive/async-data.d.ts +114 -0
- package/dist/reactive/async-data.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +2 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive-DSkct0dO.js +254 -0
- package/dist/reactive-DSkct0dO.js.map +1 -0
- package/dist/reactive.es.mjs +18 -32
- package/dist/router-CbDhl8rS.js +188 -0
- package/dist/router-CbDhl8rS.js.map +1 -0
- package/dist/router.es.mjs +11 -200
- package/dist/sanitize-Bs2dkMby.js +313 -0
- package/dist/sanitize-Bs2dkMby.js.map +1 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/index.d.ts +4 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize.d.ts +4 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-html.d.ts +53 -0
- package/dist/security/trusted-html.d.ts.map +1 -0
- package/dist/security.es.mjs +11 -56
- package/dist/store/define-store.d.ts +1 -1
- package/dist/store/define-store.d.ts.map +1 -1
- package/dist/store/mapping.d.ts +1 -1
- package/dist/store/mapping.d.ts.map +1 -1
- package/dist/store/persisted.d.ts +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/store/types.d.ts +2 -2
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/watch.d.ts +1 -1
- package/dist/store/watch.d.ts.map +1 -1
- package/dist/store-BwDvI45q.js +263 -0
- package/dist/store-BwDvI45q.js.map +1 -0
- package/dist/store.es.mjs +12 -25
- package/dist/storybook/index.d.ts +37 -0
- package/dist/storybook/index.d.ts.map +1 -0
- package/dist/storybook.es.mjs +151 -0
- package/dist/storybook.es.mjs.map +1 -0
- package/dist/untrack-B0rVscTc.js +7 -0
- package/dist/untrack-B0rVscTc.js.map +1 -0
- package/dist/view-C70lA3vf.js +397 -0
- package/dist/view-C70lA3vf.js.map +1 -0
- package/dist/view.es.mjs +11 -430
- package/package.json +141 -132
- package/src/component/component.ts +524 -289
- package/src/component/html.ts +153 -53
- package/src/component/index.ts +50 -40
- package/src/component/library.ts +518 -0
- package/src/component/types.ts +256 -85
- package/src/core/collection.ts +628 -628
- package/src/core/element.ts +774 -774
- package/src/core/index.ts +48 -48
- package/src/core/utils/function.ts +151 -151
- package/src/full.ts +229 -187
- package/src/motion/animate.ts +113 -113
- package/src/motion/flip.ts +176 -176
- package/src/motion/scroll.ts +57 -57
- package/src/motion/spring.ts +150 -150
- package/src/motion/timeline.ts +246 -246
- package/src/motion/transition.ts +97 -51
- package/src/motion/types.ts +11 -1
- package/src/platform/announcer.ts +208 -0
- package/src/platform/config.ts +163 -0
- package/src/platform/cookies.ts +165 -0
- package/src/platform/index.ts +21 -0
- package/src/platform/meta.ts +168 -0
- package/src/platform/storage.ts +215 -215
- package/src/reactive/async-data.ts +486 -0
- package/src/reactive/core.ts +114 -114
- package/src/reactive/effect.ts +54 -54
- package/src/reactive/index.ts +15 -1
- package/src/reactive/internals.ts +122 -122
- package/src/reactive/signal.ts +9 -0
- package/src/security/constants.ts +3 -1
- package/src/security/index.ts +17 -10
- package/src/security/sanitize-core.ts +364 -364
- package/src/security/sanitize.ts +70 -66
- package/src/security/trusted-html.ts +71 -0
- package/src/store/define-store.ts +49 -48
- package/src/store/mapping.ts +74 -73
- package/src/store/persisted.ts +62 -61
- package/src/store/types.ts +92 -94
- package/src/store/watch.ts +53 -52
- package/src/storybook/index.ts +479 -0
- package/src/view/evaluate.ts +290 -290
- package/dist/batch-x7b2eZST.js +0 -13
- package/dist/batch-x7b2eZST.js.map +0 -1
- package/dist/component.es.mjs.map +0 -1
- package/dist/core-BhpuvPhy.js +0 -170
- package/dist/core-BhpuvPhy.js.map +0 -1
- package/dist/core.es.mjs.map +0 -1
- package/dist/full.es.mjs.map +0 -1
- package/dist/index.es.mjs.map +0 -1
- package/dist/motion.es.mjs.map +0 -1
- package/dist/persisted-DHoi3uEs.js +0 -278
- package/dist/persisted-DHoi3uEs.js.map +0 -1
- package/dist/platform.es.mjs.map +0 -1
- package/dist/reactive.es.mjs.map +0 -1
- package/dist/router.es.mjs.map +0 -1
- package/dist/sanitize-Cxvxa-DX.js +0 -283
- package/dist/sanitize-Cxvxa-DX.js.map +0 -1
- package/dist/security.es.mjs.map +0 -1
- package/dist/store.es.mjs.map +0 -1
- package/dist/type-guards-BdKlYYlS.js +0 -32
- package/dist/type-guards-BdKlYYlS.js.map +0 -1
- package/dist/untrack-DNnnqdlR.js +0 -6
- package/dist/untrack-DNnnqdlR.js.map +0 -1
- package/dist/view.es.mjs.map +0 -1
- package/dist/watch-DXXv3iAI.js +0 -58
- package/dist/watch-DXXv3iAI.js.map +0 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility live-region announcer helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/platform
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { effect, signal, type Signal } from '../reactive/signal';
|
|
8
|
+
import { getBqueryConfig } from './config';
|
|
9
|
+
|
|
10
|
+
/** Options for creating an announcer. */
|
|
11
|
+
export interface UseAnnouncerOptions {
|
|
12
|
+
/** Live region politeness. */
|
|
13
|
+
politeness?: 'polite' | 'assertive';
|
|
14
|
+
/** Whether the live region should be atomic. */
|
|
15
|
+
atomic?: boolean;
|
|
16
|
+
/** Delay before applying the message. */
|
|
17
|
+
delay?: number;
|
|
18
|
+
/** Delay after which the message is cleared automatically. */
|
|
19
|
+
clearDelay?: number;
|
|
20
|
+
/** Optional element id for the live region. */
|
|
21
|
+
id?: string;
|
|
22
|
+
/** Optional CSS class name. */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Optional container used to append the live region. */
|
|
25
|
+
container?: HTMLElement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runtime options for a single announcement. */
|
|
29
|
+
export interface AnnounceOptions {
|
|
30
|
+
/** Override politeness for this specific announcement. */
|
|
31
|
+
politeness?: 'polite' | 'assertive';
|
|
32
|
+
/** Override the message delay for this specific announcement. */
|
|
33
|
+
delay?: number;
|
|
34
|
+
/** Override the auto-clear delay for this specific announcement. */
|
|
35
|
+
clearDelay?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returned announcer API. */
|
|
39
|
+
export interface AnnouncerHandle {
|
|
40
|
+
/** The live region element or null outside the DOM. */
|
|
41
|
+
element: HTMLElement | null;
|
|
42
|
+
/** Reactive message signal. */
|
|
43
|
+
message: Signal<string>;
|
|
44
|
+
/** Announce a message to assistive technologies. */
|
|
45
|
+
announce: (value: string, options?: AnnounceOptions) => void;
|
|
46
|
+
/** Clear the current announcement. */
|
|
47
|
+
clear: () => void;
|
|
48
|
+
/** Remove the live region if it was created by this announcer. */
|
|
49
|
+
destroy: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const visuallyHiddenStyle = [
|
|
53
|
+
'position:absolute',
|
|
54
|
+
'width:1px',
|
|
55
|
+
'height:1px',
|
|
56
|
+
'padding:0',
|
|
57
|
+
'margin:-1px',
|
|
58
|
+
'overflow:hidden',
|
|
59
|
+
'clip:rect(0, 0, 0, 0)',
|
|
60
|
+
'white-space:nowrap',
|
|
61
|
+
'border:0',
|
|
62
|
+
].join(';');
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create or reuse an accessible live region.
|
|
66
|
+
*
|
|
67
|
+
* @param options - Live region configuration
|
|
68
|
+
* @returns An announcer handle with announce(), clear(), and destroy()
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const announcer = useAnnouncer();
|
|
73
|
+
* announcer.announce('Saved successfully');
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const useAnnouncer = (options: UseAnnouncerOptions = {}): AnnouncerHandle => {
|
|
77
|
+
const defaults = getBqueryConfig().announcer;
|
|
78
|
+
const resolvedOptions: Required<
|
|
79
|
+
Pick<UseAnnouncerOptions, 'politeness' | 'atomic' | 'delay' | 'clearDelay'>
|
|
80
|
+
> &
|
|
81
|
+
UseAnnouncerOptions = {
|
|
82
|
+
politeness: defaults?.politeness ?? 'polite',
|
|
83
|
+
atomic: defaults?.atomic ?? true,
|
|
84
|
+
delay: defaults?.delay ?? 16,
|
|
85
|
+
clearDelay: defaults?.clearDelay ?? 1000,
|
|
86
|
+
...options,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const message = signal('');
|
|
90
|
+
|
|
91
|
+
if (typeof document === 'undefined') {
|
|
92
|
+
return {
|
|
93
|
+
element: null,
|
|
94
|
+
message,
|
|
95
|
+
announce(value: string) {
|
|
96
|
+
message.value = value;
|
|
97
|
+
},
|
|
98
|
+
clear() {
|
|
99
|
+
message.value = '';
|
|
100
|
+
},
|
|
101
|
+
destroy() {
|
|
102
|
+
message.value = '';
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const existing = resolvedOptions.id ? document.getElementById(resolvedOptions.id) : null;
|
|
108
|
+
const element = (existing ?? document.createElement('div')) as HTMLElement;
|
|
109
|
+
const created = !existing;
|
|
110
|
+
|
|
111
|
+
if (resolvedOptions.id) {
|
|
112
|
+
element.id = resolvedOptions.id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (resolvedOptions.className) {
|
|
116
|
+
element.className = resolvedOptions.className;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
element.setAttribute('aria-live', resolvedOptions.politeness);
|
|
120
|
+
element.setAttribute('aria-atomic', String(resolvedOptions.atomic));
|
|
121
|
+
element.setAttribute('role', resolvedOptions.politeness === 'assertive' ? 'alert' : 'status');
|
|
122
|
+
element.setAttribute('data-bquery-announcer', 'true');
|
|
123
|
+
if (!element.getAttribute('style')) {
|
|
124
|
+
element.setAttribute('style', visuallyHiddenStyle);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (created) {
|
|
128
|
+
const parent = resolvedOptions.container ?? document.body ?? document.documentElement;
|
|
129
|
+
if (!parent) {
|
|
130
|
+
return {
|
|
131
|
+
element: null,
|
|
132
|
+
message,
|
|
133
|
+
announce(value: string) {
|
|
134
|
+
message.value = value;
|
|
135
|
+
},
|
|
136
|
+
clear() {
|
|
137
|
+
message.value = '';
|
|
138
|
+
},
|
|
139
|
+
destroy() {
|
|
140
|
+
message.value = '';
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
parent.appendChild(element);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const disposeMessageEffect = effect(() => {
|
|
148
|
+
element.textContent = message.value;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let messageTimer: ReturnType<typeof setTimeout> | undefined;
|
|
152
|
+
let clearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
153
|
+
let destroyed = false;
|
|
154
|
+
|
|
155
|
+
const clearTimers = (): void => {
|
|
156
|
+
if (messageTimer) {
|
|
157
|
+
clearTimeout(messageTimer);
|
|
158
|
+
messageTimer = undefined;
|
|
159
|
+
}
|
|
160
|
+
if (clearTimer) {
|
|
161
|
+
clearTimeout(clearTimer);
|
|
162
|
+
clearTimer = undefined;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const clear = (): void => {
|
|
167
|
+
if (destroyed) return;
|
|
168
|
+
clearTimers();
|
|
169
|
+
message.value = '';
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const announce = (value: string, announceOptions: AnnounceOptions = {}): void => {
|
|
173
|
+
if (destroyed) return;
|
|
174
|
+
const politeness = announceOptions.politeness ?? resolvedOptions.politeness;
|
|
175
|
+
const delay = announceOptions.delay ?? resolvedOptions.delay;
|
|
176
|
+
const clearDelay = announceOptions.clearDelay ?? resolvedOptions.clearDelay;
|
|
177
|
+
|
|
178
|
+
clearTimers();
|
|
179
|
+
|
|
180
|
+
element.setAttribute('aria-live', politeness);
|
|
181
|
+
element.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');
|
|
182
|
+
message.value = '';
|
|
183
|
+
|
|
184
|
+
messageTimer = setTimeout(() => {
|
|
185
|
+
if (destroyed) return;
|
|
186
|
+
message.value = value;
|
|
187
|
+
if (clearDelay > 0) {
|
|
188
|
+
clearTimer = setTimeout(() => {
|
|
189
|
+
if (destroyed) return;
|
|
190
|
+
message.value = '';
|
|
191
|
+
}, clearDelay);
|
|
192
|
+
}
|
|
193
|
+
}, delay);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const destroy = (): void => {
|
|
197
|
+
if (destroyed) return;
|
|
198
|
+
destroyed = true;
|
|
199
|
+
clearTimers();
|
|
200
|
+
message.value = '';
|
|
201
|
+
disposeMessageEffect();
|
|
202
|
+
if (created) {
|
|
203
|
+
element.remove();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return { element, message, announce, clear, destroy };
|
|
208
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global bQuery configuration helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/platform
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isPlainObject, merge } from '../core/utils/object';
|
|
8
|
+
|
|
9
|
+
/** Supported response parsing strategies for fetch composables. */
|
|
10
|
+
export type BqueryFetchParseAs = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'response';
|
|
11
|
+
|
|
12
|
+
/** Global fetch defaults used by useFetch(). */
|
|
13
|
+
export interface BqueryFetchConfig {
|
|
14
|
+
/** Optional base URL prepended to relative request URLs. */
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/** Default request headers. */
|
|
17
|
+
headers?: HeadersInit;
|
|
18
|
+
/** Default response parser. */
|
|
19
|
+
parseAs?: BqueryFetchParseAs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Global cookie defaults used by useCookie(). */
|
|
23
|
+
export interface BqueryCookieConfig {
|
|
24
|
+
/** Default cookie path. */
|
|
25
|
+
path?: string;
|
|
26
|
+
/** Default SameSite mode. */
|
|
27
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
28
|
+
/** Whether cookies should be marked secure by default. */
|
|
29
|
+
secure?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Global announcer defaults used by useAnnouncer(). */
|
|
33
|
+
export interface BqueryAnnouncerConfig {
|
|
34
|
+
/** Default politeness level. */
|
|
35
|
+
politeness?: 'polite' | 'assertive';
|
|
36
|
+
/** Whether announcements should be treated atomically. */
|
|
37
|
+
atomic?: boolean;
|
|
38
|
+
/** Delay before writing the message into the live region. */
|
|
39
|
+
delay?: number;
|
|
40
|
+
/** Delay after which the live region is cleared automatically. */
|
|
41
|
+
clearDelay?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Global page meta defaults used by definePageMeta(). */
|
|
45
|
+
export interface BqueryPageMetaConfig {
|
|
46
|
+
/** Optional title template function. */
|
|
47
|
+
titleTemplate?: (title: string) => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Global motion defaults used by transition(). */
|
|
51
|
+
export interface BqueryTransitionConfig {
|
|
52
|
+
/** Skip transitions when reduced motion is preferred. */
|
|
53
|
+
skipOnReducedMotion?: boolean;
|
|
54
|
+
/** Classes applied to the root element during transitions. */
|
|
55
|
+
classes?: string[];
|
|
56
|
+
/** Transition type identifiers added when supported by the browser. */
|
|
57
|
+
types?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Global default component library configuration. */
|
|
61
|
+
export interface BqueryComponentLibraryConfig {
|
|
62
|
+
/** Prefix used by registerDefaultComponents(). */
|
|
63
|
+
prefix?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Complete global bQuery configuration object. */
|
|
67
|
+
export interface BqueryConfig {
|
|
68
|
+
/** Fetch composable defaults. */
|
|
69
|
+
fetch?: BqueryFetchConfig;
|
|
70
|
+
/** Cookie composable defaults. */
|
|
71
|
+
cookies?: BqueryCookieConfig;
|
|
72
|
+
/** Announcer composable defaults. */
|
|
73
|
+
announcer?: BqueryAnnouncerConfig;
|
|
74
|
+
/** Page metadata defaults. */
|
|
75
|
+
pageMeta?: BqueryPageMetaConfig;
|
|
76
|
+
/** View transition defaults. */
|
|
77
|
+
transitions?: BqueryTransitionConfig;
|
|
78
|
+
/** Default component library options. */
|
|
79
|
+
components?: BqueryComponentLibraryConfig;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const defaultConfig: BqueryConfig = {
|
|
83
|
+
fetch: {
|
|
84
|
+
headers: {},
|
|
85
|
+
parseAs: 'json',
|
|
86
|
+
},
|
|
87
|
+
cookies: {
|
|
88
|
+
path: '/',
|
|
89
|
+
sameSite: 'Lax',
|
|
90
|
+
secure: false,
|
|
91
|
+
},
|
|
92
|
+
announcer: {
|
|
93
|
+
politeness: 'polite',
|
|
94
|
+
atomic: true,
|
|
95
|
+
delay: 16,
|
|
96
|
+
clearDelay: 1000,
|
|
97
|
+
},
|
|
98
|
+
pageMeta: {},
|
|
99
|
+
transitions: {
|
|
100
|
+
skipOnReducedMotion: false,
|
|
101
|
+
classes: [],
|
|
102
|
+
types: [],
|
|
103
|
+
},
|
|
104
|
+
components: {
|
|
105
|
+
prefix: 'bq',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const cloneConfigValue = <T>(value: T): T => {
|
|
110
|
+
if (typeof Headers !== 'undefined' && value instanceof Headers) {
|
|
111
|
+
return new Headers(value) as T;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return value.map((entry) => cloneConfigValue(entry)) as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isPlainObject(value)) {
|
|
119
|
+
const result: Record<string, unknown> = {};
|
|
120
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
121
|
+
result[key] = cloneConfigValue(entry);
|
|
122
|
+
}
|
|
123
|
+
return result as T;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let currentConfig: BqueryConfig = cloneConfigValue(defaultConfig);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Define or extend the global bQuery configuration.
|
|
133
|
+
*
|
|
134
|
+
* @param config - Partial configuration values to merge into the current config
|
|
135
|
+
* @returns The resolved configuration after merging
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* defineBqueryConfig({
|
|
140
|
+
* fetch: { baseUrl: 'https://api.example.com' },
|
|
141
|
+
* components: { prefix: 'ui' },
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export const defineBqueryConfig = (config: BqueryConfig): BqueryConfig => {
|
|
146
|
+
currentConfig = cloneConfigValue(
|
|
147
|
+
merge(
|
|
148
|
+
defaultConfig as Record<string, unknown>,
|
|
149
|
+
currentConfig as Record<string, unknown>,
|
|
150
|
+
config as Record<string, unknown>
|
|
151
|
+
) as BqueryConfig
|
|
152
|
+
);
|
|
153
|
+
return getBqueryConfig();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the currently resolved bQuery configuration.
|
|
158
|
+
*
|
|
159
|
+
* @returns A cloned snapshot of the active configuration
|
|
160
|
+
*/
|
|
161
|
+
export const getBqueryConfig = (): BqueryConfig => {
|
|
162
|
+
return cloneConfigValue(currentConfig);
|
|
163
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive cookie helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/platform
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { effect, signal, type Signal } from '../reactive/signal';
|
|
8
|
+
import { getBqueryConfig } from './config';
|
|
9
|
+
|
|
10
|
+
/** Options for useCookie(). */
|
|
11
|
+
export interface UseCookieOptions<T> {
|
|
12
|
+
/** Default value when the cookie is not present. */
|
|
13
|
+
defaultValue?: T;
|
|
14
|
+
/** Cookie path. Defaults to the global config or `/`. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Optional cookie domain. */
|
|
17
|
+
domain?: string;
|
|
18
|
+
/** Cookie SameSite attribute. */
|
|
19
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
20
|
+
/** Whether the cookie should be marked secure. */
|
|
21
|
+
secure?: boolean;
|
|
22
|
+
/** Cookie expiry date. */
|
|
23
|
+
expires?: Date;
|
|
24
|
+
/** Cookie max-age in seconds. */
|
|
25
|
+
maxAge?: number;
|
|
26
|
+
/** Automatically persist signal updates back to document.cookie. */
|
|
27
|
+
watch?: boolean;
|
|
28
|
+
/** Serialize a value before writing it into the cookie. */
|
|
29
|
+
serialize?: (value: T) => string;
|
|
30
|
+
/** Deserialize a cookie string into a typed value. */
|
|
31
|
+
deserialize?: (value: string) => T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const readCookie = (name: string): string | null => {
|
|
35
|
+
if (typeof document === 'undefined') return null;
|
|
36
|
+
|
|
37
|
+
const prefix = `${encodeURIComponent(name)}=`;
|
|
38
|
+
const segments = document.cookie ? document.cookie.split(';') : [];
|
|
39
|
+
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
const normalizedSegment = segment.trim();
|
|
42
|
+
if (normalizedSegment.startsWith(prefix)) {
|
|
43
|
+
const rawValue = normalizedSegment.slice(prefix.length);
|
|
44
|
+
try {
|
|
45
|
+
return decodeURIComponent(rawValue);
|
|
46
|
+
} catch {
|
|
47
|
+
return rawValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const requiresJsonParsing = (value: string): boolean => {
|
|
56
|
+
const normalized = value.trim();
|
|
57
|
+
return normalized.startsWith('{') || normalized.startsWith('[') || normalized.startsWith('"');
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const removeCookie = (
|
|
61
|
+
name: string,
|
|
62
|
+
options: Pick<UseCookieOptions<unknown>, 'path' | 'domain' | 'sameSite' | 'secure'>
|
|
63
|
+
): void => {
|
|
64
|
+
if (typeof document === 'undefined') return;
|
|
65
|
+
|
|
66
|
+
const segments = [`${encodeURIComponent(name)}=`, 'Expires=Thu, 01 Jan 1970 00:00:00 GMT'];
|
|
67
|
+
|
|
68
|
+
if (options.path) segments.push(`Path=${options.path}`);
|
|
69
|
+
if (options.domain) segments.push(`Domain=${options.domain}`);
|
|
70
|
+
if (options.sameSite) segments.push(`SameSite=${options.sameSite}`);
|
|
71
|
+
if (options.secure) segments.push('Secure');
|
|
72
|
+
|
|
73
|
+
document.cookie = segments.join('; ');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const writeCookie = <T>(name: string, value: T, options: UseCookieOptions<T>): void => {
|
|
77
|
+
if (typeof document === 'undefined') return;
|
|
78
|
+
|
|
79
|
+
const serialized = options.serialize
|
|
80
|
+
? options.serialize(value)
|
|
81
|
+
: typeof value === 'string'
|
|
82
|
+
? value
|
|
83
|
+
: JSON.stringify(value);
|
|
84
|
+
|
|
85
|
+
const segments = [`${encodeURIComponent(name)}=${encodeURIComponent(serialized)}`];
|
|
86
|
+
|
|
87
|
+
if (options.path) segments.push(`Path=${options.path}`);
|
|
88
|
+
if (options.domain) segments.push(`Domain=${options.domain}`);
|
|
89
|
+
if (typeof options.maxAge === 'number') segments.push(`Max-Age=${options.maxAge}`);
|
|
90
|
+
if (options.expires) segments.push(`Expires=${options.expires.toUTCString()}`);
|
|
91
|
+
if (options.sameSite) segments.push(`SameSite=${options.sameSite}`);
|
|
92
|
+
if (options.secure) segments.push('Secure');
|
|
93
|
+
|
|
94
|
+
document.cookie = segments.join('; ');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a reactive cookie signal.
|
|
99
|
+
*
|
|
100
|
+
* @template T - Cookie value type
|
|
101
|
+
* @param name - Cookie name
|
|
102
|
+
* @param options - Read/write configuration for the cookie
|
|
103
|
+
* @returns Reactive signal representing the cookie value
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const theme = useCookie('theme', { defaultValue: 'light' });
|
|
108
|
+
* theme.value = 'dark';
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const useCookie = <T>(name: string, options: UseCookieOptions<T> = {}): Signal<T | null> => {
|
|
112
|
+
const cookieConfig = getBqueryConfig().cookies;
|
|
113
|
+
const resolvedOptions: UseCookieOptions<T> = {
|
|
114
|
+
path: cookieConfig?.path ?? '/',
|
|
115
|
+
sameSite: cookieConfig?.sameSite ?? 'Lax',
|
|
116
|
+
secure: cookieConfig?.secure ?? false,
|
|
117
|
+
watch: true,
|
|
118
|
+
...options,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (resolvedOptions.sameSite === 'None') {
|
|
122
|
+
resolvedOptions.secure = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const raw = readCookie(name);
|
|
126
|
+
let initialValue = (resolvedOptions.defaultValue ?? null) as T | null;
|
|
127
|
+
|
|
128
|
+
if (raw !== null) {
|
|
129
|
+
try {
|
|
130
|
+
initialValue = resolvedOptions.deserialize
|
|
131
|
+
? resolvedOptions.deserialize(raw)
|
|
132
|
+
: requiresJsonParsing(raw)
|
|
133
|
+
? (JSON.parse(raw) as T)
|
|
134
|
+
: ((raw as T) ?? initialValue);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(`bQuery: Failed to deserialize cookie "${name}", using raw string value`, error);
|
|
137
|
+
initialValue = (raw as T) ?? initialValue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cookie = signal<T | null>(initialValue);
|
|
142
|
+
|
|
143
|
+
if (typeof document === 'undefined' || resolvedOptions.watch === false) {
|
|
144
|
+
return cookie;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let initialized = false;
|
|
148
|
+
effect(() => {
|
|
149
|
+
const nextValue = cookie.value;
|
|
150
|
+
|
|
151
|
+
if (!initialized) {
|
|
152
|
+
initialized = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (nextValue == null) {
|
|
157
|
+
removeCookie(name, resolvedOptions);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
writeCookie(name, nextValue, resolvedOptions);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return cookie;
|
|
165
|
+
};
|
package/src/platform/index.ts
CHANGED
|
@@ -11,8 +11,29 @@ export type { Bucket } from './buckets';
|
|
|
11
11
|
export { cache } from './cache';
|
|
12
12
|
export type { CacheHandle } from './cache';
|
|
13
13
|
|
|
14
|
+
export { useCookie } from './cookies';
|
|
15
|
+
export type { UseCookieOptions } from './cookies';
|
|
16
|
+
|
|
17
|
+
export { defineBqueryConfig, getBqueryConfig } from './config';
|
|
18
|
+
export type {
|
|
19
|
+
BqueryAnnouncerConfig,
|
|
20
|
+
BqueryComponentLibraryConfig,
|
|
21
|
+
BqueryConfig,
|
|
22
|
+
BqueryCookieConfig,
|
|
23
|
+
BqueryFetchConfig,
|
|
24
|
+
BqueryFetchParseAs,
|
|
25
|
+
BqueryPageMetaConfig,
|
|
26
|
+
BqueryTransitionConfig,
|
|
27
|
+
} from './config';
|
|
28
|
+
|
|
14
29
|
export { notifications } from './notifications';
|
|
15
30
|
export type { NotificationOptions } from './notifications';
|
|
16
31
|
|
|
32
|
+
export { useAnnouncer } from './announcer';
|
|
33
|
+
export type { AnnounceOptions, AnnouncerHandle, UseAnnouncerOptions } from './announcer';
|
|
34
|
+
|
|
35
|
+
export { definePageMeta } from './meta';
|
|
36
|
+
export type { PageLinkTag, PageMetaCleanup, PageMetaDefinition, PageMetaTag } from './meta';
|
|
37
|
+
|
|
17
38
|
export { storage } from './storage';
|
|
18
39
|
export type { IndexedDBOptions, StorageAdapter } from './storage';
|