From 39b9f985cef0cc5c07b0a5dbc88c0508cf0c1d57 Mon Sep 17 00:00:00 2001 From: auroraflux Date: Thu, 1 Jan 2026 01:01:46 -0800 Subject: [PATCH] feat: add input bar offset setting for curved screen devices (#89) Co-authored-by: auroraflux --- packages/ui/src/components/chat/ChatInput.tsx | 4 +- .../ui/src/components/layout/MainLayout.tsx | 8 +++- .../sections/openchamber/OpenChamberPage.tsx | 4 +- .../openchamber/OpenChamberVisualSettings.tsx | 46 ++++++++++++++++++- packages/ui/src/stores/useUIStore.ts | 14 ++++++ 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 4bb10975..b351c55f 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -82,7 +82,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const { currentProviderId, currentModelId, currentAgentName, setAgent, getVisibleAgents } = useConfigStore(); const agents = getVisibleAgents(); - const { isMobile } = useUIStore(); + const { isMobile, inputBarOffset, isKeyboardOpen } = useUIStore(); const { working } = useAssistantStatus(); const [showAbortStatus, setShowAbortStatus] = React.useState(false); const abortTimeoutRef = React.useRef | null>(null); @@ -1166,7 +1166,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return ( -
+ 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined}> { let ignoreOpenUntilZero = false; let previousHeight = 0; + const setKeyboardOpen = useUIStore.getState().setKeyboardOpen; + const forceKeyboardClosed = () => { stickyKeyboardInset = 0; ignoreOpenUntilZero = true; root.style.setProperty('--oc-keyboard-inset', '0px'); + setKeyboardOpen(false); }; const updateVisualViewport = () => { @@ -147,16 +150,19 @@ export const MainLayout: React.FC = () => { // (prevents false positives during Android keyboard animation) const closingByHeight = !isTextTarget && height > previousHeight + 6; - if (measuredInset === 0) { +if (measuredInset === 0) { stickyKeyboardInset = 0; + setKeyboardOpen(false); } else if (closingByHeight) { forceKeyboardClosed(); } else if (measuredInset > 0 && isTextTarget) { // When focus is on text input, track actual inset (allows settling // to correct value after Android animation fluctuations) stickyKeyboardInset = measuredInset; + setKeyboardOpen(true); } else if (measuredInset > stickyKeyboardInset) { stickyKeyboardInset = measuredInset; + setKeyboardOpen(true); } } diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index d5b41b62..0f2645ee 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -64,9 +64,9 @@ export const OpenChamberPage: React.FC = ({ section }) => ); }; -// Visual section: Theme Mode, Font Size, Spacing +// Visual section: Theme Mode, Font Size, Spacing, Input Bar Offset (mobile) const VisualSectionContent: React.FC = () => { - return ; + return ; }; // Chat section: Default Tool Output, Diff layout, Show reasoning traces, Queue mode diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 0186da7c..1c6f25b8 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -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 { /** Which settings to show. If undefined, shows all. */ @@ -73,6 +73,8 @@ export const OpenChamberVisualSettings: React.FC const setFontSize = useUIStore(state => state.setFontSize); const padding = useUIStore(state => state.padding); 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 setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference); const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled); @@ -226,6 +228,48 @@ export const OpenChamberVisualSettings: React.FC )} + {shouldShow('inputBarOffset') && isMobile && ( +
+
+

+ Input Bar Offset +

+

+ Raise the input bar for phones with curved screen edges. +

+
+ +
+ 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" + /> + + + {inputBarOffset}px + + + 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" + > + + +
+
+ )} + {shouldShow('toolOutput') && (
diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 877d90ea..0641c7fa 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -24,6 +24,7 @@ interface UIStore { activeMainTab: MainTab; pendingDiffFile: string | null; isMobile: boolean; + isKeyboardOpen: boolean; isCommandPaletteOpen: boolean; isHelpDialogOpen: boolean; isAboutDialogOpen: boolean; @@ -40,6 +41,7 @@ interface UIStore { toolCallExpansion: 'collapsed' | 'activity' | 'detailed'; fontSize: number; padding: number; + inputBarOffset: number; favoriteModels: Array<{ providerID: string; modelID: string }>; recentModels: Array<{ providerID: string; modelID: string }>; @@ -75,6 +77,8 @@ interface UIStore { setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void; setFontSize: (size: number) => void; setPadding: (size: number) => void; + setInputBarOffset: (offset: number) => void; + setKeyboardOpen: (open: boolean) => void; applyTypography: () => void; applyPadding: () => void; updateProportionalSidebarWidths: () => void; @@ -99,6 +103,7 @@ export const useUIStore = create()( activeMainTab: 'chat', pendingDiffFile: null, isMobile: false, + isKeyboardOpen: false, isCommandPaletteOpen: false, isHelpDialogOpen: false, isAboutDialogOpen: false, @@ -114,6 +119,7 @@ export const useUIStore = create()( toolCallExpansion: 'collapsed', fontSize: 100, padding: 100, + inputBarOffset: 0, favoriteModels: [], recentModels: [], diffLayoutPreference: 'dynamic', @@ -336,6 +342,14 @@ export const useUIStore = create()( set({ diffWrapLines: wrap }); }, + setInputBarOffset: (offset) => { + set({ inputBarOffset: offset }); + }, + + setKeyboardOpen: (open) => { + set({ isKeyboardOpen: open }); + }, + toggleFavoriteModel: (providerID, modelID) => { set((state) => { const exists = state.favoriteModels.some(