378 lines · 2 files · 11.0 kB
cases/124-theme-palette-matrix/example.tsx273 lines · entry
cases/124-theme-palette-matrix/example.tsx
import { useMemo, useRef } from 'react'
import { motion } from '@tanstack/charts/motion'
import { Chart } from '@tanstack/charts/react/core'
import {
paletteMatrixRows,
palettePaint,
paletteTreatments,
paletteValue,
paletteVariable,
} from './model'
import type { CSSProperties } from 'react'
import type { PaletteMatrixRow, PaletteTreatment } from './model'
import { areaY, defineChart, dot, lineY } from '@tanstack/charts'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { scalePoint } from '@tanstack/charts/scales/point'
type PalettePanelStyle = CSSProperties & Record<`--ts-matrix-${string}`, string>
function palettePanelStyle(treatment: PaletteTreatment): PalettePanelStyle {
return Object.fromEntries(
Object.keys(treatment.tokens).map((token) => [
paletteVariable(treatment, token as keyof PaletteTreatment['tokens']),
paletteValue(treatment, token as keyof PaletteTreatment['tokens']),
]),
)
}
export interface ExampleProps {
width?: number
height?: number
revision?: number
}
const gradientId = 'palette-area'
export function paletteMatrixDefinition(
rows: readonly PaletteMatrixRow[],
treatment: PaletteTreatment,
preview = false,
) {
const primary = palettePaint(treatment, 'primary')
const secondary = palettePaint(treatment, 'secondary')
const surface = palettePaint(treatment, 'surface')
return defineChart({
motion: {
transition: {
type: 'spring',
stiffness: 180,
damping: 22,
mass: 0.8,
},
},
marks: [
areaY(rows, {
id: 'value-area',
x: 'period',
y: 'value',
key: 'id',
fill: `url(#${gradientId})`,
}),
lineY(rows, {
id: 'value-line',
x: 'period',
y: 'value',
key: 'id',
stroke: primary,
strokeWidth: preview ? 2 : 2.5,
}),
lineY(rows, {
id: 'comparison-line',
x: 'period',
y: 'comparison',
key: 'id',
stroke: secondary,
strokeOpacity: 0.8,
strokeWidth: preview ? 1 : 1.5,
strokeDasharray: '3 4',
motion: {
transition: { type: 'tween', duration: 420, easing: 'ease-out' },
},
}),
dot(rows.slice(-1), {
id: 'latest-value',
x: 'period',
y: 'value',
key: 'id',
r: preview ? 2 : 3.5,
fill: primary,
stroke: surface,
strokeWidth: preview ? 1 : 2,
motion: { transition: { type: 'spring', mass: 1.15 } },
}),
],
scales: {
x: {
scale: () =>
scalePoint<string>()
.domain(rows.map((row) => row.period))
.padding(0.12),
},
y: { scale: scaleLinear().domain([20, 100]) },
},
guides: false,
margin: preview ? 5 : { top: 12, right: 14, bottom: 10, left: 14 },
gradients: [
{
id: gradientId,
x1: 0,
y1: 1,
x2: 0,
y2: 0,
stops: [
{ offset: 0, color: primary, opacity: 0.02 },
{ offset: 0.55, color: primary, opacity: 0.16 },
{ offset: 1, color: primary, opacity: 0.52 },
],
},
],
clip: true,
theme: {
background: surface,
foreground: palettePaint(treatment, 'foreground'),
muted: palettePaint(treatment, 'muted'),
grid: palettePaint(treatment, 'grid'),
palette: [primary, secondary],
},
focus: false,
keyboard: false,
tooltip: false,
})
}
export default function ThemePaletteMatrix({
width = 640,
height = 480,
revision = 0,
}: ExampleProps = {}) {
const input = { width, height, revision, preview: false, interactive: true }
const idPrefix = '124-theme-palette-matrix'
const rootRef = useRef<HTMLDivElement>(null)
const rows = useMemo(
() => paletteMatrixRows(input.revision),
[input.revision],
)
const definitions = useMemo(
() =>
paletteTreatments.map((treatment) =>
paletteMatrixDefinition(rows, treatment, false),
),
[false, rows],
)
const renderer = useMemo(
() =>
motion({
initial: true,
respectReducedMotion: true,
transition: {
type: 'spring',
stiffness: 180,
damping: 22,
mass: 0.8,
},
}),
[],
)
const gap = false ? 4 : 10
const padding = false ? 0 : 12
const availableHeight = input.height - padding * 2 - gap * 2
const panelHeight = Math.max(1, availableHeight / 3)
const labelWidth = input.width < 440 ? 120 : 124
const chartWidth = false
? input.width
: Math.max(1, input.width - padding * 2 - labelWidth)
return (
<div
ref={rootRef}
data-catalog-preview-composition={
false ? 'theme-palette-matrix' : undefined
}
data-conformance-view="main"
role="region"
aria-label="Theme palette matrix"
style={{
boxSizing: 'border-box',
display: 'grid',
gridTemplateRows: `repeat(3, ${panelHeight}px)`,
gap,
width: input.width,
height: input.height,
padding,
}}
>
{paletteTreatments.map((treatment, index) => {
const definition = definitions[index]
if (!definition) return null
const style = palettePanelStyle(treatment)
return (
<section
key={treatment.id}
data-palette-treatment={treatment.id}
aria-label={`${treatment.label} palette`}
style={{
...style,
boxSizing: 'border-box',
colorScheme: 'light dark',
display: false ? 'block' : 'grid',
gridTemplateColumns: false
? undefined
: `${labelWidth}px minmax(0, 1fr)`,
alignItems: 'center',
width: input.width - padding * 2,
height: panelHeight,
overflow: 'hidden',
border: false
? undefined
: `1px solid ${palettePaint(treatment, 'grid')}`,
borderRadius: false ? undefined : 14,
background: palettePaint(treatment, 'surface'),
color: palettePaint(treatment, 'foreground'),
}}
>
{false ? null : (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
minWidth: 0,
paddingInline: input.width < 440 ? 10 : 14,
font: '650 12px/1.2 system-ui, sans-serif',
}}
>
<span
aria-hidden="true"
style={{
width: 8,
height: 8,
flex: '0 0 auto',
borderRadius: 999,
background: palettePaint(treatment, 'primary'),
}}
/>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{treatment.label}
</span>
</div>
)}
<Chart
idPrefix={idPrefix ? `${idPrefix}-${treatment.id}` : undefined}
definition={definition}
renderer={renderer}
width={chartWidth}
height={panelHeight}
ariaLabel={`${treatment.label} revenue trend`}
/>
</section>
)
})}
</div>
)
}cases/124-theme-palette-matrix/model.ts105 lines · dependency
cases/124-theme-palette-matrix/model.ts
export interface PaletteMatrixRow {
id: string
period: string
value: number
comparison: number
}
export type PaletteTreatmentId = 'neutral' | 'vibrant' | 'monochrome'
export type PaletteToken =
'primary' | 'secondary' | 'surface' | 'foreground' | 'muted' | 'grid'
interface PaletteTone {
light: string
dark: string
}
export interface PaletteTreatment {
id: PaletteTreatmentId
label: string
tokens: Readonly<Record<PaletteToken, PaletteTone>>
}
const revisions: readonly (readonly PaletteMatrixRow[])[] = [
[
{ id: 'jan', period: 'Jan', value: 42, comparison: 38 },
{ id: 'feb', period: 'Feb', value: 51, comparison: 43 },
{ id: 'mar', period: 'Mar', value: 47, comparison: 49 },
{ id: 'apr', period: 'Apr', value: 66, comparison: 55 },
{ id: 'may', period: 'May', value: 61, comparison: 60 },
{ id: 'jun', period: 'Jun', value: 76, comparison: 65 },
{ id: 'jul', period: 'Jul', value: 72, comparison: 70 },
{ id: 'aug', period: 'Aug', value: 86, comparison: 75 },
],
[
{ id: 'jan', period: 'Jan', value: 46, comparison: 40 },
{ id: 'feb', period: 'Feb', value: 48, comparison: 45 },
{ id: 'mar', period: 'Mar', value: 58, comparison: 50 },
{ id: 'apr', period: 'Apr', value: 63, comparison: 56 },
{ id: 'may', period: 'May', value: 70, comparison: 61 },
{ id: 'jun', period: 'Jun', value: 68, comparison: 66 },
{ id: 'jul', period: 'Jul', value: 82, comparison: 71 },
{ id: 'aug', period: 'Aug', value: 79, comparison: 76 },
],
]
export const paletteTreatments: readonly PaletteTreatment[] = [
{
id: 'neutral',
label: 'Neutral',
tokens: {
primary: { light: '#475569', dark: '#cbd5e1' },
secondary: { light: '#94a3b8', dark: '#64748b' },
surface: { light: '#f8fafc', dark: '#111827' },
foreground: { light: '#0f172a', dark: '#f8fafc' },
muted: { light: '#64748b', dark: '#94a3b8' },
grid: { light: '#cbd5e1', dark: '#334155' },
},
},
{
id: 'vibrant',
label: 'Vibrant',
tokens: {
primary: { light: '#7c3aed', dark: '#a78bfa' },
secondary: { light: '#f97316', dark: '#fb923c' },
surface: { light: '#faf7ff', dark: '#181225' },
foreground: { light: '#2e1065', dark: '#f5f3ff' },
muted: { light: '#7e22ce', dark: '#c4b5fd' },
grid: { light: '#ddd6fe', dark: '#3b2d55' },
},
},
{
id: 'monochrome',
label: 'Monochrome',
tokens: {
primary: { light: '#18181b', dark: '#fafafa' },
secondary: { light: '#71717a', dark: '#a1a1aa' },
surface: { light: '#fafafa', dark: '#18181b' },
foreground: { light: '#09090b', dark: '#fafafa' },
muted: { light: '#71717a', dark: '#a1a1aa' },
grid: { light: '#d4d4d8', dark: '#3f3f46' },
},
},
]
export function paletteMatrixRows(revision: number) {
return revisions[Math.abs(revision) % revisions.length] ?? revisions[0]
}
export function paletteVariable(
treatment: PaletteTreatment,
token: PaletteToken,
) {
return `--ts-matrix-${treatment.id}-${token}`
}
/** Resolve inside an element whose `color-scheme` includes `light dark`. */
export function paletteValue(treatment: PaletteTreatment, token: PaletteToken) {
const tone = treatment.tokens[token]
return `light-dark(${tone.light}, ${tone.dark})`
}
export function palettePaint(treatment: PaletteTreatment, token: PaletteToken) {
return `var(${paletteVariable(treatment, token)}, ${paletteValue(treatment, token)})`
}