@classic-homes/theme-svelte 1.2.2 → 1.3.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 (55) hide show
  1. package/README.md +43 -0
  2. package/dist/__tests__/a11y/Media.a11y.test.js +145 -0
  3. package/dist/__tests__/media-components.test.js +206 -0
  4. package/dist/__tests__/media-hls.test.js +273 -0
  5. package/dist/__tests__/media-matterport.test.js +296 -0
  6. package/dist/__tests__/media-players.test.js +244 -0
  7. package/dist/__tests__/media-scrubber.test.js +44 -0
  8. package/dist/__tests__/media-utils.test.js +427 -0
  9. package/dist/__tests__/setup.js +50 -0
  10. package/dist/lib/components/DataPanel.svelte +8 -0
  11. package/dist/lib/components/DataPanel.svelte.d.ts +6 -0
  12. package/dist/lib/components/DataPanelTab.svelte +38 -8
  13. package/dist/lib/components/DataPanelTab.svelte.d.ts +7 -0
  14. package/dist/lib/components/FileUpload.svelte +221 -4
  15. package/dist/lib/components/FileUpload.svelte.d.ts +13 -1
  16. package/dist/lib/components/media/AspectRatio.svelte +47 -0
  17. package/dist/lib/components/media/AspectRatio.svelte.d.ts +20 -0
  18. package/dist/lib/components/media/Audio.svelte +247 -0
  19. package/dist/lib/components/media/Audio.svelte.d.ts +34 -0
  20. package/dist/lib/components/media/Image.svelte +209 -0
  21. package/dist/lib/components/media/Image.svelte.d.ts +52 -0
  22. package/dist/lib/components/media/Lightbox.svelte +449 -0
  23. package/dist/lib/components/media/Lightbox.svelte.d.ts +33 -0
  24. package/dist/lib/components/media/MediaCarousel.svelte +359 -0
  25. package/dist/lib/components/media/MediaCarousel.svelte.d.ts +30 -0
  26. package/dist/lib/components/media/MediaEmbed.svelte +314 -0
  27. package/dist/lib/components/media/MediaEmbed.svelte.d.ts +99 -0
  28. package/dist/lib/components/media/MediaGrid.svelte +327 -0
  29. package/dist/lib/components/media/MediaGrid.svelte.d.ts +57 -0
  30. package/dist/lib/components/media/Video.svelte +455 -0
  31. package/dist/lib/components/media/Video.svelte.d.ts +81 -0
  32. package/dist/lib/components/media/index.d.ts +14 -0
  33. package/dist/lib/components/media/index.js +14 -0
  34. package/dist/lib/components/media/internal/MediaControls.svelte +283 -0
  35. package/dist/lib/components/media/internal/MediaControls.svelte.d.ts +32 -0
  36. package/dist/lib/components/media/internal/MediaScrubber.svelte +211 -0
  37. package/dist/lib/components/media/internal/MediaScrubber.svelte.d.ts +27 -0
  38. package/dist/lib/components/media/internal/MediaTile.svelte +129 -0
  39. package/dist/lib/components/media/internal/MediaTile.svelte.d.ts +22 -0
  40. package/dist/lib/composables/index.d.ts +3 -0
  41. package/dist/lib/composables/index.js +4 -0
  42. package/dist/lib/composables/useHlsSource.svelte.d.ts +70 -0
  43. package/dist/lib/composables/useHlsSource.svelte.js +119 -0
  44. package/dist/lib/composables/useMediaPlayer.svelte.d.ts +114 -0
  45. package/dist/lib/composables/useMediaPlayer.svelte.js +422 -0
  46. package/dist/lib/composables/useMediaZoom.svelte.d.ts +59 -0
  47. package/dist/lib/composables/useMediaZoom.svelte.js +199 -0
  48. package/dist/lib/index.d.ts +8 -2
  49. package/dist/lib/index.js +14 -1
  50. package/dist/lib/types/components.d.ts +172 -0
  51. package/dist/lib/utils/hls.d.ts +76 -0
  52. package/dist/lib/utils/hls.js +83 -0
  53. package/dist/lib/utils/media.d.ts +379 -0
  54. package/dist/lib/utils/media.js +715 -0
  55. package/package.json +6 -2
@@ -58,7 +58,9 @@ export { default as Signature } from './components/Signature.svelte';
58
58
  export { default as DataTable } from './components/DataTable.svelte';
59
59
  export { default as Tabs } from './components/Tabs.svelte';
60
60
  export { default as TabPanel } from './components/TabPanel.svelte';
61
- export type { DropdownMenuItem, DropdownMenuGroup, SelectOption, SelectGroup, Tab, FileMetadata, DataTableColumn, DataTableFilterFn, DataTableFilterOption, RadioOption, ComboboxOption, MultiSelectOption, SignatureData, SignatureStroke, SignaturePoint, SignatureMetadata, SignatureFont, SignatureValidationResult, } from './types/components.js';
61
+ export { AspectRatio, Image, Video, Audio, MediaEmbed, MediaGrid, MediaCarousel, Lightbox, } from './components/media/index.js';
62
+ export type { MediaGridColumns } from './components/media/MediaGrid.svelte';
63
+ export type { DropdownMenuItem, DropdownMenuGroup, SelectOption, SelectGroup, Tab, FileMetadata, DataTableColumn, DataTableFilterFn, DataTableFilterOption, RadioOption, ComboboxOption, MultiSelectOption, SignatureData, SignatureStroke, SignaturePoint, SignatureMetadata, SignatureFont, SignatureValidationResult, MediaKind, MediaEmbedProvider, MediaSource, MediaTextTrack, MediaItemBase, ImageMediaItem, VideoMediaItem, AudioMediaItem, EmbedMediaItem, MediaItem, MediaAspectRatio, MediaFit, CloudflareStreamAccount, MatterportPlayerOptions, } from './types/components.js';
62
64
  export { createDataTable, type DataTableOptions, type DataTableRow, type DataTableInstance, type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type RowSelectionState, type ColumnVisibilityState, type ColumnFiltersState, type PaginationState, type DataTableProps, type DataTableCoreProps, type DataTablePaginationProps, type InferRowType, type FormatResult, getAlignClass, formatCellValue, getColumnValue, defaultGetRowId, DataTableToolbar, DataTableSearch, DataTableColumnVisibility, DataTableColumnFilter, DataTableFacetedFilter, DataTablePagination, DataTableVirtual, DataTableEditableCell, } from './components/data-table/index.js';
63
65
  export { default as LogoMain } from './components/LogoMain.svelte';
64
66
  export { default as LoadingLogo } from './components/LoadingLogo.svelte';
@@ -88,9 +90,13 @@ export type { PanelEdge, PanelMode, PanelVariant, SnapPoint, CardConfig, Positio
88
90
  export { cn } from './utils.js';
89
91
  export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
90
92
  export { generateId, computeDescribedBy } from './utils/form.js';
93
+ export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, type ParsedEmbed, type EmbedSrcOptions, } from './utils/media.js';
94
+ export { cloudflareStream, cloudflareStreamUrls, cloudflareStreamThumbnail, cloudflareStreamHost, type CloudflareStreamItemOptions, type CloudflareStreamUrlOptions, type CloudflareStreamUrls, type CloudflareStreamThumbnailOptions, } from './utils/media.js';
95
+ export { matterportTour, buildMatterportSrc, fetchMatterportMetadata, type MatterportTourOptions, type MatterportMetadata, } from './utils/media.js';
96
+ export { setHlsLoader, hasHlsLoader, loadHls, canPlayHlsNatively, type HlsLoader, type HlsConstructor, type HlsInstance, } from './utils/hls.js';
91
97
  export { tv, type VariantProps } from 'tailwind-variants';
92
98
  export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange, validateProps, createValidator, type ValidationResult, } from './validation.js';
93
99
  export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, type PerformanceMark, } from './performance.js';
94
- export { useForm, useAsync, runAsync, usePersistedForm, useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useEscapeKey, useClickOutside, clickOutsideAction, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, type UseFocusTrapOptions, type UseFocusRefOptions, type UseArrowNavigationOptions, type ArrowNavigationOrientation, type UseRovingTabindexOptions, type UseAnnounceOptions, type UseAnnounceReturn, type UseEscapeKeyOptions, type UseClickOutsideOptions, } from './composables/index.js';
100
+ export { useForm, useAsync, runAsync, usePersistedForm, useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useMediaPlayer, useMediaZoom, useHlsSource, useEscapeKey, useClickOutside, clickOutsideAction, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, type UseFocusTrapOptions, type UseFocusRefOptions, type UseArrowNavigationOptions, type ArrowNavigationOrientation, type UseRovingTabindexOptions, type UseAnnounceOptions, type UseAnnounceReturn, type UseEscapeKeyOptions, type UseClickOutsideOptions, type UseMediaPlayerOptions, type MediaPlayerApi, type MediaTimeRange, type MediaTrackInfo, type UseMediaZoomOptions, type MediaZoomApi, type UseHlsSourceOptions, type HlsSourceApi, type HlsStatus, } from './composables/index.js';
95
101
  export * from './schemas/index.js';
96
102
  export { type Notice, type NoticeConfig, type NoticeLocation, type NoticeVariant, type NoticePage, type ActiveNotice, NoticeSchema, NoticeLinkSchema, validateNotice, validateNotices, initializeNotices, setCurrentPath, dismissNotice, clearDismissedNotices, isNoticeDismissed, activeNotices, bannerNotices, cardNotices, getNoticesForPage, refreshNotices, cleanupDismissedNotices, isNoticeActive, isNoticeForPage, isNoticeForLocation, sanitizeHtml, initSanitizer, NoticeBanner, NoticeCard, } from './notices/index.js';
package/dist/lib/index.js CHANGED
@@ -70,6 +70,8 @@ export { default as DataTable } from './components/DataTable.svelte';
70
70
  // Tabs
71
71
  export { default as Tabs } from './components/Tabs.svelte';
72
72
  export { default as TabPanel } from './components/TabPanel.svelte';
73
+ // Media Components
74
+ export { AspectRatio, Image, Video, Audio, MediaEmbed, MediaGrid, MediaCarousel, Lightbox, } from './components/media/index.js';
73
75
  // DataTable sub-components and utilities
74
76
  export {
75
77
  // Core state management (native Svelte 5 runes)
@@ -113,6 +115,15 @@ export { cn } from './utils.js';
113
115
  export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
114
116
  // Form utilities
115
117
  export { generateId, computeDescribedBy } from './utils/form.js';
118
+ // Media utilities
119
+ export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, } from './utils/media.js';
120
+ // Cloudflare Stream helpers
121
+ export { cloudflareStream, cloudflareStreamUrls, cloudflareStreamThumbnail, cloudflareStreamHost, } from './utils/media.js';
122
+ // Matterport 3D tours
123
+ export { matterportTour, buildMatterportSrc, fetchMatterportMetadata, } from './utils/media.js';
124
+ // HLS support — hls.js is an optional peer dependency; register a loader once
125
+ // at app startup to enable adaptive streams outside Safari
126
+ export { setHlsLoader, hasHlsLoader, loadHls, canPlayHlsNatively, } from './utils/hls.js';
116
127
  // Re-export tailwind-variants types for consumer convenience
117
128
  export { tv } from 'tailwind-variants';
118
129
  // Validation utilities (dev-only, tree-shaken in production)
@@ -122,7 +133,9 @@ export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clear
122
133
  // Composables - reusable Svelte 5 state management patterns
123
134
  export { useForm, useAsync, runAsync, usePersistedForm,
124
135
  // Accessibility composables
125
- useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useEscapeKey, useClickOutside, clickOutsideAction, } from './composables/index.js';
136
+ useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce,
137
+ // Media composables
138
+ useMediaPlayer, useMediaZoom, useHlsSource, useEscapeKey, useClickOutside, clickOutsideAction, } from './composables/index.js';
126
139
  // Validation schemas - Zod schemas for forms and data validation
127
140
  export * from './schemas/index.js';
128
141
  // Notice System - Scheduled, page-specific, dismissible announcements
@@ -60,6 +60,12 @@ export interface FileMetadata {
60
60
  error?: string;
61
61
  /** Whether the file is currently uploading */
62
62
  uploading?: boolean;
63
+ /** Playback duration in seconds, for audio and video files */
64
+ duration?: number;
65
+ /** Intrinsic width in pixels, for images and video */
66
+ width?: number;
67
+ /** Intrinsic height in pixels, for images and video */
68
+ height?: number;
63
69
  }
64
70
  /**
65
71
  * DataTable Types
@@ -205,3 +211,169 @@ export interface SignatureValidationResult {
205
211
  valid: boolean;
206
212
  error?: string;
207
213
  }
214
+ /**
215
+ * Media Types
216
+ */
217
+ /** The kind of media an item represents */
218
+ export type MediaKind = 'image' | 'video' | 'audio' | 'embed';
219
+ /**
220
+ * Third-party providers supported by MediaEmbed.
221
+ *
222
+ * `matterport` embeds an interactive 3D space rather than a video, so it has
223
+ * no timeline: it shows no play affordance or duration in grids and strips.
224
+ */
225
+ export type MediaEmbedProvider = 'youtube' | 'vimeo' | 'cloudflare' | 'matterport';
226
+ /**
227
+ * Location of a Cloudflare Stream account.
228
+ *
229
+ * Supply `customerCode` (the `customer-<CODE>` part of the subdomain shown in
230
+ * the Stream dashboard), or `domain` when Stream is served from a custom
231
+ * hostname.
232
+ */
233
+ export interface CloudflareStreamAccount {
234
+ /** Customer code from the Stream dashboard */
235
+ customerCode?: string;
236
+ /** Full hostname, when serving Stream from a custom domain */
237
+ domain?: string;
238
+ }
239
+ /** A single `<source>` entry for a video or audio element */
240
+ export interface MediaSource {
241
+ /** Source URL */
242
+ src: string;
243
+ /** MIME type, e.g. `video/mp4` — lets the browser skip sources it cannot play */
244
+ type?: string;
245
+ }
246
+ /** A `<track>` entry providing captions, subtitles, or descriptions */
247
+ export interface MediaTextTrack {
248
+ /** URL of the WebVTT file */
249
+ src: string;
250
+ /** Track kind (defaults to `subtitles`) */
251
+ kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
252
+ /** BCP 47 language tag, e.g. `en` */
253
+ srclang: string;
254
+ /** Human-readable label shown in the captions menu */
255
+ label: string;
256
+ /** Whether this track is enabled by default */
257
+ default?: boolean;
258
+ }
259
+ /** Fields shared by every media item */
260
+ export interface MediaItemBase {
261
+ /** Unique identifier, used as the keyed-each key */
262
+ id: string;
263
+ /** Short title shown in the lightbox and used as an accessible name */
264
+ title?: string;
265
+ /** Longer caption shown beneath the media */
266
+ caption?: string;
267
+ /** Thumbnail URL used in grids, carousels, and thumbnail strips */
268
+ thumbnail?: string;
269
+ }
270
+ /** An image item */
271
+ export interface ImageMediaItem extends MediaItemBase {
272
+ kind: 'image';
273
+ /** Image URL */
274
+ src: string;
275
+ /** Alternative text — required; use an empty string only for decorative images */
276
+ alt: string;
277
+ /** Responsive candidate list */
278
+ srcset?: string;
279
+ /** Sizes attribute paired with `srcset` */
280
+ sizes?: string;
281
+ /** Low-resolution placeholder (typically a data URL) blurred in while loading */
282
+ placeholder?: string;
283
+ /** Intrinsic width in pixels — set it to reserve layout space */
284
+ width?: number;
285
+ /** Intrinsic height in pixels — set it to reserve layout space */
286
+ height?: number;
287
+ /** Download URL, when it differs from `src` */
288
+ downloadUrl?: string;
289
+ }
290
+ /** A self-hosted video item */
291
+ export interface VideoMediaItem extends MediaItemBase {
292
+ kind: 'video';
293
+ /** A single URL, or several sources for format fallback */
294
+ src: string | MediaSource[];
295
+ /** Poster image shown before playback starts */
296
+ poster?: string;
297
+ /** Caption and subtitle tracks */
298
+ tracks?: MediaTextTrack[];
299
+ /** Known duration in seconds, shown before metadata loads */
300
+ duration?: number;
301
+ /** Download URL, when downloads are offered */
302
+ downloadUrl?: string;
303
+ }
304
+ /** A self-hosted audio item */
305
+ export interface AudioMediaItem extends MediaItemBase {
306
+ kind: 'audio';
307
+ /** A single URL, or several sources for format fallback */
308
+ src: string | MediaSource[];
309
+ /** Performer or author, shown beside the title */
310
+ artist?: string;
311
+ /** Artwork image URL */
312
+ artwork?: string;
313
+ /** Caption and transcript tracks */
314
+ tracks?: MediaTextTrack[];
315
+ /** Known duration in seconds, shown before metadata loads */
316
+ duration?: number;
317
+ /** Download URL, when downloads are offered */
318
+ downloadUrl?: string;
319
+ }
320
+ /**
321
+ * Options for the Matterport showcase player.
322
+ *
323
+ * Matterport's own defaults are left alone unless an option asks otherwise,
324
+ * so an embed looks the way the space's owner configured it.
325
+ */
326
+ export interface MatterportPlayerOptions {
327
+ /** Start the space moving as soon as it loads */
328
+ autoplay?: boolean;
329
+ /** Load a lightweight first view so the space is explorable sooner (default `true`) */
330
+ quickStart?: boolean;
331
+ /** Show Matterport's "Presented by" branding and contact details (default `true`) */
332
+ brand?: boolean;
333
+ /** Show the in-player help button (default `true`) */
334
+ help?: boolean;
335
+ /** Show the highlight reel along the bottom (default `true`) */
336
+ highlightReel?: boolean;
337
+ /** Show the VR button (default `true`) */
338
+ vr?: boolean;
339
+ /** Show the floor selector (default `true`) */
340
+ floorSelector?: boolean;
341
+ /** Seconds after load before the guided tour starts on its own */
342
+ guidedTourStart?: number;
343
+ /** Use the MLS-compliant presentation, which strips branding, links, and VR */
344
+ mls?: boolean;
345
+ /** Interface language, as a two-letter code */
346
+ lang?: string;
347
+ /**
348
+ * Any further showcase parameters, passed through untouched. Booleans are
349
+ * sent as Matterport's `1`/`0`.
350
+ */
351
+ params?: Record<string, string | number | boolean>;
352
+ }
353
+ /** A third-party embedded video item */
354
+ export interface EmbedMediaItem extends MediaItemBase, CloudflareStreamAccount {
355
+ kind: 'embed';
356
+ /** Hosting provider */
357
+ provider: MediaEmbedProvider;
358
+ /**
359
+ * Provider-specific identifier. The video id for YouTube and Vimeo; for
360
+ * Cloudflare Stream the video UID, or a signed playback token when the
361
+ * video is private; for Matterport the space's model id.
362
+ */
363
+ videoId: string;
364
+ /**
365
+ * Poster image. Derived automatically for YouTube and Cloudflare Stream;
366
+ * required for Vimeo, whose thumbnails need a network lookup.
367
+ */
368
+ poster?: string;
369
+ /** Start offset in seconds */
370
+ start?: number;
371
+ /** Showcase options, for `provider: 'matterport'` */
372
+ matterport?: MatterportPlayerOptions;
373
+ }
374
+ /** Any item renderable by the media components */
375
+ export type MediaItem = ImageMediaItem | VideoMediaItem | AudioMediaItem | EmbedMediaItem;
376
+ /** Named aspect ratios accepted by AspectRatio and media components */
377
+ export type MediaAspectRatio = 'square' | 'video' | 'photo' | 'portrait' | 'wide';
378
+ /** How media should be fitted into its box */
379
+ export type MediaFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * HLS support
3
+ *
4
+ * Adaptive streams — Cloudflare Stream among them — are delivered as HLS
5
+ * playlists. Safari and iOS play those natively; Chrome and Firefox need
6
+ * `hls.js`.
7
+ *
8
+ * `hls.js` is an *optional* peer dependency, and the import that loads it
9
+ * deliberately lives in application code rather than in this package. A
10
+ * dynamic `import('hls.js')` written here would be resolved at build time by
11
+ * every consumer's bundler, so anyone who had not installed hls.js would fail
12
+ * to build even if they never touched a stream. Registering a loader keeps the
13
+ * dependency genuinely optional.
14
+ *
15
+ * Three ways to provide it, checked in order:
16
+ *
17
+ * 1. A loader passed to the component: `<Video hlsLoader={...} />`
18
+ * 2. A loader registered once at app startup via {@link setHlsLoader}
19
+ * 3. A global `Hls`, as set by the library's CDN build in a `<script>` tag
20
+ *
21
+ * @example Register once at app startup
22
+ * ```ts
23
+ * import { setHlsLoader } from '@classic-homes/theme-svelte';
24
+ *
25
+ * setHlsLoader(() => import('hls.js').then((m) => m.default));
26
+ * ```
27
+ */
28
+ /**
29
+ * The shape of `hls.js` this package relies on.
30
+ *
31
+ * Deliberately structural rather than an `import type` of the real package —
32
+ * typing against `hls.js` would make it a required dependency for type
33
+ * checking, which is exactly what an optional peer dependency must avoid.
34
+ */
35
+ export interface HlsInstance {
36
+ loadSource(url: string): void;
37
+ attachMedia(media: HTMLMediaElement): void;
38
+ destroy(): void;
39
+ on(event: string, callback: (event: string, data: HlsErrorData) => void): void;
40
+ startLoad(): void;
41
+ recoverMediaError(): void;
42
+ }
43
+ /** The error payload hls.js passes to its `hlsError` event. */
44
+ export interface HlsErrorData {
45
+ type?: string;
46
+ details?: string;
47
+ fatal?: boolean;
48
+ }
49
+ /** The `hls.js` default export: a constructor plus a static support probe. */
50
+ export interface HlsConstructor {
51
+ new (config?: Record<string, unknown>): HlsInstance;
52
+ isSupported(): boolean;
53
+ readonly Events: Record<string, string>;
54
+ readonly ErrorTypes: Record<string, string>;
55
+ }
56
+ /** A function that resolves the `hls.js` constructor. */
57
+ export type HlsLoader = () => Promise<HlsConstructor> | HlsConstructor;
58
+ /**
59
+ * Register how `hls.js` should be loaded, once, at application startup.
60
+ *
61
+ * @example
62
+ * setHlsLoader(() => import('hls.js').then((m) => m.default));
63
+ */
64
+ export declare function setHlsLoader(loader: HlsLoader | null): void;
65
+ /** Whether an `hls.js` loader is available, without loading it. */
66
+ export declare function hasHlsLoader(): boolean;
67
+ /**
68
+ * Resolve the `hls.js` constructor, or `null` when none is available.
69
+ *
70
+ * The result is cached, so several players on one page share a single load.
71
+ */
72
+ export declare function loadHls(loader?: HlsLoader): Promise<HlsConstructor | null>;
73
+ /** Whether the browser can play HLS without `hls.js`. */
74
+ export declare function canPlayHlsNatively(element?: HTMLMediaElement | null): boolean;
75
+ /** Reset the cached loader. Exported for tests. */
76
+ export declare function resetHlsLoader(): void;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * HLS support
3
+ *
4
+ * Adaptive streams — Cloudflare Stream among them — are delivered as HLS
5
+ * playlists. Safari and iOS play those natively; Chrome and Firefox need
6
+ * `hls.js`.
7
+ *
8
+ * `hls.js` is an *optional* peer dependency, and the import that loads it
9
+ * deliberately lives in application code rather than in this package. A
10
+ * dynamic `import('hls.js')` written here would be resolved at build time by
11
+ * every consumer's bundler, so anyone who had not installed hls.js would fail
12
+ * to build even if they never touched a stream. Registering a loader keeps the
13
+ * dependency genuinely optional.
14
+ *
15
+ * Three ways to provide it, checked in order:
16
+ *
17
+ * 1. A loader passed to the component: `<Video hlsLoader={...} />`
18
+ * 2. A loader registered once at app startup via {@link setHlsLoader}
19
+ * 3. A global `Hls`, as set by the library's CDN build in a `<script>` tag
20
+ *
21
+ * @example Register once at app startup
22
+ * ```ts
23
+ * import { setHlsLoader } from '@classic-homes/theme-svelte';
24
+ *
25
+ * setHlsLoader(() => import('hls.js').then((m) => m.default));
26
+ * ```
27
+ */
28
+ let registeredLoader = null;
29
+ let cached = null;
30
+ /**
31
+ * Register how `hls.js` should be loaded, once, at application startup.
32
+ *
33
+ * @example
34
+ * setHlsLoader(() => import('hls.js').then((m) => m.default));
35
+ */
36
+ export function setHlsLoader(loader) {
37
+ registeredLoader = loader;
38
+ // Drop any cached resolution so a newly registered loader takes effect.
39
+ cached = null;
40
+ }
41
+ /** Whether an `hls.js` loader is available, without loading it. */
42
+ export function hasHlsLoader() {
43
+ return registeredLoader !== null || getGlobalHls() !== null;
44
+ }
45
+ function getGlobalHls() {
46
+ if (typeof globalThis === 'undefined')
47
+ return null;
48
+ const candidate = globalThis.Hls;
49
+ return typeof candidate === 'function' ? candidate : null;
50
+ }
51
+ /**
52
+ * Resolve the `hls.js` constructor, or `null` when none is available.
53
+ *
54
+ * The result is cached, so several players on one page share a single load.
55
+ */
56
+ export function loadHls(loader) {
57
+ // A per-call loader bypasses the shared cache; it may differ per component.
58
+ if (loader) {
59
+ return Promise.resolve(loader()).catch(() => null);
60
+ }
61
+ if (!cached) {
62
+ const resolved = registeredLoader ?? (() => getGlobalHls());
63
+ cached = Promise.resolve()
64
+ .then(() => resolved())
65
+ .then((ctor) => ctor ?? null)
66
+ .catch(() => null);
67
+ }
68
+ return cached;
69
+ }
70
+ /** Whether the browser can play HLS without `hls.js`. */
71
+ export function canPlayHlsNatively(element) {
72
+ const probe = element ?? (typeof document === 'undefined' ? null : document.createElement('video'));
73
+ if (!probe)
74
+ return false;
75
+ // Safari answers 'maybe' or 'probably'; everything else answers ''.
76
+ return (probe.canPlayType('application/vnd.apple.mpegurl') !== '' ||
77
+ probe.canPlayType('application/x-mpegURL') !== '');
78
+ }
79
+ /** Reset the cached loader. Exported for tests. */
80
+ export function resetHlsLoader() {
81
+ registeredLoader = null;
82
+ cached = null;
83
+ }