feat(builder): make the app usable on mobile viewports - #970
Conversation
| */} | ||
| <header className="sticky top-0 z-20 flex h-12 shrink-0 items-center gap-2 border-b bg-background px-2 pt-[env(safe-area-inset-top)] md:hidden"> | ||
| <SidebarMobileTrigger /> | ||
| <span className="truncate font-medium text-sm"> |
There was a problem hiding this comment.
Thanks for the contribution.
We want to dedicate the entire screen to the main content,
so we intentionally chose not to display the header.
Please remove it.
There was a problem hiding this comment.
Done — the header is gone from both shells (the workspace one and the manage/admin console), so the page now starts at the top of the viewport.
One thing worth flagging: that bar was the only way to open the sidebar below md. SidebarTrigger is hidden md:inline-flex, AppSidebar doesn't render SidebarRail, and the Base UI Sheet only does swipe-to-dismiss, not swipe-to-open — so removing it outright left phones with no navigation at all.
Rather than put another bar back, the control moved to a fixed drawer handle on the inline-start edge (24×64, md:hidden). It reserves no height, so the whole viewport still belongs to the page content.
Removing the header also let the inbox drop its copy of the shell's h-12: the mobile pane was sized calc(100dvh - 3rem) and is now simply h-[100dvh].
Verified at 390×844 — no <header> in either shell, main starts at y=0, the handle opens the Sheet, tapping a nav link closes it, and the inbox pane is exactly the viewport height. Desktop is unchanged: the handle is display:none, SidebarTrigger sits where it always did, and the inbox keeps its three panels.
Happy to drop the handle as well if you'd rather have no mobile chrome at all — it's isolated in its own commit.
The review on ChatbotXIO#970 asked for the whole screen to go to page content, so neither shell renders a top bar below md any more. That bar was the only way into the sidebar on a phone — SidebarTrigger is hidden md:inline-flex and the Sheet has no swipe-to-open — so the control moves to the screen's inline-start edge as a fixed drawer handle, which reserves no height. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review on ChatbotXIO#970 asked for the whole screen to go to page content, so neither shell renders a top bar below md any more. That bar was the only way into the sidebar on a phone — SidebarTrigger is hidden md:inline-flex and the Sheet has no swipe-to-open — so the control moves to the screen's inline-start edge as a fixed drawer handle, which reserves no height. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eight The mobile pane sized itself with calc(100dvh - 3rem), a hand-copy of the shell's h-12 header. With no header, the pane is simply h-[100dvh] and the cross-module value is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6546021 to
dffed6e
Compare
…ding The mobile single-pane inbox unmounts ConversationList whenever the user goes back from a thread. Its mount effects re-read `?conversationId=` from the URL and re-select it, and re-run the auto-select-first-conversation logic on every remount, so tapping back reopened the same thread and a fresh phone visit to /inbox could skip the list and land in a thread. Add an `autoSelectFirst` option to the chat store's `loadMoreConversations`, a `useConversationIdParam` hook to read/write the URL param from one place, and thread an `autoSelectFirstConversation` prop through ConversationList and ConversationListPane. The mobile layout now passes `false` and clears the URL param in its back handler; desktop is unaffected. Also reset the mobile contact sheet when the active conversation changes, so it can't reopen bound to a previous conversation.
…scrollbar triad
- Remove DataTable's `scrollable` prop: the default flipped to `true` in
this PR and no caller passes `false` anymore, so the prop, its ternary,
and the redundant `scrollable` on workspace-members-table were dead.
- Drop the now-duplicate `data-state` on DataTableRowCard — the DataTable
wrapper already sets it on the card container, and nothing styles it on
cards.
- Drop the unreachable `= {}` default on MessageHead's props; its one
caller always passes an object.
- Add a shared `scrollbar-hide` Tailwind utility in packages/ui and use it
in place of the `[-ms-overflow-style:none] [scrollbar-width:none]
[&::-webkit-scrollbar]:hidden` triad duplicated across app-tab,
analytics-nav and message-input.
- Simplify setup-dom's media-query evaluator with `matchAll` instead of a
manual `exec` loop with a `lastIndex` reset.
- Fix comments left over from removing the mobile shell header: the
viewport comment named a header that no longer exists, the vitest-config
preset comment didn't mention the ResizeObserver stub, and two test
comments narrated PR review history instead of stating the invariant.
The ResizeObserver stub let Base UI's ScrollAreaViewport past the guard at
the top of its layout effect, and the effect then schedules
`viewport.getAnimations({ subtree: true })` on a 0ms timeout. jsdom
implements no Web Animations API, so any suite driving fake timers fires
that timeout and throws `getAnimations is not a function` outside an act
boundary, taking the whole file down through an unhandled rejection.
`__tests__/category-resource-list-selection.test.tsx` is the file that hit
it: it advances timers by 500ms to drive the search debounce.
Return an empty list — truthful under jsdom, which runs no animations, and
the branch Base UI short-circuits on, so no Animation object has to be
faked. Guarded like the matchMedia and ResizeObserver stubs beside it, so a
real implementation (jsdom gaining one) is left untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnHNPtTMsp9aeUogicybch
|
Thanks for the PR! Nice change. |
What
Makes the builder usable on a phone. Before this, the app was desktop-only in
practice: of 1137
.tsxfiles, 99 contained any responsive prefix, and of 44layout files exactly one did. The shadcn
Sidebaralready collapsed into aSheetbelowmd, but nothing in the app could open it — the only trigger isabsolutely positioned off the inset's inline edge — and no nav link closed it.
Scope is the app shell, the inbox, and the table/list surfaces. The flow
builder is deliberately out of scope and untouched; it needs its own pass.
Shell and navigation
viewportexport withviewportFit: "cover", soenv(safe-area-inset-*)reports real values.
SidebarMobileHandleinpackages/ui— a drawer handle pinned to thescreen's inline-start edge, as opposed to
SidebarTrigger, which is arail-collapse chevron and reads as nothing below
md. Both shells (workspaceand manage console) render it unconditionally; it hides itself from
mdup.No shell header was added — the whole viewport stays page content.
NavMainnow closes the mobile sheet when a link is tapped; it used to stayopen on top of the destination page.
AppTabscrolls horizontally instead of overflowing. One file, and it unblocksthe 15 surfaces that render 4–6 tabs.
-m-6negative margin — which silently depended on the shell'sp-6— is replaced by a documentedFullBleedcomponent, so changing theshell's padding can no longer break the page from a distance.
Tables and lists
DataTablenow scrolls horizontally. The default wasoverflow-hiddenand only 1 of its 36 consumers opted out of it, so 35 tables silently clipped
their rightmost columns with no way to reach them.
mobileCardrender mode plus a genericDataTableRowCardthatrenders a row through the column's own
flexRender— no duplicated celllogic, and field labels come from
meta.label, so no new translation keys.Applied to contacts (bespoke card), flows and broadcasts (generic).
layout is in the first paint rather than flipping after hydration.
column;
SettingRowstacks (itsgrid-cols-4reached every settings form).Inbox
Below
mdthe inbox is a single-pane master/detail view: the conversation list,then the thread with a back control, with the contact panel behind a button in a
Sheet. Frommdup the three-columnResizablePanelGroupand its layout cookieare unchanged.
This is the one place the layout is chosen in JS rather than CSS: the three panes
are heavy and self-fetching, so rendering both arrangements would mount and fetch
everything twice.
useIsMobileStatewas added for it — it returnsundefineduntil the first measurement so the layout waits instead of guessing desktop and
remounting a frame later.
Bugs fixed along the way:
<ChatRealtime />lived inside the message pane. In the mobile single-pane viewthat pane unmounts when returning to the list, which would have taken the
realtime socket down with it. It now sits at the layout root.
ConversationListevery time theuser goes back from a thread. Its mount effects re-read
?conversationId=fromthe URL and re-select it, and re-ran the auto-select-first-conversation logic —
so tapping back reopened the same thread, and landing on
/inboxon a phonecould skip the list entirely. The back control now also clears the URL param,
and the mobile list passes
autoSelectFirstConversation={false}.Dialogs
DialogContent's base width goes fromw-fulltow-[calc(100%-2rem)].tailwind-merge resolves
max-w-*by group, so the ~60 dialogs passing anunprefixed
max-w-*were replacing themax-w-[calc(100%-2rem)]guard andrendering edge-to-edge on a phone. Width is a separate group, so the gutter now
survives. One line, no consumer changes.
Verification
pnpm lintclean;check-typesclean for builder, ui, analytics-nextjs andvitest-config;
pnpm buildpasses.mobile inbox's back-button/URL/auto-select fix.
settings and both analytics dashboards: every route measures
scrollWidth == clientWidth, with no element outside the viewport. Re-checkedat 1440×900 to confirm desktop is unchanged.
admins-analysis.tsxcarried an unprefixedcol-span-2, which against the new one-column mobile grid created an implicitsecond column and pushed the analytics page to 471px in a 375px viewport. It
was invisible before because the grid was always two columns. Fixed here.
packages/vitest-configgainsmatchMediaandResizeObserverstubs for jsdom;without them
useIsMobile, the sidebar's mobile branch, and anything using aresizable panel throw on mount and could not be tested at all.
Not included
max-h-screenappears on 101 dialog lines. On iOS Safari100vhovershoots thevisible area, so a dialog's footer can sit under the browser chrome. Left alone
because the sweep would cross into the flow builder.
🤖 Generated with Claude Code