Repository navigation
Anchor the Respond.io launcher at one fixed inset across all states - #235
Conversation
The closed launcher jumped ~36px toward the corner whenever the teaser or panel appeared: our data-launcher-only rule translated the marked iframe 36px right/down (issue #125 calibration), and the vendor drops that marker — and the translate — as soon as the same iframe grows into the prompt card. Closed sat at a cramped ~13px edge while every other state rested at the vendor's 43px inset. The translate is now 6px toward the corner, landing the closed launcher's edge on the same 43px inset the prompt and open states use — one fixed anchor for the circle in every state. The clip-path hit-region shrink is unchanged, no animation added, and the prompt/open iframes keep vendor positioning. E2E stand-ins now assert the shared 43px anchor for launcher and prompt states. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Reviewer's GuideThe PR changes the closed-state launcher calibration from a 36px inward shift to a scoped 6px nudge, aligning its effective position with the teaser and open states at a shared 43px inset without altering vendor geometry or hit-region behavior. Tests and a new multi-viewport verification script validate the invariant across states. Sequence diagram for verifying the launcher anchor across statessequenceDiagram
participant Verify as Anchor verification
participant Page
participant CSS as globals.css
participant Widget as Webchat Widget
Verify->>Page: injectState(closed)
Page->>CSS: apply data-launcher-only rule
CSS-->>Widget: transform: translate(6px, 6px)
Verify->>Page: injectState(teaser)
Page->>Widget: use vendor geometry at 43px inset
Verify->>Page: injectState(open)
Page->>Widget: use vendor geometry at 43px inset
Verify->>Page: markerAnchor()
Page-->>Verify: compare marker coordinates across states
State diagram for the fixed chat launcher anchorstateDiagram-v2
[*] --> Closed
Closed --> Teaser: vendor prompt appears
Teaser --> Open: user opens panel
Open --> Closed: panel closes
state Closed {
[*] --> LauncherOnlyIframe
LauncherOnlyIframe: data-launcher-only
LauncherOnlyIframe: translate(6px, 6px)
LauncherOnlyIframe: effective inset 43px
}
state Teaser {
[*] --> PromptIframe
PromptIframe: vendor inset 43px
}
state Open {
[*] --> OpenIframe
OpenIframe: vendor inset 43px
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe launcher-only iframe now uses a 6px translate on both axes. Tests check the shared 43px anchor. The footer’s bottom padding increases to 7.5rem. A Playwright script injects closed, teaser, and open states at three viewport sizes and records geometry and screenshots. ChangesChat launcher anchor
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The launcher alignment and footer clearance have no established merge-blocking issue. Live-widget verification remains useful before rollout. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="app/globals.css" line_range="400" />
<code_context>
*/
iframe[title="Webchat Widget"][data-launcher-only] {
- transform: translate(36px, 36px);
+ transform: translate(6px, 6px);
clip-path: inset(25% 0 0 25%);
}
</code_context>
<issue_to_address>
**Footer links become unclickable**
When the launcher is visible while a visitor reaches the footer, the `translate(6px, 6px)` rule places the clipped iframe hit region about 110px from the viewport bottom, while the footer reserves only 88px below its bottom bar. The iframe overlaps bottom-bar links and intercepts clicks, so visitors cannot activate them.
Adjust the iframe hit region or footer clearance so the launcher cannot intercept clicks on footer links.
</issue_to_address>
### Comment 2
<location path="verify-chat-anchor.mjs" line_range="39-40" />
<code_context>
+ // ~5px inside the iframe's bottom-right corner.
+ const m = document.createElement("div");
+ m.id = "anchor-marker";
+ const r = f.getBoundingClientRect();
+ m.style.cssText = `position:fixed;left:${r.right - 63}px;top:${r.bottom - 63}px;width:58px;height:58px;border-radius:50%;background:#0b72de;z-index:10000;pointer-events:none`;
+ document.body.appendChild(m);
+ }, kind);
</code_context>
<issue_to_address>
**Launcher movement goes unverified**
When the real launcher has a state-dependent offset inside its iframe, `injectState` places each synthetic marker at `iframe.right/bottom - 63px`, and `markerAnchor` measures that marker instead of the vendor circle. The reported coordinates stay identical across states, so the verification passes even when the real launcher moves.
Measure the real launcher circle across states instead of placing and measuring a marker at a fixed offset.
</issue_to_address>
### Comment 3
<location path="app/globals.css" line_range="400" />
<code_context>
*/
iframe[title="Webchat Widget"][data-launcher-only] {
- transform: translate(36px, 36px);
+ transform: translate(6px, 6px);
clip-path: inset(25% 0 0 25%);
}
</code_context>
<issue_to_address>
**Footer test documents obsolete inset**
When a maintainer uses the footer-widget test's geometry comment to understand or update launcher behavior, `tests/e2e/footer-widgets.spec.ts` still describes a `translate(36px, 36px)` and a 13px iframe edge, while this rule now translates 6px and leaves a 43px edge. Maintainers reading that test get a conflicting account of the launcher geometry.
Update the footer-widget test's geometry comment to describe the 6px translation and 43px inset.
</issue_to_address>Sourcery assessment
Approval pending. 2 findings to address first.
Blocking findings: app/globals.css:400, verify-chat-anchor.mjs:40
Moving the launcher off the corner lifts its clipped hit region: the zone top rose from ~vh-80 to ~vh-110.5, so the footer's 5.5rem bottom padding let the last content row slide beneath the circle at scroll-bottom (footer-widgets caught Google Reviews inside the zone). 7.5rem restores a ~9px gap at every width. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PR #235 stopped the state-dependent jumping but anchored to the wrong target: the 43px value came from the vendor's teaser iframe inset, not from where the launcher circle should rest. Re-measured on production, the vendor anchors the same iframe at right/bottom:43px in every steady state (launcher 90x90, prompt ~330x178, open panel ~400x600), briefly mounts at 25px before remote config lands, and goes full-bleed 0px for the open panel on small portrait viewports. The ~58px circle sits ~4px inside the iframe's bottom-right corner, so 43px of edge clearance read as ~47px of visible inset — detached from the corner. Replace the static CSS nudge with an adaptive anchor: the geometry watcher now measures the vendor's own right/bottom inset on every sync and applies an inline translate that lands the iframe edge at RESPOND_IO_ANCHOR_EDGE_PX (18px) — ~22px of visible circle clearance — in closed, transient, teaser, open, and closing states alike, with no per-state constants to drift. A ~0px vendor inset (full-bleed mobile panel) is left untouched, and cleanup strips the transform with the marker. The watcher is also decoupled from the cId env var so any "Webchat Widget" iframe in the DOM is governed. globals.css keeps only the launcher-only hit-region clip; footer bottom padding drops from 7.5rem to 6rem since the clipped zone now tops out ~86px above the viewport bottom instead of ~110px. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PR #235 stopped the state-dependent jumping but anchored to the wrong target: the 43px value came from the vendor's teaser iframe inset, not from where the launcher circle should rest. Re-measured on production, the vendor anchors the same iframe at right/bottom:43px in every steady state (launcher 90x90, prompt ~330x178, open panel ~400x600), briefly mounts at 25px before remote config lands, and goes full-bleed 0px for the open panel on small portrait viewports. The ~58px circle sits ~4px inside the iframe's bottom-right corner, so 43px of edge clearance read as ~47px of visible inset — detached from the corner. Replace the static CSS nudge with an adaptive anchor: the geometry watcher now measures the vendor's own right/bottom inset on every sync and applies an inline translate that lands the iframe edges at RESPOND_IO_ANCHOR_RIGHT_PX (18px -> ~22px visible circle clearance) and RESPOND_IO_ANCHOR_BOTTOM_PX (8px -> ~12px, matching the rolled-back production resting point). The deltas are computed from the measured vendor inset rather than hardcoded per state, so the 25px mount transient, the steady 43px, and future vendor spacing changes all land on the same anchor; a ~0px inset (full-bleed mobile open panel) is left untouched. Re-syncs are idempotent: the vendor inset is recovered from the transformed rect, a wiped transform is detected and rewritten, and replaced iframes get fresh observers. Cleanup strips marker+transform. The watcher is decoupled from the cId env var so any "Webchat Widget" iframe in the DOM is governed. globals.css keeps only the launcher-only hit-region clip; footer bottom padding returns to 5.5rem since the clipped zone now tops out ~76px above the viewport bottom. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PR #235 stopped the state-dependent jumping but anchored to the wrong target: the 43px value came from the vendor's teaser iframe inset, not from where the launcher circle should rest. Re-measured on production, the vendor anchors the same iframe at right/bottom:43px in every steady state (launcher 90x90, prompt ~330x178, open panel ~400x600), briefly mounts at 25px before remote config lands, and goes full-bleed 0px for the open panel on small portrait viewports. The ~58px circle sits ~4px inside the iframe's bottom-right corner, so 43px of edge clearance read as ~47px of visible inset — detached from the corner. Replace the static CSS nudge with an adaptive anchor: the geometry watcher now measures the vendor's own right/bottom inset on every sync and applies an inline translate that lands the iframe edges at RESPOND_IO_ANCHOR_RIGHT_PX (18px -> ~22px visible circle clearance) and RESPOND_IO_ANCHOR_BOTTOM_PX (8px -> ~12px, matching the rolled-back production resting point). The deltas are computed from the measured vendor inset rather than hardcoded per state, so the 25px mount transient, the steady 43px, and future vendor spacing changes all land on the same anchor; a ~0px inset (full-bleed mobile open panel) is left untouched. Re-syncs are idempotent: the vendor inset is recovered from the transformed rect, a wiped transform is detected and rewritten, and replaced iframes get fresh observers. Cleanup strips marker+transform. The watcher is decoupled from the cId env var so any "Webchat Widget" iframe in the DOM is governed. globals.css keeps only the launcher-only hit-region clip; footer bottom padding returns to 5.5rem since the clipped zone now tops out ~76px above the viewport bottom. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PR #235 stopped the state-dependent jumping but anchored to the wrong target: the 43px value came from the vendor's teaser iframe inset, not from where the launcher circle should rest. Re-measured on production, the vendor anchors the same iframe at right/bottom:43px in every steady state (launcher 90x90, prompt ~330x178, open panel ~400x600), briefly mounts at 25px before remote config lands, and goes full-bleed 0px for the open panel on small portrait viewports. The ~58px circle sits ~4px inside the iframe's bottom-right corner, so 43px of edge clearance read as ~47px of visible inset — detached from the corner. Replace the static CSS nudge with an adaptive anchor: the geometry watcher now measures the vendor's own right/bottom inset on every sync and applies an inline translate that lands the iframe edges at RESPOND_IO_ANCHOR_RIGHT_PX (18px -> ~22px visible circle clearance) and RESPOND_IO_ANCHOR_BOTTOM_PX (6px -> ~10px, matching the rolled-back production resting point). The deltas are computed from the measured vendor inset rather than hardcoded per state, so the 25px mount transient, the steady 43px, and future vendor spacing changes all land on the same anchor; a ~0px inset (full-bleed mobile open panel) is left untouched. Re-syncs are idempotent: the vendor inset is recovered from the transformed rect, a wiped transform is detected and rewritten, and replaced iframes get fresh observers while outgoing ones are stripped. Cleanup clears marker+transform. The watcher is decoupled from the cId env var so any "Webchat Widget" iframe in the DOM is governed. globals.css keeps only the launcher-only hit-region clip; footer bottom padding returns to 5.5rem since the clipped zone now tops out ~74px above the viewport bottom. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
The Respond.io launcher shifted position between states: fully closed it
retreated ~36px into the corner, while the teaser and open panel rested
farther inward. The launcher should be a fixed anchor — surrounding UI
appears/disappears around it, but the circle itself never moves.
Root cause: the
data-launcher-onlyrule inglobals.css(issue#125's calibration) applied
transform: translate(36px, 36px)to themarked 90x90 closed iframe, pushing it to a cramped ~13px edge. The vendor
reuses that same iframe for the prompt card — the geometry watcher then
removes
data-launcher-only, the translate drops, and the circle snaps tothe vendor's resting inset (right/bottom:43px). The open panel uses vendor
positioning too, producing a third resting point.
Fix: the scoped translate is now
translate(6px, 6px)— a 6px nudgetoward the corner that lands the closed launcher's edge on the same 43px
inset the prompt card (measured: same iframe at right/bottom:43px) and
open states use. The launcher circle stays bottom-right-anchored inside
the iframe in all states, so its screen position is now identical across
closed → teaser → open → closing.
Nothing else changes: the
clip-pathhit-region shrink stays, noanimation was added, vendor code/inline positioning is untouched (the
override stays scoped to our
data-launcher-onlymarker), and theprompt/open iframes keep full vendor geometry.
Test plan
tests/e2e/chat-launcher.spec.ts— 15 pass (desktop-chromium,mobile-chromium, mobile-webkit); closed launcher now asserted at the
shared 43px anchor, prompt test pins 43px, open stays unrepositioned
tests/unit/respond-io.test.ts— 8 passtests/integration/respond-io-widget.test.tsx— 17 passnpm run lint·typecheck·check·build:test— cleantest:coverage— 558/558verify-chat-anchor.mjs+review-shots/chat-anchor/— marker atidentical screen coordinates across closed/teaser/open at
390px / 768px / 1440px
Generated with Devin
Summary by Sourcery
Anchor the Respond.io launcher consistently across all widget states while preserving existing vendor geometry and interaction behavior.
Bug Fixes:
Enhancements:
Tests:
Summary by CodeRabbit