fix: design-system audit — motion, interaction states, focus & accessibility#856
fix: design-system audit — motion, interaction states, focus & accessibility#856ravisuhag wants to merge 42 commits into
Conversation
Compose the shared easing, duration, pressed-scale and focus-ring tokens so components stop hand-rolling their own timings and curves.
Step body letter-spacing and h1-h4 line-heights so they change monotonically across sizes and follow Inter's own tracking curve.
Give numeric columns tabular figures so digits align, alongside the shared row-selection and sticky-header styling that lives in the same two stylesheets.
A slightly faster spinner raises perceived load speed. Recomputed all 8 pole animation-delays to keep the same 1/8-cycle stagger ratio at the new duration, so the sweep still rotates evenly.
Centering used translateX(-50%), which would collide with any transform-based entrance. Move centering to inset+margin-inline:auto (safe since the root is width:fit-content) so transform is free for motion. Add a fade-in for everyone, plus an 8px rise from the anchored edge under prefers-reduced-motion: no-preference. Exit stays instant (no mount controller exists to hold the node past removal).
The error handler swapped src to the fallback with no re-entry guard -- if the fallback URL itself 404s, onError fires again and re-assigns the same failing src, looping requests indefinitely. Track a one-shot ref so the fallback is only ever assigned once; a second failure falls through to showing the alt/broken rendering instead. Both Image and Avatar's photo popped in abruptly with no load transition. Image now tracks load state via a useLayoutEffect that checks node.complete on mount/src-change -- already-decoded (cached or SSR-painted) images stay visible with no fade; genuinely loading images fade in over 150ms on load. Avatar's image only mounts once Base UI's probe reports loaded, so its fade is a one-shot keyframe animation, skipped entirely for cache-fast loads (<100ms between loading and loaded) so a quick load isn't artificially delayed. Reduced motion shows both instantly at full opacity, never invisible.
A display:block heading already fills its container in normal flow; the explicit width:100% only mattered in flex/grid contexts, where it made a heading claim the full row/track and push siblings around. Ellipsis truncation is the only variant that needs an explicit width, and it already declares its own -- removing the base rule loses nothing there. BREAKING (mild): consumers relying on the base width:100% inside flex/ grid parents (e.g. a centered heading, or a heading used as a full-row flex child) will see it shrink to content width. One-line fix on their side: set width:100% via className where the old behavior was intentional.
The action button was the bar's only interactive element but had no hover, press, or focus feedback. Add opacity-step hover/press states and a currentColor focus ring that stays visible against the bar's coloured backgrounds.
…hiding it Both variants hid the shared Tabs.Indicator and painted the active state directly on the trigger, so switching tabs snapped instantly instead of gliding like the default segmented variant. The indicator already tracks the active tab via Base UI's --active-tab-* variables for free -- hiding it threw that away. Restyle the indicator per variant instead: standalone becomes the active chip fill (background + border) gliding between tabs, with the active trigger going transparent so the pill shows through (the same trick the segmented variant already uses); plain becomes a 1px underline sliding along the bottom edge, positioned via the trigger's own --active-tab-height so it lines up with the reserved border-bottom exactly. Both inherit the indicator's existing --rs-duration-moderate/--rs-ease-in-out transition, gated behind reduced motion, with no new media query needed.
Breadcrumb links changed colour instantly on hover while the rest of the library eases. Add a short colour transition, gated behind prefers-reduced-motion, so the hover matches every other interactive element.
Popover -- a larger floating surface than the tooltip -- cast a lighter shadow (--rs-shadow-soft) than the tiny tooltip (--rs-shadow-lifted), so elevation read backwards when both were on screen. Match menu, a peer surface of similar size, which already uses --rs-shadow-lifted.
overflow: scroll forced a permanently visible scrollbar gutter on platforms with classic scrollbars, even when content didn't overflow. Use auto. Also document via JSDoc that SidePanel intentionally has no enter/exit animation -- it's persistent layout chrome like Sidebar, not an overlay.
The scrollbar grew its whole track on hover, reflowing the page each time, and the thumb never showed it was grabbed. Keep the track a constant, invisible width so layout never shifts; draw the thumb thinner at rest and grow it to full width on hover; and deepen its colour one step on hover and again while dragging.
The positioner transitioned top/left/right/bottom and the popup transitioned width/height on every open -- but Base UI only writes inset positioning and drives --popup-width/--popup-height when a PreviewCard.Viewport child triggers its adaptiveOrigin mode. In the common case (no Viewport), Base UI positions purely via transform, so the four inset transitions were dead weight forcing unnecessary transition-list overhead on every reposition. Split into two rulesets: the default path transitions transform + opacity only (compositor-only), while `:has(.viewport)` keeps the inset/size transitions where they're the actual morph mechanism.
A sticky header drew its bottom hairline even at rest, before anything had scrolled beneath it. Drive the hairline from a scroll-linked animation so it fades in over the first bit of scroll, gated behind a real scroll container so the plain table keeps its static separator.
Resting day cells used radius-2 while hover/selected/range-cap states all used radius-5, so sweeping the pointer across the grid made each cell's shape pop between rounded-square and pill. The radius-2 resting state was never actually visible (the cell's background matches the calendar root's), so unify on radius-5 -- zero visible states change, one invisible one gets fixed. Hover/selection highlights stay instant, per the audit's explicit rejection of transitions on direct-manipulation surfaces. Loading hard-swapped the whole table for a differently-sized Skeleton in one frame, flashing on fast loads and jumping the popover height. Keep the table mounted and overlay the skeleton, cross-fading via opacity/visibility (~150ms, instant under reduced motion) so the grid never disappears and the height never moves.
Dimming a link to 80% opacity on hover is a weak signal and makes the text harder to read at the exact moment the user is targeting it. The underline geometry now always exists (text-decoration-color: transparent at rest) so hover is a pure color fade to currentColor -- zero layout shift, works for every Text color variant automatically. Press gets a brief variant-agnostic dim (:active, opacity 0.7) instead of a hardcoded color, since a fixed active color would clobber non-accent variants. Timing moves from a hardcoded 0.2s to --rs-duration-fast.
Proportional digits meant amounts jittered as values changed and misaligned in table columns. Add a CSS module with font-variant-numeric: tabular-nums as the default; className/style still merge through as the opt-out escape hatch.
The floating copy button had no transition of its own duration (it inherited the interactive tier's opacity fade), so its reveal moved at hover-feedback speed instead of a dedicated reveal timing. Re-compose the IconButton base transition and override just the opacity entry with --rs-duration-fast/--rs-ease-out. The :focus-visible reveal rule (added by the earlier focus-indicators pass) is merged into the same selector as the hover reveal for clarity.
Row selection was wired up in data-table and data-view-beta (both set
data-state="selected") but no CSS anywhere styled it -- selecting a
row changed nothing on screen. Add an accent-subtle background for
selected rows (wins over hover/pressed) and a one-step-darker pressed
background for clickable rows, instant (no transition, matching the
house rule for direct-manipulation highlights like menu items).
Virtualized cells painted an opaque background that covered the row's
color entirely -- add `.virtualRow .virtualCell { background: inherit }`
so selection/hover/press show through in virtualized mode, same as the
non-virtualized cells already do.
data-view (no selection support) gets the same pressed-row treatment.
The bare Table primitive had no row-state hooks at all. Table.Row now
accepts an `interactive` prop (cursor + hover + pressed) and responds
to a consumer-set data-state="selected", with cells inheriting the row
background through the opaque .cell rule.
…bled cursor Hovering an off toggle looked identical to the on state -- the control lied about whether it was selected. Give hover a lighter tint and pressed (Base UI's toggled-on state) a deeper, inset-shadowed look in both standalone and group mode; hovering an already-on toggle now deepens it one more step instead of reverting to the hover tint. The content background and inset shadow had no transition at all (only color did), so state changes snapped. Add a fast-tier transition for both. pointer-events: none on disabled toggles suppressed the declared cursor: not-allowed, so disabled toggles gave zero affordance. Restore pointer-events so the cursor shows, with explicit hover neutralizers so a disabled-but-off toggle doesn't pick up hover styling (clicks stay blocked natively via the disabled <button>).
The select positioner painted an opaque square that flashed behind the popup as it scaled, and multi-select options nested a real focusable checkbox inside each row, adding a stray tab stop. Drop the positioner background and use a presentational check indicator; the trigger shows a static up/down caret to signal it opens a menu.
A chip with onClick rendered as a <span onClick>: no keyboard activation, not in the tab order, no focus ring, and cursor: default even though it was clickable. Render a <button type="button"> when onClick is passed and the chip isn't dismissible (a button containing the dismiss button would be invalid nested-interactive HTML, so dismissible chips keep the span). Interactive chips get cursor: pointer, inherit the app font, and a focus-visible accent ring.
The Field error popped in with no transition and replaced the description in one jarring jump, and the DatePicker set an error that was never shown. Cross-fade the error in over the description so the swap is smooth, and render the DatePicker error as reserved-space helper text below the input.
…es in The 2D area thumb was positioned via left/top on every pointermove, forcing layout on each frame of a drag. Drive it from two CSS custom properties (--thumb-x/--thumb-y, unitless 0..1 fractions set from JS) composed with container query units and the existing -50% self-centering transform -- compositor-only, no transition on transform so the drag stays 1:1 with the pointer. Also add touch-action: none / user-select: none to .selectionRoot (the sliders already had this; the 2D area didn't, so dragging on touch also scrolled the page), and a fast opacity transition on the thumb so its first-render reveal fades instead of popping.
.nav-text declared an opacity transition, but the collapsed state set display: none, which can't transition and short-circuited it -- labels popped out of existence while the frame animated its width. Same for any consumer content marked data-collapse-hidden. Replace display:none with opacity + visibility (visibility keeps collapsed content out of the accessibility tree and unfocusable, same intent as display:none, but participates in the transition). Also fix the resize handle's cursor: it only click-toggles collapse, but showed ew-resize, promising a drag that doesn't exist. Use pointer on both left- and right-positioned sidebars (per the audit's explicit decision not to implement drag-to-resize).
Uncontrolled dismiss returned null on the next render, so the callout vanished in one frame and content below it snapped up by its full height. There was no enter transition either. Wrap the callout in a transition shell driven by a data-state attribute (open/closing/closed). Dismiss now plays a 200ms fade + grid-row height collapse before unmounting, so content below glides up instead of snapping. Mount fades in over the same duration. Reduced motion is instant both ways. The controlled API (onDismiss provided) is unchanged -- the consumer still owns removal.
Tooltip animated with one-shot @Keyframes: not interruptible (restarts from zero instead of retargeting), no exit at all (popup unmounted instantly), a dead unused fade keyframe, and a dead transform-origin (every keyframe was a pure translate, origin-independent). Replace with [data-starting-style]/[data-ending-style] transitions at --rs-duration-fast/--rs-ease-out, keeping the exact per-side and per-align 2px directional offsets the keyframes used. Enter and exit now mirror each other, rapid hover toggling retargets smoothly instead of restarting, data-instant still skips the animation via transition-duration: 0, and reduced motion gets an opacity-only fade (previously nothing, since every rule lived inside the no-preference media query).
Four independent gaps: - Toast starting-style set a transform but no opacity: 0, so under reduced motion (where the transform never transitions) toasts popped in at full opacity instead of fading. - Navbar's hide-on-scroll transform had no reduced-motion fallback at all, so the bar teleported in/out; add an additive `reduce` block that fades it instead. - Spinner poles had no static opacity, so reduced motion (where the spin animation never applies) rendered a static asterisk instead of a graded ring that still reads as loading. - Dialog and command's backdrop blur had no prefers-reduced-transparency guard; add one with a heavier solid scrim (alert-dialog shares dialog's stylesheet, so it's covered too).
…uced motion The popup kept its 450ms transform transition active while swiping, so it eased toward the finger instead of tracking it directly (the backdrop already zeroed its own transition). Add transition: none on [data-swiping]. The five swipe-strength exit calcs hardcoded 400ms while enter runs 450ms via --rs-duration-drawer -- non-swipe exits ran measurably faster than opens for no reason. Use the token so enter/exit match and retuning the drawer duration doesn't require finding five literals. The panel had no transition outside the motion media query, so under prefers-reduced-motion it popped open/closed instantly. Add a base opacity fade (with matching [data-starting-style]/[data-ending-style] opacity: 0) so reduced motion gets a fade instead of a teleport, and move the per-side slide transforms into the no-preference block alongside the transition they animate.
The input had zero focus, hover, or invalid styling -- a focused field looked identical to an idle one. Mirror Field's states verbatim (hover/focus border+background, invalid border) and add transitions to both the input and the steppers, which previously snapped instantly. Stepper :active and :focus-visible already landed via the library-wide passes (plans 004/005); this plan's remaining gap was the missing transitions plus the input's hover/invalid states and the input:focus background-color.
value={null} (indeterminate) rendered a frozen empty track since
nothing styled it. Add a static dimmed fallback plus a looping sweep
(linear) / rotation (circular) gated behind prefers-reduced-motion,
keyed off Base UI's data-indeterminate attribute.
Also move both Progress and Meter's linear fill from a transitioned
width to transform: scaleX(), driven by the existing
--rs-progress-percentage / --rs-meter-percentage variables — the
indicator now renders full-width and Base UI's inline width is
neutralized by an explicit style prop, so value changes composite
instead of triggering layout.
- toolbar.module.css: background-color: none is not valid CSS and was silently dropped; use transparent. - text: the strikeThrough+underline compound variant class was named text-italic-strike-through but applies no italic (it merges the two decoration lines so they don't cancel out). Rename to text-underline-strike-through rather than adding italic, which would wrongly italicize any underlined+struck-through text.
The Ordering control's sort icon rendered unsized and swapped between two separate glyphs for ascending/descending. Give it an explicit size and flip a single icon vertically around its own centre, so a change of direction reads as one state turning over rather than the icon being replaced.
The reset setTimeout was never cleared, so rapid clicks stacked timers and the check icon could revert early; it also fired after unmount. Hold the timer in a ref, clear/restart it per click and on unmount. Replace the boolean copied flag with an idle/copied/error status. Stack all three icons and cross-fade via opacity (plus a subtle scale under motion) instead of a hard swap, and add a visually hidden role="status" region announcing "Copied"/"Copy failed". CopyButton now always mounts CheckCircleFilledIcon and CrossCircleFilledIcon (previously only Check rendered, conditionally). Both are SVG-asset icons that only transform via the rollup build's @svgr plugin, not under vitest, so two consumer tests (code-block, color-picker) that render CopyButton needed their icon mocks extended to cover the new icon — same pattern code-block's test already used.
Many controls stripped the outline (all: unset, outline: none/0) with nothing in its place, leaving keyboard users unable to see where focus is: tabs, collapsible, checkbox, radio, switch, toggle, slider thumb, color-picker thumbs, icon-button, link, borderless input/text-area, chip/filter-chip dismiss buttons, number-field, code-block's floating copy button, and breadcrumb links/dropdown trigger. Apply three house treatments by element type: an accent ring for controls (upgrading the existing faint 1px button ring to 2px with offset), a subtle background for nav-style breadcrumb items (the sidebar pattern from #808), and a soft box-shadow ring for chromeless text-entry surfaces. Also split accordion's focus from its hover (audit #52) and add otp-field's soft ring on top of its border shift (audit #53/#58). Plans: plans/005-focus-indicators-pass.md
…s tier --rs-transition-interactive hardcoded 0.2s ease instead of composing the duration/easing tokens declared a few lines below it, and applied the same 200ms to :active — so a button press arrived as slowly as a hover fade. Compose the token, add --rs-duration-press (100ms) and --rs-transition-pressed, and wire it into button/icon-button :active. Also replace every hardcoded duration/easing across 14 component stylesheets with the matching --rs-duration-*/--rs-ease-* token, and sync tour's FADE_OUT_MS (160, matching nothing) to 150ms, the actual .spotlightCover fade duration.
Consolidates hand-typed easing curves and durations into shared --rs-ease-*/--rs-duration-* tokens, and fixes a batch of motion issues across the library: removes the command palette's open/close animation (keyboard-driven, 100+ uses/day), rebuilds popover/menu/select/combobox entrances as interruptible origin-anchored transitions, fixes a toast transition that fought active swipe gestures, replaces `transition: all` with the interactive token in four components, syncs switch/checkbox/ radio color transitions with their movement, fixes an inverted reduced-motion override in the sidebar, moves the tabs indicator off layout properties onto `translate`, and adds missing entrance motion to checkbox/radio/empty-state.
Use a non-rotating up/down caret on the select trigger and a plain down chevron on the combobox input, and align the command palette overlay class naming.
Remove the unused eslint config and the np release-tool config and devDeps (releases run through pnpm publish), and refresh the contributing/development docs.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 33 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (23)
📝 WalkthroughWalkthroughThis PR refreshes Raystack’s design-system foundations and component behavior. It adds shared motion, focus-ring, pressed-state, and typography tokens; updates documentation and release commands; improves loading, fallback, dismissal, selection, and indicator lifecycles; and standardizes interactive CSS across controls, overlays, navigation, tables, forms, and feedback components. Several animations move to tokenized transitions with reduced-motion handling, while tests are updated for callout, chip, image, field, code-block, and text behavior. Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (8)
packages/raystack/components/tabs/tabs.module.css (1)
115-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent fallback units for
translatecustom properties.Line 117 uses unitless
0fallbacks while lines 159–160 use0px. Both are valid CSS, but aligning them improves consistency.✏️ Proposed fix
translate: var(--active-tab-left, 0) var(--active-tab-top, 0); + /* or for consistency with the plain variant: */ + translate: var(--active-tab-left, 0px) var(--active-tab-top, 0px);Also applies to: 159-160
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/tabs/tabs.module.css` around lines 115 - 117, Align the fallback values in the translate declarations by replacing the unitless 0 defaults for --active-tab-left and --active-tab-top with 0px, matching the corresponding declarations at lines 159–160.packages/raystack/components/sidebar/sidebar.module.css (1)
395-417: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse shared motion tokens instead of hardcoded
0.2s easein theprefers-reduced-motionblock.Within the same media query,
.nav-textusesvar(--rs-duration-normal)andvar(--rs-ease-out), but.root,.nav-group-panel, and[data-collapse-hidden]still use hardcoded0.2s ease. The PR objective is to consolidate motion tokens — these should be aligned. Note thatease≠--rs-ease-out(cubic-bezier(0.22, 1, 0.36, 1)), so the easing curves differ too.♻️ Proposed fix to use shared tokens
`@media` (prefers-reduced-motion: no-preference) { .root { - transition: width 0.2s ease; + transition: width var(--rs-duration-normal) var(--rs-ease-out); } .nav-group-panel { transition: - width 0.2s ease, - height 0.2s ease; + width var(--rs-duration-normal) var(--rs-ease-out), + height var(--rs-duration-normal) var(--rs-ease-out); } .nav-text { transition: max-width var(--rs-duration-normal) var(--rs-ease-out), opacity var(--rs-duration-normal) var(--rs-ease-out), visibility var(--rs-duration-normal) var(--rs-ease-out); } .root [data-collapse-hidden] { transition: - max-width 0.2s ease, - opacity 0.2s ease, - visibility 0.2s ease; + max-width var(--rs-duration-normal) var(--rs-ease-out), + opacity var(--rs-duration-normal) var(--rs-ease-out), + visibility var(--rs-duration-normal) var(--rs-ease-out); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/sidebar/sidebar.module.css` around lines 395 - 417, Replace the hardcoded 0.2s ease transitions in .root, .nav-group-panel, and .root [data-collapse-hidden] within the prefers-reduced-motion block with the shared --rs-duration-normal and --rs-ease-out motion tokens, matching .nav-text.packages/raystack/components/preview-card/preview-card.module.css (1)
106-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant
.positionertransition inside the media query.The
.positionertransition at lines 110–112 (opacity var(--animation-duration) var(--easing)) is identical to the base rule at line 9. Since the media query no longer addstransformor positional edges to.positioner(those moved to.positioner:has(.viewport)), this block is redundant and can be removed to reduce confusion.♻️ Proposed cleanup
`@media` (prefers-reduced-motion: no-preference) { /* Default mode (no PreviewCard.Viewport): Base UI positions the positioner via transform (floating-ui). Don't transition that transform, or the card eases toward every reposition and swims behind its anchor. The popup owns the entrance (scale below); the positioner only fades — like popover/menu/tooltip. */ - .positioner { - transition: opacity var(--animation-duration) var(--easing); - } - .popup {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/preview-card/preview-card.module.css` around lines 106 - 112, Remove the redundant `.positioner` rule inside the media query in preview-card.module.css, since its opacity transition is already defined by the base `.positioner` rule. Leave the viewport-specific positioning rules and all other media-query behavior unchanged.packages/raystack/components/otp-field/otp-field.module.css (1)
50-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMerge the duplicate invalid-focus rules. In
packages/raystack/components/otp-field/otp-field.module.css, combine the two.otp-field-input[data-invalid]:focus:not([data-readonly])blocks into one for clarity; keep the current explicit dangerbox-shadowsince there’s no matching danger focus-ring token.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/otp-field/otp-field.module.css` around lines 50 - 56, Merge the two duplicate .otp-field-input[data-invalid]:focus:not([data-readonly]) rules into a single CSS block, preserving both the danger border-color and the explicit danger box-shadow declarations.packages/raystack/components/chip/chip.tsx (2)
119-140: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDouble cast on
onClickis a type-safety code smell.Line 134 uses
onClick as unknown as React.MouseEventHandler<HTMLButtonElement>to bridgeComponentProps<'span'>and<button>. Consider wideningonClickinChipPropstoMouseEventHandler<HTMLElement>to eliminate the cast, since the handler works for both span and button targets.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/chip/chip.tsx` around lines 119 - 140, Widen the ChipProps onClick type to React.MouseEventHandler<HTMLElement>, then update the interactive button path in the Chip component to pass onClick directly and remove the double cast. Preserve the existing span and button click behavior.
61-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAccessibility gap: dismissible chips with
onClickare not keyboard-accessible.When both
onClickandisDismissibleare provided,isInteractiveisfalse, so the chip renders a<span>withonClickbut notabindex, norole="button", and no keyboard event handler. Keyboard users cannot activate the chip'sonClick. The PR's accessibility objectives ("accessible clickable chips") make this gap worth addressing.Consider adding
tabindex={0},role="button", and a key-down handler forEnter/Spaceon the span whenonClickis present butisInteractiveis false, or document this as an intentional limitation.Also applies to: 142-148
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/chip/chip.tsx` at line 61, Update the non-interactive rendering path in the chip component so a chip with onClick and isDismissible remains keyboard-accessible: add focusability, button semantics, and Enter/Space key handling that invokes the existing onClick behavior. Preserve the current dismissible behavior and the existing isInteractive path for chips without this combination.packages/raystack/styles/effects.css (1)
40-51: 🎯 Functional Correctness | 🔵 TrivialConfirm 0.5px focus-ring width is intentional.
--rs-focus-ringuses a0.5pxoutline. This matches the codebase's existing hairline-border convention, but as a keyboard-focus indicator specifically (an explicit accessibility deliverable of this PR), sub-pixel outline widths can round away or become hard to perceive on standard-density (1x) displays depending on the browser/engine. Worth confirming this meets the intended focus-visibility bar across target browsers/DPIs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/styles/effects.css` around lines 40 - 51, Confirm the intended accessibility behavior of --rs-focus-ring across target browsers and display densities; if 0.5px can render imperceptibly, increase the outline width to a reliably visible value while preserving the existing focus-ring styling and shadow variant.packages/raystack/components/button/button.module.css (1)
216-218: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider guarding the pressed
transformbehindprefers-reduced-motion.The
:activescale transform at line 216 is outside the@media (prefers-reduced-motion: no-preference)block, so it applies even when users request reduced motion. While the transition is correctly gated (no animation), the transform itself still shifts the element. For strict WCAG 2.3.3 compliance, consider moving the transform inside the media query or adding a reduced-motion override.♻️ Proposed refactor: move transform inside the media query
-@media (prefers-reduced-motion: no-preference) { - .button { - transition: var(--rs-transition-interactive); - } - - .button:active { - transition: var(--rs-transition-pressed); - } -} +@media (prefers-reduced-motion: no-preference) { + .button { + transition: var(--rs-transition-interactive); + } + + .button:active:not(:disabled):not(.button-disabled):not(.button-loading) { + transform: scale(var(--rs-scale-pressed)); + transition: var(--rs-transition-pressed); + } +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/raystack/components/button/button.module.css` around lines 216 - 218, Move the pressed-state transform rule for .button:active:not(:disabled):not(.button-disabled):not(.button-loading) inside the existing `@media` (prefers-reduced-motion: no-preference) block, so reduced-motion users do not experience the scale shift while preserving the current behavior otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CONTRIBUTING.md`:
- Around line 225-228: Clarify the release candidate instructions around the git
pull command: show the release-branch variant explicitly, such as pulling from
origin release/x.y, instead of leaving “or the release branch” as an ambiguous
comment. Preserve the existing main-branch command for non-release workflows.
In `@packages/raystack/components/announcement-bar/announcement-bar.module.css`:
- Around line 65-67: Update the .action-btn:focus-visible rule to use the shared
--rs-focus-ring and --rs-focus-ring-offset-inset tokens instead of hardcoded
outline values, ensuring focus styling matches the project standard.
In `@packages/raystack/components/avatar/avatar.tsx`:
- Around line 182-192: Update handleLoadingStatusChange to handle the 'error'
status by resetting sawLoadingRef.current to false and setFadeIn to false,
preventing a later cached image from incorrectly fading in after a failed load.
In `@packages/raystack/components/command/command.module.css`:
- Around line 197-204: Restore the enter and exit motion rules for .dialogPopup
in command.module.css, including its transition/animation and
[data-starting-style]/[data-ending-style] state selectors, matching the
established behavior of the other dialog popups.
In `@packages/raystack/components/copy-button/copy-button.module.css`:
- Around line 47-57: Update the .sr-only accessibility utility to replace the
deprecated clip declaration with clip-path: inset(50%), while preserving the
existing visually-hidden behavior and all other declarations.
In `@packages/raystack/components/drawer/drawer.module.css`:
- Around line 21-24: Collapse each of the five multiline calc() expressions in
drawer.module.css—including the transition-duration declarations—onto a single
line so the multiplication operator is not followed by a newline, preserving the
existing values and formatting semantics.
In `@packages/raystack/components/link/link.module.css`:
- Around line 24-27: Update the `.link:hover` rule’s `text-decoration-color`
value from `currentColor` to lowercase `currentcolor` to satisfy the project’s
stylelint `value-keyword-case` rule.
In `@packages/raystack/components/meter/meter.module.css`:
- Around line 58-61: Update the --rs-meter-radius calculation so the division
operator and its operand 2 remain on the same line, resolving the stylelint
scss/operator-no-newline-after violation while preserving the existing
calculation.
In `@packages/raystack/components/progress/progress.module.css`:
- Around line 56-61: Fix the calc() formatting in .circularSvg by keeping the /
operator and its right-hand operand on the same line, while preserving the
existing spacing and calculation.
In `@packages/raystack/components/select/select-trigger.tsx`:
- Line 7: Update the `@radix-ui/react-icons` vi.mock in code-block.test.tsx to
export CaretSortIcon alongside the existing mocked icons, including
ChevronDownIcon, so tests can resolve the select trigger import.
In `@packages/raystack/components/switch/switch.module.css`:
- Around line 100-102: Add transform-origin: right center to the
.switch.small[data-checked]:active:not([data-disabled]) .thumb rule, matching
the non-small checked pressed state while preserving its existing transform.
In `@packages/raystack/components/toast/toast.module.css`:
- Line 69: Remove the empty line immediately before the transition declaration
in the toast stylesheet, keeping z-index and transition as consecutive
declarations so Stylelint passes.
---
Nitpick comments:
In `@packages/raystack/components/button/button.module.css`:
- Around line 216-218: Move the pressed-state transform rule for
.button:active:not(:disabled):not(.button-disabled):not(.button-loading) inside
the existing `@media` (prefers-reduced-motion: no-preference) block, so
reduced-motion users do not experience the scale shift while preserving the
current behavior otherwise.
In `@packages/raystack/components/chip/chip.tsx`:
- Around line 119-140: Widen the ChipProps onClick type to
React.MouseEventHandler<HTMLElement>, then update the interactive button path in
the Chip component to pass onClick directly and remove the double cast. Preserve
the existing span and button click behavior.
- Line 61: Update the non-interactive rendering path in the chip component so a
chip with onClick and isDismissible remains keyboard-accessible: add
focusability, button semantics, and Enter/Space key handling that invokes the
existing onClick behavior. Preserve the current dismissible behavior and the
existing isInteractive path for chips without this combination.
In `@packages/raystack/components/otp-field/otp-field.module.css`:
- Around line 50-56: Merge the two duplicate
.otp-field-input[data-invalid]:focus:not([data-readonly]) rules into a single
CSS block, preserving both the danger border-color and the explicit danger
box-shadow declarations.
In `@packages/raystack/components/preview-card/preview-card.module.css`:
- Around line 106-112: Remove the redundant `.positioner` rule inside the media
query in preview-card.module.css, since its opacity transition is already
defined by the base `.positioner` rule. Leave the viewport-specific positioning
rules and all other media-query behavior unchanged.
In `@packages/raystack/components/sidebar/sidebar.module.css`:
- Around line 395-417: Replace the hardcoded 0.2s ease transitions in .root,
.nav-group-panel, and .root [data-collapse-hidden] within the
prefers-reduced-motion block with the shared --rs-duration-normal and
--rs-ease-out motion tokens, matching .nav-text.
In `@packages/raystack/components/tabs/tabs.module.css`:
- Around line 115-117: Align the fallback values in the translate declarations
by replacing the unitless 0 defaults for --active-tab-left and --active-tab-top
with 0px, matching the corresponding declarations at lines 159–160.
In `@packages/raystack/styles/effects.css`:
- Around line 40-51: Confirm the intended accessibility behavior of
--rs-focus-ring across target browsers and display densities; if 0.5px can
render imperceptibly, increase the outline width to a reliably visible value
while preserving the existing focus-ring styling and shadow variant.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 580b812f-9606-4f76-81b2-2530f9e3a3c1
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (93)
.eslintrc.js.eslintrs.jsCONTRIBUTING.mdDEVELOPMENT.mdpackages/raystack/.np-config.jsonpackages/raystack/components/accordion/accordion.module.csspackages/raystack/components/amount/amount.module.csspackages/raystack/components/amount/amount.tsxpackages/raystack/components/announcement-bar/announcement-bar.module.csspackages/raystack/components/avatar/avatar.module.csspackages/raystack/components/avatar/avatar.tsxpackages/raystack/components/breadcrumb/breadcrumb.module.csspackages/raystack/components/button/button.module.csspackages/raystack/components/calendar/calendar.module.csspackages/raystack/components/calendar/calendar.tsxpackages/raystack/components/calendar/date-picker.tsxpackages/raystack/components/callout/__tests__/callout.test.tsxpackages/raystack/components/callout/callout.module.csspackages/raystack/components/callout/callout.tsxpackages/raystack/components/checkbox/checkbox.module.csspackages/raystack/components/checkbox/checkbox.tsxpackages/raystack/components/chip/__tests__/chip.test.tsxpackages/raystack/components/chip/chip.module.csspackages/raystack/components/chip/chip.tsxpackages/raystack/components/code-block/__tests__/code-block.test.tsxpackages/raystack/components/code-block/code-block.module.csspackages/raystack/components/collapsible/collapsible.module.csspackages/raystack/components/color-picker/color-picker-area.tsxpackages/raystack/components/color-picker/color-picker.module.csspackages/raystack/components/combobox/combobox-input.tsxpackages/raystack/components/combobox/combobox.module.csspackages/raystack/components/command/command-dialog.tsxpackages/raystack/components/command/command.module.csspackages/raystack/components/copy-button/copy-button.module.csspackages/raystack/components/copy-button/copy-button.tsxpackages/raystack/components/data-table/data-table.module.csspackages/raystack/components/data-view-beta/data-view.module.csspackages/raystack/components/data-view/components/ordering.tsxpackages/raystack/components/data-view/data-view.module.csspackages/raystack/components/dialog/dialog.module.csspackages/raystack/components/drawer/drawer.module.csspackages/raystack/components/empty-state/empty-state.module.csspackages/raystack/components/field/__tests__/field.test.tsxpackages/raystack/components/field/field-root.tsxpackages/raystack/components/field/field.module.csspackages/raystack/components/filter-chip/filter-chip.module.csspackages/raystack/components/floating-actions/floating-actions.module.csspackages/raystack/components/headline/headline.module.csspackages/raystack/components/icon-button/icon-button.module.csspackages/raystack/components/image/__tests__/image.test.tsxpackages/raystack/components/image/image.module.csspackages/raystack/components/image/image.tsxpackages/raystack/components/input/input.module.csspackages/raystack/components/link/link.module.csspackages/raystack/components/menu/menu.module.csspackages/raystack/components/meter/meter-track.tsxpackages/raystack/components/meter/meter.module.csspackages/raystack/components/navbar/navbar.module.csspackages/raystack/components/number-field/number-field.module.csspackages/raystack/components/otp-field/otp-field.module.csspackages/raystack/components/popover/popover.module.csspackages/raystack/components/preview-card/preview-card.module.csspackages/raystack/components/progress/progress-track.tsxpackages/raystack/components/progress/progress.module.csspackages/raystack/components/radio/radio.module.csspackages/raystack/components/radio/radio.tsxpackages/raystack/components/scroll-area/scroll-area.module.csspackages/raystack/components/select/select-item.tsxpackages/raystack/components/select/select-trigger.tsxpackages/raystack/components/select/select.module.csspackages/raystack/components/side-panel/side-panel.module.csspackages/raystack/components/sidebar/sidebar.module.csspackages/raystack/components/skeleton/skeleton.module.csspackages/raystack/components/slider/slider.module.csspackages/raystack/components/spinner/spinner.module.csspackages/raystack/components/switch/switch.module.csspackages/raystack/components/table/table.module.csspackages/raystack/components/table/table.tsxpackages/raystack/components/tabs/tabs.module.csspackages/raystack/components/text-area/text-area.module.csspackages/raystack/components/text/__tests__/text.test.tsxpackages/raystack/components/text/text.module.csspackages/raystack/components/text/text.tsxpackages/raystack/components/toast/toast.module.csspackages/raystack/components/toggle/toggle.module.csspackages/raystack/components/toolbar/toolbar.module.csspackages/raystack/components/tooltip/tooltip.module.csspackages/raystack/components/tour-beta/tour.module.csspackages/raystack/components/tour/tour-root.tsxpackages/raystack/components/tour/tour.module.csspackages/raystack/package.jsonpackages/raystack/styles/effects.csspackages/raystack/styles/typography.css
💤 Files with no reviewable changes (5)
- packages/raystack/.np-config.json
- .eslintrc.js
- .eslintrs.js
- packages/raystack/components/headline/headline.module.css
- packages/raystack/package.json
| git checkout release/x.y # for release candidate | ||
|
|
||
| # Pull latest changes | ||
| git pull origin main # or develop | ||
| git pull origin main # or the release branch |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the git pull command for release candidate branches.
Line 228 shows git pull origin main with a comment "or the release branch", but if a maintainer is on release/x.y, running this command as-is would merge main into the release branch. The command should explicitly show the release branch variant to avoid accidental cross-branch merges.
📝 Suggested fix
# Pull latest changes
- git pull origin main # or the release branch
+ git pull origin main # for production release
+ # OR
+ git pull origin release/x.y # for release candidate📝 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.
| git checkout release/x.y # for release candidate | |
| # Pull latest changes | |
| git pull origin main # or develop | |
| git pull origin main # or the release branch | |
| git checkout release/x.y # for release candidate | |
| # Pull latest changes | |
| git pull origin main # for production release | |
| # OR | |
| git pull origin release/x.y # for release candidate |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CONTRIBUTING.md` around lines 225 - 228, Clarify the release candidate
instructions around the git pull command: show the release-branch variant
explicitly, such as pulling from origin release/x.y, instead of leaving “or the
release branch” as an ambiguous comment. Preserve the existing main-branch
command for non-release workflows.
| .action-btn:focus-visible { | ||
| outline: 1px solid currentColor; | ||
| outline-offset: 2px; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use shared focus-ring tokens for consistency and fix currentcolor casing.
Two concerns:
- Stylelint flags
currentColor— CSS keyword should be lowercasecurrentcolorper the project'svalue-keyword-caserule. - The accordion and other components use
outline: var(--rs-focus-ring)andoutline-offset: var(--rs-focus-ring-offset-inset)for:focus-visible. Using hardcoded values here deviates from the PR's standardization goal.
🔧 Proposed fix
.action-btn:focus-visible {
- outline: 1px solid currentColor;
- outline-offset: 2px;
+ outline: var(--rs-focus-ring);
+ outline-offset: var(--rs-focus-ring-offset-inset);
}If currentColor is intentionally desired here (e.g., the announcement bar inherits a contextual text color), keep the approach but fix the casing:
.action-btn:focus-visible {
- outline: 1px solid currentColor;
+ outline: 1px solid currentcolor;
outline-offset: 2px;
}📝 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.
| .action-btn:focus-visible { | |
| outline: 1px solid currentColor; | |
| outline-offset: 2px; | |
| .action-btn:focus-visible { | |
| outline: var(--rs-focus-ring); | |
| outline-offset: var(--rs-focus-ring-offset-inset); | |
| } |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 66-66: Expected "currentColor" to be "currentcolor" (value-keyword-case)
(value-keyword-case)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/announcement-bar/announcement-bar.module.css`
around lines 65 - 67, Update the .action-btn:focus-visible rule to use the
shared --rs-focus-ring and --rs-focus-ring-offset-inset tokens instead of
hardcoded outline values, ensuring focus styling matches the project standard.
Source: Linters/SAST tools
| const handleLoadingStatusChange = (status: ImageLoadingStatus) => { | ||
| if (status === 'loading') { | ||
| sawLoadingRef.current = true; | ||
| setFadeIn(false); | ||
| } else if (status === 'loaded') { | ||
| // A cached image reports 'loaded' with no 'loading' phase (Base UI never | ||
| // emits it), so it stays instant; only network loads fade in. | ||
| setFadeIn(sawLoadingRef.current); | ||
| sawLoadingRef.current = false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle the 'error' loading status to reset fade state.
When the image fails to load (status === 'error'), sawLoadingRef.current is never reset. If a subsequent image is cached (reports 'loaded' without 'loading'), it will incorrectly fade in because the ref is still true from the failed attempt. Add an error branch to reset both the ref and fadeIn.
🐛 Proposed fix for unhandled error status
const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
if (status === 'loading') {
sawLoadingRef.current = true;
setFadeIn(false);
} else if (status === 'loaded') {
// A cached image reports 'loaded' with no 'loading' phase (Base UI never
// emits it), so it stays instant; only network loads fade in.
setFadeIn(sawLoadingRef.current);
sawLoadingRef.current = false;
+ } else if (status === 'error') {
+ sawLoadingRef.current = false;
+ setFadeIn(false);
}
};📝 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.
| const handleLoadingStatusChange = (status: ImageLoadingStatus) => { | |
| if (status === 'loading') { | |
| sawLoadingRef.current = true; | |
| setFadeIn(false); | |
| } else if (status === 'loaded') { | |
| // A cached image reports 'loaded' with no 'loading' phase (Base UI never | |
| // emits it), so it stays instant; only network loads fade in. | |
| setFadeIn(sawLoadingRef.current); | |
| sawLoadingRef.current = false; | |
| } | |
| }; | |
| const handleLoadingStatusChange = (status: ImageLoadingStatus) => { | |
| if (status === 'loading') { | |
| sawLoadingRef.current = true; | |
| setFadeIn(false); | |
| } else if (status === 'loaded') { | |
| // A cached image reports 'loaded' with no 'loading' phase (Base UI never | |
| // emits it), so it stays instant; only network loads fade in. | |
| setFadeIn(sawLoadingRef.current); | |
| sawLoadingRef.current = false; | |
| } else if (status === 'error') { | |
| sawLoadingRef.current = false; | |
| setFadeIn(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/avatar/avatar.tsx` around lines 182 - 192,
Update handleLoadingStatusChange to handle the 'error' status by resetting
sawLoadingRef.current to false and setFadeIn to false, preventing a later cached
image from incorrectly fading in after a failed load.
| } | ||
|
|
||
| .dialogPopup[data-starting-style], | ||
| .dialogPopup[data-ending-style] { | ||
| opacity: 0; | ||
| transform: scale(0.98); | ||
| /* Reduced transparency: drop the blur, compensate with a heavier solid scrim */ | ||
| @media (prefers-reduced-transparency: reduce) { | ||
| .overlayBlur { | ||
| backdrop-filter: none; | ||
| background-color: var(--rs-color-overlay-black-a9); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Base UI's Dialog.Popup applies transitions internally
rg -n 'transition|animation|data-starting-style|data-ending-style' \
node_modules/@base_ui/react/esm/dialog/ 2>/dev/null | head -30
# Also check the dialog.module.css for comparison — does the shared dialog animate its popup?
fd 'dialog.module.css' packages/raystack/components/dialog/ --exec rg -n 'transition|data-starting-style|data-ending-style' {}Repository: raystack/apsara
Length of output: 589
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== command.module.css around dialogPopup ==="
FILE="packages/raystack/components/command/command.module.css"
nl -ba "$FILE" | sed -n '160,230p'
echo
echo "=== dialog.module.css references to popup/state animations ==="
DIALOG_FILE="$(fd -a 'dialog.module.css' packages/raystack/components/dialog | head -n 1)"
echo "$DIALOG_FILE"
nl -ba "$DIALOG_FILE" | sed -n '1,140p'
echo
echo "=== search for Dialog.Popup / starting-ending style usage in repo ==="
rg -n 'Dialog\.Popup|data-starting-style|data-ending-style|showNestedAnimation|transition: opacity|animation:' packages/raystack/components -g '*.css' -g '*.tsx' -g '*.ts' | sed -n '1,220p'
echo
echo "=== search Base UI dialog package if present in workspace ==="
fd -a 'dialog' node_modules/@base_ui/react 2>/dev/null | head -n 50Repository: raystack/apsara
Length of output: 239
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== command.module.css around dialogPopup ==="
sed -n '160,230p' packages/raystack/components/command/command.module.css
echo
echo "=== shared dialog.module.css around animation rules ==="
DIALOG_FILE="$(fd -a 'dialog.module.css' packages/raystack/components/dialog | head -n 1)"
echo "$DIALOG_FILE"
sed -n '1,140p' "$DIALOG_FILE"
echo
echo "=== repo search for dialog popup animation/state hooks ==="
rg -n 'dialogPopup|Dialog\.Popup|data-starting-style|data-ending-style|showNestedAnimation|transition: opacity|animation:' packages/raystack/components -g '*.css' -g '*.tsx' -g '*.ts'Repository: raystack/apsara
Length of output: 20842
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== command-dialog.tsx ==="
sed -n '1,160p' packages/raystack/components/command/command-dialog.tsx
echo
echo "=== command.module.css dialogPopup block ==="
sed -n '175,220p' packages/raystack/components/command/command.module.css
echo
echo "=== Base UI dialog CSS matches already present in workspace ==="
rg -n 'dialogOverlay|dialogContent|data-starting-style|data-ending-style|transition: opacity' node_modules/@base_ui/react/esm/dialog/ 2>/dev/null | sed -n '1,120p'Repository: raystack/apsara
Length of output: 2689
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== components using DialogPrimitive.Popup ==="
rg -n 'DialogPrimitive\.Popup|DialogPrimitive\.Backdrop|DialogPrimitive\.Viewport' packages/raystack/components -g '*.tsx'
echo
echo "=== dialog-like CSS files with Popup/data-starting-style rules ==="
rg -n '(\.popup|\.dialogContent|\.drawerPopup|data-starting-style|data-ending-style)' packages/raystack/components -g '*.css' | sed -n '1,220p'Repository: raystack/apsara
Length of output: 14598
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== dialog-content.tsx ==="
sed -n '1,120p' packages/raystack/components/dialog/dialog-content.tsx
echo
echo "=== alert-dialog-content.tsx ==="
sed -n '1,120p' packages/raystack/components/alert-dialog/alert-dialog-content.tsx
echo
echo "=== alert-dialog.module.css if present ==="
fd -a 'alert-dialog.module.css' packages/raystack/components | while read -r f; do
echo "$f"
sed -n '1,220p' "$f"
doneRepository: raystack/apsara
Length of output: 3416
Restore dialogPopup enter/exit styles
packages/raystack/components/command/command.module.css:183-204 no longer defines any transition, animation, or [data-starting-style]/[data-ending-style] rules for .dialogPopup, so the command dialog now opens/closes without motion. Add the popup enter/exit styles back if this should match the other dialog popups.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/command/command.module.css` around lines 197 -
204, Restore the enter and exit motion rules for .dialogPopup in
command.module.css, including its transition/animation and
[data-starting-style]/[data-ending-style] state selectors, matching the
established behavior of the other dialog popups.
| .sr-only { | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; | ||
| overflow: hidden; | ||
| clip: rect(0, 0, 0, 0); | ||
| white-space: nowrap; | ||
| border: 0; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace deprecated clip with clip-path.
Stylelint flags clip: rect(0, 0, 0, 0) as deprecated. Use the modern clip-path: inset(50%) equivalent.
♻️ Proposed fix
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
- clip: rect(0, 0, 0, 0);
+ clip-path: inset(50%);
white-space: nowrap;
border: 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.
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip-path: inset(50%); | |
| white-space: nowrap; | |
| border: 0; | |
| } |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 54-54: Deprecated property "clip" (property-no-deprecated)
(property-no-deprecated)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/copy-button/copy-button.module.css` around lines
47 - 57, Update the .sr-only accessibility utility to replace the deprecated
clip declaration with clip-path: inset(50%), while preserving the existing
visually-hidden behavior and all other declarations.
Source: Linters/SAST tools
| --rs-meter-radius: calc( | ||
| (var(--rs-space-14) - var(--rs-meter-track-size) * 2) / | ||
| 2 | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix stylelint scss/operator-no-newline-after error: keep / and 2 on the same line.
Stylelint reports Unexpected newline after "/" at line 59. The / operator should not be followed by a newline.
🛡️ Proposed fix
.circularSvg {
--rs-meter-track-size: var(--rs-space-2);
--rs-meter-radius: calc(
- (var(--rs-space-14) - var(--rs-meter-track-size) * 2) /
- 2
+ (var(--rs-space-14) - var(--rs-meter-track-size) * 2) / 2
);📝 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.
| --rs-meter-radius: calc( | |
| (var(--rs-space-14) - var(--rs-meter-track-size) * 2) / | |
| 2 | |
| ); | |
| --rs-meter-radius: calc( | |
| (var(--rs-space-14) - var(--rs-meter-track-size) * 2) / 2 | |
| ); |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 59-59: Unexpected newline after "/" (scss/operator-no-newline-after)
(scss/operator-no-newline-after)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/meter/meter.module.css` around lines 58 - 61,
Update the --rs-meter-radius calculation so the division operator and its
operand 2 remain on the same line, resolving the stylelint
scss/operator-no-newline-after violation while preserving the existing
calculation.
Source: Linters/SAST tools
| .circularSvg { | ||
| --rs-progress-track-size: var(--rs-space-2); | ||
| --rs-progress-radius: calc((var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2); | ||
| --rs-progress-radius: calc( | ||
| (var(--rs-space-14) - var(--rs-progress-track-size) * 2) / | ||
| 2 | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix stylelint error: newline after / operator in calc().
Stylelint reports scss/operator-no-newline-after on line 59. The / operator should not be followed by a newline. This is a linting error that could fail CI.
🔧 Proposed fix
--rs-progress-radius: calc(
- (var(--rs-space-14) - var(--rs-progress-track-size) * 2) /
- 2
+ (var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2
);📝 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.
| .circularSvg { | |
| --rs-progress-track-size: var(--rs-space-2); | |
| --rs-progress-radius: calc((var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2); | |
| --rs-progress-radius: calc( | |
| (var(--rs-space-14) - var(--rs-progress-track-size) * 2) / | |
| 2 | |
| ); | |
| .circularSvg { | |
| --rs-progress-track-size: var(--rs-space-2); | |
| --rs-progress-radius: calc( | |
| (var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2 | |
| ); |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 59-59: Unexpected newline after "/" (scss/operator-no-newline-after)
(scss/operator-no-newline-after)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/progress/progress.module.css` around lines 56 -
61, Fix the calc() formatting in .circularSvg by keeping the / operator and its
right-hand operand on the same line, while preserving the existing spacing and
calculation.
Source: Linters/SAST tools
| .switch.small[data-checked]:active:not([data-disabled]) .thumb { | ||
| transform: translateX(var(--rs-space-4)) scaleX(1.15); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Missing transform-origin on small checked pressed state.
.switch.small[data-checked]:active:not([data-disabled]) .thumb omits transform-origin: right center, which is present on the non-small checked variant (line 97). Without it, the default 50% 50% applies and the thumb stretches from center rather than from the right edge — inconsistent with the iOS-like behavior described in the comment on line 87.
✏️ Proposed fix
.switch.small[data-checked]:active:not([data-disabled]) .thumb {
transform: translateX(var(--rs-space-4)) scaleX(1.15);
+ transform-origin: right center;
}📝 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.
| .switch.small[data-checked]:active:not([data-disabled]) .thumb { | |
| transform: translateX(var(--rs-space-4)) scaleX(1.15); | |
| } | |
| .switch.small[data-checked]:active:not([data-disabled]) .thumb { | |
| transform: translateX(var(--rs-space-4)) scaleX(1.15); | |
| transform-origin: right center; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/switch/switch.module.css` around lines 100 -
102, Add transform-origin: right center to the
.switch.small[data-checked]:active:not([data-disabled]) .thumb rule, matching
the non-small checked pressed state while preserving its existing transform.
| z-index: calc(1000 - var(--toast-index)); | ||
|
|
||
| transition: opacity 500ms; | ||
| transition: opacity var(--rs-duration-slow) var(--rs-ease-out); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the blank line before this declaration to satisfy Stylelint.
The empty line at line 68 (between z-index and transition) trips declaration-empty-line-before and will fail lint/CI.
🧹 Proposed fix
z-index: calc(1000 - var(--toast-index));
-
transition: opacity var(--rs-duration-slow) var(--rs-ease-out);📝 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.
| transition: opacity var(--rs-duration-slow) var(--rs-ease-out); | |
| z-index: calc(1000 - var(--toast-index)); | |
| transition: opacity var(--rs-duration-slow) var(--rs-ease-out); |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 69-69: Expected no empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/toast/toast.module.css` at line 69, Remove the
empty line immediately before the transition declaration in the toast
stylesheet, keeping z-index and transition as consecutive declarations so
Stylelint passes.
Source: Linters/SAST tools
The code-block language selector renders a Select, whose trigger now uses CaretSortIcon; add it to the test's @radix-ui/react-icons mock.
…README - Gate a handful of ungated transforms/animations behind prefers-reduced-motion so reduced-motion users get a plain fade like the rest of the library: dialog nested-scale, empty-state entrance, preview-card morph, sidebar chevron, and the tour cards. - Remove dead and redundant rules across components: unused classes (menu/meter/progress/tooltip), no-op hover/focus/disabled overrides, duplicated declarations, and a stale commented-out block. - Consolidate the duplicated light-theme shadow tokens into one grouped selector so they cannot drift. - Rewrite the package README.
Summary
A design-system-wide audit of the component library, consolidating the animation work and the broader interaction/craft pass into a single branch.
What's covered
:focus-visiblering across the library.styles/effects.css; Inter body tracking and the h1–h4 type ramp retuned.Related issues
Closes #681 — consistent, tokenized
:focus-visiblering applied across the library.Closes #621 — Image error/loading handling: one-shot fallback guard plus load-state fade-in.
Closes #625 — Link's remaining items (focus-visible, reduced-motion) are now done.
Partially addresses:
TableRownow uses CVA and gains row-state hooks. Cell truncation accessibility is still open.announementBartypo are still open.Test plan
pnpm buildpasses.@base-uimodule-resolution failure affects a handful of unrelated test files onmainas well.