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:
Darius Cepulis
2025-10-28 13:33:25 -05:00
parent b9032ca387
commit 5dbaddc513
7 changed files with 74 additions and 124 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-7
View File
@@ -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

-8
View File
@@ -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>
+11 -41
View File
@@ -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')}
/>
);
}
+34 -29
View File
@@ -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>
);
}
+1 -1
View File
@@ -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">