mirror of
https://github.com/zoriya/v10.git
synced 2026-08-12 08:59:02 +00:00
feat(website): init dark mode
This commit is contained in:
@@ -26,7 +26,9 @@ export default antfu(
|
||||
'**/.vercel/',
|
||||
'**/dist/',
|
||||
'**/styles/vjs.css',
|
||||
// some files that make eslint/prettier panic
|
||||
'website/src/components/Posthog.astro',
|
||||
'website/src/components/ThemeInit.astro',
|
||||
],
|
||||
plugins: {
|
||||
'jsx-a11y': jsxA11y,
|
||||
|
||||
@@ -8,5 +8,4 @@ not comprehensive. just the things I'm worried about forgetting
|
||||
- animations
|
||||
- docs features
|
||||
- copy code blocks
|
||||
- dark mode
|
||||
- search
|
||||
|
||||
@@ -36,6 +36,7 @@ export default defineConfig({
|
||||
shikiConfig: {
|
||||
themes: {
|
||||
light: 'gruvbox-light-hard',
|
||||
dark: 'gruvbox-dark-medium',
|
||||
},
|
||||
// TODO shiki transformers
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
||||
import { createHighlighter, hastToHtml } from 'shiki';
|
||||
import html from 'shiki/langs/html.mjs';
|
||||
import tsx from 'shiki/langs/tsx.mjs';
|
||||
import gruvboxDarkSoft from 'shiki/themes/gruvbox-dark-soft.mjs';
|
||||
import gruvboxLightHard from 'shiki/themes/gruvbox-light-hard.mjs';
|
||||
|
||||
// If you try importing more than one island with ClientCode in it,
|
||||
@@ -12,7 +13,7 @@ import gruvboxLightHard from 'shiki/themes/gruvbox-light-hard.mjs';
|
||||
|
||||
// eslint-disable-next-line antfu/no-top-level-await
|
||||
const highlighter: Highlighter = await createHighlighter({
|
||||
themes: [gruvboxLightHard],
|
||||
themes: [gruvboxLightHard, gruvboxDarkSoft],
|
||||
langs: [html, tsx],
|
||||
});
|
||||
|
||||
@@ -25,7 +26,10 @@ export interface ClientCodeProps {
|
||||
export default function ClientCode({ code, lang, className }: ClientCodeProps) {
|
||||
const hast = highlighter.codeToHast(code, {
|
||||
lang,
|
||||
theme: 'gruvbox-light-hard',
|
||||
themes: {
|
||||
light: 'gruvbox-light-hard',
|
||||
dark: 'gruvbox-dark-soft',
|
||||
},
|
||||
});
|
||||
|
||||
// shiki gives us a root > pre > code > text structure
|
||||
@@ -51,7 +55,7 @@ export default function ClientCode({ code, lang, className }: ClientCodeProps) {
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={clsx('rounded-lg p-6 overflow-x-auto overflow-y-scroll max-h-96 border border-light-40 bg-light-100', preClassName, className)}
|
||||
className={clsx('rounded-lg p-6 overflow-x-auto overflow-y-scroll max-h-96 border border-light-40 dark:border-dark-80 bg-light-100 dark:bg-dark-110', preClassName, className)}
|
||||
>
|
||||
<code
|
||||
className={clsx('font-mono text-code', codeClassName)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DISCORD_INVITE_URL, GITHUB_REPO_URL } from '@/consts';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import GitHub from './icons/GitHub';
|
||||
import Discord from './icons/Discord';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
type NavLink = {
|
||||
href: string;
|
||||
@@ -22,40 +23,49 @@ type Props = {
|
||||
const { class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class:list={['border-t border-light-40 py-6 px-3 grid gap-3', className]}>
|
||||
<nav class="flex flex-col sm:flex-row">
|
||||
<div class="flex justify-center sm:justify-start">
|
||||
{
|
||||
navLinks.map((link) => (
|
||||
<a href={link.href} class="intent:underline px-3 py-2 inline-flex gap-1 items-center">
|
||||
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-center sm:justify-start">
|
||||
<a
|
||||
href={GITHUB_REPO_URL}
|
||||
class="group px-3 py-2 inline-flex items-center"
|
||||
aria-label="GitHub"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHub size="1em" className="text-dark-80 group-intent:text-dark-100" />
|
||||
</a>
|
||||
<a
|
||||
href={DISCORD_INVITE_URL}
|
||||
class="group px-3 py-2 inline-flex items-center"
|
||||
aria-label="Discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Discord size="1em" className="text-dark-80 group-intent:text-dark-100" />
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<footer class:list={['border-t border-light-40 dark:border-dark-80 py-6 px-3 grid gap-3', className]}>
|
||||
<div class="flex items-center justify-between flex-col sm:flex-row gap-3 mb-2 sm:mb-0">
|
||||
<nav class="flex flex-col sm:flex-row gap-1">
|
||||
<div class="flex justify-center sm:justify-start">
|
||||
{
|
||||
navLinks.map((link) => (
|
||||
<a href={link.href} class="intent:underline px-3 py-2 inline-flex gap-1 items-center">
|
||||
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-center sm:justify-start">
|
||||
<a
|
||||
href={GITHUB_REPO_URL}
|
||||
class="group px-3 py-2 inline-flex items-center"
|
||||
aria-label="GitHub"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHub
|
||||
size="1em"
|
||||
className="text-dark-80 dark:text-light-80 group-intent:text-dark-100 dark:group-intent:text-light-40"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={DISCORD_INVITE_URL}
|
||||
class="group px-3 py-2 inline-flex items-center"
|
||||
aria-label="Discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Discord
|
||||
size="1em"
|
||||
className="text-dark-80 dark:text-light-80 group-intent:text-dark-100 dark:group-intent:text-light-40"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<ThemeToggle client:idle />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-center sm:text-left text-dark-40 px-3">
|
||||
<p class="text-sm text-center sm:text-left text-dark-40 dark:text-light-40 px-3">
|
||||
© 2010–{new Date().getFullYear()}, Video.js contributors
|
||||
<br />
|
||||
Video.js is a free and open source HTML5 video player framework
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="absolute h-1/2 bottom-0 left-0 right-0 bg-red"></div>
|
||||
<div class="absolute h-1/4 bottom-0 left-0 right-0 bg-purple"></div>
|
||||
<div
|
||||
class="relative font-semibold tracking-tighter text-light-80 select-none"
|
||||
class="relative font-semibold tracking-tighter text-light-80 dark:text-dark-100 select-none"
|
||||
style="font-size:28cqi;line-height:0.72;"
|
||||
>
|
||||
VideoJS
|
||||
|
||||
@@ -28,7 +28,9 @@ const { class: className, dark = false } = Astro.props;
|
||||
<nav
|
||||
class:list={[
|
||||
'pl-3 flex justify-between sticky top-0 z-40 border-b',
|
||||
dark ? 'bg-dark-100 text-light-80 border-dark-100' : 'bg-light-80 border-light-40',
|
||||
dark
|
||||
? 'bg-dark-100 text-light-80 border-dark-100 dark:border-dark-80'
|
||||
: 'bg-light-80 border-light-40 dark:bg-dark-100 dark:border-dark-80',
|
||||
className,
|
||||
]}
|
||||
style="height: var(--nav-h);"
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Select<T extends string = string>({
|
||||
<BaseSelect.Trigger
|
||||
className={twMerge(
|
||||
clsx(
|
||||
'inline-flex items-center gap-2 bg-light-60 border border-light-40 rounded-lg text-sm p-2',
|
||||
'inline-flex items-center gap-2 bg-light-60 dark:bg-dark-90 dark:text-light-100 border border-light-40 dark:border-dark-80 rounded-lg text-sm p-2',
|
||||
),
|
||||
className,
|
||||
)}
|
||||
@@ -55,7 +55,7 @@ export function Select<T extends string = string>({
|
||||
>
|
||||
<BaseSelect.Popup
|
||||
className={clsx(
|
||||
'border border-light-40 rounded-lg bg-light-60 shadow-xl text-sm',
|
||||
'border border-light-40 dark:border-dark-80 rounded-lg bg-light-60 dark:bg-dark-80 shadow-xl text-sm',
|
||||
'overflow-hidden',
|
||||
)}
|
||||
style={{
|
||||
@@ -77,9 +77,9 @@ export function Select<T extends string = string>({
|
||||
disabled={option.disabled}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 p-2',
|
||||
'cursor-pointer intent:bg-light-80',
|
||||
'cursor-pointer intent:bg-light-80 dark:intent:bg-dark-100',
|
||||
option.disabled && 'opacity-50 cursor-not-allowed',
|
||||
option.value === value && 'bg-light-80',
|
||||
option.value === value && 'bg-light-80 dark:bg-dark-100',
|
||||
)}
|
||||
>
|
||||
<BaseSelect.ItemText>{option.label}</BaseSelect.ItemText>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import { THEME_KEY } from '@/consts';
|
||||
---
|
||||
|
||||
{/* Dark mode initialization script - runs before paint to prevent FOUC */}
|
||||
<script is:inline define:vars={{ localStorageKey: THEME_KEY }}>
|
||||
if (
|
||||
typeof localStorage !== 'undefined' &&
|
||||
typeof document !== 'undefined' &&
|
||||
typeof document.documentElement !== 'undefined'
|
||||
) {
|
||||
let localStorageValue = localStorage[localStorageKey];
|
||||
const isValidKey = localStorageValue === 'light' || localStorageValue === 'dark' || localStorageValue === 'system';
|
||||
if (!localStorageValue || !isValidKey) {
|
||||
localStorage[localStorageKey] = 'system';
|
||||
localStorageValue = 'system';
|
||||
}
|
||||
if (localStorageValue === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (localStorageValue === 'system') {
|
||||
if (typeof window.matchMedia !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Toggle } from '@base-ui-components/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
||||
import clsx from 'clsx';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { THEME_KEY } from '@/consts';
|
||||
|
||||
type Preference = 'system' | 'light' | 'dark';
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'system' as const, label: 'System', icon: Monitor },
|
||||
{ value: 'light' as const, label: 'Light', icon: Sun },
|
||||
{ value: 'dark' as const, label: 'Dark', icon: Moon },
|
||||
];
|
||||
|
||||
function initPreference(): Preference {
|
||||
if (typeof localStorage === 'undefined') return 'system';
|
||||
if (localStorage[THEME_KEY] === 'light') return 'light';
|
||||
if (localStorage[THEME_KEY] === 'dark') return 'dark';
|
||||
if (localStorage[THEME_KEY] === 'system') return 'system';
|
||||
// Shouldn't be possible after head script runs, but handle it
|
||||
localStorage[THEME_KEY] = 'system';
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function getThemeFromPreference(preference: Preference): Theme {
|
||||
if (preference === 'light') return 'light';
|
||||
if (preference === 'dark') return 'dark';
|
||||
if (preference === 'system') {
|
||||
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [preference, _setPreference] = useState<Preference | null>(null);
|
||||
const [theme, setTheme] = useState<Theme | null>(null);
|
||||
|
||||
const setPreference = (newPreference: Preference) => {
|
||||
_setPreference(newPreference);
|
||||
if (typeof localStorage !== 'undefined') localStorage[THEME_KEY] = newPreference;
|
||||
setTheme(getThemeFromPreference(newPreference));
|
||||
};
|
||||
|
||||
const handleValueChange = (values: Preference[]) => {
|
||||
if (values.length > 0) setPreference(values[0]);
|
||||
};
|
||||
|
||||
// Initialize preference and theme on mount
|
||||
useEffect(() => {
|
||||
const initialPreference = initPreference();
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
_setPreference(initialPreference);
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setTheme(getThemeFromPreference(initialPreference));
|
||||
}, []);
|
||||
|
||||
// Listen to media query changes when preference is 'system'
|
||||
useEffect(() => {
|
||||
if (preference !== 'system') return;
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') return;
|
||||
|
||||
const onMediaChange = (e: MediaQueryListEvent) => setTheme(e.matches ? 'dark' : 'light');
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', onMediaChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', onMediaChange);
|
||||
};
|
||||
}, [preference]);
|
||||
|
||||
// Keep document.documentElement in sync with theme
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (theme === 'light') document.documentElement.classList.remove('dark');
|
||||
else if (theme === 'dark') document.documentElement.classList.add('dark');
|
||||
|
||||
return () => {
|
||||
document.documentElement.classList.remove('dark');
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ToggleGroup
|
||||
disabled={preference === null}
|
||||
value={[preference]}
|
||||
onValueChange={handleValueChange}
|
||||
multiple={false}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-1 bg-light-60 dark:bg-dark-90 dark:text-light-100 border border-light-40 dark:border-dark-80 rounded-lg p-1',
|
||||
)}
|
||||
>
|
||||
{themeOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<Toggle
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={clsx(
|
||||
'relative',
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded text-sm',
|
||||
preference === null ? 'cursor-wait' : 'cursor-pointer',
|
||||
preference === option.value ? 'bg-light-80 dark:bg-dark-100' : preference !== null ? 'intent:bg-light-80/50 dark:intent:bg-dark-100/50' : '',
|
||||
)}
|
||||
aria-label={option.label}
|
||||
>
|
||||
<Icon size={14} />
|
||||
</Toggle>
|
||||
);
|
||||
})}
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
@@ -49,9 +49,9 @@ export default function ToggleGroup<T extends string = string>({
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
className={clsx(
|
||||
'text-dark-40 data-[pressed]:text-current',
|
||||
'text-dark-40 dark:text-light-40 data-[pressed]:text-current',
|
||||
'py-3 border-b border-transparent',
|
||||
option.disabled ? 'opacity-50 cursor-not-allowed' : 'intent:border-dark-40 data-[pressed]:border-current cursor-pointer',
|
||||
option.disabled ? 'opacity-50 cursor-not-allowed' : 'intent:border-dark-40 dark:intent:border-light-40 data-[pressed]:border-current cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -38,13 +38,13 @@ export function Tooltip({
|
||||
<BaseTooltip.Positioner side={side} sideOffset={sideOffset}>
|
||||
<BaseTooltip.Popup
|
||||
className={clsx(
|
||||
'bg-dark-100 text-light-80 text-sm px-3 py-2 rounded-lg shadow-lg',
|
||||
'bg-dark-100 dark:bg-dark-90 text-light-80 dark:text-light-100 text-sm px-3 py-2 rounded-lg shadow-lg',
|
||||
'z-50 max-w-xs',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<BaseTooltip.Arrow className="fill-dark-100" />
|
||||
<BaseTooltip.Arrow className="fill-dark-100 dark:fill-dark-90" />
|
||||
</BaseTooltip.Popup>
|
||||
</BaseTooltip.Positioner>
|
||||
</BaseTooltip.Portal>
|
||||
|
||||
@@ -67,9 +67,9 @@ export function AuthorSocialLinks({ socialLinks, className }: AuthorSocialLinksP
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${config.label} profile`}
|
||||
className="inline-flex items-center justify-center p-2 rounded-lg intent:text-dark-40 transition-colors"
|
||||
className="inline-flex items-center justify-center p-2 rounded-lg intent:text-dark-40 dark:text-light-40"
|
||||
>
|
||||
<Icon size={20} />
|
||||
<Icon size={20} strokeWidth={1.5} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
@@ -14,16 +14,16 @@ const { post, authors, asLink } = Astro.props;
|
||||
|
||||
<article>
|
||||
<a href={`/blog/${post.id}`} class="group grid max-w-3xl">
|
||||
<time datetime={post.data.pubDate.toISOString()} class="text-dark-40 mb-2">
|
||||
<time datetime={post.data.pubDate.toISOString()} class="text-dark-40 dark:text-light-40 mb-2">
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
</time>
|
||||
<div class="mb-3">
|
||||
<h2 class="text-md font-medium md:text-h4 group-intent:underline mb-1">{post.data.title}</h2>
|
||||
<p class="font-medium md:text-md text-dark-40">{post.data.description}</p>
|
||||
<p class="font-medium md:text-md text-dark-40 dark:text-light-40">{post.data.description}</p>
|
||||
</div>
|
||||
{
|
||||
authors.length > 0 && (
|
||||
<div class="text-dark-40">
|
||||
<div class="text-dark-40 dark:text-light-40">
|
||||
{new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(
|
||||
authors.map((author) => author.data.name),
|
||||
)}
|
||||
|
||||
@@ -22,10 +22,10 @@ function getGuideUrl<F extends SupportedFramework>(framework: F, style: Supporte
|
||||
prev ? (
|
||||
<a
|
||||
href={getGuideUrl(framework, style, prev.slug)}
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 rounded-lg intent:border-dark-40"
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 dark:intent:border-dark-40"
|
||||
style="grid-template-areas: 'empty direction' 'icon title'; grid-template-columns: auto minmax(0,1fr); grid-template-rows: auto minmax(0,1fr);"
|
||||
>
|
||||
<div class="text-sm text-dark-80" style="grid-area: direction;">
|
||||
<div class="text-sm text-dark-80 dark:text-light-40" style="grid-area: direction;">
|
||||
Previous
|
||||
</div>
|
||||
<div class="text-base font-medium text-balance" style="grid-area: title">
|
||||
@@ -43,10 +43,10 @@ function getGuideUrl<F extends SupportedFramework>(framework: F, style: Supporte
|
||||
next ? (
|
||||
<a
|
||||
href={getGuideUrl(framework, style, next.slug)}
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 rounded-lg intent:border-dark-40 text-right"
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 text-right dark:intent:border-dark-40"
|
||||
style="grid-template-areas: 'direction empty' 'title icon';grid-template-columns: minmax(0,1fr) auto; grid-template-rows: auto minmax(0,1fr);"
|
||||
>
|
||||
<div class="text-sm text-dark-80" style="grid-area: direction;">
|
||||
<div class="text-sm text-dark-80 dark:text-light-40" style="grid-area: direction;">
|
||||
Next
|
||||
</div>
|
||||
<div class="text-base font-medium text-balance" style="grid-area: title">
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
|
||||
import { PreferenceUpdater } from '@/components/docs/PreferenceUpdater';
|
||||
import { Selectors } from '@/components/docs/Selectors';
|
||||
import SidebarItem from '@/components/docs/SidebarItem.astro';
|
||||
import { filterSidebar } from '@/utils/docs/sidebar';
|
||||
|
||||
@@ -18,6 +17,6 @@ const filteredSidebar = filterSidebar(framework, style);
|
||||
|
||||
<slot />
|
||||
<PreferenceUpdater client:idle currentFramework={framework} currentStyle={style} />
|
||||
<nav class="p-6 text-dark-80">
|
||||
<nav class="p-6 text-dark-80 dark:text-light-40">
|
||||
{filteredSidebar.map((item) => <SidebarItem item={item} framework={framework} style={style} docTitles={docTitles} />)}
|
||||
</nav>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function Selectors({
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="p-6 lg:py-2.5 xl:p-6 border-b border-light-40">
|
||||
<div className="p-6 lg:py-2.5 xl:p-6 border-b border-light-40 dark:border-dark-80">
|
||||
<div className="max-w-3xl mx-auto w-full grid gap-x-6 gap-y-2 items-center" style={{ gridTemplateColumns: 'auto minmax(0, 1fr)' }}>
|
||||
<span>Framework</span>
|
||||
<Select
|
||||
|
||||
@@ -33,10 +33,10 @@ const isActive = containsActivePath(item);
|
||||
<summary
|
||||
class:list={[
|
||||
'py-2 flex items-center gap-4 ',
|
||||
'intent:text-dark-100 cursor-pointer',
|
||||
'intent:text-dark-100 dark:intent:text-light-80 cursor-pointer',
|
||||
depth === 0 && 'font-medium',
|
||||
depth > 0 && 'border-l',
|
||||
isActive ? 'text-dark-100 border-dark-100 ' : 'border-light-40',
|
||||
isActive ? 'text-dark-100 dark:text-light-80 border-dark-100' : 'border-light-40 dark:border-dark-40',
|
||||
]}
|
||||
>
|
||||
<span class="flex-1">{item.sidebarLabel}</span>
|
||||
@@ -52,10 +52,10 @@ const isActive = containsActivePath(item);
|
||||
<a
|
||||
class:list={[
|
||||
'py-2 flex items-center gap-4 ',
|
||||
'intent:text-dark-100 cursor-pointer',
|
||||
'intent:text-dark-100 dark:intent:text-light-100 cursor-pointer',
|
||||
depth === 0 && 'font-medium',
|
||||
depth > 0 && 'border-l',
|
||||
isActive ? 'text-dark-100 border-dark-100 ' : 'border-light-40',
|
||||
isActive ? 'text-dark-100 dark:text-light-100 border-current ' : 'border-light-40 dark:border-dark-40',
|
||||
]}
|
||||
style={`padding-left: calc(var(--spacing) * ${depth * 4})`}
|
||||
href={`/docs/framework/${framework}/style/${style}/${item.slug}`}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function TableOfContentsDesktop({ headings, activeId, onNavigate, classNa
|
||||
<a
|
||||
href={`#${heading.slug}`}
|
||||
onClick={e => handleClick(e, heading.slug)}
|
||||
className={clsx('text-sm block', activeId === heading.slug ? 'text-dark-100' : 'text-dark-40 intent:text-dark-100',
|
||||
className={clsx('text-sm block', activeId === heading.slug ? 'text-dark-100 dark:text-light-100' : 'text-dark-40 dark:text-light-40 intent:text-dark-100 dark:intent:text-light-100',
|
||||
)}
|
||||
style={{ paddingLeft: `calc(${(heading.depth - 2)} * var(--spacing) * 4)` }}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function TableOfContentsMobile({ headings, activeId, onNavigate, classNam
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('border-b border-light-40 bg-light-80 px-6 lg:px-12 h-(--h) flex items-center', className)}
|
||||
className={clsx('border-b border-light-40 dark:border-dark-80 bg-light-80 dark:bg-dark-100 px-6 lg:px-12 h-(--h) flex items-center', className)}
|
||||
style={{ '--h': 'var(--mobile-toc-h)' } as React.CSSProperties}
|
||||
>
|
||||
<div className="w-full max-w-3xl mx-auto">
|
||||
|
||||
@@ -20,7 +20,13 @@ const isCodeBlock = codeBlock === 'true';
|
||||
<slot />
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag class={twMerge('bg-light-60 border border-light-40 px-1 rounded font-mono text-code', className)} {...props}>
|
||||
<Tag
|
||||
class={twMerge(
|
||||
'bg-light-60 dark:bg-dark-90 dark:text-light-100 border border-light-40 dark:border-dark-80 px-1 rounded font-mono text-code',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
</Tag>
|
||||
)
|
||||
|
||||
@@ -8,13 +8,15 @@ type Props<Tag extends HTMLTag = 'pre'> = Polymorphic<{ as: Tag }> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const { as: Tag = 'pre', maxWidth = true, class: className, style, ...props } = Astro.props;
|
||||
const { as: Tag = 'pre', maxWidth = true, class: className, style: _style, ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={twMerge(clsx('relative my-6 group', maxWidth && 'max-w-3xl mx-auto'))}>
|
||||
<Tag
|
||||
class={twMerge('rounded-lg p-6 overflow-x-auto overflow-y-scroll max-h-96 border border-light-40', className)}
|
||||
style={`${style};background-color:var(--color-light-100);`}
|
||||
class={twMerge(
|
||||
'rounded-lg p-6 overflow-x-auto overflow-y-scroll max-h-96 border border-light-40 dark:border-dark-80 bg-light-100 dark:bg-dark-110',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -2,3 +2,4 @@ export const SITE_TITLE = 'Video.js 10';
|
||||
export const SITE_DESCRIPTION = `For over 15 years, Video.js has been the world's web video player. Now rebuilt in v10 for modern development and performance.`;
|
||||
export const GITHUB_REPO_URL = 'https://github.com/muxinc/vjs-10-monorepo/';
|
||||
export const DISCORD_INVITE_URL = 'https://discord.gg/yyatwmQr';
|
||||
export const THEME_KEY = 'vjs-site-theme';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
import type { ImageMetadata } from 'astro';
|
||||
|
||||
import { Font } from 'astro:assets';
|
||||
|
||||
import { SITE_TITLE } from '@/consts';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
import FooterEasterEgg from '@/components/FooterEasterEgg.astro';
|
||||
import Posthog from '@/components/Posthog.astro';
|
||||
import ThemeInit from '@/components/ThemeInit.astro';
|
||||
|
||||
interface Props {
|
||||
title: string | string[];
|
||||
@@ -69,11 +72,13 @@ const fullTitle = Array.isArray(title)
|
||||
|
||||
{/* Analytics */}
|
||||
<Posthog />
|
||||
|
||||
<ThemeInit />
|
||||
</head>
|
||||
<body
|
||||
class:list={[
|
||||
'relative',
|
||||
'bg-light-80 text-dark-100',
|
||||
'bg-light-80 dark:bg-dark-100 text-dark-100 dark:text-light-80',
|
||||
'font-sans antialiased text-pretty text-base',
|
||||
'min-w-80',
|
||||
'[--nav-h:calc(var(--spacing)*13)] sm:[--nav-h:calc(var(--spacing)*14)]',
|
||||
|
||||
@@ -34,7 +34,7 @@ const docTitles = new Map(allDocs.map((d) => [d.id, d.data.title]));
|
||||
<DocsSidebar slot="mobile-nav" framework={framework} style={style} docTitles={docTitles} />
|
||||
</NavBar>
|
||||
<aside
|
||||
class="hidden lg:block lg:border-r border-light-40 sticky overflow-y-scroll"
|
||||
class="hidden lg:block lg:border-r border-light-40 dark:border-dark-80 sticky overflow-y-scroll"
|
||||
style="top: var(--nav-h); max-height: calc(100vh - var(--nav-h));"
|
||||
>
|
||||
<DocsSidebar framework={framework} style={style} docTitles={docTitles}>
|
||||
|
||||
@@ -48,7 +48,7 @@ const jsonLdSchema = createBlogCollectionSchema({
|
||||
<JsonLd slot="head" schema={jsonLdSchema} />
|
||||
<header class="mb-10 md:mb-20 w-full max-w-3xl mx-auto">
|
||||
<h1 class="text-h4 md:text-h2 mb-2">Video.js Blog</h1>
|
||||
<p class="text-md font-medium md:text-h5 text-dark-40">
|
||||
<p class="text-md font-medium md:text-h5 text-dark-40 dark:text-light-40">
|
||||
{page.data.length === 0 ? 'Coming soon' : 'Articles, announcements, news, updates, and more'}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -39,14 +39,17 @@ const jsonLdSchema = createBlogPostingSchema({
|
||||
<Blog title={[post.data.title, 'Blog']} description={post.data.description}>
|
||||
<JsonLd slot="head" schema={jsonLdSchema} />
|
||||
<article class="@container">
|
||||
<header class="border-b border-light-40 max-w-3xl mx-auto pb-10 mb-10">
|
||||
<time datetime={post.data.pubDate.toISOString()} class="font-medium md:text-md text-dark-40 flex gap-4 mb-1">
|
||||
<header class="border-b border-light-40 dark:border-dark-80 max-w-3xl mx-auto pb-10 mb-10">
|
||||
<time
|
||||
datetime={post.data.pubDate.toISOString()}
|
||||
class="font-medium md:text-md text-dark-40 dark:text-light-40 flex gap-4 mb-1"
|
||||
>
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
<span>•</span>
|
||||
<span>{remarkPluginFrontmatter.minutesRead}</span>
|
||||
</time>
|
||||
<h1 class="text-h4 md:text-h2 mb-2">{post.data.title}</h1>
|
||||
<div class="font-medium md:text-md text-dark-40">
|
||||
<div class="font-medium md:text-md text-dark-40 dark:text-light-40">
|
||||
By {
|
||||
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' })
|
||||
.formatToParts(authors.map((author) => author.data.name))
|
||||
|
||||
@@ -56,12 +56,12 @@ const jsonLdSchema = createProfilePageSchema({
|
||||
<img
|
||||
src={author.data.avatar}
|
||||
alt={author.data.name}
|
||||
class="w-24 h-24 md:w-36 md:h-36 rounded-full border border-light-40 bg-light-100 row-span-3"
|
||||
class="w-24 h-24 md:w-36 md:h-36 rounded-full border border-light-40 dark:border-dark-80 bg-light-100 dark:bg-dark-110 row-span-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<h1 class="text-h2 mb-1">{author.data.name}</h1>
|
||||
{author.data.bio && <p class="text-md text-dark-80 mb-4">{author.data.bio}</p>}
|
||||
{author.data.bio && <p class="text-md text-dark-80 dark:text-light-80 mb-4">{author.data.bio}</p>}
|
||||
{
|
||||
author.data.socialLinks && (
|
||||
<AuthorSocialLinks client:idle socialLinks={author.data.socialLinks} className="flex gap-2" />
|
||||
|
||||
@@ -119,7 +119,7 @@ const jsonLdSchema = createTechArticleSchema({
|
||||
</div>
|
||||
<div class="@container px-6 lg:px-12 flex flex-col" style="grid-area: article;">
|
||||
<article class="mb-18 flex-1">
|
||||
<header class="border-b border-light-40 max-w-3xl mx-auto py-8 mb-8">
|
||||
<header class="border-b border-light-40 dark:border-dark-80 max-w-3xl mx-auto py-8 mb-8">
|
||||
<H3 as="h1" class="mt-0 mb-2">{doc.data.title}</H3>
|
||||
<p class="@lg:font-medium">{doc.data.description}</p>
|
||||
</header>
|
||||
|
||||
@@ -30,7 +30,11 @@ const poster = `https://image.mux.com/${PLAYBACK_ID}/thumbnail.webp?time=0`;
|
||||
</p>
|
||||
</div>
|
||||
<div class="vjs relative z-10 w-full -mb-11 md:-mb-21 lg:-mb-32 grid grid-cols-1 grid-rows-1">
|
||||
<HeroVideo client:load className="aspect-video rounded-3xl bg-dark-80 shadow-2xl" poster={poster} />
|
||||
<HeroVideo
|
||||
client:load
|
||||
className="aspect-video rounded-3xl bg-dark-80 dark:bg-dark-40 shadow-2xl"
|
||||
poster={poster}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-28 md:h-67 grid grid-rows-4">
|
||||
@@ -57,7 +61,7 @@ const poster = `https://image.mux.com/${PLAYBACK_ID}/thumbnail.webp?time=0`;
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 p-6">
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 dark:border-dark-80 p-6">
|
||||
<CircleCheck size={24} className="flex-shrink-0" />
|
||||
<div>
|
||||
<h3 class="font-semibold text-md">Preview</h3>
|
||||
@@ -65,17 +69,17 @@ const poster = `https://image.mux.com/${PLAYBACK_ID}/thumbnail.webp?time=0`;
|
||||
<p class="max-w-3xl">First public look, with limited features. Not production ready.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 p-6">
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 dark:border-dark-80 p-6">
|
||||
<div>
|
||||
<Clock size={24} className="flex-shrink-0" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-md">Beta</h3>
|
||||
<p class="font-medium mb-3">December 2025</p>
|
||||
<p class="font-medium mb-3">February 2025</p>
|
||||
<p class="max-w-3xl">Core features, like basic controls and adaptive streaming. Bugs likely.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 p-6">
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 dark:border-dark-80 p-6">
|
||||
<div>
|
||||
<Clock size={24} className="flex-shrink-0" />
|
||||
</div>
|
||||
@@ -85,7 +89,7 @@ const poster = `https://image.mux.com/${PLAYBACK_ID}/thumbnail.webp?time=0`;
|
||||
<p class="max-w-3xl">Stable release. Fundamental v8 feature parity.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 p-6">
|
||||
<section class="flex gap-3 rounded-xl border border-light-40 dark:border-dark-80 p-6">
|
||||
<div>
|
||||
<Clock size={24} className="flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* before we begin with tailwind, let's go over some global rules */
|
||||
/* dark mode should use dark chrome */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
* {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
/* nested lists should have lower-alpha style */
|
||||
.list-decimal .list-decimal {
|
||||
list-style-type: lower-alpha;
|
||||
@@ -37,6 +31,16 @@
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
.dark * {
|
||||
color-scheme: dark;
|
||||
}
|
||||
.dark .shiki,
|
||||
.dark .shiki span,
|
||||
.dark .astro-code,
|
||||
.dark .astro-code span {
|
||||
color: var(--shiki-dark) !important;
|
||||
}
|
||||
|
||||
/* theme inline tells tailwind to reference the value of these properties */
|
||||
@theme inline {
|
||||
@@ -52,7 +56,9 @@
|
||||
--color-white: white;
|
||||
--color-black: black;
|
||||
|
||||
--color-dark-110: #2a2928;
|
||||
--color-dark-100: #393836;
|
||||
--color-dark-90: #494741;
|
||||
--color-dark-80: #5a5850;
|
||||
--color-dark-40: #7d796a;
|
||||
|
||||
@@ -108,11 +114,11 @@
|
||||
--text-sm--letter-spacing: -0.02em;
|
||||
|
||||
/* Notice the use of em, so that code scales with, say, headers */
|
||||
--text-code: 0.875em;
|
||||
--text-code: 0.9375em;
|
||||
--text-code--line-height: calc(28 / 16);
|
||||
--text-code--letter-spacing: -0.02em;
|
||||
/* if you don't want this scaling, use text-base-code */
|
||||
--text-base-code: 0.875rem;
|
||||
--text-base-code: 0.9375rem;
|
||||
--text-base-code--line-height: calc(28 / 16);
|
||||
--text-base-code--letter-spacing: -0.02em;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user