From 92b82e94f9cc856164f3b1c1a4e356fb2b07a73f Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Wed, 22 Oct 2025 16:05:31 -0700 Subject: [PATCH] feat(react): add Tailwind v4 compiled CSS for skins with vjs prefix (#114) Co-authored-by: Claude Co-authored-by: Darius Cepulis --- .gitignore | 1 - examples/react-demo/src/App.tsx | 4 +- examples/react-demo/src/globals.css | 3 - examples/react-demo/src/main.tsx | 3 + packages/react/react/build/build-styles.ts | 152 ++++++++++++++++ packages/react/react/package.json | 10 +- .../react/src/skins/frosted/frosted-skin.css | 6 + .../react/react/src/skins/frosted/styles.ts | 169 +++++++++--------- .../react/src/skins/minimal/minimal-skin.css | 3 + .../react/react/src/skins/minimal/styles.ts | 141 +++++++-------- packages/react/react/src/skins/preflight.css | 1 + packages/react/react/tsdown.config.ts | 6 + pnpm-lock.yaml | 67 +++---- website/package.json | 9 +- website/scripts/compile-vjs-styles.js | 71 -------- website/src/components/home/HeroVideo.tsx | 3 +- website/src/styles/vjs-input.css | 11 -- 17 files changed, 370 insertions(+), 290 deletions(-) create mode 100644 packages/react/react/build/build-styles.ts create mode 100644 packages/react/react/src/skins/frosted/frosted-skin.css create mode 100644 packages/react/react/src/skins/minimal/minimal-skin.css create mode 100644 packages/react/react/src/skins/preflight.css delete mode 100644 website/scripts/compile-vjs-styles.js delete mode 100644 website/src/styles/vjs-input.css diff --git a/.gitignore b/.gitignore index ddeb33b0..9dabae00 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ lerna-debug.log* # Build & Generated Outputs # ------------------------- dist/ -build/ lib/ out/ *.tsbuildinfo diff --git a/examples/react-demo/src/App.tsx b/examples/react-demo/src/App.tsx index 20594042..7886ed7a 100644 --- a/examples/react-demo/src/App.tsx +++ b/examples/react-demo/src/App.tsx @@ -2,15 +2,17 @@ import type { ChangeEventHandler } from 'react'; import { FrostedSkin, MediaProvider, MinimalSkin, Video } from '@vjs-10/react'; import { FullscreenEnterAltIcon, FullscreenExitAltIcon } from '@vjs-10/react-icons'; +import clsx from 'clsx'; // import FrostedSkin from './skins/frosted/FrostedSkin'; // import MinimalSkin from './skins/toasted/MinimalSkin'; -import clsx from 'clsx'; // NOTE: Commented out imports are for testing locally/externally defined skins. // import { MediaProvider, Video } from '@vjs-10/react'; import { useCallback, useMemo, useRef, useState } from 'react'; import { useFullscreen } from './hooks/useFullscreen'; +import '@vjs-10/react/skins/frosted.css'; +import '@vjs-10/react/skins/minimal.css'; import './globals.css'; const skins = [ diff --git a/examples/react-demo/src/globals.css b/examples/react-demo/src/globals.css index 18f9a175..b2dac7b1 100644 --- a/examples/react-demo/src/globals.css +++ b/examples/react-demo/src/globals.css @@ -1,8 +1,5 @@ @import 'tailwindcss'; -/* TODO: Documentation for how this will work in the wild. */ -@source "../../../packages/react"; - @theme { --font-sans: InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', diff --git a/examples/react-demo/src/main.tsx b/examples/react-demo/src/main.tsx index ed3872fe..e01f8322 100644 --- a/examples/react-demo/src/main.tsx +++ b/examples/react-demo/src/main.tsx @@ -2,6 +2,9 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; +import '@vjs-10/react/skins/frosted.css'; + +import '@vjs-10/react/skins/minimal.css'; import './globals.css'; diff --git a/packages/react/react/build/build-styles.ts b/packages/react/react/build/build-styles.ts new file mode 100644 index 00000000..f3a4bb0d --- /dev/null +++ b/packages/react/react/build/build-styles.ts @@ -0,0 +1,152 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +/** + * Build Styles Script + * + * This unified script handles the complete CSS build process for skins: + * + * 1. COMPILATION: Runs Tailwind CLI to compile CSS files + * - preflight.css (browser resets) + * - frosted.css (frosted skin) + * - minimal.css (minimal skin) + * + * 2. SCOPING: Scopes preflight selectors to `.vjs` class to prevent global resets + * - Transforms `* { margin: 0 }` → `.vjs * { margin: 0 }` + * - Special cases like `html`, `body`, `:root` → `.vjs` + * + * 3. CONCATENATION: Prepends scoped preflight to each skin CSS file + * - frosted.css = scoped-preflight + frosted styles + * - minimal.css = scoped-preflight + minimal styles + * - Deletes standalone preflight.css (no longer needed) + * + * Why scope preflight? + * - Tailwind's preflight resets affect ALL elements globally + * - Scoping to `.vjs` ensures resets only apply inside video player containers + * + * Why concatenate? + * - Users only need to import one CSS file per skin + * - Simpler API: import '@vjs-10/react/skins/frosted.css' + */ + +import { execSync } from 'node:child_process'; +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import postcss from 'postcss'; +import prefixSelector from 'postcss-prefix-selector'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, '..'); + +const preflightFile = 'dist/skins/preflight.css'; +const frostedFile = 'dist/skins/frosted.css'; +const minimalFile = 'dist/skins/minimal.css'; + +export default async function buildStyles() { + console.log('🎨 Building skin styles...'); + + try { + // ============================================================================ + // STEP 1: Compile CSS files with Tailwind CLI + // ============================================================================ + console.log(' → Compiling CSS with Tailwind...'); + + // Create output directory + const execOpts = { cwd: rootDir, stdio: 'inherit', encoding: 'utf8' }; + const skinsDir = join(rootDir, 'dist', 'skins'); + + mkdirSync(skinsDir, { recursive: true }); + + // Compile preflight (browser resets) + execSync( + 'pnpm exec tailwindcss -i src/skins/preflight.css -o dist/skins/preflight.css --minify', + execOpts, + ); + + // Compile frosted skin + execSync( + 'pnpm exec tailwindcss -i src/skins/frosted/frosted-skin.css -o dist/skins/frosted.css --minify', + execOpts, + ); + + // Compile minimal skin + execSync( + 'pnpm exec tailwindcss -i src/skins/minimal/minimal-skin.css -o dist/skins/minimal.css --minify', + execOpts, + ); + + // Verify all CSS files were created + const preflightPath = join(rootDir, preflightFile); + const frostedPath = join(rootDir, frostedFile); + const minimalPath = join(rootDir, minimalFile); + + if (!existsSync(preflightPath)) { + throw new Error(`Tailwind failed to create ${preflightFile}`); + } + if (!existsSync(frostedPath)) { + throw new Error(`Tailwind failed to create ${frostedFile}`); + } + if (!existsSync(minimalPath)) { + throw new Error(`Tailwind failed to create ${minimalFile}`); + } + + // ============================================================================ + // STEP 2: Scope preflight CSS to .vjs + // ============================================================================ + console.log(' → Scoping preflight selectors to .vjs...'); + + const preflightCss = readFileSync(preflightPath, 'utf-8'); + + // Use PostCSS with postcss-prefix-selector plugin to transform all selectors + const result = await postcss([ + prefixSelector({ + prefix: '.vjs', + transform(prefix, selector, prefixedSelector) { + // Special case transformations: + // - `html`, `body`, `:root` should become just `.vjs` (not `.vjs html`) + if (selector === 'body' || selector === 'html' || selector === ':root') { + return prefix; + } else if (selector === ':host') { + // Keep :host as-is (for web components) + return selector; + } else { + // All other selectors: prepend .vjs + return prefixedSelector; + } + }, + }), + ]).process(preflightCss, { from: preflightFile, to: preflightFile }); + + // Clean up any duplicate .vjs classes that may have been generated + const scopedPreflight = result.css.replace(/(\.vjs\s+)+/g, '.vjs '); + + // ============================================================================ + // STEP 3: Concatenate scoped preflight into each skin + // ============================================================================ + console.log(' → Concatenating preflight into frosted.css...'); + + const frostedCss = readFileSync(frostedPath, 'utf-8'); + const frostedWithPreflight = `${scopedPreflight}\n${frostedCss}`; + writeFileSync(frostedPath, frostedWithPreflight, 'utf-8'); + + console.log(' → Concatenating preflight into minimal.css...'); + + const minimalCss = readFileSync(minimalPath, 'utf-8'); + const minimalWithPreflight = `${scopedPreflight}\n${minimalCss}`; + writeFileSync(minimalPath, minimalWithPreflight, 'utf-8'); + + // ============================================================================ + // STEP 4: Clean up - delete standalone preflight.css + // ============================================================================ + console.log(' → Removing standalone preflight.css...'); + unlinkSync(preflightPath); + + console.log('✅ Skin styles built successfully'); + } catch (error) { + console.error('❌ Failed to build styles:', error.message); + process.exit(1); + } +} diff --git a/packages/react/react/package.json b/packages/react/react/package.json index 8a55e571..21ce74ac 100644 --- a/packages/react/react/package.json +++ b/packages/react/react/package.json @@ -15,7 +15,9 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./skins/frosted.css": "./dist/skins/frosted.css", + "./skins/minimal.css": "./dist/skins/minimal.css" }, "main": "dist/index.js", "module": "dist/index.js", @@ -25,7 +27,7 @@ ], "scripts": { "build": "tsdown", - "build:watch": "tsdown --watch --silent", + "build:watch": "tsdown --watch ./src --silent", "dev": "pnpm run build:watch", "test": "echo \"No tests yet\"", "clean": "rm -rf dist" @@ -43,8 +45,12 @@ "@vjs-10/react-media-store": "workspace:*" }, "devDependencies": { + "@tailwindcss/cli": "^4.1.0", "@types/react": "^18.0.0", + "postcss": "^8.4.49", + "postcss-prefix-selector": "^1.16.1", "react": "^18.0.0", + "tailwindcss": "^4.1.13", "tsdown": "^0.15.9", "typescript": "^5.9.2" }, diff --git a/packages/react/react/src/skins/frosted/frosted-skin.css b/packages/react/react/src/skins/frosted/frosted-skin.css new file mode 100644 index 00000000..7f1db366 --- /dev/null +++ b/packages/react/react/src/skins/frosted/frosted-skin.css @@ -0,0 +1,6 @@ +@layer theme, base, components, utilities; +@import 'tailwindcss/theme.css' layer(theme) prefix(vjs); +@import 'tailwindcss/utilities.css' layer(utilities) prefix(vjs); + +/* Custom variants used by the frosted skin */ +@custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce); diff --git a/packages/react/react/src/skins/frosted/styles.ts b/packages/react/react/src/skins/frosted/styles.ts index e79d1e0c..191e9b4d 100644 --- a/packages/react/react/src/skins/frosted/styles.ts +++ b/packages/react/react/src/skins/frosted/styles.ts @@ -11,171 +11,172 @@ function cn(...classes: (string | undefined)[]): string { const styles: FrostedSkinStyles = { MediaContainer: cn( - 'relative isolate @container/root group/root overflow-clip', + 'vjs', // scope preflight + 'vjs:relative vjs:isolate vjs:@container/root vjs:group/root vjs:overflow-clip', // Base typography - 'text-[0.8125rem]', // 13px + 'vjs:text-[0.8125rem]', // 13px // Prevent rounded corners in fullscreen. - '[&:fullscreen]:rounded-none [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full', + 'vjs:[&:fullscreen]:rounded-none vjs:[&:fullscreen]:[&_video]:h-full vjs:[&:fullscreen]:[&_video]:w-full', // Fancy borders. - 'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-white/10 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]', - 'before:absolute before:inset-px before:rounded-[inherit] before:ring-white/15 before:ring-1 before:ring-inset before:z-10 before:pointer-events-none dark:before:ring-0', + 'vjs:after:absolute vjs:after:inset-0 vjs:after:ring-black/10 vjs:after:ring-1 vjs:dark:after:ring-white/10 vjs:after:ring-inset vjs:after:z-10 vjs:after:pointer-events-none vjs:after:rounded-[inherit]', + 'vjs:before:absolute vjs:before:inset-px vjs:before:rounded-[inherit] vjs:before:ring-white/15 vjs:before:ring-1 vjs:before:ring-inset vjs:before:z-10 vjs:before:pointer-events-none vjs:dark:before:ring-0', // Ensure the nested video inherits the radius. - '[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto', + 'vjs:[&_video]:rounded-[inherit] vjs:[&_video]:w-full vjs:[&_video]:h-auto', ), Overlay: cn( - 'opacity-0 delay-500 rounded-[inherit] absolute inset-0 pointer-events-none bg-gradient-to-t from-black/50 via-black/20 to-transparent transition-opacity backdrop-saturate-150 backdrop-brightness-90', + 'vjs:opacity-0 vjs:delay-500 vjs:rounded-[inherit] vjs:absolute vjs:inset-0 vjs:pointer-events-none vjs:bg-gradient-to-t vjs:from-black/50 vjs:via-black/20 vjs:to-transparent vjs:transition-opacity vjs:backdrop-saturate-150 vjs:backdrop-brightness-90', // Hide when playing (for now). - // FIXME: This is crude temporary logic, we’ll improve it later I guess with a [data-show-controls] attribute or something? - 'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0', - 'has-[+.controls_[aria-expanded="true"]]:opacity-100 has-[+.controls_[aria-expanded="true"]]:delay-0', - 'group-hover/root:opacity-100 group-hover/root:delay-0', + // FIXME: This is crude temporary logic, we'll improve it later I guess with a [data-show-controls] attribute or something? + 'vjs:has-[+.controls_[data-paused]]:opacity-100 vjs:has-[+.controls_[data-paused]]:delay-0', + 'vjs:has-[+.controls_[aria-expanded="true"]]:opacity-100 vjs:has-[+.controls_[aria-expanded="true"]]:delay-0', + 'vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0', ), Controls: cn( 'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility. - '@container/controls absolute inset-x-3 bottom-3 rounded-full flex items-center p-1 ring ring-white/10 ring-inset gap-0.5 text-white', - 'shadow-sm shadow-black/15', + 'vjs:@container/controls vjs:absolute vjs:inset-x-3 vjs:bottom-3 vjs:rounded-full vjs:flex vjs:items-center vjs:p-1 vjs:ring vjs:ring-white/10 vjs:ring-inset vjs:gap-0.5 vjs:text-white', + 'vjs:shadow-sm vjs:shadow-black/15', // Background - 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', + 'vjs:bg-white/10 vjs:backdrop-blur-3xl vjs:backdrop-saturate-150 vjs:backdrop-brightness-90', // Animation - 'transition will-change-transform origin-bottom ease-out', + 'vjs:transition vjs:will-change-transform vjs:origin-bottom vjs:ease-out', // FIXME: Temporary hide/show logic - 'scale-90 opacity-0 delay-500 duration-300', - 'has-[[data-paused]]:scale-100 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0', - 'has-[[aria-expanded="true"]]:scale-100 has-[[aria-expanded="true"]]:opacity-100 has-[[aria-expanded="true"]]:delay-0', - 'group-hover/root:scale-100 group-hover/root:opacity-100 group-hover/root:delay-0', + 'vjs:scale-90 vjs:opacity-0 vjs:delay-500 vjs:duration-300', + 'vjs:has-[[data-paused]]:scale-100 vjs:has-[[data-paused]]:opacity-100 vjs:has-[[data-paused]]:delay-0', + 'vjs:has-[[aria-expanded="true"]]:scale-100 vjs:has-[[aria-expanded="true"]]:opacity-100 vjs:has-[[aria-expanded="true"]]:delay-0', + 'vjs:group-hover/root:scale-100 vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0', // Border to enhance contrast on lighter videos - 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10', + 'vjs:after:absolute vjs:after:inset-0 vjs:after:ring vjs:after:rounded-[inherit] vjs:after:ring-black/15 vjs:after:pointer-events-none vjs:after:z-10', // Reduced transparency for users with preference // XXX: This requires a Tailwind custom variant (see 1 below) - 'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20', + 'vjs:reduced-transparency:bg-black/70 vjs:reduced-transparency:ring-black vjs:reduced-transparency:after:ring-white/20', // High contrast mode - 'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20', + 'vjs:contrast-more:bg-black/90 vjs:contrast-more:ring-black vjs:contrast-more:after:ring-white/20', ), Icon: cn('icon'), Button: cn( - 'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-full', + 'vjs:group/button vjs:cursor-pointer vjs:relative vjs:shrink-0 vjs:transition vjs:select-none vjs:p-2 vjs:rounded-full', // Background/foreground - 'bg-transparent text-white/90', + 'vjs:bg-transparent vjs:text-white/90', // Hover and focus states - 'hover:no-underline hover:bg-white/10 hover:text-white focus-visible:no-underline focus-visible:bg-white/10 focus-visible:text-white', + 'vjs:hover:no-underline vjs:hover:bg-white/10 vjs:hover:text-white vjs:focus-visible:no-underline vjs:focus-visible:bg-white/10 vjs:focus-visible:text-white', // Focus state - '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', + 'vjs:-outline-offset-2 vjs:focus-visible:outline-2 vjs:focus-visible:outline-offset-2 vjs:focus-visible:outline-blue-500', // Disabled state - 'aria-disabled:grayscale aria-disabled:opacity-50 aria-disabled:cursor-not-allowed', + 'vjs:aria-disabled:grayscale vjs:aria-disabled:opacity-50 vjs:aria-disabled:cursor-not-allowed', // Loading state - 'aria-busy:pointer-events-none aria-busy:cursor-not-allowed', + 'vjs:aria-busy:pointer-events-none vjs:aria-busy:cursor-not-allowed', // Expanded state - 'aria-expanded:bg-white/10 aria-expanded:text-white', + 'vjs:aria-expanded:bg-white/10 vjs:aria-expanded:text-white', // Pressed state - 'active:scale-95', + 'vjs:active:scale-95', ), IconButton: cn( - 'grid [&_.icon]:[grid-area:1/1]', - '[&_.icon]:shrink-0 [&_.icon]:transition-opacity [&_.icon]:duration-300 [&_.icon]:ease-out [&_.icon]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] [&_.icon]:shadow-black/25', + 'vjs:grid vjs:[&_.icon]:[grid-area:1/1]', + 'vjs:[&_.icon]:shrink-0 vjs:[&_.icon]:transition-opacity vjs:[&_.icon]:duration-300 vjs:[&_.icon]:ease-out vjs:[&_.icon]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] vjs:[&_.icon]:shadow-black/25', ), PlayButton: cn( - '[&_.pause-icon]:opacity-100 [&[data-paused]_.pause-icon]:opacity-0', - '[&_.play-icon]:opacity-0 [&[data-paused]_.play-icon]:opacity-100', + 'vjs:[&_.pause-icon]:opacity-100 vjs:[&[data-paused]_.pause-icon]:opacity-0', + 'vjs:[&_.play-icon]:opacity-0 vjs:[&[data-paused]_.play-icon]:opacity-100', ), PlayIcon: cn('play-icon'), PauseIcon: cn('pause-icon'), TooltipPopup: cn( - 'whitespace-nowrap flex origin-[var(--transform-origin)] flex-col rounded-md text-white text-xs @7xl/root:text-sm px-2 py-1', + 'vjs:whitespace-nowrap vjs:flex vjs:origin-[var(--transform-origin)] vjs:flex-col vjs:rounded-md vjs:text-white vjs:text-xs vjs:@7xl/root:text-sm vjs:px-2 vjs:py-1', // Background - 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', + 'vjs:bg-white/10 vjs:backdrop-blur-3xl vjs:backdrop-saturate-150 vjs:backdrop-brightness-90', // Animation - 'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0', + 'vjs:transition-[transform,scale,opacity] vjs:data-[ending-style]:scale-90 vjs:data-[ending-style]:opacity-0 vjs:data-[instant]:duration-0 vjs:data-[starting-style]:scale-90 vjs:data-[starting-style]:opacity-0', // Ring - 'ring-1 ring-white/10 ring-inset', + 'vjs:ring-1 vjs:ring-white/10 vjs:ring-inset', // Border to enhance contrast on lighter videos - 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none', + 'vjs:after:absolute vjs:after:inset-0 vjs:after:ring vjs:after:rounded-[inherit] vjs:after:ring-black/15 vjs:after:pointer-events-none', ), PlayTooltipPopup: cn( - '[&_.pause-tooltip]:inline [&[data-paused]_.pause-tooltip]:hidden', - '[&_.play-tooltip]:hidden [&[data-paused]_.play-tooltip]:inline', + 'vjs:[&_.pause-tooltip]:inline vjs:[&[data-paused]_.pause-tooltip]:hidden', + 'vjs:[&_.play-tooltip]:hidden vjs:[&[data-paused]_.play-tooltip]:inline', ), PlayTooltip: cn('play-tooltip'), PauseTooltip: cn('pause-tooltip'), MuteButton: cn( - '[&_.icon]:opacity-0', - '[&[data-volume-level="high"]_.volume-high-icon]:opacity-100', - '[&[data-volume-level="medium"]_.volume-low-icon]:opacity-100', - '[&[data-volume-level="low"]_.volume-low-icon]:opacity-100', - '[&[data-volume-level="off"]_.volume-off-icon]:opacity-100', + 'vjs:[&_.icon]:opacity-0', + 'vjs:[&[data-volume-level="high"]_.volume-high-icon]:opacity-100', + 'vjs:[&[data-volume-level="medium"]_.volume-low-icon]:opacity-100', + 'vjs:[&[data-volume-level="low"]_.volume-low-icon]:opacity-100', + 'vjs:[&[data-volume-level="off"]_.volume-off-icon]:opacity-100', ), VolumeHighIcon: cn('volume-high-icon'), VolumeLowIcon: cn('volume-low-icon'), VolumeOffIcon: cn('volume-off-icon'), FullscreenButton: cn( - '[&_.fullscreen-enter-icon]:opacity-100 [&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0', - '[&_.fullscreen-exit-icon]:opacity-0 [&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100', - '[&_path]:transition-transform ease-out', + 'vjs:[&_.fullscreen-enter-icon]:opacity-100 vjs:[&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0', + 'vjs:[&_.fullscreen-exit-icon]:opacity-0 vjs:[&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100', + 'vjs:[&_path]:transition-transform vjs:ease-out', ), FullscreenEnterIcon: cn( 'fullscreen-enter-icon', - 'group-hover/button:[&_.arrow-1]:-translate-x-px group-hover/button:[&_.arrow-1]:-translate-y-px', - 'group-hover/button:[&_.arrow-2]:translate-x-px group-hover/button:[&_.arrow-2]:translate-y-px', + 'vjs:group-hover/button:[&_.arrow-1]:-translate-x-px vjs:group-hover/button:[&_.arrow-1]:-translate-y-px', + 'vjs:group-hover/button:[&_.arrow-2]:translate-x-px vjs:group-hover/button:[&_.arrow-2]:translate-y-px', ), FullscreenExitIcon: cn( 'fullscreen-exit-icon', - '[&_.arrow-1]:-translate-x-px [&_.arrow-1]:-translate-y-px', - '[&_.arrow-2]:translate-x-px [&_.arrow-2]:translate-y-px', - 'group-hover/button:[&_.arrow-1]:translate-0', - 'group-hover/button:[&_.arrow-2]:translate-0', + 'vjs:[&_.arrow-1]:-translate-x-px vjs:[&_.arrow-1]:-translate-y-px', + 'vjs:[&_.arrow-2]:translate-x-px vjs:[&_.arrow-2]:translate-y-px', + 'vjs:group-hover/button:[&_.arrow-1]:translate-0', + 'vjs:group-hover/button:[&_.arrow-2]:translate-0', ), FullscreenTooltipPopup: cn( - '[&_.fullscreen-enter-tooltip]:inline [&[data-fullscreen]_.fullscreen-enter-tooltip]:hidden', - '[&_.fullscreen-exit-tooltip]:hidden [&[data-fullscreen]_.fullscreen-exit-tooltip]:inline', + 'vjs:[&_.fullscreen-enter-tooltip]:inline vjs:[&[data-fullscreen]_.fullscreen-enter-tooltip]:hidden', + 'vjs:[&_.fullscreen-exit-tooltip]:hidden vjs:[&[data-fullscreen]_.fullscreen-exit-tooltip]:inline', ), FullscreenEnterTooltip: cn('fullscreen-enter-tooltip'), FullscreenExitTooltip: cn('fullscreen-exit-tooltip'), - TimeControls: cn('flex-1 flex items-center gap-3 px-1.5'), - TimeDisplay: cn('tabular-nums text-shadow-2xs/25'), + TimeControls: cn('vjs:flex-1 vjs:flex vjs:items-center vjs:gap-3 vjs:px-1.5'), + TimeDisplay: cn('vjs:tabular-nums vjs:text-shadow-2xs/25'), SliderRoot: cn( - 'flex items-center justify-center flex-1 group/slider relative rounded-full', - '[&[data-orientation="horizontal"]]:h-5 [&[data-orientation="horizontal"]]:min-w-20', - '[&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20', + 'vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:group/slider vjs:relative vjs:rounded-full', + 'vjs:[&[data-orientation="horizontal"]]:h-5 vjs:[&[data-orientation="horizontal"]]:min-w-20', + 'vjs:[&[data-orientation="vertical"]]:w-5 vjs:[&[data-orientation="vertical"]]:h-20', // Focus state - 'focus-visible:outline-2 focus-visible:outline-blue-500', + 'vjs:focus-visible:outline-2 vjs:focus-visible:outline-blue-500', ), SliderTrack: cn( - 'w-full relative select-none rounded-full bg-white/20 ring-1 ring-black/5', - '[&[data-orientation="horizontal"]]:h-1', - '[&[data-orientation="vertical"]]:w-1', + 'vjs:w-full vjs:relative vjs:select-none vjs:rounded-full vjs:bg-white/20 vjs:ring-1 vjs:ring-black/5', + 'vjs:[&[data-orientation="horizontal"]]:h-1', + 'vjs:[&[data-orientation="vertical"]]:w-1', ), - SliderProgress: cn('bg-white rounded-[inherit]'), + SliderProgress: cn('vjs:bg-white vjs:rounded-[inherit]'), // TODO: Work out what we want to do here. - SliderPointer: cn('bg-white/20 rounded-[inherit]'), + SliderPointer: cn('vjs:bg-white/20 vjs:rounded-[inherit]'), SliderThumb: cn( - 'bg-white z-10 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15', - 'opacity-0 transition-[opacity,height,width] ease-in-out', - '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', - 'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100', - 'size-2.5 active:size-3 group-active/slider:size-3', - '[&[data-orientation="horizontal"]]:hover:cursor-ew-resize', - '[&[data-orientation="vertical"]]:hover:cursor-ns-resize', + 'vjs:bg-white vjs:z-10 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15', + 'vjs:opacity-0 vjs:transition-[opacity,height,width] vjs:ease-in-out', + 'vjs:-outline-offset-2 vjs:focus-visible:outline-2 vjs:focus-visible:outline-offset-2 vjs:focus-visible:outline-blue-500', + 'vjs:group-hover/slider:opacity-100 vjs:group-focus-within/slider:opacity-100', + 'vjs:size-2.5 vjs:active:size-3 vjs:group-active/slider:size-3', + 'vjs:[&[data-orientation="horizontal"]]:hover:cursor-ew-resize', + 'vjs:[&[data-orientation="vertical"]]:hover:cursor-ns-resize', ), TimeSliderRoot: cn( // Focus state - '-outline-offset-8 focus-visible:outline-offset-8', + 'vjs:-outline-offset-8 vjs:focus-visible:outline-offset-8', ), VolumeSliderRoot: cn( // Focus state - '-outline-offset-4 focus-visible:outline-offset-4', + 'vjs:-outline-offset-4 vjs:focus-visible:outline-offset-4', ), PopoverPopup: cn( - 'relative px-1.5 py-3.5 rounded-full', - 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', - 'ring ring-white/10 ring-inset shadow-sm shadow-black/15', + 'vjs:relative vjs:px-1.5 vjs:py-3.5 vjs:rounded-full', + 'vjs:bg-white/10 vjs:backdrop-blur-3xl vjs:backdrop-saturate-150 vjs:backdrop-brightness-90', + 'vjs:ring vjs:ring-white/10 vjs:ring-inset vjs:shadow-sm vjs:shadow-black/15', // Animation - 'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0', + 'vjs:transition-[transform,scale,opacity] vjs:data-[ending-style]:scale-90 vjs:data-[ending-style]:opacity-0 vjs:data-[instant]:duration-0 vjs:data-[starting-style]:scale-90 vjs:data-[starting-style]:opacity-0', // Border to enhance contrast on lighter videos - 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10', + 'vjs:after:absolute vjs:after:inset-0 vjs:after:ring vjs:after:rounded-[inherit] vjs:after:ring-black/15 vjs:after:pointer-events-none vjs:after:z-10', // Reduced transparency for users with preference // XXX: This requires a Tailwind custom variant (see 1 below) - 'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20', + 'vjs:reduced-transparency:bg-black/70 vjs:reduced-transparency:ring-black vjs:reduced-transparency:after:ring-white/20', // High contrast mode - 'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20', + 'vjs:contrast-more:bg-black/90 vjs:contrast-more:ring-black vjs:contrast-more:after:ring-white/20', ), }; diff --git a/packages/react/react/src/skins/minimal/minimal-skin.css b/packages/react/react/src/skins/minimal/minimal-skin.css new file mode 100644 index 00000000..9e59a062 --- /dev/null +++ b/packages/react/react/src/skins/minimal/minimal-skin.css @@ -0,0 +1,3 @@ +@layer theme, base, components, utilities; +@import 'tailwindcss/theme.css' layer(theme) prefix(vjs); +@import 'tailwindcss/utilities.css' layer(utilities) prefix(vjs); diff --git a/packages/react/react/src/skins/minimal/styles.ts b/packages/react/react/src/skins/minimal/styles.ts index 08633bce..9af5b4d2 100644 --- a/packages/react/react/src/skins/minimal/styles.ts +++ b/packages/react/react/src/skins/minimal/styles.ts @@ -11,138 +11,139 @@ function cn(...classes: (string | undefined)[]): string { const styles: MinimalSkinStyles = { MediaContainer: cn( - 'relative isolate @container/root group/root overflow-clip bg-black', + 'vjs', // scope preflight + 'vjs:relative vjs:isolate vjs:@container/root vjs:group/root vjs:overflow-clip vjs:bg-black', // Base typography - 'text-[0.8125rem]', // 13px + 'vjs:text-[0.8125rem]', // 13px // 'ring-1 ring-inset ring-black/10 dark:ring-white/10', - 'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-white/10 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]', + 'vjs:after:absolute vjs:after:inset-0 vjs:after:ring-black/10 vjs:after:ring-1 vjs:dark:after:ring-white/10 vjs:after:ring-inset vjs:after:z-10 vjs:after:pointer-events-none vjs:after:rounded-[inherit]', // Prevent rounded corners in fullscreen. - '[&:fullscreen]:rounded-none [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full', + 'vjs:[&:fullscreen]:rounded-none vjs:[&:fullscreen]:[&_video]:h-full vjs:[&:fullscreen]:[&_video]:w-full', // Ensure the nested video inherits the radius. - '[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto', + 'vjs:[&_video]:rounded-[inherit] vjs:[&_video]:w-full vjs:[&_video]:h-auto', ), Overlay: cn( - 'absolute inset-0 rounded-[inherit]', - 'bg-gradient-to-t from-black/70 via-black/50 via-[120px] to-transparent', - 'opacity-0 delay-500 duration-300', + 'vjs:absolute vjs:inset-0 vjs:rounded-[inherit]', + 'vjs:bg-gradient-to-t vjs:from-black/70 vjs:via-black/50 vjs:via-[120px] vjs:to-transparent', + 'vjs:opacity-0 vjs:delay-500 vjs:duration-300', // FIXME: Temporary hide/show logic - 'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0 has-[+.controls_[data-paused]]:duration-75', - 'has-[+.controls_[aria-expanded="true"]]:opacity-100 has-[+.controls_[aria-expanded="true"]]:delay-0 has-[+.controls_[aria-expanded="true"]]:duration-75', - 'group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75', + 'vjs:has-[+.controls_[data-paused]]:opacity-100 vjs:has-[+.controls_[data-paused]]:delay-0 vjs:has-[+.controls_[data-paused]]:duration-75', + 'vjs:has-[+.controls_[aria-expanded="true"]]:opacity-100 vjs:has-[+.controls_[aria-expanded="true"]]:delay-0 vjs:has-[+.controls_[aria-expanded="true"]]:duration-75', + 'vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0 vjs:group-hover/root:duration-75', // High contrast mode - 'contrast-more:from-black/75', + 'vjs:contrast-more:from-black/75', ), Controls: cn( 'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility. - '@container/controls absolute inset-x-0 bottom-0 flex items-center gap-3.5 z-20 px-3 pb-3 pt-10 text-white', + 'vjs:@container/controls vjs:absolute vjs:inset-x-0 vjs:bottom-0 vjs:flex vjs:items-center vjs:gap-3.5 vjs:z-20 vjs:px-3 vjs:pb-3 vjs:pt-10 vjs:text-white', // Animation - 'transition ease-in-out', + 'vjs:transition vjs:ease-in-out', // FIXME: Temporary hide/show logic - 'translate-y-full opacity-0 delay-500 duration-300', - 'has-[[data-paused]]:translate-y-0 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0 has-[[data-paused]]:duration-75', - 'has-[[aria-expanded="true"]]:translate-y-0 has-[[aria-expanded="true"]]:opacity-100 has-[[aria-expanded="true"]]:delay-0 has-[[aria-expanded="true"]]:duration-75', - 'group-hover/root:translate-y-0 group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75', + 'vjs:translate-y-full vjs:opacity-0 vjs:delay-500 vjs:duration-300', + 'vjs:has-[[data-paused]]:translate-y-0 vjs:has-[[data-paused]]:opacity-100 vjs:has-[[data-paused]]:delay-0 vjs:has-[[data-paused]]:duration-75', + 'vjs:has-[[aria-expanded="true"]]:translate-y-0 vjs:has-[[aria-expanded="true"]]:opacity-100 vjs:has-[[aria-expanded="true"]]:delay-0 vjs:has-[[aria-expanded="true"]]:duration-75', + 'vjs:group-hover/root:translate-y-0 vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0 vjs:group-hover/root:duration-75', ), Icon: cn('icon'), Button: cn( - 'group/button cursor-pointer relative shrink-0 transition select-none p-2.5 rounded-md', + 'vjs:group/button vjs:cursor-pointer vjs:relative vjs:shrink-0 vjs:transition vjs:select-none vjs:p-2.5 vjs:rounded-md', // Background/foreground - 'bg-transparent text-white', + 'vjs:bg-transparent vjs:text-white', // Hover and focus states - 'hover:text-white/80 focus-visible:text-white/80', + 'vjs:hover:text-white/80 vjs:focus-visible:text-white/80', // Focus state - '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white', + 'vjs:-outline-offset-2 vjs:focus-visible:outline-2 vjs:focus-visible:outline-offset-2 vjs:focus-visible:outline-white', // Disabled state - 'aria-disabled:grayscale aria-disabled:opacity-50 aria-disabled:cursor-not-allowed', + 'vjs:aria-disabled:grayscale vjs:aria-disabled:opacity-50 vjs:aria-disabled:cursor-not-allowed', // Loading state - 'aria-busy:pointer-events-none aria-busy:cursor-not-allowed', + 'vjs:aria-busy:pointer-events-none vjs:aria-busy:cursor-not-allowed', // Expanded state - 'aria-expanded:text-white/80', + 'vjs:aria-expanded:text-white/80', ), - ButtonGroup: cn('flex items-center gap-1.5'), + ButtonGroup: cn('vjs:flex vjs:items-center vjs:gap-1.5'), IconButton: cn( - 'grid [&_.icon]:[grid-area:1/1]', - '[&_.icon]:shrink-0 [&_.icon]:transition [&_.icon]:duration-300 [&_.icon]:ease-out [&_.icon]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] [&_.icon]:shadow-black/40', + 'vjs:grid vjs:[&_.icon]:[grid-area:1/1]', + 'vjs:[&_.icon]:shrink-0 vjs:[&_.icon]:transition vjs:[&_.icon]:duration-300 vjs:[&_.icon]:ease-out vjs:[&_.icon]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] vjs:[&_.icon]:shadow-black/40', ), PlayButton: cn( - '[&_.pause-icon]:opacity-100 [&[data-paused]_.pause-icon]:opacity-0', - '[&_.play-icon]:opacity-0 [&[data-paused]_.play-icon]:opacity-100', + 'vjs:[&_.pause-icon]:opacity-100 vjs:[&[data-paused]_.pause-icon]:opacity-0', + 'vjs:[&_.play-icon]:opacity-0 vjs:[&[data-paused]_.play-icon]:opacity-100', ), PlayIcon: cn('play-icon'), PauseIcon: cn('pause-icon'), TooltipPopup: cn( - 'whitespace-nowrap flex origin-[var(--transform-origin)] flex-col rounded-md text-white text-xs @7xl/root:text-sm px-2 py-1', - 'bg-black/10 backdrop-blur-md', + 'vjs:whitespace-nowrap vjs:flex vjs:origin-[var(--transform-origin)] vjs:flex-col vjs:rounded-md vjs:text-white vjs:text-xs vjs:@7xl/root:text-sm vjs:px-2 vjs:py-1', + 'vjs:bg-black/10 vjs:backdrop-blur-md', // Animation - 'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0', + 'vjs:transition-[transform,scale,opacity] vjs:data-[ending-style]:scale-90 vjs:data-[ending-style]:opacity-0 vjs:data-[instant]:duration-0 vjs:data-[starting-style]:scale-90 vjs:data-[starting-style]:opacity-0', ), PlayTooltipPopup: cn( - '[&_.pause-tooltip]:inline [&[data-paused]_.pause-tooltip]:hidden', - '[&_.play-tooltip]:hidden [&[data-paused]_.play-tooltip]:inline', + 'vjs:[&_.pause-tooltip]:inline vjs:[&[data-paused]_.pause-tooltip]:hidden', + 'vjs:[&_.play-tooltip]:hidden vjs:[&[data-paused]_.play-tooltip]:inline', ), PlayTooltip: cn('play-tooltip'), PauseTooltip: cn('pause-tooltip'), MuteButton: cn( - '[&_.icon]:hidden', - '[&[data-volume-level="high"]_.volume-high-icon]:inline', - '[&[data-volume-level="medium"]_.volume-low-icon]:inline', - '[&[data-volume-level="low"]_.volume-low-icon]:inline', - '[&[data-volume-level="off"]_.volume-off-icon]:inline', + 'vjs:[&_.icon]:hidden', + 'vjs:[&[data-volume-level="high"]_.volume-high-icon]:inline', + 'vjs:[&[data-volume-level="medium"]_.volume-low-icon]:inline', + 'vjs:[&[data-volume-level="low"]_.volume-low-icon]:inline', + 'vjs:[&[data-volume-level="off"]_.volume-off-icon]:inline', ), VolumeHighIcon: cn('volume-high-icon'), VolumeLowIcon: cn('volume-low-icon'), VolumeOffIcon: cn('volume-off-icon'), FullscreenButton: cn( - '[&_.fullscreen-enter-icon]:opacity-100 [&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0', - '[&_.fullscreen-exit-icon]:opacity-0 [&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100', - '[&_path]:transition-transform ease-out', + 'vjs:[&_.fullscreen-enter-icon]:opacity-100 vjs:[&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0', + 'vjs:[&_.fullscreen-exit-icon]:opacity-0 vjs:[&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100', + 'vjs:[&_path]:transition-transform vjs:ease-out', ), FullscreenEnterIcon: cn( 'fullscreen-enter-icon', - 'group-hover/button:[&_.arrow-1]:translate-x-px group-hover/button:[&_.arrow-1]:-translate-y-px', - 'group-hover/button:[&_.arrow-2]:-translate-x-px group-hover/button:[&_.arrow-2]:translate-y-px', + 'vjs:group-hover/button:[&_.arrow-1]:translate-x-px vjs:group-hover/button:[&_.arrow-1]:-translate-y-px', + 'vjs:group-hover/button:[&_.arrow-2]:-translate-x-px vjs:group-hover/button:[&_.arrow-2]:translate-y-px', ), FullscreenExitIcon: cn( 'fullscreen-exit-icon', - '[&_.arrow-1]:translate-x-px [&_.arrow-1]:-translate-y-px', - '[&_.arrow-2]:-translate-x-px [&_.arrow-2]:translate-y-px', - 'group-hover/button:[&_.arrow-1]:translate-0', - 'group-hover/button:[&_.arrow-2]:translate-0', + 'vjs:[&_.arrow-1]:translate-x-px vjs:[&_.arrow-1]:-translate-y-px', + 'vjs:[&_.arrow-2]:-translate-x-px vjs:[&_.arrow-2]:translate-y-px', + 'vjs:group-hover/button:[&_.arrow-1]:translate-0', + 'vjs:group-hover/button:[&_.arrow-2]:translate-0', ), FullscreenTooltipPopup: cn( - '[&_.fullscreen-enter-tooltip]:inline [&[data-fullscreen]_.fullscreen-enter-tooltip]:hidden', - '[&_.fullscreen-exit-tooltip]:hidden [&[data-fullscreen]_.fullscreen-exit-tooltip]:inline', + 'vjs:[&_.fullscreen-enter-tooltip]:inline vjs:[&[data-fullscreen]_.fullscreen-enter-tooltip]:hidden', + 'vjs:[&_.fullscreen-exit-tooltip]:hidden vjs:[&[data-fullscreen]_.fullscreen-exit-tooltip]:inline', ), FullscreenEnterTooltip: cn('fullscreen-enter-tooltip'), FullscreenExitTooltip: cn('fullscreen-exit-tooltip'), - TimeSliderRoot: cn('mx-2'), - TimeDisplay: cn('tabular-nums text-shadow-2xs/20'), - DurationDisplay: cn('text-white/50 contents'), + TimeSliderRoot: cn('vjs:mx-2'), + TimeDisplay: cn('vjs:tabular-nums vjs:text-shadow-2xs/20'), + DurationDisplay: cn('vjs:text-white/50 vjs:contents'), SliderRoot: cn( - 'flex items-center justify-center flex-1 group/slider relative rounded-sm', - '[&[data-orientation="horizontal"]]:h-5 [&[data-orientation="horizontal"]]:min-w-20', - '[&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-18', + 'vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:group/slider vjs:relative vjs:rounded-sm', + 'vjs:[&[data-orientation="horizontal"]]:h-5 vjs:[&[data-orientation="horizontal"]]:min-w-20', + 'vjs:[&[data-orientation="vertical"]]:w-5 vjs:[&[data-orientation="vertical"]]:h-18', // Focus state - '-outline-offset-8 focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-white', + 'vjs:-outline-offset-8 vjs:focus-visible:outline-2 vjs:focus-visible:outline-offset-8 vjs:focus-visible:outline-white', ), SliderTrack: cn( - 'relative select-none rounded-full bg-white/10 transition-[height,width] ease-in-out', - '[&[data-orientation="horizontal"]]:w-full [&[data-orientation="horizontal"]]:h-[0.1875rem]', - '[&[data-orientation="vertical"]]:w-[0.1875rem]', + 'vjs:relative vjs:select-none vjs:rounded-full vjs:bg-white/10 vjs:transition-[height,width] vjs:ease-in-out', + 'vjs:[&[data-orientation="horizontal"]]:w-full vjs:[&[data-orientation="horizontal"]]:h-[0.1875rem]', + 'vjs:[&[data-orientation="vertical"]]:w-[0.1875rem]', ), - SliderProgress: cn('bg-white rounded-[inherit]'), - SliderPointer: cn('hidden'), + SliderProgress: cn('vjs:bg-white vjs:rounded-[inherit]'), + SliderPointer: cn('vjs:hidden'), SliderThumb: cn( - 'opacity-0 scale-70 group-hover/slider:opacity-100 group-hover/slider:scale-100 focus-visible:opacity-100 focus-visible:scale-100', - 'bg-white z-10 size-3 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15 transition-[opacity,scale] ease-out', - '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500', - '[&[data-orientation="horizontal"]]:hover:cursor-ew-resize', - '[&[data-orientation="vertical"]]:hover:cursor-ns-resize', + 'vjs:opacity-0 vjs:scale-70 vjs:group-hover/slider:opacity-100 vjs:group-hover/slider:scale-100 vjs:focus-visible:opacity-100 vjs:focus-visible:scale-100', + 'vjs:bg-white vjs:z-10 vjs:size-3 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15 vjs:transition-[opacity,scale] vjs:ease-out', + 'vjs:-outline-offset-2 vjs:focus-visible:outline-2 vjs:focus-visible:outline-offset-2 vjs:focus-visible:outline-amber-500', + 'vjs:[&[data-orientation="horizontal"]]:hover:cursor-ew-resize', + 'vjs:[&[data-orientation="vertical"]]:hover:cursor-ns-resize', ), PopoverPopup: cn( - 'py-2', + 'vjs:py-2', // Animation - 'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0', + 'vjs:transition-[transform,scale,opacity] vjs:data-[ending-style]:scale-90 vjs:data-[ending-style]:opacity-0 vjs:data-[instant]:duration-0 vjs:data-[starting-style]:scale-90 vjs:data-[starting-style]:opacity-0', ), }; diff --git a/packages/react/react/src/skins/preflight.css b/packages/react/react/src/skins/preflight.css new file mode 100644 index 00000000..62515aec --- /dev/null +++ b/packages/react/react/src/skins/preflight.css @@ -0,0 +1 @@ +@import 'tailwindcss/preflight.css' layer(base) prefix(vjs); diff --git a/packages/react/react/tsdown.config.ts b/packages/react/react/tsdown.config.ts index 4f1ce74a..6373df2e 100644 --- a/packages/react/react/tsdown.config.ts +++ b/packages/react/react/tsdown.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'tsdown'; +import buildStyles from './build/build-styles.ts'; export default defineConfig({ entry: 'src/index.ts', @@ -12,4 +13,9 @@ export default defineConfig({ dts: { oxc: true, }, + hooks: { + 'build:done': async () => { + await buildStyles(); + }, + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24743386..e8af218d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,12 +292,24 @@ importers: specifier: workspace:* version: link:../react-media-store devDependencies: + '@tailwindcss/cli': + specifier: ^4.1.0 + version: 4.1.14 '@types/react': specifier: ^18.0.0 version: 18.3.24 + postcss: + specifier: ^8.4.49 + version: 8.5.6 + postcss-prefix-selector: + specifier: ^1.16.1 + version: 1.16.1(postcss@8.5.6) react: specifier: ^18.0.0 version: 18.3.1 + tailwindcss: + specifier: ^4.1.13 + version: 4.1.14 tsdown: specifier: ^0.15.9 version: 0.15.9(typescript@5.9.2) @@ -458,9 +470,6 @@ importers: specifier: ^5.0.0 version: 5.0.0 devDependencies: - '@tailwindcss/cli': - specifier: ^4.1.14 - version: 4.1.14 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -491,18 +500,9 @@ importers: jsdom: specifier: ^27.0.0 version: 27.0.0(postcss@8.5.6) - postcss: - specifier: ^8.4.49 - version: 8.5.6 - postcss-nested: - specifier: ^7.0.2 - version: 7.0.2(postcss@8.5.6) - postcss-prefix-selector: - specifier: ^1.16.1 - version: 1.16.1(postcss@8.5.6) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0)(lightningcss@1.30.1)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(yaml@2.8.1) packages: @@ -4395,12 +4395,6 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-nested@7.0.2: - resolution: {integrity: sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==} - engines: {node: '>=18.0'} - peerDependencies: - postcss: ^8.2.14 - postcss-prefix-selector@1.16.1: resolution: {integrity: sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==} peerDependencies: @@ -6456,7 +6450,7 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0 nopt: 8.1.0 - semver: 7.7.3 + semver: 7.7.2 tar: 7.5.1 transitivePeerDependencies: - encoding @@ -7159,7 +7153,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.2 ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -7261,7 +7255,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0)(lightningcss@1.30.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -7272,7 +7266,7 @@ snapshots: eslint: 9.37.0(jiti@2.6.1) optionalDependencies: typescript: 5.9.2 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0)(lightningcss@1.30.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -7321,7 +7315,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0)(lightningcss@1.30.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -7497,7 +7491,7 @@ snapshots: espree: 10.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 - semver: 7.7.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -8166,7 +8160,7 @@ snapshots: eslint-compat-utils@0.6.5(eslint@9.37.0(jiti@2.6.1)): dependencies: eslint: 9.37.0(jiti@2.6.1) - semver: 7.7.3 + semver: 7.7.2 eslint-config-flat-gitignore@2.1.0(eslint@9.37.0(jiti@2.6.1)): dependencies: @@ -8255,7 +8249,7 @@ snapshots: esquery: 1.6.0 object-deep-merge: 1.0.5 parse-imports-exports: 0.2.4 - semver: 7.7.3 + semver: 7.7.2 spdx-expression-parse: 4.0.0 transitivePeerDependencies: - supports-color @@ -8303,7 +8297,7 @@ snapshots: globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.7.3 + semver: 7.7.2 ts-declaration-location: 1.0.7(typescript@5.9.2) transitivePeerDependencies: - typescript @@ -8506,7 +8500,7 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.12.0 - semver: 7.7.3 + semver: 7.7.2 strip-indent: 4.1.0 eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.37.0(jiti@2.6.1)): @@ -8522,7 +8516,7 @@ snapshots: natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.2 - semver: 7.7.3 + semver: 7.7.2 vue-eslint-parser: 10.2.0(eslint@9.37.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: @@ -9325,7 +9319,7 @@ snapshots: acorn: 8.15.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - semver: 7.7.3 + semver: 7.7.2 jsx-ast-utils@3.3.5: dependencies: @@ -9483,7 +9477,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.2 markdown-extensions@2.0.0: {} @@ -10219,11 +10213,6 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@7.0.2(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 - postcss-prefix-selector@1.16.1(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -11305,7 +11294,7 @@ snapshots: optionalDependencies: vite: 6.3.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0)(lightningcss@1.30.1)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -11357,7 +11346,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 esquery: 1.6.0 - semver: 7.7.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color diff --git a/website/package.json b/website/package.json index 6bc8ede1..aa145488 100644 --- a/website/package.json +++ b/website/package.json @@ -3,9 +3,8 @@ "type": "module", "version": "0.0.1", "scripts": { - "compile-vjs-styles": "node scripts/compile-vjs-styles.js", - "dev": "npm run compile-vjs-styles && astro dev", - "build": "npm run compile-vjs-styles && astro build", + "dev": " astro dev", + "build": "astro build", "preview": "astro preview", "astro": "astro", "test": "vitest run", @@ -43,7 +42,6 @@ "unist-util-visit": "^5.0.0" }, "devDependencies": { - "@tailwindcss/cli": "^4.1.14", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -54,9 +52,6 @@ "@vitest/ui": "^3.2.4", "babel-plugin-react-compiler": "1.0.0", "jsdom": "^27.0.0", - "postcss": "^8.4.49", - "postcss-nested": "^7.0.2", - "postcss-prefix-selector": "^1.16.1", "vitest": "^3.2.4" } } diff --git a/website/scripts/compile-vjs-styles.js b/website/scripts/compile-vjs-styles.js deleted file mode 100644 index 9c6e6b8a..00000000 --- a/website/scripts/compile-vjs-styles.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -import { execSync } from 'node:child_process'; -import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; -import postcss from 'postcss'; -import nested from 'postcss-nested'; -import prefixSelector from 'postcss-prefix-selector'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const rootDir = join(__dirname, '..'); - -const inputFile = 'src/styles/vjs-input.css'; -const tempFile = 'src/styles/vjs-temp.css'; -const outputFile = 'src/styles/vjs.css'; - -console.log('🎨 Compiling VJS styles...'); - -try { - // Step 1: Run Tailwind CLI - console.log(' → Running Tailwind CSS compiler...'); - execSync( - `npx @tailwindcss/cli -i ${inputFile} -o ${tempFile}`, - { cwd: rootDir, stdio: 'inherit' }, - ); - - // Step 2: Read compiled CSS - const tempPath = join(rootDir, tempFile); - const css = readFileSync(tempPath, 'utf-8'); - - // Step 3: Post-process with PostCSS to scope selectors - console.log(' → Scoping selectors to .vjs...'); - const result = await postcss([ - nested(), - prefixSelector({ - prefix: '.vjs', - transform(prefix, selector, prefixedSelector) { - // this is supposed to happen automatically, but isn't... idk - if (selector === 'body' || selector === 'html' || selector === ':root') { - return prefix; - } else if (selector === ':host') { - return selector; - } else { - return prefixedSelector; - } - }, - }), - ]).process(css, { from: tempFile, to: outputFile }); - - // Step 3.5: For reasons I don't fully understand, we're getting duplicate .vjs classes - // e.g., .vjs .vjs or .vjs .vjs .vjs - // let's just regex them to oblivion - result.css = result.css.replace(/(\.vjs\s+)+/g, '.vjs '); - - // Step 4: Write output - const outputPath = join(rootDir, outputFile); - writeFileSync(outputPath, result.css, 'utf-8'); - - // Step 5: Clean up temp file - if (existsSync(tempPath)) { - unlinkSync(tempPath); - } - - console.log(`✅ VJS styles compiled to ${outputFile}`); -} catch (error) { - console.error('❌ Failed to compile VJS styles:', error.message); - process.exit(1); -} diff --git a/website/src/components/home/HeroVideo.tsx b/website/src/components/home/HeroVideo.tsx index 0b9de297..e988a387 100644 --- a/website/src/components/home/HeroVideo.tsx +++ b/website/src/components/home/HeroVideo.tsx @@ -2,7 +2,8 @@ import { useStore } from '@nanostores/react'; import { FrostedSkin, MediaProvider, MinimalSkin, Video } from '@vjs-10/react'; import { skin } from '@/stores/homePageDemos'; import { PLAYBACK_ID } from './config'; -import '@/styles/vjs.css'; +import '@vjs-10/react/skins/frosted.css'; +import '@vjs-10/react/skins/minimal.css'; export default function HeroVideo({ className, poster }: { className?: string; poster: string }) { // Subscribe to skin store for future skin switching functionality diff --git a/website/src/styles/vjs-input.css b/website/src/styles/vjs-input.css deleted file mode 100644 index 45e9012f..00000000 --- a/website/src/styles/vjs-input.css +++ /dev/null @@ -1,11 +0,0 @@ -@import 'tailwindcss'; - -@source "../../../packages/react"; - -/* theme inline tells tailwind to reference the value of these properties */ -@theme inline { - --font-*: initial; - /* --font-instrument-sans and --font-ibm-plex-mono are defined in Base.astro */ - --font-sans: var(--font-instrument-sans); - --font-mono: var(--font-ibm-plex-mono); -}