feat: 引入videojs
This commit is contained in:
+4
-2
@@ -21,5 +21,7 @@
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
"registries": {
|
||||
"@videojs": "https://shadcn.videojs.org/r/react/{name}.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"@base-ui/react": "^1.8.0",
|
||||
"@tanstack/react-query": "^5.103.2",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
"@videojs/core": "10.0.1",
|
||||
"@videojs/react": "10.0.1",
|
||||
"axios": "^1.20.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cn": "^0.3.2",
|
||||
|
||||
Generated
+142
@@ -17,6 +17,12 @@ importers:
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.14.13
|
||||
version: 3.14.13(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@videojs/core':
|
||||
specifier: 10.0.1
|
||||
version: 10.0.1(react@19.2.8)
|
||||
'@videojs/react':
|
||||
specifier: 10.0.1
|
||||
version: 10.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
axios:
|
||||
specifier: ^1.20.0
|
||||
version: 1.20.0
|
||||
@@ -937,6 +943,81 @@ packages:
|
||||
resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@videojs/core@10.0.1':
|
||||
resolution: {integrity: sha512-N+f41wgFcmpe0QUhO0Q9sFHpMxKvr38ojkX5FlxAo3LKDAo2UrG2KIeklte+1VVae3oaJfP6J/KYtrRRObRxxw==}
|
||||
|
||||
'@videojs/media@10.0.1':
|
||||
resolution: {integrity: sha512-hMC2aS/8PovwJBi8y2qShC/amYfH6SBce7SZAM54qzRq8hl4FwWGFeUMMmXp8x4agRxLuCSqGqZg/EoGH2I5mQ==}
|
||||
|
||||
'@videojs/native-hls-video@10.0.1':
|
||||
resolution: {integrity: sha512-MElgeDaxOJUj+OjHV3++TuW8TTgipTuzlRkfFDMMbnutlPLkSlX3VKhXo5ZNPhhXkt47oBVx4RGnAQJLWzilPw==}
|
||||
|
||||
'@videojs/react@10.0.1':
|
||||
resolution: {integrity: sha512-qK2GrVnWM2Hadcj6bWHS+UGThTtFD5VNfUrwGoFA6+D2/iW1Q8VVIU6N3+JY0YEq8dazvPVMPpSVvnmfQmPK0w==}
|
||||
peerDependencies:
|
||||
'@videojs/cloudflare-video': 10.0.1
|
||||
'@videojs/dash-video': 10.0.1
|
||||
'@videojs/google-cast': 10.0.1
|
||||
'@videojs/hlsjs-video': 10.0.1
|
||||
'@videojs/mux-audio': 10.0.1
|
||||
'@videojs/mux-data': 10.0.1
|
||||
'@videojs/mux-video': 10.0.1
|
||||
'@videojs/shaka-video': 10.0.1
|
||||
'@videojs/spotify-audio': 10.0.1
|
||||
'@videojs/tiktok-video': 10.0.1
|
||||
'@videojs/twitch-video': 10.0.1
|
||||
'@videojs/vimeo-video': 10.0.1
|
||||
'@videojs/wistia-video': 10.0.1
|
||||
'@videojs/youtube-video': 10.0.1
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@videojs/cloudflare-video':
|
||||
optional: true
|
||||
'@videojs/dash-video':
|
||||
optional: true
|
||||
'@videojs/google-cast':
|
||||
optional: true
|
||||
'@videojs/hlsjs-video':
|
||||
optional: true
|
||||
'@videojs/mux-audio':
|
||||
optional: true
|
||||
'@videojs/mux-data':
|
||||
optional: true
|
||||
'@videojs/mux-video':
|
||||
optional: true
|
||||
'@videojs/shaka-video':
|
||||
optional: true
|
||||
'@videojs/spotify-audio':
|
||||
optional: true
|
||||
'@videojs/tiktok-video':
|
||||
optional: true
|
||||
'@videojs/twitch-video':
|
||||
optional: true
|
||||
'@videojs/vimeo-video':
|
||||
optional: true
|
||||
'@videojs/wistia-video':
|
||||
optional: true
|
||||
'@videojs/youtube-video':
|
||||
optional: true
|
||||
|
||||
'@videojs/spf@10.0.1':
|
||||
resolution: {integrity: sha512-1HUTI1W++EKZuO3MaklFdEU/cEUhkJlKKuQx0Xi3VwjeHgVToAz7b1ySDH903KluPQy6cAGqXpro5Wt00y4DYA==}
|
||||
|
||||
'@videojs/store@10.0.1':
|
||||
resolution: {integrity: sha512-BTkkTRHAU0XaZi+n3yWhtQJbe0BNjQLBd414u/s/nMZDz5bc1fp6b/voNbNriuSMd0Ro+ZmyzfPaMskiCa6GUQ==}
|
||||
peerDependencies:
|
||||
'@videojs/element': 10.0.1
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@videojs/element':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@videojs/utils@10.0.1':
|
||||
resolution: {integrity: sha512-S7WO9lPfyCz5YQYWtpE42JZTtJKUaiWaAZGAa+VLoBiFMfpoogCloSP4pjChTYcQ6sGcLlQJNxVKhM1RvI/Dig==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2656,6 +2737,11 @@ packages:
|
||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
react-compiler-runtime@1.0.0:
|
||||
resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental
|
||||
|
||||
react-dom@19.2.8:
|
||||
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
||||
peerDependencies:
|
||||
@@ -2831,6 +2917,9 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
signal-polyfill@0.2.2:
|
||||
resolution: {integrity: sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -4118,6 +4207,53 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@videojs/core@10.0.1(react@19.2.8)':
|
||||
dependencies:
|
||||
'@videojs/media': 10.0.1
|
||||
'@videojs/store': 10.0.1(react@19.2.8)
|
||||
'@videojs/utils': 10.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@videojs/element'
|
||||
- react
|
||||
|
||||
'@videojs/media@10.0.1':
|
||||
dependencies:
|
||||
'@videojs/utils': 10.0.1
|
||||
|
||||
'@videojs/native-hls-video@10.0.1':
|
||||
dependencies:
|
||||
'@videojs/media': 10.0.1
|
||||
'@videojs/utils': 10.0.1
|
||||
|
||||
'@videojs/react@10.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
'@videojs/core': 10.0.1(react@19.2.8)
|
||||
'@videojs/media': 10.0.1
|
||||
'@videojs/native-hls-video': 10.0.1
|
||||
'@videojs/spf': 10.0.1
|
||||
'@videojs/store': 10.0.1(react@19.2.8)
|
||||
'@videojs/utils': 10.0.1
|
||||
react: 19.2.8
|
||||
react-compiler-runtime: 1.0.0(react@19.2.8)
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
transitivePeerDependencies:
|
||||
- '@videojs/element'
|
||||
|
||||
'@videojs/spf@10.0.1':
|
||||
dependencies:
|
||||
'@videojs/media': 10.0.1
|
||||
'@videojs/utils': 10.0.1
|
||||
signal-polyfill: 0.2.2
|
||||
|
||||
'@videojs/store@10.0.1(react@19.2.8)':
|
||||
dependencies:
|
||||
'@videojs/utils': 10.0.1
|
||||
react-compiler-runtime: 1.0.0(react@19.2.8)
|
||||
optionalDependencies:
|
||||
react: 19.2.8
|
||||
|
||||
'@videojs/utils@10.0.1': {}
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
@@ -5941,6 +6077,10 @@ snapshots:
|
||||
iconv-lite: 0.7.3
|
||||
unpipe: 1.0.0
|
||||
|
||||
react-compiler-runtime@1.0.0(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
react-dom@19.2.8(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
@@ -6219,6 +6359,8 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
signal-polyfill@0.2.2: {}
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@4.0.0: {}
|
||||
|
||||
@@ -50,6 +50,81 @@
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-media-accent: var(--media-accent);
|
||||
--color-media-background: var(--media-background);
|
||||
--color-media-border: var(--media-border);
|
||||
--color-media-controls: var(--media-controls);
|
||||
--color-media-controls-foreground: var(--media-controls-foreground);
|
||||
--color-media-foreground: var(--media-foreground);
|
||||
--color-media-live: var(--media-live-color);
|
||||
--color-media-muted: var(--media-muted);
|
||||
--color-media-muted-foreground: var(--media-muted-foreground);
|
||||
--color-media-popover: var(--media-popover);
|
||||
--color-media-popover-foreground: var(--media-popover-foreground);
|
||||
--color-media-primary: var(--media-primary);
|
||||
--color-media-primary-foreground: var(--media-primary-foreground);
|
||||
--color-media-ring: var(--media-ring);
|
||||
--color-media-backdrop: var(--media-backdrop);
|
||||
--container-media-280: 17.5rem;
|
||||
--container-media-xs: 20rem;
|
||||
--container-media-360: 22.5rem;
|
||||
--container-media-sm: 24rem;
|
||||
--container-media-md: 28rem;
|
||||
--container-media-lg: 32rem;
|
||||
--container-media-xl: 36rem;
|
||||
--container-media-2xl: 42rem;
|
||||
--container-media-dialog: var(--media-dialog-max-width);
|
||||
--text-media-xs: .7em;
|
||||
--text-media-sm: calc(var(--media-spacing) * 2.75);
|
||||
--text-media: calc(var(--media-spacing) * 3.25);
|
||||
--text-media-lg: calc(var(--media-spacing) * 3.75);
|
||||
--text-media-xl: calc(var(--media-spacing) * 4.25);
|
||||
--spacing-media-icon-sm: calc(var(--media-icon-size, calc(var(--media-spacing) * 4.5)) * 7 / 9);
|
||||
--spacing-media-icon: var(--media-icon-size, calc(var(--media-spacing) * 4.5));
|
||||
--spacing-media-icon-lg: calc(var(--media-icon-size, calc(var(--media-spacing) * 4.5)) * 1.5);
|
||||
--spacing-media-icon-xl: calc(var(--media-icon-size, calc(var(--media-spacing) * 4.5)) * 2);
|
||||
--spacing-media-control: var(--media-control-size);
|
||||
--spacing-media-dialog-width: var(--media-dialog-width);
|
||||
--radius-media-control: var(--media-control-radius);
|
||||
--radius-media-controls: var(--media-controls-radius);
|
||||
--radius-media-dialog: var(--media-dialog-radius);
|
||||
--radius-media-menu-item: var(--media-menu-item-radius);
|
||||
--radius-media-pill: 9999px;
|
||||
--radius-media-player: var(--media-video-border-radius);
|
||||
--radius-media-popup: var(--media-popup-radius);
|
||||
--drop-shadow-media-icon: 0 1px 0 var(--media-shadow-current-color);
|
||||
--shadow-media-hairline: 0 0 0 1px var(--media-border);
|
||||
--shadow-media-separator: var(--media-shadow-separator);
|
||||
--shadow-media-thumb: var(--media-shadow-thumb);
|
||||
--shadow-media-tooltip: var(--media-shadow-tooltip);
|
||||
--text-shadow-media: 0 1px 0 var(--media-shadow-current-color);
|
||||
--text-shadow-media-dialog: var(--media-text-shadow-dialog);
|
||||
--backdrop-filter-media-indicator: var(--media-backdrop-filter-indicator);
|
||||
--backdrop-filter-media-dialog: var(--media-backdrop-filter-dialog);
|
||||
--duration-media-instant: var(--media-duration-instant);
|
||||
--duration-media-fast: var(--media-duration-fast);
|
||||
--duration-media-base: var(--media-duration);
|
||||
--duration-media-slow: var(--media-duration-slow);
|
||||
--duration-media-slower: var(--media-duration-slower);
|
||||
--duration-media-controls: var(--media-duration-controls);
|
||||
--duration-media-controls-enter: calc(var(--media-duration-controls) / 2);
|
||||
--duration-media-menu: var(--media-duration-menu);
|
||||
--duration-media-dialog: var(--media-duration-dialog);
|
||||
--duration-media-indicator: var(--media-duration-indicator);
|
||||
--duration-media-slider: var(--media-duration-slider);
|
||||
--delay-media-dialog: var(--media-delay-dialog);
|
||||
--scale-media-hidden: var(--media-hidden-scale);
|
||||
--scale-media-hidden-indicator: var(--media-hidden-indicator-scale);
|
||||
--scale-media-hidden-playback: var(--media-hidden-playback-scale);
|
||||
--scale-media-hidden-icon: var(--media-hidden-icon-scale);
|
||||
--scale-media-hidden-popup: var(--media-hidden-popup-scale);
|
||||
--scale-media-hidden-preview: var(--media-hidden-preview-scale);
|
||||
--blur-media-hidden: var(--media-hidden-blur);
|
||||
--blur-media-hidden-popup: var(--media-hidden-popup-blur);
|
||||
--spacing-media-hidden-offset: var(--media-hidden-offset);
|
||||
--spacing-media-hidden-indicator-offset: var(--media-hidden-indicator-offset);
|
||||
--spacing-media-hidden-preview-offset: var(--media-hidden-preview-offset);
|
||||
--spacing-media-hidden-seek-offset: var(--media-hidden-seek-offset);
|
||||
}
|
||||
|
||||
/* 仅深色主题::root 与 .dark 使用同一套 TorBox 品牌色,规范见 docs/design-system.md */
|
||||
@@ -151,3 +226,293 @@
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
@utility duration-media-* {
|
||||
transition-duration: --value(--duration-media-*);
|
||||
}
|
||||
|
||||
@utility delay-media-* {
|
||||
transition-delay: --value(--delay-media-*);
|
||||
}
|
||||
|
||||
@utility backdrop-filter-media-* {
|
||||
backdrop-filter: --value(--backdrop-filter-media-*);
|
||||
}
|
||||
|
||||
@utility font-media {
|
||||
font-family: var(--media-font-family, "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif);
|
||||
}
|
||||
|
||||
@custom-variant media-high-contrast {
|
||||
|
||||
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-280 {
|
||||
|
||||
@container media-root (width > theme(--container-media-280)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-280 {
|
||||
|
||||
@container media-root (width <= theme(--container-media-280)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-xs {
|
||||
|
||||
@container media-root (width >= theme(--container-media-xs)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-xs {
|
||||
|
||||
@container media-root (width < theme(--container-media-xs)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-360 {
|
||||
|
||||
@container media-root (width >= theme(--container-media-360)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-360 {
|
||||
|
||||
@container media-root (width < theme(--container-media-360)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-sm {
|
||||
|
||||
@container media-root (width >= theme(--container-media-sm)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-sm {
|
||||
|
||||
@container media-root (width < theme(--container-media-sm)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-md {
|
||||
|
||||
@container media-root (width >= theme(--container-media-md)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-md {
|
||||
|
||||
@container media-root (width < theme(--container-media-md)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-lg {
|
||||
|
||||
@container media-root (width >= theme(--container-media-lg)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-lg {
|
||||
|
||||
@container media-root (width < theme(--container-media-lg)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-xl {
|
||||
|
||||
@container media-root (width >= theme(--container-media-xl)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-xl {
|
||||
|
||||
@container media-root (width < theme(--container-media-xl)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-2xl {
|
||||
|
||||
@container media-root (width >= theme(--container-media-2xl)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-max-2xl {
|
||||
|
||||
@container media-root (width < theme(--container-media-2xl)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-highlighted {
|
||||
&:where(:focus-visible, [aria-expanded="true"], [data-highlighted]):where(:not([aria-disabled="true"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-transitioning {
|
||||
&:is([data-starting-style], [data-ending-style]) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant media-anchored {
|
||||
|
||||
@supports (top: anchor(top)) {
|
||||
|
||||
@slot ;
|
||||
}
|
||||
}
|
||||
|
||||
@utility highlight-media {
|
||||
background-color: var(--media-accent);
|
||||
color: var(--media-accent-foreground);
|
||||
}
|
||||
|
||||
@utility surface-media {
|
||||
box-shadow: 0 0 0 1px var(--media-surface-border, var(--media-border)), var(--media-shadow-surface);
|
||||
backdrop-filter: var(--media-backdrop-filter-surface);
|
||||
}
|
||||
|
||||
@utility surface-media-inset {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
box-shadow: var(--media-shadow-surface-inset);
|
||||
}
|
||||
|
||||
@utility surface-media-none {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@utility focus-ring-media {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@utility mask-media-volume {
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 100% 0;
|
||||
transition: mask-position var(--media-duration-instant) ease-out;
|
||||
}
|
||||
|
||||
@utility mask-media-volume-open {
|
||||
mask-image: linear-gradient(to right, transparent 10%, black 25%, black 100%);
|
||||
mask-position: 0 0;
|
||||
}
|
||||
|
||||
@utility nudge-media {
|
||||
transform: translateX(0.25px);
|
||||
transition: transform 300ms linear(0, -24 20%, 16 40%, -8 60%, 4 80%, 1);
|
||||
}
|
||||
|
||||
@utility layer-media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
@utility object-media {
|
||||
object-fit: var(--media-object-fit, contain);
|
||||
object-position: var(--media-object-position, center);
|
||||
}
|
||||
|
||||
@utility transition-media-popup {
|
||||
transition-property: opacity, filter, transform, scale;
|
||||
transition-duration: var(--media-duration-fast);
|
||||
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@utility transition-media-menu-resize {
|
||||
transition-property: opacity, filter, transform, scale, width, height;
|
||||
transition-duration: var(--media-duration-fast), var(--media-duration-fast), var(--media-duration-fast), var(--media-duration-fast), var(--media-duration-menu), var(--media-duration-menu);
|
||||
}
|
||||
|
||||
@utility motion-media-* {
|
||||
transition-property: --value([*]);
|
||||
will-change: --value([*]);
|
||||
}
|
||||
|
||||
@utility anchor-media-highlight {
|
||||
position: absolute;
|
||||
position-anchor: --media-menu-item-highlight-anchor;
|
||||
inset: anchor(inside);
|
||||
overflow-anchor: none;
|
||||
pointer-events: none;
|
||||
border-radius: var(--media-menu-item-radius);
|
||||
background-color: var(--media-accent);
|
||||
transition: inset var(--media-duration-fast) ease-in-out;
|
||||
}
|
||||
|
||||
@utility clip-media-x-* {
|
||||
left: calc(var(--media-slider-chapter-start, 0%) + var(--media-spacing) * var(--media-chapter-inset-start, 0));
|
||||
right: max( calc(100% - var(--value([*]), 0%)), calc(100% - var(--media-slider-chapter-end, 100%) + var(--media-spacing) * var(--media-chapter-inset-end, 0)) );
|
||||
clip-path: inset(-1px 0);
|
||||
}
|
||||
|
||||
@utility clip-media-y-* {
|
||||
bottom: calc(var(--media-slider-chapter-start, 0%) + var(--media-spacing) * var(--media-chapter-inset-start, 0));
|
||||
top: max( calc(100% - var(--value([*]), 0%)), calc(100% - var(--media-slider-chapter-end, 100%) + var(--media-spacing) * var(--media-chapter-inset-end, 0)) );
|
||||
clip-path: inset(0 -1px);
|
||||
}
|
||||
|
||||
@utility clip-media-chapter-x {
|
||||
clip-path: inset(0 calc(100% - var(--media-slider-chapter-end)) 0 var(--media-slider-chapter-start));
|
||||
}
|
||||
|
||||
@utility clip-media-chapter-y {
|
||||
clip-path: inset(calc(100% - var(--media-slider-chapter-end)) 0 var(--media-slider-chapter-start) 0);
|
||||
}
|
||||
|
||||
@utility clip-media-chapter-track-x {
|
||||
left: calc(var(--media-slider-chapter-start) + var(--media-spacing) * var(--media-chapter-inset-start));
|
||||
right: calc(100% - var(--media-slider-chapter-end) + var(--media-spacing) * var(--media-chapter-inset-end));
|
||||
clip-path: inset(-1px);
|
||||
}
|
||||
|
||||
@utility clip-media-chapter-track-y {
|
||||
top: calc(100% - var(--media-slider-chapter-end) + var(--media-spacing) * var(--media-chapter-inset-end));
|
||||
bottom: calc(var(--media-slider-chapter-start) + var(--media-spacing) * var(--media-chapter-inset-start));
|
||||
clip-path: inset(-1px);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "../base.css";
|
||||
@import "./theme.css";
|
||||
@@ -0,0 +1,59 @@
|
||||
@layer base.theme, base.preset, base.preferences;
|
||||
|
||||
@layer base.preset {
|
||||
/* Light colors for browsers without `light-dark()`; the `@supports` block below adds the dark ones. */
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"]) {
|
||||
--media-background: oklch(1 0 0);
|
||||
--media-foreground: oklch(0 0 0);
|
||||
--media-controls: oklch(1 0 0 / 0.5);
|
||||
--media-controls-foreground: var(--media-foreground);
|
||||
--media-popover: var(--media-controls);
|
||||
--media-popover-foreground: var(--media-foreground);
|
||||
--media-surface-border: oklch(0 0 0 / 0.05);
|
||||
--media-primary: var(--media-accent-color, var(--media-foreground));
|
||||
--media-border: oklch(0 0 0 / 0.1);
|
||||
--media-ring: var(--media-foreground);
|
||||
--media-default-accent-color: oklch(0 0 0);
|
||||
--media-default-accent-text-color: oklch(1 0 0);
|
||||
--media-internal-accent-text-fallback: oklch(1 0 0);
|
||||
--media-popover-boundary-offset: calc(var(--media-spacing) * 2);
|
||||
--media-popover-side-offset: calc(var(--media-spacing) * 3);
|
||||
--media-slider-preview-label-offset: calc(var(--media-spacing) * 10);
|
||||
}
|
||||
|
||||
/*
|
||||
* Keep `light-dark()` behind this check. A build that lowers it for Safari before 17.5, such as Next.js, leaves it
|
||||
* alone here but elsewhere rewrites it into variables that only the app's own `color-scheme` rules define.
|
||||
*/
|
||||
@supports (color: light-dark(red, red)) {
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"]) {
|
||||
--media-background: light-dark(oklch(1 0 0), oklch(0 0 0));
|
||||
--media-foreground: light-dark(oklch(0 0 0), oklch(1 0 0));
|
||||
--media-controls: light-dark(oklch(1 0 0 / 0.5), oklch(0 0 0 / 0.4));
|
||||
--media-border: light-dark(oklch(0 0 0 / 0.1), oklch(1 0 0 / 0.1));
|
||||
--media-default-accent-color: light-dark(oklch(0 0 0), oklch(1 0 0));
|
||||
--media-default-accent-text-color: light-dark(oklch(1 0 0), oklch(0 0 0));
|
||||
--media-internal-accent-text-fallback: light-dark(oklch(1 0 0), oklch(0 0 0));
|
||||
}
|
||||
}
|
||||
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"])[data-theme="neutral"] {
|
||||
--media-controls: var(--media-background);
|
||||
--media-popover: var(--media-background);
|
||||
--media-controls-radius: calc(var(--media-spacing) * 3.5);
|
||||
--media-popover-boundary-offset: calc(var(--media-spacing) * 3);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"]) {
|
||||
--media-controls: var(--media-background);
|
||||
--media-surface-border: oklch(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
@supports (color: light-dark(red, red)) {
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"]) {
|
||||
--media-surface-border: light-dark(oklch(0 0 0 / 0.05), transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
@layer base, components, utilities;
|
||||
/* Preset adaptations and preference overrides win over theme tokens regardless of import order. */
|
||||
@layer base.theme, base.preset, base.preferences;
|
||||
|
||||
@import "./themes/theme.css";
|
||||
@import "./themes/preferences.css";
|
||||
|
||||
/* Interpolate progress before chapter bounds are applied. */
|
||||
@property --media-slider-fill {
|
||||
syntax: "<percentage>";
|
||||
inherits: true;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
@property --media-slider-buffer {
|
||||
syntax: "<percentage>";
|
||||
inherits: true;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@scope (.media-skin) {
|
||||
:scope {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
text-align: start;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
:scope,
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-transform: none;
|
||||
letter-spacing: inherit;
|
||||
appearance: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
img,
|
||||
svg,
|
||||
media-icon {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
svg[fill="currentColor"] {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
svg[fill="none"] {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
svg[stroke="currentColor"] {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: inherit;
|
||||
object-fit: var(--media-object-fit, contain);
|
||||
object-position: var(--media-object-position, center);
|
||||
}
|
||||
|
||||
media-icon > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
media-tooltip-group,
|
||||
media-dialog,
|
||||
media-alert-dialog,
|
||||
media-error-dialog,
|
||||
media-controls {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
[hidden][hidden] {
|
||||
/* Hidden must override component display styles. */
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.media-skin {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
}
|
||||
|
||||
/* Without the Popover API nothing hides a closed popup, so hide it until its component opens it or while it exits. */
|
||||
@supports not selector(:popover-open) {
|
||||
:where(.media-skin) [popover]:not([data-open], [data-ending-style]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Popups stay inside the player without the top layer, so let them extend past the short audio bar. */
|
||||
.media-skin:is([data-preset="audio"], [data-preset="live-audio"]) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
@layer base.theme, base.preset, base.preferences;
|
||||
|
||||
@layer base.preferences {
|
||||
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
|
||||
.media-skin:not([data-controls-visible]) {
|
||||
--media-duration-controls: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (prefers-reduced-motion: no-preference) {
|
||||
.media-skin:not([data-controls-visible]) {
|
||||
--media-duration-controls: 150ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.media-skin {
|
||||
--media-duration-fast: var(--media-duration-instant);
|
||||
--media-duration: var(--media-duration-instant);
|
||||
--media-duration-slow: var(--media-duration-instant);
|
||||
--media-duration-slower: var(--media-duration-instant);
|
||||
--media-duration-controls: var(--media-duration-instant);
|
||||
--media-duration-dialog: var(--media-duration-instant);
|
||||
--media-duration-indicator: var(--media-duration-instant);
|
||||
--media-duration-menu: 0ms;
|
||||
--media-duration-slider: 0ms;
|
||||
--media-delay-dialog: 0ms;
|
||||
--media-hidden-scale: 1;
|
||||
--media-hidden-blur: 0px;
|
||||
--media-hidden-offset: 0px;
|
||||
--media-hidden-icon-scale: 1;
|
||||
--media-hidden-indicator-scale: 1;
|
||||
--media-hidden-indicator-offset: 0px;
|
||||
--media-hidden-playback-scale: 1;
|
||||
--media-hidden-popup-scale: 1;
|
||||
--media-hidden-popup-blur: 0px;
|
||||
--media-hidden-preview-scale: 1;
|
||||
--media-hidden-preview-offset: 0px;
|
||||
--media-hidden-seek-offset: 0px;
|
||||
--media-popup-translate-distance: 0px;
|
||||
--media-spinner-animation: none;
|
||||
--media-icon-airplay-fill-animation: none;
|
||||
--media-icon-airplay-triangle-animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
||||
.media-skin {
|
||||
--media-popover: var(--media-background);
|
||||
--media-scrollbar-thumb-color: color-mix(in oklab, currentColor 80%, transparent);
|
||||
--media-backdrop-filter-surface: none;
|
||||
--media-backdrop-filter-indicator: none;
|
||||
--media-backdrop-filter-dialog: none;
|
||||
--media-shadow-surface-inset: inset 0 1px 0 0 rgb(255 255 255 / 0.25), inset 0 0 0 1px rgb(255 255 255 / 0.125);
|
||||
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
@supports (color: oklch(from currentColor l c h)) {
|
||||
.media-skin {
|
||||
--media-scrollbar-thumb-color: oklch(from currentColor l c h / 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.media-skin {
|
||||
--media-border: CanvasText;
|
||||
--media-frame-border: CanvasText;
|
||||
--media-popover: Canvas;
|
||||
--media-popover-foreground: CanvasText;
|
||||
--media-ring: CanvasText;
|
||||
--media-shadow-surface-inset: inset 0 1px 0 0 CanvasText, inset 0 0 0 1px CanvasText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
@layer base.theme, base.preset, base.preferences;
|
||||
|
||||
@layer base.theme {
|
||||
/* Default theme tokens. Neutral selectors, preset adaptations, and preferences override these. */
|
||||
.media-skin {
|
||||
/* Colors. */
|
||||
--media-background: oklch(0 0 0);
|
||||
--media-foreground: oklch(1 0 0);
|
||||
--media-controls: oklch(0 0 0 / 0.35);
|
||||
--media-controls-foreground: var(--media-foreground);
|
||||
--media-popover: oklch(1 0 0 / 0.1);
|
||||
--media-popover-foreground: var(--media-foreground);
|
||||
--media-primary: var(--media-accent-color, var(--media-default-accent-color));
|
||||
/* Browsers without `contrast-color()` use the theme's default text color; see the `@supports` blocks below. */
|
||||
--media-primary-foreground: var(--media-accent-text-color, var(--media-default-accent-text-color));
|
||||
--media-accent: var(--media-accent-color, color-mix(in oklab, var(--media-default-accent-color) 10%, transparent));
|
||||
--media-accent-foreground: var(--media-accent-text-color, currentColor);
|
||||
--media-muted: oklch(1 0 0 / 0.15);
|
||||
--media-muted-foreground: oklch(1 0 0 / 0.65);
|
||||
--media-border: oklch(0 0 0 / 0.1);
|
||||
--media-ring: oklch(1 0 0);
|
||||
--media-backdrop: oklch(0 0 0);
|
||||
--media-live-color: oklch(0.65 0.22 27);
|
||||
|
||||
/* Shadows and surfaces. Preferences switch the backdrop filters off and strengthen the inset highlight. */
|
||||
--media-shadow-surface: 0 1px 3px 0 oklch(0 0 0 / 0.15), 0 1px 2px -1px oklch(0 0 0 / 0.15);
|
||||
--media-shadow-surface-inset: inset 0 1px 0 0 rgb(255 255 255 / 0.1), inset 0 0 0 1px rgb(255 255 255 / 0.05);
|
||||
--media-shadow-thumb: 0 0 0 1px rgb(0 0 0 / 0.1), 0 1px 3px 0 rgb(0 0 0 / 0.35), 0 1px 2px -1px rgb(0 0 0 / 0.35);
|
||||
--media-shadow-tooltip: 0 0 0 1px var(--media-border), var(--media-shadow-surface);
|
||||
--media-shadow-separator: 0 1px 0 0 oklch(1 0 0 / 0.075);
|
||||
--media-shadow-current-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
|
||||
--media-shadow-subtle-current-color: color-mix(in oklab, var(--media-shadow-current-color) 40%, transparent);
|
||||
--media-text-shadow-dialog: 0 1px 0 rgb(0 0 0 / 0.25);
|
||||
--media-backdrop-filter-surface: blur(16px) saturate(110%);
|
||||
--media-backdrop-filter-indicator: blur(8px);
|
||||
--media-backdrop-filter-dialog: blur(16px) saturate(110%);
|
||||
|
||||
/* Controls. */
|
||||
--media-control-corner-shape: round;
|
||||
--media-control-radius: 99px;
|
||||
--media-control-size: calc(var(--media-spacing) * 9);
|
||||
--media-controls-radius: var(--media-control-radius);
|
||||
--media-default-accent-color: oklch(1 0 0);
|
||||
--media-default-accent-text-color: oklch(0 0 0);
|
||||
|
||||
/* Motion. Preferences collapse durations and neutralize hidden-state values under reduced motion. */
|
||||
--media-duration-instant: 50ms;
|
||||
--media-duration-fast: 100ms;
|
||||
--media-duration: 150ms;
|
||||
--media-duration-slow: 200ms;
|
||||
--media-duration-slower: 250ms;
|
||||
--media-duration-controls: 100ms;
|
||||
--media-duration-dialog: 350ms;
|
||||
--media-duration-indicator: var(--media-duration-slower);
|
||||
--media-duration-menu: 250ms;
|
||||
--media-duration-slider: var(--media-duration-fast);
|
||||
--media-delay-dialog: 100ms;
|
||||
--media-hidden-scale: 0.95;
|
||||
--media-hidden-blur: 8px;
|
||||
--media-hidden-offset: calc(var(--media-spacing) * 1);
|
||||
--media-hidden-icon-scale: 0;
|
||||
--media-hidden-indicator-scale: 0.9;
|
||||
--media-hidden-indicator-offset: -25%;
|
||||
--media-hidden-playback-scale: 0.85;
|
||||
--media-hidden-popup-scale: 0.95;
|
||||
--media-hidden-popup-blur: 4px;
|
||||
--media-hidden-preview-scale: 0.8;
|
||||
--media-hidden-preview-offset: calc(var(--media-spacing) * 2);
|
||||
--media-hidden-seek-offset: 60%;
|
||||
|
||||
/* Popups and menus. */
|
||||
--media-menu-item-radius: calc(var(--media-spacing) * 2);
|
||||
--media-popover-boundary-offset: calc(var(--media-spacing) * 3);
|
||||
--media-popover-side-offset: calc(var(--media-spacing) * 3);
|
||||
--media-popup-radius: calc(var(--media-spacing) * 3);
|
||||
--media-popup-translate-distance: calc(var(--media-spacing) * 2);
|
||||
--media-tooltip-boundary-offset: var(--media-popover-boundary-offset);
|
||||
--media-tooltip-side-offset: var(--media-popover-side-offset);
|
||||
|
||||
/* Dialogs. */
|
||||
--media-dialog-radius: calc(var(--media-spacing) * 7);
|
||||
--media-dialog-width: calc(100% - var(--media-spacing) * 6);
|
||||
--media-dialog-max-width: calc(var(--media-spacing) * 72);
|
||||
|
||||
/* Sliders. */
|
||||
--media-slider-preview-label-offset: calc(var(--media-spacing) * 10.5);
|
||||
|
||||
/* Player frame and scale. */
|
||||
--media-video-border-radius: var(--media-border-radius, 28px);
|
||||
--media-scale: 1;
|
||||
--media-scrollbar-thumb-color: color-mix(in oklab, currentColor 30%, transparent);
|
||||
--media-spacing: calc(var(--media-scale-unit, 16px) * var(--media-scale) / 4);
|
||||
|
||||
scrollbar-color: var(--media-scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/*
|
||||
* Newer color functions stay behind these checks, and the rule above holds the fallback for browsers without them.
|
||||
* Relative colors that keep their origin's channels fall back to the same color through `color-mix()`.
|
||||
*/
|
||||
@supports (color: contrast-color(red)) {
|
||||
.media-skin {
|
||||
--media-primary-foreground: var(--media-accent-text-color, contrast-color(var(--media-primary)));
|
||||
--media-accent-foreground: var(
|
||||
--media-accent-text-color,
|
||||
contrast-color(var(--media-accent-color, var(--media-internal-accent-text-fallback, oklch(0 0 0))))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (color: oklch(from red l c h)) {
|
||||
.media-skin {
|
||||
--media-shadow-subtle-current-color: oklch(from var(--media-shadow-current-color) l c h / calc(alpha * 0.4));
|
||||
}
|
||||
}
|
||||
|
||||
@supports (color: oklch(from currentColor l c h)) {
|
||||
.media-skin {
|
||||
--media-scrollbar-thumb-color: oklch(from currentColor l c h / 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.media-skin::-webkit-scrollbar-thumb {
|
||||
background: var(--media-scrollbar-thumb-color);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.media-skin:fullscreen {
|
||||
--media-object-fit: contain;
|
||||
--media-video-border-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.media-skin:fullscreen {
|
||||
--media-scale: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.media-skin:fullscreen {
|
||||
--media-scale: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.media-skin:fullscreen {
|
||||
--media-scale: 1.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "../base.css";
|
||||
@import "./captions.css";
|
||||
@import "./theme.css";
|
||||
@@ -0,0 +1,37 @@
|
||||
@layer components {
|
||||
@scope (.media-skin) {
|
||||
:scope {
|
||||
--media-caption-track-duration: var(--media-duration-controls);
|
||||
--media-caption-track-delay: 25ms;
|
||||
--media-caption-track-y: calc(var(--media-spacing) * -2);
|
||||
}
|
||||
|
||||
video::-webkit-media-text-track-container {
|
||||
z-index: 20;
|
||||
font-family: inherit;
|
||||
scale: 0.98;
|
||||
translate: 0 var(--media-caption-track-y);
|
||||
transition: translate var(--media-caption-track-duration) ease-out;
|
||||
transition-delay: var(--media-caption-track-delay);
|
||||
}
|
||||
|
||||
:scope[data-controls-visible] {
|
||||
--media-caption-track-y: var(--media-caption-controls-y);
|
||||
}
|
||||
|
||||
/* Compat combines the offset and scale into one transform for browsers without individual transform properties. */
|
||||
:scope[data-theme="compat"] video::-webkit-media-text-track-container {
|
||||
scale: none;
|
||||
translate: none;
|
||||
transform: translateY(var(--media-caption-track-y, 0px)) scale(0.98);
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
/* Matches the `--container-media-2xl` layout breakpoint. */
|
||||
@container media-root (width >= 42rem) {
|
||||
:scope[data-theme="neutral"][data-controls-visible] video {
|
||||
--media-caption-track-y: calc(var(--media-spacing) * -12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@layer base.theme, base.preset, base.preferences;
|
||||
|
||||
@layer base.preset {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"]) {
|
||||
/* Light border for browsers without `light-dark()`; see the `@supports` block below. */
|
||||
--media-frame-border: var(--media-border-color, rgb(0 0 0 / 0.1));
|
||||
/* Scrims for browsers without relative colors or gradient interpolation; the `@supports` blocks below refine them. */
|
||||
--media-controls-gradient: linear-gradient(
|
||||
to top,
|
||||
color-mix(in oklab, var(--media-backdrop) 40%, transparent),
|
||||
color-mix(in oklab, var(--media-backdrop) 20%, transparent),
|
||||
color-mix(in oklab, var(--media-backdrop) 50%, transparent)
|
||||
);
|
||||
--media-caption-controls-y: calc(var(--media-spacing) * -14);
|
||||
--media-slider-preview-offset: calc(var(--media-spacing) * 9);
|
||||
--media-thumbnail-gradient: linear-gradient(
|
||||
to top,
|
||||
color-mix(in oklab, var(--media-backdrop) 50%, transparent),
|
||||
color-mix(in oklab, var(--media-backdrop) 10%, transparent),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
@supports (color: oklch(from red l c h)) and (background-image: linear-gradient(in oklab, red, red)) {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"]) {
|
||||
--media-controls-gradient: linear-gradient(
|
||||
to top in oklab,
|
||||
oklch(from var(--media-backdrop) l c h / 0.4),
|
||||
oklch(from var(--media-backdrop) l c h / 0.2),
|
||||
oklch(from var(--media-backdrop) l c h / 0.5)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (color: oklch(from red l c h)) {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"]) {
|
||||
--media-thumbnail-gradient: linear-gradient(
|
||||
to top,
|
||||
oklch(from var(--media-backdrop) l c h / 0.5),
|
||||
oklch(from var(--media-backdrop) l c h / 0.1),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"])[data-theme="neutral"] {
|
||||
--media-frame-border: var(--media-border-color, rgb(0 0 0 / 0.15));
|
||||
--media-controls-gradient: linear-gradient(
|
||||
to top,
|
||||
color-mix(in oklab, var(--media-backdrop) 50%, transparent),
|
||||
color-mix(in oklab, var(--media-backdrop) 20%, transparent)
|
||||
);
|
||||
--media-indicator-gradient: linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in oklab, var(--media-backdrop) 35%, transparent),
|
||||
color-mix(in oklab, var(--media-backdrop) 20%, transparent) calc(var(--media-spacing) * 12),
|
||||
transparent
|
||||
);
|
||||
--media-caption-controls-y: calc(var(--media-spacing) * -18);
|
||||
--media-slider-preview-offset: calc(var(--media-spacing) * 11);
|
||||
}
|
||||
|
||||
@supports (color: oklch(from red l c h)) and (background-image: linear-gradient(in oklab, red, red)) {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"])[data-theme="neutral"] {
|
||||
--media-controls-gradient: linear-gradient(
|
||||
to top in oklab,
|
||||
oklch(from var(--media-backdrop) l c h / 0.5),
|
||||
oklch(from var(--media-backdrop) l c h / 0.2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (color: oklch(from red l c h)) {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"])[data-theme="neutral"] {
|
||||
--media-indicator-gradient: linear-gradient(
|
||||
to bottom,
|
||||
oklch(from var(--media-backdrop) l c h / 0.35),
|
||||
oklch(from var(--media-backdrop) l c h / 0.2) calc(var(--media-spacing) * 12),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep `light-dark()` behind this check so a build that lowers it cannot rewrite it; see `audio/theme.css`. */
|
||||
@supports (color: light-dark(red, red)) {
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"]) {
|
||||
--media-frame-border: var(--media-border-color, light-dark(rgb(0 0 0 / 0.1), rgb(255 255 255 / 0.15)));
|
||||
}
|
||||
|
||||
.media-skin:is([data-preset="video"], [data-preset="live-video"])[data-theme="neutral"] {
|
||||
--media-frame-border: var(--media-border-color, light-dark(rgb(0 0 0 / 0.15), rgb(255 255 255 / 0.15)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { AirPlayButton as AirPlayButtonPrimitive } from '@videojs/react';
|
||||
import {
|
||||
AirPlayEnterIcon as AirPlayEnterIconPrimitive,
|
||||
AirPlayExitIcon as AirPlayExitIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
|
||||
return (
|
||||
<AirPlayButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/airplay',
|
||||
'not-data-[airplay-state=connected]:[--media-icon-airplay-fill-animation:none]',
|
||||
'not-data-[airplay-state=connected]:[--media-icon-airplay-triangle-animation:none]',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<AirPlayEnterIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-not-data-[airplay-state=connected]/airplay:scale-100',
|
||||
'group-not-data-[airplay-state=connected]/airplay:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<AirPlayExitIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-[airplay-state=connected]/airplay:scale-100',
|
||||
'group-data-[airplay-state=connected]/airplay:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</AirPlayButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { audioText } from '@videojs/core/i18n/text/menu';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { Text as TextPrimitive } from '@videojs/react';
|
||||
import { SpeechIcon as SpeechIconPrimitive } from '@videojs/react/icons';
|
||||
import { AudioTrackRadioGroup } from '@videojs/react/ui/audio-track-radio-group';
|
||||
|
||||
import { MenuChevron } from '@/components/videojs/ui/menu-chevron';
|
||||
import { RadioItem } from '@/components/videojs/ui/radio-item';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type AudioTrackMenuProps = Omit<Menu.RootProps, 'children'>;
|
||||
|
||||
export function AudioTrackMenu({ ...props }: AudioTrackMenuProps = {}) {
|
||||
return (
|
||||
<Menu.Root {...props}>
|
||||
<AudioTrackRadioGroup.Root>
|
||||
<Menu.Trigger
|
||||
className={cn(
|
||||
'group/menu-trigger-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'justify-between tabular-nums text-inherit',
|
||||
'data-[availability=unavailable]:hidden data-[availability=unsupported]:hidden',
|
||||
'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'media-anchored:data-highlighted:[anchor-name:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:media-highlighted:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<SpeechIconPrimitive
|
||||
className={cn(
|
||||
'shrink-0 drop-shadow-media-icon text-media-muted-foreground',
|
||||
'size-media-icon',
|
||||
'group-media-highlighted/menu-trigger-item:text-inherit'
|
||||
)}
|
||||
/>
|
||||
<TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
|
||||
<span className={'ms-auto inline-flex min-w-0 items-center gap-1 ps-2'}>
|
||||
<AudioTrackRadioGroup.Value className={'max-w-24 truncate opacity-65'} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Content
|
||||
className={cn(
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0',
|
||||
'not-data-submenu:flex not-data-submenu:flex-col not-data-submenu:gap-0.5',
|
||||
'not-data-submenu:data-child-open:blur-media-hidden',
|
||||
'not-data-submenu:data-child-open:before:hidden',
|
||||
'data-submenu:media-transitioning:pointer-events-none data-submenu:media-transitioning:overflow-hidden',
|
||||
'data-submenu:media-transitioning:blur-media-hidden',
|
||||
'absolute max-h-[inherit] overflow-auto overscroll-none outline-hidden',
|
||||
'transition-[translate,filter] duration-media-menu ease-out',
|
||||
'not-data-submenu:inset-x-1 not-data-submenu:top-1',
|
||||
'not-data-submenu:data-child-open:-translate-x-full',
|
||||
'not-data-submenu:data-child-open:rtl:translate-x-full',
|
||||
'data-submenu:inset-x-0 data-submenu:top-0 data-submenu:z-10 data-submenu:max-h-[inherit] data-submenu:p-1',
|
||||
'data-submenu:media-transitioning:translate-x-full data-submenu:media-transitioning:rtl:-translate-x-full'
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
className={cn(
|
||||
'group/menu-back-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'mb-0.5 w-full'
|
||||
)}
|
||||
>
|
||||
<MenuChevron back />
|
||||
<TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
|
||||
</Menu.Item>
|
||||
<Menu.Separator
|
||||
className={cn(
|
||||
'my-1 block border-b border-media-border media-high-contrast:border-media-foreground/25',
|
||||
'shadow-media-separator'
|
||||
)}
|
||||
/>
|
||||
<AudioTrackRadioGroup.Options
|
||||
className={cn(
|
||||
'flex [max-height:inherit] flex-col gap-0.5',
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0'
|
||||
)}
|
||||
renderItem={(props, item) => (
|
||||
<RadioItem {...props}>
|
||||
<span>{item.label}</span>
|
||||
</RadioItem>
|
||||
)}
|
||||
></AudioTrackRadioGroup.Options>
|
||||
</Menu.Content>
|
||||
</AudioTrackRadioGroup.Root>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { BufferingIndicator as BufferingIndicatorPrimitive } from '@videojs/react';
|
||||
import { SpinnerIcon as SpinnerIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;
|
||||
|
||||
export function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
|
||||
return (
|
||||
<BufferingIndicatorPrimitive
|
||||
className={(state) =>
|
||||
cn(
|
||||
'pointer-events-none absolute inset-0 hidden place-content-center text-media-controls-foreground',
|
||||
'before:absolute before:inset-0 before:bg-media-backdrop/35 before:backdrop-filter-media-indicator',
|
||||
'not-data-visible:[--media-spinner-animation:none] data-visible:grid',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<SpinnerIconPrimitive className={'relative z-30 size-media-icon drop-shadow-media-icon'} />
|
||||
</BufferingIndicatorPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { Tooltip } from '@videojs/react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface ButtonTooltipProps extends Omit<Tooltip.RootProps, 'children'> {
|
||||
children: ReactElement;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
export function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
|
||||
return (
|
||||
<Tooltip.Root {...props}>
|
||||
<Tooltip.Trigger render={children} />
|
||||
<Tooltip.Popup
|
||||
className={cn(
|
||||
'm-0 overflow-visible border-0 text-inherit',
|
||||
'media-transitioning:opacity-0 media-transitioning:blur-media-hidden-popup media-transitioning:scale-media-hidden-popup',
|
||||
'data-starting-style:transform-[translate(var(--media-popup-translate-x-distance,0),var(--media-popup-translate-y-distance,0))]',
|
||||
'data-ending-style:transform-none',
|
||||
'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left',
|
||||
'data-[side=top]:[--media-popup-translate-y-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=bottom]:[--media-popup-translate-y-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'data-[side=left]:[--media-popup-translate-x-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=right]:[--media-popup-translate-x-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'before:pointer-events-auto before:absolute',
|
||||
'data-[side=top]:before:inset-x-0 data-[side=top]:before:top-full',
|
||||
'data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:bottom-full',
|
||||
'data-[side=left]:before:inset-y-0 data-[side=left]:before:left-full',
|
||||
'data-[side=right]:before:inset-y-0 data-[side=right]:before:right-full',
|
||||
'data-[side=top]:before:h-(--media-popup-side-offset) data-[side=bottom]:before:h-(--media-popup-side-offset)',
|
||||
'data-[side=left]:before:w-(--media-popup-side-offset) data-[side=right]:before:w-(--media-popup-side-offset)',
|
||||
'transition-media-popup data-ending-style:duration-media-instant',
|
||||
'bg-media-popover text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'whitespace-nowrap rounded-media-control py-1 text-media [--media-popup-side-offset:var(--media-tooltip-side-offset)]',
|
||||
'data-open:flex data-open:items-center data-open:gap-1',
|
||||
'px-2.5'
|
||||
)}
|
||||
>
|
||||
{label ?? <Tooltip.Label />}
|
||||
{!label && (
|
||||
<Tooltip.Shortcut
|
||||
className={
|
||||
'min-w-[1.5em] rounded-[--spacing(1)] bg-media-muted p-[0.1em] text-center text-media-sm [font-family:inherit] font-semibold leading-tight'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Shared button carrying the base interactive styles used by media controls. */
|
||||
export type ButtonProps = ComponentProps<'button'>;
|
||||
|
||||
export function Button({ className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'grid size-media-control min-h-0 shrink-0 touch-manipulation select-none place-items-center rounded-media-control border-0 bg-transparent p-0 text-center text-inherit [corner-shape:var(--media-control-corner-shape)]',
|
||||
'cursor-pointer focus-ring-media',
|
||||
'will-change-[scale] duration-media-base ease-out [transition-property:background-color,color,outline-offset,scale]',
|
||||
'media-highlighted:highlight-media',
|
||||
'not-aria-disabled:hover:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'not-aria-disabled:active:scale-[0.97]',
|
||||
'motion-reduce:scale-100 motion-reduce:will-change-auto motion-reduce:[transition-property:background-color,color]',
|
||||
'aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { CaptionsButton as CaptionsButtonPrimitive } from '@videojs/react';
|
||||
import {
|
||||
CaptionsOffIcon as CaptionsOffIconPrimitive,
|
||||
CaptionsOnIcon as CaptionsOnIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
|
||||
return (
|
||||
<CaptionsButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/captions', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<CaptionsOffIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-not-data-active/captions:scale-100 group-not-data-active/captions:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<CaptionsOnIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-active/captions:scale-100 group-data-active/captions:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</CaptionsButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { captionsText } from '@videojs/core/i18n/text/menu';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { Text as TextPrimitive } from '@videojs/react';
|
||||
import { CaptionsOffIcon as CaptionsOffIconPrimitive } from '@videojs/react/icons';
|
||||
import { CaptionsRadioGroup } from '@videojs/react/ui/captions-radio-group';
|
||||
|
||||
import { MenuChevron } from '@/components/videojs/ui/menu-chevron';
|
||||
import { RadioItem } from '@/components/videojs/ui/radio-item';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type CaptionsSubmenuProps = Omit<Menu.RootProps, 'children'>;
|
||||
|
||||
export function CaptionsSubmenu({ ...props }: CaptionsSubmenuProps = {}) {
|
||||
return (
|
||||
<Menu.Root {...props}>
|
||||
<CaptionsRadioGroup.Root>
|
||||
<Menu.Trigger
|
||||
className={cn(
|
||||
'group/menu-trigger-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'justify-between tabular-nums text-inherit',
|
||||
'data-[availability=unavailable]:hidden data-[availability=unsupported]:hidden',
|
||||
'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'media-anchored:data-highlighted:[anchor-name:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:media-highlighted:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<CaptionsOffIconPrimitive
|
||||
className={cn(
|
||||
'shrink-0 drop-shadow-media-icon text-media-muted-foreground',
|
||||
'size-media-icon',
|
||||
'group-media-highlighted/menu-trigger-item:text-inherit'
|
||||
)}
|
||||
/>
|
||||
<TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
|
||||
<span className={'ms-auto inline-flex min-w-0 items-center gap-1 ps-2'}>
|
||||
<CaptionsRadioGroup.Value className={'max-w-24 truncate opacity-65'} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Content
|
||||
className={cn(
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0',
|
||||
'not-data-submenu:flex not-data-submenu:flex-col not-data-submenu:gap-0.5',
|
||||
'not-data-submenu:data-child-open:blur-media-hidden',
|
||||
'not-data-submenu:data-child-open:before:hidden',
|
||||
'data-submenu:media-transitioning:pointer-events-none data-submenu:media-transitioning:overflow-hidden',
|
||||
'data-submenu:media-transitioning:blur-media-hidden',
|
||||
'absolute max-h-[inherit] overflow-auto overscroll-none outline-hidden',
|
||||
'transition-[translate,filter] duration-media-menu ease-out',
|
||||
'not-data-submenu:inset-x-1 not-data-submenu:top-1',
|
||||
'not-data-submenu:data-child-open:-translate-x-full',
|
||||
'not-data-submenu:data-child-open:rtl:translate-x-full',
|
||||
'data-submenu:inset-x-0 data-submenu:top-0 data-submenu:z-10 data-submenu:max-h-[inherit] data-submenu:p-1',
|
||||
'data-submenu:media-transitioning:translate-x-full data-submenu:media-transitioning:rtl:-translate-x-full'
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
className={cn(
|
||||
'group/menu-back-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'mb-0.5 w-full'
|
||||
)}
|
||||
>
|
||||
<MenuChevron back />
|
||||
<TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
|
||||
</Menu.Item>
|
||||
<Menu.Separator
|
||||
className={cn(
|
||||
'my-1 block border-b border-media-border media-high-contrast:border-media-foreground/25',
|
||||
'shadow-media-separator'
|
||||
)}
|
||||
/>
|
||||
<CaptionsRadioGroup.Options
|
||||
className={cn(
|
||||
'flex [max-height:inherit] flex-col gap-0.5',
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0'
|
||||
)}
|
||||
renderItem={(props, item) => (
|
||||
<RadioItem {...props}>
|
||||
<span>{item.label}</span>
|
||||
</RadioItem>
|
||||
)}
|
||||
></CaptionsRadioGroup.Options>
|
||||
</Menu.Content>
|
||||
</CaptionsRadioGroup.Root>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { CastButton as CastButtonPrimitive } from '@videojs/react';
|
||||
import { CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function CastButton({ className, ...props }: CastButtonProps = {}) {
|
||||
return (
|
||||
<CastButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/cast', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<CastEnterIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-not-data-[cast-state=connected]/cast:scale-100',
|
||||
'group-not-data-[cast-state=connected]/cast:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<CastExitIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-[cast-state=connected]/cast:scale-100',
|
||||
'group-data-[cast-state=connected]/cast:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</CastButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { Container as ContainerPrimitive } from '@videojs/react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface ContainerProps extends Omit<ContainerPrimitive.Props, 'children'> {
|
||||
children?: ContainerPrimitive.Props['children'];
|
||||
}
|
||||
|
||||
export function Container({ children, className, ...props }: ContainerProps) {
|
||||
return (
|
||||
<ContainerPrimitive
|
||||
className={cn(
|
||||
'media-skin',
|
||||
'relative isolate block h-full w-full overflow-clip rounded-media-player bg-media-background @container/media-root',
|
||||
'[--spacing:var(--media-spacing)] font-media text-media leading-normal subpixel-antialiased',
|
||||
'outline-2 -outline-offset-4 outline-transparent transition-[outline-offset,outline-color] duration-media-fast ease-out',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'after:pointer-events-none after:absolute after:inset-0 after:z-10 after:rounded-[inherit]',
|
||||
'after:border after:border-(--media-frame-border) [&:fullscreen]:after:hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContainerPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { ErrorDialog as ErrorDialogPrimitive } from '@videojs/react';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function ErrorDialog() {
|
||||
return (
|
||||
<ErrorDialogPrimitive.Root>
|
||||
<ErrorDialogPrimitive.Backdrop
|
||||
className={cn(
|
||||
'absolute inset-0 z-40 bg-media-backdrop/20 opacity-100 backdrop-filter-media-dialog rounded-[inherit]',
|
||||
'not-data-open:hidden transition-opacity delay-media-dialog duration-media-dialog ease-out',
|
||||
'media-transitioning:opacity-0 data-ending-style:delay-0',
|
||||
'data-ending-style:duration-media-slower'
|
||||
)}
|
||||
/>
|
||||
<ErrorDialogPrimitive.Popup
|
||||
className={cn(
|
||||
'absolute top-1/2 left-1/2 z-50 flex w-media-dialog-width max-w-media-dialog max-h-[calc(100%-0.5rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-3 rounded-media-dialog text-media-popover-foreground outline-hidden not-data-open:hidden',
|
||||
'text-shadow-media-dialog transition-[opacity,scale] delay-media-dialog duration-media-dialog ease-out',
|
||||
'media-transitioning:scale-media-hidden-popup media-transitioning:opacity-0 data-ending-style:delay-0',
|
||||
'bg-media-popover surface-media after:surface-media-inset',
|
||||
'p-3',
|
||||
'data-ending-style:duration-media-slower'
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex min-h-0 flex-col gap-2 overflow-y-auto', 'px-2 pt-2 pb-1.5')}>
|
||||
<ErrorDialogPrimitive.Title className={'m-0 text-media-lg font-semibold leading-tight'} />
|
||||
<ErrorDialogPrimitive.Description className={'m-0 opacity-70 wrap-anywhere'} />
|
||||
</div>
|
||||
<div className={'flex shrink-0 gap-2'}>
|
||||
<ErrorDialogPrimitive.Close
|
||||
render={<Button />}
|
||||
className={
|
||||
'h-media-control w-full flex-1 bg-media-primary! px-4 py-2 font-medium text-media-primary-foreground!'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ErrorDialogPrimitive.Popup>
|
||||
</ErrorDialogPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { FullscreenButton as FullscreenButtonPrimitive } from '@videojs/react';
|
||||
import {
|
||||
FullscreenEnterIcon as FullscreenEnterIconPrimitive,
|
||||
FullscreenExitIcon as FullscreenExitIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
|
||||
return (
|
||||
<FullscreenButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/fullscreen', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<FullscreenEnterIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-not-data-fullscreen/fullscreen:scale-100 group-not-data-fullscreen/fullscreen:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<FullscreenExitIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-fullscreen/fullscreen:scale-100 group-data-fullscreen/fullscreen:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</FullscreenButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ChevronIcon as ChevronIconPrimitive } from '@videojs/react/icons';
|
||||
import type { ClassValue } from 'cn';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface MenuChevronProps {
|
||||
back?: boolean;
|
||||
className?: ClassValue;
|
||||
}
|
||||
|
||||
export function MenuChevron({ back = false, className }: MenuChevronProps = {}) {
|
||||
return (
|
||||
<ChevronIconPrimitive
|
||||
className={cn(
|
||||
back
|
||||
? 'shrink-0 drop-shadow-media-icon text-media-muted-foreground size-media-icon-sm rotate-180 rtl:rotate-0 rtl:scale-[1_1] group-media-highlighted/menu-back-item:text-inherit'
|
||||
: 'shrink-0 drop-shadow-media-icon text-media-muted-foreground size-media-icon-sm rtl:scale-[-1_1] group-media-highlighted/menu-trigger-item:text-inherit',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { MuteButton as MuteButtonPrimitive } from '@videojs/react';
|
||||
import {
|
||||
VolumeOffIcon as VolumeOffIconPrimitive,
|
||||
VolumeLowIcon as VolumeLowIconPrimitive,
|
||||
VolumeHighIcon as VolumeHighIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function MuteButton({ className, ...props }: MuteButtonProps = {}) {
|
||||
return (
|
||||
<MuteButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/mute', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<VolumeOffIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-muted/mute:scale-100 group-data-muted/mute:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<VolumeLowIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0',
|
||||
'group-not-data-muted/mute:group-data-[volume-level=low]/mute:opacity-100',
|
||||
'group-not-data-muted/mute:group-data-[volume-level=low]/mute:scale-100'
|
||||
)}
|
||||
/>
|
||||
<VolumeHighIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0',
|
||||
'group-not-data-muted/mute:group-not-data-[volume-level=low]/mute:opacity-100',
|
||||
'group-not-data-muted/mute:group-not-data-[volume-level=low]/mute:scale-100'
|
||||
)}
|
||||
/>
|
||||
</MuteButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { PiPButton as PiPButtonPrimitive } from '@videojs/react';
|
||||
import { PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function PiPButton({ className, ...props }: PiPButtonProps = {}) {
|
||||
return (
|
||||
<PiPButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/pip', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<PipEnterIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-not-data-pip/pip:scale-100 group-not-data-pip/pip:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<PipExitIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'opacity-0 group-data-pip/pip:scale-100 group-data-pip/pip:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</PiPButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { PlayButton as PlayButtonPrimitive } from '@videojs/react';
|
||||
import {
|
||||
RestartIcon as RestartIconPrimitive,
|
||||
PlayIcon as PlayIconPrimitive,
|
||||
PauseIcon as PauseIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;
|
||||
|
||||
export function PlayButton({ className, ...props }: PlayButtonProps = {}) {
|
||||
return (
|
||||
<PlayButtonPrimitive
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/play', resolveClassName(className, state))}
|
||||
{...props}
|
||||
>
|
||||
<RestartIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'scale-media-hidden-icon opacity-0 group-data-ended/play:scale-100 group-data-ended/play:opacity-100'
|
||||
)}
|
||||
/>
|
||||
<PlayIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'scale-media-hidden-icon opacity-0',
|
||||
'group-not-data-ended/play:group-data-paused/play:opacity-100',
|
||||
'group-not-data-ended/play:group-data-paused/play:scale-100',
|
||||
'group-not-data-ended/play:group-not-data-started/play:opacity-100',
|
||||
'group-not-data-ended/play:group-not-data-started/play:scale-100'
|
||||
)}
|
||||
/>
|
||||
<PauseIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'scale-media-hidden-icon opacity-0',
|
||||
'group-data-started/play:group-not-data-paused/play:group-not-data-ended/play:opacity-100',
|
||||
'group-data-started/play:group-not-data-paused/play:group-not-data-ended/play:scale-100'
|
||||
)}
|
||||
/>
|
||||
</PlayButtonPrimitive>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { speedText } from '@videojs/core/i18n/text/menu';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { Text as TextPrimitive } from '@videojs/react';
|
||||
import { SpeedIcon as SpeedIconPrimitive } from '@videojs/react/icons';
|
||||
import { PlaybackRateRadioGroup } from '@videojs/react/ui/playback-rate-radio-group';
|
||||
|
||||
import { MenuChevron } from '@/components/videojs/ui/menu-chevron';
|
||||
import { RadioItem } from '@/components/videojs/ui/radio-item';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type PlaybackRateSubmenuProps = Omit<Menu.RootProps, 'children'>;
|
||||
|
||||
export function PlaybackRateSubmenu({ ...props }: PlaybackRateSubmenuProps = {}) {
|
||||
return (
|
||||
<Menu.Root {...props}>
|
||||
<PlaybackRateRadioGroup.Root>
|
||||
<Menu.Trigger
|
||||
className={cn(
|
||||
'group/menu-trigger-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'justify-between tabular-nums text-inherit',
|
||||
'data-[availability=unavailable]:hidden data-[availability=unsupported]:hidden',
|
||||
'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'media-anchored:data-highlighted:[anchor-name:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:media-highlighted:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<SpeedIconPrimitive
|
||||
className={cn(
|
||||
'shrink-0 drop-shadow-media-icon text-media-muted-foreground',
|
||||
'size-media-icon',
|
||||
'group-media-highlighted/menu-trigger-item:text-inherit'
|
||||
)}
|
||||
/>
|
||||
<TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
|
||||
<span className={'ms-auto inline-flex min-w-0 items-center gap-1 ps-2'}>
|
||||
<PlaybackRateRadioGroup.Value className={'max-w-24 truncate opacity-65'} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Content
|
||||
className={cn(
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0',
|
||||
'not-data-submenu:flex not-data-submenu:flex-col not-data-submenu:gap-0.5',
|
||||
'not-data-submenu:data-child-open:blur-media-hidden',
|
||||
'not-data-submenu:data-child-open:before:hidden',
|
||||
'data-submenu:media-transitioning:pointer-events-none data-submenu:media-transitioning:overflow-hidden',
|
||||
'data-submenu:media-transitioning:blur-media-hidden',
|
||||
'absolute max-h-[inherit] overflow-auto overscroll-none outline-hidden',
|
||||
'transition-[translate,filter] duration-media-menu ease-out',
|
||||
'not-data-submenu:inset-x-1 not-data-submenu:top-1',
|
||||
'not-data-submenu:data-child-open:-translate-x-full',
|
||||
'not-data-submenu:data-child-open:rtl:translate-x-full',
|
||||
'data-submenu:inset-x-0 data-submenu:top-0 data-submenu:z-10 data-submenu:max-h-[inherit] data-submenu:p-1',
|
||||
'data-submenu:media-transitioning:translate-x-full data-submenu:media-transitioning:rtl:-translate-x-full'
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
className={cn(
|
||||
'group/menu-back-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'mb-0.5 w-full'
|
||||
)}
|
||||
>
|
||||
<MenuChevron back />
|
||||
<TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
|
||||
</Menu.Item>
|
||||
<Menu.Separator
|
||||
className={cn(
|
||||
'my-1 block border-b border-media-border media-high-contrast:border-media-foreground/25',
|
||||
'shadow-media-separator'
|
||||
)}
|
||||
/>
|
||||
<PlaybackRateRadioGroup.Options
|
||||
className={cn(
|
||||
'flex [max-height:inherit] flex-col gap-0.5',
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0'
|
||||
)}
|
||||
renderItem={(props, item) => (
|
||||
<RadioItem {...props}>
|
||||
<span>{item.label}</span>
|
||||
</RadioItem>
|
||||
)}
|
||||
></PlaybackRateRadioGroup.Options>
|
||||
</Menu.Content>
|
||||
</PlaybackRateRadioGroup.Root>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { Poster as PosterPrimitive } from '@videojs/react';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface PosterProps extends Omit<PosterPrimitive.ImageProps, 'children' | 'render'> {
|
||||
/** Draws the poster image in place of the one the skin renders. */
|
||||
renderImage?: PosterPrimitive.ImageProps['render'];
|
||||
|
||||
children?: PosterPrimitive.RootProps['children'];
|
||||
}
|
||||
|
||||
export function Poster({ children, className, renderImage, ...props }: PosterProps = {}) {
|
||||
return (
|
||||
<PosterPrimitive.Root
|
||||
className={(state) =>
|
||||
cn(
|
||||
'pointer-events-none layer-media',
|
||||
'transition-opacity duration-media-slower not-data-visible:opacity-0',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
>
|
||||
<PosterPrimitive.Image
|
||||
render={renderImage}
|
||||
className={cn('layer-media object-media', '[&:not([src]):not([srcset])]:invisible')}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</PosterPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { qualityText } from '@videojs/core/i18n/text/menu';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { Text as TextPrimitive } from '@videojs/react';
|
||||
import { SwitchesIcon as SwitchesIconPrimitive } from '@videojs/react/icons';
|
||||
import { QualityRadioGroup } from '@videojs/react/ui/quality-radio-group';
|
||||
|
||||
import { MenuChevron } from '@/components/videojs/ui/menu-chevron';
|
||||
import { RadioItem } from '@/components/videojs/ui/radio-item';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type QualityMenuProps = Omit<Menu.RootProps, 'children'>;
|
||||
|
||||
export function QualityMenu({ ...props }: QualityMenuProps = {}) {
|
||||
return (
|
||||
<Menu.Root {...props}>
|
||||
<QualityRadioGroup.Root>
|
||||
<Menu.Trigger
|
||||
className={cn(
|
||||
'group/menu-trigger-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'justify-between tabular-nums text-inherit',
|
||||
'data-[availability=unavailable]:hidden data-[availability=unsupported]:hidden',
|
||||
'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'media-anchored:data-highlighted:[anchor-name:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:media-highlighted:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<SwitchesIconPrimitive
|
||||
className={cn(
|
||||
'shrink-0 drop-shadow-media-icon text-media-muted-foreground',
|
||||
'size-media-icon',
|
||||
'group-media-highlighted/menu-trigger-item:text-inherit'
|
||||
)}
|
||||
/>
|
||||
<TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
|
||||
<span className={'ms-auto inline-flex min-w-0 items-center gap-1 ps-2'}>
|
||||
<QualityRadioGroup.Value className={'max-w-24 truncate opacity-65'} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Content
|
||||
className={cn(
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0',
|
||||
'not-data-submenu:flex not-data-submenu:flex-col not-data-submenu:gap-0.5',
|
||||
'not-data-submenu:data-child-open:blur-media-hidden',
|
||||
'not-data-submenu:data-child-open:before:hidden',
|
||||
'data-submenu:media-transitioning:pointer-events-none data-submenu:media-transitioning:overflow-hidden',
|
||||
'data-submenu:media-transitioning:blur-media-hidden',
|
||||
'absolute max-h-[inherit] overflow-auto overscroll-none outline-hidden',
|
||||
'transition-[translate,filter] duration-media-menu ease-out',
|
||||
'not-data-submenu:inset-x-1 not-data-submenu:top-1',
|
||||
'not-data-submenu:data-child-open:-translate-x-full',
|
||||
'not-data-submenu:data-child-open:rtl:translate-x-full',
|
||||
'data-submenu:inset-x-0 data-submenu:top-0 data-submenu:z-10 data-submenu:max-h-[inherit] data-submenu:p-1',
|
||||
'data-submenu:media-transitioning:translate-x-full data-submenu:media-transitioning:rtl:-translate-x-full'
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
className={cn(
|
||||
'group/menu-back-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'mb-0.5 w-full'
|
||||
)}
|
||||
>
|
||||
<MenuChevron back />
|
||||
<TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
|
||||
</Menu.Item>
|
||||
<Menu.Separator
|
||||
className={cn(
|
||||
'my-1 block border-b border-media-border media-high-contrast:border-media-foreground/25',
|
||||
'shadow-media-separator'
|
||||
)}
|
||||
/>
|
||||
<QualityRadioGroup.Options
|
||||
className={cn(
|
||||
'flex [max-height:inherit] flex-col gap-0.5',
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0'
|
||||
)}
|
||||
renderItem={(props, item) => (
|
||||
<RadioItem {...props}>
|
||||
<span>
|
||||
<span>{item.label}</span>
|
||||
{item.tier ? (
|
||||
<sup className={'ps-0.5 pt-px text-media-xs font-semibold leading-none opacity-70'}>
|
||||
{item.tier}
|
||||
</sup>
|
||||
) : null}
|
||||
</span>
|
||||
{item.badge ? (
|
||||
<span className={'rounded-media-control bg-media-accent px-1.5 text-media-xs font-semibold'}>
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
</RadioItem>
|
||||
)}
|
||||
></QualityRadioGroup.Options>
|
||||
</Menu.Content>
|
||||
</QualityRadioGroup.Root>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { CheckIcon as CheckIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface RadioItemProps extends Omit<Menu.RadioItemProps, 'children'> {
|
||||
children?: Menu.RadioItemProps['children'];
|
||||
}
|
||||
|
||||
export function RadioItem({ children, className, ...props }: RadioItemProps) {
|
||||
return (
|
||||
<Menu.RadioItem
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/menu-radio-item',
|
||||
'relative flex cursor-pointer select-none items-center gap-1.5 rounded-media-menu-item px-2 py-1.5 text-start whitespace-nowrap',
|
||||
'focus-ring-media',
|
||||
'media-highlighted:highlight-media',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'text-shadow-media',
|
||||
'transition-[background-color,color] duration-media-fast ease-in-out',
|
||||
'media-anchored:duration-media-instant media-anchored:media-highlighted:duration-media-slow',
|
||||
'justify-between tabular-nums text-inherit',
|
||||
'data-[availability=unavailable]:hidden data-[availability=unsupported]:hidden',
|
||||
'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'media-anchored:data-highlighted:[anchor-name:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:media-highlighted:bg-transparent',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Menu.ItemIndicator
|
||||
forceMount
|
||||
className={'ms-auto -me-1 shrink-0 opacity-0 group-aria-checked/menu-radio-item:opacity-100'}
|
||||
>
|
||||
<CheckIconPrimitive
|
||||
className={cn(
|
||||
'shrink-0 drop-shadow-media-icon text-media-muted-foreground',
|
||||
'size-media-icon',
|
||||
'group-media-highlighted/menu-radio-item:text-inherit'
|
||||
)}
|
||||
/>
|
||||
</Menu.ItemIndicator>
|
||||
</Menu.RadioItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { SeekIndicator as SeekIndicatorPrimitive } from '@videojs/react';
|
||||
import { ChevronIcon as ChevronIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type SeekIndicatorProps = Omit<SeekIndicatorPrimitive.RootProps, 'children'>;
|
||||
|
||||
export function SeekIndicator({ className, ...props }: SeekIndicatorProps = {}) {
|
||||
return (
|
||||
<SeekIndicatorPrimitive.Root
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/seek-status col-start-2 row-start-1 grid place-content-center gap-1 p-4 text-center',
|
||||
'media-2xl:p-6',
|
||||
'data-[direction=backward]:col-start-1 data-[direction=backward]:justify-self-start',
|
||||
'data-[direction=forward]:col-start-3 data-[direction=forward]:justify-self-end',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<ChevronIconPrimitive
|
||||
className={cn(
|
||||
'hidden size-media-icon-lg group-data-direction/seek-status:block',
|
||||
'group-data-[direction=backward]/seek-status:scale-[-1_1]',
|
||||
'transition-[translate,opacity] duration-media-slow ease-in-out',
|
||||
'group-media-transitioning/seek-status:opacity-0',
|
||||
'group-data-[direction=forward]/seek-status:group-data-starting-style/seek-status:-translate-x-media-hidden-seek-offset',
|
||||
'group-data-[direction=backward]/seek-status:group-data-starting-style/seek-status:translate-x-media-hidden-seek-offset'
|
||||
)}
|
||||
/>
|
||||
<SeekIndicatorPrimitive.Value className={'tabular-nums'} />
|
||||
</SeekIndicatorPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { settingsText } from '@videojs/core/i18n/text/menu';
|
||||
import { Menu } from '@videojs/react';
|
||||
import { Text as TextPrimitive } from '@videojs/react';
|
||||
import { GearIcon as GearIconPrimitive } from '@videojs/react/icons';
|
||||
import type { ClassValue } from 'cn';
|
||||
|
||||
import { Button } from '@/components/videojs/ui/button';
|
||||
import { ButtonTooltip } from '@/components/videojs/ui/button-tooltip';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface SettingsMenuProps extends Omit<Menu.RootProps, 'children'> {
|
||||
className?: ClassValue;
|
||||
|
||||
children?: Menu.ContentProps['children'];
|
||||
}
|
||||
|
||||
export function SettingsMenu({ children, className, ...props }: SettingsMenuProps) {
|
||||
return (
|
||||
<Menu.Root side="top" align="center" {...props}>
|
||||
<ButtonTooltip label={<TextPrimitive token={settingsText.key}>{settingsText.text}</TextPrimitive>} side="top">
|
||||
<Menu.Trigger
|
||||
render={<Button />}
|
||||
className={(state) => cn('group/settings', resolveClassName(className, state))}
|
||||
>
|
||||
<GearIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 size-media-icon drop-shadow-media-icon [text-shadow:inherit]',
|
||||
'transition-transform duration-media-base ease-in-out motion-reduce:transition-none',
|
||||
'group-aria-expanded/settings:rotate-90'
|
||||
)}
|
||||
/>
|
||||
<TextPrimitive className={'sr-only'} token={settingsText.key}>
|
||||
{settingsText.text}
|
||||
</TextPrimitive>
|
||||
</Menu.Trigger>
|
||||
</ButtonTooltip>
|
||||
<Menu.Popup
|
||||
keepMounted
|
||||
className={cn(
|
||||
'm-0 overflow-visible border-0 text-inherit',
|
||||
'media-transitioning:opacity-0 media-transitioning:blur-media-hidden-popup media-transitioning:scale-media-hidden-popup',
|
||||
'data-starting-style:transform-[translate(var(--media-popup-translate-x-distance,0),var(--media-popup-translate-y-distance,0))]',
|
||||
'data-ending-style:transform-none',
|
||||
'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left',
|
||||
'data-[side=top]:[--media-popup-translate-y-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=bottom]:[--media-popup-translate-y-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'data-[side=left]:[--media-popup-translate-x-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=right]:[--media-popup-translate-x-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'bg-media-popover text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'm-0 min-w-44 max-w-(--media-menu-available-width) overflow-hidden! rounded-media-popup border-0 p-1 [--media-popup-side-offset:var(--media-popover-side-offset)]',
|
||||
'max-h-[min(var(--media-menu-available-height,--spacing(56)),--spacing(56))] overscroll-none',
|
||||
'h-(--media-menu-height) w-(--media-menu-width)',
|
||||
'transition-media-popup media-transitioning:transition-media-popup',
|
||||
'transition-media-menu-resize'
|
||||
)}
|
||||
>
|
||||
<Menu.Content
|
||||
className={cn(
|
||||
'[anchor-scope:--media-menu-item-highlight-anchor]',
|
||||
'media-anchored:before:anchor-media-highlight',
|
||||
'media-transitioning:before:hidden',
|
||||
'media-transitioning:[&_*]:before:hidden',
|
||||
'media-anchored:has-data-[highlighted=]:before:duration-0',
|
||||
'not-data-submenu:flex not-data-submenu:flex-col not-data-submenu:gap-0.5',
|
||||
'not-data-submenu:data-child-open:blur-media-hidden',
|
||||
'not-data-submenu:data-child-open:before:hidden',
|
||||
'data-submenu:media-transitioning:pointer-events-none data-submenu:media-transitioning:overflow-hidden',
|
||||
'data-submenu:media-transitioning:blur-media-hidden',
|
||||
'absolute max-h-[inherit] overflow-auto overscroll-none outline-hidden',
|
||||
'transition-[translate,filter] duration-media-menu ease-out',
|
||||
'not-data-submenu:inset-x-1 not-data-submenu:top-1',
|
||||
'not-data-submenu:data-child-open:-translate-x-full',
|
||||
'not-data-submenu:data-child-open:rtl:translate-x-full',
|
||||
'data-submenu:inset-x-0 data-submenu:top-0 data-submenu:z-10 data-submenu:max-h-[inherit] data-submenu:p-1',
|
||||
'data-submenu:media-transitioning:translate-x-full data-submenu:media-transitioning:rtl:-translate-x-full'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Menu.Content>
|
||||
</Menu.Popup>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Shared slider track. */
|
||||
export type SliderTrackProps = ComponentProps<'div'>;
|
||||
|
||||
export function SliderTrack({ className, ...props }: SliderTrackProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative isolate w-full select-none rounded-media-pill before:pointer-events-none before:absolute before:inset-0 before:rounded-media-control before:bg-current/20',
|
||||
'data-[orientation=horizontal]:h-1 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Shared slider fill. */
|
||||
export type SliderFillProps = ComponentProps<'div'>;
|
||||
|
||||
export function SliderFill({ className, ...props }: SliderFillProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 before:absolute before:size-full before:rounded-media-control',
|
||||
'data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:min-w-1',
|
||||
'data-[orientation=vertical]:before:bottom-0 data-[orientation=vertical]:before:min-h-1',
|
||||
'before:bg-media-primary',
|
||||
'data-[orientation=horizontal]:clip-media-x-[--media-slider-fill]',
|
||||
'group-data-dragging/slider:data-[orientation=horizontal]:clip-media-x-[--media-slider-pointer]',
|
||||
'data-[orientation=vertical]:clip-media-y-[--media-slider-fill]',
|
||||
'group-data-dragging/slider:data-[orientation=vertical]:clip-media-y-[--media-slider-pointer]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Shared slider buffer. */
|
||||
export type SliderBufferProps = ComponentProps<'div'>;
|
||||
|
||||
export function SliderBuffer({ className, ...props }: SliderBufferProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 before:absolute before:size-full before:rounded-media-control',
|
||||
'data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:min-w-1',
|
||||
'data-[orientation=vertical]:before:bottom-0 data-[orientation=vertical]:before:min-h-1',
|
||||
'before:bg-current/20',
|
||||
'data-[orientation=horizontal]:clip-media-x-[--media-slider-buffer]',
|
||||
'data-[orientation=vertical]:clip-media-y-[--media-slider-buffer]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Shared slider thumb. */
|
||||
export type SliderThumbProps = ComponentProps<'div'>;
|
||||
|
||||
export function SliderThumb({ className, ...props }: SliderThumbProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute z-10 top-1/2 left-(--media-slider-fill,0%) size-3 -translate-x-1/2 -translate-y-1/2 rounded-media-control bg-white',
|
||||
'select-none transition-[opacity,height,width,outline-offset,scale] duration-media-slider ease-out',
|
||||
'group-data-dragging/slider:scale-90',
|
||||
'data-[orientation=vertical]:top-[calc(100%-var(--media-slider-fill,0%))] data-[orientation=vertical]:left-1/2',
|
||||
'group-data-dragging/slider:data-[orientation=horizontal]:left-(--media-slider-pointer)',
|
||||
'group-data-dragging/slider:data-[orientation=vertical]:top-[calc(100%-var(--media-slider-pointer))]',
|
||||
'outline-transparent shadow-media-thumb',
|
||||
'outline-4 -outline-offset-4',
|
||||
'hover:outline-current/15 hover:outline-offset-0 focus-visible:outline-current/15 focus-visible:outline-offset-0',
|
||||
'after:pointer-events-none after:absolute after:-inset-1 after:scale-50 after:rounded-[inherit] after:opacity-0',
|
||||
'after:shadow-[0_0_0_2px_currentColor] after:transition-[opacity,scale] after:duration-media-base after:ease-out',
|
||||
'focus-visible:after:scale-100 focus-visible:after:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { StatusAnnouncer as StatusAnnouncerPrimitive } from '@videojs/react';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;
|
||||
|
||||
export function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
|
||||
return (
|
||||
<StatusAnnouncerPrimitive className={(state) => cn('sr-only', resolveClassName(className, state))} {...props} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { StatusIndicator as StatusIndicatorPrimitive } from '@videojs/react';
|
||||
import {
|
||||
CaptionsOnIcon as CaptionsOnIconPrimitive,
|
||||
CaptionsOffIcon as CaptionsOffIconPrimitive,
|
||||
FullscreenEnterIcon as FullscreenEnterIconPrimitive,
|
||||
FullscreenExitIcon as FullscreenExitIconPrimitive,
|
||||
PipEnterIcon as PipEnterIconPrimitive,
|
||||
PipExitIcon as PipExitIconPrimitive,
|
||||
PlayIcon as PlayIconPrimitive,
|
||||
PauseIcon as PauseIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;
|
||||
|
||||
const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;
|
||||
|
||||
export type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;
|
||||
|
||||
export function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
|
||||
return (
|
||||
<StatusIndicatorPrimitive.Root
|
||||
actions={TOP_STATUS_ACTIONS}
|
||||
className={(state) =>
|
||||
cn(
|
||||
'pointer-events-none absolute origin-top',
|
||||
'duration-media-fast ease-out',
|
||||
'media-transitioning:opacity-0 media-transitioning:duration-media-indicator media-transitioning:ease-in',
|
||||
'top-3 rounded-media-control font-medium',
|
||||
'bg-media-backdrop/25 text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'media-high-contrast:bg-media-background',
|
||||
'pointer-coarse:motion-media-[scale,translate,opacity]',
|
||||
'pointer-fine:motion-media-[scale,translate,filter,opacity]',
|
||||
'pointer-fine:media-transitioning:scale-media-hidden-indicator pointer-fine:media-transitioning:blur-media-hidden',
|
||||
'data-ending-style:translate-y-media-hidden-indicator-offset',
|
||||
'group/input-status',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('items-center justify-between gap-2 px-2.5 py-1', 'flex', 'w-full')}>
|
||||
<CaptionsOnIconPrimitive
|
||||
className={cn(
|
||||
'hidden shrink-0',
|
||||
'group-data-[status=captions-on]/input-status:block',
|
||||
'mix-blend-difference'
|
||||
)}
|
||||
/>
|
||||
<CaptionsOffIconPrimitive
|
||||
className={cn(
|
||||
'hidden shrink-0',
|
||||
'group-data-[status=captions-off]/input-status:block',
|
||||
'mix-blend-difference'
|
||||
)}
|
||||
/>
|
||||
<FullscreenEnterIconPrimitive
|
||||
className={cn('hidden shrink-0', 'group-data-[status=fullscreen]/input-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<FullscreenExitIconPrimitive
|
||||
className={cn(
|
||||
'hidden shrink-0',
|
||||
'group-data-[status=exit-fullscreen]/input-status:block',
|
||||
'mix-blend-difference'
|
||||
)}
|
||||
/>
|
||||
<PipEnterIconPrimitive
|
||||
className={cn('hidden shrink-0', 'group-data-[status=pip]/input-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<PipExitIconPrimitive
|
||||
className={cn('hidden shrink-0', 'group-data-[status=exit-pip]/input-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<StatusIndicatorPrimitive.Value className={cn('ml-auto', 'mix-blend-difference')} />
|
||||
</div>
|
||||
</StatusIndicatorPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;
|
||||
|
||||
export function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
|
||||
return (
|
||||
<StatusIndicatorPrimitive.Root
|
||||
actions={PLAYBACK_STATUS_ACTIONS}
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/playback-status col-start-2 row-start-1 grid place-content-center p-4 text-center',
|
||||
'transition-[opacity,scale] duration-media-slow ease-out',
|
||||
'media-transitioning:scale-media-hidden-playback media-transitioning:opacity-0',
|
||||
'data-ending-style:duration-media-fast data-ending-style:ease-in',
|
||||
'rounded-media-pill bg-media-backdrop/35 backdrop-filter-media-indicator',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<PlayIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 scale-media-hidden-icon opacity-0',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'group-data-[status=play]/playback-status:scale-100 group-data-[status=play]/playback-status:opacity-100',
|
||||
'size-media-icon-lg',
|
||||
'group-data-[status=play]/playback-status:translate-x-px'
|
||||
)}
|
||||
/>
|
||||
<PauseIconPrimitive
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 scale-media-hidden-icon opacity-0',
|
||||
'transition-[opacity,scale] duration-media-base ease-out',
|
||||
'group-data-[status=pause]/playback-status:scale-100 group-data-[status=pause]/playback-status:opacity-100',
|
||||
'size-media-icon-lg'
|
||||
)}
|
||||
/>
|
||||
</StatusIndicatorPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import type { SliderPreviewOverflow } from '@videojs/core';
|
||||
import { Slider as SliderPrimitive, TimeSlider as TimeSliderPrimitive } from '@videojs/react';
|
||||
import { SpinnerIcon as SpinnerIconPrimitive } from '@videojs/react/icons';
|
||||
|
||||
import { SliderBuffer, SliderFill, SliderThumb, SliderTrack } from '@/components/videojs/ui/slider';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TimeSliderProps extends Omit<TimeSliderPrimitive.RootProps, 'children'> {
|
||||
previewOverflow?: SliderPreviewOverflow | undefined;
|
||||
/** Draws the thumbnail preview image in place of the one the skin renders. */
|
||||
renderThumbnail?: SliderPrimitive.Thumbnail.ImageProps['render'];
|
||||
}
|
||||
|
||||
export function TimeSlider({
|
||||
className,
|
||||
previewOverflow = 'visible',
|
||||
renderThumbnail,
|
||||
...props
|
||||
}: TimeSliderProps = {}) {
|
||||
return (
|
||||
<TimeSliderPrimitive.Root
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/slider relative flex flex-1 cursor-pointer items-center justify-center outline-hidden',
|
||||
'data-disabled:pointer-events-none',
|
||||
'transition-[--media-slider-fill,--media-slider-buffer] duration-media-slider ease-out data-dragging:duration-0',
|
||||
'rounded-media-pill',
|
||||
'data-[orientation=horizontal]:h-(--media-slider-height,--spacing(8))',
|
||||
'data-[orientation=vertical]:w-8 data-[orientation=vertical]:min-w-0',
|
||||
'data-[orientation=horizontal]:min-w-18 data-[orientation=vertical]:h-18',
|
||||
'media-time-slider',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<TimeSliderPrimitive.Chapters
|
||||
className={'relative flex size-full min-h-0 min-w-0 flex-1 items-center rounded-[inherit]'}
|
||||
renderChapter={(props) => (
|
||||
<div
|
||||
className={cn(
|
||||
'group/chapter absolute inset-0 flex min-h-0 min-w-0 items-center justify-center',
|
||||
'[--media-chapter-inset-start:0.5] [--media-chapter-inset-end:0.5]',
|
||||
'first-of-type:[--media-chapter-inset-start:0] last-of-type:[--media-chapter-inset-end:0]',
|
||||
'data-[orientation=horizontal]:clip-media-chapter-x data-[orientation=vertical]:clip-media-chapter-y'
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<TimeSliderPrimitive.Track
|
||||
render={<SliderTrack />}
|
||||
className={cn(
|
||||
'transition-[height,width] duration-media-slow ease-out',
|
||||
'data-[orientation=horizontal]:before:clip-media-chapter-track-x data-[orientation=vertical]:before:clip-media-chapter-track-y',
|
||||
'group-data-highlighted/chapter:data-[orientation=horizontal]:h-1.75',
|
||||
'group-data-highlighted/chapter:data-[orientation=vertical]:w-1.75'
|
||||
)}
|
||||
>
|
||||
<TimeSliderPrimitive.Buffer
|
||||
render={<SliderBuffer />}
|
||||
className={cn(
|
||||
'group-data-highlighted/chapter:data-[orientation=horizontal]:before:min-w-1.75',
|
||||
'group-data-highlighted/chapter:data-[orientation=vertical]:before:min-h-1.75'
|
||||
)}
|
||||
/>
|
||||
<TimeSliderPrimitive.Fill
|
||||
render={<SliderFill />}
|
||||
className={cn(
|
||||
'group-data-highlighted/chapter:data-[orientation=horizontal]:before:min-w-1.75',
|
||||
'group-data-highlighted/chapter:data-[orientation=vertical]:before:min-h-1.75'
|
||||
)}
|
||||
/>
|
||||
</TimeSliderPrimitive.Track>
|
||||
</div>
|
||||
)}
|
||||
></TimeSliderPrimitive.Chapters>
|
||||
<TimeSliderPrimitive.Thumb
|
||||
render={<SliderThumb />}
|
||||
className={cn(
|
||||
'opacity-0 data-interactive:opacity-100 focus-visible:opacity-100',
|
||||
'pointer-fine:group-hover/slider:scale-100 pointer-fine:group-hover/slider:opacity-100',
|
||||
'scale-80'
|
||||
)}
|
||||
/>
|
||||
<TimeSliderPrimitive.Preview
|
||||
className={cn(
|
||||
'group/preview relative h-1 [--media-slider-preview-max-height:var(--media-slider-preview-max-width)]',
|
||||
'media-2xl:[--media-slider-preview-max-width:min(--spacing(48),100cqi)]',
|
||||
'before:pointer-events-none before:absolute before:z-1 before:-translate-1/2 before:scale-50 before:opacity-0',
|
||||
'before:transition-[opacity,scale] before:duration-media-slow before:ease-out',
|
||||
'data-pointing:not-data-dragging:before:scale-100 data-pointing:not-data-dragging:before:opacity-100',
|
||||
'min-w-(--media-slider-preview-max-width)',
|
||||
'[--media-slider-preview-max-width:min(--spacing(32),100cqi)]',
|
||||
'@min-[30rem]/media-root:[--media-slider-preview-max-width:min(--spacing(40),100cqi)]',
|
||||
'before:top-1/2 before:left-1/2 before:size-1 before:rounded-media-control before:bg-current'
|
||||
)}
|
||||
overflow={previewOverflow}
|
||||
>
|
||||
<SliderPrimitive.Thumbnail.Root
|
||||
className={cn(
|
||||
'absolute max-w-(--media-slider-preview-max-width) -translate-x-1/2 translate-y-media-hidden-preview-offset scale-media-hidden-preview opacity-0',
|
||||
'origin-bottom blur-media-hidden',
|
||||
'transition-[filter,opacity,scale] duration-media-base ease-out',
|
||||
'group-data-pointing/preview:scale-100 group-data-pointing/preview:opacity-100 group-data-pointing/preview:filter-none',
|
||||
'group-has-focus-visible/slider:scale-100 group-has-focus-visible/slider:opacity-100 group-has-focus-visible/slider:filter-none',
|
||||
'bg-media-popover text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'group/thumbnail pointer-events-none overflow-hidden rounded-media-popup bg-media-backdrop/90',
|
||||
'bottom-[calc(100%+var(--media-slider-preview-offset))]',
|
||||
'max-h-(--media-slider-preview-max-height)',
|
||||
'data-loading:aspect-video data-loading:w-(--media-slider-preview-max-width)',
|
||||
'left-1/2',
|
||||
'after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-(image:--media-thumbnail-gradient)'
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Thumbnail.Image
|
||||
render={renderThumbnail}
|
||||
className={cn(
|
||||
'block transition-opacity duration-media-base ease-out',
|
||||
'group-data-loading/thumbnail:opacity-0'
|
||||
)}
|
||||
/>
|
||||
|
||||
<SpinnerIconPrimitive
|
||||
className={cn(
|
||||
'absolute top-1/2 left-1/2 z-10 size-media-icon -translate-x-1/2 -translate-y-1/2 opacity-0',
|
||||
'transition-opacity duration-media-base ease-out',
|
||||
'group-not-data-loading/thumbnail:[--media-spinner-animation:none]',
|
||||
'group-data-loading/thumbnail:opacity-100',
|
||||
'drop-shadow-media-icon'
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Thumbnail.Root>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute max-w-(--media-slider-preview-max-width) -translate-x-1/2 translate-y-media-hidden-preview-offset scale-media-hidden-preview opacity-0',
|
||||
'origin-bottom blur-media-hidden',
|
||||
'transition-[filter,opacity,scale] duration-media-base ease-out',
|
||||
'group-data-pointing/preview:scale-100 group-data-pointing/preview:opacity-100 group-data-pointing/preview:filter-none',
|
||||
'group-has-focus-visible/slider:scale-100 group-has-focus-visible/slider:opacity-100 group-has-focus-visible/slider:filter-none',
|
||||
'flex bottom-[calc(100%+var(--media-slider-preview-label-offset))] tabular-nums',
|
||||
'left-1/2 flex-col items-center'
|
||||
)}
|
||||
>
|
||||
<TimeSliderPrimitive.ChapterTitle
|
||||
className={cn('max-w-(--media-slider-preview-max-width) min-w-0 truncate empty:hidden', 'px-6')}
|
||||
/>
|
||||
<TimeSliderPrimitive.Value className={'tabular-nums'} type="pointer" />
|
||||
</div>
|
||||
</TimeSliderPrimitive.Preview>
|
||||
</TimeSliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { Title as TitlePrimitive } from '@videojs/react';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type TitleProps = Omit<TitlePrimitive.Props, 'children'>;
|
||||
|
||||
export function Title({ className, ...props }: TitleProps = {}) {
|
||||
return (
|
||||
<TitlePrimitive
|
||||
className={(state) =>
|
||||
cn(
|
||||
'pointer-events-none absolute inset-x-0 top-0 z-20 isolate',
|
||||
'wrap-anywhere px-6 pt-2.5 text-media font-medium tracking-[-0.0125em] text-media-controls-foreground text-shadow-media',
|
||||
'media-md:text-media-xl media-md:px-6 media-md:pt-4',
|
||||
'origin-top duration-media-controls-enter ease-out',
|
||||
'not-data-visible:opacity-0 not-data-visible:-translate-y-media-hidden-offset not-data-visible:duration-media-controls',
|
||||
'pointer-fine:not-data-visible:blur-media-hidden',
|
||||
'flex items-center min-h-[calc(var(--media-control-size)+(--spacing(2)))]',
|
||||
'media-max-lg:pr-36',
|
||||
'transition-[filter,opacity,scale,translate]',
|
||||
'not-data-visible:scale-media-hidden',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { VolumeIndicator as VolumeIndicatorPrimitive } from '@videojs/react';
|
||||
import {
|
||||
VolumeHighIcon as VolumeHighIconPrimitive,
|
||||
VolumeLowIcon as VolumeLowIconPrimitive,
|
||||
VolumeOffIcon as VolumeOffIconPrimitive,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;
|
||||
|
||||
export function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
|
||||
return (
|
||||
<VolumeIndicatorPrimitive.Root
|
||||
className={(state) =>
|
||||
cn(
|
||||
'pointer-events-none absolute origin-top',
|
||||
'duration-media-fast ease-out',
|
||||
'media-transitioning:opacity-0 media-transitioning:duration-media-indicator media-transitioning:ease-in',
|
||||
'top-3 rounded-media-control font-medium',
|
||||
'bg-media-backdrop/25 text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'media-high-contrast:bg-media-background',
|
||||
'pointer-coarse:motion-media-[scale,translate,opacity]',
|
||||
'pointer-fine:motion-media-[scale,translate,filter,opacity]',
|
||||
'pointer-fine:media-transitioning:scale-media-hidden-indicator pointer-fine:media-transitioning:blur-media-hidden',
|
||||
'data-ending-style:translate-y-media-hidden-indicator-offset',
|
||||
'group/volume-status',
|
||||
'w-[min(80%,12rem)] transform-[translateX(0)]',
|
||||
'motion-safe:[&:is([data-min],[data-max]):not([data-starting-style],[data-ending-style])]:nudge-media',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<VolumeIndicatorPrimitive.Fill
|
||||
className={cn(
|
||||
'items-center justify-between gap-2 px-2.5 py-1',
|
||||
'rounded-[inherit]',
|
||||
'flex w-full bg-left bg-no-repeat',
|
||||
'bg-[linear-gradient(currentColor,currentColor)]',
|
||||
'bg-size-[var(--media-volume-fill,0%)_100%] transition-[background-size] duration-media-slow ease-linear'
|
||||
)}
|
||||
>
|
||||
<VolumeHighIconPrimitive
|
||||
className={cn('hidden shrink-0 group-data-[level=high]/volume-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<VolumeLowIconPrimitive
|
||||
className={cn('hidden shrink-0 group-data-[level=low]/volume-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<VolumeOffIconPrimitive
|
||||
className={cn('hidden shrink-0 group-data-[level=off]/volume-status:block', 'mix-blend-difference')}
|
||||
/>
|
||||
<VolumeIndicatorPrimitive.Value className={'ml-auto mix-blend-difference'} />
|
||||
</VolumeIndicatorPrimitive.Fill>
|
||||
</VolumeIndicatorPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import type { VolumeSliderProps as CoreVolumeSliderProps } from '@videojs/core';
|
||||
import { VolumePopover as VolumePopoverPrimitive } from '@videojs/react';
|
||||
import type { ClassValue } from 'cn';
|
||||
|
||||
import { ButtonTooltip } from '@/components/videojs/ui/button-tooltip';
|
||||
import { MuteButton } from '@/components/videojs/ui/mute-button';
|
||||
import { VolumeSlider } from '@/components/videojs/ui/volume-slider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
|
||||
className?: ClassValue;
|
||||
orientation?: CoreVolumeSliderProps['orientation'];
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
export function VolumePopover({
|
||||
className,
|
||||
showTooltip = false,
|
||||
side = 'top',
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: VolumePopoverProps = {}) {
|
||||
return (
|
||||
<VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
|
||||
<ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
|
||||
<VolumePopoverPrimitive.Trigger render={<MuteButton className={cn(className)} />} />
|
||||
</ButtonTooltip>
|
||||
<VolumePopoverPrimitive.Popup
|
||||
className={cn(
|
||||
'm-0 overflow-visible border-0 text-inherit',
|
||||
'media-transitioning:opacity-0 media-transitioning:blur-media-hidden-popup media-transitioning:scale-media-hidden-popup',
|
||||
'data-starting-style:transform-[translate(var(--media-popup-translate-x-distance,0),var(--media-popup-translate-y-distance,0))]',
|
||||
'data-ending-style:transform-none',
|
||||
'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left',
|
||||
'data-[side=top]:[--media-popup-translate-y-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=bottom]:[--media-popup-translate-y-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'data-[side=left]:[--media-popup-translate-x-distance:var(--media-popup-translate-distance)]',
|
||||
'data-[side=right]:[--media-popup-translate-x-distance:calc(var(--media-popup-translate-distance)*-1)]',
|
||||
'before:pointer-events-auto before:absolute',
|
||||
'data-[side=top]:before:inset-x-0 data-[side=top]:before:top-full',
|
||||
'data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:bottom-full',
|
||||
'data-[side=left]:before:inset-y-0 data-[side=left]:before:left-full',
|
||||
'data-[side=right]:before:inset-y-0 data-[side=right]:before:right-full',
|
||||
'data-[side=top]:before:h-(--media-popup-side-offset) data-[side=bottom]:before:h-(--media-popup-side-offset)',
|
||||
'data-[side=left]:before:w-(--media-popup-side-offset) data-[side=right]:before:w-(--media-popup-side-offset)',
|
||||
'transition-media-popup data-ending-style:duration-media-instant',
|
||||
'bg-media-popover text-media-popover-foreground surface-media after:surface-media-inset',
|
||||
'rounded-media-control px-0 py-3 [--media-popup-side-offset:var(--media-popover-side-offset)]',
|
||||
'data-[side=right]:rounded-none data-[side=right]:p-0 data-[side=right]:px-3 data-[side=right]:surface-media-none! data-[side=right]:after:hidden',
|
||||
'data-[side=right]:[--media-popover-side-offset:0rem]'
|
||||
)}
|
||||
>
|
||||
<VolumeSlider orientation={orientation} />
|
||||
</VolumePopoverPrimitive.Popup>
|
||||
</VolumePopoverPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/base.css';
|
||||
import '../styles/audio/theme.css';
|
||||
import '../styles/video/captions.css';
|
||||
import '../styles/video/theme.css';
|
||||
import { VolumeSlider as VolumeSliderPrimitive } from '@videojs/react';
|
||||
|
||||
import { SliderFill, SliderThumb, SliderTrack } from '@/components/videojs/ui/slider';
|
||||
import { resolveClassName } from '@/lib/resolve-class-name';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;
|
||||
|
||||
export function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
|
||||
return (
|
||||
<VolumeSliderPrimitive.Root
|
||||
className={(state) =>
|
||||
cn(
|
||||
'group/slider relative flex flex-1 cursor-pointer items-center justify-center outline-hidden',
|
||||
'data-disabled:pointer-events-none',
|
||||
'transition-[--media-slider-fill,--media-slider-buffer] duration-media-slider ease-out data-dragging:duration-0',
|
||||
'rounded-media-pill',
|
||||
'data-[orientation=horizontal]:h-(--media-slider-height,--spacing(8))',
|
||||
'data-[orientation=vertical]:w-8 data-[orientation=vertical]:min-w-0',
|
||||
'data-[orientation=horizontal]:min-w-18 data-[orientation=vertical]:h-18',
|
||||
'media-volume-slider',
|
||||
resolveClassName(className, state)
|
||||
)
|
||||
}
|
||||
thumbAlignment="edge"
|
||||
{...props}
|
||||
>
|
||||
<VolumeSliderPrimitive.Track render={<SliderTrack />}>
|
||||
<VolumeSliderPrimitive.Fill render={<SliderFill />} />
|
||||
</VolumeSliderPrimitive.Track>
|
||||
<VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'scale-100 opacity-100'} />
|
||||
</VolumeSliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Gesture } from '@videojs/react';
|
||||
|
||||
export interface VideoGesturesProps {
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export function VideoGestures({ disabled = false }: VideoGesturesProps = {}) {
|
||||
return (
|
||||
<>
|
||||
<Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
|
||||
<Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
|
||||
<Gesture disabled={disabled} type="doubletap" action="seekStep" region="left" />
|
||||
<Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
|
||||
<Gesture disabled={disabled} type="doubletap" action="seekStep" region="right" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Hotkey } from '@videojs/react';
|
||||
|
||||
import { PlaybackHotkeys } from './playback-hotkeys';
|
||||
|
||||
export interface VideoHotkeysProps {
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export function VideoHotkeys({ disabled = false }: VideoHotkeysProps = {}) {
|
||||
return (
|
||||
<>
|
||||
<PlaybackHotkeys disabled={disabled} />
|
||||
<Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
|
||||
<Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
|
||||
<Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hotkey } from '@videojs/react';
|
||||
|
||||
export interface PlaybackHotkeysProps {
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
|
||||
return (
|
||||
<>
|
||||
<Hotkey disabled={disabled} keys="Space" action="togglePaused" />
|
||||
<Hotkey disabled={disabled} keys="k" action="togglePaused" />
|
||||
<Hotkey disabled={disabled} keys="m" action="toggleMuted" />
|
||||
<Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" />
|
||||
<Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" />
|
||||
<Hotkey disabled={disabled} keys="l" action="seekStep" />
|
||||
<Hotkey disabled={disabled} keys="j" action="seekStep" />
|
||||
<Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
|
||||
<Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
|
||||
<Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
|
||||
<Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
|
||||
<Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
|
||||
<Hotkey disabled={disabled} keys=">" action="speedUp" />
|
||||
<Hotkey disabled={disabled} keys="<" action="speedDown" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { SeekIndicator } from '@/components/videojs/ui/seek-indicator';
|
||||
import { StatusAnnouncer } from '@/components/videojs/ui/status-announcer';
|
||||
import { PlaybackStatusIndicator, StatusIndicator } from '@/components/videojs/ui/status-indicator';
|
||||
import { VolumeIndicator } from '@/components/videojs/ui/volume-indicator';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type VideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;
|
||||
|
||||
export function VideoStatusIndicators({ className, ...props }: VideoStatusIndicatorsProps = {}) {
|
||||
return (
|
||||
<>
|
||||
<StatusAnnouncer />
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 z-20 grid grid-cols-3 items-center justify-items-center',
|
||||
'text-media-controls-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<VolumeIndicator />
|
||||
<StatusIndicator />
|
||||
<SeekIndicator />
|
||||
<PlaybackStatusIndicator />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Controls, Time, Tooltip } from '@videojs/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { AirPlayButton } from '@/components/videojs/ui/airplay-button';
|
||||
import { ButtonTooltip } from '@/components/videojs/ui/button-tooltip';
|
||||
import { CaptionsButton } from '@/components/videojs/ui/captions-button';
|
||||
import { CastButton } from '@/components/videojs/ui/cast-button';
|
||||
import { FullscreenButton } from '@/components/videojs/ui/fullscreen-button';
|
||||
import { PiPButton } from '@/components/videojs/ui/pip-button';
|
||||
import { PlayButton } from '@/components/videojs/ui/play-button';
|
||||
import { TimeSlider } from '@/components/videojs/ui/time-slider';
|
||||
import { VolumePopover } from '@/components/videojs/ui/volume-popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { VideoSettingsMenu } from '../menus/settings-menu';
|
||||
|
||||
export interface DefaultVideoControlsProps {
|
||||
renderThumbnail?: NonNullable<ComponentProps<typeof TimeSlider>>['renderThumbnail'];
|
||||
}
|
||||
|
||||
export function DefaultVideoControls({ renderThumbnail }: DefaultVideoControlsProps = {}) {
|
||||
return (
|
||||
<Controls.Root>
|
||||
<Controls.Backdrop
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 z-10 rounded-[inherit] bg-(image:--media-controls-gradient)',
|
||||
'transition-opacity duration-media-controls ease-out not-data-visible:opacity-0'
|
||||
)}
|
||||
/>
|
||||
<Controls.Content
|
||||
className={cn(
|
||||
'video-controls',
|
||||
'group/controls text-media-controls-foreground text-shadow-media',
|
||||
'duration-media-controls-enter ease-out',
|
||||
'contents p-1 transition-[filter,opacity,scale,translate]',
|
||||
'media-lg:absolute media-lg:inset-x-2 media-lg:bottom-2 media-lg:z-30',
|
||||
'media-lg:flex media-lg:items-center media-lg:rtl:flex-row-reverse media-lg:rounded-media-controls',
|
||||
'media-lg:bg-media-popover media-lg:text-media-popover-foreground',
|
||||
'media-lg:surface-media media-lg:after:surface-media-inset',
|
||||
'media-2xl:inset-x-3 media-2xl:bottom-3',
|
||||
'media-lg:not-data-visible:pointer-events-none media-lg:not-data-visible:opacity-0',
|
||||
'media-lg:not-data-visible:scale-media-hidden media-lg:not-data-visible:translate-y-media-hidden-offset',
|
||||
'media-lg:pointer-fine:not-data-visible:blur-media-hidden',
|
||||
'media-lg:not-data-visible:duration-media-controls'
|
||||
)}
|
||||
>
|
||||
<Tooltip.Provider>
|
||||
<Controls.Group
|
||||
className={cn(
|
||||
'absolute inset-x-2 bottom-2 z-30 flex origin-bottom items-center rtl:flex-row-reverse rounded-media-controls p-0.5',
|
||||
'media-max-lg:bg-media-popover media-max-lg:text-media-popover-foreground',
|
||||
'media-max-lg:surface-media media-max-lg:after:surface-media-inset',
|
||||
'media-lg:contents',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:pointer-events-none',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:opacity-0',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:scale-media-hidden',
|
||||
'media-max-lg:pointer-fine:group-[:not([data-visible])]/controls:blur-media-hidden',
|
||||
'transition-[filter,opacity,scale,translate] duration-media-controls-enter ease-out',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:duration-media-controls',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:translate-y-media-hidden-offset'
|
||||
)}
|
||||
>
|
||||
<ButtonTooltip side="top">
|
||||
<PlayButton />
|
||||
</ButtonTooltip>
|
||||
<VolumePopover className={'ms-px'} />
|
||||
|
||||
<Controls.Group
|
||||
className={
|
||||
'@container/media-time flex flex-1 items-center rtl:flex-row-reverse gap-2.5 px-2 media-lg:px-3'
|
||||
}
|
||||
>
|
||||
<Time.Value
|
||||
className={cn(
|
||||
'tabular-nums transition-opacity duration-media-slow ease-out data-unavailable:opacity-50',
|
||||
'@max-[16rem]/media-time:hidden'
|
||||
)}
|
||||
type="current"
|
||||
/>
|
||||
<TimeSlider renderThumbnail={renderThumbnail} />
|
||||
<Time.Value
|
||||
className={cn(
|
||||
'cursor-pointer rounded-sm tabular-nums focus-ring-media',
|
||||
'aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
|
||||
'transition-[outline-color,outline-offset] duration-media-fast ease-out',
|
||||
'focus-visible:outline-media-ring focus-visible:outline-offset-2',
|
||||
'@max-[16rem]/media-time:hidden'
|
||||
)}
|
||||
type="remaining"
|
||||
toggle
|
||||
/>
|
||||
</Controls.Group>
|
||||
|
||||
<ButtonTooltip side="top">
|
||||
<CaptionsButton className={'media-max-lg:hidden'} />
|
||||
</ButtonTooltip>
|
||||
<VideoSettingsMenu className={'media-lg:ms-px'} />
|
||||
</Controls.Group>
|
||||
|
||||
<Controls.Group
|
||||
className={cn(
|
||||
'absolute top-2 right-2 z-30 flex origin-top items-center rtl:flex-row-reverse gap-px rounded-media-controls p-0.5',
|
||||
'media-max-lg:bg-media-popover media-max-lg:text-media-popover-foreground',
|
||||
'media-max-lg:surface-media media-max-lg:after:surface-media-inset',
|
||||
'media-lg:static media-lg:p-0',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:pointer-events-none',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:opacity-0',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:scale-media-hidden',
|
||||
'media-max-lg:pointer-fine:group-[:not([data-visible])]/controls:blur-media-hidden',
|
||||
'transition-[filter,opacity,scale,translate] duration-media-controls-enter ease-out',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:duration-media-controls',
|
||||
'media-max-lg:group-[:not([data-visible])]/controls:-translate-y-media-hidden-offset'
|
||||
)}
|
||||
>
|
||||
<ButtonTooltip side="top">
|
||||
<CastButton />
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip side="top">
|
||||
<AirPlayButton />
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip side="top">
|
||||
<PiPButton />
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip side="top">
|
||||
<FullscreenButton />
|
||||
</ButtonTooltip>
|
||||
</Controls.Group>
|
||||
</Tooltip.Provider>
|
||||
</Controls.Content>
|
||||
</Controls.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { AudioTrackMenu } from '@/components/videojs/ui/audio-track-menu';
|
||||
import { CaptionsSubmenu } from '@/components/videojs/ui/captions-submenu';
|
||||
import { PlaybackRateSubmenu } from '@/components/videojs/ui/playback-rate-submenu';
|
||||
import { QualityMenu } from '@/components/videojs/ui/quality-menu';
|
||||
import { SettingsMenu } from '@/components/videojs/ui/settings-menu';
|
||||
|
||||
export type VideoSettingsMenuProps = Omit<NonNullable<ComponentProps<typeof SettingsMenu>>, 'children'>;
|
||||
|
||||
export function VideoSettingsMenu(props: VideoSettingsMenuProps = {}) {
|
||||
return (
|
||||
<SettingsMenu {...props}>
|
||||
<QualityMenu />
|
||||
<AudioTrackMenu />
|
||||
<PlaybackRateSubmenu />
|
||||
<CaptionsSubmenu />
|
||||
</SettingsMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import '../styles/video/base.css';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
|
||||
import { BufferingIndicator } from '@/components/videojs/ui/buffering-indicator';
|
||||
import { Container } from '@/components/videojs/ui/container';
|
||||
import { ErrorDialog } from '@/components/videojs/ui/error-dialog';
|
||||
import { Poster } from '@/components/videojs/ui/poster';
|
||||
import { Title } from '@/components/videojs/ui/title';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { VideoGestures } from './behaviors/gestures';
|
||||
import { VideoHotkeys } from './behaviors/hotkeys';
|
||||
import { VideoStatusIndicators } from './display/status-indicators';
|
||||
import { DefaultVideoControls } from './layout/controls';
|
||||
|
||||
export interface VideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
|
||||
children?: ReactNode;
|
||||
renderPoster?: NonNullable<ComponentProps<typeof Poster>>['renderImage'];
|
||||
renderThumbnail?: NonNullable<ComponentProps<typeof DefaultVideoControls>>['renderThumbnail'];
|
||||
}
|
||||
|
||||
export function VideoSkin({ children, className, renderPoster, renderThumbnail, ...props }: VideoSkinProps = {}) {
|
||||
return (
|
||||
<Container
|
||||
className={cn('pointer-fine:not-data-controls-visible:cursor-none', className)}
|
||||
data-theme="default"
|
||||
data-preset="video"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Poster renderImage={renderPoster} />
|
||||
<BufferingIndicator />
|
||||
<ErrorDialog />
|
||||
<Title />
|
||||
|
||||
<DefaultVideoControls renderThumbnail={renderThumbnail} />
|
||||
|
||||
<VideoHotkeys />
|
||||
<VideoGestures />
|
||||
<VideoStatusIndicators />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from 'cn';
|
||||
|
||||
export type ClassName<State> = ClassValue | ((state: State) => ClassValue);
|
||||
|
||||
export function resolveClassName<State>(className: ClassName<State>, state: State): ClassValue {
|
||||
return typeof className === 'function' ? className(state) : className;
|
||||
}
|
||||
Reference in New Issue
Block a user