feat: add input bar offset setting for curved screen devices (#89)
Co-authored-by: auroraflux <auroraflux@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
auroraflux
parent
919eeea53b
commit
39b9f985ce
@@ -82,7 +82,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
|
|
||||||
const { currentProviderId, currentModelId, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
|
const { currentProviderId, currentModelId, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
|
||||||
const agents = getVisibleAgents();
|
const agents = getVisibleAgents();
|
||||||
const { isMobile } = useUIStore();
|
const { isMobile, inputBarOffset, isKeyboardOpen } = useUIStore();
|
||||||
const { working } = useAssistantStatus();
|
const { working } = useAssistantStatus();
|
||||||
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
||||||
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -1166,7 +1166,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="pt-0 pb-2 md:pb-4 bottom-safe-area">
|
<form onSubmit={handleSubmit} className="pt-0 pb-2 md:pb-4 bottom-safe-area" style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined}>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
isWorking={working.isWorking}
|
isWorking={working.isWorking}
|
||||||
statusText={workingStatusText}
|
statusText={workingStatusText}
|
||||||
|
|||||||
@@ -98,10 +98,13 @@ export const MainLayout: React.FC = () => {
|
|||||||
let ignoreOpenUntilZero = false;
|
let ignoreOpenUntilZero = false;
|
||||||
let previousHeight = 0;
|
let previousHeight = 0;
|
||||||
|
|
||||||
|
const setKeyboardOpen = useUIStore.getState().setKeyboardOpen;
|
||||||
|
|
||||||
const forceKeyboardClosed = () => {
|
const forceKeyboardClosed = () => {
|
||||||
stickyKeyboardInset = 0;
|
stickyKeyboardInset = 0;
|
||||||
ignoreOpenUntilZero = true;
|
ignoreOpenUntilZero = true;
|
||||||
root.style.setProperty('--oc-keyboard-inset', '0px');
|
root.style.setProperty('--oc-keyboard-inset', '0px');
|
||||||
|
setKeyboardOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVisualViewport = () => {
|
const updateVisualViewport = () => {
|
||||||
@@ -147,16 +150,19 @@ export const MainLayout: React.FC = () => {
|
|||||||
// (prevents false positives during Android keyboard animation)
|
// (prevents false positives during Android keyboard animation)
|
||||||
const closingByHeight = !isTextTarget && height > previousHeight + 6;
|
const closingByHeight = !isTextTarget && height > previousHeight + 6;
|
||||||
|
|
||||||
if (measuredInset === 0) {
|
if (measuredInset === 0) {
|
||||||
stickyKeyboardInset = 0;
|
stickyKeyboardInset = 0;
|
||||||
|
setKeyboardOpen(false);
|
||||||
} else if (closingByHeight) {
|
} else if (closingByHeight) {
|
||||||
forceKeyboardClosed();
|
forceKeyboardClosed();
|
||||||
} else if (measuredInset > 0 && isTextTarget) {
|
} else if (measuredInset > 0 && isTextTarget) {
|
||||||
// When focus is on text input, track actual inset (allows settling
|
// When focus is on text input, track actual inset (allows settling
|
||||||
// to correct value after Android animation fluctuations)
|
// to correct value after Android animation fluctuations)
|
||||||
stickyKeyboardInset = measuredInset;
|
stickyKeyboardInset = measuredInset;
|
||||||
|
setKeyboardOpen(true);
|
||||||
} else if (measuredInset > stickyKeyboardInset) {
|
} else if (measuredInset > stickyKeyboardInset) {
|
||||||
stickyKeyboardInset = measuredInset;
|
stickyKeyboardInset = measuredInset;
|
||||||
|
setKeyboardOpen(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Visual section: Theme Mode, Font Size, Spacing
|
// Visual section: Theme Mode, Font Size, Spacing, Input Bar Offset (mobile)
|
||||||
const VisualSectionContent: React.FC = () => {
|
const VisualSectionContent: React.FC = () => {
|
||||||
return <OpenChamberVisualSettings visibleSettings={['theme', 'fontSize', 'spacing']} />;
|
return <OpenChamberVisualSettings visibleSettings={['theme', 'fontSize', 'spacing', 'inputBarOffset']} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chat section: Default Tool Output, Diff layout, Show reasoning traces, Queue mode
|
// Chat section: Default Tool Output, Diff layout, Show reasoning traces, Queue mode
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const DIFF_LAYOUT_OPTIONS: Option<'dynamic' | 'inline' | 'side-by-side'>[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type VisibleSetting = 'theme' | 'fontSize' | 'spacing' | 'toolOutput' | 'diffLayout' | 'reasoning' | 'queueMode';
|
export type VisibleSetting = 'theme' | 'fontSize' | 'spacing' | 'inputBarOffset' | 'toolOutput' | 'diffLayout' | 'reasoning' | 'queueMode';
|
||||||
|
|
||||||
interface OpenChamberVisualSettingsProps {
|
interface OpenChamberVisualSettingsProps {
|
||||||
/** Which settings to show. If undefined, shows all. */
|
/** Which settings to show. If undefined, shows all. */
|
||||||
@@ -73,6 +73,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const setFontSize = useUIStore(state => state.setFontSize);
|
const setFontSize = useUIStore(state => state.setFontSize);
|
||||||
const padding = useUIStore(state => state.padding);
|
const padding = useUIStore(state => state.padding);
|
||||||
const setPadding = useUIStore(state => state.setPadding);
|
const setPadding = useUIStore(state => state.setPadding);
|
||||||
|
const inputBarOffset = useUIStore(state => state.inputBarOffset);
|
||||||
|
const setInputBarOffset = useUIStore(state => state.setInputBarOffset);
|
||||||
const diffLayoutPreference = useUIStore(state => state.diffLayoutPreference);
|
const diffLayoutPreference = useUIStore(state => state.diffLayoutPreference);
|
||||||
const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference);
|
const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference);
|
||||||
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
||||||
@@ -226,6 +228,48 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShow('inputBarOffset') && isMobile && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||||
|
Input Bar Offset
|
||||||
|
</h3>
|
||||||
|
<p className="typography-meta text-muted-foreground">
|
||||||
|
Raise the input bar for phones with curved screen edges.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="5"
|
||||||
|
value={inputBarOffset}
|
||||||
|
onChange={(e) => setInputBarOffset(Number(e.target.value))}
|
||||||
|
className="flex-1 min-w-0 h-3 bg-muted rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0"
|
||||||
|
aria-label="Input bar offset in pixels"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="typography-ui-label font-medium text-foreground tabular-nums rounded-md border border-border bg-background px-2 py-1.5 min-w-[3.75rem] text-center">
|
||||||
|
{inputBarOffset}px
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ButtonSmall
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setInputBarOffset(0)}
|
||||||
|
disabled={inputBarOffset === 0}
|
||||||
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
||||||
|
aria-label="Reset input bar offset"
|
||||||
|
title="Reset"
|
||||||
|
>
|
||||||
|
<RiRestartLine className="h-3.5 w-3.5" />
|
||||||
|
</ButtonSmall>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShow('toolOutput') && (
|
{shouldShow('toolOutput') && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface UIStore {
|
|||||||
activeMainTab: MainTab;
|
activeMainTab: MainTab;
|
||||||
pendingDiffFile: string | null;
|
pendingDiffFile: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
isKeyboardOpen: boolean;
|
||||||
isCommandPaletteOpen: boolean;
|
isCommandPaletteOpen: boolean;
|
||||||
isHelpDialogOpen: boolean;
|
isHelpDialogOpen: boolean;
|
||||||
isAboutDialogOpen: boolean;
|
isAboutDialogOpen: boolean;
|
||||||
@@ -40,6 +41,7 @@ interface UIStore {
|
|||||||
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
|
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
padding: number;
|
padding: number;
|
||||||
|
inputBarOffset: number;
|
||||||
|
|
||||||
favoriteModels: Array<{ providerID: string; modelID: string }>;
|
favoriteModels: Array<{ providerID: string; modelID: string }>;
|
||||||
recentModels: Array<{ providerID: string; modelID: string }>;
|
recentModels: Array<{ providerID: string; modelID: string }>;
|
||||||
@@ -75,6 +77,8 @@ interface UIStore {
|
|||||||
setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void;
|
setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void;
|
||||||
setFontSize: (size: number) => void;
|
setFontSize: (size: number) => void;
|
||||||
setPadding: (size: number) => void;
|
setPadding: (size: number) => void;
|
||||||
|
setInputBarOffset: (offset: number) => void;
|
||||||
|
setKeyboardOpen: (open: boolean) => void;
|
||||||
applyTypography: () => void;
|
applyTypography: () => void;
|
||||||
applyPadding: () => void;
|
applyPadding: () => void;
|
||||||
updateProportionalSidebarWidths: () => void;
|
updateProportionalSidebarWidths: () => void;
|
||||||
@@ -99,6 +103,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
activeMainTab: 'chat',
|
activeMainTab: 'chat',
|
||||||
pendingDiffFile: null,
|
pendingDiffFile: null,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
|
isKeyboardOpen: false,
|
||||||
isCommandPaletteOpen: false,
|
isCommandPaletteOpen: false,
|
||||||
isHelpDialogOpen: false,
|
isHelpDialogOpen: false,
|
||||||
isAboutDialogOpen: false,
|
isAboutDialogOpen: false,
|
||||||
@@ -114,6 +119,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
toolCallExpansion: 'collapsed',
|
toolCallExpansion: 'collapsed',
|
||||||
fontSize: 100,
|
fontSize: 100,
|
||||||
padding: 100,
|
padding: 100,
|
||||||
|
inputBarOffset: 0,
|
||||||
favoriteModels: [],
|
favoriteModels: [],
|
||||||
recentModels: [],
|
recentModels: [],
|
||||||
diffLayoutPreference: 'dynamic',
|
diffLayoutPreference: 'dynamic',
|
||||||
@@ -336,6 +342,14 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ diffWrapLines: wrap });
|
set({ diffWrapLines: wrap });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setInputBarOffset: (offset) => {
|
||||||
|
set({ inputBarOffset: offset });
|
||||||
|
},
|
||||||
|
|
||||||
|
setKeyboardOpen: (open) => {
|
||||||
|
set({ isKeyboardOpen: open });
|
||||||
|
},
|
||||||
|
|
||||||
toggleFavoriteModel: (providerID, modelID) => {
|
toggleFavoriteModel: (providerID, modelID) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exists = state.favoriteModels.some(
|
const exists = state.favoriteModels.some(
|
||||||
|
|||||||
Reference in New Issue
Block a user