@bquery/bquery 1.5.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.
Files changed (106) hide show
  1. package/README.md +586 -546
  2. package/dist/component/component.d.ts +13 -5
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/html.d.ts +40 -3
  5. package/dist/component/html.d.ts.map +1 -1
  6. package/dist/component/index.d.ts +2 -2
  7. package/dist/component/index.d.ts.map +1 -1
  8. package/dist/component/library.d.ts.map +1 -1
  9. package/dist/component/types.d.ts +131 -16
  10. package/dist/component/types.d.ts.map +1 -1
  11. package/dist/component-BEQgt5hl.js +600 -0
  12. package/dist/component-BEQgt5hl.js.map +1 -0
  13. package/dist/component.es.mjs +7 -6
  14. package/dist/config-DRmZZno3.js.map +1 -1
  15. package/dist/core-BGQJVw0-.js +35 -0
  16. package/dist/core-BGQJVw0-.js.map +1 -0
  17. package/dist/{core-CK2Mfpf4.js → core-CCEabVHl.js} +2 -2
  18. package/dist/{core-CK2Mfpf4.js.map → core-CCEabVHl.js.map} +1 -1
  19. package/dist/core.es.mjs +1 -1
  20. package/dist/effect-AFRW_Plg.js +84 -0
  21. package/dist/effect-AFRW_Plg.js.map +1 -0
  22. package/dist/full.d.ts +4 -4
  23. package/dist/full.d.ts.map +1 -1
  24. package/dist/full.es.mjs +98 -94
  25. package/dist/full.iife.js +14 -14
  26. package/dist/full.iife.js.map +1 -1
  27. package/dist/full.umd.js +14 -14
  28. package/dist/full.umd.js.map +1 -1
  29. package/dist/index.es.mjs +143 -139
  30. package/dist/{motion-C5DRdPnO.js → motion-D9TcHxOF.js} +1 -1
  31. package/dist/{motion-C5DRdPnO.js.map → motion-D9TcHxOF.js.map} +1 -1
  32. package/dist/motion.es.mjs +1 -1
  33. package/dist/{platform-B7JhGBc7.js → platform-Dr9b6fsq.js} +21 -20
  34. package/dist/platform-Dr9b6fsq.js.map +1 -0
  35. package/dist/platform.es.mjs +1 -1
  36. package/dist/{reactive-BDya-ia8.js → reactive-DSkct0dO.js} +51 -50
  37. package/dist/reactive-DSkct0dO.js.map +1 -0
  38. package/dist/reactive.es.mjs +19 -17
  39. package/dist/{router-CijiICxt.js → router-CbDhl8rS.js} +3 -3
  40. package/dist/{router-CijiICxt.js.map → router-CbDhl8rS.js.map} +1 -1
  41. package/dist/router.es.mjs +1 -1
  42. package/dist/{sanitize-jyJ2ryE2.js → sanitize-Bs2dkMby.js} +94 -83
  43. package/dist/sanitize-Bs2dkMby.js.map +1 -0
  44. package/dist/security/index.d.ts +4 -2
  45. package/dist/security/index.d.ts.map +1 -1
  46. package/dist/security/sanitize.d.ts +4 -1
  47. package/dist/security/sanitize.d.ts.map +1 -1
  48. package/dist/security/trusted-html.d.ts +53 -0
  49. package/dist/security/trusted-html.d.ts.map +1 -0
  50. package/dist/security.es.mjs +10 -9
  51. package/dist/store/define-store.d.ts +1 -1
  52. package/dist/store/define-store.d.ts.map +1 -1
  53. package/dist/store/mapping.d.ts +1 -1
  54. package/dist/store/mapping.d.ts.map +1 -1
  55. package/dist/store/persisted.d.ts +1 -1
  56. package/dist/store/persisted.d.ts.map +1 -1
  57. package/dist/store/types.d.ts +2 -2
  58. package/dist/store/types.d.ts.map +1 -1
  59. package/dist/store/watch.d.ts +1 -1
  60. package/dist/store/watch.d.ts.map +1 -1
  61. package/dist/{store-CPK9E62U.js → store-BwDvI45q.js} +49 -48
  62. package/dist/{store-CPK9E62U.js.map → store-BwDvI45q.js.map} +1 -1
  63. package/dist/store.es.mjs +1 -1
  64. package/dist/storybook/index.d.ts +37 -0
  65. package/dist/storybook/index.d.ts.map +1 -0
  66. package/dist/storybook.es.mjs +151 -0
  67. package/dist/storybook.es.mjs.map +1 -0
  68. package/dist/untrack-B0rVscTc.js +7 -0
  69. package/dist/untrack-B0rVscTc.js.map +1 -0
  70. package/dist/{view-Cdi0g-qo.js → view-C70lA3vf.js} +29 -28
  71. package/dist/{view-Cdi0g-qo.js.map → view-C70lA3vf.js.map} +1 -1
  72. package/dist/view.es.mjs +9 -8
  73. package/package.json +141 -136
  74. package/src/component/component.ts +259 -54
  75. package/src/component/html.ts +153 -53
  76. package/src/component/index.ts +10 -2
  77. package/src/component/library.ts +42 -28
  78. package/src/component/types.ts +184 -19
  79. package/src/full.ts +8 -2
  80. package/src/motion/transition.ts +97 -97
  81. package/src/motion/types.ts +208 -208
  82. package/src/platform/announcer.ts +208 -208
  83. package/src/platform/config.ts +163 -163
  84. package/src/platform/cookies.ts +165 -165
  85. package/src/platform/index.ts +39 -39
  86. package/src/platform/meta.ts +168 -168
  87. package/src/reactive/async-data.ts +486 -486
  88. package/src/reactive/index.ts +37 -37
  89. package/src/reactive/signal.ts +29 -29
  90. package/src/security/constants.ts +211 -211
  91. package/src/security/index.ts +17 -10
  92. package/src/security/sanitize.ts +70 -66
  93. package/src/security/trusted-html.ts +71 -0
  94. package/src/store/define-store.ts +49 -48
  95. package/src/store/mapping.ts +74 -73
  96. package/src/store/persisted.ts +62 -61
  97. package/src/store/types.ts +92 -94
  98. package/src/store/watch.ts +53 -52
  99. package/src/storybook/index.ts +479 -0
  100. package/dist/component-CY5MVoYN.js +0 -531
  101. package/dist/component-CY5MVoYN.js.map +0 -1
  102. package/dist/core-DPdbItcq.js +0 -112
  103. package/dist/core-DPdbItcq.js.map +0 -1
  104. package/dist/platform-B7JhGBc7.js.map +0 -1
  105. package/dist/reactive-BDya-ia8.js.map +0 -1
  106. package/dist/sanitize-jyJ2ryE2.js.map +0 -1
@@ -87,20 +87,25 @@ const storeHandler = (element: HTMLElement, key: string, value: EventListener):
87
87
  handlerStore.set(element, handlers);
88
88
  };
89
89
 
90
- const getShadowLabelText = (element: HTMLElement): string => {
91
- return element.shadowRoot?.querySelector('.label')?.textContent ?? '';
92
- };
93
-
94
90
  /**
95
91
  * Detect a value-only input update, patch the live control in place, and
96
92
  * return whether the component can skip a full shadow DOM re-render.
97
93
  *
98
94
  * @param element - The host custom element whose shadow DOM is being updated
99
- * @param props - The next reflected input props for the pending update
95
+ * @param newProps - The next reflected input props for the pending update
96
+ * @param oldProps - The previous reflected input props from the last render
100
97
  */
101
98
  const canSkipInputRender = (
102
99
  element: HTMLElement,
103
- props: {
100
+ newProps: {
101
+ label: string;
102
+ type: string;
103
+ value: string;
104
+ placeholder: string;
105
+ name: string;
106
+ disabled: boolean;
107
+ },
108
+ oldProps: {
104
109
  label: string;
105
110
  type: string;
106
111
  value: string;
@@ -109,17 +114,17 @@ const canSkipInputRender = (
109
114
  disabled: boolean;
110
115
  }
111
116
  ): boolean => {
117
+ if (oldProps.label !== newProps.label) return false;
118
+ if (oldProps.type !== newProps.type) return false;
119
+ if (oldProps.placeholder !== newProps.placeholder) return false;
120
+ if (oldProps.name !== newProps.name) return false;
121
+ if (oldProps.disabled !== newProps.disabled) return false;
122
+
112
123
  const control = element.shadowRoot?.querySelector('input.control') as HTMLInputElement | null;
113
124
  if (!control) return false;
114
125
 
115
- if (getShadowLabelText(element) !== props.label) return false;
116
- if ((control.getAttribute('type') ?? 'text') !== props.type) return false;
117
- if ((control.getAttribute('placeholder') ?? '') !== props.placeholder) return false;
118
- if ((control.getAttribute('name') ?? '') !== props.name) return false;
119
- if (control.disabled !== props.disabled) return false;
120
-
121
- if (control.value !== props.value) {
122
- control.value = props.value;
126
+ if (control.value !== newProps.value) {
127
+ control.value = newProps.value;
123
128
  }
124
129
 
125
130
  return true;
@@ -130,11 +135,20 @@ const canSkipInputRender = (
130
135
  * return whether the component can skip a full shadow DOM re-render.
131
136
  *
132
137
  * @param element - The host custom element whose shadow DOM is being updated
133
- * @param props - The next reflected textarea props for the pending update
138
+ * @param newProps - The next reflected textarea props for the pending update
139
+ * @param oldProps - The previous reflected textarea props from the last render
134
140
  */
135
141
  const canSkipTextareaRender = (
136
142
  element: HTMLElement,
137
- props: {
143
+ newProps: {
144
+ label: string;
145
+ value: string;
146
+ placeholder: string;
147
+ name: string;
148
+ rows: number;
149
+ disabled: boolean;
150
+ },
151
+ oldProps: {
138
152
  label: string;
139
153
  value: string;
140
154
  placeholder: string;
@@ -143,19 +157,19 @@ const canSkipTextareaRender = (
143
157
  disabled: boolean;
144
158
  }
145
159
  ): boolean => {
160
+ if (oldProps.label !== newProps.label) return false;
161
+ if (oldProps.placeholder !== newProps.placeholder) return false;
162
+ if (oldProps.name !== newProps.name) return false;
163
+ if (oldProps.rows !== newProps.rows) return false;
164
+ if (oldProps.disabled !== newProps.disabled) return false;
165
+
146
166
  const control = element.shadowRoot?.querySelector(
147
167
  'textarea.control'
148
168
  ) as HTMLTextAreaElement | null;
149
169
  if (!control) return false;
150
170
 
151
- if (getShadowLabelText(element) !== props.label) return false;
152
- if ((control.getAttribute('placeholder') ?? '') !== props.placeholder) return false;
153
- if ((control.getAttribute('name') ?? '') !== props.name) return false;
154
- if (control.getAttribute('rows') !== String(props.rows)) return false;
155
- if (control.disabled !== props.disabled) return false;
156
-
157
- if (control.value !== props.value) {
158
- control.value = props.value;
171
+ if (control.value !== newProps.value) {
172
+ control.value = newProps.value;
159
173
  }
160
174
  return true;
161
175
  };
@@ -328,8 +342,8 @@ export const registerDefaultComponents = (
328
342
  * Skip the full shadow DOM re-render when only the reflected input value
329
343
  * changed, because the live control has already been patched in place.
330
344
  */
331
- beforeUpdate(props) {
332
- if (canSkipInputRender(this, props)) {
345
+ beforeUpdate(newProps, oldProps) {
346
+ if (canSkipInputRender(this, newProps, oldProps)) {
333
347
  return false;
334
348
  }
335
349
  return true;
@@ -399,8 +413,8 @@ export const registerDefaultComponents = (
399
413
  * Skip the full shadow DOM re-render when only the reflected textarea value
400
414
  * changed, because the live control has already been patched in place.
401
415
  */
402
- beforeUpdate(props) {
403
- if (canSkipTextareaRender(this, props)) {
416
+ beforeUpdate(newProps, oldProps) {
417
+ if (canSkipTextareaRender(this, newProps, oldProps)) {
404
418
  return false;
405
419
  }
406
420
  return true;
@@ -4,6 +4,8 @@
4
4
  * @module bquery/component
5
5
  */
6
6
 
7
+ import type { SanitizeOptions } from '../security/types';
8
+
7
9
  /**
8
10
  * Defines a single prop's type and configuration.
9
11
  *
@@ -43,49 +45,212 @@ export type PropDefinition<T = unknown> = {
43
45
  construct?: boolean;
44
46
  };
45
47
 
48
+ /**
49
+ * Resolves the concrete runtime state shape exposed by component APIs.
50
+ *
51
+ * When no explicit state generic is provided, component state falls back to
52
+ * an untyped string-keyed record for backwards compatibility.
53
+ */
54
+ export type ComponentStateShape<
55
+ TState extends Record<string, unknown> | undefined = undefined,
56
+ > = TState extends Record<string, unknown> ? TState : Record<string, unknown>;
57
+
58
+ /**
59
+ * Component state keys are string-based because runtime state access is backed
60
+ * by plain object properties.
61
+ */
62
+ export type ComponentStateKey<
63
+ TState extends Record<string, unknown> | undefined = undefined,
64
+ > =
65
+ keyof ComponentStateShape<TState> & string;
66
+
67
+ /**
68
+ * Public component element instance shape exposed by lifecycle hooks and
69
+ * `defineComponent()` return values.
70
+ */
71
+ export type ComponentElement<
72
+ TState extends Record<string, unknown> | undefined = undefined,
73
+ > = HTMLElement & {
74
+ /**
75
+ * Updates a state property and triggers a re-render.
76
+ *
77
+ * @param key - The state property key
78
+ * @param value - The new value
79
+ */
80
+ setState<TKey extends ComponentStateKey<TState>>(
81
+ key: TKey,
82
+ value: ComponentStateShape<TState>[TKey]
83
+ ): void;
84
+ /**
85
+ * Gets a state property value.
86
+ *
87
+ * @param key - The state property key
88
+ * @returns The current value
89
+ */
90
+ getState<TKey extends ComponentStateKey<TState>>(
91
+ key: TKey
92
+ ): ComponentStateShape<TState>[TKey];
93
+ /**
94
+ * Gets a state property value with an explicit cast for backwards
95
+ * compatibility with the pre-typed-state API.
96
+ *
97
+ * @param key - The state property key
98
+ * @returns The current value cast to `TResult`
99
+ */
100
+ getState<TResult = unknown>(key: string): TResult;
101
+ };
102
+
103
+ /**
104
+ * Constructor returned by `defineComponent()`.
105
+ */
106
+ export type ComponentClass<TState extends Record<string, unknown> | undefined = undefined> =
107
+ CustomElementConstructor & {
108
+ new (): ComponentElement<TState>;
109
+ prototype: ComponentElement<TState>;
110
+ readonly observedAttributes: string[];
111
+ };
112
+
113
+ /**
114
+ * Minimal reactive source shape supported by component `signals`.
115
+ *
116
+ * @template T - Value exposed by the signal-like source
117
+ */
118
+ export type ComponentSignalLike<T = unknown> = {
119
+ /** Gets the current reactive value */
120
+ readonly value: T;
121
+ /** Gets the current value without dependency tracking */
122
+ peek(): T;
123
+ };
124
+
125
+ /**
126
+ * Named reactive sources that can drive component re-renders.
127
+ */
128
+ export type ComponentSignals = Record<string, ComponentSignalLike<unknown>>;
129
+
46
130
  /**
47
131
  * Render context passed into a component render function.
132
+ *
133
+ * @template TProps - Type of the component's props
134
+ * @template TState - Type of the component's internal state
135
+ * @template TSignals - Declared reactive sources available during render
48
136
  */
49
- export type ComponentRenderContext<TProps extends Record<string, unknown>> = {
137
+ export type ComponentRenderContext<
138
+ TProps extends Record<string, unknown>,
139
+ TState extends Record<string, unknown> | undefined = undefined,
140
+ TSignals extends ComponentSignals = Record<string, never>,
141
+ > = {
50
142
  /** Typed props object populated from attributes */
51
143
  props: TProps;
52
144
  /** Internal mutable state object */
53
- state: Record<string, unknown>;
145
+ state: ComponentStateShape<TState>;
146
+ /** External reactive sources subscribed for re-rendering */
147
+ signals: TSignals;
54
148
  /** Emit a custom event from the component */
55
149
  emit: (event: string, detail?: unknown) => void;
56
150
  };
57
151
 
152
+ /**
153
+ * Describes an observed attribute change that triggered a component update.
154
+ */
155
+ export type AttributeChange = {
156
+ /** The observed attribute name */
157
+ name: string;
158
+ /** The previous serialized attribute value */
159
+ oldValue: string | null;
160
+ /** The next serialized attribute value */
161
+ newValue: string | null;
162
+ };
163
+
58
164
  /**
59
165
  * Complete component definition including props, state, styles, and lifecycle.
60
166
  *
61
167
  * @template TProps - Type of the component's props
168
+ * @template TState - Type of the component's internal state
62
169
  */
63
- type ComponentHook<TResult = void> = ((this: HTMLElement) => TResult) | (() => TResult);
64
- type ComponentHookWithProps<TProps extends Record<string, unknown>, TResult = void> =
65
- | ((this: HTMLElement, props: TProps) => TResult)
66
- | ((props: TProps) => TResult);
67
- type ComponentErrorHook = ((this: HTMLElement, error: Error) => void) | ((error: Error) => void);
170
+ /*
171
+ * Lifecycle hooks use dynamic `this` when declared with method/function syntax.
172
+ * Arrow functions capture outer scope, so component APIs like `this.getState()`
173
+ * are only available from method/function syntax.
174
+ */
175
+ type ComponentSanitizeOptions = Pick<SanitizeOptions, 'allowTags' | 'allowAttributes'>;
176
+ type ComponentHook<
177
+ TState extends Record<string, unknown> | undefined = undefined,
178
+ TResult = void,
179
+ > = {
180
+ (this: ComponentElement<TState>): TResult;
181
+ (): TResult;
182
+ };
183
+ type ComponentHookWithProps<
184
+ TProps extends Record<string, unknown>,
185
+ TState extends Record<string, unknown> | undefined = undefined,
186
+ TResult = void,
187
+ > = {
188
+ (this: ComponentElement<TState>, newProps: TProps, oldProps: TProps): TResult;
189
+ (newProps: TProps, oldProps: TProps): TResult;
190
+ };
191
+ type ComponentUpdatedHook<
192
+ TState extends Record<string, unknown> | undefined = undefined,
193
+ TResult = void,
194
+ > = {
195
+ (this: ComponentElement<TState>, change?: AttributeChange): TResult;
196
+ (change?: AttributeChange): TResult;
197
+ };
198
+ type ComponentErrorHook<TState extends Record<string, unknown> | undefined = undefined> = {
199
+ (this: ComponentElement<TState>, error: Error): void;
200
+ (error: Error): void;
201
+ };
202
+
203
+ type ComponentStateDefinition<TState extends Record<string, unknown> | undefined = undefined> =
204
+ TState extends Record<string, unknown>
205
+ ? {
206
+ /** Initial internal state */
207
+ state: TState;
208
+ }
209
+ : {
210
+ /** Initial internal state */
211
+ state?: Record<string, unknown>;
212
+ };
213
+
214
+ type ComponentSignalsDefinition<TSignals extends ComponentSignals = Record<string, never>> =
215
+ TSignals extends Record<string, never>
216
+ ? {
217
+ /** External signals/computed values that should trigger re-renders */
218
+ signals?: TSignals;
219
+ }
220
+ : {
221
+ /** External signals/computed values that should trigger re-renders */
222
+ signals: TSignals;
223
+ };
68
224
 
69
- export type ComponentDefinition<TProps extends Record<string, unknown> = Record<string, unknown>> =
70
- {
225
+ export type ComponentDefinition<
226
+ TProps extends Record<string, unknown> = Record<string, unknown>,
227
+ TState extends Record<string, unknown> | undefined = undefined,
228
+ TSignals extends ComponentSignals = Record<string, never>,
229
+ > = ComponentStateDefinition<TState> &
230
+ ComponentSignalsDefinition<TSignals> & {
71
231
  /** Prop definitions with types and defaults */
72
232
  props?: Record<keyof TProps, PropDefinition>;
73
- /** Initial internal state */
74
- state?: Record<string, unknown>;
75
233
  /** CSS styles scoped to the component's shadow DOM */
76
234
  styles?: string;
235
+ /**
236
+ * Extra sanitizer options merged with the framework base allowlist during render.
237
+ * Only opt in attributes/tags whose values you control or validate. Sensitive
238
+ * attributes such as `style` are not value-sanitized and can reintroduce XSS
239
+ * or UI-redressing risks if used with untrusted input.
240
+ */
241
+ sanitize?: ComponentSanitizeOptions;
77
242
  /** Lifecycle hook called before the component mounts (before first render) */
78
- beforeMount?: ComponentHook;
243
+ beforeMount?: ComponentHook<TState>;
79
244
  /** Lifecycle hook called when component is added to DOM */
80
- connected?: ComponentHook;
245
+ connected?: ComponentHook<TState>;
81
246
  /** Lifecycle hook called when component is removed from DOM */
82
- disconnected?: ComponentHook;
247
+ disconnected?: ComponentHook<TState>;
83
248
  /** Lifecycle hook called before an update render; return false to prevent */
84
- beforeUpdate?: ComponentHookWithProps<TProps, boolean | void>;
85
- /** Lifecycle hook called after reactive updates trigger a render */
86
- updated?: ComponentHook;
249
+ beforeUpdate?: ComponentHookWithProps<TProps, TState, boolean | void>;
250
+ /** Lifecycle hook called after update renders; receives attribute change info when applicable */
251
+ updated?: ComponentUpdatedHook<TState>;
87
252
  /** Error handler for errors during rendering or lifecycle */
88
- onError?: ComponentErrorHook;
253
+ onError?: ComponentErrorHook<TState>;
89
254
  /** Render function returning HTML string */
90
- render: (context: ComponentRenderContext<TProps>) => string;
255
+ render: (context: ComponentRenderContext<TProps, TState, TSignals>) => string;
91
256
  };
package/src/full.ts CHANGED
@@ -77,9 +77,14 @@ export type {
77
77
  // ============================================================================
78
78
  // Component Module: Web Components helper with Shadow DOM
79
79
  // ============================================================================
80
- export { component, html, registerDefaultComponents, safeHtml } from './component/index';
80
+ export { bool, component, html, registerDefaultComponents, safeHtml } from './component/index';
81
81
  export type {
82
+ AttributeChange,
82
83
  ComponentDefinition,
84
+ ComponentRenderContext,
85
+ ComponentStateKey,
86
+ ComponentSignalLike,
87
+ ComponentSignals,
83
88
  DefaultComponentLibraryOptions,
84
89
  PropDefinition,
85
90
  RegisteredDefaultComponents,
@@ -147,8 +152,9 @@ export {
147
152
  sanitize,
148
153
  sanitizeHtml,
149
154
  stripTags,
155
+ trusted,
150
156
  } from './security/index';
151
- export type { SanitizeOptions } from './security/index';
157
+ export type { SanitizedHtml, SanitizeOptions, TrustedHtml } from './security/index';
152
158
 
153
159
  // ============================================================================
154
160
  // Platform Module: Storage, buckets, notifications, cache
@@ -1,97 +1,97 @@
1
- /**
2
- * View transition helpers.
3
- *
4
- * @module bquery/motion
5
- */
6
-
7
- import type { TransitionOptions } from './types';
8
- import { prefersReducedMotion } from './reduced-motion';
9
- import { getBqueryConfig } from '../platform/config';
10
-
11
- /** Extended document type with View Transitions API */
12
- type DocumentWithTransition = Document & {
13
- startViewTransition?: (callback: () => void | Promise<void>) => {
14
- finished: Promise<void>;
15
- ready: Promise<void>;
16
- updateCallbackDone: Promise<void>;
17
- skipTransition?: () => void;
18
- types?: {
19
- add: (type: string) => void;
20
- };
21
- };
22
- };
23
-
24
- const sanitizeTokens = (tokens?: string[]): string[] =>
25
- (tokens ?? []).map((token) => token.trim()).filter((token) => token.length > 0);
26
-
27
- /**
28
- * Execute a DOM update with view transition animation.
29
- * Falls back to immediate update when View Transitions API is unavailable.
30
- *
31
- * @param updateOrOptions - Update function or options object
32
- * @returns Promise that resolves when transition completes
33
- *
34
- * @example
35
- * ```ts
36
- * await transition(() => {
37
- * $('#content').text('Updated');
38
- * });
39
- * ```
40
- */
41
- export const transition = async (
42
- updateOrOptions: (() => void | Promise<void>) | TransitionOptions
43
- ): Promise<void> => {
44
- const config = getBqueryConfig().transitions;
45
- const options: TransitionOptions =
46
- typeof updateOrOptions === 'function'
47
- ? {
48
- update: updateOrOptions,
49
- classes: config?.classes,
50
- types: config?.types,
51
- skipOnReducedMotion: config?.skipOnReducedMotion,
52
- }
53
- : {
54
- ...updateOrOptions,
55
- classes: updateOrOptions.classes ?? config?.classes,
56
- types: updateOrOptions.types ?? config?.types,
57
- skipOnReducedMotion: updateOrOptions.skipOnReducedMotion ?? config?.skipOnReducedMotion,
58
- };
59
- const update = options.update;
60
-
61
- // SSR/non-DOM environment fallback
62
- if (typeof document === 'undefined') {
63
- await update();
64
- return;
65
- }
66
-
67
- const doc = document as DocumentWithTransition;
68
- const root = document.documentElement;
69
- const classes = sanitizeTokens(options.classes);
70
- const types = sanitizeTokens(options.types);
71
-
72
- if (!doc.startViewTransition || (options.skipOnReducedMotion && prefersReducedMotion())) {
73
- await update();
74
- options.onFinish?.();
75
- return;
76
- }
77
-
78
- classes.forEach((className: string) => root.classList.add(className));
79
-
80
- try {
81
- const viewTransition = doc.startViewTransition(() => update());
82
- const transitionTypes = viewTransition.types;
83
-
84
- if (transitionTypes) {
85
- for (const type of types) {
86
- transitionTypes.add(type);
87
- }
88
- }
89
-
90
- await viewTransition.ready;
91
- options.onReady?.();
92
- await viewTransition.finished;
93
- options.onFinish?.();
94
- } finally {
95
- classes.forEach((className: string) => root.classList.remove(className));
96
- }
97
- };
1
+ /**
2
+ * View transition helpers.
3
+ *
4
+ * @module bquery/motion
5
+ */
6
+
7
+ import type { TransitionOptions } from './types';
8
+ import { prefersReducedMotion } from './reduced-motion';
9
+ import { getBqueryConfig } from '../platform/config';
10
+
11
+ /** Extended document type with View Transitions API */
12
+ type DocumentWithTransition = Document & {
13
+ startViewTransition?: (callback: () => void | Promise<void>) => {
14
+ finished: Promise<void>;
15
+ ready: Promise<void>;
16
+ updateCallbackDone: Promise<void>;
17
+ skipTransition?: () => void;
18
+ types?: {
19
+ add: (type: string) => void;
20
+ };
21
+ };
22
+ };
23
+
24
+ const sanitizeTokens = (tokens?: string[]): string[] =>
25
+ (tokens ?? []).map((token) => token.trim()).filter((token) => token.length > 0);
26
+
27
+ /**
28
+ * Execute a DOM update with view transition animation.
29
+ * Falls back to immediate update when View Transitions API is unavailable.
30
+ *
31
+ * @param updateOrOptions - Update function or options object
32
+ * @returns Promise that resolves when transition completes
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * await transition(() => {
37
+ * $('#content').text('Updated');
38
+ * });
39
+ * ```
40
+ */
41
+ export const transition = async (
42
+ updateOrOptions: (() => void | Promise<void>) | TransitionOptions
43
+ ): Promise<void> => {
44
+ const config = getBqueryConfig().transitions;
45
+ const options: TransitionOptions =
46
+ typeof updateOrOptions === 'function'
47
+ ? {
48
+ update: updateOrOptions,
49
+ classes: config?.classes,
50
+ types: config?.types,
51
+ skipOnReducedMotion: config?.skipOnReducedMotion,
52
+ }
53
+ : {
54
+ ...updateOrOptions,
55
+ classes: updateOrOptions.classes ?? config?.classes,
56
+ types: updateOrOptions.types ?? config?.types,
57
+ skipOnReducedMotion: updateOrOptions.skipOnReducedMotion ?? config?.skipOnReducedMotion,
58
+ };
59
+ const update = options.update;
60
+
61
+ // SSR/non-DOM environment fallback
62
+ if (typeof document === 'undefined') {
63
+ await update();
64
+ return;
65
+ }
66
+
67
+ const doc = document as DocumentWithTransition;
68
+ const root = document.documentElement;
69
+ const classes = sanitizeTokens(options.classes);
70
+ const types = sanitizeTokens(options.types);
71
+
72
+ if (!doc.startViewTransition || (options.skipOnReducedMotion && prefersReducedMotion())) {
73
+ await update();
74
+ options.onFinish?.();
75
+ return;
76
+ }
77
+
78
+ classes.forEach((className: string) => root.classList.add(className));
79
+
80
+ try {
81
+ const viewTransition = doc.startViewTransition(() => update());
82
+ const transitionTypes = viewTransition.types;
83
+
84
+ if (transitionTypes) {
85
+ for (const type of types) {
86
+ transitionTypes.add(type);
87
+ }
88
+ }
89
+
90
+ await viewTransition.ready;
91
+ options.onReady?.();
92
+ await viewTransition.finished;
93
+ options.onFinish?.();
94
+ } finally {
95
+ classes.forEach((className: string) => root.classList.remove(className));
96
+ }
97
+ };