Skip to content

feat: add Meter component#691

Open
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-meter
Open

feat: add Meter component#691
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-meter

Conversation

@rohanchkrabrty
Copy link
Contributor

@rohanchkrabrty rohanchkrabrty commented Mar 11, 2026

Summary

  • Add Meter component with linear and circular variants
  • Composable API: Meter.Root, Meter.Track, Meter.Label, Meter.Value
  • Circular variant uses SVG with viewBox scaling — size controlled via CSS width/height, stroke via --rs-meter-track-size variable
  • All circular geometry (radius, circumference, dash offset) derived in CSS from --rs-meter-track-size
  • Percentage-based indicator driven by --rs-meter-percentage CSS variable set on root
  • Renders a default track when no children are provided
  • Includes tests, docs, and playground

Summary by CodeRabbit

  • New Features

    • Added Meter component for displaying measurements and progress with linear and circular variants.
    • Supports customizable labels, values, and track styling.
  • Documentation

    • Added comprehensive component documentation with usage examples and API reference.
  • Tests

    • Added test coverage for rendering, variants, and accessibility features.

@vercel
Copy link

vercel bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Mar 11, 2026 5:37am

@rohanchkrabrty rohanchkrabrty self-assigned this Mar 11, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

A new Meter component is introduced to the raystack library with support for linear and circular variants. The component includes subcomponents (Label, Value, Track), context-based state sharing, comprehensive CSS styling for both variants, full test coverage, and detailed MDX documentation with multiple demo examples.

Changes

Cohort / File(s) Summary
Component Implementation
packages/raystack/components/meter/meter.tsx, packages/raystack/components/meter/meter-root.tsx, packages/raystack/components/meter/meter-track.tsx, packages/raystack/components/meter/meter-misc.tsx, packages/raystack/components/meter/index.tsx
Core Meter component exports with MeterRoot as the main component, augmented with Label, Value, and Track subcomponents. MeterRoot manages context (variant, value, percentage), MeterTrack handles variant-specific rendering (linear vs SVG-based circular), and MeterLabel/MeterValue provide utility wrappers around base primitives.
Styling
packages/raystack/components/meter/meter.module.css
CSS module defining visual styles for both linear and circular meter variants, including track, indicator, labels, and animated stroke properties for the circular variant.
Testing
packages/raystack/components/meter/__tests__/meter.test.tsx
Comprehensive test suite covering rendering, ref forwarding, variants (linear/circular), subcomponents, and accessibility (ARIA roles and attributes).
Documentation & Demos
apps/www/src/content/docs/components/meter/demo.ts, apps/www/src/content/docs/components/meter/index.mdx, apps/www/src/content/docs/components/meter/props.ts
Demo component examples (playground, variants, customization, labels, custom ranges), MDX documentation with anatomy, API reference, and accessibility details. Props interfaces for Meter, MeterLabel, MeterValue, and MeterTrack.
Library Exports
packages/raystack/index.tsx
Public re-export of Meter component at the package root level.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • rsbh
  • paanSinghCoder

Poem

🐰 A meter's born, both straight and round,
With labels, tracks, and values sound!
Variants dance in context divine,
Tests ensure each line's design,
The library grows, our work complete!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Meter component' directly and clearly describes the main change: introducing a new Meter component to the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-meter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
packages/raystack/components/meter/__tests__/meter.test.tsx (1)

88-97: Assert that only one track is rendered in this branch.

This test is meant to protect the “custom children instead of implicit default track” path, but it currently only proves that a track exists. If the component rendered both tracks, this would still pass.

Suggested fix
     it('renders custom children instead of default track', () => {
       const { container } = render(
         <Meter value={50}>
           <Meter.Label>Custom</Meter.Label>
           <Meter.Track />
         </Meter>
       );
       expect(screen.getByText('Custom')).toBeInTheDocument();
-      expect(container.querySelector(`.${styles.track}`)).toBeInTheDocument();
+      expect(container.querySelectorAll(`.${styles.track}`)).toHaveLength(1);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/__tests__/meter.test.tsx` around lines 88
- 97, The test should assert that exactly one track is rendered to prevent both
the implicit default and the provided <Meter.Track /> from coexisting; update
the test in meter.test.tsx (the 'renders custom children instead of default
track' case) to query all elements matching the track class (use
container.querySelectorAll(`.${styles.track}`) or equivalent) and assert the
node list length is 1, while keeping the existing asserts for the custom label
and presence of a track.
packages/raystack/components/meter/meter-root.tsx (1)

57-59: Consider removing redundant fallback.

The variant ?? 'linear' fallback on line 58 is redundant since variant already defaults to 'linear' in the destructuring on line 45.

♻️ Proposed simplification
       <MeterContext.Provider
-        value={{ variant: variant ?? 'linear', value, percentage }}
+        value={{ variant, value, percentage }}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/meter-root.tsx` around lines 57 - 59,
Remove the redundant nullish fallback when providing context: in the MeterRoot
component where MeterContext.Provider is given value={{ variant: variant ??
'linear', value, percentage }}, drop the "?? 'linear'" because the component's
props destructuring already sets variant = 'linear'; change the provider to pass
variant directly (value={{ variant, value, percentage }}) so the default from
the prop destructuring is used consistently.
packages/raystack/components/meter/meter-track.tsx (1)

28-30: Indicator render prop discards incoming props.

The render function for MeterPrimitive.Indicator ignores all props passed by the primitive. Compare with lines 21-25 where trackProps are correctly spread onto the <svg> element. The indicator may not receive necessary attributes (e.g., ARIA props, data attributes) from the primitive.

♻️ Proposed fix to forward indicator props
         <MeterPrimitive.Indicator
-          render={() => <circle className={styles.circularIndicatorCircle} />}
+          render={(indicatorProps) => (
+            <circle className={styles.circularIndicatorCircle} {...indicatorProps} />
+          )}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/meter-track.tsx` around lines 28 - 30, The
Indicator's render prop currently ignores props from MeterPrimitive.Indicator;
update the render to accept and forward those props to the DOM element so
ARIA/data attributes and event handlers are preserved. Specifically, change the
render used in MeterPrimitive.Indicator to a function like
render={(indicatorProps) => <circle {...indicatorProps}
className={styles.circularIndicatorCircle} />} so the incoming props are spread
onto the <circle> while still applying the styles.circularIndicatorCircle class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/www/src/content/docs/components/meter/demo.ts`:
- Around line 11-20: The playground controls can produce min >= max and
out-of-range values; update the controls object in demo.ts so the 'max' control
always stays greater than 'min' and 'value' is clamped to [min, max] whenever
any control changes: enforce a relation when updating 'min' and 'max' (e.g., if
newMin >= currentMax, set max = newMin + 1; if newMax <= currentMin, set min =
newMax - 1), and ensure the 'value' control is adjusted/clamped to Math.min(max,
Math.max(min, value)) after min/max changes; also update the 'value' control's
metadata to reflect the current min/max bounds so the playground UI cannot set
values outside the valid range (referencing the controls object and the 'min',
'max', and 'value' fields and the meter-root calculation that divides by (max -
min)).

In `@apps/www/src/content/docs/components/meter/props.ts`:
- Around line 1-25: The MeterProps interface is missing documented customization
and accessibility hooks referenced in the docs; update the MeterProps interface
(and any related exported prop types) to include a trackStyle prop for styling
Meter.Track (e.g., trackStyle?: React.CSSProperties) and add accessibility props
ariaLabel?: string and ariaValueText?: string (or similarly named camelCase
variants used across the codebase) so the API tables match the documented usage
of Meter.Track style={...} and the aria-label / aria-valuetext props. Ensure the
new props are documented with the same JSDoc defaults/comments as the rest of
the interface.

In `@packages/raystack/components/meter/meter-root.tsx`:
- Line 54: Guard the percentage calc in meter-root.tsx against a zero
denominator by checking if max === min before computing percentage; if they are
equal, set percentage to 0 (or 100 when value >= max) to produce a stable CSS
value, otherwise compute percentage = ((value - min) * 100) / (max - min);
finally clamp the resulting percentage to the 0–100 range so the CSS variable
used by the meter never receives NaN/Infinity or out-of-range values.

In `@packages/raystack/components/meter/meter.module.css`:
- Around line 55-63: The .circularSvg rule violates Stylelint: add a blank line
before the width declaration and fix the broken operator/newline split in the
calc expression used for stroke-dashoffset (and the similar calc usage around
lines 81-84) so the operator isn't orphaned on a new line; locate the
.circularSvg block and the stroke-dashoffset declaration and reformat them to
have the empty line before width and a single-line or correctly spaced calc()
expression to satisfy the stylelint operator/newline rule.
- Around line 1-6: The .meter root class sets width: 100% which causes circular
meters to stretch; update the circular variant to reset that rule (e.g., in the
CSS rule that targets the circular variant such as the class used for circular
meters like .meter.circular or .meter--circular) by setting width: auto or
width: unset (and remove any conflicting inline or inherited width) so the
circular meter sizes to its SVG content rather than the full row.

---

Nitpick comments:
In `@packages/raystack/components/meter/__tests__/meter.test.tsx`:
- Around line 88-97: The test should assert that exactly one track is rendered
to prevent both the implicit default and the provided <Meter.Track /> from
coexisting; update the test in meter.test.tsx (the 'renders custom children
instead of default track' case) to query all elements matching the track class
(use container.querySelectorAll(`.${styles.track}`) or equivalent) and assert
the node list length is 1, while keeping the existing asserts for the custom
label and presence of a track.

In `@packages/raystack/components/meter/meter-root.tsx`:
- Around line 57-59: Remove the redundant nullish fallback when providing
context: in the MeterRoot component where MeterContext.Provider is given
value={{ variant: variant ?? 'linear', value, percentage }}, drop the "??
'linear'" because the component's props destructuring already sets variant =
'linear'; change the provider to pass variant directly (value={{ variant, value,
percentage }}) so the default from the prop destructuring is used consistently.

In `@packages/raystack/components/meter/meter-track.tsx`:
- Around line 28-30: The Indicator's render prop currently ignores props from
MeterPrimitive.Indicator; update the render to accept and forward those props to
the DOM element so ARIA/data attributes and event handlers are preserved.
Specifically, change the render used in MeterPrimitive.Indicator to a function
like render={(indicatorProps) => <circle {...indicatorProps}
className={styles.circularIndicatorCircle} />} so the incoming props are spread
onto the <circle> while still applying the styles.circularIndicatorCircle class.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db6ef343-434a-4b36-b6ac-ebcac84827cb

📥 Commits

Reviewing files that changed from the base of the PR and between 14de0ba and 5efce06.

📒 Files selected for processing (11)
  • apps/www/src/content/docs/components/meter/demo.ts
  • apps/www/src/content/docs/components/meter/index.mdx
  • apps/www/src/content/docs/components/meter/props.ts
  • packages/raystack/components/meter/__tests__/meter.test.tsx
  • packages/raystack/components/meter/index.tsx
  • packages/raystack/components/meter/meter-misc.tsx
  • packages/raystack/components/meter/meter-root.tsx
  • packages/raystack/components/meter/meter-track.tsx
  • packages/raystack/components/meter/meter.module.css
  • packages/raystack/components/meter/meter.tsx
  • packages/raystack/index.tsx

Comment on lines +11 to +20
controls: {
value: { type: 'number', initialValue: 40, min: 0, max: 100 },
variant: {
type: 'select',
initialValue: 'linear',
options: ['linear', 'circular']
},
min: { type: 'number', defaultValue: 0, min: 0, max: 99 },
max: { type: 'number', defaultValue: 100, min: 1, max: 100 }
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Constrain the playground controls to a valid range.

These controls can currently produce min >= max and values outside [min, max]. packages/raystack/components/meter/meter-root.tsx:54-68 divides by (max - min) directly, so the docs demo can end up rendering Infinity/NaN or percentages above 100. Please keep max > min and clamp value when the controls change so the playground can't enter a broken state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/meter/demo.ts` around lines 11 - 20, The
playground controls can produce min >= max and out-of-range values; update the
controls object in demo.ts so the 'max' control always stays greater than 'min'
and 'value' is clamped to [min, max] whenever any control changes: enforce a
relation when updating 'min' and 'max' (e.g., if newMin >= currentMax, set max =
newMin + 1; if newMax <= currentMin, set min = newMax - 1), and ensure the
'value' control is adjusted/clamped to Math.min(max, Math.max(min, value)) after
min/max changes; also update the 'value' control's metadata to reflect the
current min/max bounds so the playground UI cannot set values outside the valid
range (referencing the controls object and the 'min', 'max', and 'value' fields
and the meter-root calculation that divides by (max - min)).

Comment on lines +1 to +25
export interface MeterProps {
/** The current value of the meter. */
value: number;

/**
* Minimum value.
* @default 0
*/
min?: number;

/**
* Maximum value.
* @default 100
*/
max?: number;

/**
* The visual style of the meter.
* @default "linear"
*/
variant?: 'linear' | 'circular';

/** Additional CSS class name. */
className?: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the generated API tables aligned with the documented surface.

The docs page demonstrates Meter.Track style={...} customization and explicitly calls out aria-label / aria-valuetext, but those props never appear in the interfaces that drive the API reference. Right now the generated tables under-document the main customization and accessibility hooks for this component.

Suggested fix
 export interface MeterProps {
   /** The current value of the meter. */
   value: number;
@@
   /** Additional CSS class name. */
   className?: string;
+
+  /** Accessible name for screen readers. */
+  'aria-label'?: string;
+
+  /** Screen-reader-specific text for the current value. */
+  'aria-valuetext'?: string;
 }
@@
 export interface MeterTrackProps {
   /** Additional CSS class name. */
   className?: string;
+
+  /** Inline styles for sizing and CSS custom properties like `--rs-meter-track-size`. */
+  style?: React.CSSProperties;
 }

Also applies to: 40-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/meter/props.ts` around lines 1 - 25, The
MeterProps interface is missing documented customization and accessibility hooks
referenced in the docs; update the MeterProps interface (and any related
exported prop types) to include a trackStyle prop for styling Meter.Track (e.g.,
trackStyle?: React.CSSProperties) and add accessibility props ariaLabel?: string
and ariaValueText?: string (or similarly named camelCase variants used across
the codebase) so the API tables match the documented usage of Meter.Track
style={...} and the aria-label / aria-valuetext props. Ensure the new props are
documented with the same JSDoc defaults/comments as the rest of the interface.

},
ref
) => {
const percentage = ((value - min) * 100) / (max - min);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against division by zero when max === min.

If max equals min, the denominator becomes zero, resulting in NaN or Infinity. This will produce an invalid CSS variable value and break the meter rendering.

🛡️ Proposed fix to handle edge case
-    const percentage = ((value - min) * 100) / (max - min);
+    const range = max - min;
+    const percentage = range > 0 ? ((value - min) * 100) / range : 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const percentage = ((value - min) * 100) / (max - min);
const range = max - min;
const percentage = range > 0 ? ((value - min) * 100) / range : 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/meter-root.tsx` at line 54, Guard the
percentage calc in meter-root.tsx against a zero denominator by checking if max
=== min before computing percentage; if they are equal, set percentage to 0 (or
100 when value >= max) to produce a stable CSS value, otherwise compute
percentage = ((value - min) * 100) / (max - min); finally clamp the resulting
percentage to the 0–100 range so the CSS variable used by the meter never
receives NaN/Infinity or out-of-range values.

Comment on lines +1 to +6
.meter {
display: flex;
flex-direction: column;
gap: var(--rs-space-3);
width: 100%;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the root width for the circular variant.

.meter forces width: 100%, and the circular override never undoes it. That makes a circular meter take the full row width instead of matching its SVG footprint, which is especially noticeable in the horizontal demos. A circular meter should size to its content here.

Suggested fix
 .meter-variant-circular {
+  width: fit-content;
   align-items: center;
   justify-content: center;
   position: relative;
 }

Also applies to: 49-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/meter.module.css` around lines 1 - 6, The
.meter root class sets width: 100% which causes circular meters to stretch;
update the circular variant to reset that rule (e.g., in the CSS rule that
targets the circular variant such as the class used for circular meters like
.meter.circular or .meter--circular) by setting width: auto or width: unset (and
remove any conflicting inline or inherited width) so the circular meter sizes to
its SVG content rather than the full row.

Comment on lines +55 to +63
.circularSvg {
--rs-meter-track-size: 4px;
--rs-meter-radius: calc((72px - var(--rs-meter-track-size) * 2) / 2);
--rs-meter-circumference: calc(2 * 3.14159265 * var(--rs-meter-radius));
width: 72px;
height: 72px;
aspect-ratio: 1;
transform: rotate(-90deg);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This block currently fails the reported Stylelint rules.

The formatting here matches the static-analysis errors: missing empty line before width and the operator/newline split inside stroke-dashoffset. If lint runs in CI, this file will keep failing until those are normalized.

Suggested fix
 .circularSvg {
   --rs-meter-track-size: 4px;
   --rs-meter-radius: calc((72px - var(--rs-meter-track-size) * 2) / 2);
   --rs-meter-circumference: calc(2 * 3.14159265 * var(--rs-meter-radius));
+
   width: 72px;
   height: 72px;
   aspect-ratio: 1;
   transform: rotate(-90deg);
 }
@@
 .circularIndicatorCircle {
   stroke: var(--rs-color-background-accent-emphasis);
   stroke-dasharray: var(--rs-meter-circumference);
-  stroke-dashoffset: calc(
-    var(--rs-meter-circumference) *
-    (1 - var(--rs-meter-percentage, 0) / 100)
-  );
+  stroke-dashoffset: calc(var(--rs-meter-circumference) * (1 - var(--rs-meter-percentage, 0) / 100));
   stroke-linecap: butt;
   transition: stroke-dashoffset 500ms;
 }

Also applies to: 81-84

🧰 Tools
🪛 Stylelint (17.4.0)

[error] 59-59: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/meter/meter.module.css` around lines 55 - 63,
The .circularSvg rule violates Stylelint: add a blank line before the width
declaration and fix the broken operator/newline split in the calc expression
used for stroke-dashoffset (and the similar calc usage around lines 81-84) so
the operator isn't orphaned on a new line; locate the .circularSvg block and the
stroke-dashoffset declaration and reformat them to have the empty line before
width and a single-line or correctly spaced calc() expression to satisfy the
stylelint operator/newline rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant