Files
hermes-agent/apps/desktop/src/components/ui/progress.tsx
T
Brooklyn Nicholson e4b2b77852 refactor(desktop): extract shared Progress primitive; billing uses it
Add components/ui/progress.tsx — one rounded track + animated fill that owns
role="progressbar" + aria. Migrate the hand-rolled bars (updates overlay,
onboarding, install overlay, billing usage, pet hatch) onto it.

- Pet's bar was never a color variant (--primary and --ui-accent are the same
  brand color); its only real difference is the sliding indeterminate, now an
  `animated` prop. Color stays a normal `fillClassName` override.
- Billing usage now uses the plain primitive — dropped the bespoke dither
  track, inset shadow, and danger nub; tone rides `destructive`/`fillClassName`.
- Drop the redundant emoji "no saved card" row description; the page-level
  warn banner is the single explainer.
- Rename pet CSS .pet-progress* -> generic .progress-slide.
2026-07-22 18:53:26 -05:00

83 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react'
import { cn } from '@/lib/utils'
const TRACK_HEIGHT = {
sm: 'h-1',
default: 'h-1.5',
lg: 'h-2'
} as const
export interface ProgressProps extends Omit<React.ComponentProps<'div'>, 'children'> {
/** Completion as a 0–1 fraction; clamped. Ignored when `indeterminate`. */
value?: number
/** No known endpoint — animate instead of showing a fixed width. */
indeterminate?: boolean
/**
* Use the continuous sliding animation for the indeterminate state (the pet
* hatch look) instead of the default pulse. No effect when determinate.
*/
animated?: boolean
/** Swap the fill to the destructive color for error / over-limit states. */
destructive?: boolean
size?: keyof typeof TRACK_HEIGHT
/** Override the fill color/shape (e.g. billing's tone mapping, pet's accent). */
fillClassName?: string
fillStyle?: React.CSSProperties
/** Extra content inside the track, behind the fill (e.g. a threshold marker). */
children?: React.ReactNode
}
/**
* The app's one progress/meter bar: a rounded track with an animated fill. The
* track owns `role="progressbar"` and its aria values; the fill is width-driven
* (determinate) or animated (indeterminate). Consumers needing a bespoke look —
* billing's hatched empty state, tone-mapped or accent fills — override
* `className` (track) and `fillClassName` while inheriting the structure,
* sizing, and accessibility.
*/
export function Progress({
value = 0,
indeterminate = false,
animated = false,
destructive = false,
size = 'default',
className,
fillClassName,
fillStyle,
children,
...props
}: ProgressProps) {
const pct = Math.round(Math.min(1, Math.max(0, value)) * 100)
const fillColor = destructive ? 'bg-destructive' : 'bg-primary'
return (
<div
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={indeterminate ? undefined : pct}
className={cn('relative w-full overflow-hidden rounded-full bg-muted', TRACK_HEIGHT[size], className)}
data-slot="progress"
role="progressbar"
{...props}
>
{children}
{indeterminate && animated ? (
// Sliding block — width/animation come from `.progress-slide` (styles.css),
// which also honors prefers-reduced-motion.
<div className={cn('progress-slide absolute inset-y-0 rounded-full', fillColor, fillClassName)} />
) : (
<div
className={cn(
'relative h-full rounded-full transition-[width] duration-300 ease-out',
fillColor,
indeterminate && 'w-1/3 animate-pulse',
fillClassName
)}
style={indeterminate ? fillStyle : { width: `${pct}%`, ...fillStyle }}
/>
)}
</div>
)
}