Skip to content

Fix mobile chat: sticky header, hide dock switch, dvh viewport, closed-state-wins#4

Open
aaronaccessvr wants to merge 1 commit into
mainfrom
fix/mobile-chat-experience
Open

Fix mobile chat: sticky header, hide dock switch, dvh viewport, closed-state-wins#4
aaronaccessvr wants to merge 1 commit into
mainfrom
fix/mobile-chat-experience

Conversation

@aaronaccessvr
Copy link
Copy Markdown
Collaborator

Four small Vue/JS changes that together fix the chat panel's mobile experience.

Changes

ChatHeader.vue — sticky top. Adds sticky top-0 z-10 to the header so the back-and-close chrome stays anchored even when iOS Safari's collapsing URL bar lets the flex layout overflow the visible viewport. The header was previously shrink-0 in a flex column, which works on desktop but doesn't survive the mobile visual-viewport quirk.

ChatHeader.vue — hide dock buttons on mobile. Replaces a $store.state.config.isMobile check (which was effectively dead — that flag isn't populated in the chat-iframe store) with hidden sm:block Tailwind responsive classes on both the dock and undock buttons. Docking the widget on mobile doesn't make sense (the chat already fills the viewport), so the controls are simply hidden below the sm breakpoint.

Chat.vue — h-dvh + responsive chrome. Switches the container height from h-screen (100vh) to h-dvh (100dvh), and moves the rounded-border-overflow chrome to sm: prefixes so the popup chrome only applies on desktop sizes. h-screen was the root cause of the header-scrolling-out-of-view bug — on iOS Safari, 100vh is larger than the visible viewport while the URL bar is shown, which pushes the flex layout below the fold.

widget.js initClient — closed-state-wins. Reorders the persistence-restore checks so persistedState.open === false short-circuits before the docked check. dock() opens the panel as a side effect, so a stale docked: true flag persisting alongside open: false (a rare but observed combination on mobile, where storage quotas and incognito make persistence flakier) used to override the user's explicit close. Now the explicit close always wins.

Consumed by

AccessVR/OrchestrateLaravelApp PR (fix/mobile-chat-experience) bumps the submodule to this PR's tip and rebuilds + republishes the widget bundle. Originating bug: AccessVR/OrchestrateLaravelApp#1492.

Test plan

  • Open the host app on a mobile device, start a chat session, scroll the conversation — header stays at the top.
  • On the same mobile device, confirm the docking-mode button is not visible in the header.
  • Close the widget, reload the page — widget loads closed (beacon visible, panel hidden).
  • On desktop, dock the widget, undock it — toggle still works.

🤖 Generated with Claude Code

Three changes to the chat-iframe Vue components plus a defensive
reorder in the widget controller, addressing what mobile users see
when the chat panel takes the full viewport.

ChatHeader: pinned with `sticky top-0 z-10` so the back-and-close
chrome stays anchored even when 100vh ends up larger than the visible
mobile viewport (iOS Safari's URL-bar collapse) and the iframe scrolls.
The header was already `shrink-0` in a flex layout, which works on
desktop but doesn't survive the visual-viewport quirk on mobile.

Dock / undock buttons: gated with `hidden sm:block` so they only show
on the sm breakpoint and up. The previous code referenced
`$store.state.config.isMobile`, but that flag is never populated in
the chat-iframe store (it's read from a PHP-side config object that
has no device awareness), so the rounded-corner and visibility checks
that depended on it were effectively constant. Tailwind responsive
classes handle this without an out-of-band signal.

Chat container: switched to `h-dvh` (dynamic viewport height) and
moved the rounded/border/overflow chrome to `sm:` prefixes so it only
applies on the popup-and-up sizes. `h-screen` (100vh) overflows the
visible viewport on mobile browsers with a collapsing URL bar, which
was the underlying cause of the header-scrolling-out-of-view bug.

widget.js initClient: reordered so `persistedState.open === false`
short-circuits before the docked check. dock() opens the panel as a
side effect, so if a stale `docked: true` flag survived alongside
`open: false` (a stale combination that has shown up on mobile, where
storage quotas and incognito states make persistence flakier), the
old order would override the user's explicit close. Closed-state-wins
now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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