diff --git a/packages/propel/src/components/progress/index.tsx b/packages/propel/src/components/progress/index.tsx index f06ae21f..c793eea6 100644 --- a/packages/propel/src/components/progress/index.tsx +++ b/packages/propel/src/components/progress/index.tsx @@ -2,5 +2,6 @@ export { Progress, type ProgressProps, type ProgressMagnitude, + type ProgressTone, type ProgressVariant, } from "./progress"; diff --git a/packages/propel/src/components/progress/progress.stories.tsx b/packages/propel/src/components/progress/progress.stories.tsx index aabb5bd9..b24e545e 100644 --- a/packages/propel/src/components/progress/progress.stories.tsx +++ b/packages/propel/src/components/progress/progress.stories.tsx @@ -6,7 +6,13 @@ import { Progress } from "./index"; const meta = { title: "Components/Progress", component: Progress, - args: { variant: "linear", value: 32, magnitude: "md", "aria-label": "Upload progress" }, + args: { + variant: "linear", + value: 32, + magnitude: "md", + tone: "brand", + "aria-label": "Upload progress", + }, // Give the bar a width to fill (it grows to its container). decorators: [ (Story) => ( @@ -34,8 +40,20 @@ export const Magnitudes: Story = { parameters: { controls: { disable: true } }, render: () => (
- - + +
), }; @@ -45,24 +63,83 @@ export const Values: Story = { parameters: { controls: { disable: true } }, render: () => (
- - - + + +
), }; +/** All four sentiment tones (`brand` / `success` / `warning` / `danger`). */ +export const Tones: Story = { + parameters: { controls: { disable: true } }, + render: () => ( +
+ + + + +
+ ), +}; + +/** + * Indeterminate mode — pass `value={null}` when the end time is unknown (e.g. an in-flight network + * request). The bar slides a shorter fill back and forth. `showValue` is automatically hidden since + * there is no percentage to display. + */ +export const Indeterminate: Story = { + args: { + variant: "linear", + value: null, + magnitude: "md", + tone: "brand", + showValue: false, + "aria-label": "Loading", + }, +}; + /** Hide the trailing label with `showValue={false}` (e.g. a slim inline track). */ export const WithoutLabel: Story = { - args: { variant: "linear", value: 64, magnitude: "md", showValue: false }, + args: { variant: "linear", value: 64, magnitude: "md", tone: "brand", showValue: false }, }; /** - * `variant="circular"` is a small determinate ring — a gray track plus an accent arc proportional - * to the value, with no label. The arc starts at 12 o'clock and sweeps clockwise. + * `variant="circular"` is a small determinate ring — a gray track plus a toned arc proportional to + * the value, with no label. The arc starts at 12 o'clock and sweeps clockwise. */ export const Circular: Story = { - args: { variant: "circular", value: 32, magnitude: "md", "aria-label": "Sync progress" }, + args: { + variant: "circular", + value: 32, + magnitude: "md", + tone: "brand", + "aria-label": "Sync progress", + }, // The ring sizes itself; the meta's 80-wide wrapper isn't needed. }; @@ -71,12 +148,48 @@ export const CircularMagnitudes: Story = { parameters: { controls: { disable: true } }, render: () => (
- - - - - - + + + + + +
), }; diff --git a/packages/propel/src/components/progress/progress.tsx b/packages/propel/src/components/progress/progress.tsx index c1b509b5..a84d2cce 100644 --- a/packages/propel/src/components/progress/progress.tsx +++ b/packages/propel/src/components/progress/progress.tsx @@ -3,33 +3,56 @@ import { Progress as BaseProgress } from "@base-ui/react/progress"; import { Progress as ProgressRoot, ProgressCircle, + ProgressCircleIndicator, + ProgressCircleSvg, + ProgressCircleTrack, ProgressIndicator, ProgressTrack, ProgressValue, + type ProgressIndicatorProps, type ProgressTrackProps, } from "../../ui/progress"; // The Figma "Progress" component has two variants: -// - linear (node 1990-51): a pill-shaped track (`layer-3-selected`) with an -// accent-filled indicator and an optional trailing percentage label -// (`text/accent/primary`, 12px medium). `magnitude` only changes the track -// thickness — `sm` 5px, `md` 8px. +// - linear (node 1990-51): a pill-shaped track (`layer-3-selected`) with a toned indicator +// and an optional trailing percentage label (12px medium). `magnitude` only changes the +// track thickness — `sm` 5px, `md` 8px. // - circular (node 5736-3457): a small determinate ring — a subtle track circle -// (`layer-3-selected`, the same surface the linear track uses) plus an accent arc -// (`background/accent/primary`) proportional to the value, with rounded caps and no -// label. `magnitude` changes the diameter — `sm` 16px, `md` 20px. +// (`layer-3-selected`) plus a toned arc proportional to the value, with rounded caps and +// no label. `magnitude` changes the diameter — `sm` 16px, `md` 20px. // Both are built on Base UI `Progress`, which owns the `progressbar` role + // `aria-valuenow` for us. export type ProgressMagnitude = NonNullable; +export type ProgressTone = NonNullable; export type ProgressVariant = "linear" | "circular"; +// Circle geometry per magnitude (value model, not styling). The Figma circular variant +// (nodes 5736-3457 / 5736-3460) is a 16px (sm) / 20px (md) box holding a 2px-stroke ring: +// a 14px circle (sm) or 18px circle (md). The centerline radius is therefore +// (diameter - stroke) / 2 -> 6 (sm) / 8 (md). The viewBox matches the box so 1 SVG user +// unit == 1px. +const RING_GEOMETRY: Record = { + sm: { box: 16, radius: 6 }, + md: { box: 20, radius: 8 }, +}; +const RING_STROKE = 2; + export type ProgressProps = Omit & { /** `linear` = a horizontal bar. `circular` = a determinate ring. */ variant: ProgressVariant; - /** Completion from 0 to `max` (default max 100). */ - value: number; + /** + * Completion from 0 to `max` (default max 100). Pass `null` for indeterminate mode — the bar + * animates a sliding fill and `aria-valuenow` is unset. Only applies to `variant="linear"`; + * `circular` always requires a number. + */ + value: number | null; /** `linear`: track thickness (`sm` 5px / `md` 8px). `circular`: diameter (`sm` 16px / `md` 20px). */ magnitude: ProgressMagnitude; + /** + * Fill color for the indicator (and arc). Maps to semantic signal: `brand` is the default accent, + * `success`/`warning`/`danger` encode task outcome. + */ + tone: ProgressTone; /** * Show the trailing percentage label. Only applies to `variant="linear"` — the circular rings are * too small for a label, so this is ignored when `circular`. @@ -53,13 +76,50 @@ export type ProgressProps = Omit; + const { box, radius } = RING_GEOMETRY[magnitude]; + const circumference = 2 * Math.PI * radius; + const max = props.max ?? 100; + // Clamp once and feed the same value to the SVG arc and the Root, so the arc and + // `aria-valuenow` never disagree for out-of-range input. Indeterminate mode is a + // linear-only feature; fall back to 0 for null so the ring renders empty. + const clampedValue = Math.min(Math.max(value ?? 0, 0), max); + const fraction = max > 0 ? clampedValue / max : 0; + const dashOffset = circumference * (1 - fraction); + const center = box / 2; + return ( + + + + + + + ); } return ( @@ -67,7 +127,7 @@ export function Progress({ variant, value, magnitude, showValue = true, ...props {/* Base UI sets the indicator's `width` (and `inset-inline-start: 0`) from the value; the ui primitive owns its fill, pill radius, and the fill transition. */} - + {showValue ? ( diff --git a/packages/propel/src/components/toast/toast.tsx b/packages/propel/src/components/toast/toast.tsx index 24844b66..edddb009 100644 --- a/packages/propel/src/components/toast/toast.tsx +++ b/packages/propel/src/components/toast/toast.tsx @@ -113,6 +113,7 @@ export function Toast({ toast, ...props }: ToastProps) { variant="linear" value={data.progress} magnitude="sm" + tone="brand" aria-label={typeof toast.title === "string" && toast.title ? toast.title : "Progress"} /> ) : null} diff --git a/packages/propel/src/styles/animations.css b/packages/propel/src/styles/animations.css index 95f679d8..69346926 100644 --- a/packages/propel/src/styles/animations.css +++ b/packages/propel/src/styles/animations.css @@ -174,6 +174,20 @@ } } + /* progress indeterminate: slide a 1/3-width bar across the track and back */ + --animate-progress-indeterminate: progress-indeterminate 1.4s ease-in-out infinite; + @keyframes progress-indeterminate { + 0% { + inset-inline-start: -33.333%; + } + 50% { + inset-inline-start: 100%; + } + 100% { + inset-inline-start: -33.333%; + } + } + /* this '--animate-notification-bell-ring' used for workspace notifications for reminders */ --animate-notification-bell-ring: notification-bell-ring 2.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; diff --git a/packages/propel/src/ui/progress/index.tsx b/packages/propel/src/ui/progress/index.tsx index c319b7b9..171ca428 100644 --- a/packages/propel/src/ui/progress/index.tsx +++ b/packages/propel/src/ui/progress/index.tsx @@ -1,5 +1,11 @@ export * from "./progress"; export { ProgressCircle, type ProgressCircleProps } from "./progress-circle"; +export { + ProgressCircleIndicator, + type ProgressCircleIndicatorProps, +} from "./progress-circle-indicator"; +export { ProgressCircleSvg, type ProgressCircleSvgProps } from "./progress-circle-svg"; +export { ProgressCircleTrack, type ProgressCircleTrackProps } from "./progress-circle-track"; export { ProgressIndicator, type ProgressIndicatorProps } from "./progress-indicator"; export { ProgressLabel, type ProgressLabelProps } from "./progress-label"; export { ProgressTrack, type ProgressTrackProps } from "./progress-track"; diff --git a/packages/propel/src/ui/progress/progress-circle-indicator.tsx b/packages/propel/src/ui/progress/progress-circle-indicator.tsx new file mode 100644 index 00000000..f95727b4 --- /dev/null +++ b/packages/propel/src/ui/progress/progress-circle-indicator.tsx @@ -0,0 +1,20 @@ +import { type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { progressCircleIndicatorVariants } from "./variants"; + +/** Props for {@link ProgressCircleIndicator}. */ +export type ProgressCircleIndicatorProps = Omit< + React.ComponentPropsWithoutRef<"circle">, + "className" | "style" +> & + Required, "tone">>; + +/** + * The toned arc proportional to the value (the circular analogue of `ProgressIndicator`). `tone` + * drives the stroke color. Pass the geometry (`cx` / `cy` / `r` / `strokeWidth` / `strokeDasharray` + * / `strokeDashoffset` / `strokeLinecap`) as SVG attributes. + */ +export function ProgressCircleIndicator({ tone, ...props }: ProgressCircleIndicatorProps) { + return ; +} diff --git a/packages/propel/src/ui/progress/progress-circle-svg.tsx b/packages/propel/src/ui/progress/progress-circle-svg.tsx new file mode 100644 index 00000000..4cbd16b7 --- /dev/null +++ b/packages/propel/src/ui/progress/progress-circle-svg.tsx @@ -0,0 +1,19 @@ +import type * as React from "react"; + +import { progressCircleSvgVariants } from "./variants"; + +/** Props for {@link ProgressCircleSvg}. */ +export type ProgressCircleSvgProps = Omit< + React.ComponentPropsWithoutRef<"svg">, + "className" | "style" +>; + +/** + * The SVG viewport of the circular ring. Fills its `ProgressCircle` box and holds the + * `ProgressCircleTrack` + `ProgressCircleIndicator` circles. Decorative (the `ProgressCircle` root + * carries the `progressbar` a11y), so it is `aria-hidden`. Pass a `viewBox` matching the ring box + * so one SVG user unit equals one pixel. + */ +export function ProgressCircleSvg(props: ProgressCircleSvgProps) { + return ; +} diff --git a/packages/propel/src/ui/progress/progress-circle-track.tsx b/packages/propel/src/ui/progress/progress-circle-track.tsx new file mode 100644 index 00000000..f31d19d2 --- /dev/null +++ b/packages/propel/src/ui/progress/progress-circle-track.tsx @@ -0,0 +1,18 @@ +import type * as React from "react"; + +import { progressCircleTrackVariants } from "./variants"; + +/** Props for {@link ProgressCircleTrack}. */ +export type ProgressCircleTrackProps = Omit< + React.ComponentPropsWithoutRef<"circle">, + "className" | "style" +>; + +/** + * The full subtle ring behind the arc (the circular analogue of `ProgressTrack`). Pass the geometry + * (`cx` / `cy` / `r` / `strokeWidth`) as SVG attributes; the low-emphasis stroke color is baked + * in. + */ +export function ProgressCircleTrack(props: ProgressCircleTrackProps) { + return ; +} diff --git a/packages/propel/src/ui/progress/progress-circle.tsx b/packages/propel/src/ui/progress/progress-circle.tsx index 390db75e..814daf79 100644 --- a/packages/propel/src/ui/progress/progress-circle.tsx +++ b/packages/propel/src/ui/progress/progress-circle.tsx @@ -1,83 +1,17 @@ import { Progress as BaseProgress } from "@base-ui/react/progress"; import { type VariantProps } from "class-variance-authority"; -import { ringVariants } from "./variants"; - -// Circle geometry per magnitude. The Figma circular variant (nodes 5736-3457 / 5736-3460) -// is a 16px (sm) / 20px (md) box holding a 2px-stroke ring: a 14px circle (sm) or 18px -// circle (md). The centerline radius is therefore (diameter - stroke) / 2 -> 6 (sm) / 8 -// (md). The viewBox matches the box so 1 SVG user unit == 1px. -const RING_GEOMETRY = { - sm: { box: 16, radius: 6 }, - md: { box: 20, radius: 8 }, -} as const; -const RING_STROKE = 2; - -type RingMagnitude = NonNullable["magnitude"]>; +import { progressCircleVariants } from "./variants"; /** Props for {@link ProgressCircle}. */ -export type ProgressCircleProps = Omit & { - /** Completion from 0 to `max` (default max 100). */ - value: number; - /** Diameter (`sm` 16px / `md` 20px). */ - magnitude: RingMagnitude; - /** Accessible name for the ring. */ - "aria-label": string; -}; +export type ProgressCircleProps = Omit & + Required, "magnitude">>; /** - * The atomic determinate progress ring — a styled `Progress.Root` wrapping an SVG: a subtle track - * circle (`layer-3-selected`) plus an accent arc (`accent-primary`) proportional to the value, with - * rounded caps. `magnitude` changes the diameter (`sm` 16px / `md` 20px). Base UI `Progress.Root` - * owns the `progressbar` role + `aria-valuenow`. + * The circular ring root — a styled Base UI `Progress.Root` that sizes the ring box (`magnitude` + * `sm` 16px / `md` 20px) and owns the `progressbar` role + `aria-valuenow`. Compose a + * `ProgressCircleSvg` (holding `ProgressCircleTrack` + `ProgressCircleIndicator`) inside it. */ -export function ProgressCircle({ value, magnitude, ...props }: ProgressCircleProps) { - const { box, radius } = RING_GEOMETRY[magnitude]; - const circumference = 2 * Math.PI * radius; - const max = props.max ?? 100; - // Clamp once and feed the same value to the SVG arc and the Root, so the arc - // and `aria-valuenow` never disagree for out-of-range input. - const clampedValue = Math.min(Math.max(value, 0), max); - const fraction = max > 0 ? clampedValue / max : 0; - const dashOffset = circumference * (1 - fraction); - const center = box / 2; - return ( - - - - ); +export function ProgressCircle({ magnitude, ...props }: ProgressCircleProps) { + return ; } diff --git a/packages/propel/src/ui/progress/progress-indicator.tsx b/packages/propel/src/ui/progress/progress-indicator.tsx index 802ad4ae..44a07d73 100644 --- a/packages/propel/src/ui/progress/progress-indicator.tsx +++ b/packages/propel/src/ui/progress/progress-indicator.tsx @@ -1,11 +1,13 @@ import { Progress as BaseProgress } from "@base-ui/react/progress"; +import { type VariantProps } from "class-variance-authority"; import { progressIndicatorVariants } from "./variants"; /** Props for {@link ProgressIndicator}; 1:1 with Base UI `Progress.Indicator`. */ -export type ProgressIndicatorProps = Omit; +export type ProgressIndicatorProps = Omit & + Required, "tone">>; -/** 1:1 wrapper around Base UI `Progress.Indicator`. */ -export function ProgressIndicator(props: ProgressIndicatorProps) { - return ; +/** 1:1 wrapper around Base UI `Progress.Indicator`. The `tone` drives the fill color. */ +export function ProgressIndicator({ tone, ...props }: ProgressIndicatorProps) { + return ; } diff --git a/packages/propel/src/ui/progress/progress-track.tsx b/packages/propel/src/ui/progress/progress-track.tsx index fecc910c..2f70e39c 100644 --- a/packages/propel/src/ui/progress/progress-track.tsx +++ b/packages/propel/src/ui/progress/progress-track.tsx @@ -1,16 +1,16 @@ import { Progress as BaseProgress } from "@base-ui/react/progress"; import { type VariantProps } from "class-variance-authority"; -import { trackVariants } from "./variants"; +import { progressTrackVariants } from "./variants"; /** Props for {@link ProgressTrack}; 1:1 with Base UI `Progress.Track`. */ export type ProgressTrackProps = Omit & - VariantProps; + VariantProps; /** * 1:1 wrapper around Base UI `Progress.Track`. `magnitude` drives the track thickness (`sm` 5px / * `md` 8px). */ export function ProgressTrack({ magnitude, ...props }: ProgressTrackProps) { - return ; + return ; } diff --git a/packages/propel/src/ui/progress/progress-value.tsx b/packages/propel/src/ui/progress/progress-value.tsx index 6741a77a..dc549870 100644 --- a/packages/propel/src/ui/progress/progress-value.tsx +++ b/packages/propel/src/ui/progress/progress-value.tsx @@ -5,7 +5,10 @@ import { progressValueVariants } from "./variants"; /** Props for {@link ProgressValue}; 1:1 with Base UI `Progress.Value`. */ export type ProgressValueProps = Omit; -/** 1:1 wrapper around Base UI `Progress.Value`. */ +/** + * 1:1 wrapper around Base UI `Progress.Value`. Renders the percentage as a neutral readout; the + * semantic tone lives on the fill, not the number. + */ export function ProgressValue(props: ProgressValueProps) { return ; } diff --git a/packages/propel/src/ui/progress/progress.stories.tsx b/packages/propel/src/ui/progress/progress.stories.tsx index 8417c981..cb619f00 100644 --- a/packages/propel/src/ui/progress/progress.stories.tsx +++ b/packages/propel/src/ui/progress/progress.stories.tsx @@ -4,6 +4,9 @@ import { expect } from "storybook/test"; import { Progress, ProgressCircle, + ProgressCircleIndicator, + ProgressCircleSvg, + ProgressCircleTrack, ProgressIndicator, ProgressLabel, ProgressTrack, @@ -12,13 +15,23 @@ import { // UI-tier story: composes the ATOMIC progress parts. `Progress` (Base UI `Progress.Root`) // owns the `progressbar` value model; `ProgressTrack` › `ProgressIndicator` paint the bar, -// `ProgressLabel`/`ProgressValue` the accessible name + trailing text. `ProgressCircle` is -// the atomic determinate ring. The ready-made linear+circular `Progress` (magnitude, the -// trailing `%`) lives in `components/progress`. +// `ProgressLabel`/`ProgressValue` the accessible name + trailing text. The circular ring is +// `ProgressCircle` › `ProgressCircleSvg` › `ProgressCircleTrack` + `ProgressCircleIndicator`. +// The ready-made linear+circular `Progress` (magnitude, the trailing `%`) lives in +// `components/progress`. const meta = { title: "UI/Progress", component: Progress, - subcomponents: { ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue, ProgressCircle }, + subcomponents: { + ProgressTrack, + ProgressIndicator, + ProgressLabel, + ProgressValue, + ProgressCircle, + ProgressCircleSvg, + ProgressCircleTrack, + ProgressCircleIndicator, + }, // The render fns assemble their own atoms; these satisfy the Root's value-model props. args: { value: 32, "aria-label": "Progress" }, decorators: [ @@ -43,7 +56,7 @@ export const Default: Story = { Upload progress - + {(_, value) => (value == null ? "" : `${Math.round(value)}%`)} @@ -56,15 +69,32 @@ export const Default: Story = { }; /** - * The atomic determinate ring — a styled `Progress.Root` wrapping an SVG. It owns its own - * `progressbar` role; pass `aria-label` for the accessible name and `magnitude` for the diameter - * (`sm` 16px / `md` 20px). + * Assemble the determinate ring from atoms: `ProgressCircle` (the styled `Progress.Root`) › + * `ProgressCircleSvg` › `ProgressCircleTrack` (the subtle full ring) + `ProgressCircleIndicator` + * (the toned arc). The root owns the `progressbar` role; pass `aria-label` for the accessible name, + * `magnitude` for the diameter, and `tone` for the arc fill color. Geometry (radius, dash offset) + * is passed to the circles as SVG attributes. */ export const Circle: Story = { - render: () => ( -
- - -
- ), + render: () => { + const radius = 8; + const circumference = 2 * Math.PI * radius; + return ( + + + + + + + ); + }, }; diff --git a/packages/propel/src/ui/progress/progress.tsx b/packages/propel/src/ui/progress/progress.tsx index a1e6e555..958f6730 100644 --- a/packages/propel/src/ui/progress/progress.tsx +++ b/packages/propel/src/ui/progress/progress.tsx @@ -1,11 +1,11 @@ import { Progress as BaseProgress } from "@base-ui/react/progress"; import { type VariantProps } from "class-variance-authority"; -import { rootVariants } from "./variants"; +import { progressVariants } from "./variants"; /** Props for {@link Progress} (the Base UI `Progress.Root`). */ export type ProgressProps = Omit & - VariantProps; + VariantProps; /** * The atomic `Progress.Root` — maps 1:1 to Base UI's `Progress.Root`. It owns the `progressbar` @@ -19,5 +19,5 @@ export type ProgressProps = Omit * `Progress` from `@plane/propel/components/progress`. */ export function Progress({ layout, ...props }: ProgressProps) { - return ; + return ; } diff --git a/packages/propel/src/ui/progress/variants.ts b/packages/propel/src/ui/progress/variants.ts index c9253d04..f2225566 100644 --- a/packages/propel/src/ui/progress/variants.ts +++ b/packages/propel/src/ui/progress/variants.ts @@ -1,12 +1,43 @@ import { cva } from "class-variance-authority"; +/** + * Indicator fill variants. `tone` drives the fill color of the bar. All other indicator styles — + * pill radius, transition, inset — are always the same. + * + * Indeterminate state: Base UI sets `data-indeterminate` on its Root and propagates it to the + * Indicator; we target it here with a slide-pulse animation so the bar communicates an + * unknown-duration operation without a fixed value. + */ export const progressIndicatorVariants = cva( - "absolute inset-y-0 rounded-full bg-accent-primary transition-[width] duration-300 ease-out", + [ + "absolute inset-y-0 rounded-full transition-[width] duration-300 ease-out", + // Indeterminate: slide a shorter bar back and forth across the track. + // Suppress the determinate width transition so it doesn't interfere. + "data-indeterminate:w-1/3 data-indeterminate:animate-progress-indeterminate data-indeterminate:transition-none", + ], + { + variants: { + tone: { + brand: "bg-accent-primary", + success: "bg-success-primary", + warning: "bg-warning-primary", + danger: "bg-danger-primary", + }, + }, + }, ); + export const progressLabelVariants = cva("text-13 font-medium text-secondary"); -export const progressValueVariants = cva("text-12 font-medium text-accent-primary tabular-nums"); -export const rootVariants = cva("", { +/** + * Value text variants. The percentage is a neutral readout (matching `ProgressLabel`'s + * `text-secondary`), not a toned signal: the semantic color lives on the fill bar/arc. Keeping the + * number neutral also avoids the low-contrast amber/green readouts against a neutral surface (toned + * text only meets WCAG AA on its own soft tone background, which the percentage does not have). + */ +export const progressValueVariants = cva("text-12 font-medium text-secondary tabular-nums"); + +export const progressVariants = cva("", { variants: { layout: { linear: "flex w-full items-center gap-2", @@ -14,7 +45,7 @@ export const rootVariants = cva("", { }, }); -export const trackVariants = cva( +export const progressTrackVariants = cva( "relative min-w-0 flex-1 overflow-hidden rounded-full bg-layer-3-selected", { variants: { @@ -26,7 +57,11 @@ export const trackVariants = cva( }, ); -export const ringVariants = cva("shrink-0", { +/** + * Circular ring root variants. `magnitude` sets the diameter of the ring box. The arc and track + * circles fill the box; geometry (radius, dash) is passed to those parts as SVG attributes. + */ +export const progressCircleVariants = cva("shrink-0", { variants: { magnitude: { sm: "size-4", @@ -34,3 +69,33 @@ export const ringVariants = cva("shrink-0", { }, }, }); + +/** The circular ring's SVG viewport. Fills its `ProgressCircle` box. */ +export const progressCircleSvgVariants = cva("block size-full"); + +/** + * The full subtle ring behind the arc. Strokes with the same `layer-3-selected` surface token the + * linear track fills with, so both variants read as the same low-emphasis track and re-tint + * together in every theme. + */ +export const progressCircleTrackVariants = cva("[stroke:var(--bg-layer-3-selected)]"); + +/** + * The toned arc proportional to the value, with rounded caps. `tone` drives the stroke color. The + * arc is rotated so it starts at 12 o'clock; `transform-origin: center` keeps the rotation about + * the circle's center so the visual sweep stays clockwise in RTL too. The dash offset (the value + * model) is passed as an SVG attribute. + */ +export const progressCircleIndicatorVariants = cva( + "origin-center -rotate-90 transition-[stroke-dashoffset] duration-300 ease-out", + { + variants: { + tone: { + brand: "[stroke:var(--bg-accent-primary)]", + success: "[stroke:var(--bg-success-primary)]", + warning: "[stroke:var(--bg-warning-primary)]", + danger: "[stroke:var(--bg-danger-primary)]", + }, + }, + }, +);