@djangocfg/ui-tools 2.1.402 → 2.1.407

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +16 -1
  2. package/package.json +11 -9
  3. package/src/tools/AudioPlayer/lazy.tsx +13 -27
  4. package/src/tools/ImageViewer/components/ImageViewer.tsx +10 -2
  5. package/src/tools/JsonForm/JsonSchemaForm.tsx +3 -1
  6. package/src/tools/JsonForm/README.md +12 -2
  7. package/src/tools/JsonForm/widgets/TextareaWidget.tsx +25 -0
  8. package/src/tools/JsonForm/widgets/index.ts +1 -0
  9. package/src/tools/JsonTree/README.md +12 -0
  10. package/src/tools/PrettyCode/README.md +81 -0
  11. package/src/tools/VideoPlayer/README.md +87 -230
  12. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  13. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  14. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +38 -0
  15. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  16. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  17. package/src/tools/VideoPlayer/canvas/jsx.d.ts +54 -0
  18. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  19. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +39 -0
  20. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +77 -0
  21. package/src/tools/VideoPlayer/index.ts +51 -65
  22. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  23. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  24. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  25. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  26. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  27. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  28. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  29. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  30. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  31. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  32. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  33. package/src/tools/VideoPlayer/types.ts +82 -0
  34. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  35. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  36. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  37. package/src/tools/index.ts +35 -28
  38. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  39. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  40. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  41. package/src/tools/VideoPlayer/components/index.ts +0 -14
  42. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  43. package/src/tools/VideoPlayer/context/index.ts +0 -8
  44. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  45. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  46. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  47. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  48. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  49. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  50. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  51. package/src/tools/VideoPlayer/types/index.ts +0 -38
  52. package/src/tools/VideoPlayer/types/player.ts +0 -116
  53. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  54. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  55. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  56. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  57. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  58. package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * `<vimeo-video slot="media">` — Vimeo Player API wrapper as a custom
5
+ * element via `vimeo-video-element`. Same media-chrome integration story
6
+ * as the YouTube canvas.
7
+ */
8
+
9
+ import 'vimeo-video-element';
10
+ import { MediaPosterImage } from 'media-chrome/react';
11
+ import type { VimeoSource, VideoPlayerSettings } from '../types';
12
+
13
+ export interface VimeoCanvasProps extends VideoPlayerSettings {
14
+ readonly source: VimeoSource;
15
+ }
16
+
17
+ export function VimeoCanvas({
18
+ source,
19
+ autoPlay,
20
+ muted,
21
+ loop,
22
+ }: VimeoCanvasProps) {
23
+ const base = `https://vimeo.com/${source.videoId}`;
24
+ const src = source.startTime ? `${base}#t=${source.startTime}s` : base;
25
+
26
+ return (
27
+ <>
28
+ <vimeo-video
29
+ slot="media"
30
+ src={src}
31
+ {...(muted && { muted: true })}
32
+ {...(autoPlay && { autoplay: true })}
33
+ {...(loop && { loop: true })}
34
+ playsInline
35
+ />
36
+ {source.poster && <MediaPosterImage slot="poster" src={source.poster} />}
37
+ </>
38
+ );
39
+ }
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * `<youtube-video slot="media">` — registers the custom element via
5
+ * `youtube-video-element` side-effect import. The element wraps the
6
+ * YouTube IFrame API and exposes an `HTMLVideoElement`-shaped surface
7
+ * that media-chrome's `<MediaController>` drives.
8
+ *
9
+ * `youtube-video-element` already defaults the embed to `controls=0`,
10
+ * `rel=0`, `iv_load_policy=3`, `modestbranding=1`. We extend that via
11
+ * the `config` prop (serialized into `data-config`, merged into the
12
+ * IFrame `playerVars`) to also suppress YouTube's keyboard handling
13
+ * (`disablekb=1` — our controls own keyboard) and the in-player
14
+ * close-captions toggle the element turns on by default.
15
+ *
16
+ * NOTE: YouTube's ToS forbids fully hiding its branding — a small logo
17
+ * and the pause-screen overlay remain by design. `modestbranding=1` is
18
+ * the maximum allowed suppression.
19
+ */
20
+
21
+ import 'youtube-video-element';
22
+ import { MediaPosterImage } from 'media-chrome/react';
23
+ import type { YouTubeSource, VideoPlayerSettings } from '../types';
24
+
25
+ export interface YouTubeCanvasProps extends VideoPlayerSettings {
26
+ readonly source: YouTubeSource;
27
+ }
28
+
29
+ /**
30
+ * Extra YouTube IFrame `playerVars` merged on top of the element
31
+ * defaults. Keep keys YouTube-documented:
32
+ * https://developers.google.com/youtube/player_parameters
33
+ */
34
+ const YT_PLAYER_VARS = {
35
+ // We render our own media-chrome control bar — hide YouTube's.
36
+ controls: 0,
37
+ // Our keyboard shortcuts own the player; disable YouTube's.
38
+ disablekb: 1,
39
+ // Minimise the YouTube logo chrome (max suppression allowed by ToS).
40
+ modestbranding: 1,
41
+ // Do not surface "More videos" / related-channel end-screen suggestions.
42
+ rel: 0,
43
+ // Hide video annotations.
44
+ iv_load_policy: 3,
45
+ // Keep playback inline on iOS instead of forcing native fullscreen.
46
+ playsinline: 1,
47
+ // Don't auto-load closed captions.
48
+ cc_load_policy: 0,
49
+ } as const;
50
+
51
+ export function YouTubeCanvas({
52
+ source,
53
+ autoPlay,
54
+ muted,
55
+ loop,
56
+ }: YouTubeCanvasProps) {
57
+ const params = new URLSearchParams();
58
+ params.set('v', source.videoId);
59
+ if (source.startTime) params.set('t', `${source.startTime}s`);
60
+ if (source.playlistId) params.set('list', source.playlistId);
61
+ const src = `https://www.youtube.com/watch?${params.toString()}`;
62
+
63
+ return (
64
+ <>
65
+ <youtube-video
66
+ slot="media"
67
+ src={src}
68
+ config={YT_PLAYER_VARS}
69
+ {...(muted && { muted: true })}
70
+ {...(autoPlay && { autoplay: true })}
71
+ {...(loop && { loop: true })}
72
+ playsInline
73
+ />
74
+ {source.poster && <MediaPosterImage slot="poster" src={source.poster} />}
75
+ </>
76
+ );
77
+ }
@@ -1,77 +1,63 @@
1
1
  /**
2
- * VideoPlayer - Unified Video Player
3
- * Supports Vidstack (YouTube, Vimeo, HLS, DASH), Native HTML5, and HTTP Streaming
2
+ * VideoPlayer full public surface.
3
+ *
4
+ * Prefer importing through the subpath entry:
5
+ * `import { VideoPlayer } from '@djangocfg/ui-tools/video-player'`
6
+ *
7
+ * which routes through `lazy.tsx`. This `index.ts` is the underlying barrel.
4
8
  */
5
9
 
6
- // Main component
7
- export { VideoPlayer } from './components';
10
+ export { VideoPlayer } from './VideoPlayer';
8
11
 
9
- // Controls (can be used standalone with Vidstack)
10
- export { VideoControls } from './components';
11
-
12
- // Error Fallback
13
- export {
14
- VideoErrorFallback,
15
- createVideoErrorFallback,
16
- } from './components';
17
12
  export type {
18
- VideoErrorFallbackProps,
19
- CreateVideoErrorFallbackOptions,
20
- } from './components';
13
+ VideoPlayerProps,
14
+ VideoPlayerSettings,
15
+ VideoSource,
16
+ UrlSource,
17
+ YouTubeSource,
18
+ VimeoSource,
19
+ HlsSource,
20
+ IframeSource,
21
+ AspectRatioValue,
22
+ } from './types';
21
23
 
22
- // Providers (for advanced usage)
23
- export { VidstackProvider, NativeProvider, StreamProvider } from './providers';
24
+ export { parseEmbedUrl } from './utils/parse-embed-url';
25
+ export { extractYouTubeId, parseYouTubeStartTime } from './utils/youtube-id';
26
+ export { extractVimeoId } from './utils/vimeo-id';
24
27
 
25
- // Context (for streaming configuration)
26
28
  export {
27
- VideoPlayerProvider,
28
- useVideoPlayerContext,
29
- } from './context';
30
-
31
- // Hooks
32
- export { useVideoPositionCache } from './hooks';
29
+ CanvasDispatcher,
30
+ NativeCanvas,
31
+ YouTubeCanvas,
32
+ VimeoCanvas,
33
+ HlsCanvas,
34
+ IframeCanvas,
35
+ } from './canvas';
33
36
  export type {
34
- UseVideoPositionCacheOptions,
35
- UseVideoPositionCacheReturn,
36
- } from './hooks';
37
+ CanvasDispatcherProps,
38
+ NativeCanvasProps,
39
+ YouTubeCanvasProps,
40
+ VimeoCanvasProps,
41
+ HlsCanvasProps,
42
+ IframeCanvasProps,
43
+ } from './canvas';
37
44
 
38
- // Utils
39
45
  export {
40
- resolvePlayerMode,
41
- resolveFileSource,
42
- isSimpleStreamSource,
43
- resolveStreamSource,
44
- } from './utils';
45
-
46
- // Types
46
+ PlayButton,
47
+ SeekBar,
48
+ Volume,
49
+ Fullscreen,
50
+ Pip,
51
+ PlaybackRate,
52
+ ControlsBar,
53
+ Poster,
54
+ } from './parts';
47
55
  export type {
48
- // Source types
49
- VideoSourceUnion,
50
- UrlSource,
51
- YouTubeSource,
52
- VimeoSource,
53
- HLSSource,
54
- DASHSource,
55
- StreamSource,
56
- BlobSource,
57
- DataUrlSource,
58
- // Player types
59
- PlayerMode,
60
- AspectRatioValue,
61
- VideoPlayerProps,
62
- VideoPlayerRef,
63
- ErrorFallbackProps,
64
- // Provider props (internal)
65
- VidstackProviderProps,
66
- NativeProviderProps,
67
- StreamProviderProps,
68
- // Common types
69
- CommonPlayerSettings,
70
- CommonPlayerEvents,
71
- // File source helper types
72
- ResolveFileSourceOptions,
73
- // Context types
74
- VideoPlayerContextValue,
75
- VideoPlayerProviderProps,
76
- SimpleStreamSource,
77
- } from './types';
56
+ PlayButtonProps,
57
+ SeekBarProps,
58
+ VolumeProps,
59
+ FullscreenProps,
60
+ PipProps,
61
+ PlaybackRateProps,
62
+ ControlsBarProps,
63
+ } from './parts';
@@ -1,63 +1,20 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * Lazy-loaded VideoPlayer Component
4
+ * Subpath entry: `@djangocfg/ui-tools/video-player`.
5
5
  *
6
- * Heavy Vidstack (~150KB) is loaded only when component is rendered.
7
- * Use this for automatic code-splitting with Suspense fallback.
6
+ * Built on `media-chrome` web components the React layer is ~3 KB of
7
+ * thin wrappers, and provider engines (`youtube-video-element`,
8
+ * `vimeo-video-element`, `hls-video-element`) are imported only by the
9
+ * matching canvas file, so unused engines are tree-shaken.
8
10
  *
9
- * For direct imports without lazy loading, use:
10
- * import { VideoPlayer } from '@djangocfg/ui-tools/video'
11
+ * `LazyVideoPlayer` is preserved as a synchronous alias for back-compat
12
+ * with existing consumers (e.g. cmdop desktop's document-preview).
11
13
  */
12
14
 
13
- import { createLazyComponent, LoadingFallback } from '../../components';
14
- import type { VideoPlayerProps } from './types';
15
+ export * from './index';
15
16
 
16
- // ============================================================================
17
- // Re-export types
18
- // ============================================================================
17
+ import { VideoPlayer } from './VideoPlayer';
19
18
 
20
- export type { VideoPlayerProps };
21
-
22
- // ============================================================================
23
- // Video Loading Fallback
24
- // ============================================================================
25
-
26
- function VideoLoadingFallback() {
27
- return (
28
- <div className="flex items-center justify-center bg-black/90 rounded-lg aspect-video">
29
- <div className="flex flex-col items-center gap-2">
30
- <div className="relative">
31
- <div className="h-10 w-10 animate-spin rounded-full border-4 border-white/20 border-t-white" />
32
- <div className="absolute inset-0 flex items-center justify-center">
33
- <svg
34
- className="h-5 w-5 text-white/60"
35
- fill="currentColor"
36
- viewBox="0 0 24 24"
37
- >
38
- <path d="M8 5v14l11-7z" />
39
- </svg>
40
- </div>
41
- </div>
42
- <span className="text-sm text-white/60">Loading video player...</span>
43
- </div>
44
- </div>
45
- );
46
- }
47
-
48
- // ============================================================================
49
- // Lazy Component
50
- // ============================================================================
51
-
52
- /**
53
- * LazyVideoPlayer - Lazy-loaded professional video player
54
- *
55
- * Automatically shows loading state while Vidstack loads (~150KB)
56
- */
57
- export const LazyVideoPlayer = createLazyComponent<VideoPlayerProps>(
58
- () => import('./components').then((mod) => ({ default: mod.VideoPlayer })),
59
- {
60
- displayName: 'LazyVideoPlayer',
61
- fallback: <VideoLoadingFallback />,
62
- }
63
- );
19
+ /** @deprecated Use `VideoPlayer` directly — no lazy boundary is needed. */
20
+ export const LazyVideoPlayer = VideoPlayer;
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import { MediaControlBar } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type ControlsBarProps = ComponentProps<typeof MediaControlBar>;
8
+
9
+ /**
10
+ * Control bar pinned to the bottom over a bottom-up black scrim so the
11
+ * controls stay legible on any video frame. Colours read on-black
12
+ * regardless of theme — the video canvas is always a black surface.
13
+ *
14
+ * The bar participates in media-chrome's `autohide`: when the
15
+ * `<MediaController>` adds the `mediacontrolshidden` user-inactive
16
+ * state, we fade the whole bar (controls + scrim) out together.
17
+ */
18
+ export function ControlsBar({ className, children, ...props }: ControlsBarProps) {
19
+ return (
20
+ <MediaControlBar
21
+ {...props}
22
+ className={cn(
23
+ // Layout — extra top padding gives the scrim room to fade in.
24
+ 'video-player__control-bar',
25
+ 'absolute inset-x-0 bottom-0 flex items-center gap-1 px-2 pb-1.5 pt-8',
26
+ // Bottom-up scrim so controls read over any frame.
27
+ 'bg-gradient-to-t from-black/75 via-black/40 to-transparent',
28
+ 'text-foreground',
29
+ className,
30
+ )}
31
+ >
32
+ {children}
33
+ </MediaControlBar>
34
+ );
35
+ }
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { MediaFullscreenButton } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type FullscreenProps = ComponentProps<typeof MediaFullscreenButton>;
8
+
9
+ export function Fullscreen({ className, ...props }: FullscreenProps) {
10
+ return (
11
+ <MediaFullscreenButton
12
+ {...props}
13
+ className={cn(
14
+ 'media-control-square h-8 w-8 rounded-md text-foreground hover:bg-foreground/10',
15
+ className,
16
+ )}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1,15 @@
1
+ export { PlayButton } from './play-button';
2
+ export type { PlayButtonProps } from './play-button';
3
+ export { SeekBar } from './seek-bar';
4
+ export type { SeekBarProps } from './seek-bar';
5
+ export { Volume } from './volume';
6
+ export type { VolumeProps } from './volume';
7
+ export { Fullscreen } from './fullscreen';
8
+ export type { FullscreenProps } from './fullscreen';
9
+ export { Pip } from './pip';
10
+ export type { PipProps } from './pip';
11
+ export { PlaybackRate } from './playback-rate';
12
+ export type { PlaybackRateProps } from './playback-rate';
13
+ export { ControlsBar } from './controls-bar';
14
+ export type { ControlsBarProps } from './controls-bar';
15
+ export { Poster } from './poster';
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { MediaPipButton } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type PipProps = ComponentProps<typeof MediaPipButton>;
8
+
9
+ export function Pip({ className, ...props }: PipProps) {
10
+ return (
11
+ <MediaPipButton
12
+ {...props}
13
+ className={cn(
14
+ 'media-control-square h-8 w-8 rounded-md text-foreground hover:bg-foreground/10',
15
+ className,
16
+ )}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { MediaPlayButton } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type PlayButtonProps = ComponentProps<typeof MediaPlayButton>;
8
+
9
+ export function PlayButton({ className, ...props }: PlayButtonProps) {
10
+ return (
11
+ <MediaPlayButton
12
+ {...props}
13
+ className={cn(
14
+ 'media-control-square video-player__control h-8 w-8',
15
+ className,
16
+ )}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { MediaPlaybackRateButton } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type PlaybackRateProps = Omit<
8
+ ComponentProps<typeof MediaPlaybackRateButton>,
9
+ 'rates'
10
+ > & {
11
+ /** Space-separated list, e.g. `'0.5 1 1.5 2'`, or an array of numbers. */
12
+ readonly rates?: string | readonly number[];
13
+ };
14
+
15
+ const DEFAULT_RATES: readonly number[] = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
16
+
17
+ export function PlaybackRate({ className, rates, ...props }: PlaybackRateProps) {
18
+ const value = rates ?? DEFAULT_RATES;
19
+ return (
20
+ <MediaPlaybackRateButton
21
+ {...props}
22
+ // media-chrome's setter accepts `string | ArrayLike<number>` — the
23
+ // React typing pulls from the getter only, which is narrower.
24
+ rates={value as unknown as ArrayLike<number>}
25
+ className={cn(
26
+ 'h-8 rounded-md px-2 text-xs tabular-nums text-foreground hover:bg-foreground/10',
27
+ className,
28
+ )}
29
+ />
30
+ );
31
+ }
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+
3
+ export { MediaPosterImage as Poster } from 'media-chrome/react';
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { MediaTimeRange, MediaTimeDisplay } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export type SeekBarProps = ComponentProps<typeof MediaTimeRange> & {
8
+ readonly showTime?: boolean;
9
+ };
10
+
11
+ export function SeekBar({ className, showTime = true, ...props }: SeekBarProps) {
12
+ return (
13
+ <>
14
+ <MediaTimeRange
15
+ {...props}
16
+ className={cn('h-8 flex-1 text-foreground', className)}
17
+ />
18
+ {showTime && (
19
+ <MediaTimeDisplay
20
+ showDuration
21
+ className="px-2 text-xs tabular-nums text-muted-foreground"
22
+ />
23
+ )}
24
+ </>
25
+ );
26
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { MediaMuteButton, MediaVolumeRange } from 'media-chrome/react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import type { ComponentProps } from 'react';
6
+
7
+ export interface VolumeProps {
8
+ readonly className?: string;
9
+ readonly iconOnly?: boolean;
10
+ readonly muteProps?: ComponentProps<typeof MediaMuteButton>;
11
+ readonly rangeProps?: ComponentProps<typeof MediaVolumeRange>;
12
+ }
13
+
14
+ export function Volume({ className, iconOnly, muteProps, rangeProps }: VolumeProps) {
15
+ return (
16
+ <div className={cn('flex items-center', className)}>
17
+ <MediaMuteButton
18
+ {...muteProps}
19
+ className={cn(
20
+ 'media-control-square h-8 w-8 rounded-md text-foreground hover:bg-foreground/10',
21
+ muteProps?.className,
22
+ )}
23
+ />
24
+ {!iconOnly && (
25
+ <MediaVolumeRange
26
+ {...rangeProps}
27
+ className={cn('h-8 w-20 text-foreground', rangeProps?.className)}
28
+ />
29
+ )}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,141 @@
1
+ /*
2
+ * media-chrome theme.
3
+ *
4
+ * The video canvas is ALWAYS a black surface, so the control chrome is
5
+ * themed against on-black contrast — icons/text stay light in both the
6
+ * light and dark app themes. Only the accent (progress bar / thumb) and
7
+ * the scrub-time popover follow semantic tokens, since the popover is a
8
+ * detached surface that should match the rest of the platform.
9
+ *
10
+ * media-chrome ships its own focus-ring + tooltip styles; we only override
11
+ * what's needed to match the rest of the platform.
12
+ */
13
+
14
+ media-controller {
15
+ /* On-black control chrome — fixed regardless of app theme. */
16
+ --video-on-canvas: rgb(245 245 245);
17
+
18
+ /* Surfaces */
19
+ --media-control-background: transparent;
20
+ --media-control-hover-background: rgb(255 255 255 / 0.14);
21
+ --media-control-padding: 0;
22
+
23
+ /* Foreground / accent */
24
+ --media-primary-color: var(--primary);
25
+ --media-secondary-color: rgb(255 255 255 / 0.6);
26
+ --media-text-color: var(--video-on-canvas);
27
+ --media-icon-color: var(--video-on-canvas);
28
+ --media-loading-icon-color: var(--video-on-canvas);
29
+
30
+ /* Range / progress bar — thin translucent rail, small accent thumb.
31
+ The range element keeps a tall hit-area; only the painted track is
32
+ 4px so it reads as a slim progress line over the video. */
33
+ --media-range-track-background: color-mix(in oklab, white 28%, transparent);
34
+ --media-range-track-border-radius: 9999px;
35
+ --media-range-track-height: 4px;
36
+ --media-range-track-pointer-background: color-mix(in oklab, white 40%, transparent);
37
+ --media-range-bar-color: var(--primary);
38
+ --media-range-thumb-background: var(--primary);
39
+ --media-range-thumb-border-radius: 9999px;
40
+ --media-range-thumb-width: 12px;
41
+ --media-range-thumb-height: 12px;
42
+ --media-range-thumb-opacity: 0;
43
+ --media-range-thumb-transition: opacity 0.15s ease;
44
+
45
+ /* Tooltip */
46
+ --media-tooltip-background: var(--popover);
47
+ --media-tooltip-color: var(--popover-foreground);
48
+ --media-tooltip-border-radius: 6px;
49
+
50
+ /* Font */
51
+ --media-font-family: inherit;
52
+ --media-font-size: 12px;
53
+
54
+ /* Layout */
55
+ display: block;
56
+ position: relative;
57
+ width: 100%;
58
+ }
59
+
60
+ /* Square icon buttons should size to the wrapper. */
61
+ media-controller .media-control-square::part(button) {
62
+ width: 100%;
63
+ height: 100%;
64
+ }
65
+
66
+ /*
67
+ * Control buttons / text — fixed on-black colours. The video canvas is
68
+ * always dark, so these never follow the app's light/dark theme.
69
+ */
70
+ .video-player__control {
71
+ color: var(--video-on-canvas, rgb(245 245 245));
72
+ border-radius: 0.375rem;
73
+ transition: background-color 0.15s ease;
74
+ }
75
+
76
+ .video-player__control:hover {
77
+ background-color: rgb(255 255 255 / 0.14);
78
+ }
79
+
80
+ .video-player__time {
81
+ color: rgb(255 255 255 / 0.72);
82
+ }
83
+
84
+ /* Hide the default control bar background — we paint our own gradient
85
+ scrim via the <ControlsBar> wrapper. */
86
+ media-control-bar {
87
+ background: transparent;
88
+ }
89
+
90
+ /*
91
+ * Auto-hide. media-chrome flags inactivity by setting `userinactive` on
92
+ * the <media-controller>; it only does so while playback is running, and
93
+ * removes it on mousemove / pause / focus. We fade the whole control bar
94
+ * (controls + gradient scrim) together with a smooth opacity transition.
95
+ */
96
+ .video-player__control-bar {
97
+ opacity: 1;
98
+ transition: opacity 0.3s ease-out;
99
+ }
100
+
101
+ media-controller[userinactive]:not([mediapaused]) .video-player__control-bar {
102
+ opacity: 0;
103
+ pointer-events: none;
104
+ }
105
+
106
+ /*
107
+ * Seek-bar hover preview. `<media-time-range>` exposes two slotted
108
+ * boxes: `preview-box` (a 120×109 thumbnail panel) and `current-box`
109
+ * (the scrub time label). With no `thumbnails` track wired up the
110
+ * preview-box has zero slotted content but still paints its default
111
+ * dark `rgb(31,31,31)` background — it shows as a stray empty
112
+ * rectangle floating over the video.
113
+ *
114
+ * Hide the empty preview-box entirely; keep `current-box` and style
115
+ * its time label as a compact popover chip.
116
+ */
117
+ media-time-range {
118
+ /* media-chrome's own preview/box vars — point them at our popover
119
+ surface so the scrub time chip matches the rest of the controls. */
120
+ --media-preview-background: var(--popover);
121
+ --media-preview-border-radius: 6px;
122
+ --media-text-color: var(--popover-foreground);
123
+ }
124
+
125
+ /* Reveal the accent thumb only while hovering / scrubbing the rail. */
126
+ media-time-range:hover {
127
+ --media-range-thumb-opacity: 1;
128
+ }
129
+
130
+ media-time-range::part(preview-box) {
131
+ display: none;
132
+ }
133
+
134
+ media-time-range::part(current-box) {
135
+ padding: 2px 6px;
136
+ border-radius: 6px;
137
+ background: var(--popover) !important;
138
+ color: var(--popover-foreground);
139
+ font-size: 11px;
140
+ box-shadow: 0 4px 12px color-mix(in oklab, var(--foreground) 18%, transparent);
141
+ }