Skip to content

fix: Design audit — motion, states, focus & craft across the library#853

Closed
ravisuhag wants to merge 41 commits into
improve-animationsfrom
design-audit-plans
Closed

fix: Design audit — motion, states, focus & craft across the library#853
ravisuhag wants to merge 41 commits into
improve-animationsfrom
design-audit-plans

Conversation

@ravisuhag

@ravisuhag ravisuhag commented Jul 12, 2026

Copy link
Copy Markdown
Member

Design audit — library-wide motion, states, focus & craft

This branch implements the approved fixes from a design audit of every
component in @raystack/apsara. 41 source-only commits, all under
packages/raystack/.

Stacked on improve-animations (this PR is based on that branch), so the diff
here is just the audit work.

What changed, by phase

  • Phase 1 — Foundations: heading line-heights, sidebar width-transition
    scope, motion tokens + a fast press tier, monotonic typography tracking/leading.
  • Phase 2 — System passes: distinct :active pressed states, :focus-visible
    rings, and 24px minimum hit targets across the library.
  • Phase 3 — Component fixes: CopyButton timer + cross-fade, data-table sort
    icon, indeterminate Progress, number-field states, drawer swipe/fades,
    reduced-motion & reduced-transparency fallbacks, tooltip/callout motion,
    select/combobox, toggle, table row states, code-block copy, link states,
    calendar, sticky headers, and more.
  • Phase 4 — Polish: preview-card, scroll-area thumb, side-panel overflow,
    popover shadow, breadcrumb, tabs indicator, announcement bar, headline width
    scope, avatar/image, tour overlay exit, floating actions, spinner speed.

Notes

  • Draft — for review of the source changes.
  • The local before/after playground (apps/playground), the implementation
    plans, and the audit report are intentionally not included — they are
    local working tools, not part of this change.

ravisuhag added 30 commits July 12, 2026 10:47
h1-h3 all used --rs-line-height-t1 (24px) regardless of font size, so a
32px h1 overlapped its own wrapped lines. Point each heading at its
matching t4/t3/t2 line-height; h4 already paired correctly.

Plan: plans/001-heading-line-heights.md
A later transition:width rule on .root, .nav-item, .nav-text, and
.resizeHandle replaced (not merged with) each element's own transition,
silently killing nav-item hover/press fades, the nav-text opacity fade,
and the resize-handle hover fade for every motion-enabled user. Scope
the width transition to .root, the only element that actually resizes.

Plan: plans/002-sidebar-width-transition-scope.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.

Plan: plans/003-motion-tokens-and-press-tier.md
Tracking ran backwards (16px large carried more spacing than 14px
regular) and titles had zero tracking at every size. Leading was
non-monotonic too (mini looser than small; t1 tighter than every larger
title). Re-step both ramps: body tracking shrinks toward neutral as
size grows, titles step progressively negative in em; body leading
rises smoothly to the 16px peak, title leading falls as display size
grows.

NEEDS VISUAL QA: this reflows all text across every consuming app.
Requires a designer eyeball pass in apps/www (docs page, data-table
page, t1-t4 headings) before this ships to production, per the plan's
verification section.

Plan: plans/006-typography-tracking-leading.md
…nents

:active either duplicated :hover or didn't exist, so pointer-down gave
no acknowledgment before an action's result appeared. Add a pressed
vocabulary: one shade darker background where the hover ladder allows
it, otherwise a subtle scale/stretch (button 0.97, checkbox/radio 0.92,
switch thumb stretch, chip dismiss 0.9, slider thumb grow+lift).

Also fold in the slider part of audit #55: delete a dead
.thumb:hover { border-color } rule and add grab feedback on the thumb
children (position itself stays transition-free for 1:1 drag).

Plans: plans/004-pressed-states-pass.md
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
Small checkbox/radio (12px), icon-button sizes 1-2 (12/16px), toggle
sizes 1-2 (12/16px), and the slider's small thumb (8x16px) all render
below the 24x24px WCAG 2.5.8 minimum. Add an invisible, absolutely
positioned ::before on each that pads the hit box to 24x24px; visible
rendering and layout are unchanged.

Plan: plans/018-hit-targets.md
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.

Plan: plans/007-copy-button.md
ordering.tsx referenced styles['display-popover-sort-icon'], a class
that never existed in data-table.module.css (it only lived in
data-view's module), so the icon rendered unsized. Add the class.

Also replace the two-icon swap (TextAlignBottomIcon <-> TextAlignTopIcon)
with a single icon rotated 180deg on sort direction, in both data-table
and data-view's identical ordering component — reads as one state
flipping instead of the icon being replaced.

Plan: plans/008-data-table-sort-icon.md
- 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.

Plan: plans/009-misc-css-fixes.md
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.

Plan: plans/010-progress-meter.md
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.

Plan: plans/011-number-field-states.md
…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.

Plan: plans/012-drawer-swipe-and-fades.md
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).

Plan: plans/013-reduced-motion-and-transparency.md
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).

Plan: plans/014-tooltip-transitions.md
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.

Plan: plans/015-callout-enter-exit.md
.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).

Plan: plans/016-sidebar-collapse-fades.md
…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.

Plan: plans/017-color-picker.md
Field's error message popped in with no transition and unmounted the
description in the same frame -- a double layout jolt right when the
user needed to read what went wrong. Keep the description mounted
alongside the error, and animate the error's appearance with a
grid-row height-expand + fade (@starting-style, 150ms, instant under
reduced motion). Disappearing stays instant -- only the appearance
animates.

DatePicker set an "Invalid date" error but only ever surfaced it as
aria-invalid (a border tint) -- the message itself was never shown.
Render it as reserved-space helper text below the default input, styled
like Field's own error text, announced via role="alert".

Plan: plans/019-field-validation.md
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.

Plan: plans/020-chip-interactive.md
…otation

The select positioner painted an opaque background behind the
0.97-scale popup, so a square flash of it stuck out past the shrunken
popup corners during enter/exit. Drop the background, matching
combobox's positioner.

Multi-select options nested a real, focusable Checkbox inside an
element with role="option" -- nested interactives confuse screen
readers and add a second tab stop inside the listbox. Replace it with
a non-interactive, aria-hidden span that reproduces the checkbox's
exact resting visuals; selection is already conveyed by aria-selected
on the option itself.

Neither the select trigger's nor the combobox input's chevron rotated
on open. Add a 180deg rotation keyed off Base UI's data-popup-open
(select trigger directly; combobox needs a :has() hook since
data-popup-open lands on the sibling <input>, not an ancestor of the
chevron), reduced-motion-gated.

Plan: plans/021-select-combobox.md
…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>).

Plan: plans/022-toggle-states.md
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.

Plan: plans/023-data-row-states.md
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.

Plan: plans/024-code-block-copy.md
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.

Plan: plans/025-amount-tabular-nums.md
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.

Plan: plans/026-link-states.md
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.

Plan: plans/027-calendar-polish.md
Sticky headers and group anchors in table, data-table, and data-view
drew an always-visible hairline even at rest (scroll position 0), when
nothing is actually floating over anything yet. Use CSS scroll-driven
animations (animation-timeline: scroll(), gated behind @supports) to
fade the hairline in across the first 24px of scroll -- it tracks
scroll position 1:1 as state, not time-based motion, so it stays
active under prefers-reduced-motion and degrades to today's static
edge in browsers without support.

data-table's non-virtualized .stickySectionHeader is deliberately left
alone: its many instances stick at different per-element scroll
offsets that a single scroll() timeline can't express (documented in
a comment; @container scroll-state(stuck: top) is the future fix once
support matures). data-view-beta is untouched per the audit's Skip
decision on its doubled divider.

Plan: plans/028-sticky-scroll-edges.md
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.

Plan: plans/029-preview-card-transforms.md
Hovering the scrollbar grew its width/height from 4px to 6px, reflowing
the viewport content every hover (the scrollbar is a real flex sibling
of the viewport). Keep the track a constant 6px (transparent, so the
extra gutter is invisible) and scale the thumb itself between 0.667
and full size, anchored to the outer edge -- compositor-only, no
layout, content no longer shifts.

The thumb also never changed color on hover or drag, giving no
acknowledgment that it was grabbed. Add one overlay step per state
(a5 resting -> a6 hover -> a7 pressed via :active, since Base UI
exposes no data-pressed attribute for this component).

Plan: plans/030-scroll-area-thumb.md
ravisuhag added 11 commits July 12, 2026 12:25
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.

Plan: plans/031-side-panel-overflow.md
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.

Plan: plans/032-popover-shadow.md
Hover color snapped instantly instead of easing like every other
interactive element in the library. Add a fast-tier color transition.

The active crumb's font-weight: medium was a no-op -- the root already
set the whole breadcrumb to medium, so every crumb rendered identically.
Drop the root to regular so the active item's medium weight (plus its
existing color difference) actually makes the current page stand out.

Plan: plans/033-breadcrumb-polish.md
…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.

Plan: plans/034-tabs-variant-indicator.md
The action button -- the bar's only interactive element -- had no
hover, active, or focus-visible styles at all. Add opacity-step
hover/press feedback and a currentColor focus ring (the house accent
ring would vanish against the bar's colored backgrounds).

Add an opt-in dismissible prop mirroring Callout's controlled/
uncontrolled pattern: dismissible shows a close IconButton; onDismiss
makes removal consumer-owned (bar stays mounted, collapsed) or the bar
removes itself. Exit is a grid-row height-collapse + fade over
--rs-duration-normal, instant under reduced motion, so content below
glides up instead of snapping when the bar disappears.

Plan: plans/035-announcement-bar.md
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.

Plan: plans/036-headline-width-scope.md
…dius

Fallback initials used fixed body-text tracking (+0.5px) and a fixed
24px line-height at every avatar size -- visibly gappy at large sizes
(36px initials in a 120px avatar) where display type wants neutral to
negative tracking, and simply wrong at both extremes. Add a
--fallback-letter-spacing custom property per size variant (same
pattern as the existing --fallback-font-size), stepping from +0.05em
at the smallest size to -0.01em at the largest; line-height moves to a
unitless 1 since flex centering already handles vertical alignment.

.avatar-full used !important only to beat the size variants' own
border-radius at equal specificity. Use .avatar.avatar-full (two
classes) instead -- both classes always co-occur, so it wins regardless
of source order without needing !important.

Plan: plans/037-avatar-fallback-typography.md
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.

Plan: plans/038-image-load-fade-and-guard.md
Escape/programmatic close used to snap the spotlight dim away
instantly; now it holds the last-painted hole and fades it out over
FADE_OUT_MS (150ms, matching --rs-duration-fast), mirroring the enter
animation. A mid-tour orphan (target unmounts while still active)
still hides instantly with no fade, and no longer leaves a stale
frame behind for a later real close to replay.

Ref: plans/039-tour-overlay-exit.md
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).

Ref: plans/040-floating-actions-enter-exit.md
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.

Ref: plans/041-spinner-speed-feel-check.md
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Jul 12, 2026 11:59am

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87a2437c-a227-4416-83b4-08d9d157aa17

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@ravisuhag ravisuhag changed the title fix: Apple-design audit — motion, states, focus & craft across the library fix: Design audit — motion, states, focus & craft across the library Jul 12, 2026
@ravisuhag

Copy link
Copy Markdown
Member Author

Superseded by #856, which consolidates this design pass and the animation work (#852) into a single branch with review changes folded in.

@ravisuhag ravisuhag closed this Jul 12, 2026
@ravisuhag ravisuhag deleted the design-audit-plans branch July 12, 2026 21:34
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