@stacksjs/components 0.2.93 → 0.2.94

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 (187) hide show
  1. package/dist/index.js +4 -4
  2. package/package.json +5 -6
  3. package/src/components/CodeBlock.stx +0 -54
  4. package/src/components/Footer.stx +0 -66
  5. package/src/components/Hero.stx +0 -25
  6. package/src/components/Installation.stx +0 -37
  7. package/src/components/index.ts +0 -4
  8. package/src/composables/index.ts +0 -4
  9. package/src/composables/useCopyCode.ts +0 -73
  10. package/src/composables/useDarkMode.ts +0 -100
  11. package/src/composables/useSEO.ts +0 -230
  12. package/src/composables/utilities/index.ts +0 -7
  13. package/src/composables/utilities/useClickOutside.ts +0 -72
  14. package/src/composables/utilities/useFocusTrap.ts +0 -159
  15. package/src/composables/utilities/useKeyboard.ts +0 -124
  16. package/src/composables/utilities/useLocalStorage.ts +0 -132
  17. package/src/composables/utilities/useMediaQuery.ts +0 -118
  18. package/src/index.ts +0 -99
  19. package/src/stx.d.ts +0 -4
  20. package/src/ui/accordion/Accordion.stx +0 -146
  21. package/src/ui/accordion/AccordionItem.stx +0 -37
  22. package/src/ui/accordion/index.ts +0 -47
  23. package/src/ui/audio/Audio.stx +0 -50
  24. package/src/ui/audio/README.md +0 -41
  25. package/src/ui/audio/index.ts +0 -13
  26. package/src/ui/auth/Login.stx +0 -161
  27. package/src/ui/auth/README.md +0 -142
  28. package/src/ui/auth/Signup.stx +0 -159
  29. package/src/ui/auth/TwoFactorChallenge.stx +0 -111
  30. package/src/ui/auth/index.ts +0 -32
  31. package/src/ui/avatar/Avatar.stx +0 -77
  32. package/src/ui/avatar/index.ts +0 -12
  33. package/src/ui/badge/Badge.stx +0 -62
  34. package/src/ui/badge/index.ts +0 -10
  35. package/src/ui/breadcrumb/Breadcrumb.stx +0 -79
  36. package/src/ui/breadcrumb/index.ts +0 -15
  37. package/src/ui/button/Button.stx +0 -74
  38. package/src/ui/button/README.md +0 -80
  39. package/src/ui/button/index.ts +0 -85
  40. package/src/ui/calendar/Calendar.stx +0 -189
  41. package/src/ui/calendar/README.md +0 -254
  42. package/src/ui/calendar/index.ts +0 -14
  43. package/src/ui/card/Card.stx +0 -68
  44. package/src/ui/card/index.ts +0 -13
  45. package/src/ui/checkbox/Checkbox.stx +0 -86
  46. package/src/ui/checkbox/index.ts +0 -16
  47. package/src/ui/combobox/Combobox.stx +0 -96
  48. package/src/ui/combobox/ComboboxButton.stx +0 -25
  49. package/src/ui/combobox/ComboboxInput.stx +0 -32
  50. package/src/ui/combobox/ComboboxOption.stx +0 -49
  51. package/src/ui/combobox/ComboboxOptions.stx +0 -16
  52. package/src/ui/combobox/README.md +0 -150
  53. package/src/ui/combobox/index.ts +0 -40
  54. package/src/ui/command-palette/CommandPalette.stx +0 -81
  55. package/src/ui/command-palette/CommandPaletteItem.stx +0 -27
  56. package/src/ui/command-palette/README.md +0 -42
  57. package/src/ui/command-palette/index.ts +0 -15
  58. package/src/ui/dialog/Dialog.stx +0 -41
  59. package/src/ui/dialog/DialogBackdrop.stx +0 -9
  60. package/src/ui/dialog/DialogDescription.stx +0 -11
  61. package/src/ui/dialog/DialogPanel.stx +0 -16
  62. package/src/ui/dialog/DialogTitle.stx +0 -11
  63. package/src/ui/dialog/README.md +0 -121
  64. package/src/ui/dialog/index.ts +0 -32
  65. package/src/ui/drawer/Drawer.stx +0 -120
  66. package/src/ui/drawer/README.md +0 -90
  67. package/src/ui/drawer/index.ts +0 -9
  68. package/src/ui/dropdown/Dropdown.stx +0 -82
  69. package/src/ui/dropdown/DropdownButton.stx +0 -29
  70. package/src/ui/dropdown/DropdownItem.stx +0 -35
  71. package/src/ui/dropdown/DropdownItems.stx +0 -18
  72. package/src/ui/dropdown/README.md +0 -114
  73. package/src/ui/dropdown/index.ts +0 -29
  74. package/src/ui/form/Form.stx +0 -95
  75. package/src/ui/form/index.ts +0 -67
  76. package/src/ui/heatmap/Heatmap.stx +0 -294
  77. package/src/ui/image/Image.stx +0 -165
  78. package/src/ui/image/README.md +0 -164
  79. package/src/ui/image/index.ts +0 -22
  80. package/src/ui/input/EmailInput.stx +0 -30
  81. package/src/ui/input/NumberInput.stx +0 -126
  82. package/src/ui/input/PasswordInput.stx +0 -143
  83. package/src/ui/input/SearchInput.stx +0 -44
  84. package/src/ui/input/TextInput.stx +0 -132
  85. package/src/ui/input/index.ts +0 -180
  86. package/src/ui/listbox/Listbox.stx +0 -101
  87. package/src/ui/listbox/ListboxButton.stx +0 -28
  88. package/src/ui/listbox/ListboxLabel.stx +0 -11
  89. package/src/ui/listbox/ListboxOption.stx +0 -49
  90. package/src/ui/listbox/ListboxOptions.stx +0 -16
  91. package/src/ui/listbox/README.md +0 -135
  92. package/src/ui/listbox/index.ts +0 -39
  93. package/src/ui/navigator/Navigator.stx +0 -113
  94. package/src/ui/navigator/README.md +0 -352
  95. package/src/ui/navigator/index.ts +0 -20
  96. package/src/ui/notification/Notification.stx +0 -117
  97. package/src/ui/notification/README.md +0 -114
  98. package/src/ui/notification/index.ts +0 -12
  99. package/src/ui/pagination/Pagination.stx +0 -150
  100. package/src/ui/pagination/index.ts +0 -11
  101. package/src/ui/payment/Checkout.stx +0 -31
  102. package/src/ui/payment/DefaultPaymentMethod.stx +0 -62
  103. package/src/ui/payment/PaymentMethods.stx +0 -104
  104. package/src/ui/payment/README.md +0 -345
  105. package/src/ui/payment/SubscriptionCheckout.stx +0 -323
  106. package/src/ui/payment/index.ts +0 -59
  107. package/src/ui/popover/Popover.stx +0 -60
  108. package/src/ui/popover/PopoverButton.stx +0 -29
  109. package/src/ui/popover/PopoverPanel.stx +0 -23
  110. package/src/ui/popover/README.md +0 -91
  111. package/src/ui/popover/index.ts +0 -22
  112. package/src/ui/portal/Portal.stx +0 -57
  113. package/src/ui/portal/index.ts +0 -6
  114. package/src/ui/progress/Progress.stx +0 -131
  115. package/src/ui/progress/index.ts +0 -12
  116. package/src/ui/radio/Radio.stx +0 -77
  117. package/src/ui/radio/index.ts +0 -15
  118. package/src/ui/radio-group/README.md +0 -139
  119. package/src/ui/radio-group/RadioGroup.stx +0 -60
  120. package/src/ui/radio-group/RadioGroupDescription.stx +0 -11
  121. package/src/ui/radio-group/RadioGroupLabel.stx +0 -11
  122. package/src/ui/radio-group/RadioGroupOption.stx +0 -58
  123. package/src/ui/radio-group/index.ts +0 -30
  124. package/src/ui/select/README.md +0 -180
  125. package/src/ui/select/Select.stx +0 -82
  126. package/src/ui/select/index.ts +0 -21
  127. package/src/ui/sidebar/Sidebar.stx +0 -476
  128. package/src/ui/sidebar/SidebarFooter.stx +0 -71
  129. package/src/ui/sidebar/SidebarHeader.stx +0 -103
  130. package/src/ui/sidebar/SidebarItem.stx +0 -86
  131. package/src/ui/sidebar/SidebarSection.stx +0 -83
  132. package/src/ui/sidebar/index.ts +0 -153
  133. package/src/ui/sidebar/themes.ts +0 -240
  134. package/src/ui/skeleton/Skeleton.stx +0 -48
  135. package/src/ui/skeleton/index.ts +0 -11
  136. package/src/ui/spinner/Spinner.stx +0 -109
  137. package/src/ui/spinner/index.ts +0 -9
  138. package/src/ui/stepper/README.md +0 -36
  139. package/src/ui/stepper/Stepper.stx +0 -17
  140. package/src/ui/stepper/StepperStep.stx +0 -57
  141. package/src/ui/stepper/index.ts +0 -19
  142. package/src/ui/storage/README.md +0 -193
  143. package/src/ui/storage/index.ts +0 -347
  144. package/src/ui/switch/README.md +0 -114
  145. package/src/ui/switch/Switch.stx +0 -72
  146. package/src/ui/switch/index.ts +0 -10
  147. package/src/ui/table/README.md +0 -39
  148. package/src/ui/table/Table.stx +0 -14
  149. package/src/ui/table/TableBody.stx +0 -10
  150. package/src/ui/table/TableCell.stx +0 -10
  151. package/src/ui/table/TableHead.stx +0 -10
  152. package/src/ui/table/TableHeader.stx +0 -20
  153. package/src/ui/table/TableRow.stx +0 -11
  154. package/src/ui/table/index.ts +0 -35
  155. package/src/ui/tabs/TabPanel.stx +0 -25
  156. package/src/ui/tabs/Tabs.stx +0 -202
  157. package/src/ui/tabs/index.ts +0 -50
  158. package/src/ui/teleport/Teleport.stx +0 -65
  159. package/src/ui/teleport/index.ts +0 -7
  160. package/src/ui/textarea/Textarea.stx +0 -108
  161. package/src/ui/textarea/index.ts +0 -20
  162. package/src/ui/tooltip/Tooltip.stx +0 -71
  163. package/src/ui/tooltip/index.ts +0 -10
  164. package/src/ui/transition/README.md +0 -30
  165. package/src/ui/transition/Transition.stx +0 -102
  166. package/src/ui/transition/index.ts +0 -25
  167. package/src/ui/video/README.md +0 -47
  168. package/src/ui/video/Video.stx +0 -45
  169. package/src/ui/video/index.ts +0 -16
  170. package/src/ui/virtual-list/VirtualList.stx +0 -67
  171. package/src/ui/virtual-list/index.ts +0 -10
  172. package/src/ui/virtual-table/VirtualTable.stx +0 -103
  173. package/src/ui/virtual-table/index.ts +0 -21
  174. package/src/utils/PROP_VALIDATION.md +0 -458
  175. package/src/utils/THEME_SYSTEM.md +0 -702
  176. package/src/utils/accessibility.ts +0 -841
  177. package/src/utils/animation/easing.ts +0 -368
  178. package/src/utils/animation/helpers.ts +0 -223
  179. package/src/utils/animation/index.ts +0 -5
  180. package/src/utils/animation/keyframes.ts +0 -230
  181. package/src/utils/error-handling.ts +0 -475
  182. package/src/utils/highlighter.ts +0 -93
  183. package/src/utils/i18n.ts +0 -634
  184. package/src/utils/keep-alive.ts +0 -819
  185. package/src/utils/lazy-loading.ts +0 -530
  186. package/src/utils/prop-validation.ts +0 -478
  187. package/src/utils/theme.ts +0 -626
@@ -1,530 +0,0 @@
1
- /**
2
- * Lazy loading utilities for @stacksjs/components
3
- *
4
- * Provides component-level code splitting and lazy loading with
5
- * intersection observer support for on-demand loading
6
- *
7
- * @example
8
- * ```ts
9
- * import { lazyLoad, lazyLoadComponent } from '@stacksjs/components'
10
- *
11
- * // Lazy load a component
12
- * const LazyButton = lazyLoadComponent(() => import('./Button'))
13
- *
14
- * // Lazy load on intersection
15
- * lazyLoad(element, () => import('./heavy-component'))
16
- * ```
17
- */
18
-
19
- /**
20
- * Lazy loading options
21
- */
22
- export interface LazyLoadOptions {
23
- /** IntersectionObserver root element */
24
- root?: Element | null
25
- /** Root margin for intersection observer */
26
- rootMargin?: string
27
- /** Threshold for intersection observer */
28
- threshold?: number | number[]
29
- /** Placeholder content while loading */
30
- placeholder?: string | HTMLElement
31
- /** Error content if loading fails */
32
- errorContent?: string | HTMLElement
33
- /** Callback when component starts loading */
34
- onLoading?: () => void
35
- /** Callback when component loads successfully */
36
- onLoaded?: (module: any) => void
37
- /** Callback when loading fails */
38
- onError?: (error: Error) => void
39
- }
40
-
41
- /**
42
- * Component lazy load options
43
- */
44
- export interface ComponentLazyLoadOptions extends LazyLoadOptions {
45
- /** Whether to preload the component on hover */
46
- preloadOnHover?: boolean
47
- /** Retry attempts on failure */
48
- retryAttempts?: number
49
- /** Retry delay in milliseconds */
50
- retryDelay?: number
51
- }
52
-
53
- /**
54
- * Lazy load state
55
- */
56
- export type LazyLoadState = 'idle' | 'loading' | 'loaded' | 'error'
57
-
58
- /**
59
- * Lazy loaded component wrapper
60
- */
61
- export interface LazyComponent<T = any> {
62
- /** Current load state */
63
- state: LazyLoadState
64
- /** Loaded module (if loaded) */
65
- module?: T
66
- /** Loading error (if failed) */
67
- error?: Error
68
- /** Load the component */
69
- load: () => Promise<T>
70
- /** Preload the component without rendering */
71
- preload: () => Promise<T>
72
- }
73
-
74
- /**
75
- * Cache for lazy loaded modules
76
- */
77
- const moduleCache = new Map<string, Promise<any>>()
78
-
79
- /**
80
- * Lazy load a module with caching
81
- *
82
- * @param importFn - Dynamic import function
83
- * @param cacheKey - Optional cache key (defaults to function string)
84
- * @returns Promise of module
85
- */
86
- export async function lazyLoadModule<T = any>(
87
- importFn: () => Promise<T>,
88
- cacheKey?: string,
89
- ): Promise<T> {
90
- const key = cacheKey || importFn.toString()
91
-
92
- if (moduleCache.has(key)) {
93
- return moduleCache.get(key)!
94
- }
95
-
96
- const promise = importFn()
97
- moduleCache.set(key, promise)
98
-
99
- try {
100
- return await promise
101
- }
102
- catch (error) {
103
- // Remove from cache on error to allow retry
104
- moduleCache.delete(key)
105
- throw error
106
- }
107
- }
108
-
109
- /**
110
- * Create a lazy loaded component
111
- *
112
- * @param importFn - Dynamic import function
113
- * @param options - Lazy load options
114
- * @returns Lazy component wrapper
115
- *
116
- * @example
117
- * ```ts
118
- * const LazyButton = lazyLoadComponent(
119
- * () => import('./components/Button'),
120
- * { preloadOnHover: true }
121
- * )
122
- *
123
- * // Later, load and use
124
- * const button = await LazyButton.load()
125
- * ```
126
- */
127
- export function lazyLoadComponent<T = any>(
128
- importFn: () => Promise<T>,
129
- options: ComponentLazyLoadOptions = {},
130
- ): LazyComponent<T> {
131
- const {
132
- retryAttempts = 3,
133
- retryDelay = 1000,
134
- onLoading,
135
- onLoaded,
136
- onError,
137
- } = options
138
-
139
- let state: LazyLoadState = 'idle'
140
- let module: T | undefined
141
- let error: Error | undefined
142
-
143
- const loadWithRetry = async (attempt = 0): Promise<T> => {
144
- try {
145
- if (state === 'loading' || state === 'loaded') {
146
- if (module) {
147
- return module
148
- }
149
- }
150
-
151
- state = 'loading'
152
- onLoading?.()
153
-
154
- const loaded = await lazyLoadModule(importFn)
155
- module = loaded
156
- state = 'loaded'
157
- onLoaded?.(loaded)
158
-
159
- return loaded
160
- }
161
- catch (err) {
162
- const loadError = err as Error
163
-
164
- if (attempt < retryAttempts) {
165
- // Retry after delay
166
- await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)))
167
- return loadWithRetry(attempt + 1)
168
- }
169
-
170
- state = 'error'
171
- error = loadError
172
- onError?.(loadError)
173
- throw loadError
174
- }
175
- }
176
-
177
- return {
178
- get state() {
179
- return state
180
- },
181
- get module() {
182
- return module
183
- },
184
- get error() {
185
- return error
186
- },
187
- load: () => loadWithRetry(),
188
- preload: () => loadWithRetry(),
189
- }
190
- }
191
-
192
- /**
193
- * Lazy load content when element enters viewport
194
- *
195
- * @param element - Target element to observe
196
- * @param importFn - Dynamic import function
197
- * @param options - Lazy load options
198
- * @returns Cleanup function
199
- *
200
- * @example
201
- * ```ts
202
- * const cleanup = lazyLoad(
203
- * document.getElementById('heavy-component'),
204
- * () => import('./HeavyComponent'),
205
- * {
206
- * rootMargin: '50px',
207
- * placeholder: '<div>Loading...</div>'
208
- * }
209
- * )
210
- * ```
211
- */
212
- export function lazyLoad<T = any>(
213
- element: Element | null,
214
- importFn: () => Promise<T>,
215
- options: LazyLoadOptions = {},
216
- ): () => void {
217
- if (!element) {
218
- console.warn('lazyLoad: element is null')
219
- return () => {}
220
- }
221
-
222
- const {
223
- root = null,
224
- rootMargin = '0px',
225
- threshold = 0.01,
226
- placeholder,
227
- errorContent,
228
- onLoading,
229
- onLoaded,
230
- onError,
231
- } = options
232
-
233
- let hasLoaded = false
234
- let observer: IntersectionObserver | null = null
235
-
236
- // Set placeholder
237
- if (placeholder) {
238
- if (typeof placeholder === 'string') {
239
- element.innerHTML = placeholder
240
- }
241
- else {
242
- element.innerHTML = ''
243
- element.appendChild(placeholder)
244
- }
245
- }
246
-
247
- const handleIntersection: IntersectionObserverCallback = async (entries) => {
248
- for (const entry of entries) {
249
- if (entry.isIntersecting && !hasLoaded) {
250
- hasLoaded = true
251
- observer?.disconnect()
252
-
253
- try {
254
- onLoading?.()
255
-
256
- const module = await lazyLoadModule(importFn)
257
-
258
- onLoaded?.(module)
259
-
260
- // If module has a default export with render method
261
- if (module && typeof (module as any).default?.render === 'function') {
262
- const content = await (module as any).default.render()
263
- element.innerHTML = content
264
- }
265
- }
266
- catch (error) {
267
- const loadError = error as Error
268
- onError?.(loadError)
269
-
270
- // Set error content
271
- if (errorContent) {
272
- if (typeof errorContent === 'string') {
273
- element.innerHTML = errorContent
274
- }
275
- else {
276
- element.innerHTML = ''
277
- element.appendChild(errorContent)
278
- }
279
- }
280
- else {
281
- element.innerHTML = `<div style="color: red;">Failed to load component: ${loadError.message}</div>`
282
- }
283
- }
284
- }
285
- }
286
- }
287
-
288
- // Create intersection observer
289
- observer = new IntersectionObserver(handleIntersection, {
290
- root,
291
- rootMargin,
292
- threshold,
293
- })
294
-
295
- observer.observe(element)
296
-
297
- // Return cleanup function
298
- return () => {
299
- observer?.disconnect()
300
- observer = null
301
- }
302
- }
303
-
304
- /**
305
- * Lazy load multiple elements
306
- *
307
- * @param elements - Array of elements or NodeList
308
- * @param importFn - Dynamic import function or function that takes element
309
- * @param options - Lazy load options
310
- * @returns Cleanup function
311
- */
312
- export function lazyLoadAll<T = any>(
313
- elements: Element[] | NodeListOf<Element>,
314
- importFn: ((_element: Element) => Promise<T>) | (() => Promise<T>),
315
- options: LazyLoadOptions = {},
316
- ): () => void {
317
- const cleanupFunctions: Array<() => void> = []
318
-
319
- const elementsArray = Array.from(elements)
320
-
321
- for (const element of elementsArray) {
322
- const importFunction = typeof importFn === 'function' && importFn.length > 0
323
- ? () => (importFn as (element: Element) => Promise<T>)(element)
324
- : (importFn as () => Promise<T>)
325
-
326
- const cleanup = lazyLoad(element, importFunction, options)
327
- cleanupFunctions.push(cleanup)
328
- }
329
-
330
- // Return combined cleanup function
331
- return () => {
332
- cleanupFunctions.forEach(cleanup => cleanup())
333
- }
334
- }
335
-
336
- /**
337
- * Preload a module without executing it
338
- *
339
- * Useful for prefetching components that will likely be needed soon
340
- *
341
- * @param importFn - Dynamic import function
342
- * @returns Promise of module
343
- */
344
- export function preloadModule<T = any>(importFn: () => Promise<T>): Promise<T> {
345
- return lazyLoadModule(importFn)
346
- }
347
-
348
- /**
349
- * Preload multiple modules
350
- *
351
- * @param importFns - Array of dynamic import functions
352
- * @returns Promise of all modules
353
- */
354
- export function preloadModules(importFns: Array<() => Promise<any>>): Promise<any[]> {
355
- return Promise.all(importFns.map(fn => preloadModule(fn)))
356
- }
357
-
358
- /**
359
- * Clear module cache
360
- *
361
- * Useful for testing or forcing re-imports
362
- *
363
- * @param cacheKey - Optional specific key to clear, or clear all if undefined
364
- */
365
- export function clearModuleCache(cacheKey?: string): void {
366
- if (cacheKey) {
367
- moduleCache.delete(cacheKey)
368
- }
369
- else {
370
- moduleCache.clear()
371
- }
372
- }
373
-
374
- /**
375
- * Get cache statistics
376
- *
377
- * @returns Cache size and keys
378
- */
379
- export function getCacheStats(): { size: number, keys: string[] } {
380
- return {
381
- size: moduleCache.size,
382
- keys: Array.from(moduleCache.keys()),
383
- }
384
- }
385
-
386
- /**
387
- * Route-based code splitting helper
388
- *
389
- * Lazy loads components based on route patterns
390
- *
391
- * @example
392
- * ```ts
393
- * const routes = {
394
- * '/': () => import('./pages/Home'),
395
- * '/about': () => import('./pages/About'),
396
- * '/blog/:id': () => import('./pages/BlogPost')
397
- * }
398
- *
399
- * const loader = createRouteLoader(routes)
400
- * const component = await loader.load('/about')
401
- * ```
402
- */
403
- export function createRouteLoader(routes: Record<string, () => Promise<any>>) {
404
- return {
405
- /**
406
- * Load component for route
407
- */
408
- async load(path: string): Promise<any> {
409
- const importFn = routes[path]
410
- if (!importFn) {
411
- throw new Error(`No route found for path: ${path}`)
412
- }
413
- return lazyLoadModule(importFn, `route:${path}`)
414
- },
415
-
416
- /**
417
- * Preload component for route
418
- */
419
- async preload(path: string): Promise<any> {
420
- return this.load(path)
421
- },
422
-
423
- /**
424
- * Preload multiple routes
425
- */
426
- async preloadRoutes(paths: string[]): Promise<any[]> {
427
- return Promise.all(paths.map(path => this.preload(path)))
428
- },
429
- }
430
- }
431
-
432
- /**
433
- * Image lazy loading helper
434
- *
435
- * @param img - Image element
436
- * @param src - Image source URL
437
- * @param options - Lazy load options
438
- * @returns Cleanup function
439
- */
440
- export function lazyLoadImage(
441
- img: HTMLImageElement | null,
442
- src: string,
443
- options: LazyLoadOptions = {},
444
- ): () => void {
445
- if (!img) {
446
- return () => {}
447
- }
448
-
449
- const {
450
- root = null,
451
- rootMargin = '50px',
452
- threshold = 0.01,
453
- placeholder,
454
- onLoading,
455
- onLoaded,
456
- onError,
457
- } = options
458
-
459
- // Set placeholder
460
- if (placeholder && typeof placeholder === 'string') {
461
- img.src = placeholder
462
- }
463
-
464
- let hasLoaded = false
465
- let observer: IntersectionObserver | null = null
466
-
467
- const handleIntersection: IntersectionObserverCallback = (entries) => {
468
- for (const entry of entries) {
469
- if (entry.isIntersecting && !hasLoaded) {
470
- hasLoaded = true
471
- observer?.disconnect()
472
-
473
- onLoading?.()
474
-
475
- img.src = src
476
-
477
- img.addEventListener('load', () => {
478
- onLoaded?.(img)
479
- img.classList.add('loaded')
480
- }, { once: true })
481
-
482
- img.addEventListener('error', (_event) => {
483
- const error = new Error(`Failed to load image: ${src}`)
484
- onError?.(error)
485
- img.classList.add('error')
486
- }, { once: true })
487
- }
488
- }
489
- }
490
-
491
- observer = new IntersectionObserver(handleIntersection, {
492
- root,
493
- rootMargin,
494
- threshold,
495
- })
496
-
497
- observer.observe(img)
498
-
499
- return () => {
500
- observer?.disconnect()
501
- observer = null
502
- }
503
- }
504
-
505
- /**
506
- * Lazy load all images with data-lazy-src attribute
507
- *
508
- * @param container - Container element to search within
509
- * @param options - Lazy load options
510
- * @returns Cleanup function
511
- */
512
- export function lazyLoadImages(
513
- container: Element | Document = document,
514
- options: LazyLoadOptions = {},
515
- ): () => void {
516
- const images = container.querySelectorAll<HTMLImageElement>('img[data-lazy-src]')
517
- const cleanupFunctions: Array<() => void> = []
518
-
519
- images.forEach((img) => {
520
- const src = img.getAttribute('data-lazy-src')
521
- if (src) {
522
- const cleanup = lazyLoadImage(img, src, options)
523
- cleanupFunctions.push(cleanup)
524
- }
525
- })
526
-
527
- return () => {
528
- cleanupFunctions.forEach(cleanup => cleanup())
529
- }
530
- }