64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
// StateDot: session state indicator (figma nodes 14:3303/3305/3312, 122:9182).
|
|
// done/warning/error: 10x10 halo (same color, 10% opacity) around a 6x6 solid
|
|
// core. ongoing: a pixel-art chase — the 8 outer cells of a 3x3 matrix light
|
|
// up clockwise with a stepped trail. Colors resolve through --dsw-* tokens only.
|
|
|
|
import clsx from 'clsx'
|
|
import css from './StateDot.module.css'
|
|
|
|
/** Four-color state semantic (green done / amber user-attention / blue running ring / red error). */
|
|
export type StateDotState = 'done' | 'warning' | 'ongoing' | 'error'
|
|
|
|
/** Outer 3x3 matrix cells (2px pixels on a 10px grid), clockwise from top-left. */
|
|
const MATRIX_CELLS: readonly (readonly [number, number])[] = [
|
|
[0, 0], [4, 0], [8, 0], [8, 4], [8, 8], [4, 8], [0, 8], [0, 4],
|
|
]
|
|
|
|
/**
|
|
* Render a state dot.
|
|
* @param props.state - which of the four states to show.
|
|
* @param props.size - outer diameter in px (default 10, the figma size).
|
|
* @param props.className - extra class for layout placement.
|
|
* @returns the dot element (aria-hidden; pair with text for accessibility).
|
|
*/
|
|
export function StateDot({ state, size = 10, className }: {
|
|
state: StateDotState
|
|
size?: number | undefined
|
|
className?: string | undefined
|
|
}) {
|
|
if (state === 'ongoing') {
|
|
return (
|
|
<svg
|
|
className={clsx(css.matrix, className)}
|
|
data-state="ongoing"
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 10 10"
|
|
shapeRendering="crispEdges"
|
|
aria-hidden="true"
|
|
>
|
|
{MATRIX_CELLS.map(([x, y], index) => (
|
|
<rect
|
|
key={`${x}-${y}`}
|
|
className={css.cell}
|
|
x={x}
|
|
y={y}
|
|
width="2"
|
|
height="2"
|
|
/* Negative delay phases the chase so every cell animates from mount. */
|
|
style={{ animationDelay: `${(index - MATRIX_CELLS.length) * 125}ms` }}
|
|
/>
|
|
))}
|
|
</svg>
|
|
)
|
|
}
|
|
return (
|
|
<span
|
|
className={clsx(css.dot, className)}
|
|
data-state={state}
|
|
style={{ width: size, height: size }}
|
|
aria-hidden="true"
|
|
/>
|
|
)
|
|
}
|