@page-speed/skins
v0.1.2
Published
Lightweight JSON-based skin system for platform-wide component styling. CDN-accessible configuration files for video players, carousels, and more.
Downloads
456
Readme
@page-speed/skins
Lightweight JSON-based skin system for platform-wide component styling. CDN-accessible configuration files for video players, carousels, and more.
Features
- JSON-First: Pure declarative skins (no runtime JavaScript overhead)
- CDN-Accessible: Load skins directly from jsDelivr
- Tailwind-Friendly: Uses Tailwind classes and CSS variables
- Component Agnostic: Supports video players, carousels, and any component
- Type-Safe: Full TypeScript support
- Tiny Runtime: Minimal JavaScript footprint
- Extensible: Easy to add new component types and skins
- Cacheable: Built-in memory caching for loaded skins
Installation
npm install @page-speed/skins
# or
pnpm add @page-speed/skins
# or
yarn add @page-speed/skinsQuick Start
Load a Skin from CDN
import { loadSkinFromJsDelivr } from '@page-speed/skins';
// Load the base video skin
const skin = await loadSkinFromJsDelivr('0.1.0', 'skins/video/base.json');
console.log(skin.name); // "Base Video Skin"
console.log(skin.tokens); // CSS variables
console.log(skin.classes); // Tailwind classesApply Skin to an Element
import { applySkinToElement } from '@page-speed/skins';
const videoContainer = document.getElementById('video-container');
applySkinToElement(videoContainer, skin);
// Now the element has:
// - CSS variables set via style.setProperty
// - data-skin-id, data-skin-version attributesUse with React Video Component
import { Video } from '@page-speed/video';
import { getVideoSkinAttributes } from '@page-speed/skins';
const skin = await loadSkinFromJsDelivr('0.1.0', 'skins/video/linear-inspired.json');
const attrs = getVideoSkinAttributes(skin);
<Video
src="video.mp4"
{...attrs}
/>Available Skins
Video Player Skins
| Skin | File | Description |
|------|------|-------------|
| Base | skins/video/base.json | Minimal base skin with essential styling |
| Linear-Inspired | skins/video/linear-inspired.json | Clean, modern design inspired by Linear |
| Minimal Hover | skins/video/minimal-hover.json | Controls appear on hover |
| Social Card | skins/video/social-card.json | Optimized for social media embeds |
| YouTube Classic | skins/video/youtube-classic.json | Familiar YouTube-style design |
CDN URLs (jsDelivr)
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/base.json
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/linear-inspired.json
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/minimal-hover.json
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/social-card.json
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/youtube-classic.jsonAPI Reference
Core Types
SkinDefinition
interface SkinDefinition {
id: string; // Unique identifier
name: string; // Human-readable name
version: string; // Semantic version
targets: string[]; // Component types: ["video"]
tokens: SkinTokens; // CSS variables
classes: SkinClasses; // Tailwind/CSS classes
assets?: SkinAssets; // Optional icons/images
description?: string; // Optional description
metadata?: object; // Optional metadata
}Runtime Functions
applySkinToElement(element, skin)
Applies a complete skin to an HTML element.
const container = document.getElementById('my-container');
applySkinToElement(container, skin);resolveClasses(skin, slot)
Gets classes for a specific slot.
const playButtonClasses = resolveClasses(skin, 'playButton');
// "flex items-center justify-center w-10 h-10 ..."getSkinStyleObject(skin)
Gets CSS variables as a style object (useful for React).
const styleObj = getSkinStyleObject(skin);
// { "--video-bg": "#000", "--video-accent-color": "#3b82f6", ... }
<div style={styleObj}>...</div>Video Adapter
getVideoSkinAttributes(skin)
Gets all attributes for a <video> element.
const attrs = getVideoSkinAttributes(skin);
// {
// className: "w-full h-full object-contain",
// style: { "--video-bg": "#000", ... },
// "data-skin-id": "video-base",
// ...
// }
<video {...attrs} src="video.mp4" />resolveVideoClasses(skin)
Gets all video-specific classes.
const classes = resolveVideoClasses(skin);
// {
// container: "relative w-full ...",
// video: "w-full h-full ...",
// controlsBar: "absolute bottom-0 ...",
// playButton: "flex items-center ...",
// ...
// }Utilities
loadSkin(options)
Loads a skin from any URL with caching.
const skin = await loadSkin({
url: 'https://example.com/skin.json',
cache: true,
timeout: 10000,
debug: false,
});loadSkinFromJsDelivr(version, path)
Convenience wrapper for jsDelivr CDN.
const skin = await loadSkinFromJsDelivr(
'0.1.0',
'skins/video/base.json'
);preloadSkins(urls)
Preloads multiple skins in parallel.
await preloadSkins([
'https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/base.json',
'https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/linear-inspired.json',
]);Usage Patterns
1. Direct CDN Load (Vanilla JS)
fetch('https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/base.json')
.then(r => r.json())
.then(skin => {
const video = document.querySelector('video');
Object.entries(skin.tokens).forEach(([key, value]) => {
video.style.setProperty(`--${key}`, value);
});
});2. React Component
import { useEffect, useState } from 'react';
import { loadSkinFromJsDelivr, getVideoSkinAttributes } from '@page-speed/skins';
import { Video } from '@page-speed/video';
function VideoWithSkin() {
const [skin, setSkin] = useState(null);
useEffect(() => {
loadSkinFromJsDelivr('0.1.0', 'skins/video/linear-inspired.json')
.then(setSkin);
}, []);
if (!skin) return <div>Loading skin...</div>;
const attrs = getVideoSkinAttributes(skin);
return (
<Video
src="video.mp4"
{...attrs}
/>
);
}3. Custom Controls Wrapper
import { resolveVideoClasses, getSkinStyleObject } from '@page-speed/skins';
function VideoPlayer({ skin, src }) {
const classes = resolveVideoClasses(skin);
const style = getSkinStyleObject(skin);
return (
<div className={classes.container} style={style}>
<video className={classes.video} src={src} />
<div className={classes.controlsBar}>
<button className={classes.playButton}>Play</button>
<div className={classes.timeline}>
<div className={classes.timelineProgress} />
</div>
<span className={classes.timeText}>0:00 / 5:23</span>
</div>
</div>
);
}4. Dynamic Skin Switching
const [currentSkin, setCurrentSkin] = useState('base');
const skinUrls = {
base: 'skins/video/base.json',
linear: 'skins/video/linear-inspired.json',
minimal: 'skins/video/minimal-hover.json',
};
const skin = await loadSkinFromJsDelivr('0.1.0', skinUrls[currentSkin]);Creating Custom Skins
Skin JSON Structure
{
"id": "my-custom-skin",
"name": "My Custom Skin",
"version": "1.0.0",
"targets": ["video"],
"tokens": {
"--video-bg": "#000000",
"--video-accent-color": "#3b82f6"
},
"classes": {
"container": "relative w-full bg-black",
"video": "w-full h-full",
"playButton": "flex items-center justify-center w-10 h-10"
},
"assets": {
"playIcon": "data:image/svg+xml,..."
}
}Video Skin Slots
Standard slots for video players:
container- Wrapper elementvideo- Video element itselfcontrolsBar- Controls containerplayButton- Play/pause buttontimeline- Progress bartimelineProgress- Progress indicatortimelineBuffered- Buffered indicatortimeText- Time displayvolumeControl- Volume controlfullscreenButton- Fullscreen buttonsettingsButton- Settings buttonloadingSpinner- Loading indicatorplayOverlay- Large play button overlay
CDN Delivery
jsDelivr (Recommended)
https://cdn.jsdelivr.net/npm/@page-speed/skins@{version}/{path}Examples:
https://cdn.jsdelivr.net/npm/@page-speed/[email protected]/skins/video/base.json
https://cdn.jsdelivr.net/npm/@page-speed/skins@latest/skins/video/linear-inspired.jsonunpkg (Alternative)
https://unpkg.com/@page-speed/skins@{version}/{path}Direct Import (ES Modules)
import baseSkin from '@page-speed/skins/skins/video/base.json';Performance
Bundle Size
- Runtime Code: ~2 KB gzipped
- Each Skin JSON: ~1-3 KB
- Total Impact: Minimal (JSON is cheap)
Caching Strategy
- Memory Cache: Loaded skins cached in JavaScript
- CDN Cache: jsDelivr caches files at edge
- Browser Cache: Standard HTTP caching headers
Best Practices
- ✅ Preload skins during app initialization
- ✅ Use memory caching for frequently used skins
- ✅ Load from CDN (don't bundle all skins)
- ✅ Consider lazy loading for theme switchers
Browser Support
- Modern Browsers: Full support
- ES Modules: Required
- Fetch API: Required
- CSS Variables: Required
- Fallback: Provide default styles for old browsers
TypeScript
Full TypeScript support with strict types:
import type { SkinDefinition, VideoSkinSlots } from '@page-speed/skins';
const skin: SkinDefinition = {
id: 'my-skin',
name: 'My Skin',
version: '1.0.0',
targets: ['video'],
tokens: {},
classes: {},
};Future Component Types
The system is designed to support any component type:
{
"targets": ["carousel"],
"classes": {
"container": "...",
"slide": "...",
"navButton": "...",
"indicator": "..."
}
}Contributing
- Add new skin JSON to
skins/{component-type}/{skin-name}.json - Run validation:
pnpm run validate:skins - Add export to
package.jsonexports map - Update documentation
- Submit PR
License
BSD-3-Clause
Author
OpenSite AI (https://opensite.ai)
