fix mobile screen issues

This commit is contained in:
2026-08-24 13:14:22 +05:30
parent dcf9770ada
commit 02eb48af99
34 changed files with 1214 additions and 291 deletions

View File

@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils';
import KrowAssistant from './KrowAssistant';
import { useAssistantPanel } from './AssistantPanelContext';
import { ResizeDivider } from './ResizeDivider';
import { useIsPhone, useViewportWidth, useVisualViewport } from './viewport';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
const EXPANDED_WIDTH = 620;
@@ -18,32 +19,6 @@ const LEAD_PADDING = 8;
const GUTTER = HANDLE_WIDTH + LEAD_PADDING;
/** Expanded must never dominate: the dashboard stays the primary experience. */
const MAX_VIEWPORT_SHARE = 0.42;
/**
* Below this width there is no room for a column *beside* the dashboard — a
* 380px track on a 375px phone collapses `main` to nothing — so Owliver stacks
* underneath the page instead.
*
* Deliberately Tailwind's `md` (768px), not `lg`: tablets already lay the inline
* column out acceptably, so they keep the two-column workspace and only phones
* stack.
*/
const STACK_BREAKPOINT = 768;
/** Tracks viewport width so the panel can be clamped and the layout switched. */
function useViewportWidth() {
const [width, setWidth] = React.useState(() =>
typeof window === 'undefined' ? 1440 : window.innerWidth
);
React.useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}
/**
* The collapsed state — a compact docked trigger.
*
@@ -64,7 +39,12 @@ function CollapsedTrigger({ page, onRestore }) {
onClick={onRestore}
aria-label={`Show the Owliver workspace for ${page}`}
aria-expanded={false}
className="group fixed bottom-5 right-5 z-30 inline-flex items-center gap-2 rounded-full border border-border
/* `bottom` is a `max()` against the bottom safe-area inset rather than a
flat 20px: on a phone with a home indicator a flat offset puts the pill
under the gesture bar, where the tap belongs to the OS. `env()` is 0
everywhere else, so desktop keeps the offset it always had. */
className="group fixed bottom-[max(1.25rem,env(safe-area-inset-bottom))] right-[max(1.25rem,env(safe-area-inset-right))]
z-30 inline-flex items-center gap-2 rounded-full border border-border
bg-surface py-2 pl-2 pr-3.5 shadow-md transition-[box-shadow,border-color] duration-base
hover:border-krow-blue/40 hover:shadow-lg
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
@@ -77,11 +57,105 @@ function CollapsedTrigger({ page, onRestore }) {
}
/**
* AssistantPanel — the Owliver workspace column the Admin layout renders.
* MobileWorkspace — Owliver on a phone.
*
* Owliver is part of the page on every supported route, so this is a column in
* the layout rather than an overlay: the dashboard reflows beside it instead of
* being covered, and collapsing restores the original layout exactly.
* The same `KrowAssistant`, the same context, the same runtime, the same
* conversation. What changes is only where it is mounted: on a phone the
* workspace is an overlay above the page rather than a column beside it.
*
* That is forced by arithmetic, not taste. The desktop workspace is
* `main + 400px`; at 375px the 400px track leaves `main` negative, so the two
* surfaces stop being a layout and start being a fight over the same pixels —
* which is exactly what the broken state was. An overlay takes the page out of
* that arithmetic entirely: the page stays `width: 100%` whether Owliver is
* open or closed, and there is never a reserved column standing empty.
*
* Three things this is deliberately not:
*
* - Not a second chat. Nothing about the assistant is re-implemented; this
* component is a positioned container and nothing else.
* - Not a takeover. It stops below the app header, so the reader can still see
* where they are and can still leave.
* - Not a fixed height. It is sized to `visualViewport` where that exists and
* to `100dvh` where it does not, so an open keyboard shortens the sheet
* instead of pushing the composer off the bottom of it.
*/
function MobileWorkspace({ context, onClose }) {
const viewport = useVisualViewport();
/* The page behind an overlay must not scroll: on a touch screen a drag that
starts on the scrim and lands on the page is otherwise indistinguishable
from scrolling the conversation, and the reader loses their place on both
surfaces at once. Restored exactly as found — another overlay may already
own it. */
React.useEffect(() => {
const { body } = document;
const previous = body.style.overflow;
body.style.overflow = 'hidden';
return () => { body.style.overflow = previous; };
}, []);
return (
<div
className="fixed inset-x-0 top-0 z-50 h-[100dvh] md:hidden"
style={viewport ? { height: viewport.height, top: viewport.offsetTop } : undefined}
>
{/* Tapping the page dismisses, which is what a sheet over a page should
do. A button rather than a bare div so it is reachable without a
pointer. */}
<button
type="button"
aria-label="Close the Owliver workspace"
onClick={onClose}
className="absolute inset-0 bg-ink-1/30 backdrop-blur-[2px] motion-safe:animate-fade-in"
/>
{/* `top-14` is the header's own height: the sheet starts under the app
bar rather than over it, so navigation is never covered. `min-h-0` is
what lets the assistant's internal scroller own the overflow instead
of the sheet growing past the viewport. */}
<div
className="absolute inset-x-0 bottom-0 top-14 flex min-h-0 flex-col px-3
pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3"
role="dialog"
aria-modal="true"
aria-label="Owliver workspace"
>
<KrowAssistant
key={context.id}
context={context}
/* Expanded is a desktop-only width state; there is no wider to go
here, so the control is not offered rather than offered and inert. */
expanded={false}
onClose={onClose}
onExpand={null}
onRestore={null}
className="h-full min-h-0"
/>
</div>
</div>
);
}
/**
* AssistantPanel — the Owliver workspace the Admin layout renders.
*
* One panel, two presentations, chosen by how much room there is beside the
* page rather than by what kind of device is asking:
*
* ≥ 768px a column in the layout. Owliver is part of the page on every
* supported route, so the dashboard reflows beside it instead of
* being covered, and collapsing restores the original layout
* exactly. This is the protected desktop geometry and everything
* below describes it.
* < 768px an overlay (`MobileWorkspace`), because a 400px track does not fit
* beside anything on a 375px phone. The page is `width: 100%` in
* both states and never participates in a two-column width
* calculation it cannot satisfy.
*
* Both presentations mount the same `KrowAssistant` with the same context and
* read the same open/collapsed state, so there is one assistant in the product
* and one set of actions that change it.
*
* Five structural details matter, and every one of them was a bug at some point:
*
@@ -111,10 +185,24 @@ export function AssistantPanel({ stickyClassName, panelHeightClassName }) {
setWidth, resetWidth, open, close, expand, restore,
} = useAssistantPanel();
const viewportWidth = useViewportWidth();
const isPhone = useIsPhone();
// No assistant on this route: no column, no rail, no trace in the layout.
if (!context) return null;
/* Phones: Owliver is never a column, in either state.
Closed, the layout is one column and `main` has the whole viewport — there
is no reserved 400px gutter to leave a blank strip down the right. Open,
the workspace is an overlay, so the page keeps that full width underneath
rather than being asked to share it with a track wider than the phone.
Both states are rendered from the same panel state the desktop column uses,
so opening, collapsing and reopening are the same three actions here. */
if (isPhone) {
return isOpen
? <MobileWorkspace context={context} onClose={close} />
: <CollapsedTrigger page={context.page} onRestore={open} />;
}
/* Expanded overrides the dragged width; otherwise the user's own width wins.
Both are clamped against the viewport so the dashboard is never squeezed. */
const viewportCap = Math.round(viewportWidth * MAX_VIEWPORT_SHARE);
@@ -131,25 +219,6 @@ export function AssistantPanel({ stickyClassName, panelHeightClassName }) {
const trackWidth = panelWidth + GUTTER;
/* Phones: stack Owliver under the dashboard. A 380px column beside the page is
not a layout at this width, and an overlay that opens on load would put a
sheet between the user and the page they asked for. */
if (viewportWidth < STACK_BREAKPOINT) {
return (
<aside aria-label="Owliver workspace" className="pb-6 pt-2">
<KrowAssistant
key={context.id}
context={context}
expanded={false}
onClose={close}
onExpand={null}
onRestore={null}
className="h-[32rem]"
/>
</aside>
);
}
return (
<aside
aria-label="Owliver workspace"