TanStack
Catalog

Theme palette matrix

theme

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)})`
}