fix: mobile goal UX — bottom-sheet dialog, keyboard-safe target button, capacitor safe-top for overlays
- the goal dialog renders as the shared MobileOverlayPanel bottom sheet on mobile instead of a centered dialog - tapping the target button to ARM keeps the soft keyboard open (the next message is the objective; same guard as the attachment/mic buttons), while opening the manage sheet lets the keyboard close as usual - capacitor: bottom-sheet overlays cap their height by the keyboard inset AND the top safe area — with the keyboard raised while typing inside a sheet, 100dvh does not shrink (native resize is off) and the panel could slide under the notch/status bar; applies to every MobileOverlayPanel
This commit is contained in:
@@ -76,6 +76,20 @@ export const SessionGoalButton: React.FC<SessionGoalButtonProps> = React.memo(({
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(footerIconButtonClass, colorClass)}
|
className={cn(footerIconButtonClass, colorClass)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
// Same guard as PermissionAutoAcceptButton, but only for the ARM
|
||||||
|
// toggle: arming happens mid-typing (the next message IS the
|
||||||
|
// objective), so that tap must not dismiss the soft keyboard or
|
||||||
|
// trigger the Android keyboard-close relayout that moves the button
|
||||||
|
// before the click lands. Opening the manage sheet (goal exists) is a
|
||||||
|
// context switch — there the keyboard should close as usual.
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (!goal) event.preventDefault();
|
||||||
|
}}
|
||||||
|
onPointerDownCapture={(event) => {
|
||||||
|
if (!goal && event.pointerType === 'touch') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={isEngaged}
|
aria-pressed={isEngaged}
|
||||||
{...(withTooltip ? {} : { title: label })}
|
{...(withTooltip ? {} : { title: label })}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation';
|
import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation';
|
||||||
import { clearSessionGoal, setSessionGoal } from '@/lib/sessionGoalActions';
|
import { clearSessionGoal, setSessionGoal } from '@/lib/sessionGoalActions';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||||
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
|
||||||
interface SessionGoalDialogProps {
|
interface SessionGoalDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -31,6 +33,7 @@ interface SessionGoalDialogProps {
|
|||||||
// (pause/resume/complete/clear) once a goal exists.
|
// (pause/resume/complete/clear) once a goal exists.
|
||||||
export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }: SessionGoalDialogProps) {
|
export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }: SessionGoalDialogProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const { goal } = useSessionGoal(sessionId, directory);
|
const { goal } = useSessionGoal(sessionId, directory);
|
||||||
const objectiveContent = useGoalObjectiveContent(sessionId, goal);
|
const objectiveContent = useGoalObjectiveContent(sessionId, goal);
|
||||||
|
|
||||||
@@ -86,13 +89,9 @@ export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }:
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const title = goal ? t('chat.goal.dialog.titleManage') : t('chat.goal.dialog.titleCreate');
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{goal ? t('chat.goal.dialog.titleManage') : t('chat.goal.dialog.titleCreate')}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
|
const body = (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{goal && (
|
{goal && (
|
||||||
<div className="space-y-1 p-2 rounded-lg" style={{ backgroundColor: 'var(--surface-elevated)' }}>
|
<div className="space-y-1 p-2 rounded-lg" style={{ backgroundColor: 'var(--surface-elevated)' }}>
|
||||||
@@ -193,6 +192,25 @@ export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mobile renders the shared bottom-sheet overlay instead of a centered
|
||||||
|
// dialog — same pattern as model controls and the session status panel.
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<MobileOverlayPanel open={open} title={title} onClose={() => onOpenChange(false)}>
|
||||||
|
{body}
|
||||||
|
</MobileOverlayPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{body}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -615,6 +615,20 @@
|
|||||||
transition: opacity 0.2s ease-out;
|
transition: opacity 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bottom-sheet overlays live inside a keyboard-inset surface whose bottom is
|
||||||
|
lifted by the keyboard height, but their max-height is expressed in 100dvh —
|
||||||
|
which does NOT shrink in the Capacitor app (native keyboard resize is off).
|
||||||
|
Without this cap a sheet with the keyboard raised (typing inside it) can
|
||||||
|
grow past the top safe area and slide under the notch/status bar. Subtract
|
||||||
|
both the keyboard inset and the top safe area from the available height. */
|
||||||
|
:root.oc-capacitor-app .pwa-overlay-panel {
|
||||||
|
max-height: calc(
|
||||||
|
100dvh
|
||||||
|
- var(--oc-keyboard-inset, 0px)
|
||||||
|
- max(0.75rem, env(safe-area-inset-top))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* Full-screen scroll views (e.g. the connect/login screen) live outside the app
|
/* Full-screen scroll views (e.g. the connect/login screen) live outside the app
|
||||||
shell, so shrink them by the keyboard height the same way the shell does. Capping
|
shell, so shrink them by the keyboard height the same way the shell does. Capping
|
||||||
the height (instead of min-height: 100dvh) is what makes overflow-y-auto actually
|
the height (instead of min-height: 100dvh) is what makes overflow-y-auto actually
|
||||||
|
|||||||
Reference in New Issue
Block a user