import { createSignal, onFrame, onLayout, getBoundingBox, 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 ProgressBarProps { // Progress from 0 to 1. Omit (or leave undefined) for an indeterminate bar: a // segment that slides back and forth. value?: number layout?: LayoutProps style?: StyleProps } const HEIGHT = 6 // Indeterminate: fraction of the track the sliding segment occupies, and its // travel speed in track-widths per second. const SEGMENT = 0.3 const SPEED = 0.8 let clamp = (x: number, lo: number, hi: number) => (x < lo ? lo : x > hi ? hi : x) // A horizontal progress bar. Determinate when given a value in [0, 1]: the fill // grows from the left. Indeterminate when value is undefined: a short segment // slides back and forth (driven by core onFrame). Colors come from the theme; // override the track via style.backgroundColor and the fill via style.color. export function ProgressBar(props: ProgressBarProps) { let h = () => (props.layout?.height as number) ?? HEIGHT let radius = () => h() / 2 // Theme-level per-component overrides merged under the instance style. let styled = () => ({ ...theme.components.progressBar, ...props.style }) let track = () => styled().backgroundColor ?? theme.color.surfaceAlt let fill = () => styled().color ?? theme.color.primary let indeterminate = () => props.value === undefined // Measured track width in pixels. Both the determinate fill and the // indeterminate segment are drawn as a detached d-rect sized in pixels (paint // only) rather than animating a percentage `width`, which would reflow taffy // whenever the value changes. Refreshed each layout so it tracks resizes. let trackNode: { id: number } | undefined let [trackWidth, setTrackWidth] = createSignal(0) onLayout(() => { if (!trackNode) return setTrackWidth(getBoundingBox(trackNode)?.width ?? 0) }) // tick is in milliseconds (like performance.now()). The frame loop is mounted // through the below only while indeterminate and the motion policy // allows it: onFrame holds a standing frame request while registered, so a // check inside the callback would keep the renderer free-running (which is // also why a determinate bar must not register it at all). "reduced" halves // the travel speed; under "none" the segment parks centered (phase 0.5). let [phase, setPhase] = createSignal(0) let animating = () => indeterminate() && policy.motion !== "none" let Animate = () => { onFrame((tick) => { // Triangle wave in [0, 1]: the segment travels left edge -> right edge // and back. let t = ((tick / 1000) * (policy.motion === "reduced" ? SPEED / 2 : SPEED)) % 2 setPhase(t > 1 ? 2 - t : t) }) return null } // Fill width in pixels: a fixed segment while indeterminate, the value fraction // otherwise. Drawn via the detached d-rect `w` below (paint only), so a // changing value never reflows. The indeterminate slide comes from the `x` // offset. let fillWidth = () => (indeterminate() ? SEGMENT : clamp(props.value ?? 0, 0, 1)) * trackWidth() let effectivePhase = () => (policy.motion === "none" ? 0.5 : phase()) let offset = () => effectivePhase() * trackWidth() * (1 - SEGMENT) return ( (trackNode = n)} position="relative" width="100%" height={h()} {...props.layout}> ) }