@stacksjs/components 0.2.92 → 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 (188) hide show
  1. package/dist/{Sidebar-yb115a2w.stx → Sidebar-4hn7vw4n.stx} +18 -0
  2. package/dist/index.js +5 -5
  3. package/package.json +5 -6
  4. package/src/components/CodeBlock.stx +0 -54
  5. package/src/components/Footer.stx +0 -66
  6. package/src/components/Hero.stx +0 -25
  7. package/src/components/Installation.stx +0 -37
  8. package/src/components/index.ts +0 -4
  9. package/src/composables/index.ts +0 -4
  10. package/src/composables/useCopyCode.ts +0 -73
  11. package/src/composables/useDarkMode.ts +0 -100
  12. package/src/composables/useSEO.ts +0 -230
  13. package/src/composables/utilities/index.ts +0 -7
  14. package/src/composables/utilities/useClickOutside.ts +0 -72
  15. package/src/composables/utilities/useFocusTrap.ts +0 -159
  16. package/src/composables/utilities/useKeyboard.ts +0 -124
  17. package/src/composables/utilities/useLocalStorage.ts +0 -132
  18. package/src/composables/utilities/useMediaQuery.ts +0 -118
  19. package/src/index.ts +0 -99
  20. package/src/stx.d.ts +0 -4
  21. package/src/ui/accordion/Accordion.stx +0 -146
  22. package/src/ui/accordion/AccordionItem.stx +0 -37
  23. package/src/ui/accordion/index.ts +0 -47
  24. package/src/ui/audio/Audio.stx +0 -50
  25. package/src/ui/audio/README.md +0 -41
  26. package/src/ui/audio/index.ts +0 -13
  27. package/src/ui/auth/Login.stx +0 -161
  28. package/src/ui/auth/README.md +0 -142
  29. package/src/ui/auth/Signup.stx +0 -159
  30. package/src/ui/auth/TwoFactorChallenge.stx +0 -111
  31. package/src/ui/auth/index.ts +0 -32
  32. package/src/ui/avatar/Avatar.stx +0 -77
  33. package/src/ui/avatar/index.ts +0 -12
  34. package/src/ui/badge/Badge.stx +0 -62
  35. package/src/ui/badge/index.ts +0 -10
  36. package/src/ui/breadcrumb/Breadcrumb.stx +0 -79
  37. package/src/ui/breadcrumb/index.ts +0 -15
  38. package/src/ui/button/Button.stx +0 -74
  39. package/src/ui/button/README.md +0 -80
  40. package/src/ui/button/index.ts +0 -85
  41. package/src/ui/calendar/Calendar.stx +0 -189
  42. package/src/ui/calendar/README.md +0 -254
  43. package/src/ui/calendar/index.ts +0 -14
  44. package/src/ui/card/Card.stx +0 -68
  45. package/src/ui/card/index.ts +0 -13
  46. package/src/ui/checkbox/Checkbox.stx +0 -86
  47. package/src/ui/checkbox/index.ts +0 -16
  48. package/src/ui/combobox/Combobox.stx +0 -96
  49. package/src/ui/combobox/ComboboxButton.stx +0 -25
  50. package/src/ui/combobox/ComboboxInput.stx +0 -32
  51. package/src/ui/combobox/ComboboxOption.stx +0 -49
  52. package/src/ui/combobox/ComboboxOptions.stx +0 -16
  53. package/src/ui/combobox/README.md +0 -150
  54. package/src/ui/combobox/index.ts +0 -40
  55. package/src/ui/command-palette/CommandPalette.stx +0 -81
  56. package/src/ui/command-palette/CommandPaletteItem.stx +0 -27
  57. package/src/ui/command-palette/README.md +0 -42
  58. package/src/ui/command-palette/index.ts +0 -15
  59. package/src/ui/dialog/Dialog.stx +0 -41
  60. package/src/ui/dialog/DialogBackdrop.stx +0 -9
  61. package/src/ui/dialog/DialogDescription.stx +0 -11
  62. package/src/ui/dialog/DialogPanel.stx +0 -16
  63. package/src/ui/dialog/DialogTitle.stx +0 -11
  64. package/src/ui/dialog/README.md +0 -121
  65. package/src/ui/dialog/index.ts +0 -32
  66. package/src/ui/drawer/Drawer.stx +0 -120
  67. package/src/ui/drawer/README.md +0 -90
  68. package/src/ui/drawer/index.ts +0 -9
  69. package/src/ui/dropdown/Dropdown.stx +0 -82
  70. package/src/ui/dropdown/DropdownButton.stx +0 -29
  71. package/src/ui/dropdown/DropdownItem.stx +0 -35
  72. package/src/ui/dropdown/DropdownItems.stx +0 -18
  73. package/src/ui/dropdown/README.md +0 -114
  74. package/src/ui/dropdown/index.ts +0 -29
  75. package/src/ui/form/Form.stx +0 -95
  76. package/src/ui/form/index.ts +0 -67
  77. package/src/ui/heatmap/Heatmap.stx +0 -294
  78. package/src/ui/image/Image.stx +0 -165
  79. package/src/ui/image/README.md +0 -164
  80. package/src/ui/image/index.ts +0 -22
  81. package/src/ui/input/EmailInput.stx +0 -30
  82. package/src/ui/input/NumberInput.stx +0 -126
  83. package/src/ui/input/PasswordInput.stx +0 -143
  84. package/src/ui/input/SearchInput.stx +0 -44
  85. package/src/ui/input/TextInput.stx +0 -132
  86. package/src/ui/input/index.ts +0 -180
  87. package/src/ui/listbox/Listbox.stx +0 -101
  88. package/src/ui/listbox/ListboxButton.stx +0 -28
  89. package/src/ui/listbox/ListboxLabel.stx +0 -11
  90. package/src/ui/listbox/ListboxOption.stx +0 -49
  91. package/src/ui/listbox/ListboxOptions.stx +0 -16
  92. package/src/ui/listbox/README.md +0 -135
  93. package/src/ui/listbox/index.ts +0 -39
  94. package/src/ui/navigator/Navigator.stx +0 -113
  95. package/src/ui/navigator/README.md +0 -352
  96. package/src/ui/navigator/index.ts +0 -20
  97. package/src/ui/notification/Notification.stx +0 -117
  98. package/src/ui/notification/README.md +0 -114
  99. package/src/ui/notification/index.ts +0 -12
  100. package/src/ui/pagination/Pagination.stx +0 -150
  101. package/src/ui/pagination/index.ts +0 -11
  102. package/src/ui/payment/Checkout.stx +0 -31
  103. package/src/ui/payment/DefaultPaymentMethod.stx +0 -62
  104. package/src/ui/payment/PaymentMethods.stx +0 -104
  105. package/src/ui/payment/README.md +0 -345
  106. package/src/ui/payment/SubscriptionCheckout.stx +0 -323
  107. package/src/ui/payment/index.ts +0 -59
  108. package/src/ui/popover/Popover.stx +0 -60
  109. package/src/ui/popover/PopoverButton.stx +0 -29
  110. package/src/ui/popover/PopoverPanel.stx +0 -23
  111. package/src/ui/popover/README.md +0 -91
  112. package/src/ui/popover/index.ts +0 -22
  113. package/src/ui/portal/Portal.stx +0 -57
  114. package/src/ui/portal/index.ts +0 -6
  115. package/src/ui/progress/Progress.stx +0 -131
  116. package/src/ui/progress/index.ts +0 -12
  117. package/src/ui/radio/Radio.stx +0 -77
  118. package/src/ui/radio/index.ts +0 -15
  119. package/src/ui/radio-group/README.md +0 -139
  120. package/src/ui/radio-group/RadioGroup.stx +0 -60
  121. package/src/ui/radio-group/RadioGroupDescription.stx +0 -11
  122. package/src/ui/radio-group/RadioGroupLabel.stx +0 -11
  123. package/src/ui/radio-group/RadioGroupOption.stx +0 -58
  124. package/src/ui/radio-group/index.ts +0 -30
  125. package/src/ui/select/README.md +0 -180
  126. package/src/ui/select/Select.stx +0 -82
  127. package/src/ui/select/index.ts +0 -21
  128. package/src/ui/sidebar/Sidebar.stx +0 -458
  129. package/src/ui/sidebar/SidebarFooter.stx +0 -71
  130. package/src/ui/sidebar/SidebarHeader.stx +0 -103
  131. package/src/ui/sidebar/SidebarItem.stx +0 -86
  132. package/src/ui/sidebar/SidebarSection.stx +0 -83
  133. package/src/ui/sidebar/index.ts +0 -153
  134. package/src/ui/sidebar/themes.ts +0 -240
  135. package/src/ui/skeleton/Skeleton.stx +0 -48
  136. package/src/ui/skeleton/index.ts +0 -11
  137. package/src/ui/spinner/Spinner.stx +0 -109
  138. package/src/ui/spinner/index.ts +0 -9
  139. package/src/ui/stepper/README.md +0 -36
  140. package/src/ui/stepper/Stepper.stx +0 -17
  141. package/src/ui/stepper/StepperStep.stx +0 -57
  142. package/src/ui/stepper/index.ts +0 -19
  143. package/src/ui/storage/README.md +0 -193
  144. package/src/ui/storage/index.ts +0 -347
  145. package/src/ui/switch/README.md +0 -114
  146. package/src/ui/switch/Switch.stx +0 -72
  147. package/src/ui/switch/index.ts +0 -10
  148. package/src/ui/table/README.md +0 -39
  149. package/src/ui/table/Table.stx +0 -14
  150. package/src/ui/table/TableBody.stx +0 -10
  151. package/src/ui/table/TableCell.stx +0 -10
  152. package/src/ui/table/TableHead.stx +0 -10
  153. package/src/ui/table/TableHeader.stx +0 -20
  154. package/src/ui/table/TableRow.stx +0 -11
  155. package/src/ui/table/index.ts +0 -35
  156. package/src/ui/tabs/TabPanel.stx +0 -25
  157. package/src/ui/tabs/Tabs.stx +0 -202
  158. package/src/ui/tabs/index.ts +0 -50
  159. package/src/ui/teleport/Teleport.stx +0 -65
  160. package/src/ui/teleport/index.ts +0 -7
  161. package/src/ui/textarea/Textarea.stx +0 -108
  162. package/src/ui/textarea/index.ts +0 -20
  163. package/src/ui/tooltip/Tooltip.stx +0 -71
  164. package/src/ui/tooltip/index.ts +0 -10
  165. package/src/ui/transition/README.md +0 -30
  166. package/src/ui/transition/Transition.stx +0 -102
  167. package/src/ui/transition/index.ts +0 -25
  168. package/src/ui/video/README.md +0 -47
  169. package/src/ui/video/Video.stx +0 -45
  170. package/src/ui/video/index.ts +0 -16
  171. package/src/ui/virtual-list/VirtualList.stx +0 -67
  172. package/src/ui/virtual-list/index.ts +0 -10
  173. package/src/ui/virtual-table/VirtualTable.stx +0 -103
  174. package/src/ui/virtual-table/index.ts +0 -21
  175. package/src/utils/PROP_VALIDATION.md +0 -458
  176. package/src/utils/THEME_SYSTEM.md +0 -702
  177. package/src/utils/accessibility.ts +0 -841
  178. package/src/utils/animation/easing.ts +0 -368
  179. package/src/utils/animation/helpers.ts +0 -223
  180. package/src/utils/animation/index.ts +0 -5
  181. package/src/utils/animation/keyframes.ts +0 -230
  182. package/src/utils/error-handling.ts +0 -475
  183. package/src/utils/highlighter.ts +0 -93
  184. package/src/utils/i18n.ts +0 -634
  185. package/src/utils/keep-alive.ts +0 -819
  186. package/src/utils/lazy-loading.ts +0 -530
  187. package/src/utils/prop-validation.ts +0 -478
  188. 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
- }