Skip to content

feat(sdk): neutral, container-query loading skeleton#64

Open
LEVI-RIVKIN wants to merge 2 commits into
mainfrom
claude/sdk-iframe-skeleton-ywhs0k
Open

feat(sdk): neutral, container-query loading skeleton#64
LEVI-RIVKIN wants to merge 2 commits into
mainfrom
claude/sdk-iframe-skeleton-ywhs0k

Conversation

@LEVI-RIVKIN

Copy link
Copy Markdown
Contributor

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.

  • Generic shape, not internals. Draws a single "interview box": a centered box on wide slots, a calm full-bleed surface on narrow ones (slider / popup / float / mobile).
  • Container-query responsive. The skeleton renders in the host page DOM as an overlay sibling of the cross-origin iframe, so responsiveness is driven by a CSS container query on the consumer-provided slot — not a viewport media query, which would wrongly track the host page. The breakpoint (672px) matches the interview app's own (verified against the live app), so there's no shape jump at handoff.
  • Calm animation. 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.
  • No background flash. The default background matches the app's default interview surface (#f5f2f0 light, #15171e dark, its bg-interview-bg token). A custom brand.bg still takes precedence.
  • Height-relative inset. 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 (vh), and not width (as padding/margin percentages would). container-type: size / cqh were rejected: under inline-size the block axis isn't queryable, so height container-units fall back to the viewport anyway.
  • One shared stylesheet. Per-instance colors ride in as CSS custom properties, so a single injected stylesheet serves every embed.

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, with position: absolute + percentage insets and clamp() (Baseline 2020) for the height-relative spacing — all older than the container query that gates them. Browsers too old for @container fall 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/sdk unit suite: 411 passed (loading.test.ts rewritten for the new structure).
  • Typecheck, oxlint, prettier: clean.
  • Existing e2e specs depend only on the .perspective-loading root class (preserved) detaching at handoff — unaffected.
  • Verified in a real browser (Chromium) against a live research id across all embed types, both light/dark themes, and tall / short / narrow slots; empirically confirmed the vertical inset scales with slot height rather than the viewport.

Changeset

@perspective-ai/sdk — patch (loading-UX polish; no public API change — the only removal, the unused appearance option, is on a non-exported type).

🤖 Generated with Claude Code


Generated by Claude Code

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
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

📦 Snapshot Release

Published 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-20260702200405

Or 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread packages/sdk/src/loading.test.ts
Comment on lines +110 to 113
function injectStyles(): void {
if (stylesInjected || !hasDom()) return;
stylesInjected = true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
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.

2 participants