diff --git a/packages/propel/src/components/circular-progress/circular-progress.stories.tsx b/packages/propel/src/components/circular-progress/circular-progress.stories.tsx
index 21b87d2a..51aff667 100644
--- a/packages/propel/src/components/circular-progress/circular-progress.stories.tsx
+++ b/packages/propel/src/components/circular-progress/circular-progress.stories.tsx
@@ -66,6 +66,20 @@ export const HasProgressbarRole: Story = {
},
};
+/**
+ * Out-of-range `value` is clamped to `[0, max]` before the arc and `aria-valuenow` are derived, so
+ * the two never disagree. Tagged out of the sidebar/docs/manifest while still running under
+ * `test`.
+ */
+export const ClampsOutOfRange: Story = {
+ tags: ["!dev", "!autodocs", "!manifest"],
+ args: { value: 150, "aria-label": "Overshoot" },
+ play: async ({ canvas }) => {
+ const ring = canvas.getByRole("progressbar", { name: "Overshoot" });
+ await expect(ring).toHaveAttribute("aria-valuenow", "100");
+ },
+};
+
/** `value={null}` is indeterminate: a fixed quarter arc spins with no `aria-valuenow`. */
export const Indeterminate: Story = {
args: { value: null, magnitude: "md", tone: "brand", "aria-label": "Syncing" },
diff --git a/packages/propel/src/components/circular-progress/circular-progress.tsx b/packages/propel/src/components/circular-progress/circular-progress.tsx
index 2525e8f1..57b22330 100644
--- a/packages/propel/src/components/circular-progress/circular-progress.tsx
+++ b/packages/propel/src/components/circular-progress/circular-progress.tsx
@@ -22,7 +22,7 @@ const RING_STROKE = 2;
export type CircularProgressProps = Omit<
BaseProgress.Root.Props,
- "className" | "style" | "value"
+ "className" | "style" | "value" | "render"
> & {
/**
* Completion from 0 to `max` (default 100). `null` = indeterminate: a fixed quarter arc spins
@@ -47,17 +47,20 @@ export function CircularProgress({ value, magnitude, tone, ...props }: CircularP
const { box, radius } = RING_GEOMETRY[magnitude];
const circumference = 2 * Math.PI * radius;
const max = props.max ?? 100;
- // Clamp once so the arc and `aria-valuenow` never disagree for out-of-range input. While
- // indeterminate a fixed quarter arc shows; the svg part spins it off `data-indeterminate`.
- const clampedValue = value == null ? null : Math.min(Math.max(value, 0), max);
- const fraction = clampedValue == null ? 0.25 : max > 0 ? clampedValue / max : 0;
+ const min = props.min ?? 0;
+ const span = max - min;
+ // Clamp once so the arc and `aria-valuenow` never disagree for out-of-range input, and derive the
+ // fraction from both bounds so a non-zero `min` maps correctly. While indeterminate a fixed
+ // quarter arc shows; the svg part spins it off `data-indeterminate`.
+ const clampedValue = value == null ? null : Math.min(Math.max(value, min), max);
+ const fraction = clampedValue == null ? 0.25 : span > 0 ? (clampedValue - min) / span : 0;
const dashOffset = circumference * (1 - fraction);
const center = box / 2;
return (
}
{...props}
+ render={}
>
diff --git a/packages/propel/src/components/linear-progress/linear-progress.stories.tsx b/packages/propel/src/components/linear-progress/linear-progress.stories.tsx
index 7d02dd27..4e3567b8 100644
--- a/packages/propel/src/components/linear-progress/linear-progress.stories.tsx
+++ b/packages/propel/src/components/linear-progress/linear-progress.stories.tsx
@@ -4,6 +4,7 @@ import { expect, waitFor } from "storybook/test";
import {
LinearProgressIndicator,
+ LinearProgressLabel,
LinearProgressTrack,
LinearProgressValue,
} from "../../elements/linear-progress";
@@ -15,7 +16,12 @@ const TONES: LinearProgressTone[] = ["brand", "success", "warning", "danger"];
const meta = {
title: "Components/LinearProgress",
component: LinearProgress,
- subcomponents: { LinearProgressTrack, LinearProgressIndicator, LinearProgressValue },
+ subcomponents: {
+ LinearProgressLabel,
+ LinearProgressTrack,
+ LinearProgressIndicator,
+ LinearProgressValue,
+ },
args: { value: 60, magnitude: "md", tone: "brand", "aria-label": "Upload progress" },
// The bar has no intrinsic width (`w-full` root, `flex-1 min-w-0` track), so a bare render
// collapses to 0px under the centered layout — give every story a real width to fill.
@@ -29,7 +35,10 @@ const meta = {
} satisfies Meta;
export default meta;
-type Story = StoryObj;
+// Typed against the component (not `typeof meta`): the props are a discriminated union on
+// `label`/`aria-label`, which `StoryObj` collapses to `never`. `StoryObj` keeps per-story args as a `Partial` of the union so stories still type-check.
+type Story = StoryObj;
export const Default: Story = {};
@@ -106,5 +115,11 @@ export const HasProgressbarRole: Story = {
/** A visible text label before the track — `label` also names the bar for assistive tech. */
export const WithLabel: Story = {
- args: { value: 60, magnitude: "md", tone: "brand", label: "Uploading attachments" },
+ args: {
+ value: 60,
+ magnitude: "md",
+ tone: "brand",
+ label: "Uploading attachments",
+ "aria-label": undefined,
+ },
};
diff --git a/packages/propel/src/components/linear-progress/linear-progress.tsx b/packages/propel/src/components/linear-progress/linear-progress.tsx
index da06ecf3..823448c4 100644
--- a/packages/propel/src/components/linear-progress/linear-progress.tsx
+++ b/packages/propel/src/components/linear-progress/linear-progress.tsx
@@ -13,7 +13,10 @@ import {
export type LinearProgressMagnitude = NonNullable;
export type LinearProgressTone = NonNullable;
-export type LinearProgressProps = Omit & {
+type LinearProgressOwnProps = Omit<
+ BaseProgress.Root.Props,
+ "className" | "style" | "value" | "render" | "aria-label"
+> & {
/**
* Completion from 0 to `max` (default 100). `null` = indeterminate (animated fill,
* `aria-valuenow` unset).
@@ -25,12 +28,28 @@ export type LinearProgressProps = Omit}>
+ }>
{label != null ? (
}>{label}
) : null}
diff --git a/packages/propel/src/elements/circular-progress/circular-progress-indicator.tsx b/packages/propel/src/elements/circular-progress/circular-progress-indicator.tsx
index b926607e..6b9f07b4 100644
--- a/packages/propel/src/elements/circular-progress/circular-progress-indicator.tsx
+++ b/packages/propel/src/elements/circular-progress/circular-progress-indicator.tsx
@@ -6,7 +6,6 @@ import {
circularProgressIndicatorVariants,
} from "./variants";
-/** Props for {@link CircularProgressIndicator}. */
export type CircularProgressIndicatorProps = Omit<
useRender.ComponentProps<"circle">,
"className" | "style"
diff --git a/packages/propel/src/elements/circular-progress/circular-progress-svg.tsx b/packages/propel/src/elements/circular-progress/circular-progress-svg.tsx
index c2133586..02322c98 100644
--- a/packages/propel/src/elements/circular-progress/circular-progress-svg.tsx
+++ b/packages/propel/src/elements/circular-progress/circular-progress-svg.tsx
@@ -3,7 +3,6 @@ import { useRender } from "@base-ui/react/use-render";
import { circularProgressSvgVariants } from "./variants";
-/** Props for {@link CircularProgressSvg}. */
export type CircularProgressSvgProps = Omit, "className" | "style">;
/**
diff --git a/packages/propel/src/elements/circular-progress/circular-progress-track.tsx b/packages/propel/src/elements/circular-progress/circular-progress-track.tsx
index 894a76f1..90ab34b3 100644
--- a/packages/propel/src/elements/circular-progress/circular-progress-track.tsx
+++ b/packages/propel/src/elements/circular-progress/circular-progress-track.tsx
@@ -3,7 +3,6 @@ import { useRender } from "@base-ui/react/use-render";
import { circularProgressTrackVariants } from "./variants";
-/** Props for {@link CircularProgressTrack}. */
export type CircularProgressTrackProps = Omit<
useRender.ComponentProps<"circle">,
"className" | "style"
diff --git a/packages/propel/src/elements/circular-progress/circular-progress.tsx b/packages/propel/src/elements/circular-progress/circular-progress.tsx
index 62d69b52..c7bcbe7d 100644
--- a/packages/propel/src/elements/circular-progress/circular-progress.tsx
+++ b/packages/propel/src/elements/circular-progress/circular-progress.tsx
@@ -3,7 +3,6 @@ import { useRender } from "@base-ui/react/use-render";
import { type CircularProgressVariantProps, circularProgressVariants } from "./variants";
-/** Props for {@link CircularProgress}. */
export type CircularProgressProps = Omit, "className" | "style"> &
CircularProgressVariantProps;
diff --git a/packages/propel/src/elements/linear-progress/variants.ts b/packages/propel/src/elements/linear-progress/variants.ts
index 974fe83f..2b257dce 100644
--- a/packages/propel/src/elements/linear-progress/variants.ts
+++ b/packages/propel/src/elements/linear-progress/variants.ts
@@ -34,7 +34,8 @@ export const linearProgressIndicatorVariants = cva(
export const linearProgressLabelVariants = cva("text-13 font-medium text-secondary");
// The percentage is a neutral readout (matching the label), not a toned signal — the semantic color
-// lives on the fill bar. Neutral also avoids low-contrast amber/green readouts on a neutral surface.
+// lives on the fill bar. Neutral also keeps the small 12px number AA-legible: the warning amber
+// text token lands at 4.49:1 on the neutral surface (below WCAG AA), so tinting the % is not viable.
export const linearProgressValueVariants = cva("text-12 font-medium text-secondary tabular-nums");
export type LinearProgressTrackVariantProps = StrictVariantProps<