import { createSignal, onFrame, Show } from "@solidrt/core" import type { LayoutProps } from "@solidrt/core" import { theme } from "./theme" import { policy } from "./policy" import type { StyleProps } from "./types" export interface SpinnerProps { // Overall diameter in pixels. size?: number // Arc stroke width in pixels. thickness?: number // Revolutions per second. speed?: number layout?: LayoutProps style?: StyleProps } const SIZE = 24 const THICKNESS = 3 // An indeterminate spinner: a 270-degree arc that rotates continuously, driven // by core onFrame (so it participates in demand-driven rendering and stops when // unmounted). Color comes from the theme; override via style.color. export function Spinner(props: SpinnerProps) { let size = () => props.size ?? SIZE let thickness = () => props.thickness ?? THICKNESS // Theme-level per-component overrides merged under the instance style. let styled = () => ({ ...theme.components.spinner, ...props.style }) let color = () => styled().color ?? theme.color.primary let speed = () => props.speed ?? 1 // tick is in milliseconds (like performance.now()). The frame loop is mounted // through the below so the motion policy can unmount it: onFrame holds // a standing frame request while registered, so a check inside the callback // would still keep the renderer free-running. Under "none" the arc freezes; // "reduced" halves the spin. let [angle, setAngle] = createSignal(0) let Animate = () => { onFrame((tick) => setAngle((tick / 1000) * speed() * (policy.motion === "reduced" ? 0.5 : 1) * Math.PI * 2), ) return null } // A 270-degree arc starting at the top, sweeping clockwise. Coordinates are in // the parent box space, so the wrapping view is sized to match. let path = () => { let s = size() let r = (s - thickness()) / 2 let c = s / 2 return `M ${c} ${c - r} A ${r} ${r} 0 1 1 ${c - r} ${c}` } return ( ) }