Add skeleton component - #135
Conversation
|
Warning Review limit reached
Next review available in: 15 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: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis PR adds a new Skeleton loading placeholder component to void_ui, including a builder API ( ChangesSkeleton component
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Masaga
left a comment
There was a problem hiding this comment.
Review — skeleton component
Really clean, idiomatic work — textbook two-layer split, #[must_use] builder, theme passed at .render(), rebuild diffs each field to a targeted set_* mutation, and the anim loop mirrors SpinnerWidget idiom-for-idiom. A couple of the things I suspected were bugs turned out to be correct convention-matching: measure()'s _ => Length::px(0.0) fill fallback is identical to separator, and the u32::try_from(interval)… nanosecond handling is copied verbatim from spinner. Nice.
One thing I'd treat as a change request, and a few follow-ups.
1. Tests — the one blocker by our bar
There are no #[cfg(test)] tests in the PR, and there's genuinely testable pure logic here:
Skeleton::renderdefaulting — height →size_body * 1.2, color →surface_hi/surface_2bysecondary, explicitcoloroverridingsecondary,circle()settingw == h == diameter, radius default.animated(bool)→Pulse/Nonemapping.- Ideally a masonry
TestHarnesssmoke test that mount doesn't panic and the hidden-from-AT node holds.
We've been unit-testing this kind of behavior everywhere else (row_click Enter fallback, data_grid alignment, collection id resolution), so this is really just holding skeleton to the same line.
Happy to write these for you — say the word and I'll either push to this branch or hand you a ready-to-drop test module, whichever you prefer.
2. animated(true) silently clobbers .wave() (minor API sharp edge)
.wave().animated(true) resolves to Pulse, discarding the wave. It's documented, but it's an order-dependent footgun. Could make animated(true) a no-op when already animating (only false → None is unambiguous).
3. Reduced motion — follow-up issue, not a blocker here
Pulse/wave animate unconditionally; the only opt-out is per-instance .animated(false). prefers-reduced-motion / WCAG 2.3.3 wants this suppressible. This is a codebase-wide gap — spinner has the same behavior — so not fair to pin on this PR; worth filing an issue for a theme-level motion token both components honor.
4. A11y: no loading cue vs. spinner (doc-worthy)
node.set_hidden() makes each skeleton invisible to assistive tech — defensible (beats empty generic containers), but spinner exposes Role::ProgressIndicator, so a screen full of skeletons announces nothing is loading while a spinner would. The host has to supply aria-busy / a live region. Worth stating that expectation in the module docs.
5. Repaint scales with count (note, not a blocker)
Each skeleton runs its own 60fps request_anim_frame + full repaint. Unlike spinners, skeletons show up in bulk (a 50-row loading table = 50 always-repainting widgets). Matches spinner precedent so fine to ship, but a future shared-clock/coalesced-repaint pass may be worth it.
Nits: the doc example is duplicated verbatim in mod.rs and view.rs; all skeletons pulse in phase-lock (t: 0.0) which can read a touch mechanical vs. a slight per-instance stagger — pure taste.
Net: great component, only #1 stands between it and our usual bar. Everything else is a follow-up issue, a doc line, or optional polish.
🤖 Review generated by Claude Code, posted on Chris's behalf.
animated(true) unconditionally forced Pulse, so .wave().animated(true) silently discarded the wave — an order-dependent footgun. Re-enable the default pulse only from the off state; false still unambiguously disables. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155b9d9 to
4e27cd1
Compare
Skeletons hide themselves from AT, so unlike spinner (ProgressIndicator) a screen of them announces nothing loading or loaded. Spell out that the host must supply the loading cue (aria-busy / live region). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Bug Fixes