// // Copyright 2025 DXOS.org // import { type Meta } from '@storybook/react-vite'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { hueShades, hues } from './defs'; import { mx } from './util'; // prettier-ignore const neutralShades: [number, string][] = [ [50, 'bg-neutral-50'], [75, 'bg-neutral-75'], [100, 'bg-neutral-100'], [125, 'bg-neutral-125'], [150, 'bg-neutral-150'], [200, 'bg-neutral-200'], [250, 'bg-neutral-250'], [300, 'bg-neutral-300'], [400, 'bg-neutral-400'], [500, 'bg-neutral-500'], [600, 'bg-neutral-600'], [700, 'bg-neutral-700'], [750, 'bg-neutral-750'], [800, 'bg-neutral-800'], [825, 'bg-neutral-825'], [850, 'bg-neutral-850'], [875, 'bg-neutral-875'], [900, 'bg-neutral-900'], [925, 'bg-neutral-925'], [950, 'bg-neutral-950'], ]; const StyleSwatch = ({ hue }: { hue: string }) => { return (
{hue}
-text
{hue}
-fg/-surface
); }; const HueSwatch = ({ hue, shades }: { hue: string; shades: readonly number[] }) => (
{shades.map((shade) => (
{shade} {shade === 500 && {hue}}
))}
); const meta = { title: 'ui/ui-theme/Theme', parameters: { layout: 'fullscreen', }, } satisfies Meta; export default meta; export const Styles = { render: () => { return (
{['neutral', ...hues].map((hue) => ( ))}
); }, }; export const Colors = { render: () => { return (
{['neutral', ...hues].map((hue) => ( ))}
); }, }; export const Neutral = { render: () => { return (
{neutralShades.map(([value, className]) => (
{value}
))}
); }, }; // prettier-ignore const surfaces: [surface: string, foreground: string, label: string][] = [ // Sorted lightest -> darkest at runtime (see Surfaces story); surfaces without a dedicated // foreground fall back to base-fg. ['bg-base-surface', 'text-base-fg', 'base'], ['bg-deck-surface', 'text-base-fg', 'deck'], ['bg-card-surface', 'text-base-fg', 'card'], ['bg-toolbar-surface', 'text-base-fg', 'toolbar'], ['bg-sidebar-surface', 'text-base-fg', 'sidebar'], ['bg-group-surface', 'text-base-fg', 'group'], ['bg-header-surface', 'text-base-fg', 'header'], ['bg-modal-surface', 'text-base-fg', 'modal'], ['bg-l1-surface', 'text-base-fg', 'l1'], ['bg-r1-surface', 'text-base-fg', 'r1'], ['bg-hover-surface', 'text-hover-fg', 'hover'], ['bg-current-surface', 'text-current-fg', 'current'], ['bg-selected-surface','text-selected-fg', 'selected'], ['bg-l0-surface', 'text-base-fg', 'l0'], ['bg-r0-surface', 'text-base-fg', 'r0'], ['bg-input-surface', 'text-input-fg', 'input'], ['bg-inverse-surface', 'text-inverse-fg', 'inverse'], ['bg-scrim-surface', 'text-base-fg', 'scrim'], ]; // Resolve any CSS color (oklch, light-dark(), rgb, ...) to a 0-1 luminance via a 1x1 canvas. const colorLuminance = (css: string): number => { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; const ctx = canvas.getContext('2d'); if (!ctx) { return 0; } ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = css; ctx.fillRect(0, 0, 1, 1); const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data; return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; }; export const Surfaces = { render: () => { const rowRefs = useRef(new Map()); const [order, setOrder] = useState(surfaces); useLayoutEffect(() => { // Sort lightest -> darkest by the resolved background; re-runs when the theme toggles. const sort = () => { const ranked = surfaces .map((row) => { const element = rowRefs.current.get(row[2]); return { row, luminance: element ? colorLuminance(getComputedStyle(element).backgroundColor) : 0 }; }) .sort((a, b) => b.luminance - a.luminance) .map(({ row }) => row); setOrder(ranked); }; sort(); const observer = new MutationObserver(sort); observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class', 'style', 'data-theme'], }); return () => observer.disconnect(); }, []); return (
{order.map(([surface, foreground, label]) => (
{ if (element) { rowRefs.current.set(label, element); } }} className={mx('flex items-baseline justify-between px-4 py-3', surface, foreground)} > {label} {surface} ยท {foreground}
))}
); }, }; export const Tags = { render: () => { return (
{['neutral', ...hues].map((hue) => (
{hue}
{hue}
))}
); }, }; export const Animation = { render: () => { return (
* experimental *
); }, };