@bquery/bquery 1.4.0 → 1.5.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.
Files changed (127) hide show
  1. package/README.md +139 -120
  2. package/dist/component/component.d.ts.map +1 -1
  3. package/dist/component/index.d.ts +2 -0
  4. package/dist/component/index.d.ts.map +1 -1
  5. package/dist/component/library.d.ts +34 -0
  6. package/dist/component/library.d.ts.map +1 -0
  7. package/dist/component/types.d.ts +10 -6
  8. package/dist/component/types.d.ts.map +1 -1
  9. package/dist/component-CY5MVoYN.js +531 -0
  10. package/dist/component-CY5MVoYN.js.map +1 -0
  11. package/dist/component.es.mjs +6 -184
  12. package/dist/config-DRmZZno3.js +40 -0
  13. package/dist/config-DRmZZno3.js.map +1 -0
  14. package/dist/core-CK2Mfpf4.js +648 -0
  15. package/dist/core-CK2Mfpf4.js.map +1 -0
  16. package/dist/core-DPdbItcq.js +112 -0
  17. package/dist/core-DPdbItcq.js.map +1 -0
  18. package/dist/core.es.mjs +45 -1261
  19. package/dist/full.d.ts +6 -6
  20. package/dist/full.d.ts.map +1 -1
  21. package/dist/full.es.mjs +98 -92
  22. package/dist/full.iife.js +173 -3
  23. package/dist/full.iife.js.map +1 -1
  24. package/dist/full.umd.js +173 -3
  25. package/dist/full.umd.js.map +1 -1
  26. package/dist/index.es.mjs +143 -139
  27. package/dist/motion/transition.d.ts +1 -1
  28. package/dist/motion/transition.d.ts.map +1 -1
  29. package/dist/motion/types.d.ts +11 -1
  30. package/dist/motion/types.d.ts.map +1 -1
  31. package/dist/motion-C5DRdPnO.js +415 -0
  32. package/dist/motion-C5DRdPnO.js.map +1 -0
  33. package/dist/motion.es.mjs +25 -361
  34. package/dist/object-qGpWr6-J.js +38 -0
  35. package/dist/object-qGpWr6-J.js.map +1 -0
  36. package/dist/platform/announcer.d.ts +59 -0
  37. package/dist/platform/announcer.d.ts.map +1 -0
  38. package/dist/platform/config.d.ts +92 -0
  39. package/dist/platform/config.d.ts.map +1 -0
  40. package/dist/platform/cookies.d.ts +45 -0
  41. package/dist/platform/cookies.d.ts.map +1 -0
  42. package/dist/platform/index.d.ts +8 -0
  43. package/dist/platform/index.d.ts.map +1 -1
  44. package/dist/platform/meta.d.ts +62 -0
  45. package/dist/platform/meta.d.ts.map +1 -0
  46. package/dist/platform-B7JhGBc7.js +361 -0
  47. package/dist/platform-B7JhGBc7.js.map +1 -0
  48. package/dist/platform.es.mjs +11 -248
  49. package/dist/reactive/async-data.d.ts +114 -0
  50. package/dist/reactive/async-data.d.ts.map +1 -0
  51. package/dist/reactive/index.d.ts +2 -2
  52. package/dist/reactive/index.d.ts.map +1 -1
  53. package/dist/reactive/signal.d.ts +2 -0
  54. package/dist/reactive/signal.d.ts.map +1 -1
  55. package/dist/reactive-BDya-ia8.js +253 -0
  56. package/dist/reactive-BDya-ia8.js.map +1 -0
  57. package/dist/reactive.es.mjs +18 -34
  58. package/dist/router-CijiICxt.js +188 -0
  59. package/dist/router-CijiICxt.js.map +1 -0
  60. package/dist/router.es.mjs +11 -200
  61. package/dist/sanitize-jyJ2ryE2.js +302 -0
  62. package/dist/sanitize-jyJ2ryE2.js.map +1 -0
  63. package/dist/security/constants.d.ts.map +1 -1
  64. package/dist/security.es.mjs +10 -56
  65. package/dist/store-CPK9E62U.js +262 -0
  66. package/dist/store-CPK9E62U.js.map +1 -0
  67. package/dist/store.es.mjs +12 -25
  68. package/dist/view-Cdi0g-qo.js +396 -0
  69. package/dist/view-Cdi0g-qo.js.map +1 -0
  70. package/dist/view.es.mjs +10 -430
  71. package/package.json +15 -11
  72. package/src/component/component.ts +319 -289
  73. package/src/component/index.ts +42 -40
  74. package/src/component/library.ts +504 -0
  75. package/src/component/types.ts +91 -85
  76. package/src/core/collection.ts +628 -628
  77. package/src/core/element.ts +774 -774
  78. package/src/core/index.ts +48 -48
  79. package/src/core/utils/function.ts +151 -151
  80. package/src/full.ts +223 -187
  81. package/src/motion/animate.ts +113 -113
  82. package/src/motion/flip.ts +176 -176
  83. package/src/motion/scroll.ts +57 -57
  84. package/src/motion/spring.ts +150 -150
  85. package/src/motion/timeline.ts +246 -246
  86. package/src/motion/transition.ts +53 -7
  87. package/src/motion/types.ts +208 -198
  88. package/src/platform/announcer.ts +208 -0
  89. package/src/platform/config.ts +163 -0
  90. package/src/platform/cookies.ts +165 -0
  91. package/src/platform/index.ts +39 -18
  92. package/src/platform/meta.ts +168 -0
  93. package/src/platform/storage.ts +215 -215
  94. package/src/reactive/async-data.ts +486 -0
  95. package/src/reactive/core.ts +114 -114
  96. package/src/reactive/effect.ts +54 -54
  97. package/src/reactive/index.ts +37 -23
  98. package/src/reactive/internals.ts +122 -122
  99. package/src/reactive/signal.ts +29 -20
  100. package/src/security/constants.ts +211 -209
  101. package/src/security/sanitize-core.ts +364 -364
  102. package/src/view/evaluate.ts +290 -290
  103. package/dist/batch-x7b2eZST.js +0 -13
  104. package/dist/batch-x7b2eZST.js.map +0 -1
  105. package/dist/component.es.mjs.map +0 -1
  106. package/dist/core-BhpuvPhy.js +0 -170
  107. package/dist/core-BhpuvPhy.js.map +0 -1
  108. package/dist/core.es.mjs.map +0 -1
  109. package/dist/full.es.mjs.map +0 -1
  110. package/dist/index.es.mjs.map +0 -1
  111. package/dist/motion.es.mjs.map +0 -1
  112. package/dist/persisted-DHoi3uEs.js +0 -278
  113. package/dist/persisted-DHoi3uEs.js.map +0 -1
  114. package/dist/platform.es.mjs.map +0 -1
  115. package/dist/reactive.es.mjs.map +0 -1
  116. package/dist/router.es.mjs.map +0 -1
  117. package/dist/sanitize-Cxvxa-DX.js +0 -283
  118. package/dist/sanitize-Cxvxa-DX.js.map +0 -1
  119. package/dist/security.es.mjs.map +0 -1
  120. package/dist/store.es.mjs.map +0 -1
  121. package/dist/type-guards-BdKlYYlS.js +0 -32
  122. package/dist/type-guards-BdKlYYlS.js.map +0 -1
  123. package/dist/untrack-DNnnqdlR.js +0 -6
  124. package/dist/untrack-DNnnqdlR.js.map +0 -1
  125. package/dist/view.es.mjs.map +0 -1
  126. package/dist/watch-DXXv3iAI.js +0 -58
  127. package/dist/watch-DXXv3iAI.js.map +0 -1
@@ -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
+ };
@@ -1,18 +1,39 @@
1
- /**
2
- * Platform module providing unified endpoints for web platform APIs.
3
- * Offers consistent, promise-based interfaces with predictable errors.
4
- *
5
- * @module bquery/platform
6
- */
7
-
8
- export { buckets } from './buckets';
9
- export type { Bucket } from './buckets';
10
-
11
- export { cache } from './cache';
12
- export type { CacheHandle } from './cache';
13
-
14
- export { notifications } from './notifications';
15
- export type { NotificationOptions } from './notifications';
16
-
17
- export { storage } from './storage';
18
- export type { IndexedDBOptions, StorageAdapter } from './storage';
1
+ /**
2
+ * Platform module providing unified endpoints for web platform APIs.
3
+ * Offers consistent, promise-based interfaces with predictable errors.
4
+ *
5
+ * @module bquery/platform
6
+ */
7
+
8
+ export { buckets } from './buckets';
9
+ export type { Bucket } from './buckets';
10
+
11
+ export { cache } from './cache';
12
+ export type { CacheHandle } from './cache';
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
+
29
+ export { notifications } from './notifications';
30
+ export type { NotificationOptions } from './notifications';
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
+
38
+ export { storage } from './storage';
39
+ export type { IndexedDBOptions, StorageAdapter } from './storage';
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Document title and meta helpers.
3
+ *
4
+ * @module bquery/platform
5
+ */
6
+
7
+ import { getBqueryConfig } from './config';
8
+
9
+ /** Meta tag definition. */
10
+ export interface PageMetaTag {
11
+ /** Standard meta name attribute. */
12
+ name?: string;
13
+ /** Open Graph / custom property attribute. */
14
+ property?: string;
15
+ /** http-equiv attribute. */
16
+ httpEquiv?: string;
17
+ /** Meta tag content. */
18
+ content: string;
19
+ }
20
+
21
+ /** Link tag definition. */
22
+ export interface PageLinkTag {
23
+ /** Link relation. */
24
+ rel: string;
25
+ /** Link URL. */
26
+ href: string;
27
+ /** Optional type attribute. */
28
+ type?: string;
29
+ /** Optional media query. */
30
+ media?: string;
31
+ /** Optional crossOrigin attribute. */
32
+ crossOrigin?: 'anonymous' | 'use-credentials';
33
+ }
34
+
35
+ /** Page metadata definition. */
36
+ export interface PageMetaDefinition {
37
+ /** Document title. */
38
+ title?: string;
39
+ /** Convenience shortcut for the description meta tag. */
40
+ description?: string;
41
+ /** Additional meta tags. */
42
+ meta?: PageMetaTag[];
43
+ /** Additional link tags. */
44
+ link?: PageLinkTag[];
45
+ /** Attributes applied to the html element. */
46
+ htmlAttributes?: Record<string, string>;
47
+ /** Attributes applied to the body element. */
48
+ bodyAttributes?: Record<string, string>;
49
+ }
50
+
51
+ /** Cleanup function returned by definePageMeta(). */
52
+ export type PageMetaCleanup = () => void;
53
+
54
+ const setAttributes = (target: HTMLElement, attributes: Record<string, string>): (() => void) => {
55
+ const previousValues = new Map<string, string | null>();
56
+
57
+ for (const [name, value] of Object.entries(attributes)) {
58
+ previousValues.set(name, target.getAttribute(name));
59
+ target.setAttribute(name, value);
60
+ }
61
+
62
+ return () => {
63
+ for (const [name, value] of previousValues.entries()) {
64
+ if (value == null) {
65
+ target.removeAttribute(name);
66
+ } else {
67
+ target.setAttribute(name, value);
68
+ }
69
+ }
70
+ };
71
+ };
72
+
73
+ const createElement = <T extends 'meta' | 'link'>(
74
+ tagName: T,
75
+ attributes: Record<string, string | undefined>
76
+ ): HTMLElementTagNameMap[T] => {
77
+ const element = document.createElement(tagName);
78
+ element.setAttribute('data-bquery-page-meta', 'true');
79
+
80
+ for (const [name, value] of Object.entries(attributes)) {
81
+ if (value !== undefined) {
82
+ element.setAttribute(name, value);
83
+ }
84
+ }
85
+
86
+ return element;
87
+ };
88
+
89
+ /**
90
+ * Apply document metadata for the current page.
91
+ *
92
+ * @param definition - Title, meta tags, link tags, and document attributes
93
+ * @returns Cleanup function that restores the previous document state
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const cleanup = definePageMeta({
98
+ * title: 'Dashboard',
99
+ * description: 'Overview of your account',
100
+ * });
101
+ * ```
102
+ */
103
+ export const definePageMeta = (definition: PageMetaDefinition): PageMetaCleanup => {
104
+ if (typeof document === 'undefined') {
105
+ return () => {};
106
+ }
107
+
108
+ const config = getBqueryConfig().pageMeta;
109
+ const title = definition.title
110
+ ? config?.titleTemplate
111
+ ? config.titleTemplate(definition.title)
112
+ : definition.title
113
+ : undefined;
114
+
115
+ const inserted: HTMLElement[] = [];
116
+ const restoreFns: Array<() => void> = [];
117
+ const previousTitle = document.title;
118
+
119
+ if (title !== undefined) {
120
+ document.title = title;
121
+ }
122
+
123
+ const metaEntries = [...(definition.meta ?? [])];
124
+ if (definition.description) {
125
+ metaEntries.unshift({ name: 'description', content: definition.description });
126
+ }
127
+
128
+ for (const entry of metaEntries) {
129
+ const meta = createElement('meta', {
130
+ name: entry.name,
131
+ property: entry.property,
132
+ 'http-equiv': entry.httpEquiv,
133
+ content: entry.content,
134
+ });
135
+ document.head.appendChild(meta);
136
+ inserted.push(meta);
137
+ }
138
+
139
+ for (const entry of definition.link ?? []) {
140
+ const link = createElement('link', {
141
+ rel: entry.rel,
142
+ href: entry.href,
143
+ type: entry.type,
144
+ media: entry.media,
145
+ crossorigin: entry.crossOrigin,
146
+ });
147
+ document.head.appendChild(link);
148
+ inserted.push(link);
149
+ }
150
+
151
+ if (definition.htmlAttributes) {
152
+ restoreFns.push(setAttributes(document.documentElement, definition.htmlAttributes));
153
+ }
154
+
155
+ if (definition.bodyAttributes && document.body) {
156
+ restoreFns.push(setAttributes(document.body, definition.bodyAttributes));
157
+ }
158
+
159
+ return () => {
160
+ document.title = previousTitle;
161
+ for (const restore of restoreFns.reverse()) {
162
+ restore();
163
+ }
164
+ for (const element of inserted) {
165
+ element.remove();
166
+ }
167
+ };
168
+ };