feat(sdk): neutral, container-query loading skeleton#64
Conversation
Rework the embed loading skeleton so it conveys only the *shape* the iframe occupies rather than mirroring the interview UI's internal layout (welcome card, message, input pill), which drifted every time that UI changed. - Draws a single "interview box": a centered box on wide slots, a calm full-bleed surface on narrow ones (slider / popup / float / mobile). - Responsive via a CSS container query on the slot the consumer provides — not a viewport media query, since the skeleton lives in the host page DOM as an overlay sibling of the cross-origin iframe. The breakpoint (672px) matches the interview app's own so there's no shape jump at handoff. - The only animated element is a thin shimmer bar near the top, so loading is signalled without a heavy full-area sweep (which read especially strong full-bleed on mobile). Honors prefers-reduced-motion. - Default background matches the app's default interview surface (#f5f2f0 light, #15171e dark) so there's no color flash before the iframe paints; a custom brand.bg still takes precedence. - The box's vertical inset uses a percentage inset on the absolutely-positioned content wrapper, so it tracks the slot's height (not the viewport, and not width as padding/margin percentages would). - Per-instance colors ride in as CSS custom properties so one shared stylesheet serves every embed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kJptBdnAQ2VfzrE9ob1Wb
📦 Snapshot ReleasePublished snapshot packages for this PR: pnpm add @perspective-ai/sdk@1.12.1-pr-64-20260702200405
pnpm add @perspective-ai/sdk-react@1.12.1-pr-64-20260702200405Or use the npm tag (always points to the latest snapshot from this PR): pnpm add @perspective-ai/sdk@pr-64
pnpm add @perspective-ai/sdk-react@pr-64 |
There was a problem hiding this comment.
Pull request overview
This PR updates the SDK’s embed loading skeleton to be a neutral, container-query–responsive overlay that reflects only the iframe’s outer “shape” (boxed on wide slots, full-bleed on narrow slots), with a calmer single-bar shimmer animation and updated default background colors aligned to the interview app’s surface tokens.
Changes:
- Replaced the detailed, UI-mirroring skeleton DOM with a minimal structure (content/surface + single shimmer bar) and moved layout/animation to a shared injected stylesheet driven by container queries.
- Updated default light/dark background and shimmer/border translucency values to match the app’s interview surface and reduce visual flash at handoff.
- Rewrote unit tests to assert the new structure, shared-style injection, container-query usage, and reduced-motion behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/sdk/src/loading.ts | Reworks skeleton implementation to a generic, container-query–responsive overlay with a shared injected stylesheet and per-instance CSS variables. |
| packages/sdk/src/loading.test.ts | Updates tests to validate the new DOM structure, stylesheet injection, and animation/scoping behavior. |
| .changeset/generic-loading-skeleton.md | Adds a patch changeset describing the new neutral skeleton behavior and compatibility notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function injectStyles(): void { | ||
| if (stylesInjected || !hasDom()) return; | ||
| stylesInjected = true; | ||
|
|
There was a problem hiding this comment.
Leaving this as-is intentionally. The module-flag guard here mirrors the existing convention in styles.ts (injectStyles() there uses the same if (stylesInjected) return; pattern for the SDK's core <style id="perspective-embed-styles">). The two scenarios raised apply equally to that core injector and are low-impact: duplicate <style> tags from multiple SDK copies still apply their CSS correctly (id-uniqueness isn't enforced for stylesheet application), and node removal is already the accepted trade-off repo-wide. Switching only this injector to a DOM-presence check would make the two inconsistent, and changing both is out of scope for this PR. Happy to do a follow-up that converts both injectors to a DOM-presence guard if we want that hardening across the board.
Generated by Claude Code
The "insets the box via a descendant" test read the injected stylesheet without first creating a skeleton, so it relied on an earlier test having injected it and would fail when run in isolation. Create a skeleton first so the shared stylesheet is present regardless of test order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kJptBdnAQ2VfzrE9ob1Wb
Summary
Reworks the embed loading skeleton so it conveys only the shape the iframe occupies rather than mirroring the interview UI's internal layout (welcome card, message, input pill) — the old skeleton drifted every time that UI changed.
672px) matches the interview app's own (verified against the live app), so there's no shape jump at handoff.prefers-reduced-motion.#f5f2f0light,#15171edark, itsbg-interview-bgtoken). A custombrand.bgstill takes precedence.inseton the absolutely-positioned content wrapper, so it tracks the slot's height — not the viewport (vh), and not width (aspadding/marginpercentages would).container-type: size/cqhwere rejected: underinline-sizethe block axis isn't queryable, so height container-units fall back to the viewport anyway.Why
The previous skeleton reproduced the interview's welcome card, message lines, and input pill. Each of those is a moving target — any UI change to the app left the loading state visibly out of sync. This version commits to the stable outer shape only, which is far less likely to drift, while still reserving the right layout per embed size.
Compatibility
Uses
@container(Baseline 2023) for the layout switch, withposition: absolute+ percentage insets andclamp()(Baseline 2020) for the height-relative spacing — all older than the container query that gates them. Browsers too old for@containerfall back to the compact full-bleed layout (they never reach the boxed branch). The SDK already injects its core styles via a<style>element, so the skeleton's shared stylesheet introduces no new CSP requirement.Testing
packages/sdkunit suite: 411 passed (loading.test.tsrewritten for the new structure)..perspective-loadingroot class (preserved) detaching at handoff — unaffected.Changeset
@perspective-ai/sdk— patch (loading-UX polish; no public API change — the only removal, the unusedappearanceoption, is on a non-exported type).🤖 Generated with Claude Code
Generated by Claude Code