From ce23f0e66f3a7084e609b73f90c21b5d4f214f46 Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Wed, 18 Mar 2026 22:01:32 +0200 Subject: [PATCH 1/2] feat(ui): replace prompt window resize handle with bottom-edge drag handle --- .../components/Core/ParamNegativePrompt.tsx | 8 +- .../components/Core/ParamPositivePrompt.tsx | 9 +- .../components/Prompts/PromptResizeHandle.tsx | 135 ++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 685a0b7a2aa..b0405c0ff38 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -4,6 +4,7 @@ import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize' import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; +import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -22,6 +23,8 @@ const persistOptions: Parameters[2] = { trackHeight: true, }; +const NEGATIVE_PROMPT_MIN_HEIGHT = 28; + export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); const prompt = useAppSelector(selectNegativePromptWithFallback); @@ -70,14 +73,16 @@ export const ParamNegativePrompt = memo(() => { onChange={onChange} onKeyDown={onKeyDown} variant="darkFilled" - minH={28} borderTopWidth={24} // This prevents the prompt from being hidden behind the header paddingInlineEnd={10} paddingInlineStart={3} paddingTop={0} paddingBottom={3} + resize="none" + minH={NEGATIVE_PROMPT_MIN_HEIGHT} fontFamily="mono" fontSize="0.82rem" + sx={{ '&::-webkit-resizer': { display: 'none' } }} /> @@ -90,6 +95,7 @@ export const ParamNegativePrompt = memo(() => { label={`${t('parameters.negativePromptPlaceholder')} (${t('stylePresets.preview')})`} /> )} + ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index 55071a6eadf..b2ba5c516ef 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -11,6 +11,7 @@ import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/compone import { NegativePromptToggleButton } from 'features/parameters/components/Core/NegativePromptToggleButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; +import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -35,6 +36,8 @@ const persistOptions: Parameters[2] = { initialHeight: 120, }; +const POSITIVE_PROMPT_MIN_HEIGHT = 32; + const usePromptHistory = () => { const store = useAppStore(); const history = useAppSelector(selectPositivePromptHistory); @@ -215,10 +218,11 @@ export const ParamPositivePrompt = memo(() => { paddingInlineStart={3} paddingTop={0} paddingBottom={3} - resize="vertical" - minH={32} + resize="none" + minH={POSITIVE_PROMPT_MIN_HEIGHT} fontFamily="mono" fontSize="0.82rem" + sx={{ '&::-webkit-resizer': { display: 'none' } }} /> @@ -236,6 +240,7 @@ export const ParamPositivePrompt = memo(() => { label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`} /> )} + diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx new file mode 100644 index 00000000000..7128798aa46 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx @@ -0,0 +1,135 @@ +import { Box } from '@invoke-ai/ui-library'; +import { + memo, + type PointerEvent as ReactPointerEvent, + type RefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +type PromptResizeHandleProps = { + textareaRef: RefObject; + minHeight: number; +}; + +export const PROMPT_RESIZE_HANDLE_HEIGHT_PX = 8; + +export const PromptResizeHandle = memo(({ textareaRef, minHeight }: PromptResizeHandleProps) => { + const activePointerIdRef = useRef(null); + const startHeightRef = useRef(0); + const startYRef = useRef(0); + const previousCursorRef = useRef(''); + const previousUserSelectRef = useRef(''); + const [isResizing, setIsResizing] = useState(false); + + const stopResize = useCallback(() => { + if (activePointerIdRef.current === null) { + return; + } + + activePointerIdRef.current = null; + setIsResizing(false); + document.body.style.cursor = previousCursorRef.current; + document.body.style.userSelect = previousUserSelectRef.current; + }, []); + + useEffect(() => stopResize, [stopResize]); + + const onPointerDown = useCallback( + (e: ReactPointerEvent) => { + if (e.button !== 0) { + return; + } + + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + activePointerIdRef.current = e.pointerId; + startYRef.current = e.clientY; + startHeightRef.current = textarea.offsetHeight; + previousCursorRef.current = document.body.style.cursor; + previousUserSelectRef.current = document.body.style.userSelect; + + document.body.style.cursor = 'ns-resize'; + document.body.style.userSelect = 'none'; + e.currentTarget.setPointerCapture(e.pointerId); + setIsResizing(true); + e.preventDefault(); + }, + [textareaRef] + ); + + const onPointerMove = useCallback( + (e: ReactPointerEvent) => { + if (activePointerIdRef.current !== e.pointerId) { + return; + } + + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + const nextHeight = Math.max(minHeight, startHeightRef.current + e.clientY - startYRef.current); + textarea.style.height = `${nextHeight}px`; + e.preventDefault(); + }, + [minHeight, textareaRef] + ); + + const onPointerUp = useCallback( + (e: ReactPointerEvent) => { + if (activePointerIdRef.current !== e.pointerId) { + return; + } + + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + + stopResize(); + }, + [stopResize] + ); + + const onPointerCancel = useCallback( + (e: ReactPointerEvent) => { + if (activePointerIdRef.current !== e.pointerId) { + return; + } + + stopResize(); + }, + [stopResize] + ); + + return ( + + ); +}); + +PromptResizeHandle.displayName = 'PromptResizeHandle'; From db624a8f27e61e7ba24f32611e99bfae0f3f9825 Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Wed, 18 Mar 2026 22:35:10 +0200 Subject: [PATCH 2/2] Fix: removed unused export --- .../parameters/components/Prompts/PromptResizeHandle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx index 7128798aa46..0a5f211924d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/PromptResizeHandle.tsx @@ -14,7 +14,7 @@ type PromptResizeHandleProps = { minHeight: number; }; -export const PROMPT_RESIZE_HANDLE_HEIGHT_PX = 8; +const PROMPT_RESIZE_HANDLE_HEIGHT_PX = 8; export const PromptResizeHandle = memo(({ textareaRef, minHeight }: PromptResizeHandleProps) => { const activePointerIdRef = useRef(null);