diff --git a/openhands-ui/components/spinner/Spinner.stories.tsx b/openhands-ui/components/spinner/Spinner.stories.tsx new file mode 100644 index 0000000000..aa9c9c120a --- /dev/null +++ b/openhands-ui/components/spinner/Spinner.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Spinner } from "./Spinner"; +import { useEffect, useState } from "react"; + +const meta = { + title: "Components/Spinner", + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const DeterminateSpinner = () => { + const [percentage, setPercentage] = useState(10); + + useEffect(() => { + setTimeout(() => setPercentage(Math.min(100, percentage + 30)), 600); + }, [percentage]); + return ; +}; + +export const Determinate: Story = { + render: () => , +}; + +export const Indeterminate: Story = { + render: () => , +}; diff --git a/openhands-ui/components/spinner/Spinner.tsx b/openhands-ui/components/spinner/Spinner.tsx new file mode 100644 index 0000000000..0e3c16d368 --- /dev/null +++ b/openhands-ui/components/spinner/Spinner.tsx @@ -0,0 +1,66 @@ +import { useMemo } from "react"; +import type { HTMLProps } from "../../shared/types"; +import { cn } from "../../shared/utils/cn"; +import "./index.css"; + +type BaseSpinnerProps = HTMLProps<"svg">; + +export type DeterminateSpinnerProps = BaseSpinnerProps & { + determinate: true; + value: number; +}; + +export type IndeterminateSpinnerProps = BaseSpinnerProps & { + determinate?: false | null | undefined; + value?: never; +}; + +export type SpinnerProps = DeterminateSpinnerProps | IndeterminateSpinnerProps; + +const SIZE = 48; +const STROKE_WIDTH = 6; +const radius = (SIZE - STROKE_WIDTH) / 2; +const circumference = 2 * Math.PI * radius; + +export const Spinner = ({ + value = 10, + determinate = false, + className, + ...props +}: SpinnerProps) => { + const offset = useMemo( + () => circumference - (value / 100) * circumference, + [value] + ); + + return ( + + + + + + + ); +}; diff --git a/openhands-ui/components/spinner/index.css b/openhands-ui/components/spinner/index.css new file mode 100644 index 0000000000..659a7f3e81 --- /dev/null +++ b/openhands-ui/components/spinner/index.css @@ -0,0 +1,18 @@ +@layer utilities { + @keyframes spinner-animate { + 0% { + transform: rotate(-90deg); + } + 100% { + transform: rotate(270deg); + } + } + + .animate-indeterminate-spinner { + animation: spinner-animate 2s linear infinite; + } + + .animate-determinate-spinner { + transition: stroke-dashoffset 0.3s ease; + } +} \ No newline at end of file