diff --git a/biome.json b/biome.json index 5376403a..e5659257 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.2/schema.json", "html": { "experimentalFullSupportEnabled": false }, diff --git a/packages/icons/README.md b/packages/icons/README.md index 498e8667..c17ce4ee 100644 --- a/packages/icons/README.md +++ b/packages/icons/README.md @@ -1,5 +1,116 @@ # @videojs/icons -**Note:** This package is currently a placeholder and will be populated closer to Beta release. +SVG icon library for Video.js. Ships optimized icons as **React components** and **HTML strings**, organized into icon sets. -For now, custom icons are bundled directly with skins. +## Icon Sets + +Icons are grouped into visual sets. Each set contains the same icon names with different designs. + +| Set | Description | +| --- | --- | +| `default` | Standard icon set used by the default skin | +| `minimal` | Simplified icon set used by the minimal skin | + +### Available Icons + +| Icon | React Export | HTML Export | +| --- | --- | --- | +| `fullscreen-enter` | `FullscreenEnterIcon` | `fullscreenEnterIcon` | +| `fullscreen-exit` | `FullscreenExitIcon` | `fullscreenExitIcon` | +| `pause` | `PauseIcon` | `pauseIcon` | +| `pip` | `PipIcon` | `pipIcon` | +| `play` | `PlayIcon` | `playIcon` | +| `restart` | `RestartIcon` | `restartIcon` | +| `seek` | `SeekIcon` | `seekIcon` | +| `spinner` | `SpinnerIcon` | `spinnerIcon` | +| `volume-high` | `VolumeHighIcon` | `volumeHighIcon` | +| `volume-low` | `VolumeLowIcon` | `volumeLowIcon` | +| `volume-off` | `VolumeOffIcon` | `volumeOffIcon` | + +## Usage + +### React + +Import icons as React components. They accept standard SVG props and support ref forwarding. + +```tsx +import { PlayIcon, PauseIcon } from '@videojs/icons/react'; + +function Controls() { + return ( + + ); +} +``` + +Import from a specific icon set: + +```tsx +import { PlayIcon } from '@videojs/icons/react/minimal'; +``` + +### HTML + +Import icons as SVG strings for use in plain HTML or web components. + +```ts +import { playIcon, pauseIcon } from '@videojs/icons/html'; + +button.innerHTML = playIcon; +``` + +Import from a specific icon set: + +```ts +import { playIcon } from '@videojs/icons/html/minimal'; +``` + +## Styling + +All icons use `fill="currentColor"`, so they inherit the text color of their parent element. Size and color can be controlled with CSS: + +```css +.icon { + width: 18px; + height: 18px; + color: white; +} +``` + +Icons are designed on an **18×18 grid**. Rendering at `18px` (or exact multiples like `36px`) produces the crispest result. Other sizes may introduce sub-pixel rendering artifacts. + +## Adding Icons + +1. Add an SVG file to `src/assets//` (e.g., `src/assets/default/my-icon.svg`). +2. Run `pnpm -F @videojs/icons build`. +3. The build script optimizes the SVG with SVGO and generates React components and HTML string exports. + +The build automatically: + +- Removes hardcoded `fill`, `stroke`, `clip-rule`, and `fill-rule` attributes. +- Adds `fill="currentColor"` for dynamic styling. +- Preserves the `viewBox` attribute. +- Generates `.js`, `.d.ts`, and `.tsx` files for each icon. + +## Adding an Icon Set + +Create a new directory under `src/assets/` (e.g., `src/assets/custom/`), add SVG files, and rebuild. The set is automatically available via: + +```ts +import { PlayIcon } from '@videojs/icons/react/custom'; +import { playIcon } from '@videojs/icons/html/custom'; +``` + +## Development + +Running `pnpm -F @videojs/icons dev` starts the build in watch mode. Any changes to `.svg` files under `src/assets/` trigger an automatic rebuild, including new icons and new icon sets — no restart required. + +## Scripts + +```bash +pnpm -F @videojs/icons build # Build all icon sets +pnpm -F @videojs/icons dev # Build and watch for changes +pnpm -F @videojs/icons clean # Remove build output +``` diff --git a/packages/react/src/presets/video/index.ts b/packages/react/src/presets/video/index.ts index 93f7dd44..1b5f3ff1 100644 --- a/packages/react/src/presets/video/index.ts +++ b/packages/react/src/presets/video/index.ts @@ -1,3 +1,5 @@ export { Video, type VideoProps } from '@/media/video'; export * from './minimal-skin'; +export * from './minimal-skin.tailwind'; export * from './skin'; +export * from './skin.tailwind'; diff --git a/packages/react/src/presets/video/minimal-skin.css b/packages/react/src/presets/video/minimal-skin.css index 40840be8..5de21ec4 100644 --- a/packages/react/src/presets/video/minimal-skin.css +++ b/packages/react/src/presets/video/minimal-skin.css @@ -70,27 +70,29 @@ .media-minimal-skin .media-overlay { position: absolute; inset: 0; - display: flex; - flex-flow: column nowrap; - align-items: start; pointer-events: none; border-radius: inherit; background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0)); - transition-property: opacity; - transition-duration: 75ms; - transition-delay: 0ms; - transition-timing-function: ease-out; -} -.media-minimal-skin .media-controls:not([data-visible]) ~ .media-overlay { + backdrop-filter: saturate(1.5) brightness(0.9); transition-delay: 500ms; transition-duration: 500ms; + transition-behavior: allow-discrete; + transition-property: opacity, display; + transition-timing-function: ease-out; opacity: 0; + display: none; } @media (prefers-reduced-motion: reduce) { - .media-minimal-skin .media-controls:not([data-visible]) ~ .media-overlay { + .media-minimal-skin .media-overlay { transition-duration: 100ms; } } +.media-minimal-skin .media-controls[data-visible] ~ .media-overlay { + transition-duration: 50ms; + transition-delay: 0ms; + opacity: 1; + display: block; +} /* Media Control Bar UI/Styles */ .media-minimal-skin .media-controls { @@ -126,7 +128,7 @@ scale: 1; } } -@container media-root (width > 640px) { +@container media-root (width > 40rem) { .media-minimal-skin .media-controls { padding: 2.5rem 0.75rem 0.75rem 0.75rem; gap: 0.875rem; @@ -174,7 +176,7 @@ align-items: center; gap: 0.075rem; } -@container media-root (width > 42rem) { +@container media-root (width > 40rem) { .media-minimal-skin .media-button-group { gap: 0.125rem; } @@ -452,12 +454,12 @@ } @container media-root (width > 48rem) { .media-minimal-skin .media-captions { - font-size: 2rem; + font-size: 1.875rem; } } @container media-root (width > 80rem) { .media-minimal-skin .media-captions { - font-size: 2.5rem; + font-size: 2.25rem; } } .media-minimal-skin .media-controls[data-visible] ~ .media-captions { diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx new file mode 100644 index 00000000..e1c0f026 --- /dev/null +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -0,0 +1,301 @@ +import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core'; +import { + FullscreenEnterIcon, + FullscreenExitIcon, + PauseIcon, + PipIcon, + PlayIcon, + RestartIcon, + SeekIcon, + SpinnerIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@videojs/icons/react/minimal'; +import { cn } from '@videojs/utils/style'; +import { type ComponentProps, forwardRef, type ReactNode } from 'react'; +import { Container } from '@/player/context'; +import { BufferingIndicator } from '@/ui/buffering-indicator'; +import { Controls } from '@/ui/controls'; +import { FullscreenButton } from '@/ui/fullscreen-button'; +import { MuteButton } from '@/ui/mute-button'; +import { PiPButton } from '@/ui/pip-button'; +import { PlayButton } from '@/ui/play-button'; +import { SeekButton } from '@/ui/seek-button'; +import { Time } from '@/ui/time'; +import type { MinimalVideoSkinProps } from './minimal-skin'; + +const SEEK_TIME = 10; + +/* ------------------------------------ Reused fragments ------------------------------------- */ + +const icon = cn( + '[grid-area:1/1] size-4.5', + 'drop-shadow-[0_1px_0_var(--tw-shadow-color)] shadow-black/25', + 'transition-discrete transition-[display,opacity] duration-150 ease-out' +); + +const iconHidden = 'hidden opacity-0'; + +/* --------------------------------------- Components ---------------------------------------- */ + +const Button = forwardRef>(function Button({ className, ...props }, ref) { + return ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + + + + + + + + + {/* Temporary spacer */} + + + + + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + + + {/*
+
+ + An example cue + +
+
*/} + +