TanStack
Catalog

Premium KPI sparklines

composition

1,043 lines · 7 files · 25.4 kB

cases/122-premium-kpi-sparklines/chart.ts113 lines · dependency
cases/122-premium-kpi-sparklines/chart.ts
import { areaY, d3Curve, defineChart, lineY } from '@tanstack/charts'
import { motion } from '@tanstack/charts/motion'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { scalePoint } from '@tanstack/charts/scales/point'
import { curveMonotoneX } from 'd3-shape'
import type { PremiumKpiId, PremiumKpiMetric, PremiumKpiPoint } from './model'

const monotone = d3Curve(curveMonotoneX)
const accentFallbacks = {
  revenue: '#6d5dfc',
  customers: '#0f91c7',
  churn: '#0c9b6c',
} satisfies Record<PremiumKpiId, string>

export const premiumKpiSpring = {
  type: 'spring' as const,
  stiffness: 180,
  damping: 24,
  mass: 0.8,
}

export function premiumKpiDefinition(metric: PremiumKpiMetric) {
  const values = metric.rows.map((row) => row.value)
  const minimum = Math.min(...values)
  const maximum = Math.max(...values)
  const padding = Math.max((maximum - minimum) * 0.16, 0.1)
  const baseline = minimum - padding
  const accent = `var(--premium-kpi-accent, ${accentFallbacks[metric.id]})`
  const line = lineY(metric.rows, {
    id: `${metric.id}-line`,
    x: 'period',
    y: 'value',
    key: 'id',
    stroke: accent,
    strokeWidth: 2.4,
    curve: monotone,
  })
  const marks =
    metric.surface === 'area'
      ? [
          areaY(metric.rows, {
            id: `${metric.id}-area`,
            x: 'period',
            y: 'value',
            y1: baseline,
            key: 'id',
            fill: `url(#${metric.id}-fill)`,
            fillOpacity: 1,
            curve: monotone,
          }),
          line,
        ]
      : [line]

  return defineChart(
    defineChart({
      guides: false,
      marks,
      gradients:
        metric.surface === 'area'
          ? [
              {
                id: `${metric.id}-fill`,
                x1: 0,
                y1: 0,
                x2: 0,
                y2: 1,
                stops: [
                  {
                    offset: 0,
                    color: accent,
                    opacity: 0.26,
                  },
                  {
                    offset: 1,
                    color: accent,
                    opacity: 0,
                  },
                ],
              },
            ]
          : [],
      x: {
        scale: scalePoint<number>()
          .domain(metric.rows.map((row) => row.period))
          .padding(0.08),
        axis: false,
      },
      y: {
        scale: scaleLinear().domain([baseline, maximum + padding]),
        axis: false,
      },
      margin: { top: 4, right: 3, bottom: 3, left: 3 },
      clip: true,
      motion: { transition: premiumKpiSpring },
    }),
    {
      focus: false,
      pointer: false,
      keyboard: false,
      tooltip: false,
      svgAnimation: false,
    },
  )
}

export function createPremiumKpiRenderer(initial = true) {
  return motion<PremiumKpiPoint, number, number>({
    initial,
    respectReducedMotion: true,
    transition: premiumKpiSpring,
  })
}
cases/122-premium-kpi-sparklines/model.ts99 lines · dependency
cases/122-premium-kpi-sparklines/model.ts
export type PremiumKpiId = 'revenue' | 'customers' | 'churn'

export interface PremiumKpiPoint {
  readonly id: string
  readonly period: number
  readonly value: number
}

export interface PremiumKpiMetric {
  readonly id: PremiumKpiId
  readonly label: string
  readonly value: string
  readonly trend: string
  readonly trendDirection: 'up' | 'down'
  readonly surface: 'area' | 'line'
  readonly rows: readonly PremiumKpiPoint[]
}

const stages = [
  {
    revenue: [
      264_400, 281_200, 276_800, 302_600, 319_100, 337_500, 331_800, 356_200,
      371_600, 365_900, 394_300, 412_840,
    ],
    customers: [
      2_810, 2_950, 3_030, 3_170, 3_120, 3_290, 3_410, 3_500, 3_590, 3_670,
      3_750, 3_842,
    ],
    churn: [2.8, 2.6, 2.7, 2.5, 2.4, 2.3, 2.35, 2.15, 2.1, 1.95, 1.86, 1.7],
  },
  {
    revenue: [
      279_200, 288_900, 286_400, 315_300, 329_800, 351_500, 345_900, 369_600,
      388_100, 381_700, 406_900, 429_180,
    ],
    customers: [
      2_940, 3_020, 3_140, 3_240, 3_210, 3_380, 3_500, 3_610, 3_690, 3_790,
      3_880, 3_976,
    ],
    churn: [
      2.7, 2.55, 2.62, 2.42, 2.33, 2.2, 2.24, 2.05, 1.98, 1.84, 1.75, 1.62,
    ],
  },
] as const

export function premiumKpisForRevision(
  revision: number,
): readonly PremiumKpiMetric[] {
  const stage = stages[Math.abs(revision) % stages.length] ?? stages[0]

  return [
    {
      id: 'revenue',
      label: 'Monthly revenue',
      value: formatRevenue(last(stage.revenue)),
      trend: revision % 2 === 0 ? '+9.2%' : '+10.6%',
      trendDirection: 'up',
      surface: 'area',
      rows: points('revenue', stage.revenue),
    },
    {
      id: 'customers',
      label: 'Active customers',
      value: last(stage.customers).toLocaleString('en-US'),
      trend: revision % 2 === 0 ? '+6.4%' : '+7.1%',
      trendDirection: 'up',
      surface: 'line',
      rows: points('customers', stage.customers),
    },
    {
      id: 'churn',
      label: 'Net churn',
      value: `${last(stage.churn).toFixed(1)}%`,
      trend: revision % 2 === 0 ? '−0.5 pt' : '−0.6 pt',
      trendDirection: 'down',
      surface: 'area',
      rows: points('churn', stage.churn),
    },
  ]
}

function points(
  metric: PremiumKpiId,
  values: readonly number[],
): readonly PremiumKpiPoint[] {
  return values.map((value, period) => ({
    id: `${metric}-${period}`,
    period,
    value,
  }))
}

function formatRevenue(value: number) {
  return `$${(value / 1_000).toFixed(1)}K`
}

function last(values: readonly number[]) {
  return values[values.length - 1] ?? 0
}
cases/122-premium-kpi-sparklines/tanstack.ts6 lines · entry
cases/122-premium-kpi-sparklines/tanstack.ts
export {
  createPremiumKpiRenderer,
  premiumKpiDefinition,
  premiumKpiSpring,
} from './chart'
export { catalogComponent, mount } from './view'
cases/122-premium-kpi-sparklines/view.tsx364 lines · dependency
cases/122-premium-kpi-sparklines/view.tsx
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'
import { Chart } from '@tanstack/charts/react/core'
import { settleChartMotion } from '../../shared/motion'
import { createPremiumKpiRenderer, premiumKpiDefinition } from './chart'
import { premiumKpisForRevision } from './model'
import { reactMount } from '../../shared/react-mount'
import type { CSSProperties, RefObject } from 'react'
import type { ConformanceTestDriver } from '../../types'
import type { ReactConformanceProps } from '../../shared/react-mount'
import type { PremiumKpiMetric } from './model'

const fullGap = 12

export const PremiumKpiSparklines = forwardRef<
  ConformanceTestDriver,
  ReactConformanceProps
>(function PremiumKpiSparklines({ input, idPrefix }, ref) {
  const rootRef = useRef<HTMLElement>(null)
  const metrics = premiumKpisForRevision(input.revision)

  useImperativeHandle(
    ref,
    () => ({
      resolveTarget() {
        return null
      },
      readState() {
        return {
          revision: input.revision,
          chartCount:
            rootRef.current?.querySelectorAll('.ts-chart-host').length ?? 0,
        }
      },
      settle() {
        const root = rootRef.current
        if (!root) return
        return Promise.all(
          [...root.querySelectorAll<HTMLElement>('.ts-chart-host')].map(
            (host) => settleChartMotion(host, 2_500),
          ),
        ).then(() => undefined)
      },
    }),
    [input.revision],
  )

  if (input.preview) {
    return (
      <PreviewGrid
        metrics={metrics}
        width={input.width}
        height={input.height}
        idPrefix={idPrefix}
        rootRef={rootRef}
      />
    )
  }

  const layout = fullLayout(input.width, input.height)

  return (
    <section
      ref={rootRef}
      data-conformance-view="main"
      data-premium-kpi-shell=""
      aria-label="Business metrics"
      className="premium-kpi-shell"
      style={{ width: input.width, height: input.height }}
    >
      <style>{premiumKpiStyles}</style>
      <div
        className="premium-kpi-grid"
        style={{
          gridTemplateColumns: layout.columns,
          gridTemplateRows: layout.rows,
          gap: fullGap,
        }}
      >
        {metrics.map((metric) => (
          <KpiCard
            key={metric.id}
            metric={metric}
            width={layout.cardWidth}
            height={layout.cardHeight}
            idPrefix={idPrefix}
          />
        ))}
      </div>
    </section>
  )
})

export const catalogComponent = PremiumKpiSparklines
export const mount = reactMount(PremiumKpiSparklines)

function KpiCard({
  metric,
  width,
  height,
  idPrefix,
  preview = false,
}: {
  readonly metric: PremiumKpiMetric
  readonly width: number
  readonly height: number
  readonly idPrefix?: string
  readonly preview?: boolean
}) {
  const definition = useMemo(() => premiumKpiDefinition(metric), [metric])
  const renderer = useMemo(() => createPremiumKpiRenderer(!preview), [preview])
  const compact = preview || width < 200 || height < 150
  const padding = compact ? 9 : 18
  const headerHeight = compact ? 44 : 72
  const chartWidth = Math.max(24, width - padding * 2)
  const chartHeight = Math.max(24, height - padding * 2 - headerHeight)

  return (
    <article
      data-premium-kpi={metric.id}
      data-conformance-view={metric.id}
      data-compact={compact || undefined}
      className="premium-kpi-card"
      style={
        {
          '--premium-kpi-card-width': `${width}px`,
          '--premium-kpi-card-height': `${height}px`,
        } as CSSProperties
      }
    >
      <header className="premium-kpi-header">
        <span className="premium-kpi-label">{metric.label}</span>
        <span
          className="premium-kpi-trend"
          aria-label={`${metric.trendDirection === 'up' ? 'Up' : 'Down'} ${metric.trend}`}
        >
          <span aria-hidden="true">
            {metric.trendDirection === 'up' ? '↑' : '↓'}
          </span>{' '}
          {metric.trend}
        </span>
        <strong className="premium-kpi-value">{metric.value}</strong>
      </header>
      <div className="premium-kpi-chart">
        <Chart
          idPrefix={idPrefix ? `${idPrefix}-${metric.id}` : undefined}
          definition={definition}
          renderer={renderer}
          width={chartWidth}
          height={chartHeight}
          ariaLabel={`${metric.label} trend, ending at ${metric.value}`}
        />
      </div>
    </article>
  )
}

function PreviewGrid({
  metrics,
  width,
  height,
  idPrefix,
  rootRef,
}: {
  readonly metrics: readonly PremiumKpiMetric[]
  readonly width: number
  readonly height: number
  readonly idPrefix?: string
  readonly rootRef: RefObject<HTMLElement | null>
}) {
  const gap = 5
  const primaryWidth = Math.max(1, Math.floor((width - gap) * 0.59))
  const secondaryWidth = Math.max(1, width - gap - primaryWidth)
  const secondaryHeight = Math.max(1, Math.floor((height - gap) / 2))
  const [primary, ...secondary] = metrics

  return (
    <section
      ref={rootRef}
      data-catalog-preview-composition="premium-kpi-sparklines"
      className="premium-kpi-shell premium-kpi-preview"
      style={{ width, height }}
    >
      <style>{premiumKpiStyles}</style>
      {primary ? (
        <KpiCard
          metric={primary}
          width={primaryWidth}
          height={height}
          idPrefix={idPrefix}
          preview
        />
      ) : null}
      <div
        className="premium-kpi-preview-stack"
        style={{ width: secondaryWidth, height }}
      >
        {secondary.map((metric) => (
          <KpiCard
            key={metric.id}
            metric={metric}
            width={secondaryWidth}
            height={secondaryHeight}
            idPrefix={idPrefix}
            preview
          />
        ))}
      </div>
    </section>
  )
}

function fullLayout(width: number, height: number) {
  const padding = 14
  const availableWidth = Math.max(1, width - padding * 2)
  const availableHeight = Math.max(1, height - padding * 2)
  const horizontal = width >= 680 || width / height >= 1.2

  return horizontal
    ? {
        columns: 'repeat(3, minmax(0, 1fr))',
        rows: 'minmax(0, 1fr)',
        cardWidth: Math.max(1, (availableWidth - fullGap * 2) / 3),
        cardHeight: availableHeight,
      }
    : {
        columns: 'minmax(0, 1fr)',
        rows: 'repeat(3, minmax(0, 1fr))',
        cardWidth: availableWidth,
        cardHeight: Math.max(1, (availableHeight - fullGap * 2) / 3),
      }
}

const premiumKpiStyles = `
.premium-kpi-shell {
  --premium-kpi-canvas: light-dark(#f5f6f8, #09090b);
  --premium-kpi-card: light-dark(rgba(255, 255, 255, 0.92), rgba(24, 24, 27, 0.9));
  --premium-kpi-foreground: light-dark(#18181b, #fafafa);
  --premium-kpi-muted: light-dark(#71717a, #a1a1aa);
  --premium-kpi-border: light-dark(rgba(24, 24, 27, 0.09), rgba(250, 250, 250, 0.1));
  --premium-kpi-shadow: light-dark(rgba(24, 24, 27, 0.07), rgba(0, 0, 0, 0.34));
  color-scheme: inherit;
  box-sizing: border-box;
  display: grid;
  place-items: stretch;
  padding: 14px;
  overflow: hidden;
  color: var(--premium-kpi-foreground);
  background: var(--premium-kpi-canvas);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.premium-kpi-grid {
  display: grid;
  min-width: 0;
  min-height: 0;
}

.premium-kpi-card {
  --premium-kpi-accent: #6d5dfc;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: var(--premium-kpi-card-width);
  height: var(--premium-kpi-card-height);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  padding: 18px;
  border: 1px solid var(--premium-kpi-border);
  border-radius: 18px;
  background: var(--premium-kpi-card);
  box-shadow: 0 14px 42px var(--premium-kpi-shadow);
}

.premium-kpi-card[data-premium-kpi="customers"] {
  --premium-kpi-accent: #0f91c7;
}

.premium-kpi-card[data-premium-kpi="churn"] {
  --premium-kpi-accent: #0c9b6c;
}

.premium-kpi-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 8px;
  flex: 0 0 72px;
}

.premium-kpi-label {
  overflow: hidden;
  color: var(--premium-kpi-muted);
  font-size: 12px;
  font-weight: 620;
  line-height: 1.2;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.premium-kpi-value {
  grid-column: 1 / -1;
  margin-top: 7px;
  font-size: clamp(24px, 3.1vw, 36px);
  font-weight: 730;
  letter-spacing: -0.055em;
  line-height: 1;
}

.premium-kpi-trend {
  color: var(--premium-kpi-accent);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
}

.premium-kpi-chart {
  display: grid;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  align-items: end;
}

.premium-kpi-preview {
  display: flex;
  gap: 5px;
  padding: 0;
  background: transparent;
}

.premium-kpi-card[data-compact] {
  padding: 9px;
  border-radius: 11px;
  box-shadow: 0 6px 18px var(--premium-kpi-shadow);
}

.premium-kpi-card[data-compact] .premium-kpi-header {
  flex-basis: 44px;
  column-gap: 4px;
}

.premium-kpi-card[data-compact] .premium-kpi-label {
  font-size: 7px;
}

.premium-kpi-card[data-compact] .premium-kpi-value {
  margin-top: 3px;
  font-size: 15px;
}

.premium-kpi-card[data-compact] .premium-kpi-trend {
  font-size: 7px;
}

.premium-kpi-preview-stack {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

`
shared/motion.ts28 lines · dependency
shared/motion.ts
export function readChartMotionState(root: ParentNode) {
  return (
    root.querySelector('svg.ts-chart')?.getAttribute('data-ts-motion-state') ??
    null
  )
}

export function settleChartMotion(root: HTMLElement, timeout: number) {
  const view = root.ownerDocument.defaultView
  if (!view) return Promise.resolve()
  const started = view.performance.now()

  return new Promise<void>((resolve) => {
    const check = () => {
      const state = readChartMotionState(root)
      if (
        state === 'finished' ||
        state === null ||
        view.performance.now() - started >= timeout
      ) {
        resolve()
        return
      }
      view.requestAnimationFrame(check)
    }
    check()
  })
}
shared/react-mount.ts57 lines · dependency
shared/react-mount.ts
import { createElement } from 'react'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import type { ForwardRefExoticComponent, RefAttributes } from 'react'
import type {
  ConformanceInput,
  ConformanceMount,
  ConformanceTestDriver,
} from '../types'

export interface ReactConformanceProps {
  input: ConformanceInput
  idPrefix?: string
}

export type ReactConformanceComponent = ForwardRefExoticComponent<
  ReactConformanceProps & RefAttributes<ConformanceTestDriver>
>

export function reactMount(
  Component: ReactConformanceComponent,
): ConformanceMount {
  return (container, input) => {
    const root = createRoot(container)
    let activeDriver: ConformanceTestDriver | null = null
    const driver = new Proxy({} as ConformanceTestDriver, {
      get(_target, property) {
        const value = activeDriver?.[property as keyof ConformanceTestDriver]
        return typeof value === 'function' ? value.bind(activeDriver) : value
      },
    })
    const render = (nextInput: ConformanceInput) => {
      flushSync(() => {
        root.render(
          createElement(Component, {
            input: nextInput,
            ref: (nextDriver: ConformanceTestDriver | null) => {
              activeDriver = nextDriver
            },
          }),
        )
      })
    }

    render(input)

    return {
      update: render,
      driver,
      destroy() {
        flushSync(() => {
          root.unmount()
        })
      },
    }
  }
}
types.ts376 lines · dependency
types.ts
export type ConformanceReferenceRenderer =
  'observable-plot' | 'recharts' | 'echarts'

export type ConformanceRenderer = ConformanceReferenceRenderer | 'tanstack'

export type ConformanceSupport = 'native' | 'composed' | 'gap' | 'deferred'

export type ConformanceGeometryRole =
  | 'arc'
  | 'area'
  | 'arrow'
  | 'bar'
  | 'cell'
  | 'contour'
  | 'delaunay'
  | 'density'
  | 'dot'
  | 'frame'
  | 'geo'
  | 'hexagon'
  | 'line'
  | 'link'
  | 'rect'
  | 'radar'
  | 'regression'
  | 'rule'
  | 'text'
  | 'tick'
  | 'vector'
  | 'voronoi'
  | 'waffle'

export interface ConformanceInput {
  width: number
  height: number
  revision: number
  interactive?: boolean
  /** Use lower-detail geometry suited to compact catalog cards. */
  preview?: boolean
  /** True only for semantic browser scenarios, not catalog or visual mounts. */
  behavior?: boolean
}

export interface ConformanceHandle {
  update: (input: ConformanceInput) => void
  driver?: ConformanceTestDriver
  destroy: () => void
}

export type ConformanceMount = (
  container: HTMLElement,
  input: ConformanceInput,
) => ConformanceHandle

export interface ConformanceGeometryExpectation {
  id?: string
  view?: string
  role: ConformanceGeometryRole
  count: number
  maxCount?: number
  rendererRoles?: Partial<Record<ConformanceRenderer, ConformanceGeometryRole>>
}

export type ConformanceAxis = 'x' | 'y' | 'fx' | 'fy'

export interface ConformanceGuideExpectation {
  id: string
  axis:
    | ConformanceAxis
    | (Record<'tanstack', ConformanceAxis> &
        Partial<Record<ConformanceReferenceRenderer, ConformanceAxis>>)
  sequence?: readonly string[]
  maxRepeat?: number
}

export type ConformanceJsonValue =
  | null
  | boolean
  | number
  | string
  | readonly ConformanceJsonValue[]
  | ConformanceJsonObject

export interface ConformanceJsonObject {
  readonly [key: string]: ConformanceJsonValue
}

export interface ConformanceTarget {
  view?: string
  anchor: string
}

export type ConformanceRenderedTarget =
  | {
      selector: string
      index?: number
      role?: never
      name?: never
      exact?: never
      root?: never
      page?: never
    }
  | {
      role: string
      name?: string
      exact?: boolean
      index?: number
      selector?: never
      root?: never
      page?: never
    }
  | {
      root: true
      selector?: never
      role?: never
      name?: never
      exact?: never
      index?: never
      page?: never
    }
  | {
      page: true
      selector?: never
      role?: never
      name?: never
      exact?: never
      index?: never
      root?: never
    }

export interface ConformanceResolvedTarget {
  /** Viewport-relative client coordinate used by Playwright mouse input. */
  x: number
  /** Viewport-relative client coordinate used by Playwright mouse input. */
  y: number
  /** Optional element to focus before a real Playwright keyboard action. */
  focusElement?: HTMLElement | SVGElement
}

export interface ConformanceGeometryQuery {
  view?: string
  role: ConformanceGeometryRole
}

export interface ConformanceGeometrySample {
  /** Viewport-relative client box, matching getBoundingClientRect coordinates. */
  x: number
  y: number
  width: number
  height: number
  paint?: string
}

export interface ConformanceTestDriver {
  /**
   * Benchmark-only semantic bridge. Case metadata names anchors; each renderer
   * resolves those anchors without exposing renderer-specific selectors.
   */
  resolveTarget: (target: ConformanceTarget) => ConformanceResolvedTarget | null
  readState: () => ConformanceJsonObject
  geometry?: (
    query: ConformanceGeometryQuery,
  ) => readonly ConformanceGeometrySample[]
  /**
   * Viewport-relative logical view bounds. Multi-grid renderers may expose
   * independent views without separate DOM roots.
   */
  viewBounds?: (view?: string) => ConformanceGeometrySample | null
  settle?: () => void | Promise<void>
}

export type ConformanceStateAssertion =
  | {
      path: string
      equals: ConformanceJsonValue
    }
  | {
      path: string
      includes: ConformanceJsonValue
    }
  | {
      path: string
      approx: number
      tolerance: number
    }

type ConformanceRenderedStringMatcher =
  | {
      equals: string | null
      includes?: never
    }
  | {
      includes: string
      equals?: never
    }

type ConformanceRenderedNumberMatcher =
  | {
      equals: number
      approx?: never
      tolerance?: never
      atLeast?: never
      atMost?: never
    }
  | {
      approx: number
      tolerance: number
      equals?: never
      atLeast?: never
      atMost?: never
    }
  | {
      atLeast: number
      equals?: never
      approx?: never
      tolerance?: never
      atMost?: never
    }
  | {
      atMost: number
      equals?: never
      approx?: never
      tolerance?: never
      atLeast?: never
    }

export type ConformanceRenderedAssertion =
  | ({
      target: ConformanceRenderedTarget
      property: 'count'
    } & ConformanceRenderedNumberMatcher)
  | ({
      target: ConformanceRenderedTarget
      property: 'text'
    } & ConformanceRenderedStringMatcher)
  | ({
      target: ConformanceRenderedTarget
      property: 'attribute'
      attribute: string
    } & ConformanceRenderedStringMatcher)
  | {
      target: ConformanceRenderedTarget
      property: 'visible' | 'focused'
      equals: boolean
    }
  | ({
      target: ConformanceRenderedTarget
      property:
        | 'scrollLeft'
        | 'scrollTop'
        | 'scrollWidth'
        | 'scrollHeight'
        | 'clientWidth'
        | 'clientHeight'
        | 'width'
        | 'height'
    } & ConformanceRenderedNumberMatcher)
  | {
      target: ConformanceRenderedTarget
      property: 'contained'
      within?: ConformanceRenderedTarget
      tolerance?: number
      equals: true
    }

export type ConformanceInteractionStep =
  | {
      type: 'pointerMove'
      target: ConformanceTarget
      steps?: number
    }
  | {
      type: 'pointerDown'
      target: ConformanceTarget
    }
  | {
      type: 'pointerUp'
      target: ConformanceTarget
    }
  | {
      type: 'pointerCancel'
    }
  | {
      type: 'pointerLeave'
      view?: string
    }
  | {
      type: 'update'
      revision: number
    }
  | {
      type: 'click'
      target: ConformanceTarget
    }
  | {
      type: 'key'
      key: string
      target?: ConformanceTarget
    }
  | {
      type: 'drag'
      from: ConformanceTarget
      to: ConformanceTarget
      steps?: number
    }
  | {
      type: 'wheel'
      target: ConformanceTarget
      deltaX?: number
      deltaY?: number
      steps?: number
      deltaMode?: 'pixel' | 'line' | 'page'
    }
  | {
      type: 'touchTap'
      target: ConformanceTarget
    }
  | {
      type: 'touchDrag'
      from: ConformanceTarget
      to: ConformanceTarget
      steps?: number
      cancel?: boolean
    }
  | {
      type: 'wait'
      durationMs: number
    }
  | {
      type: 'assert'
      assertions: readonly ConformanceStateAssertion[]
    }
  | {
      type: 'assertRendered'
      assertions: readonly ConformanceRenderedAssertion[]
    }
  | {
      type: 'screenshot'
      name: string
      view?: string
    }

export interface ConformanceInteractionScenario {
  id: string
  steps: readonly ConformanceInteractionStep[]
}

export interface ConformanceCaseMeta {
  schemaVersion: 1
  referenceRenderer?: ConformanceReferenceRenderer
  order: number
  id: string
  title: string
  family: string
  intent: string
  support: ConformanceSupport
  features: readonly string[]
  geometry: readonly ConformanceGeometryExpectation[]
  minimumGeometrySimilarity?: number
  guideAssertions?: readonly ConformanceGuideExpectation[]
  interactionScenarios?: readonly ConformanceInteractionScenario[]
  source: {
    title: string
    url: string
  }
  ai: {
    create: string
    maintain: string
  }
}

export interface ConformanceImplementationModule {
  mount: ConformanceMount
  /** Definition-only mount used by compact generated catalog previews. */
  catalogCase?: { mount: ConformanceMount }
}