Skip to content

Anchor the Respond.io launcher at one fixed inset across all states - #235

Merged
spizeck merged 2 commits into
masterfrom
feat/respondio-launcher-anchor
Oct 7, 2026
Merged

spizeck merged 2 commits into
masterfrom
feat/respondio-launcher-anchor

Conversation

@spizeck

@spizeck spizeck commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

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-only rule in globals.css (issue
#125's calibration) applied transform: translate(36px, 36px) to the
marked 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 to
the 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 nudge
toward 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-path hit-region shrink stays, no
animation was added, vendor code/inline positioning is untouched (the
override stays scoped to our data-launcher-only marker), and the
prompt/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 pass
  • tests/integration/respond-io-widget.test.tsx — 17 pass
  • npm run lint · typecheck · check · build:test — clean
  • test:coverage — 558/558
  • verify-chat-anchor.mjs + review-shots/chat-anchor/ — marker at
    identical screen coordinates across closed/teaser/open at
    390px / 768px / 1440px
  • Manual check against the real widget on the preview/prod deploy

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:

  • Keep the Respond.io launcher circle at a consistent screen position across closed, teaser, open, and closing states.

Enhancements:

  • Align the closed launcher with the shared 43px viewport inset while preserving scoped positioning and hit-region behavior.
  • Increase footer clearance to accommodate the launcher’s updated clipped interaction zone.

Tests:

  • Update launcher and footer end-to-end coverage to assert the shared anchor and interaction clearance.
  • Add a multi-viewport verification script with screenshots for closed, teaser, and open states.
  • Update unit coverage for the revised launcher positioning contract.

Summary by CodeRabbit

  • Bug Fixes
    • Adjusted the closed chat launcher so it sits 43px from the viewport edges across page types and screen sizes. Promotional prompts and open conversations are unaffected.
  • Improvements
    • Increased footer spacing to provide more room around the launcher and reduce overlap with page content.

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>
@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The 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 states

sequenceDiagram
    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
Loading

State diagram for the fixed chat launcher anchor

stateDiagram-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
    }
Loading

File-Level Changes

Change Details Files
Re-anchor the closed launcher iframe to the same 43px viewport inset used by teaser and open states.
  • Reduce the scoped data-launcher-only transform from 36px to 6px while preserving the hit-region clip and leaving vendor positioning untouched.
  • Update CSS comments to document the measured geometry and fixed-anchor rationale.
app/globals.css
Align automated coverage with the fixed launcher position across widget states.
  • Update end-to-end assertions for the 43px inset and 6px transform in closed states.
  • Assert teaser geometry remains at 43px and untransformed, while open state remains untransformed.
  • Update unit coverage to enforce the scoped 6px transform and retained clip-path contract.
tests/e2e/chat-launcher.spec.ts
tests/unit/respond-io.test.ts
Add a viewport-based visual verification utility for launcher anchor consistency.
  • Inject representative closed, teaser, and open iframe geometries at mobile, tablet, and desktop viewports.
  • Capture screenshots and report launcher-marker and iframe corner distances for comparison.
verify-chat-anchor.mjs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
seasaba-web Ready Ready Preview Oct 7, 2026 5:54pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-07T17:46:26.819670Z edda68a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ce3fd7c8-bc9a-47b9-81eb-9dfc74756fda
📥 Commits

Reviewing files that changed from the base of the PR and between edda68a and d66fb5a.

📒 Files selected for processing (2)
  • components/footer.tsx
  • tests/e2e/footer-widgets.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

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

Changes

Chat launcher anchor

Layer / File(s) Summary
Align and verify chat state anchors
app/globals.css, tests/e2e/chat-launcher.spec.ts, tests/unit/respond-io.test.ts, verify-chat-anchor.mjs
The launcher-only iframe translate changes from 36px to 6px on both axes. Tests check 43px clearances for launcher and prompt states. The verification script measures and screenshots closed, teaser, and open states at mobile, tablet, and desktop viewport sizes.
Update footer clearance
components/footer.tsx, tests/e2e/footer-widgets.spec.ts
Footer bottom padding increases from 5.5rem to 7.5rem, retaining the safe-area inset. The test comments describe the updated clipped launcher zone and dead-space estimate.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to d66fb

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)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: anchoring the Respond.io launcher at one fixed inset across closed, prompt, and open states.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot 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.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread app/globals.css
Comment thread verify-chat-anchor.mjs
Comment thread app/globals.css
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>
@spizeck
spizeck merged commit 73bd2ed into master Oct 7, 2026
11 checks passed
@spizeck
spizeck deleted the feat/respondio-launcher-anchor branch October 7, 2026 18:14
spizeck added a commit that referenced this pull request Oct 8, 2026
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>
spizeck added a commit that referenced this pull request Oct 8, 2026
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>
spizeck added a commit that referenced this pull request Oct 8, 2026
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>
spizeck added a commit that referenced this pull request Oct 8, 2026
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>
spizeck added a commit that referenced this pull request Oct 8, 2026
…ects #235) (#236)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — d66fb5ae Deployed Oct 7, 2026 by vercel[bot]
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