import { type JSX, Show, splitProps, createUniqueId } from 'solid-js'; import { cn } from '../utils/cn'; export type ProgressTone = 'primary' | 'success' | 'warning' | 'error' | 'info'; /** Fill hue per `tone`. `primary` is the theme accent; the rest use the kit's * tool hues (the same colors as kai-status / kai-notice). */ export const TONE_FILL: Record = { primary: 'bg-primary', success: 'bg-tool-green', warning: 'bg-tool-amber', error: 'bg-tool-red', info: 'bg-tool-blue', }; export interface ProgressBarProps extends JSX.HTMLAttributes { /** Current progress value (0..max). */ value: number; /** The value `value` runs to. Defaults to 100. */ max?: number; /** Optional caption above the track. */ label?: string; /** Fill color. Defaults to `primary`. Override via `::part(fill)` for custom hues. */ tone?: ProgressTone; } /** * A thin determinate progress bar: a rounded track with a `bg-primary` fill whose * width is `value / max`, clamped to 0..100%. Theme-aware (track = `bg-surface-strong`, * fill = `bg-primary`). Set `tone` for a semantic fill hue. Restyle via * `::part(track)` / `::part(fill)`. The fill width transitions smoothly under * `prefers-reduced-motion: no-preference`. */ export function ProgressBar(props: ProgressBarProps) { const [local, rest] = splitProps(props, [ 'value', 'max', 'label', 'tone', 'class', 'aria-label', 'aria-labelledby', ]); const labelId = createUniqueId(); const max = () => local.max ?? 100; // Clamp value into [0, max]; drive both the fill width and aria-valuenow off it // so the bar and the announced value stay consistent. const clamped = () => { const m = max(); if (!(m > 0)) return 0; return Math.max(0, Math.min(local.value, m)); }; const percent = () => { const m = max(); return m > 0 ? (clamped() / m) * 100 : 0; }; // The progressbar must always carry an accessible name. Prefer the rendered // visible label; else a caller-supplied aria-labelledby/aria-label; else a // generic default so the bar is never nameless (axe `aria-progressbar-name`). const labelledBy = () => (local.label ? labelId : local['aria-labelledby']); const ariaLabel = () => local.label || local['aria-labelledby'] ? undefined : local['aria-label'] ?? 'Progress'; return (
{local.label}
); }