//
// 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 (
);
};
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) => (
))}
);
},
};
export const Animation = {
render: () => {
return (
);
},
};