import { useId } from 'react' import type { LegendFillPattern, LegendStrokeStyle, LegendSwatchShape, } from '../../stores' export interface LegendSwatchProps { /** Shape to render. Default `'circle'`. */ shape?: LegendSwatchShape /** CSS color string (hex / rgb / named). */ color: string /** Paint the outline/border instead of the fill (square/circle only). */ isStrokeColor?: boolean /** Fill pattern when filled. Default `'solid'`. */ fillPattern?: LegendFillPattern /** Border style (stroked square/circle) or line style (`shape: 'line'`). Default `'solid'`. */ strokeStyle?: LegendStrokeStyle /** Primary dimension in px (square side / circle diameter / line length). Default 12. */ size?: number } /** Default swatch dimension in px — shared with the category renderer. */ export const DEFAULT_SWATCH_SIZE = 12 const STROKE_WIDTH = 1.5 const LINE_WIDTH = 2 const HAIRLINE = 1 /** Dash array for stroked square/circle borders. */ function borderDash(style: LegendStrokeStyle): { dash?: string round?: boolean } { switch (style) { case 'dashed': return { dash: '3 2' } case 'dotted': return { dash: `0.1 ${STROKE_WIDTH * 2}`, round: true } // `dash-dot` is line-only and `corners` is handled separately: both // degrade to a solid border here. default: return {} } } /** Dash array for `shape: 'line'`. */ function lineDash(style: LegendStrokeStyle): { dash?: string round?: boolean } { switch (style) { case 'dashed': return { dash: '4 3' } case 'dotted': return { dash: `0.1 ${LINE_WIDTH * 2}`, round: true } case 'dash-dot': return { dash: '5 2 0.1 2', round: true } // `corners` is square-only → solid line. default: return {} } } /** SVG children for a fill `` tile, drawn in `color`. */ function patternTile(pattern: LegendFillPattern, color: string) { const line = (d: string) => ( ) switch (pattern) { case 'horizontal': return { size: 4, content: line('M0 2 H4') } case 'vertical': return { size: 4, content: line('M2 0 V4') } case 'diagonal': // "/" with wrap segments for a seamless tile. return { size: 4, content: line('M0 4 L4 0 M-1 1 L1 -1 M3 5 L5 3') } case 'diagonal-alt': // "\" return { size: 4, content: line('M0 0 L4 4 M-1 3 L1 5 M3 -1 L5 1') } case 'cross': return { size: 4, content: line( 'M0 4 L4 0 M-1 1 L1 -1 M3 5 L5 3 M0 0 L4 4 M-1 3 L1 5 M3 -1 L5 1', ), } case 'dots': return { size: 4, content: } case 'grid': return { size: 3, content: , } default: return null } } /** * A single legend swatch rendered as inline SVG: a square, circle, or line, * painted as a solid/patterned fill or a styled outline. Pure and theme-free — * `color` is a raw CSS color. Decorative (`aria-hidden`); the adjacent label * carries the accessible text. * * @experimental This API is new and may change in a future release. */ export function LegendSwatch({ shape = 'circle', color, isStrokeColor = false, fillPattern = 'solid', strokeStyle = 'solid', size = DEFAULT_SWATCH_SIZE, }: LegendSwatchProps) { const patternId = useId() // ── Line ────────────────────────────────────────────────────────────────── if (shape === 'line') { const { dash, round } = lineDash(strokeStyle) return ( ) } // ── Square / circle ──────────────────────────────────────────────────────── const inset = isStrokeColor ? STROKE_WIDTH / 2 : 0 const usePattern = !isStrokeColor && fillPattern !== 'solid' const tile = usePattern ? patternTile(fillPattern, color) : null // corner-marks only apply to a stroked square. const isCorners = isStrokeColor && strokeStyle === 'corners' && shape === 'square' const { dash, round } = isStrokeColor ? borderDash(strokeStyle) : { dash: undefined, round: false } const commonStroke = { stroke: color, strokeWidth: isStrokeColor ? STROKE_WIDTH : HAIRLINE, strokeDasharray: isStrokeColor ? dash : undefined, strokeLinecap: round ? ('round' as const) : ('butt' as const), } const fill = isStrokeColor ? 'none' : usePattern ? `url(#${patternId})` : color return ( {tile && ( {tile.content} )} {isCorners ? ( ) : shape === 'circle' ? ( ) : ( )} ) } /** Four L-shaped corner brackets for the `corners` border style (square). */ function CornerMarks({ size, color }: { size: number; color: string }) { const arm = Math.max(2, size * 0.3) const o = STROKE_WIDTH / 2 const max = size - o const props = { stroke: color, strokeWidth: STROKE_WIDTH, fill: 'none', } return ( <> ) }