mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 13:48:14 +00:00
feat(site): raise prominence of home page demo toggles
Also: sneakily remove dark mode favicon. I didn't like it.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#fcb116" d="M0 0h90v90H0z" />
|
||||
<path fill="#f26222" d="M0 22.5h90V90H0z" />
|
||||
<path fill="#ea3837" d="M0 45h90v45H0z" />
|
||||
<path fill="#a83b71" d="M0 67.5h90V90H0z" />
|
||||
<path d="M71.954 45L28.046 73.125v-56.25L71.954 45z" fill="#2a2928" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 324 B |
@@ -29,13 +29,5 @@ import { THEME_KEY } from '@/consts';
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute('content', isDark ? '#393836' : '#ebe4c1');
|
||||
}
|
||||
const svgFavicon = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
|
||||
const icoFavicon = document.querySelector('link[rel="icon"][sizes="32x32"]');
|
||||
if (svgFavicon) {
|
||||
svgFavicon.setAttribute('href', isDark ? '/favicon-dark.svg' : '/favicon.svg');
|
||||
}
|
||||
if (icoFavicon) {
|
||||
icoFavicon.setAttribute('href', isDark ? '/favicon-dark.ico' : '/favicon.ico');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { THEME_KEY } from '@/consts';
|
||||
import ToggleGroup from './ToggleGroup';
|
||||
|
||||
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 },
|
||||
{ value: 'system' as const, label: <Monitor size={14} aria-hidden="true" />, 'aria-label': 'System' },
|
||||
{ value: 'light' as const, label: <Sun size={14} aria-hidden="true" />, 'aria-label': 'Light' },
|
||||
{ value: 'dark' as const, label: <Moon size={14} aria-hidden="true" />, 'aria-label': 'Dark' },
|
||||
];
|
||||
|
||||
function initPreference(): Preference {
|
||||
@@ -47,10 +46,6 @@ export function ThemeToggle() {
|
||||
setTheme(getThemeFromPreference(newPreference));
|
||||
};
|
||||
|
||||
const handleValueChange = (values: Preference[]) => {
|
||||
if (values.length > 0) setPreference(values[0]);
|
||||
};
|
||||
|
||||
// Initialize preference and theme on mount
|
||||
useEffect(() => {
|
||||
const initialPreference = initPreference();
|
||||
@@ -74,19 +69,15 @@ export function ThemeToggle() {
|
||||
};
|
||||
}, [preference]);
|
||||
|
||||
// Keep document.documentElement, theme-color, and favicons in sync with theme
|
||||
// Keep document.documentElement and theme-color in sync with theme
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#ebe4c1');
|
||||
document.querySelector('link[rel="icon"][type="image/svg+xml"]')?.setAttribute('href', '/favicon.svg');
|
||||
document.querySelector('link[rel="icon"][sizes="32x32"]')?.setAttribute('href', '/favicon.ico');
|
||||
} else if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#393836');
|
||||
document.querySelector('link[rel="icon"][type="image/svg+xml"]')?.setAttribute('href', '/favicon-dark.svg');
|
||||
document.querySelector('link[rel="icon"][sizes="32x32"]')?.setAttribute('href', '/favicon-dark.ico');
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -97,31 +88,10 @@ export function ThemeToggle() {
|
||||
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>
|
||||
value={preference ? [preference] : []}
|
||||
onChange={(values) => { if (values.length > 0) setPreference(values[0]); }}
|
||||
options={themeOptions}
|
||||
toggleClassName={clsx(preference === null && 'cursor-wait')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,17 +5,20 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ToggleOption<T = string> {
|
||||
value: T;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
'aria-label'?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ToggleGroupProps<T = string> {
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
value: T[];
|
||||
onChange: (value: T[]) => void;
|
||||
options: ToggleOption<T>[];
|
||||
className?: string;
|
||||
toggleClassName?: string;
|
||||
disabled?: boolean;
|
||||
'aria-label'?: string;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export default function ToggleGroup<T extends string = string>({
|
||||
@@ -24,41 +27,43 @@ export default function ToggleGroup<T extends string = string>({
|
||||
options,
|
||||
className,
|
||||
toggleClassName,
|
||||
disabled,
|
||||
'aria-label': ariaLabel,
|
||||
multiple = false,
|
||||
}: ToggleGroupProps<T>) {
|
||||
const handleChange = (newValue: string[]) => {
|
||||
if (newValue.length > 0) {
|
||||
onChange(newValue[0] as T);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseToggleGroup
|
||||
value={[value]}
|
||||
onValueChange={handleChange}
|
||||
multiple={false}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
disabled={disabled}
|
||||
className={twMerge(
|
||||
clsx(
|
||||
'inline-flex items-center gap-5',
|
||||
),
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
multiple={multiple}
|
||||
>
|
||||
{options.map(option => (
|
||||
<Toggle
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
className={twMerge(clsx(
|
||||
'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 dark:intent:border-light-40 data-[pressed]:border-current cursor-pointer',
|
||||
), toggleClassName)}
|
||||
>
|
||||
{option.label}
|
||||
</Toggle>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
const isPressed = value.includes(option.value as T);
|
||||
const isDisabled = disabled || option.disabled;
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(clsx(
|
||||
'relative',
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded text-sm',
|
||||
isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
isPressed ? 'bg-light-80 dark:bg-dark-100' : !isDisabled ? 'intent:bg-light-80/50 dark:intent:bg-dark-100/50' : '',
|
||||
), toggleClassName)}
|
||||
aria-label={option['aria-label']}
|
||||
>
|
||||
{option.label}
|
||||
</Toggle>
|
||||
);
|
||||
})}
|
||||
</BaseToggleGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,40 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import clsx from 'clsx';
|
||||
import { framework, media, skin } from '@/stores/homePageDemos';
|
||||
import { framework, skin } from '@/stores/homePageDemos';
|
||||
import ToggleGroup from '../ToggleGroup';
|
||||
|
||||
export default function HomePageControls({ className }: { className?: string }) {
|
||||
const $framework = useStore(framework);
|
||||
const $skin = useStore(skin);
|
||||
const $media = useStore(media);
|
||||
|
||||
return (
|
||||
<section className={clsx('flex flex-col sm:flex-row gap-2 sm:gap-20 items-center justify-center', className)}>
|
||||
<ToggleGroup
|
||||
className="mx-auto sm:mx-0 col-span-full grid grid-cols-2 justify-center sm:flex"
|
||||
toggleClassName="py-1 sm:py-3"
|
||||
value={$skin}
|
||||
onChange={value => skin.set(value)}
|
||||
options={[
|
||||
{ value: 'frosted', label: 'Frosted' },
|
||||
{ value: 'minimal', label: 'Minimal' },
|
||||
]}
|
||||
aria-label="Select skin"
|
||||
/>
|
||||
|
||||
<ToggleGroup
|
||||
className="mx-auto sm:mx-0 col-span-full grid grid-cols-2 justify-center sm:flex"
|
||||
toggleClassName="py-1 sm:py-3"
|
||||
value={$framework}
|
||||
onChange={value => framework.set(value)}
|
||||
options={[
|
||||
{ value: 'react', label: 'React' },
|
||||
{ value: 'html', label: 'HTML' },
|
||||
]}
|
||||
aria-label="Select framework"
|
||||
/>
|
||||
|
||||
<ToggleGroup
|
||||
className="mx-auto sm:mx-0 col-span-full grid grid-cols-2 justify-center sm:flex"
|
||||
toggleClassName="py-1 sm:py-3"
|
||||
value={$media}
|
||||
onChange={value => media.set(value)}
|
||||
options={[
|
||||
{ value: 'video', label: 'Simple' },
|
||||
{ value: 'hls-video', label: 'HLS' },
|
||||
]}
|
||||
aria-label="Select media element"
|
||||
/>
|
||||
<section
|
||||
className={clsx('flex flex-row flex-wrap sm:grid sm:grid-cols-2 gap-2 sm:gap-9 items-center justify-center', className)}
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<ToggleGroup
|
||||
toggleClassName="md:text-base md:py-1"
|
||||
value={[$skin]}
|
||||
onChange={(values) => { if (values.length > 0) skin.set(values[0]); }}
|
||||
options={[
|
||||
{ value: 'frosted', label: 'Frosted' },
|
||||
{ value: 'minimal', label: 'Minimal' },
|
||||
]}
|
||||
aria-label="Select skin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ToggleGroup
|
||||
toggleClassName="md:text-base md:py-1"
|
||||
value={[$framework]}
|
||||
onChange={(values) => { if (values.length > 0) framework.set(values[0]); }}
|
||||
options={[
|
||||
{ value: 'react', label: 'React' },
|
||||
{ value: 'html', label: 'HTML' },
|
||||
]}
|
||||
aria-label="Select framework"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const poster = `https://image.mux.com/${PLAYBACK_ID}/thumbnail.webp?time=0`;
|
||||
</header>
|
||||
<!-- padding corresponds to negative margin on player -->
|
||||
<main class="@container pt-11 pb-36 md:pt-21 lg:pt-32 px-6 md:px-10 w-full max-w-7xl mx-auto min-h-64">
|
||||
<HomePageControls client:idle className="col-span-full mt-6 mb-10 sm:mb-20" />
|
||||
<HomePageControls client:idle className="col-span-full my-10 sm:my-12 lg:mb-15" />
|
||||
<HomePageDemo client:idle className="mb-6 lg:mb-30" />
|
||||
<section class="rounded-3xl">
|
||||
<header class="max-w-3xl mb-3">
|
||||
|
||||
Reference in New Issue
Block a user