@bquery/bquery 1.0.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/LICENSE.md +21 -0
- package/README.md +266 -0
- package/dist/component/index.d.ts +155 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component.es.mjs +128 -0
- package/dist/component.es.mjs.map +1 -0
- package/dist/core/collection.d.ts +198 -0
- package/dist/core/collection.d.ts.map +1 -0
- package/dist/core/element.d.ts +301 -0
- package/dist/core/element.d.ts.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/selector.d.ts +11 -0
- package/dist/core/selector.d.ts.map +1 -0
- package/dist/core/shared.d.ts +7 -0
- package/dist/core/shared.d.ts.map +1 -0
- package/dist/core/utils.d.ts +300 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core.es.mjs +1015 -0
- package/dist/core.es.mjs.map +1 -0
- package/dist/full.d.ts +48 -0
- package/dist/full.d.ts.map +1 -0
- package/dist/full.es.mjs +43 -0
- package/dist/full.es.mjs.map +1 -0
- package/dist/full.iife.js +2 -0
- package/dist/full.iife.js.map +1 -0
- package/dist/full.umd.js +2 -0
- package/dist/full.umd.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.mjs +43 -0
- package/dist/index.es.mjs.map +1 -0
- package/dist/motion/index.d.ts +145 -0
- package/dist/motion/index.d.ts.map +1 -0
- package/dist/motion.es.mjs +104 -0
- package/dist/motion.es.mjs.map +1 -0
- package/dist/platform/buckets.d.ts +44 -0
- package/dist/platform/buckets.d.ts.map +1 -0
- package/dist/platform/cache.d.ts +71 -0
- package/dist/platform/cache.d.ts.map +1 -0
- package/dist/platform/index.d.ts +15 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/notifications.d.ts +52 -0
- package/dist/platform/notifications.d.ts.map +1 -0
- package/dist/platform/storage.d.ts +69 -0
- package/dist/platform/storage.d.ts.map +1 -0
- package/dist/platform.es.mjs +245 -0
- package/dist/platform.es.mjs.map +1 -0
- package/dist/reactive/index.d.ts +8 -0
- package/dist/reactive/index.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +204 -0
- package/dist/reactive/signal.d.ts.map +1 -0
- package/dist/reactive.es.mjs +123 -0
- package/dist/reactive.es.mjs.map +1 -0
- package/dist/security/index.d.ts +8 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/sanitize.d.ts +99 -0
- package/dist/security/sanitize.d.ts.map +1 -0
- package/dist/security.es.mjs +194 -0
- package/dist/security.es.mjs.map +1 -0
- package/package.json +120 -0
- package/src/component/index.ts +360 -0
- package/src/core/collection.ts +339 -0
- package/src/core/element.ts +493 -0
- package/src/core/index.ts +4 -0
- package/src/core/selector.ts +29 -0
- package/src/core/shared.ts +13 -0
- package/src/core/utils.ts +425 -0
- package/src/full.ts +101 -0
- package/src/index.ts +27 -0
- package/src/motion/index.ts +365 -0
- package/src/platform/buckets.ts +115 -0
- package/src/platform/cache.ts +130 -0
- package/src/platform/index.ts +18 -0
- package/src/platform/notifications.ts +87 -0
- package/src/platform/storage.ts +208 -0
- package/src/reactive/index.ts +9 -0
- package/src/reactive/signal.ts +347 -0
- package/src/security/index.ts +18 -0
- package/src/security/sanitize.ts +446 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion module providing view transitions, FLIP animations, and spring physics.
|
|
3
|
+
* Designed to work with modern browser APIs while providing smooth fallbacks.
|
|
4
|
+
*
|
|
5
|
+
* @module bquery/motion
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for view transitions.
|
|
14
|
+
*/
|
|
15
|
+
export interface TransitionOptions {
|
|
16
|
+
/** The DOM update function to execute during transition */
|
|
17
|
+
update: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Captured element bounds for FLIP animations.
|
|
22
|
+
*/
|
|
23
|
+
export interface ElementBounds {
|
|
24
|
+
top: number;
|
|
25
|
+
left: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* FLIP animation configuration options.
|
|
32
|
+
*/
|
|
33
|
+
export interface FlipOptions {
|
|
34
|
+
/** Animation duration in milliseconds */
|
|
35
|
+
duration?: number;
|
|
36
|
+
/** CSS easing function */
|
|
37
|
+
easing?: string;
|
|
38
|
+
/** Callback when animation completes */
|
|
39
|
+
onComplete?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Spring physics configuration.
|
|
44
|
+
*/
|
|
45
|
+
export interface SpringConfig {
|
|
46
|
+
/** Spring stiffness (default: 100) */
|
|
47
|
+
stiffness?: number;
|
|
48
|
+
/** Damping coefficient (default: 10) */
|
|
49
|
+
damping?: number;
|
|
50
|
+
/** Mass of the object (default: 1) */
|
|
51
|
+
mass?: number;
|
|
52
|
+
/** Velocity threshold for completion (default: 0.01) */
|
|
53
|
+
precision?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Spring instance for animating values.
|
|
58
|
+
*/
|
|
59
|
+
export interface Spring {
|
|
60
|
+
/** Start animating to target value */
|
|
61
|
+
to(target: number): Promise<void>;
|
|
62
|
+
/** Get current animated value */
|
|
63
|
+
current(): number;
|
|
64
|
+
/** Stop the animation */
|
|
65
|
+
stop(): void;
|
|
66
|
+
/** Subscribe to value changes */
|
|
67
|
+
onChange(callback: (value: number) => void): () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// View Transitions
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/** Extended document type with View Transitions API */
|
|
75
|
+
type DocumentWithTransition = Document & {
|
|
76
|
+
startViewTransition?: (callback: () => void) => {
|
|
77
|
+
finished: Promise<void>;
|
|
78
|
+
ready: Promise<void>;
|
|
79
|
+
updateCallbackDone: Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute a DOM update with view transition animation.
|
|
85
|
+
* Falls back to immediate update when View Transitions API is unavailable.
|
|
86
|
+
*
|
|
87
|
+
* @param updateOrOptions - Update function or options object
|
|
88
|
+
* @returns Promise that resolves when transition completes
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* await transition(() => {
|
|
93
|
+
* $('#content').text('Updated');
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export const transition = async (
|
|
98
|
+
updateOrOptions: (() => void) | TransitionOptions
|
|
99
|
+
): Promise<void> => {
|
|
100
|
+
const update = typeof updateOrOptions === 'function' ? updateOrOptions : updateOrOptions.update;
|
|
101
|
+
|
|
102
|
+
const doc = document as DocumentWithTransition;
|
|
103
|
+
|
|
104
|
+
if (doc.startViewTransition) {
|
|
105
|
+
await doc.startViewTransition(() => update()).finished;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
update();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// FLIP Animations
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Capture the current bounds of an element for FLIP animation.
|
|
118
|
+
*
|
|
119
|
+
* @param element - The DOM element to measure
|
|
120
|
+
* @returns The element's current position and size
|
|
121
|
+
*/
|
|
122
|
+
export const capturePosition = (element: Element): ElementBounds => {
|
|
123
|
+
const rect = element.getBoundingClientRect();
|
|
124
|
+
return {
|
|
125
|
+
top: rect.top,
|
|
126
|
+
left: rect.left,
|
|
127
|
+
width: rect.width,
|
|
128
|
+
height: rect.height,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Perform a FLIP (First, Last, Invert, Play) animation.
|
|
134
|
+
* Animates an element from its captured position to its current position.
|
|
135
|
+
*
|
|
136
|
+
* @param element - The element to animate
|
|
137
|
+
* @param firstBounds - The previously captured bounds
|
|
138
|
+
* @param options - Animation configuration
|
|
139
|
+
* @returns Promise that resolves when animation completes
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const first = capturePosition(element);
|
|
144
|
+
* // ... DOM changes that move the element ...
|
|
145
|
+
* await flip(element, first, { duration: 300 });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export const flip = (
|
|
149
|
+
element: Element,
|
|
150
|
+
firstBounds: ElementBounds,
|
|
151
|
+
options: FlipOptions = {}
|
|
152
|
+
): Promise<void> => {
|
|
153
|
+
const { duration = 300, easing = 'ease-out', onComplete } = options;
|
|
154
|
+
|
|
155
|
+
// Last: Get current position
|
|
156
|
+
const lastBounds = capturePosition(element);
|
|
157
|
+
|
|
158
|
+
// Skip animation if element has zero dimensions (avoid division by zero)
|
|
159
|
+
if (lastBounds.width === 0 || lastBounds.height === 0) {
|
|
160
|
+
return Promise.resolve();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Invert: Calculate the delta
|
|
164
|
+
const deltaX = firstBounds.left - lastBounds.left;
|
|
165
|
+
const deltaY = firstBounds.top - lastBounds.top;
|
|
166
|
+
const deltaW = firstBounds.width / lastBounds.width;
|
|
167
|
+
const deltaH = firstBounds.height / lastBounds.height;
|
|
168
|
+
|
|
169
|
+
// Skip animation if no change
|
|
170
|
+
if (deltaX === 0 && deltaY === 0 && deltaW === 1 && deltaH === 1) {
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const htmlElement = element as HTMLElement;
|
|
175
|
+
|
|
176
|
+
// Apply inverted transform
|
|
177
|
+
htmlElement.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`;
|
|
178
|
+
htmlElement.style.transformOrigin = 'top left';
|
|
179
|
+
|
|
180
|
+
// Force reflow
|
|
181
|
+
void htmlElement.offsetHeight;
|
|
182
|
+
|
|
183
|
+
// Play: Animate back to current position
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const animation = htmlElement.animate(
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
transform: `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`,
|
|
189
|
+
},
|
|
190
|
+
{ transform: 'translate(0, 0) scale(1, 1)' },
|
|
191
|
+
],
|
|
192
|
+
{ duration, easing, fill: 'forwards' }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
animation.onfinish = () => {
|
|
196
|
+
htmlElement.style.transform = '';
|
|
197
|
+
htmlElement.style.transformOrigin = '';
|
|
198
|
+
onComplete?.();
|
|
199
|
+
resolve();
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* FLIP helper for animating a list of elements.
|
|
206
|
+
* Useful for reordering lists with smooth animations.
|
|
207
|
+
*
|
|
208
|
+
* @param elements - Array of elements to animate
|
|
209
|
+
* @param performUpdate - Function that performs the DOM update
|
|
210
|
+
* @param options - Animation configuration
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* await flipList(listItems, () => {
|
|
215
|
+
* container.appendChild(container.firstChild); // Move first to last
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export const flipList = async (
|
|
220
|
+
elements: Element[],
|
|
221
|
+
performUpdate: () => void,
|
|
222
|
+
options: FlipOptions = {}
|
|
223
|
+
): Promise<void> => {
|
|
224
|
+
// First: Capture all positions
|
|
225
|
+
const positions = new Map<Element, ElementBounds>();
|
|
226
|
+
for (const el of elements) {
|
|
227
|
+
positions.set(el, capturePosition(el));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Perform DOM update
|
|
231
|
+
performUpdate();
|
|
232
|
+
|
|
233
|
+
// Animate each element
|
|
234
|
+
const animations = elements.map((el) => {
|
|
235
|
+
const first = positions.get(el);
|
|
236
|
+
if (!first) return Promise.resolve();
|
|
237
|
+
return flip(el, first, options);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await Promise.all(animations);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Spring Physics
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Default spring configuration values.
|
|
249
|
+
*/
|
|
250
|
+
const DEFAULT_SPRING_CONFIG: Required<SpringConfig> = {
|
|
251
|
+
stiffness: 100,
|
|
252
|
+
damping: 10,
|
|
253
|
+
mass: 1,
|
|
254
|
+
precision: 0.01,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a spring-based animation for smooth, physics-based motion.
|
|
259
|
+
*
|
|
260
|
+
* @param initialValue - Starting value for the spring
|
|
261
|
+
* @param config - Spring physics configuration
|
|
262
|
+
* @returns Spring instance for controlling the animation
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* const x = spring(0, { stiffness: 120, damping: 14 });
|
|
267
|
+
* x.onChange((value) => {
|
|
268
|
+
* element.style.transform = `translateX(${value}px)`;
|
|
269
|
+
* });
|
|
270
|
+
* await x.to(100);
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export const spring = (initialValue: number, config: SpringConfig = {}): Spring => {
|
|
274
|
+
const { stiffness, damping, mass, precision } = {
|
|
275
|
+
...DEFAULT_SPRING_CONFIG,
|
|
276
|
+
...config,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
let current = initialValue;
|
|
280
|
+
let velocity = 0;
|
|
281
|
+
let target = initialValue;
|
|
282
|
+
let animationFrame: number | null = null;
|
|
283
|
+
let resolvePromise: (() => void) | null = null;
|
|
284
|
+
const listeners = new Set<(value: number) => void>();
|
|
285
|
+
|
|
286
|
+
const notifyListeners = () => {
|
|
287
|
+
for (const listener of listeners) {
|
|
288
|
+
listener(current);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const step = () => {
|
|
293
|
+
// Spring physics calculation
|
|
294
|
+
const displacement = current - target;
|
|
295
|
+
const springForce = -stiffness * displacement;
|
|
296
|
+
const dampingForce = -damping * velocity;
|
|
297
|
+
const acceleration = (springForce + dampingForce) / mass;
|
|
298
|
+
|
|
299
|
+
velocity += acceleration * (1 / 60); // Assuming 60fps
|
|
300
|
+
current += velocity * (1 / 60);
|
|
301
|
+
|
|
302
|
+
notifyListeners();
|
|
303
|
+
|
|
304
|
+
// Check if spring has settled
|
|
305
|
+
if (Math.abs(velocity) < precision && Math.abs(displacement) < precision) {
|
|
306
|
+
current = target;
|
|
307
|
+
velocity = 0;
|
|
308
|
+
animationFrame = null;
|
|
309
|
+
notifyListeners();
|
|
310
|
+
resolvePromise?.();
|
|
311
|
+
resolvePromise = null;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
animationFrame = requestAnimationFrame(step);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
to(newTarget: number): Promise<void> {
|
|
320
|
+
target = newTarget;
|
|
321
|
+
|
|
322
|
+
if (animationFrame !== null) {
|
|
323
|
+
cancelAnimationFrame(animationFrame);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
resolvePromise = resolve;
|
|
328
|
+
animationFrame = requestAnimationFrame(step);
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
current(): number {
|
|
333
|
+
return current;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
stop(): void {
|
|
337
|
+
if (animationFrame !== null) {
|
|
338
|
+
cancelAnimationFrame(animationFrame);
|
|
339
|
+
animationFrame = null;
|
|
340
|
+
}
|
|
341
|
+
velocity = 0;
|
|
342
|
+
resolvePromise?.();
|
|
343
|
+
resolvePromise = null;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
onChange(callback: (value: number) => void): () => void {
|
|
347
|
+
listeners.add(callback);
|
|
348
|
+
return () => listeners.delete(callback);
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Preset spring configurations for common use cases.
|
|
355
|
+
*/
|
|
356
|
+
export const springPresets = {
|
|
357
|
+
/** Gentle, slow-settling spring */
|
|
358
|
+
gentle: { stiffness: 80, damping: 15 } as SpringConfig,
|
|
359
|
+
/** Responsive, snappy spring */
|
|
360
|
+
snappy: { stiffness: 200, damping: 20 } as SpringConfig,
|
|
361
|
+
/** Bouncy, playful spring */
|
|
362
|
+
bouncy: { stiffness: 300, damping: 8 } as SpringConfig,
|
|
363
|
+
/** Stiff, quick spring with minimal overshoot */
|
|
364
|
+
stiff: { stiffness: 400, damping: 30 } as SpringConfig,
|
|
365
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Buckets API wrapper.
|
|
3
|
+
* Provides a simplified interface for storing blobs and binary data.
|
|
4
|
+
* Falls back to IndexedDB when Storage Buckets API is not available.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bucket interface for blob storage operations.
|
|
9
|
+
*/
|
|
10
|
+
export interface Bucket {
|
|
11
|
+
/**
|
|
12
|
+
* Store a blob in the bucket.
|
|
13
|
+
* @param key - Unique identifier for the blob
|
|
14
|
+
* @param data - Blob data to store
|
|
15
|
+
*/
|
|
16
|
+
put(key: string, data: Blob): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieve a blob from the bucket.
|
|
20
|
+
* @param key - Blob identifier
|
|
21
|
+
* @returns The stored blob or null if not found
|
|
22
|
+
*/
|
|
23
|
+
get(key: string): Promise<Blob | null>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Remove a blob from the bucket.
|
|
27
|
+
* @param key - Blob identifier
|
|
28
|
+
*/
|
|
29
|
+
remove(key: string): Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List all keys in the bucket.
|
|
33
|
+
* @returns Array of blob keys
|
|
34
|
+
*/
|
|
35
|
+
keys(): Promise<string[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* IndexedDB-based bucket implementation.
|
|
40
|
+
* Used as fallback when Storage Buckets API is unavailable.
|
|
41
|
+
*/
|
|
42
|
+
class IndexedDBBucket implements Bucket {
|
|
43
|
+
private dbPromise: Promise<IDBDatabase> | null = null;
|
|
44
|
+
private readonly storeName = 'blobs';
|
|
45
|
+
|
|
46
|
+
constructor(private readonly bucketName: string) {}
|
|
47
|
+
|
|
48
|
+
private openDB(): Promise<IDBDatabase> {
|
|
49
|
+
if (this.dbPromise) return this.dbPromise;
|
|
50
|
+
|
|
51
|
+
const dbName = `bquery-bucket-${this.bucketName}`;
|
|
52
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
53
|
+
const request = indexedDB.open(dbName, 1);
|
|
54
|
+
|
|
55
|
+
request.onupgradeneeded = () => {
|
|
56
|
+
const db = request.result;
|
|
57
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
58
|
+
db.createObjectStore(this.storeName);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
request.onsuccess = () => resolve(request.result);
|
|
63
|
+
request.onerror = () => reject(request.error);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return this.dbPromise;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async withStore<T>(
|
|
70
|
+
mode: IDBTransactionMode,
|
|
71
|
+
operation: (store: IDBObjectStore) => IDBRequest<T>
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
const db = await this.openDB();
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const tx = db.transaction(this.storeName, mode);
|
|
76
|
+
const store = tx.objectStore(this.storeName);
|
|
77
|
+
const request = operation(store);
|
|
78
|
+
request.onsuccess = () => resolve(request.result);
|
|
79
|
+
request.onerror = () => reject(request.error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async put(key: string, data: Blob): Promise<void> {
|
|
84
|
+
await this.withStore('readwrite', (store) => store.put(data, key));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async get(key: string): Promise<Blob | null> {
|
|
88
|
+
const result = await this.withStore<Blob | undefined>('readonly', (store) => store.get(key));
|
|
89
|
+
return result ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async remove(key: string): Promise<void> {
|
|
93
|
+
await this.withStore('readwrite', (store) => store.delete(key));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async keys(): Promise<string[]> {
|
|
97
|
+
const result = await this.withStore<IDBValidKey[]>('readonly', (store) => store.getAllKeys());
|
|
98
|
+
return result.map((key) => String(key));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bucket manager for creating and accessing storage buckets.
|
|
104
|
+
*/
|
|
105
|
+
export const buckets = {
|
|
106
|
+
/**
|
|
107
|
+
* Open or create a storage bucket.
|
|
108
|
+
* @param name - Bucket name
|
|
109
|
+
* @returns Bucket instance for blob operations
|
|
110
|
+
*/
|
|
111
|
+
async open(name: string): Promise<Bucket> {
|
|
112
|
+
// Storage Buckets API is experimental; use IndexedDB fallback
|
|
113
|
+
return new IndexedDBBucket(name);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Storage API wrapper.
|
|
3
|
+
* Provides a simplified interface for caching responses and assets.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cache handle interface for managing cached resources.
|
|
8
|
+
*/
|
|
9
|
+
export interface CacheHandle {
|
|
10
|
+
/**
|
|
11
|
+
* Add a resource to the cache by URL.
|
|
12
|
+
* Fetches the resource and stores the response.
|
|
13
|
+
* @param url - URL to fetch and cache
|
|
14
|
+
*/
|
|
15
|
+
add(url: string): Promise<void>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Add multiple resources to the cache.
|
|
19
|
+
* @param urls - Array of URLs to fetch and cache
|
|
20
|
+
*/
|
|
21
|
+
addAll(urls: string[]): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Store a custom response in the cache.
|
|
25
|
+
* @param url - URL key for the cached response
|
|
26
|
+
* @param response - Response object to cache
|
|
27
|
+
*/
|
|
28
|
+
put(url: string, response: Response): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Retrieve a cached response.
|
|
32
|
+
* @param url - URL to look up
|
|
33
|
+
* @returns Cached Response or undefined if not found
|
|
34
|
+
*/
|
|
35
|
+
match(url: string): Promise<Response | undefined>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Remove a cached response.
|
|
39
|
+
* @param url - URL to remove from cache
|
|
40
|
+
* @returns True if the entry was deleted
|
|
41
|
+
*/
|
|
42
|
+
remove(url: string): Promise<boolean>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get all cached request URLs.
|
|
46
|
+
* @returns Array of cached URLs
|
|
47
|
+
*/
|
|
48
|
+
keys(): Promise<string[]>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal cache handle implementation.
|
|
53
|
+
*/
|
|
54
|
+
class CacheHandleImpl implements CacheHandle {
|
|
55
|
+
constructor(private readonly cache: Cache) {}
|
|
56
|
+
|
|
57
|
+
async add(url: string): Promise<void> {
|
|
58
|
+
await this.cache.add(url);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async addAll(urls: string[]): Promise<void> {
|
|
62
|
+
await this.cache.addAll(urls);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async put(url: string, response: Response): Promise<void> {
|
|
66
|
+
await this.cache.put(url, response);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async match(url: string): Promise<Response | undefined> {
|
|
70
|
+
return this.cache.match(url);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async remove(url: string): Promise<boolean> {
|
|
74
|
+
return this.cache.delete(url);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async keys(): Promise<string[]> {
|
|
78
|
+
const requests = await this.cache.keys();
|
|
79
|
+
return requests.map((req) => req.url);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cache manager for accessing the Cache Storage API.
|
|
85
|
+
*/
|
|
86
|
+
export const cache = {
|
|
87
|
+
/**
|
|
88
|
+
* Check if Cache Storage API is supported.
|
|
89
|
+
* @returns True if caches API is available
|
|
90
|
+
*/
|
|
91
|
+
isSupported(): boolean {
|
|
92
|
+
return 'caches' in window;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Open or create a named cache.
|
|
97
|
+
* @param name - Cache name
|
|
98
|
+
* @returns CacheHandle for cache operations
|
|
99
|
+
*/
|
|
100
|
+
async open(name: string): Promise<CacheHandle> {
|
|
101
|
+
if (!this.isSupported()) {
|
|
102
|
+
throw new Error('bQuery: Cache Storage API not supported');
|
|
103
|
+
}
|
|
104
|
+
const c = await caches.open(name);
|
|
105
|
+
return new CacheHandleImpl(c);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Delete a named cache.
|
|
110
|
+
* @param name - Cache name to delete
|
|
111
|
+
* @returns True if the cache was deleted
|
|
112
|
+
*/
|
|
113
|
+
async delete(name: string): Promise<boolean> {
|
|
114
|
+
if (!this.isSupported()) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return caches.delete(name);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* List all cache names.
|
|
122
|
+
* @returns Array of cache names
|
|
123
|
+
*/
|
|
124
|
+
async keys(): Promise<string[]> {
|
|
125
|
+
if (!this.isSupported()) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return caches.keys();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
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';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Notifications API wrapper.
|
|
3
|
+
* Provides a simplified interface for browser notifications.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Notification options matching the standard NotificationOptions interface.
|
|
8
|
+
*/
|
|
9
|
+
export interface NotificationOptions {
|
|
10
|
+
/** Body text of the notification */
|
|
11
|
+
body?: string;
|
|
12
|
+
/** Icon URL for the notification */
|
|
13
|
+
icon?: string;
|
|
14
|
+
/** Badge icon for mobile devices */
|
|
15
|
+
badge?: string;
|
|
16
|
+
/** Tag for grouping notifications */
|
|
17
|
+
tag?: string;
|
|
18
|
+
/** Whether to require user interaction */
|
|
19
|
+
requireInteraction?: boolean;
|
|
20
|
+
/** Vibration pattern for mobile devices */
|
|
21
|
+
vibrate?: number[];
|
|
22
|
+
/** Additional data attached to the notification */
|
|
23
|
+
data?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Notifications manager providing a clean interface for web notifications.
|
|
28
|
+
*/
|
|
29
|
+
export const notifications = {
|
|
30
|
+
/**
|
|
31
|
+
* Check if notifications are supported.
|
|
32
|
+
* @returns True if Notification API is available
|
|
33
|
+
*/
|
|
34
|
+
isSupported(): boolean {
|
|
35
|
+
return 'Notification' in window;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get current permission status.
|
|
40
|
+
* @returns Current permission state
|
|
41
|
+
*/
|
|
42
|
+
getPermission(): NotificationPermission {
|
|
43
|
+
if (!this.isSupported()) return 'denied';
|
|
44
|
+
return Notification.permission;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Request notification permission from the user.
|
|
49
|
+
* @returns Promise resolving to the permission result
|
|
50
|
+
*/
|
|
51
|
+
async requestPermission(): Promise<NotificationPermission> {
|
|
52
|
+
if (!this.isSupported()) {
|
|
53
|
+
return 'denied';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Notification.permission === 'granted') {
|
|
57
|
+
return 'granted';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Notification.permission === 'denied') {
|
|
61
|
+
return 'denied';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Notification.requestPermission();
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send a notification.
|
|
69
|
+
* Requires 'granted' permission.
|
|
70
|
+
* @param title - Notification title
|
|
71
|
+
* @param options - Optional notification settings
|
|
72
|
+
* @returns The Notification instance or null if not permitted
|
|
73
|
+
*/
|
|
74
|
+
send(title: string, options?: NotificationOptions): Notification | null {
|
|
75
|
+
if (!this.isSupported()) {
|
|
76
|
+
console.warn('bQuery: Notifications not supported in this browser');
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Notification.permission !== 'granted') {
|
|
81
|
+
console.warn('bQuery: Notification permission not granted');
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Notification(title, options);
|
|
86
|
+
},
|
|
87
|
+
};
|