fix mobile screen issues
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user