@signal9/era-ui 1.1.2
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/dist/dev/audit/audits/_helpers.d.ts +5 -0
- package/dist/dev/audit/audits/_helpers.js +11 -0
- package/dist/dev/audit/audits/concentric-inset.d.ts +2 -0
- package/dist/dev/audit/audits/concentric-inset.js +33 -0
- package/dist/dev/audit/audits/index.d.ts +4 -0
- package/dist/dev/audit/audits/index.js +9 -0
- package/dist/dev/audit/audits/uniform-sibling-gaps.d.ts +2 -0
- package/dist/dev/audit/audits/uniform-sibling-gaps.js +29 -0
- package/dist/dev/audit/audits/wall-matches-gap.d.ts +2 -0
- package/dist/dev/audit/audits/wall-matches-gap.js +30 -0
- package/dist/dev/audit/index.d.ts +4 -0
- package/dist/dev/audit/index.js +3 -0
- package/dist/dev/audit/registry.d.ts +13 -0
- package/dist/dev/audit/registry.js +50 -0
- package/dist/dev/audit/runner.d.ts +3 -0
- package/dist/dev/audit/runner.js +27 -0
- package/dist/dev/audit/types.d.ts +28 -0
- package/dist/dev/audit/types.js +7 -0
- package/dist/dev/audit-overlay.svelte +125 -0
- package/dist/dev/audit-overlay.svelte.d.ts +9 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/docs/badge.md +18 -0
- package/dist/docs/bits-ui-audit.md +58 -0
- package/dist/docs/button.md +18 -0
- package/dist/docs/chip.md +20 -0
- package/dist/docs/component-docs-core.d.ts +8 -0
- package/dist/docs/component-docs-core.js +72 -0
- package/dist/docs/component-docs.d.ts +2 -0
- package/dist/docs/component-docs.js +25 -0
- package/dist/docs/component-source-scanners.d.ts +18 -0
- package/dist/docs/component-source-scanners.js +46 -0
- package/dist/docs/extract-props.d.ts +13 -0
- package/dist/docs/extract-props.js +324 -0
- package/dist/docs/generated-docs.d.ts +4 -0
- package/dist/docs/generated-docs.js +23 -0
- package/dist/docs/index.d.ts +5 -0
- package/dist/docs/index.js +62 -0
- package/dist/docs/llms-txt.d.ts +2 -0
- package/dist/docs/llms-txt.js +84 -0
- package/dist/docs/quickstart.md +43 -0
- package/dist/docs/separator.md +20 -0
- package/dist/docs/table.md +35 -0
- package/dist/era-ui.css +2 -0
- package/dist/generated-docs/accordion.md +57 -0
- package/dist/generated-docs/alert-dialog.md +81 -0
- package/dist/generated-docs/aspect-ratio.md +17 -0
- package/dist/generated-docs/avatar.md +39 -0
- package/dist/generated-docs/badge.md +38 -0
- package/dist/generated-docs/bar.md +19 -0
- package/dist/generated-docs/button-group.md +19 -0
- package/dist/generated-docs/button.md +46 -0
- package/dist/generated-docs/calendar.md +126 -0
- package/dist/generated-docs/card.md +26 -0
- package/dist/generated-docs/checkbox.md +42 -0
- package/dist/generated-docs/chip.md +40 -0
- package/dist/generated-docs/code-block.md +20 -0
- package/dist/generated-docs/collapsible.md +30 -0
- package/dist/generated-docs/combobox.md +74 -0
- package/dist/generated-docs/command.md +123 -0
- package/dist/generated-docs/context-menu.md +94 -0
- package/dist/generated-docs/date-field.md +49 -0
- package/dist/generated-docs/date-picker.md +180 -0
- package/dist/generated-docs/date-range-field.md +50 -0
- package/dist/generated-docs/date-range-picker.md +180 -0
- package/dist/generated-docs/dialog.md +70 -0
- package/dist/generated-docs/draggable.md +3 -0
- package/dist/generated-docs/dropdown-menu.md +94 -0
- package/dist/generated-docs/file-upload.md +18 -0
- package/dist/generated-docs/input.md +18 -0
- package/dist/generated-docs/kv.md +20 -0
- package/dist/generated-docs/label.md +17 -0
- package/dist/generated-docs/link-preview.md +30 -0
- package/dist/generated-docs/llms-full.txt +3104 -0
- package/dist/generated-docs/llms.txt +34 -0
- package/dist/generated-docs/manifest.json +812 -0
- package/dist/generated-docs/menu.md +11 -0
- package/dist/generated-docs/menubar.md +103 -0
- package/dist/generated-docs/meter.md +17 -0
- package/dist/generated-docs/mode.md +24 -0
- package/dist/generated-docs/navigation-menu.md +11 -0
- package/dist/generated-docs/pagination.md +55 -0
- package/dist/generated-docs/pane.md +30 -0
- package/dist/generated-docs/pin-input.md +31 -0
- package/dist/generated-docs/popover.md +43 -0
- package/dist/generated-docs/progress.md +17 -0
- package/dist/generated-docs/quickstart.md +47 -0
- package/dist/generated-docs/radio-group.md +31 -0
- package/dist/generated-docs/range-calendar.md +11 -0
- package/dist/generated-docs/rating-group.md +31 -0
- package/dist/generated-docs/scroll-area.md +57 -0
- package/dist/generated-docs/select.md +49 -0
- package/dist/generated-docs/separator.md +39 -0
- package/dist/generated-docs/sheet.md +11 -0
- package/dist/generated-docs/skeleton.md +18 -0
- package/dist/generated-docs/slider.md +71 -0
- package/dist/generated-docs/surfaces.md +3 -0
- package/dist/generated-docs/switch.md +18 -0
- package/dist/generated-docs/table.md +113 -0
- package/dist/generated-docs/tabs.md +48 -0
- package/dist/generated-docs/time-field.md +49 -0
- package/dist/generated-docs/time-range-field.md +50 -0
- package/dist/generated-docs/toggle-group.md +30 -0
- package/dist/generated-docs/toggle.md +20 -0
- package/dist/generated-docs/toolbar.md +11 -0
- package/dist/generated-docs/tooltip.md +30 -0
- package/dist/generated-docs/video-player.md +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles/density.css +274 -0
- package/dist/styles/index.css +77 -0
- package/dist/styles/motion.css +56 -0
- package/dist/styles/surfaces/base.css +103 -0
- package/dist/styles/surfaces/bevel.css +71 -0
- package/dist/styles/surfaces/cartoon.css +27 -0
- package/dist/styles/surfaces/glass.css +127 -0
- package/dist/styles/themes.css +351 -0
- package/dist/styles/typography.css +28 -0
- package/dist/ui/accordion/accordion-content.svelte +15 -0
- package/dist/ui/accordion/accordion-content.svelte.d.ts +4 -0
- package/dist/ui/accordion/accordion-header.svelte +8 -0
- package/dist/ui/accordion/accordion-header.svelte.d.ts +4 -0
- package/dist/ui/accordion/accordion-item.svelte +12 -0
- package/dist/ui/accordion/accordion-item.svelte.d.ts +4 -0
- package/dist/ui/accordion/accordion-trigger.svelte +24 -0
- package/dist/ui/accordion/accordion-trigger.svelte.d.ts +5 -0
- package/dist/ui/accordion/accordion.svelte +8 -0
- package/dist/ui/accordion/accordion.svelte.d.ts +4 -0
- package/dist/ui/accordion/index.d.ts +5 -0
- package/dist/ui/accordion/index.js +5 -0
- package/dist/ui/alert-dialog/alert-dialog-action.svelte +41 -0
- package/dist/ui/alert-dialog/alert-dialog-action.svelte.d.ts +9 -0
- package/dist/ui/alert-dialog/alert-dialog-cancel.svelte +39 -0
- package/dist/ui/alert-dialog/alert-dialog-cancel.svelte.d.ts +9 -0
- package/dist/ui/alert-dialog/alert-dialog-content.svelte +25 -0
- package/dist/ui/alert-dialog/alert-dialog-content.svelte.d.ts +4 -0
- package/dist/ui/alert-dialog/alert-dialog-description.svelte +16 -0
- package/dist/ui/alert-dialog/alert-dialog-description.svelte.d.ts +4 -0
- package/dist/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
- package/dist/ui/alert-dialog/alert-dialog-overlay.svelte.d.ts +4 -0
- package/dist/ui/alert-dialog/alert-dialog-title.svelte +12 -0
- package/dist/ui/alert-dialog/alert-dialog-title.svelte.d.ts +4 -0
- package/dist/ui/alert-dialog/alert-dialog-trigger.svelte +28 -0
- package/dist/ui/alert-dialog/alert-dialog-trigger.svelte.d.ts +9 -0
- package/dist/ui/alert-dialog/index.d.ts +10 -0
- package/dist/ui/alert-dialog/index.js +10 -0
- package/dist/ui/aspect-ratio/aspect-ratio.svelte +19 -0
- package/dist/ui/aspect-ratio/aspect-ratio.svelte.d.ts +5 -0
- package/dist/ui/aspect-ratio/index.d.ts +1 -0
- package/dist/ui/aspect-ratio/index.js +1 -0
- package/dist/ui/avatar/avatar-fallback.svelte +22 -0
- package/dist/ui/avatar/avatar-fallback.svelte.d.ts +5 -0
- package/dist/ui/avatar/avatar-image.svelte +8 -0
- package/dist/ui/avatar/avatar-image.svelte.d.ts +4 -0
- package/dist/ui/avatar/avatar-root.svelte +22 -0
- package/dist/ui/avatar/avatar-root.svelte.d.ts +5 -0
- package/dist/ui/avatar/index.d.ts +3 -0
- package/dist/ui/avatar/index.js +3 -0
- package/dist/ui/badge/badge.svelte +40 -0
- package/dist/ui/badge/badge.svelte.d.ts +32 -0
- package/dist/ui/badge/index.d.ts +1 -0
- package/dist/ui/badge/index.js +1 -0
- package/dist/ui/bar/bar.svelte +45 -0
- package/dist/ui/bar/bar.svelte.d.ts +30 -0
- package/dist/ui/bar/index.d.ts +1 -0
- package/dist/ui/bar/index.js +1 -0
- package/dist/ui/button/button.svelte +53 -0
- package/dist/ui/button/button.svelte.d.ts +21 -0
- package/dist/ui/button/index.d.ts +12 -0
- package/dist/ui/button/index.js +3 -0
- package/dist/ui/button/variants.d.ts +70 -0
- package/dist/ui/button/variants.js +97 -0
- package/dist/ui/button-group/button-group.svelte +31 -0
- package/dist/ui/button-group/button-group.svelte.d.ts +10 -0
- package/dist/ui/button-group/index.d.ts +1 -0
- package/dist/ui/button-group/index.js +1 -0
- package/dist/ui/calendar/calendar-cell.svelte +8 -0
- package/dist/ui/calendar/calendar-cell.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-day.svelte +15 -0
- package/dist/ui/calendar/calendar-day.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-grid-body.svelte +7 -0
- package/dist/ui/calendar/calendar-grid-body.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-grid-head.svelte +7 -0
- package/dist/ui/calendar/calendar-grid-head.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-grid-row.svelte +8 -0
- package/dist/ui/calendar/calendar-grid-row.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-grid.svelte +8 -0
- package/dist/ui/calendar/calendar-grid.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-head-cell.svelte +15 -0
- package/dist/ui/calendar/calendar-head-cell.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-header.svelte +12 -0
- package/dist/ui/calendar/calendar-header.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-heading.svelte +8 -0
- package/dist/ui/calendar/calendar-heading.svelte.d.ts +4 -0
- package/dist/ui/calendar/calendar-next-button.svelte +30 -0
- package/dist/ui/calendar/calendar-next-button.svelte.d.ts +9 -0
- package/dist/ui/calendar/calendar-prev-button.svelte +30 -0
- package/dist/ui/calendar/calendar-prev-button.svelte.d.ts +9 -0
- package/dist/ui/calendar/calendar-root.svelte +30 -0
- package/dist/ui/calendar/calendar-root.svelte.d.ts +4 -0
- package/dist/ui/calendar/index.d.ts +12 -0
- package/dist/ui/calendar/index.js +12 -0
- package/dist/ui/card/card.svelte +159 -0
- package/dist/ui/card/card.svelte.d.ts +63 -0
- package/dist/ui/card/index.d.ts +1 -0
- package/dist/ui/card/index.js +1 -0
- package/dist/ui/checkbox/checkbox-group-label.svelte +12 -0
- package/dist/ui/checkbox/checkbox-group-label.svelte.d.ts +4 -0
- package/dist/ui/checkbox/checkbox-group.svelte +18 -0
- package/dist/ui/checkbox/checkbox-group.svelte.d.ts +4 -0
- package/dist/ui/checkbox/checkbox.svelte +33 -0
- package/dist/ui/checkbox/checkbox.svelte.d.ts +4 -0
- package/dist/ui/checkbox/index.d.ts +3 -0
- package/dist/ui/checkbox/index.js +3 -0
- package/dist/ui/chip/chip.svelte +66 -0
- package/dist/ui/chip/chip.svelte.d.ts +26 -0
- package/dist/ui/chip/index.d.ts +1 -0
- package/dist/ui/chip/index.js +1 -0
- package/dist/ui/code-block/code-block.svelte +58 -0
- package/dist/ui/code-block/code-block.svelte.d.ts +11 -0
- package/dist/ui/code-block/index.d.ts +1 -0
- package/dist/ui/code-block/index.js +1 -0
- package/dist/ui/collapsible/collapsible-content.svelte +15 -0
- package/dist/ui/collapsible/collapsible-content.svelte.d.ts +5 -0
- package/dist/ui/collapsible/collapsible-trigger.svelte +22 -0
- package/dist/ui/collapsible/collapsible-trigger.svelte.d.ts +5 -0
- package/dist/ui/collapsible/index.d.ts +4 -0
- package/dist/ui/collapsible/index.js +4 -0
- package/dist/ui/combobox/combobox-content.svelte +25 -0
- package/dist/ui/combobox/combobox-content.svelte.d.ts +5 -0
- package/dist/ui/combobox/combobox-empty.svelte +22 -0
- package/dist/ui/combobox/combobox-empty.svelte.d.ts +8 -0
- package/dist/ui/combobox/combobox-group-heading.svelte +19 -0
- package/dist/ui/combobox/combobox-group-heading.svelte.d.ts +5 -0
- package/dist/ui/combobox/combobox-group.svelte +8 -0
- package/dist/ui/combobox/combobox-group.svelte.d.ts +4 -0
- package/dist/ui/combobox/combobox-input.svelte +15 -0
- package/dist/ui/combobox/combobox-input.svelte.d.ts +4 -0
- package/dist/ui/combobox/combobox-item.svelte +17 -0
- package/dist/ui/combobox/combobox-item.svelte.d.ts +4 -0
- package/dist/ui/combobox/combobox-trigger.svelte +27 -0
- package/dist/ui/combobox/combobox-trigger.svelte.d.ts +5 -0
- package/dist/ui/combobox/index.d.ts +10 -0
- package/dist/ui/combobox/index.js +10 -0
- package/dist/ui/command/command-empty.svelte +15 -0
- package/dist/ui/command/command-empty.svelte.d.ts +4 -0
- package/dist/ui/command/command-group-heading.svelte +16 -0
- package/dist/ui/command/command-group-heading.svelte.d.ts +4 -0
- package/dist/ui/command/command-group-items.svelte +12 -0
- package/dist/ui/command/command-group-items.svelte.d.ts +4 -0
- package/dist/ui/command/command-group.svelte +7 -0
- package/dist/ui/command/command-group.svelte.d.ts +4 -0
- package/dist/ui/command/command-input.svelte +45 -0
- package/dist/ui/command/command-input.svelte.d.ts +10 -0
- package/dist/ui/command/command-item.svelte +15 -0
- package/dist/ui/command/command-item.svelte.d.ts +4 -0
- package/dist/ui/command/command-link-item.svelte +15 -0
- package/dist/ui/command/command-link-item.svelte.d.ts +4 -0
- package/dist/ui/command/command-list.svelte +12 -0
- package/dist/ui/command/command-list.svelte.d.ts +4 -0
- package/dist/ui/command/command-loading.svelte +15 -0
- package/dist/ui/command/command-loading.svelte.d.ts +4 -0
- package/dist/ui/command/command-root.svelte +21 -0
- package/dist/ui/command/command-root.svelte.d.ts +4 -0
- package/dist/ui/command/command-separator.svelte +12 -0
- package/dist/ui/command/command-separator.svelte.d.ts +4 -0
- package/dist/ui/command/command-viewport.svelte +8 -0
- package/dist/ui/command/command-viewport.svelte.d.ts +4 -0
- package/dist/ui/command/index.d.ts +12 -0
- package/dist/ui/command/index.js +12 -0
- package/dist/ui/context-menu/context-menu-checkbox-item.svelte +21 -0
- package/dist/ui/context-menu/context-menu-checkbox-item.svelte.d.ts +4 -0
- package/dist/ui/context-menu/context-menu-content.svelte +27 -0
- package/dist/ui/context-menu/context-menu-content.svelte.d.ts +4 -0
- package/dist/ui/context-menu/context-menu-group-heading.svelte +16 -0
- package/dist/ui/context-menu/context-menu-group-heading.svelte.d.ts +4 -0
- package/dist/ui/context-menu/context-menu-item.svelte +22 -0
- package/dist/ui/context-menu/context-menu-item.svelte.d.ts +5 -0
- package/dist/ui/context-menu/context-menu-radio-item.svelte +19 -0
- package/dist/ui/context-menu/context-menu-radio-item.svelte.d.ts +4 -0
- package/dist/ui/context-menu/context-menu-separator.svelte +16 -0
- package/dist/ui/context-menu/context-menu-separator.svelte.d.ts +4 -0
- package/dist/ui/context-menu/context-menu-sub-content.svelte +26 -0
- package/dist/ui/context-menu/context-menu-sub-content.svelte.d.ts +5 -0
- package/dist/ui/context-menu/context-menu-sub-trigger.svelte +24 -0
- package/dist/ui/context-menu/context-menu-sub-trigger.svelte.d.ts +5 -0
- package/dist/ui/context-menu/context-menu-trigger.svelte +12 -0
- package/dist/ui/context-menu/context-menu-trigger.svelte.d.ts +4 -0
- package/dist/ui/context-menu/index.d.ts +14 -0
- package/dist/ui/context-menu/index.js +14 -0
- package/dist/ui/date-field/date-field-input.svelte +15 -0
- package/dist/ui/date-field/date-field-input.svelte.d.ts +4 -0
- package/dist/ui/date-field/date-field-label.svelte +8 -0
- package/dist/ui/date-field/date-field-label.svelte.d.ts +4 -0
- package/dist/ui/date-field/date-field-root.svelte +11 -0
- package/dist/ui/date-field/date-field-root.svelte.d.ts +3 -0
- package/dist/ui/date-field/date-field-segment.svelte +15 -0
- package/dist/ui/date-field/date-field-segment.svelte.d.ts +4 -0
- package/dist/ui/date-field/index.d.ts +4 -0
- package/dist/ui/date-field/index.js +4 -0
- package/dist/ui/date-picker/date-picker-calendar.svelte +16 -0
- package/dist/ui/date-picker/date-picker-calendar.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-cell.svelte +8 -0
- package/dist/ui/date-picker/date-picker-cell.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-content.svelte +25 -0
- package/dist/ui/date-picker/date-picker-content.svelte.d.ts +5 -0
- package/dist/ui/date-picker/date-picker-day.svelte +15 -0
- package/dist/ui/date-picker/date-picker-day.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-grid-body.svelte +11 -0
- package/dist/ui/date-picker/date-picker-grid-body.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-grid-head.svelte +11 -0
- package/dist/ui/date-picker/date-picker-grid-head.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-grid-row.svelte +8 -0
- package/dist/ui/date-picker/date-picker-grid-row.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-grid.svelte +8 -0
- package/dist/ui/date-picker/date-picker-grid.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-head-cell.svelte +19 -0
- package/dist/ui/date-picker/date-picker-head-cell.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-header.svelte +12 -0
- package/dist/ui/date-picker/date-picker-header.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-heading.svelte +12 -0
- package/dist/ui/date-picker/date-picker-heading.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-input.svelte +15 -0
- package/dist/ui/date-picker/date-picker-input.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-label.svelte +8 -0
- package/dist/ui/date-picker/date-picker-label.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-next-button.svelte +30 -0
- package/dist/ui/date-picker/date-picker-next-button.svelte.d.ts +9 -0
- package/dist/ui/date-picker/date-picker-prev-button.svelte +30 -0
- package/dist/ui/date-picker/date-picker-prev-button.svelte.d.ts +9 -0
- package/dist/ui/date-picker/date-picker-root.svelte +19 -0
- package/dist/ui/date-picker/date-picker-root.svelte.d.ts +3 -0
- package/dist/ui/date-picker/date-picker-segment.svelte +15 -0
- package/dist/ui/date-picker/date-picker-segment.svelte.d.ts +4 -0
- package/dist/ui/date-picker/date-picker-trigger.svelte +27 -0
- package/dist/ui/date-picker/date-picker-trigger.svelte.d.ts +5 -0
- package/dist/ui/date-picker/index.d.ts +18 -0
- package/dist/ui/date-picker/index.js +18 -0
- package/dist/ui/date-range-field/date-range-field-input.svelte +19 -0
- package/dist/ui/date-range-field/date-range-field-input.svelte.d.ts +4 -0
- package/dist/ui/date-range-field/date-range-field-label.svelte +12 -0
- package/dist/ui/date-range-field/date-range-field-label.svelte.d.ts +4 -0
- package/dist/ui/date-range-field/date-range-field-root.svelte +13 -0
- package/dist/ui/date-range-field/date-range-field-root.svelte.d.ts +4 -0
- package/dist/ui/date-range-field/date-range-field-segment.svelte +19 -0
- package/dist/ui/date-range-field/date-range-field-segment.svelte.d.ts +3 -0
- package/dist/ui/date-range-field/index.d.ts +4 -0
- package/dist/ui/date-range-field/index.js +4 -0
- package/dist/ui/date-range-picker/date-range-picker-calendar.svelte +16 -0
- package/dist/ui/date-range-picker/date-range-picker-calendar.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-cell.svelte +12 -0
- package/dist/ui/date-range-picker/date-range-picker-cell.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-content.svelte +25 -0
- package/dist/ui/date-range-picker/date-range-picker-content.svelte.d.ts +5 -0
- package/dist/ui/date-range-picker/date-range-picker-day.svelte +19 -0
- package/dist/ui/date-range-picker/date-range-picker-day.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-body.svelte +12 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-body.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-head.svelte +12 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-head.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-row.svelte +12 -0
- package/dist/ui/date-range-picker/date-range-picker-grid-row.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-grid.svelte +16 -0
- package/dist/ui/date-range-picker/date-range-picker-grid.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-head-cell.svelte +19 -0
- package/dist/ui/date-range-picker/date-range-picker-head-cell.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-header.svelte +16 -0
- package/dist/ui/date-range-picker/date-range-picker-header.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-heading.svelte +16 -0
- package/dist/ui/date-range-picker/date-range-picker-heading.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-input.svelte +19 -0
- package/dist/ui/date-range-picker/date-range-picker-input.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-label.svelte +12 -0
- package/dist/ui/date-range-picker/date-range-picker-label.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-next-button.svelte +30 -0
- package/dist/ui/date-range-picker/date-range-picker-next-button.svelte.d.ts +9 -0
- package/dist/ui/date-range-picker/date-range-picker-prev-button.svelte +30 -0
- package/dist/ui/date-range-picker/date-range-picker-prev-button.svelte.d.ts +9 -0
- package/dist/ui/date-range-picker/date-range-picker-root.svelte +19 -0
- package/dist/ui/date-range-picker/date-range-picker-root.svelte.d.ts +4 -0
- package/dist/ui/date-range-picker/date-range-picker-segment.svelte +19 -0
- package/dist/ui/date-range-picker/date-range-picker-segment.svelte.d.ts +3 -0
- package/dist/ui/date-range-picker/date-range-picker-trigger.svelte +27 -0
- package/dist/ui/date-range-picker/date-range-picker-trigger.svelte.d.ts +5 -0
- package/dist/ui/date-range-picker/index.d.ts +18 -0
- package/dist/ui/date-range-picker/index.js +18 -0
- package/dist/ui/dialog/dialog-close.svelte +39 -0
- package/dist/ui/dialog/dialog-close.svelte.d.ts +9 -0
- package/dist/ui/dialog/dialog-content.svelte +25 -0
- package/dist/ui/dialog/dialog-content.svelte.d.ts +5 -0
- package/dist/ui/dialog/dialog-description.svelte +12 -0
- package/dist/ui/dialog/dialog-description.svelte.d.ts +4 -0
- package/dist/ui/dialog/dialog-overlay.svelte +12 -0
- package/dist/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
- package/dist/ui/dialog/dialog-title.svelte +8 -0
- package/dist/ui/dialog/dialog-title.svelte.d.ts +4 -0
- package/dist/ui/dialog/dialog-trigger.svelte +28 -0
- package/dist/ui/dialog/dialog-trigger.svelte.d.ts +9 -0
- package/dist/ui/dialog/index.d.ts +9 -0
- package/dist/ui/dialog/index.js +9 -0
- package/dist/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +21 -0
- package/dist/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +4 -0
- package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/dist/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +5 -0
- package/dist/ui/dropdown-menu/dropdown-menu-group-heading.svelte +16 -0
- package/dist/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +4 -0
- package/dist/ui/dropdown-menu/dropdown-menu-item.svelte +22 -0
- package/dist/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +5 -0
- package/dist/ui/dropdown-menu/dropdown-menu-radio-item.svelte +19 -0
- package/dist/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
- package/dist/ui/dropdown-menu/dropdown-menu-separator.svelte +16 -0
- package/dist/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
- package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +26 -0
- package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +5 -0
- package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +24 -0
- package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +5 -0
- package/dist/ui/dropdown-menu/dropdown-menu-trigger.svelte +24 -0
- package/dist/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +5 -0
- package/dist/ui/dropdown-menu/index.d.ts +14 -0
- package/dist/ui/dropdown-menu/index.js +14 -0
- package/dist/ui/file-upload/file-upload.svelte +98 -0
- package/dist/ui/file-upload/file-upload.svelte.d.ts +8 -0
- package/dist/ui/file-upload/index.d.ts +1 -0
- package/dist/ui/file-upload/index.js +1 -0
- package/dist/ui/index.d.ts +58 -0
- package/dist/ui/index.js +62 -0
- package/dist/ui/input/index.d.ts +2 -0
- package/dist/ui/input/index.js +2 -0
- package/dist/ui/input/input.svelte +23 -0
- package/dist/ui/input/input.svelte.d.ts +7 -0
- package/dist/ui/input/textarea.svelte +23 -0
- package/dist/ui/input/textarea.svelte.d.ts +7 -0
- package/dist/ui/kv/index.d.ts +1 -0
- package/dist/ui/kv/index.js +1 -0
- package/dist/ui/kv/kv.svelte +33 -0
- package/dist/ui/kv/kv.svelte.d.ts +10 -0
- package/dist/ui/label/index.d.ts +1 -0
- package/dist/ui/label/index.js +1 -0
- package/dist/ui/label/label.svelte +15 -0
- package/dist/ui/label/label.svelte.d.ts +5 -0
- package/dist/ui/link-preview/index.d.ts +7 -0
- package/dist/ui/link-preview/index.js +7 -0
- package/dist/ui/link-preview/link-preview-content.svelte +26 -0
- package/dist/ui/link-preview/link-preview-content.svelte.d.ts +5 -0
- package/dist/ui/link-preview/link-preview-trigger.svelte +19 -0
- package/dist/ui/link-preview/link-preview-trigger.svelte.d.ts +5 -0
- package/dist/ui/menu/index.d.ts +1 -0
- package/dist/ui/menu/index.js +3 -0
- package/dist/ui/menubar/index.d.ts +16 -0
- package/dist/ui/menubar/index.js +16 -0
- package/dist/ui/menubar/menubar-checkbox-item.svelte +21 -0
- package/dist/ui/menubar/menubar-checkbox-item.svelte.d.ts +4 -0
- package/dist/ui/menubar/menubar-content.svelte +27 -0
- package/dist/ui/menubar/menubar-content.svelte.d.ts +5 -0
- package/dist/ui/menubar/menubar-group-heading.svelte +16 -0
- package/dist/ui/menubar/menubar-group-heading.svelte.d.ts +4 -0
- package/dist/ui/menubar/menubar-item.svelte +22 -0
- package/dist/ui/menubar/menubar-item.svelte.d.ts +5 -0
- package/dist/ui/menubar/menubar-radio-item.svelte +15 -0
- package/dist/ui/menubar/menubar-radio-item.svelte.d.ts +4 -0
- package/dist/ui/menubar/menubar-root.svelte +15 -0
- package/dist/ui/menubar/menubar-root.svelte.d.ts +4 -0
- package/dist/ui/menubar/menubar-separator.svelte +12 -0
- package/dist/ui/menubar/menubar-separator.svelte.d.ts +4 -0
- package/dist/ui/menubar/menubar-sub-content.svelte +26 -0
- package/dist/ui/menubar/menubar-sub-content.svelte.d.ts +5 -0
- package/dist/ui/menubar/menubar-sub-trigger.svelte +24 -0
- package/dist/ui/menubar/menubar-sub-trigger.svelte.d.ts +5 -0
- package/dist/ui/menubar/menubar-trigger.svelte +22 -0
- package/dist/ui/menubar/menubar-trigger.svelte.d.ts +5 -0
- package/dist/ui/meter/index.d.ts +1 -0
- package/dist/ui/meter/index.js +1 -0
- package/dist/ui/meter/meter.svelte +32 -0
- package/dist/ui/meter/meter.svelte.d.ts +4 -0
- package/dist/ui/mode/index.d.ts +1 -0
- package/dist/ui/mode/index.js +1 -0
- package/dist/ui/mode/mode.svelte +61 -0
- package/dist/ui/mode/mode.svelte.d.ts +18 -0
- package/dist/ui/navigation-menu/index.d.ts +10 -0
- package/dist/ui/navigation-menu/index.js +10 -0
- package/dist/ui/pagination/index.d.ts +4 -0
- package/dist/ui/pagination/index.js +4 -0
- package/dist/ui/pagination/pagination-next-button.svelte +30 -0
- package/dist/ui/pagination/pagination-next-button.svelte.d.ts +9 -0
- package/dist/ui/pagination/pagination-page.svelte +32 -0
- package/dist/ui/pagination/pagination-page.svelte.d.ts +9 -0
- package/dist/ui/pagination/pagination-prev-button.svelte +30 -0
- package/dist/ui/pagination/pagination-prev-button.svelte.d.ts +9 -0
- package/dist/ui/pagination/pagination-root.svelte +18 -0
- package/dist/ui/pagination/pagination-root.svelte.d.ts +4 -0
- package/dist/ui/pane/index.d.ts +1 -0
- package/dist/ui/pane/index.js +1 -0
- package/dist/ui/pane/pane.svelte +145 -0
- package/dist/ui/pane/pane.svelte.d.ts +34 -0
- package/dist/ui/pin-input/index.d.ts +2 -0
- package/dist/ui/pin-input/index.js +2 -0
- package/dist/ui/pin-input/pin-input-cell.svelte +30 -0
- package/dist/ui/pin-input/pin-input-cell.svelte.d.ts +4 -0
- package/dist/ui/pin-input/pin-input-root.svelte +18 -0
- package/dist/ui/pin-input/pin-input-root.svelte.d.ts +4 -0
- package/dist/ui/popover/index.d.ts +7 -0
- package/dist/ui/popover/index.js +7 -0
- package/dist/ui/popover/popover-close.svelte +27 -0
- package/dist/ui/popover/popover-close.svelte.d.ts +9 -0
- package/dist/ui/popover/popover-content.svelte +24 -0
- package/dist/ui/popover/popover-content.svelte.d.ts +5 -0
- package/dist/ui/popover/popover-trigger.svelte +41 -0
- package/dist/ui/popover/popover-trigger.svelte.d.ts +9 -0
- package/dist/ui/progress/index.d.ts +1 -0
- package/dist/ui/progress/index.js +1 -0
- package/dist/ui/progress/progress.svelte +47 -0
- package/dist/ui/progress/progress.svelte.d.ts +4 -0
- package/dist/ui/provider/axes.d.ts +74 -0
- package/dist/ui/provider/axes.js +70 -0
- package/dist/ui/provider/index.d.ts +6 -0
- package/dist/ui/provider/index.js +6 -0
- package/dist/ui/provider/modes.d.ts +12 -0
- package/dist/ui/provider/modes.js +14 -0
- package/dist/ui/provider/surfaces.d.ts +45 -0
- package/dist/ui/provider/surfaces.js +24 -0
- package/dist/ui/radio-group/index.d.ts +2 -0
- package/dist/ui/radio-group/index.js +2 -0
- package/dist/ui/radio-group/radio-group-item.svelte +19 -0
- package/dist/ui/radio-group/radio-group-item.svelte.d.ts +4 -0
- package/dist/ui/radio-group/radio-group-root.svelte +18 -0
- package/dist/ui/radio-group/radio-group-root.svelte.d.ts +4 -0
- package/dist/ui/range-calendar/index.d.ts +15 -0
- package/dist/ui/range-calendar/index.js +15 -0
- package/dist/ui/rating-group/index.d.ts +2 -0
- package/dist/ui/rating-group/index.js +2 -0
- package/dist/ui/rating-group/rating-group-item.svelte +17 -0
- package/dist/ui/rating-group/rating-group-item.svelte.d.ts +4 -0
- package/dist/ui/rating-group/rating-group-root.svelte +20 -0
- package/dist/ui/rating-group/rating-group-root.svelte.d.ts +4 -0
- package/dist/ui/scroll-area/index.d.ts +5 -0
- package/dist/ui/scroll-area/index.js +5 -0
- package/dist/ui/scroll-area/scroll-area-corner.svelte +7 -0
- package/dist/ui/scroll-area/scroll-area-corner.svelte.d.ts +4 -0
- package/dist/ui/scroll-area/scroll-area-scrollbar.svelte +19 -0
- package/dist/ui/scroll-area/scroll-area-scrollbar.svelte.d.ts +4 -0
- package/dist/ui/scroll-area/scroll-area-thumb.svelte +12 -0
- package/dist/ui/scroll-area/scroll-area-thumb.svelte.d.ts +4 -0
- package/dist/ui/scroll-area/scroll-area-viewport.svelte +12 -0
- package/dist/ui/scroll-area/scroll-area-viewport.svelte.d.ts +4 -0
- package/dist/ui/scroll-area/scroll-area.svelte +18 -0
- package/dist/ui/scroll-area/scroll-area.svelte.d.ts +4 -0
- package/dist/ui/select/index.d.ts +6 -0
- package/dist/ui/select/index.js +6 -0
- package/dist/ui/select/select-content.svelte +25 -0
- package/dist/ui/select/select-content.svelte.d.ts +5 -0
- package/dist/ui/select/select-item.svelte +17 -0
- package/dist/ui/select/select-item.svelte.d.ts +4 -0
- package/dist/ui/select/select-trigger.svelte +24 -0
- package/dist/ui/select/select-trigger.svelte.d.ts +5 -0
- package/dist/ui/select/select-value.svelte +71 -0
- package/dist/ui/select/select-value.svelte.d.ts +9 -0
- package/dist/ui/separator/index.d.ts +1 -0
- package/dist/ui/separator/index.js +1 -0
- package/dist/ui/separator/separator.svelte +31 -0
- package/dist/ui/separator/separator.svelte.d.ts +8 -0
- package/dist/ui/sheet/index.d.ts +2 -0
- package/dist/ui/sheet/index.js +5 -0
- package/dist/ui/sheet/sheet-content.svelte +41 -0
- package/dist/ui/sheet/sheet-content.svelte.d.ts +31 -0
- package/dist/ui/skeleton/index.d.ts +1 -0
- package/dist/ui/skeleton/index.js +1 -0
- package/dist/ui/skeleton/skeleton.svelte +43 -0
- package/dist/ui/skeleton/skeleton.svelte.d.ts +27 -0
- package/dist/ui/slider/index.d.ts +6 -0
- package/dist/ui/slider/index.js +6 -0
- package/dist/ui/slider/slider-range.svelte +40 -0
- package/dist/ui/slider/slider-range.svelte.d.ts +13 -0
- package/dist/ui/slider/slider-root.svelte +29 -0
- package/dist/ui/slider/slider-root.svelte.d.ts +4 -0
- package/dist/ui/slider/slider-thumb-label.svelte +12 -0
- package/dist/ui/slider/slider-thumb-label.svelte.d.ts +4 -0
- package/dist/ui/slider/slider-thumb.svelte +15 -0
- package/dist/ui/slider/slider-thumb.svelte.d.ts +4 -0
- package/dist/ui/slider/slider-tick-label.svelte +12 -0
- package/dist/ui/slider/slider-tick-label.svelte.d.ts +4 -0
- package/dist/ui/slider/slider-tick.svelte +15 -0
- package/dist/ui/slider/slider-tick.svelte.d.ts +4 -0
- package/dist/ui/switch/index.d.ts +1 -0
- package/dist/ui/switch/index.js +1 -0
- package/dist/ui/switch/switch.svelte +36 -0
- package/dist/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/ui/table/index.d.ts +6 -0
- package/dist/ui/table/index.js +6 -0
- package/dist/ui/table/table-body.svelte +18 -0
- package/dist/ui/table/table-body.svelte.d.ts +9 -0
- package/dist/ui/table/table-cell.svelte +26 -0
- package/dist/ui/table/table-cell.svelte.d.ts +9 -0
- package/dist/ui/table/table-head-cell.svelte +26 -0
- package/dist/ui/table/table-head-cell.svelte.d.ts +9 -0
- package/dist/ui/table/table-head.svelte +18 -0
- package/dist/ui/table/table-head.svelte.d.ts +9 -0
- package/dist/ui/table/table-root.svelte +36 -0
- package/dist/ui/table/table-root.svelte.d.ts +17 -0
- package/dist/ui/table/table-row.svelte +26 -0
- package/dist/ui/table/table-row.svelte.d.ts +9 -0
- package/dist/ui/tabs/index.d.ts +4 -0
- package/dist/ui/tabs/index.js +4 -0
- package/dist/ui/tabs/tabs-content.svelte +7 -0
- package/dist/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/ui/tabs/tabs-list.svelte +7 -0
- package/dist/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/ui/tabs/tabs-trigger.svelte +7 -0
- package/dist/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/ui/tabs/tabs.svelte +7 -0
- package/dist/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/ui/time-field/index.d.ts +4 -0
- package/dist/ui/time-field/index.js +4 -0
- package/dist/ui/time-field/time-field-input.svelte +15 -0
- package/dist/ui/time-field/time-field-input.svelte.d.ts +4 -0
- package/dist/ui/time-field/time-field-label.svelte +8 -0
- package/dist/ui/time-field/time-field-label.svelte.d.ts +4 -0
- package/dist/ui/time-field/time-field-root.svelte +11 -0
- package/dist/ui/time-field/time-field-root.svelte.d.ts +4 -0
- package/dist/ui/time-field/time-field-segment.svelte +15 -0
- package/dist/ui/time-field/time-field-segment.svelte.d.ts +4 -0
- package/dist/ui/time-range-field/index.d.ts +4 -0
- package/dist/ui/time-range-field/index.js +4 -0
- package/dist/ui/time-range-field/time-range-field-input.svelte +19 -0
- package/dist/ui/time-range-field/time-range-field-input.svelte.d.ts +4 -0
- package/dist/ui/time-range-field/time-range-field-label.svelte +12 -0
- package/dist/ui/time-range-field/time-range-field-label.svelte.d.ts +4 -0
- package/dist/ui/time-range-field/time-range-field-root.svelte +13 -0
- package/dist/ui/time-range-field/time-range-field-root.svelte.d.ts +4 -0
- package/dist/ui/time-range-field/time-range-field-segment.svelte +19 -0
- package/dist/ui/time-range-field/time-range-field-segment.svelte.d.ts +3 -0
- package/dist/ui/toggle/index.d.ts +1 -0
- package/dist/ui/toggle/index.js +1 -0
- package/dist/ui/toggle/toggle.svelte +27 -0
- package/dist/ui/toggle/toggle.svelte.d.ts +9 -0
- package/dist/ui/toggle-group/index.d.ts +2 -0
- package/dist/ui/toggle-group/index.js +2 -0
- package/dist/ui/toggle-group/toggle-group-item.svelte +17 -0
- package/dist/ui/toggle-group/toggle-group-item.svelte.d.ts +4 -0
- package/dist/ui/toggle-group/toggle-group-root.svelte +19 -0
- package/dist/ui/toggle-group/toggle-group-root.svelte.d.ts +5 -0
- package/dist/ui/toolbar/index.d.ts +6 -0
- package/dist/ui/toolbar/index.js +6 -0
- package/dist/ui/tooltip/index.d.ts +7 -0
- package/dist/ui/tooltip/index.js +7 -0
- package/dist/ui/tooltip/tooltip-content.svelte +26 -0
- package/dist/ui/tooltip/tooltip-content.svelte.d.ts +5 -0
- package/dist/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
- package/dist/ui/video-player/index.d.ts +1 -0
- package/dist/ui/video-player/index.js +1 -0
- package/dist/ui/video-player/video-player.svelte +1377 -0
- package/dist/ui/video-player/video-player.svelte.d.ts +107 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.js +24 -0
- package/package.json +115 -0
- package/readme +127 -0
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { browser } from '$app/environment';
|
|
5
|
+
import {
|
|
6
|
+
ContextMenu as BitsContextMenu,
|
|
7
|
+
DropdownMenu as BitsDropdownMenu,
|
|
8
|
+
Slider as BitsSlider
|
|
9
|
+
} from 'bits-ui';
|
|
10
|
+
import {
|
|
11
|
+
MediaRemoteControl,
|
|
12
|
+
type MediaPlayerElement,
|
|
13
|
+
type MediaFullscreenRequestTarget,
|
|
14
|
+
type MediaState
|
|
15
|
+
} from 'vidstack';
|
|
16
|
+
import { Bar } from '../bar';
|
|
17
|
+
import { Badge } from '../badge';
|
|
18
|
+
import { Button } from '../button';
|
|
19
|
+
import * as ContextMenu from '../context-menu';
|
|
20
|
+
import * as DropdownMenu from '../dropdown-menu';
|
|
21
|
+
import Play from '@lucide/svelte/icons/play';
|
|
22
|
+
import Pause from '@lucide/svelte/icons/pause';
|
|
23
|
+
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
24
|
+
import Rewind from '@lucide/svelte/icons/rewind';
|
|
25
|
+
import FastForward from '@lucide/svelte/icons/fast-forward';
|
|
26
|
+
import Volume2 from '@lucide/svelte/icons/volume-2';
|
|
27
|
+
import Volume1 from '@lucide/svelte/icons/volume-1';
|
|
28
|
+
import VolumeX from '@lucide/svelte/icons/volume-x';
|
|
29
|
+
import Gauge from '@lucide/svelte/icons/gauge';
|
|
30
|
+
import Settings from '@lucide/svelte/icons/settings';
|
|
31
|
+
import PictureInPicture from '@lucide/svelte/icons/picture-in-picture';
|
|
32
|
+
import PictureInPicture2 from '@lucide/svelte/icons/picture-in-picture-2';
|
|
33
|
+
import Maximize from '@lucide/svelte/icons/maximize';
|
|
34
|
+
import Minimize from '@lucide/svelte/icons/minimize';
|
|
35
|
+
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
|
36
|
+
import Check from '@lucide/svelte/icons/check';
|
|
37
|
+
import Download from '@lucide/svelte/icons/download';
|
|
38
|
+
import { cn } from '../../utils/index.js';
|
|
39
|
+
|
|
40
|
+
// Only the engine + outlet are registered, so vidstack ships no UI chrome
|
|
41
|
+
// or default stylesheet — every control is built from this library.
|
|
42
|
+
// Wait for both custom elements to be defined before wiring remote control
|
|
43
|
+
// and subscriptions; otherwise, mount can race and leave controls inert.
|
|
44
|
+
let elementsDefined = $state(!browser);
|
|
45
|
+
if (browser) {
|
|
46
|
+
void Promise.all([
|
|
47
|
+
import('vidstack/define/media-player'),
|
|
48
|
+
import('vidstack/define/media-outlet'),
|
|
49
|
+
import('vidstack/define/media-gesture')
|
|
50
|
+
]).then(() => {
|
|
51
|
+
elementsDefined = true;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const FULLSCREEN_TARGET: MediaFullscreenRequestTarget = 'prefer-media';
|
|
56
|
+
const toScalar = (v: number | number[]) => (Array.isArray(v) ? (v[0] ?? 0) : v);
|
|
57
|
+
|
|
58
|
+
type MediaEventWithDetail<T> = CustomEvent<T> & { detail: T };
|
|
59
|
+
type ChapterTrack = {
|
|
60
|
+
src: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
srclang?: string;
|
|
63
|
+
default?: boolean;
|
|
64
|
+
};
|
|
65
|
+
type ChapterCue = {
|
|
66
|
+
start: number;
|
|
67
|
+
end: number;
|
|
68
|
+
text: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let {
|
|
72
|
+
ref = $bindable(null),
|
|
73
|
+
src,
|
|
74
|
+
poster,
|
|
75
|
+
chapters,
|
|
76
|
+
preload = 'metadata',
|
|
77
|
+
load = 'visible',
|
|
78
|
+
muted = false,
|
|
79
|
+
autoplay = false,
|
|
80
|
+
loop = false,
|
|
81
|
+
playsinline = true,
|
|
82
|
+
crossorigin,
|
|
83
|
+
initialVolume,
|
|
84
|
+
initialPlaybackRate,
|
|
85
|
+
aspectRatio,
|
|
86
|
+
viewType,
|
|
87
|
+
streamType,
|
|
88
|
+
fullscreenOrientation,
|
|
89
|
+
controls = false,
|
|
90
|
+
play: showPlay = true,
|
|
91
|
+
time: showTime = true,
|
|
92
|
+
skip = false,
|
|
93
|
+
volume: showVolume = false,
|
|
94
|
+
volumeOrientation = 'horizontal',
|
|
95
|
+
speed: showSpeed = false,
|
|
96
|
+
quality: showQuality = false,
|
|
97
|
+
pip: showPip = false,
|
|
98
|
+
fullscreen: showFullscreen = false,
|
|
99
|
+
seekPreview: showSeekPreview = false,
|
|
100
|
+
contextMenu: showContextMenu = false,
|
|
101
|
+
autoHide = true,
|
|
102
|
+
idleDelay = 500,
|
|
103
|
+
idleWhilePaused = false,
|
|
104
|
+
mobileTimelineOnly = true,
|
|
105
|
+
keyDisabled = false,
|
|
106
|
+
keyTarget,
|
|
107
|
+
keyShortcuts,
|
|
108
|
+
onplay,
|
|
109
|
+
onpause,
|
|
110
|
+
onended,
|
|
111
|
+
onplaying,
|
|
112
|
+
onseeked,
|
|
113
|
+
onseeking,
|
|
114
|
+
oncanplay,
|
|
115
|
+
ontimeupdate,
|
|
116
|
+
onvolumechange,
|
|
117
|
+
onratechange,
|
|
118
|
+
ondurationchange,
|
|
119
|
+
onloadedmetadata,
|
|
120
|
+
onfullscreenchange,
|
|
121
|
+
onpipchange,
|
|
122
|
+
onerror,
|
|
123
|
+
children,
|
|
124
|
+
class: className
|
|
125
|
+
}: {
|
|
126
|
+
ref?: HTMLElement | null;
|
|
127
|
+
src: string;
|
|
128
|
+
poster?: string;
|
|
129
|
+
/** Optional WebVTT chapters track. Pass a URL or full track descriptor. */
|
|
130
|
+
chapters?: string | ChapterTrack;
|
|
131
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
132
|
+
load?: 'eager' | 'idle' | 'visible' | 'custom';
|
|
133
|
+
muted?: boolean;
|
|
134
|
+
autoplay?: boolean;
|
|
135
|
+
loop?: boolean;
|
|
136
|
+
playsinline?: boolean;
|
|
137
|
+
crossorigin?: 'anonymous' | 'use-credentials';
|
|
138
|
+
/** Initial volume (0–1). */
|
|
139
|
+
initialVolume?: number;
|
|
140
|
+
/** Initial playback rate (e.g. 1, 1.5, 2). */
|
|
141
|
+
initialPlaybackRate?: number;
|
|
142
|
+
/** Force a fixed aspect ratio as width/height, e.g. 16/9. */
|
|
143
|
+
aspectRatio?: number;
|
|
144
|
+
viewType?: 'unknown' | 'audio' | 'video';
|
|
145
|
+
streamType?: 'unknown' | 'on-demand' | 'live' | 'live:dvr' | 'll-live' | 'll-live:dvr';
|
|
146
|
+
fullscreenOrientation?:
|
|
147
|
+
| 'any'
|
|
148
|
+
| 'natural'
|
|
149
|
+
| 'landscape'
|
|
150
|
+
| 'portrait'
|
|
151
|
+
| 'portrait-primary'
|
|
152
|
+
| 'portrait-secondary'
|
|
153
|
+
| 'landscape-primary'
|
|
154
|
+
| 'landscape-secondary';
|
|
155
|
+
/** Fall back to the provider's native controls instead of the custom chrome. */
|
|
156
|
+
controls?: boolean;
|
|
157
|
+
/** Show the play / pause button in the custom chrome. */
|
|
158
|
+
play?: boolean;
|
|
159
|
+
/** Show the elapsed and duration time badges. */
|
|
160
|
+
time?: boolean;
|
|
161
|
+
/** Show ±N-second skip buttons. `true` defaults to 10s; pass a number to override. */
|
|
162
|
+
skip?: boolean | number;
|
|
163
|
+
/** Show the mute toggle and volume slider. */
|
|
164
|
+
volume?: boolean;
|
|
165
|
+
/** Layout direction for the volume slider. */
|
|
166
|
+
volumeOrientation?: 'horizontal' | 'vertical';
|
|
167
|
+
/** Show the playback-speed dropdown. */
|
|
168
|
+
speed?: boolean;
|
|
169
|
+
/** Show the quality dropdown. Auto-hidden when the source exposes no quality options (e.g. plain MP4). */
|
|
170
|
+
quality?: boolean;
|
|
171
|
+
/** Show the picture-in-picture toggle. */
|
|
172
|
+
pip?: boolean;
|
|
173
|
+
/** Show the fullscreen toggle. */
|
|
174
|
+
fullscreen?: boolean;
|
|
175
|
+
/** Show a hover preview above the seek bar with the cursor's time and (if available) the chapter at that point. */
|
|
176
|
+
seekPreview?: boolean;
|
|
177
|
+
/** Enable a right-click context menu mirroring the enabled chrome plus a download shortcut. */
|
|
178
|
+
contextMenu?: boolean;
|
|
179
|
+
/** Whether the controls bar fades when the user is idle during playback. Set `false` to keep it visible. */
|
|
180
|
+
autoHide?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Delay (ms) of inactivity during playback before vidstack flips `userIdle` to true.
|
|
183
|
+
* Maps to the player's `user-idle-delay` attribute. Defaults to 500 ms (vidstack's
|
|
184
|
+
* own default is 2000 ms).
|
|
185
|
+
*/
|
|
186
|
+
idleDelay?: number;
|
|
187
|
+
/**
|
|
188
|
+
* Vidstack only fades the bar while playing (deliberately — its `MediaUserController`
|
|
189
|
+
* bails out of idle tracking on `pause`). Set this to `true` to also hide the bar
|
|
190
|
+
* while paused after `idleDelay` ms of pointer/keyboard inactivity.
|
|
191
|
+
*/
|
|
192
|
+
idleWhilePaused?: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* On narrow viewports / coarse-pointer devices, auto-collapse to a "timeline-only"
|
|
195
|
+
* layout — every chrome control suppresses regardless of its individual prop, leaving
|
|
196
|
+
* just the seek bar. Set `false` to honor the chrome props as-is on mobile too.
|
|
197
|
+
*/
|
|
198
|
+
mobileTimelineOnly?: boolean;
|
|
199
|
+
keyDisabled?: boolean;
|
|
200
|
+
keyTarget?: 'document' | 'player';
|
|
201
|
+
keyShortcuts?: Record<string, string>;
|
|
202
|
+
onplay?: (e: Event) => void;
|
|
203
|
+
onpause?: (e: Event) => void;
|
|
204
|
+
onended?: (e: Event) => void;
|
|
205
|
+
onplaying?: (e: Event) => void;
|
|
206
|
+
onseeked?: (e: MediaEventWithDetail<number>) => void;
|
|
207
|
+
onseeking?: (e: MediaEventWithDetail<number>) => void;
|
|
208
|
+
oncanplay?: (e: Event) => void;
|
|
209
|
+
ontimeupdate?: (e: MediaEventWithDetail<{ currentTime: number; played: TimeRanges }>) => void;
|
|
210
|
+
onvolumechange?: (e: MediaEventWithDetail<{ volume: number; muted: boolean }>) => void;
|
|
211
|
+
onratechange?: (e: MediaEventWithDetail<number>) => void;
|
|
212
|
+
ondurationchange?: (e: MediaEventWithDetail<number>) => void;
|
|
213
|
+
onloadedmetadata?: (e: Event) => void;
|
|
214
|
+
onfullscreenchange?: (e: MediaEventWithDetail<boolean>) => void;
|
|
215
|
+
onpipchange?: (e: MediaEventWithDetail<boolean>) => void;
|
|
216
|
+
onerror?: (e: Event) => void;
|
|
217
|
+
/** Optional override for the entire controls layer — replaces the default chrome. */
|
|
218
|
+
children?: Snippet;
|
|
219
|
+
class?: string;
|
|
220
|
+
} = $props();
|
|
221
|
+
|
|
222
|
+
let paused = $state(true);
|
|
223
|
+
let ended = $state(false);
|
|
224
|
+
let started = $state(false);
|
|
225
|
+
let currentTime = $state(0);
|
|
226
|
+
let duration = $state(0);
|
|
227
|
+
let volumeState = $state(1);
|
|
228
|
+
let mutedState = $state(false);
|
|
229
|
+
let rateState = $state(1);
|
|
230
|
+
let waiting = $state(false);
|
|
231
|
+
let fullscreenState = $state(false);
|
|
232
|
+
let pipState = $state(false);
|
|
233
|
+
let live = $state(false);
|
|
234
|
+
let canSeek = $state(true);
|
|
235
|
+
let qualities = $state<MediaState['qualities']>([]);
|
|
236
|
+
let currentQuality = $state<MediaState['quality']>(null);
|
|
237
|
+
let autoQuality = $state(false);
|
|
238
|
+
let canSetQuality = $state(false);
|
|
239
|
+
// Mirrored from vidstack's subscribe — container-relative breakpoint is
|
|
240
|
+
// what makes an embedded 400 px player collapse to mobile mode too,
|
|
241
|
+
// not just phone-width viewports.
|
|
242
|
+
let touchPointer = $state(false);
|
|
243
|
+
let breakpointX = $state<MediaState['breakpointX']>('lg');
|
|
244
|
+
// Vidstack reports whether the current provider/browser combo actually
|
|
245
|
+
// supports fullscreen and PiP — auto-hide those buttons rather than
|
|
246
|
+
// rendering ones that silently fail when clicked.
|
|
247
|
+
let canEnterFullscreen = $state(false);
|
|
248
|
+
let canEnterPip = $state(false);
|
|
249
|
+
const mobileMode = $derived(mobileTimelineOnly && (touchPointer || breakpointX === 'sm'));
|
|
250
|
+
// vidstack flips `userIdle` to true while the engine is playing and the
|
|
251
|
+
// pointer hasn't moved over the player for ~2.5s (and back to false on
|
|
252
|
+
// any interaction or while paused). We use it to fade the controls bar.
|
|
253
|
+
let userIdle = $state(false);
|
|
254
|
+
// Vidstack's userIdle never fires while paused (its MediaUserController
|
|
255
|
+
// bails out on `pause`). When `idleWhilePaused` is on, run our own timer
|
|
256
|
+
// and OR the result into the bar's fade.
|
|
257
|
+
let pausedIdle = $state(false);
|
|
258
|
+
// Mirror the end of vidstack's buffered / seekable ranges. Used for the
|
|
259
|
+
// seek bar's progress underlay and to clamp the slider max below
|
|
260
|
+
// `duration` when the engine reports a smaller seekable window
|
|
261
|
+
// (e.g. HLS live).
|
|
262
|
+
let bufferedEnd = $state(0);
|
|
263
|
+
let seekableEnd = $state(0);
|
|
264
|
+
let seekTimelineRef = $state<HTMLElement | null>(null);
|
|
265
|
+
let seekInteracted = $state(false);
|
|
266
|
+
let chapterCues = $state<ChapterCue[]>([]);
|
|
267
|
+
|
|
268
|
+
// While scrubbing, scrubTime overrides currentTime so the label and thumb
|
|
269
|
+
// track the pointer immediately instead of waiting for the media engine.
|
|
270
|
+
const SEEK_SETTLE_EPSILON = 0.08;
|
|
271
|
+
const SEEK_DRIFT_SNAP = 0.35;
|
|
272
|
+
const SEEK_DRIFT_CORRECTION = 0.14;
|
|
273
|
+
const SEEK_KEY_STEP = 5;
|
|
274
|
+
const SEEK_PAGE_STEP = 30;
|
|
275
|
+
let scrubbing = $state(false);
|
|
276
|
+
let scrubTime = $state(0);
|
|
277
|
+
// Hover preview: only the *whole-second* time and active flag drive
|
|
278
|
+
// re-renders. The exact pointer x position is written straight to a CSS
|
|
279
|
+
// variable on the timeline (no Svelte invalidation per pointermove tick).
|
|
280
|
+
let hoverActive = $state(false);
|
|
281
|
+
let hoverSeconds = $state(0);
|
|
282
|
+
let visualTime = 0;
|
|
283
|
+
let pendingSeekTime: number | null = null;
|
|
284
|
+
let resumeAfterScrub = false;
|
|
285
|
+
// fmt() ran on the time-text node every time-update (~60×/s). Deriving
|
|
286
|
+
// whole-second labels means it only re-runs when the visible string changes.
|
|
287
|
+
const shownTimeLabel = $derived(fmt(Math.floor(scrubbing ? scrubTime : currentTime)));
|
|
288
|
+
const durationLabel = $derived(fmt(Math.floor(duration)));
|
|
289
|
+
const seekAriaValue = $derived(Math.round((scrubbing ? scrubTime : currentTime) * 100) / 100);
|
|
290
|
+
const seekValueText = $derived(`${shownTimeLabel} of ${durationLabel}`);
|
|
291
|
+
const chapterTrack = $derived(
|
|
292
|
+
typeof chapters === 'string' ? { src: chapters, srclang: 'en-US', default: true } : chapters
|
|
293
|
+
);
|
|
294
|
+
const chapterMarkers = $derived(
|
|
295
|
+
chapterCues
|
|
296
|
+
.map((cue) => ({
|
|
297
|
+
...cue,
|
|
298
|
+
left: timeMax > 0 ? Math.max(0, Math.min(100, (cue.start / timeMax) * 100)) : 0
|
|
299
|
+
}))
|
|
300
|
+
.filter((cue) => cue.left > 0 && cue.left < 100)
|
|
301
|
+
);
|
|
302
|
+
const hoverTimeLabel = $derived(fmt(hoverSeconds));
|
|
303
|
+
const hoverChapter = $derived(
|
|
304
|
+
hoverActive
|
|
305
|
+
? chapterCues.find((c) => hoverSeconds >= c.start && hoverSeconds < c.end)
|
|
306
|
+
: undefined
|
|
307
|
+
);
|
|
308
|
+
const seekPreviewVisible = $derived(showSeekPreview && hoverActive && !mobileMode);
|
|
309
|
+
|
|
310
|
+
const playerEl = $derived(ref as (HTMLElement & MediaPlayerElement) | null);
|
|
311
|
+
// Construct only in the browser — Cloudflare worker SSR resolves vidstack's
|
|
312
|
+
// CJS build, where `MediaRemoteControl` lands as a non-constructor value
|
|
313
|
+
// and `new` throws. Templates only invoke `remote` from event handlers, so
|
|
314
|
+
// it never runs during server render.
|
|
315
|
+
let remote!: MediaRemoteControl;
|
|
316
|
+
if (browser) remote = new MediaRemoteControl();
|
|
317
|
+
|
|
318
|
+
$effect(() => {
|
|
319
|
+
if (!elementsDefined) return;
|
|
320
|
+
if (!playerEl) return;
|
|
321
|
+
remote.setPlayer(playerEl);
|
|
322
|
+
remote.setTarget(playerEl);
|
|
323
|
+
// initialVolume / initialPlaybackRate are documented as initial values.
|
|
324
|
+
// Untrack them so a parent updating those props after mount doesn't
|
|
325
|
+
// re-run this effect and tear down setPlayer / setTarget.
|
|
326
|
+
untrack(() => {
|
|
327
|
+
if (typeof initialVolume === 'number') remote.changeVolume(initialVolume);
|
|
328
|
+
if (typeof initialPlaybackRate === 'number') remote.changePlaybackRate(initialPlaybackRate);
|
|
329
|
+
});
|
|
330
|
+
return () => {
|
|
331
|
+
remote.setPlayer(null);
|
|
332
|
+
remote.setTarget(null);
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const rateOptions = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
337
|
+
|
|
338
|
+
const skipBy = $derived(typeof skip === 'number' ? skip : skip ? 10 : 0);
|
|
339
|
+
const showSkip = $derived(skipBy > 0);
|
|
340
|
+
|
|
341
|
+
// Sort highest-resolution first; bitrate breaks ties for same-height
|
|
342
|
+
// renditions. We keep the original array index because that's what
|
|
343
|
+
// `remote.changeQuality(index)` expects — sorting is purely visual.
|
|
344
|
+
const qualityOptions = $derived(
|
|
345
|
+
qualities
|
|
346
|
+
.map((q, index) => ({ q, index }))
|
|
347
|
+
.sort((a, b) =>
|
|
348
|
+
b.q.height !== a.q.height ? b.q.height - a.q.height : b.q.bitrate - a.q.bitrate
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
const showQualityMenu = $derived(showQuality && canSetQuality && qualities.length > 0);
|
|
352
|
+
const qualityValue = $derived(
|
|
353
|
+
autoQuality || !currentQuality ? 'auto' : String(qualities.indexOf(currentQuality))
|
|
354
|
+
);
|
|
355
|
+
const qualityHint = $derived(
|
|
356
|
+
autoQuality
|
|
357
|
+
? currentQuality
|
|
358
|
+
? `Auto (${currentQuality.height}p)`
|
|
359
|
+
: 'Auto'
|
|
360
|
+
: currentQuality
|
|
361
|
+
? `${currentQuality.height}p`
|
|
362
|
+
: '—'
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
function changeQuality(value: string, e?: Event) {
|
|
366
|
+
if (value === 'auto') remote.changeQuality(-1, e);
|
|
367
|
+
else {
|
|
368
|
+
const idx = Number(value);
|
|
369
|
+
if (Number.isFinite(idx)) remote.changeQuality(idx, e);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// When autoHide is false, force vidstack out of idle for the entire mount so
|
|
374
|
+
// the controls bar never fades. We can't just set a huge user-idle-delay —
|
|
375
|
+
// vidstack would still flip idle on `pause → play` if left long enough — so
|
|
376
|
+
// we tell the controller to pause idle tracking and resume on cleanup.
|
|
377
|
+
$effect(() => {
|
|
378
|
+
if (!browser || !elementsDefined || !playerEl) return;
|
|
379
|
+
if (autoHide) return;
|
|
380
|
+
remote.pauseUserIdle();
|
|
381
|
+
return () => remote.resumeUserIdle();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Custom idle timer that fills vidstack's gap: track inactivity while
|
|
385
|
+
// PAUSED and fade the bar after `idleDelay`. Only runs when `paused` is
|
|
386
|
+
// true and the consumer has opted in via `idleWhilePaused`. Reset on any
|
|
387
|
+
// pointer or keyboard activity inside the player element.
|
|
388
|
+
$effect(() => {
|
|
389
|
+
if (!browser || !elementsDefined || !playerEl) return;
|
|
390
|
+
if (!autoHide || !idleWhilePaused || !paused) {
|
|
391
|
+
pausedIdle = false;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const delay = typeof idleDelay === 'number' ? idleDelay : 500;
|
|
395
|
+
let timer = window.setTimeout(() => {
|
|
396
|
+
pausedIdle = true;
|
|
397
|
+
}, delay);
|
|
398
|
+
const reset = () => {
|
|
399
|
+
pausedIdle = false;
|
|
400
|
+
window.clearTimeout(timer);
|
|
401
|
+
timer = window.setTimeout(() => {
|
|
402
|
+
pausedIdle = true;
|
|
403
|
+
}, delay);
|
|
404
|
+
};
|
|
405
|
+
playerEl.addEventListener('pointermove', reset);
|
|
406
|
+
playerEl.addEventListener('pointerdown', reset);
|
|
407
|
+
playerEl.addEventListener('keydown', reset);
|
|
408
|
+
return () => {
|
|
409
|
+
window.clearTimeout(timer);
|
|
410
|
+
playerEl.removeEventListener('pointermove', reset);
|
|
411
|
+
playerEl.removeEventListener('pointerdown', reset);
|
|
412
|
+
playerEl.removeEventListener('keydown', reset);
|
|
413
|
+
pausedIdle = false;
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
function downloadVideo() {
|
|
418
|
+
if (!browser) return;
|
|
419
|
+
const link = document.createElement('a');
|
|
420
|
+
link.href = src;
|
|
421
|
+
// Same-origin sources honor the download attribute and save the file.
|
|
422
|
+
// Cross-origin servers without CORS download headers will navigate
|
|
423
|
+
// instead — opening in a new tab is the friendliest fallback.
|
|
424
|
+
link.download = '';
|
|
425
|
+
link.target = '_blank';
|
|
426
|
+
link.rel = 'noopener';
|
|
427
|
+
link.click();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Vidstack's `_play()` already seeks to start when play is requested on
|
|
431
|
+
// an ended player, so we just delegate.
|
|
432
|
+
function togglePlay(e: Event) {
|
|
433
|
+
remote.togglePaused(e);
|
|
434
|
+
}
|
|
435
|
+
function seekBy(delta: number, e: Event) {
|
|
436
|
+
seekInteracted = true;
|
|
437
|
+
const next = (readMediaTime() ?? currentTime) + delta;
|
|
438
|
+
remote.seek(Math.max(0, duration ? Math.min(duration, next) : next), e);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function readMediaTime(el = playerEl) {
|
|
442
|
+
const time = (el as unknown as { currentTime?: number } | null)?.currentTime;
|
|
443
|
+
return typeof time === 'number' && Number.isFinite(time) ? time : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function clampTime(t: number) {
|
|
447
|
+
if (!Number.isFinite(t)) return 0;
|
|
448
|
+
return Math.max(0, timeMax > 0 ? Math.min(timeMax, t) : t);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function setSeekVisual(time: number) {
|
|
452
|
+
const el = seekTimelineRef;
|
|
453
|
+
if (!el) return;
|
|
454
|
+
visualTime = clampTime(started || seekInteracted ? time : 0);
|
|
455
|
+
const fraction = timeMax > 0 ? visualTime / timeMax : 0;
|
|
456
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
457
|
+
el.style.setProperty('--era-video-played-scale', String(clamped));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function setBufferedVisual() {
|
|
461
|
+
seekTimelineRef?.style.setProperty(
|
|
462
|
+
'--era-video-buffered-scale',
|
|
463
|
+
String(Math.max(0, Math.min(1, bufferedFraction)))
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function pointerTime(e: PointerEvent) {
|
|
468
|
+
const rect = seekTimelineRef?.getBoundingClientRect();
|
|
469
|
+
if (!rect || rect.width <= 0) return currentTime;
|
|
470
|
+
const fraction = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
471
|
+
return clampTime(fraction * timeMax);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function scrubTo(time: number) {
|
|
475
|
+
scrubTime = clampTime(time);
|
|
476
|
+
setSeekVisual(scrubTime);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function commitSeek(time: number, e?: Event) {
|
|
480
|
+
const next = clampTime(time);
|
|
481
|
+
const atLiveEdge = live && timeMax > 0 && next / timeMax >= 0.99;
|
|
482
|
+
if (atLiveEdge) remote.seekToLiveEdge();
|
|
483
|
+
else remote.seek(next, e);
|
|
484
|
+
pendingSeekTime = next;
|
|
485
|
+
currentTime = next;
|
|
486
|
+
setSeekVisual(next);
|
|
487
|
+
scrubbing = false;
|
|
488
|
+
if (resumeAfterScrub) {
|
|
489
|
+
resumeAfterScrub = false;
|
|
490
|
+
remote.play();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function handleSeekPointerDown(e: PointerEvent) {
|
|
495
|
+
if (seekDisabled) return;
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
const target = e.currentTarget as HTMLElement;
|
|
498
|
+
target.setPointerCapture(e.pointerId);
|
|
499
|
+
seekInteracted = true;
|
|
500
|
+
scrubbing = true;
|
|
501
|
+
scrubTo(pointerTime(e));
|
|
502
|
+
if (!paused) {
|
|
503
|
+
resumeAfterScrub = true;
|
|
504
|
+
remote.pause(e);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function handleSeekPointerMove(e: PointerEvent) {
|
|
509
|
+
if (showSeekPreview) updateHover(e);
|
|
510
|
+
if (!scrubbing || seekDisabled) return;
|
|
511
|
+
scrubTo(pointerTime(e));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function updateHover(e: PointerEvent) {
|
|
515
|
+
const time = pointerTime(e);
|
|
516
|
+
const fraction = timeMax > 0 ? Math.max(0, Math.min(1, time / timeMax)) : 0;
|
|
517
|
+
seekTimelineRef?.style.setProperty('--era-video-hover-pct', `${fraction * 100}%`);
|
|
518
|
+
const sec = Math.floor(time);
|
|
519
|
+
if (sec !== hoverSeconds) hoverSeconds = sec;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function handleSeekPointerEnter(e: PointerEvent) {
|
|
523
|
+
if (!showSeekPreview) return;
|
|
524
|
+
hoverActive = true;
|
|
525
|
+
updateHover(e);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function handleSeekPointerLeave() {
|
|
529
|
+
if (hoverActive) hoverActive = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function handleSeekPointerUp(e: PointerEvent) {
|
|
533
|
+
if (!scrubbing) return;
|
|
534
|
+
const target = e.currentTarget as HTMLElement;
|
|
535
|
+
if (target.hasPointerCapture(e.pointerId)) target.releasePointerCapture(e.pointerId);
|
|
536
|
+
commitSeek(pointerTime(e), e);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function handleSeekPointerCancel(e: PointerEvent) {
|
|
540
|
+
if (!scrubbing) return;
|
|
541
|
+
const target = e.currentTarget as HTMLElement;
|
|
542
|
+
if (target.hasPointerCapture(e.pointerId)) target.releasePointerCapture(e.pointerId);
|
|
543
|
+
scrubbing = false;
|
|
544
|
+
resumeAfterScrub = false;
|
|
545
|
+
setSeekVisual(readMediaTime() ?? currentTime);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function handleSeekKeydown(e: KeyboardEvent) {
|
|
549
|
+
if (seekDisabled) return;
|
|
550
|
+
const base = readMediaTime() ?? currentTime;
|
|
551
|
+
let next: number | null = null;
|
|
552
|
+
switch (e.key) {
|
|
553
|
+
case 'ArrowLeft':
|
|
554
|
+
case 'ArrowDown':
|
|
555
|
+
next = base - SEEK_KEY_STEP;
|
|
556
|
+
break;
|
|
557
|
+
case 'ArrowRight':
|
|
558
|
+
case 'ArrowUp':
|
|
559
|
+
next = base + SEEK_KEY_STEP;
|
|
560
|
+
break;
|
|
561
|
+
case 'PageDown':
|
|
562
|
+
next = base - SEEK_PAGE_STEP;
|
|
563
|
+
break;
|
|
564
|
+
case 'PageUp':
|
|
565
|
+
next = base + SEEK_PAGE_STEP;
|
|
566
|
+
break;
|
|
567
|
+
case 'Home':
|
|
568
|
+
next = 0;
|
|
569
|
+
break;
|
|
570
|
+
case 'End':
|
|
571
|
+
next = timeMax;
|
|
572
|
+
break;
|
|
573
|
+
default:
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
seekInteracted = true;
|
|
578
|
+
commitSeek(next, e);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function fmt(t: number) {
|
|
582
|
+
if (!Number.isFinite(t) || t < 0) return '0:00';
|
|
583
|
+
const total = Math.floor(t);
|
|
584
|
+
const h = Math.floor(total / 3600);
|
|
585
|
+
const m = Math.floor((total % 3600) / 60);
|
|
586
|
+
const s = total % 60;
|
|
587
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
588
|
+
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
|
589
|
+
return `${m}:${pad(s)}`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function parseVttTime(time: string) {
|
|
593
|
+
const parts = time.trim().split(':');
|
|
594
|
+
const seconds = Number(parts.pop());
|
|
595
|
+
const minutes = Number(parts.pop() ?? 0);
|
|
596
|
+
const hours = Number(parts.pop() ?? 0);
|
|
597
|
+
if (![seconds, minutes, hours].every(Number.isFinite)) return null;
|
|
598
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function parseChaptersVtt(vtt: string) {
|
|
602
|
+
return vtt
|
|
603
|
+
.replace(/\r/g, '')
|
|
604
|
+
.split(/\n{2,}/)
|
|
605
|
+
.map((block) => {
|
|
606
|
+
const lines = block
|
|
607
|
+
.split('\n')
|
|
608
|
+
.map((line) => line.trim())
|
|
609
|
+
.filter(Boolean);
|
|
610
|
+
const timingIndex = lines.findIndex((line) => line.includes('-->'));
|
|
611
|
+
if (timingIndex === -1) return null;
|
|
612
|
+
const [rawStart, rawEnd] = lines[timingIndex].split('-->');
|
|
613
|
+
const start = parseVttTime(rawStart);
|
|
614
|
+
const end = parseVttTime(rawEnd?.trim().split(/\s+/)[0] ?? '');
|
|
615
|
+
if (start === null || end === null) return null;
|
|
616
|
+
return {
|
|
617
|
+
start,
|
|
618
|
+
end,
|
|
619
|
+
text: lines.slice(timingIndex + 1).join(' ')
|
|
620
|
+
};
|
|
621
|
+
})
|
|
622
|
+
.filter((cue): cue is ChapterCue => cue !== null);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
$effect(() => {
|
|
626
|
+
if (!browser) return;
|
|
627
|
+
const src = chapterTrack?.src;
|
|
628
|
+
if (!src) {
|
|
629
|
+
chapterCues = [];
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
let cancelled = false;
|
|
633
|
+
fetch(src)
|
|
634
|
+
.then((res) => (res.ok ? res.text() : ''))
|
|
635
|
+
.then((text) => {
|
|
636
|
+
if (!cancelled) chapterCues = parseChaptersVtt(text);
|
|
637
|
+
})
|
|
638
|
+
.catch(() => {
|
|
639
|
+
if (!cancelled) chapterCues = [];
|
|
640
|
+
});
|
|
641
|
+
return () => {
|
|
642
|
+
cancelled = true;
|
|
643
|
+
};
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
$effect(() => {
|
|
647
|
+
if (!elementsDefined) return;
|
|
648
|
+
if (!playerEl) return;
|
|
649
|
+
const el = playerEl;
|
|
650
|
+
|
|
651
|
+
// vidstack drives every piece of media state. Subscribe once and let
|
|
652
|
+
// the engine's signals overwrite our local mirrors — source changes,
|
|
653
|
+
// buffering, seek throttling, and replay are all already handled
|
|
654
|
+
// inside vidstack, so there's nothing for us to track or invalidate.
|
|
655
|
+
// The cast works around a TS quirk: `MediaPlayerElement` is an
|
|
656
|
+
// interface that extends a type-alias intersection, and TS doesn't
|
|
657
|
+
// reliably surface the conditional `subscribe` member that way.
|
|
658
|
+
type PlayerWithStore = { subscribe: (cb: (s: Readonly<MediaState>) => void) => () => void };
|
|
659
|
+
const dispose = (el as unknown as PlayerWithStore).subscribe((state) => {
|
|
660
|
+
paused = state.paused;
|
|
661
|
+
ended = state.ended;
|
|
662
|
+
started = state.started;
|
|
663
|
+
// Mirror `<media-buffering-indicator>`: spinner shows while the
|
|
664
|
+
// resource isn't ready to play or while the engine is genuinely
|
|
665
|
+
// waiting for data — never during user-initiated seeks (the moving
|
|
666
|
+
// thumb is the feedback). Folding `state.seeking` in here was the
|
|
667
|
+
// source of "spinner stuck after paused-seek to a buffered region".
|
|
668
|
+
waiting = !state.canPlay || state.waiting;
|
|
669
|
+
// vidstack's `subscribe` is a maverick `effect()`, so this callback
|
|
670
|
+
// only re-runs for state fields it actually reads. We must read
|
|
671
|
+
// `state.currentTime` to keep `time-update` ticks driving the
|
|
672
|
+
// timeline — but `state.currentTime` itself mirrors that throttled
|
|
673
|
+
// (~4 Hz) event and lags the engine by up to ~250 ms, which is
|
|
674
|
+
// visible as a thumb jump on pause/resume edges. The element's
|
|
675
|
+
// `currentTime` getter (via the underlying provider) is precise
|
|
676
|
+
// and unthrottled, so use it for the actual value and keep the
|
|
677
|
+
// `state.currentTime` read solely for dependency tracking.
|
|
678
|
+
const tracked = state.currentTime;
|
|
679
|
+
const mediaTime = readMediaTime(el) ?? tracked;
|
|
680
|
+
if (
|
|
681
|
+
pendingSeekTime !== null &&
|
|
682
|
+
state.seeking &&
|
|
683
|
+
Math.abs(mediaTime - pendingSeekTime) > SEEK_SETTLE_EPSILON
|
|
684
|
+
) {
|
|
685
|
+
currentTime = pendingSeekTime;
|
|
686
|
+
} else {
|
|
687
|
+
pendingSeekTime = null;
|
|
688
|
+
currentTime = mediaTime;
|
|
689
|
+
}
|
|
690
|
+
duration = state.duration;
|
|
691
|
+
bufferedEnd = state.bufferedEnd;
|
|
692
|
+
seekableEnd = state.seekableEnd;
|
|
693
|
+
volumeState = state.volume;
|
|
694
|
+
mutedState = state.muted;
|
|
695
|
+
rateState = state.playbackRate;
|
|
696
|
+
fullscreenState = state.fullscreen;
|
|
697
|
+
pipState = state.pictureInPicture;
|
|
698
|
+
live = state.live;
|
|
699
|
+
canSeek = state.canSeek;
|
|
700
|
+
userIdle = state.userIdle;
|
|
701
|
+
// Copy the qualities array so Svelte's reactivity sees the change —
|
|
702
|
+
// vidstack mutates this list in place as the engine learns about
|
|
703
|
+
// renditions, and assigning the same reference would skip updates.
|
|
704
|
+
qualities = [...state.qualities];
|
|
705
|
+
currentQuality = state.quality;
|
|
706
|
+
autoQuality = state.autoQuality;
|
|
707
|
+
canSetQuality = state.canSetQuality;
|
|
708
|
+
touchPointer = state.touchPointer;
|
|
709
|
+
breakpointX = state.breakpointX;
|
|
710
|
+
canEnterFullscreen = state.canFullscreen;
|
|
711
|
+
canEnterPip = state.canPictureInPicture;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Forward selected events to consumer callbacks. The thunks read
|
|
715
|
+
// each prop fresh at fire time so a parent updating a callback
|
|
716
|
+
// after mount is honored without re-attaching listeners.
|
|
717
|
+
const forward = <T extends Event>(name: string, get: () => ((e: T) => void) | undefined) => {
|
|
718
|
+
const fn = (e: Event) => get()?.(e as T);
|
|
719
|
+
el.addEventListener(name, fn);
|
|
720
|
+
return () => el.removeEventListener(name, fn);
|
|
721
|
+
};
|
|
722
|
+
const cleanups = [
|
|
723
|
+
forward('play', () => onplay),
|
|
724
|
+
forward('pause', () => onpause),
|
|
725
|
+
forward('ended', () => onended),
|
|
726
|
+
forward('playing', () => onplaying),
|
|
727
|
+
forward('seeked', () => onseeked),
|
|
728
|
+
forward('seeking', () => onseeking),
|
|
729
|
+
forward('can-play', () => oncanplay),
|
|
730
|
+
forward('time-update', () => ontimeupdate),
|
|
731
|
+
forward('volume-change', () => onvolumechange),
|
|
732
|
+
forward('rate-change', () => onratechange),
|
|
733
|
+
forward('duration-change', () => ondurationchange),
|
|
734
|
+
forward('loaded-metadata', () => onloadedmetadata),
|
|
735
|
+
forward('fullscreen-change', () => onfullscreenchange),
|
|
736
|
+
forward('picture-in-picture-change', () => onpipchange),
|
|
737
|
+
forward('error', () => onerror)
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
return () => {
|
|
741
|
+
dispose();
|
|
742
|
+
for (const fn of cleanups) fn();
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Keep the visible seek bar off Svelte's render path during playback. The
|
|
747
|
+
// timeline is a dedicated control instead of a generic form slider, so the
|
|
748
|
+
// fill and thumb can move with CSS variables without value reconciliation
|
|
749
|
+
// fighting rAF. On resume we start from the last visual time and ease small
|
|
750
|
+
// media-clock drift back toward the provider, masking the common one-frame
|
|
751
|
+
// jump without hiding real seeks or stalls.
|
|
752
|
+
$effect(() => {
|
|
753
|
+
const el = playerEl;
|
|
754
|
+
const timeline = seekTimelineRef;
|
|
755
|
+
const max = timeMax;
|
|
756
|
+
const rate = rateState;
|
|
757
|
+
if (!el || !timeline || max <= 0) return;
|
|
758
|
+
|
|
759
|
+
setBufferedVisual();
|
|
760
|
+
|
|
761
|
+
if (scrubbing) {
|
|
762
|
+
setSeekVisual(scrubTime);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (paused) {
|
|
767
|
+
const t = readMediaTime(el) ?? currentTime;
|
|
768
|
+
currentTime = t;
|
|
769
|
+
setSeekVisual(t);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const mediaTime = readMediaTime(el) ?? currentTime;
|
|
774
|
+
if (Math.abs(visualTime - mediaTime) > SEEK_DRIFT_SNAP) setSeekVisual(mediaTime);
|
|
775
|
+
|
|
776
|
+
let originTime = visualTime;
|
|
777
|
+
let originNow = performance.now();
|
|
778
|
+
let raf = requestAnimationFrame(function tick(now) {
|
|
779
|
+
const predicted = originTime + ((now - originNow) / 1000) * rate;
|
|
780
|
+
const actual = readMediaTime(el) ?? predicted;
|
|
781
|
+
const drift = actual - predicted;
|
|
782
|
+
const next =
|
|
783
|
+
Math.abs(drift) > SEEK_DRIFT_SNAP ? actual : predicted + drift * SEEK_DRIFT_CORRECTION;
|
|
784
|
+
setSeekVisual(next);
|
|
785
|
+
originTime = visualTime;
|
|
786
|
+
originNow = now;
|
|
787
|
+
raf = requestAnimationFrame(tick);
|
|
788
|
+
});
|
|
789
|
+
return () => cancelAnimationFrame(raf);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const playLabel = $derived(ended ? 'Replay' : paused ? 'Play' : 'Pause');
|
|
793
|
+
const muteLabel = $derived(mutedState || volumeState === 0 ? 'Unmute' : 'Mute');
|
|
794
|
+
// Prefer seekable.end when smaller than duration (HLS live windows); fall
|
|
795
|
+
// back to duration for VOD where the engine may report seekable === duration.
|
|
796
|
+
const timeMax = $derived(
|
|
797
|
+
seekableEnd > 0 && seekableEnd < (duration || Infinity)
|
|
798
|
+
? seekableEnd
|
|
799
|
+
: duration > 0
|
|
800
|
+
? duration
|
|
801
|
+
: 1
|
|
802
|
+
);
|
|
803
|
+
const bufferedFraction = $derived(timeMax > 0 ? bufferedEnd / timeMax : 0);
|
|
804
|
+
// Snap volumeState to the slider's step before passing it as `value`, so
|
|
805
|
+
// bits-ui's snap-to-step watcher doesn't fire onValueChange (with a
|
|
806
|
+
// rounded value) on every mount and external volume update.
|
|
807
|
+
const VOLUME_STEP = 0.01;
|
|
808
|
+
const shownVolume = $derived(
|
|
809
|
+
mutedState ? 0 : Math.round(volumeState / VOLUME_STEP) * VOLUME_STEP
|
|
810
|
+
);
|
|
811
|
+
const resolvedVolumeOrientation = $derived(
|
|
812
|
+
volumeOrientation === 'vertical' ? 'vertical' : 'horizontal'
|
|
813
|
+
);
|
|
814
|
+
// In vertical mode the popup is anchored to the mute icon — same width
|
|
815
|
+
// (h-md), shared bg, rounded top + flat bottom — so it reads as the icon
|
|
816
|
+
// extending upward (vidstack has no native API for this). The sleeve's
|
|
817
|
+
// box runs all the way down to the button (`bottom-full`, no offset) so
|
|
818
|
+
// the cursor crossing the visual gap stays inside the cluster's hover
|
|
819
|
+
// region; the gap is recreated by the slider's inset within the sleeve.
|
|
820
|
+
const volumeClusterClass = $derived(
|
|
821
|
+
resolvedVolumeOrientation === 'vertical'
|
|
822
|
+
? 'group relative shrink-0'
|
|
823
|
+
: 'relative flex shrink-0 items-center'
|
|
824
|
+
);
|
|
825
|
+
// `--era-shadow` (inset only) — `--era-shadow-lg`'s drop shadow would seam
|
|
826
|
+
// against the mute button below. Visibility is in the scoped style block.
|
|
827
|
+
const volumeSleeveClass = $derived(
|
|
828
|
+
resolvedVolumeOrientation === 'vertical'
|
|
829
|
+
? 'absolute bottom-full left-1/2 z-20 h-[calc(var(--era-h-lg)*3)] w-(--era-h-md) -translate-x-1/2 rounded-t-(--era-rd-md) bg-hover shadow-(--era-shadow) glass-blur'
|
|
830
|
+
: 'relative h-(--era-h-sm) w-[calc(var(--era-h-md)*3)]'
|
|
831
|
+
);
|
|
832
|
+
// Slider IS the visible track. Range and Thumb (both bg-fg) live inside
|
|
833
|
+
// and blend into one filled column.
|
|
834
|
+
const volumeSliderClass = $derived(
|
|
835
|
+
`absolute touch-none select-none overflow-hidden rounded-(--era-rd-xs) bg-3 shadow-(--era-shadow) focus:outline-none ${
|
|
836
|
+
resolvedVolumeOrientation === 'vertical'
|
|
837
|
+
? 'top-(--era-gap) bottom-(--era-gap) left-1/2 w-(--era-h-xs) -translate-x-1/2'
|
|
838
|
+
: 'inset-x-(--era-xs-inset-sm) top-1/2 h-(--era-h-xs) -translate-y-1/2'
|
|
839
|
+
}`
|
|
840
|
+
);
|
|
841
|
+
// Bits-ui's slider needs a thumb element to handle pointer events, but
|
|
842
|
+
// the visible position indicator is the Range fill alone — no separate
|
|
843
|
+
// thumb to align. The thumb stays in the DOM at zero size, invisible.
|
|
844
|
+
const volumeThumbClass = 'block size-0 cursor-pointer focus:outline-none';
|
|
845
|
+
const volumeRangeClass = $derived(
|
|
846
|
+
resolvedVolumeOrientation === 'vertical' ? 'absolute w-full bg-fg' : 'absolute h-full bg-fg'
|
|
847
|
+
);
|
|
848
|
+
// vidstack's `canSeek` flips false during initial metadata load on VOD
|
|
849
|
+
// (because seekableWindow isn't finite yet) — disabling the slider then
|
|
850
|
+
// is just visual noise, the engine queues the seek as soon as metadata
|
|
851
|
+
// arrives. The genuine no-seek case is a live stream without a DVR
|
|
852
|
+
// window, which is what we actually want to gate on.
|
|
853
|
+
const seekDisabled = $derived(live && !canSeek);
|
|
854
|
+
// Don't fade while the cursor is hovering the bar itself — even when
|
|
855
|
+
// vidstack's userIdle (or our paused-idle) flips true because the user
|
|
856
|
+
// has held still over the bar for a while.
|
|
857
|
+
let barHovered = $state(false);
|
|
858
|
+
const barClass = $derived(
|
|
859
|
+
`absolute inset-x-(--era-gap) bottom-(--era-gap) z-10 bg-2 shadow-(--era-shadow) transition-opacity duration-(--era-duration) ${
|
|
860
|
+
(userIdle || pausedIdle) && !barHovered ? 'pointer-events-none opacity-0' : ''
|
|
861
|
+
}`
|
|
862
|
+
);
|
|
863
|
+
</script>
|
|
864
|
+
|
|
865
|
+
<!-- Snippet so the right-click branch can spread bits-ui's trigger props
|
|
866
|
+
onto the SAME `<media-player>` (a wrapper would break vidstack's
|
|
867
|
+
fullscreen target). -->
|
|
868
|
+
{#snippet rootMediaPlayer(extraProps: Record<string, unknown>)}
|
|
869
|
+
<media-player
|
|
870
|
+
bind:this={ref}
|
|
871
|
+
class={cn(
|
|
872
|
+
'relative block w-full overflow-hidden [border-radius:calc(var(--era-rd-lg)+var(--era-gap))] bg-black',
|
|
873
|
+
className
|
|
874
|
+
)}
|
|
875
|
+
{src}
|
|
876
|
+
{poster}
|
|
877
|
+
{preload}
|
|
878
|
+
{load}
|
|
879
|
+
{crossorigin}
|
|
880
|
+
muted={muted ? '' : undefined}
|
|
881
|
+
autoplay={autoplay ? '' : undefined}
|
|
882
|
+
loop={loop ? '' : undefined}
|
|
883
|
+
view-type={viewType}
|
|
884
|
+
stream-type={streamType}
|
|
885
|
+
aspect-ratio={aspectRatio}
|
|
886
|
+
fullscreen-orientation={fullscreenOrientation}
|
|
887
|
+
controls={controls ? '' : undefined}
|
|
888
|
+
playsinline={playsinline ? '' : undefined}
|
|
889
|
+
user-idle-delay={typeof idleDelay === 'number' ? idleDelay : undefined}
|
|
890
|
+
key-disabled={keyDisabled ? '' : undefined}
|
|
891
|
+
key-target={keyTarget}
|
|
892
|
+
key-shortcuts={keyShortcuts ? JSON.stringify(keyShortcuts) : undefined}
|
|
893
|
+
{...extraProps}
|
|
894
|
+
>
|
|
895
|
+
<media-outlet>
|
|
896
|
+
{#if !controls && !children && mobileMode}
|
|
897
|
+
<!-- Mobile only: vidstack's gesture component carries an inherent
|
|
898
|
+
250 ms delay on single-tap (waits to disambiguate from a
|
|
899
|
+
double-tap), which is fine here because the dbl-tap-seek
|
|
900
|
+
halves justify the wait. Desktop uses a plain <button>
|
|
901
|
+
overlay below for instant click response. -->
|
|
902
|
+
<media-gesture
|
|
903
|
+
event="pointerup"
|
|
904
|
+
action="toggle:paused"
|
|
905
|
+
class="absolute inset-0"
|
|
906
|
+
aria-label={playLabel}
|
|
907
|
+
></media-gesture>
|
|
908
|
+
<media-gesture
|
|
909
|
+
event="dblpointerup"
|
|
910
|
+
action="seek:-10"
|
|
911
|
+
class="absolute top-0 bottom-0 left-0 w-1/2"
|
|
912
|
+
aria-label="Back 10 seconds"
|
|
913
|
+
></media-gesture>
|
|
914
|
+
<media-gesture
|
|
915
|
+
event="dblpointerup"
|
|
916
|
+
action="seek:10"
|
|
917
|
+
class="absolute top-0 right-0 bottom-0 w-1/2"
|
|
918
|
+
aria-label="Forward 10 seconds"
|
|
919
|
+
></media-gesture>
|
|
920
|
+
{/if}
|
|
921
|
+
{#if chapterTrack}
|
|
922
|
+
<track
|
|
923
|
+
src={chapterTrack.src}
|
|
924
|
+
label={chapterTrack.label}
|
|
925
|
+
srclang={chapterTrack.srclang ?? 'en-US'}
|
|
926
|
+
kind="chapters"
|
|
927
|
+
default={chapterTrack.default ?? true}
|
|
928
|
+
/>
|
|
929
|
+
{/if}
|
|
930
|
+
</media-outlet>
|
|
931
|
+
|
|
932
|
+
{#if children}
|
|
933
|
+
{@render children()}
|
|
934
|
+
{:else if !controls}
|
|
935
|
+
{#if poster && !started}
|
|
936
|
+
<img
|
|
937
|
+
src={poster}
|
|
938
|
+
alt=""
|
|
939
|
+
aria-hidden="true"
|
|
940
|
+
class="pointer-events-none absolute inset-0 z-0 size-full object-cover"
|
|
941
|
+
/>
|
|
942
|
+
{/if}
|
|
943
|
+
|
|
944
|
+
{#if !mobileMode}
|
|
945
|
+
<!-- Desktop: instant click-to-toggle. Mobile uses media-gesture
|
|
946
|
+
elements inside the outlet (above) for dbl-tap-seek. -->
|
|
947
|
+
<button
|
|
948
|
+
type="button"
|
|
949
|
+
aria-label={playLabel}
|
|
950
|
+
class="absolute inset-0 z-0 cursor-pointer bg-transparent"
|
|
951
|
+
onclick={togglePlay}
|
|
952
|
+
></button>
|
|
953
|
+
{/if}
|
|
954
|
+
|
|
955
|
+
{#if waiting}
|
|
956
|
+
<span
|
|
957
|
+
aria-hidden="true"
|
|
958
|
+
class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center"
|
|
959
|
+
>
|
|
960
|
+
<LoaderCircle class="size-(--era-h-lg) animate-spin text-fg" />
|
|
961
|
+
</span>
|
|
962
|
+
{/if}
|
|
963
|
+
|
|
964
|
+
<Bar
|
|
965
|
+
class={barClass}
|
|
966
|
+
onpointerenter={() => (barHovered = true)}
|
|
967
|
+
onpointerleave={() => (barHovered = false)}
|
|
968
|
+
>
|
|
969
|
+
{#if showSkip && !mobileMode}
|
|
970
|
+
<Button
|
|
971
|
+
icon
|
|
972
|
+
aria-label={`Back ${skipBy} seconds`}
|
|
973
|
+
onclick={(e: MouseEvent) => seekBy(-skipBy, e)}
|
|
974
|
+
>
|
|
975
|
+
<Rewind class="size-(--era-h-xs)" aria-hidden="true" />
|
|
976
|
+
</Button>
|
|
977
|
+
{/if}
|
|
978
|
+
|
|
979
|
+
{#if showPlay && !mobileMode}
|
|
980
|
+
<Button icon aria-label={playLabel} onclick={togglePlay}>
|
|
981
|
+
{#if ended}
|
|
982
|
+
<RotateCcw class="size-(--era-h-xs)" aria-hidden="true" />
|
|
983
|
+
{:else if paused}
|
|
984
|
+
<Play class="size-(--era-h-xs)" aria-hidden="true" />
|
|
985
|
+
{:else}
|
|
986
|
+
<Pause class="size-(--era-h-xs)" aria-hidden="true" />
|
|
987
|
+
{/if}
|
|
988
|
+
</Button>
|
|
989
|
+
{/if}
|
|
990
|
+
|
|
991
|
+
{#if showSkip && !mobileMode}
|
|
992
|
+
<Button
|
|
993
|
+
icon
|
|
994
|
+
aria-label={`Forward ${skipBy} seconds`}
|
|
995
|
+
onclick={(e: MouseEvent) => seekBy(skipBy, e)}
|
|
996
|
+
>
|
|
997
|
+
<FastForward class="size-(--era-h-xs)" aria-hidden="true" />
|
|
998
|
+
</Button>
|
|
999
|
+
{/if}
|
|
1000
|
+
|
|
1001
|
+
{#if showTime && !mobileMode}
|
|
1002
|
+
<Badge class="shrink-0 bg-transparent tabular-nums">
|
|
1003
|
+
{shownTimeLabel}
|
|
1004
|
+
</Badge>
|
|
1005
|
+
{/if}
|
|
1006
|
+
|
|
1007
|
+
<div
|
|
1008
|
+
bind:this={seekTimelineRef}
|
|
1009
|
+
role="slider"
|
|
1010
|
+
tabindex={seekDisabled ? -1 : 0}
|
|
1011
|
+
aria-label="Seek"
|
|
1012
|
+
aria-valuemin={0}
|
|
1013
|
+
aria-valuemax={Math.round(timeMax * 100) / 100}
|
|
1014
|
+
aria-valuenow={seekAriaValue}
|
|
1015
|
+
aria-valuetext={seekValueText}
|
|
1016
|
+
aria-disabled={seekDisabled}
|
|
1017
|
+
class="relative h-(--era-h-sm) min-w-0 flex-1 touch-none select-none focus:outline-none focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-fg data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
|
|
1018
|
+
style="--era-video-played-scale: 0; --era-video-buffered-scale: 0; --era-video-hover-pct: 0%;"
|
|
1019
|
+
data-seek-root
|
|
1020
|
+
data-disabled={seekDisabled}
|
|
1021
|
+
onpointerdown={handleSeekPointerDown}
|
|
1022
|
+
onpointermove={handleSeekPointerMove}
|
|
1023
|
+
onpointerup={handleSeekPointerUp}
|
|
1024
|
+
onpointercancel={handleSeekPointerCancel}
|
|
1025
|
+
onpointerenter={handleSeekPointerEnter}
|
|
1026
|
+
onpointerleave={handleSeekPointerLeave}
|
|
1027
|
+
onkeydown={handleSeekKeydown}
|
|
1028
|
+
>
|
|
1029
|
+
<span
|
|
1030
|
+
aria-hidden="true"
|
|
1031
|
+
class="pointer-events-none absolute inset-0 rounded-(--era-rd-sm)"
|
|
1032
|
+
>
|
|
1033
|
+
<span
|
|
1034
|
+
class="absolute inset-(--era-xs-inset-sm) overflow-hidden rounded-(--era-rd-xs) bg-3 shadow-(--era-shadow)"
|
|
1035
|
+
>
|
|
1036
|
+
<span
|
|
1037
|
+
class="absolute inset-y-0 left-0 w-full origin-left scale-x-(--era-video-buffered-scale) bg-4"
|
|
1038
|
+
></span>
|
|
1039
|
+
<span
|
|
1040
|
+
class="absolute inset-y-0 left-0 w-full origin-left scale-x-(--era-video-played-scale) bg-fg"
|
|
1041
|
+
></span>
|
|
1042
|
+
{#each chapterMarkers as chapter (chapter.start)}
|
|
1043
|
+
<span
|
|
1044
|
+
aria-hidden="true"
|
|
1045
|
+
title={chapter.text}
|
|
1046
|
+
class="absolute inset-y-0 w-px bg-2"
|
|
1047
|
+
style="left: {chapter.left}%"
|
|
1048
|
+
></span>
|
|
1049
|
+
{/each}
|
|
1050
|
+
</span>
|
|
1051
|
+
</span>
|
|
1052
|
+
{#if seekPreviewVisible}
|
|
1053
|
+
<!--
|
|
1054
|
+
Anchored to the seek-timeline div, but the timeline sits
|
|
1055
|
+
centered inside the taller Bar (h-lg around h-sm). To
|
|
1056
|
+
keep a `--era-gap`-sized clearance above the visible Bar
|
|
1057
|
+
surface — not just above the timeline div's top — we
|
|
1058
|
+
add half the bar/timeline height delta to the offset.
|
|
1059
|
+
-->
|
|
1060
|
+
<span
|
|
1061
|
+
aria-hidden="true"
|
|
1062
|
+
class="pointer-events-none absolute bottom-[calc(100%+(var(--era-h-lg)-var(--era-h-sm))/2+var(--era-gap))] left-(--era-video-hover-pct) inline-flex h-(--era-h-xxs) -translate-x-1/2 items-center gap-(--era-gap) rounded-(--era-rd-xxs) bg-(--era-surface-bg-elevated) px-(--era-inset-xxs) font-mono text-body leading-none whitespace-nowrap text-fg shadow-(--era-shadow-lg) glass-blur"
|
|
1063
|
+
>
|
|
1064
|
+
<span class="tabular-nums">{hoverTimeLabel}</span>
|
|
1065
|
+
{#if hoverChapter}
|
|
1066
|
+
<span class="text-muted">{hoverChapter.text}</span>
|
|
1067
|
+
{/if}
|
|
1068
|
+
</span>
|
|
1069
|
+
{/if}
|
|
1070
|
+
</div>
|
|
1071
|
+
|
|
1072
|
+
{#if showTime && !mobileMode}
|
|
1073
|
+
<Badge class="shrink-0 bg-transparent tabular-nums">
|
|
1074
|
+
{durationLabel}
|
|
1075
|
+
</Badge>
|
|
1076
|
+
{/if}
|
|
1077
|
+
|
|
1078
|
+
{#if showVolume && !mobileMode}
|
|
1079
|
+
<div
|
|
1080
|
+
class={volumeClusterClass}
|
|
1081
|
+
data-volume-cluster={resolvedVolumeOrientation === 'vertical' ? '' : undefined}
|
|
1082
|
+
>
|
|
1083
|
+
<Button
|
|
1084
|
+
icon
|
|
1085
|
+
aria-label={muteLabel}
|
|
1086
|
+
data-mute-button={resolvedVolumeOrientation === 'vertical' ? '' : undefined}
|
|
1087
|
+
onclick={(e: MouseEvent) => remote.toggleMuted(e)}
|
|
1088
|
+
>
|
|
1089
|
+
{#if mutedState || volumeState === 0}
|
|
1090
|
+
<VolumeX class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1091
|
+
{:else if volumeState < 0.5}
|
|
1092
|
+
<Volume1 class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1093
|
+
{:else}
|
|
1094
|
+
<Volume2 class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1095
|
+
{/if}
|
|
1096
|
+
</Button>
|
|
1097
|
+
|
|
1098
|
+
<div
|
|
1099
|
+
class={volumeSleeveClass}
|
|
1100
|
+
data-volume-sleeve={resolvedVolumeOrientation === 'vertical' ? '' : undefined}
|
|
1101
|
+
>
|
|
1102
|
+
<BitsSlider.Root
|
|
1103
|
+
type="single"
|
|
1104
|
+
value={shownVolume}
|
|
1105
|
+
min={0}
|
|
1106
|
+
max={1}
|
|
1107
|
+
step={VOLUME_STEP}
|
|
1108
|
+
orientation={resolvedVolumeOrientation}
|
|
1109
|
+
aria-label="Volume"
|
|
1110
|
+
class={volumeSliderClass}
|
|
1111
|
+
onValueChange={(v: number | number[]) => remote.changeVolume(toScalar(v))}
|
|
1112
|
+
>
|
|
1113
|
+
{#snippet children({ thumbs })}
|
|
1114
|
+
<BitsSlider.Range class={volumeRangeClass} />
|
|
1115
|
+
{#each thumbs as index (index)}
|
|
1116
|
+
<BitsSlider.Thumb {index} class={volumeThumbClass} />
|
|
1117
|
+
{/each}
|
|
1118
|
+
{/snippet}
|
|
1119
|
+
</BitsSlider.Root>
|
|
1120
|
+
</div>
|
|
1121
|
+
</div>
|
|
1122
|
+
{/if}
|
|
1123
|
+
|
|
1124
|
+
{#if showSpeed && !mobileMode}
|
|
1125
|
+
<DropdownMenu.Root
|
|
1126
|
+
onOpenChange={(open: boolean) => {
|
|
1127
|
+
if (open) remote.pauseUserIdle();
|
|
1128
|
+
else remote.resumeUserIdle();
|
|
1129
|
+
}}
|
|
1130
|
+
>
|
|
1131
|
+
<BitsDropdownMenu.Trigger>
|
|
1132
|
+
{#snippet child({ props })}
|
|
1133
|
+
<Button icon aria-label="Playback speed" {...props}>
|
|
1134
|
+
<Gauge class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1135
|
+
</Button>
|
|
1136
|
+
{/snippet}
|
|
1137
|
+
</BitsDropdownMenu.Trigger>
|
|
1138
|
+
<DropdownMenu.Content sideOffset={4}>
|
|
1139
|
+
<DropdownMenu.RadioGroup
|
|
1140
|
+
value={String(rateState)}
|
|
1141
|
+
onValueChange={(v: string) => remote.changePlaybackRate(Number(v))}
|
|
1142
|
+
>
|
|
1143
|
+
{#each rateOptions as r (r)}
|
|
1144
|
+
<DropdownMenu.RadioItem value={String(r)}>
|
|
1145
|
+
{#snippet children({ checked })}
|
|
1146
|
+
<span>{r === 1 ? 'Normal' : `${r}x`}</span>
|
|
1147
|
+
{#if checked}
|
|
1148
|
+
<Check class="ml-auto size-(--era-h-xs) text-muted" aria-hidden="true" />
|
|
1149
|
+
{/if}
|
|
1150
|
+
{/snippet}
|
|
1151
|
+
</DropdownMenu.RadioItem>
|
|
1152
|
+
{/each}
|
|
1153
|
+
</DropdownMenu.RadioGroup>
|
|
1154
|
+
</DropdownMenu.Content>
|
|
1155
|
+
</DropdownMenu.Root>
|
|
1156
|
+
{/if}
|
|
1157
|
+
|
|
1158
|
+
{#if showQualityMenu && !mobileMode}
|
|
1159
|
+
<DropdownMenu.Root
|
|
1160
|
+
onOpenChange={(open: boolean) => {
|
|
1161
|
+
if (open) remote.pauseUserIdle();
|
|
1162
|
+
else remote.resumeUserIdle();
|
|
1163
|
+
}}
|
|
1164
|
+
>
|
|
1165
|
+
<BitsDropdownMenu.Trigger>
|
|
1166
|
+
{#snippet child({ props })}
|
|
1167
|
+
<Button icon aria-label="Quality" {...props}>
|
|
1168
|
+
<Settings class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1169
|
+
</Button>
|
|
1170
|
+
{/snippet}
|
|
1171
|
+
</BitsDropdownMenu.Trigger>
|
|
1172
|
+
<DropdownMenu.Content sideOffset={4}>
|
|
1173
|
+
<DropdownMenu.RadioGroup
|
|
1174
|
+
value={qualityValue}
|
|
1175
|
+
onValueChange={(v: string) => changeQuality(v)}
|
|
1176
|
+
>
|
|
1177
|
+
<DropdownMenu.RadioItem value="auto">
|
|
1178
|
+
{#snippet children({ checked })}
|
|
1179
|
+
<span>Auto</span>
|
|
1180
|
+
{#if checked}
|
|
1181
|
+
<Check class="ml-auto size-(--era-h-xs) text-muted" aria-hidden="true" />
|
|
1182
|
+
{/if}
|
|
1183
|
+
{/snippet}
|
|
1184
|
+
</DropdownMenu.RadioItem>
|
|
1185
|
+
{#each qualityOptions as { q, index } (index)}
|
|
1186
|
+
<DropdownMenu.RadioItem value={String(index)}>
|
|
1187
|
+
{#snippet children({ checked })}
|
|
1188
|
+
<span>{q.height}p</span>
|
|
1189
|
+
{#if checked}
|
|
1190
|
+
<Check class="ml-auto size-(--era-h-xs) text-muted" aria-hidden="true" />
|
|
1191
|
+
{/if}
|
|
1192
|
+
{/snippet}
|
|
1193
|
+
</DropdownMenu.RadioItem>
|
|
1194
|
+
{/each}
|
|
1195
|
+
</DropdownMenu.RadioGroup>
|
|
1196
|
+
</DropdownMenu.Content>
|
|
1197
|
+
</DropdownMenu.Root>
|
|
1198
|
+
{/if}
|
|
1199
|
+
|
|
1200
|
+
{#if showPip && canEnterPip && !mobileMode}
|
|
1201
|
+
<Button
|
|
1202
|
+
icon
|
|
1203
|
+
aria-label="Picture-in-Picture"
|
|
1204
|
+
onclick={(e: MouseEvent) => remote.togglePictureInPicture(e)}
|
|
1205
|
+
>
|
|
1206
|
+
{#if pipState}
|
|
1207
|
+
<PictureInPicture2 class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1208
|
+
{:else}
|
|
1209
|
+
<PictureInPicture class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1210
|
+
{/if}
|
|
1211
|
+
</Button>
|
|
1212
|
+
{/if}
|
|
1213
|
+
|
|
1214
|
+
{#if showFullscreen && canEnterFullscreen && !mobileMode}
|
|
1215
|
+
<Button
|
|
1216
|
+
icon
|
|
1217
|
+
aria-label={fullscreenState ? 'Exit fullscreen' : 'Fullscreen'}
|
|
1218
|
+
onclick={(e: MouseEvent) => remote.toggleFullscreen(FULLSCREEN_TARGET, e)}
|
|
1219
|
+
>
|
|
1220
|
+
{#if fullscreenState}
|
|
1221
|
+
<Minimize class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1222
|
+
{:else}
|
|
1223
|
+
<Maximize class="size-(--era-h-xs)" aria-hidden="true" />
|
|
1224
|
+
{/if}
|
|
1225
|
+
</Button>
|
|
1226
|
+
{/if}
|
|
1227
|
+
</Bar>
|
|
1228
|
+
{/if}
|
|
1229
|
+
</media-player>
|
|
1230
|
+
{/snippet}
|
|
1231
|
+
|
|
1232
|
+
{#if showContextMenu && !mobileMode}
|
|
1233
|
+
<ContextMenu.Root
|
|
1234
|
+
onOpenChange={(open: boolean) => {
|
|
1235
|
+
if (open) remote.pauseUserIdle();
|
|
1236
|
+
else remote.resumeUserIdle();
|
|
1237
|
+
}}
|
|
1238
|
+
>
|
|
1239
|
+
<BitsContextMenu.Trigger>
|
|
1240
|
+
{#snippet child({ props: ctxProps }: { props: Record<string, unknown> })}
|
|
1241
|
+
{@render rootMediaPlayer(ctxProps)}
|
|
1242
|
+
{/snippet}
|
|
1243
|
+
</BitsContextMenu.Trigger>
|
|
1244
|
+
<ContextMenu.Content onOpenAutoFocus={(e: Event) => e.preventDefault()}>
|
|
1245
|
+
<ContextMenu.Item onSelect={togglePlay}>
|
|
1246
|
+
{#if ended}
|
|
1247
|
+
<RotateCcw class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1248
|
+
{:else if paused}
|
|
1249
|
+
<Play class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1250
|
+
{:else}
|
|
1251
|
+
<Pause class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1252
|
+
{/if}
|
|
1253
|
+
<span>{playLabel}</span>
|
|
1254
|
+
</ContextMenu.Item>
|
|
1255
|
+
|
|
1256
|
+
{#if showVolume && !mobileMode}
|
|
1257
|
+
<ContextMenu.Item onSelect={(e: Event) => remote.toggleMuted(e)}>
|
|
1258
|
+
{#if mutedState || volumeState === 0}
|
|
1259
|
+
<VolumeX class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1260
|
+
{:else if volumeState < 0.5}
|
|
1261
|
+
<Volume1 class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1262
|
+
{:else}
|
|
1263
|
+
<Volume2 class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1264
|
+
{/if}
|
|
1265
|
+
<span>{muteLabel}</span>
|
|
1266
|
+
</ContextMenu.Item>
|
|
1267
|
+
{/if}
|
|
1268
|
+
|
|
1269
|
+
{#if showSpeed && !mobileMode}
|
|
1270
|
+
<ContextMenu.Sub>
|
|
1271
|
+
<ContextMenu.SubTrigger>
|
|
1272
|
+
<Gauge class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1273
|
+
<span class="flex-1">Speed</span>
|
|
1274
|
+
<span class="ml-auto pl-(--era-gap) text-muted">
|
|
1275
|
+
{rateState === 1 ? '1x' : `${rateState}x`}
|
|
1276
|
+
</span>
|
|
1277
|
+
</ContextMenu.SubTrigger>
|
|
1278
|
+
<ContextMenu.SubContent>
|
|
1279
|
+
<ContextMenu.RadioGroup
|
|
1280
|
+
value={String(rateState)}
|
|
1281
|
+
onValueChange={(v: string) => remote.changePlaybackRate(Number(v))}
|
|
1282
|
+
>
|
|
1283
|
+
{#each rateOptions as r (r)}
|
|
1284
|
+
<ContextMenu.RadioItem value={String(r)}>
|
|
1285
|
+
<span>{r === 1 ? 'Normal' : `${r}x`}</span>
|
|
1286
|
+
</ContextMenu.RadioItem>
|
|
1287
|
+
{/each}
|
|
1288
|
+
</ContextMenu.RadioGroup>
|
|
1289
|
+
</ContextMenu.SubContent>
|
|
1290
|
+
</ContextMenu.Sub>
|
|
1291
|
+
{/if}
|
|
1292
|
+
|
|
1293
|
+
{#if showQualityMenu && !mobileMode}
|
|
1294
|
+
<ContextMenu.Sub>
|
|
1295
|
+
<ContextMenu.SubTrigger>
|
|
1296
|
+
<Settings class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1297
|
+
<span class="flex-1">Quality</span>
|
|
1298
|
+
<span class="ml-auto pl-(--era-gap) text-muted">{qualityHint}</span>
|
|
1299
|
+
</ContextMenu.SubTrigger>
|
|
1300
|
+
<ContextMenu.SubContent>
|
|
1301
|
+
<ContextMenu.RadioGroup
|
|
1302
|
+
value={qualityValue}
|
|
1303
|
+
onValueChange={(v: string) => changeQuality(v)}
|
|
1304
|
+
>
|
|
1305
|
+
<ContextMenu.RadioItem value="auto">Auto</ContextMenu.RadioItem>
|
|
1306
|
+
{#each qualityOptions as { q, index } (index)}
|
|
1307
|
+
<ContextMenu.RadioItem value={String(index)}>{q.height}p</ContextMenu.RadioItem>
|
|
1308
|
+
{/each}
|
|
1309
|
+
</ContextMenu.RadioGroup>
|
|
1310
|
+
</ContextMenu.SubContent>
|
|
1311
|
+
</ContextMenu.Sub>
|
|
1312
|
+
{/if}
|
|
1313
|
+
|
|
1314
|
+
{#if showPip && canEnterPip && !mobileMode}
|
|
1315
|
+
<ContextMenu.Item onSelect={(e: Event) => remote.togglePictureInPicture(e)}>
|
|
1316
|
+
{#if pipState}
|
|
1317
|
+
<PictureInPicture2 class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1318
|
+
{:else}
|
|
1319
|
+
<PictureInPicture class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1320
|
+
{/if}
|
|
1321
|
+
<span>{pipState ? 'Exit picture-in-picture' : 'Picture-in-picture'}</span>
|
|
1322
|
+
</ContextMenu.Item>
|
|
1323
|
+
{/if}
|
|
1324
|
+
|
|
1325
|
+
{#if canEnterFullscreen}
|
|
1326
|
+
<ContextMenu.Item onSelect={(e: Event) => remote.toggleFullscreen(FULLSCREEN_TARGET, e)}>
|
|
1327
|
+
{#if fullscreenState}
|
|
1328
|
+
<Minimize class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1329
|
+
{:else}
|
|
1330
|
+
<Maximize class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1331
|
+
{/if}
|
|
1332
|
+
<span>{fullscreenState ? 'Exit fullscreen' : 'Fullscreen'}</span>
|
|
1333
|
+
</ContextMenu.Item>
|
|
1334
|
+
{/if}
|
|
1335
|
+
|
|
1336
|
+
<ContextMenu.Separator />
|
|
1337
|
+
|
|
1338
|
+
<ContextMenu.Item onSelect={downloadVideo}>
|
|
1339
|
+
<Download class="mr-(--era-gap) size-(--era-h-xs)" aria-hidden="true" />
|
|
1340
|
+
<span>Download video</span>
|
|
1341
|
+
</ContextMenu.Item>
|
|
1342
|
+
</ContextMenu.Content>
|
|
1343
|
+
</ContextMenu.Root>
|
|
1344
|
+
{:else}
|
|
1345
|
+
{@render rootMediaPlayer({})}
|
|
1346
|
+
{/if}
|
|
1347
|
+
|
|
1348
|
+
<style>
|
|
1349
|
+
media-player :global(media-outlet) {
|
|
1350
|
+
display: block;
|
|
1351
|
+
width: 100%;
|
|
1352
|
+
}
|
|
1353
|
+
media-player :global(media-outlet video) {
|
|
1354
|
+
display: block;
|
|
1355
|
+
width: 100%;
|
|
1356
|
+
height: auto;
|
|
1357
|
+
}
|
|
1358
|
+
/* Hover keeps it open; `:has([data-active])` keeps it open while the
|
|
1359
|
+
bits-ui slider thumb is being dragged (its `data-active` is set on
|
|
1360
|
+
pointerdown and cleared on pointerup, even if the cursor strays
|
|
1361
|
+
outside the cluster). Plain CSS rather than Tailwind variants on the
|
|
1362
|
+
button sidesteps cascade ambiguity with `rounded-(--era-rd-md)`. */
|
|
1363
|
+
:global([data-volume-cluster] [data-volume-sleeve]) {
|
|
1364
|
+
visibility: hidden;
|
|
1365
|
+
}
|
|
1366
|
+
:global([data-volume-cluster]:hover [data-volume-sleeve]),
|
|
1367
|
+
:global([data-volume-cluster]:has([data-active]) [data-volume-sleeve]) {
|
|
1368
|
+
visibility: visible;
|
|
1369
|
+
}
|
|
1370
|
+
:global([data-volume-cluster]:hover [data-mute-button]),
|
|
1371
|
+
:global([data-volume-cluster]:has([data-active]) [data-mute-button]) {
|
|
1372
|
+
border-top-left-radius: 0;
|
|
1373
|
+
border-top-right-radius: 0;
|
|
1374
|
+
background-color: var(--color-hover);
|
|
1375
|
+
color: var(--color-bright);
|
|
1376
|
+
}
|
|
1377
|
+
</style>
|