+ ),
+};
+
+/**
+ * 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 = {
@@ -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)]",
+ },
+ },
+ },
+);