mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 18:22:29 +00:00
feat(promptbox): full editing controls in fullscreen focus mode
Add a footer to the fullscreen prompt modal (desktop lib + webapp) so focus mode is a complete editing surface, not just the textarea: - Model selector, always-on reference-image row, and a Done button (closes the modal) instead of Generate. - Video: also surfaces the input-mode (keyframe/reference) picker and the @Characters button (seedance_2p0). Shared image-row / character / input-mode elements are reused inline and in the modal (DRY). - Webapp: model selector + leftToolbar + image row in the footer. Layout: editor area is flex-1/min-h-0/overflow-hidden with the image row and footer shrink-0, so the editor takes only the leftover space and scrolls internally - resizing the window shorter no longer overflows. MentionTextarea gains a `style` prop so the modal can cap resize/height.
This commit is contained in:
@@ -323,24 +323,28 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
});
|
||||
}, [prompt, hasMentionItems, mentionRegex, mentionLabelMap, mentionItems]);
|
||||
|
||||
// Shared reference-image row, used both inline (gated by isImageRowVisible)
|
||||
// and always-on in the fullscreen focus modal.
|
||||
const imagePromptRowEl = supportsImagePrompts ? (
|
||||
<ImagePromptRow
|
||||
maxImagePromptCount={maxImagePromptCount}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={onReferenceImagesChange}
|
||||
onPickFromLibrary={onPickFromLibrary}
|
||||
onClearAll={onClearAllRefs}
|
||||
isVideo={isVideo}
|
||||
isReferenceMode={isReferenceMode}
|
||||
endFrameImage={endFrameImage}
|
||||
setEndFrameImage={onEndFrameImageChange}
|
||||
showEndFrameSection={showEndFrameSection}
|
||||
onPickEndFrameFromLibrary={onPickEndFrameFromLibrary}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="prompt-box-root">
|
||||
<div className="relative flex flex-col">
|
||||
{isImageRowVisible && (
|
||||
<ImagePromptRow
|
||||
maxImagePromptCount={maxImagePromptCount}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={onReferenceImagesChange}
|
||||
onPickFromLibrary={onPickFromLibrary}
|
||||
onClearAll={onClearAllRefs}
|
||||
isVideo={isVideo}
|
||||
isReferenceMode={isReferenceMode}
|
||||
endFrameImage={endFrameImage}
|
||||
setEndFrameImage={onEndFrameImageChange}
|
||||
showEndFrameSection={showEndFrameSection}
|
||||
onPickEndFrameFromLibrary={onPickEndFrameFromLibrary}
|
||||
/>
|
||||
)}
|
||||
{isImageRowVisible && imagePromptRowEl}
|
||||
|
||||
{mediaReferenceRow}
|
||||
|
||||
@@ -555,7 +559,17 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PromptFullscreenModal isOpen={isFullscreen} onClose={closeFullscreen}>
|
||||
<PromptFullscreenModal
|
||||
isOpen={isFullscreen}
|
||||
onClose={closeFullscreen}
|
||||
footerControls={
|
||||
<>
|
||||
{modelSelector}
|
||||
{leftToolbar}
|
||||
</>
|
||||
}
|
||||
imagePromptRow={imagePromptRowEl}
|
||||
>
|
||||
{hasMentionItems && mentionItems ? (
|
||||
<MentionTextarea
|
||||
value={prompt}
|
||||
@@ -563,7 +577,7 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
mentionItems={mentionItems}
|
||||
placeholder={placeholder}
|
||||
className="promptbox-scrollbar h-full min-h-0 w-full overflow-y-auto text-base-fg placeholder-base-fg/60"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)", resize: "none" }}
|
||||
style={{ resize: "none" }}
|
||||
colorMap={mentionColorMap}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
@@ -581,7 +595,6 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
<textarea
|
||||
placeholder={placeholder}
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUpRightAndDownLeftFromCenter } from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -56,12 +57,19 @@ interface PromptFullscreenModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
/** Controls rendered in the footer, left of the Done button (e.g. the model
|
||||
* selector + character button). */
|
||||
footerControls?: ReactNode;
|
||||
/** Reference-image row, always shown in focus mode. */
|
||||
imagePromptRow?: ReactNode;
|
||||
}
|
||||
|
||||
export const PromptFullscreenModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
footerControls,
|
||||
imagePromptRow,
|
||||
}: PromptFullscreenModalProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -79,18 +87,30 @@ export const PromptFullscreenModal = ({
|
||||
accessibleTitle="Prompt focus mode"
|
||||
closeOnOutsideClick
|
||||
closeOnEsc
|
||||
className="h-[70vh] w-full max-w-4xl"
|
||||
className="w-full max-w-4xl"
|
||||
backdropClassName="backdrop-blur-md"
|
||||
>
|
||||
{/* Title lives inside the flex column (not the Modal title slot) so the
|
||||
editor fits the fixed panel height cleanly. No overflow-hidden here —
|
||||
the mention autocomplete dropdown renders outside the editor bounds
|
||||
and must not be clipped. */}
|
||||
<div ref={contentRef} className="flex h-full flex-col gap-2">
|
||||
<h2 className="text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex column so a flex-1 editor (the MentionTextarea root wrapper)
|
||||
or an h-full textarea fills the remaining space. */}
|
||||
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
||||
{/* Explicit inline height (not a Tailwind arbitrary class) so the column
|
||||
is reliably bounded across the modal's nested wrappers — that's what
|
||||
lets the editor scroll instead of stretching the panel. min-h-0 lets
|
||||
the flex children shrink below content size. */}
|
||||
<div className="flex min-h-0 flex-col gap-2" style={{ height: "70vh" }}>
|
||||
<h2 className="shrink-0 text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex-1 so the editor takes only the space left after the (shrink-0)
|
||||
image row + footer — keeps it from overflowing when the window is
|
||||
shorter. min-h-0 + overflow-hidden bound the editor area so its own
|
||||
textarea scrolls instead of pushing the layout. */}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{imagePromptRow && <div className="shrink-0">{imagePromptRow}</div>}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{footerControls}</div>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -558,11 +558,26 @@ export const PromptBox2D = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe your image..."
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -898,11 +898,26 @@ export const PromptBox3D = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe your image..."
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -568,11 +568,25 @@ export const PromptBoxEdit = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Write what you want to change in your image and click generate..."
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -574,11 +574,26 @@ export const PromptBoxImage = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe what you want in the image..."
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -967,6 +967,70 @@ export const PromptBoxVideo = ({
|
||||
}
|
||||
}, [selectedModel, endFrameImage, setEndFrameImage]);
|
||||
|
||||
// Shared reference-image row, used both inline (gated by isImageRowVisible)
|
||||
// and always-on in the fullscreen focus modal.
|
||||
const imagePromptRowEl = (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
isVideo={true}
|
||||
isReferenceMode={isReferenceMode}
|
||||
maxImagePromptCount={maxImageCount}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
onImageClick={(image) => {
|
||||
setContent(
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>,
|
||||
);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
uploadImage={uploadImage}
|
||||
endFrameImage={isReferenceMode ? undefined : endFrameImage}
|
||||
setEndFrameImage={isReferenceMode ? undefined : setEndFrameImage}
|
||||
allowUploadEnd={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
showEndFrameSection={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
referenceVideos={isReferenceMode ? referenceVideos : undefined}
|
||||
setReferenceVideos={isReferenceMode ? setReferenceVideos : undefined}
|
||||
maxVideoCount={selectedModel?.maxReferenceVideos ?? 3}
|
||||
maxVideoRefDuration={selectedModel?.maxVideoRefDuration ?? 15}
|
||||
showVideoReferenceSection={isReferenceMode}
|
||||
uploadVideo={uploadVideo}
|
||||
referenceAudios={isReferenceMode ? referenceAudios : undefined}
|
||||
setReferenceAudios={isReferenceMode ? setReferenceAudios : undefined}
|
||||
maxAudioCount={selectedModel?.maxReferenceAudios ?? 2}
|
||||
maxAudioRefDuration={selectedModel?.maxAudioRefDuration ?? 15}
|
||||
uploadAudio={uploadAudio}
|
||||
/>
|
||||
);
|
||||
|
||||
// Character button (seedance_2p0 only), reused in the fullscreen footer.
|
||||
const characterButtonEl =
|
||||
selectedModel?.id === "seedance_2p0" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCharactersModalOpen(true)}
|
||||
className="flex h-9 items-center justify-center gap-1 rounded-lg border border-ui-controls-border bg-ui-controls px-3 text-sm font-medium text-base-fg transition-all duration-150 hover:bg-ui-controls/80 active:scale-95"
|
||||
>
|
||||
@Characters
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
// Input-mode (keyframe/reference) picker, reused in the fullscreen footer.
|
||||
const inputModeEl = inputModeOptions ? (
|
||||
<Tooltip content="Input Mode" position="top" className="z-50" closeOnClick>
|
||||
<PopoverMenu
|
||||
items={inputModeOptions}
|
||||
onSelect={handleInputModeSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Input Mode"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -979,47 +1043,7 @@ export const PromptBoxVideo = ({
|
||||
{content}
|
||||
</Modal>
|
||||
<div className="relative z-20 flex flex-col gap-3">
|
||||
{isImageRowVisible && (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
isVideo={true}
|
||||
isReferenceMode={isReferenceMode}
|
||||
maxImagePromptCount={maxImageCount}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
onImageClick={(image) => {
|
||||
setContent(
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>,
|
||||
);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
uploadImage={uploadImage}
|
||||
endFrameImage={isReferenceMode ? undefined : endFrameImage}
|
||||
setEndFrameImage={isReferenceMode ? undefined : setEndFrameImage}
|
||||
allowUploadEnd={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
showEndFrameSection={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
referenceVideos={isReferenceMode ? referenceVideos : undefined}
|
||||
setReferenceVideos={
|
||||
isReferenceMode ? setReferenceVideos : undefined
|
||||
}
|
||||
maxVideoCount={selectedModel?.maxReferenceVideos ?? 3}
|
||||
maxVideoRefDuration={selectedModel?.maxVideoRefDuration ?? 15}
|
||||
showVideoReferenceSection={isReferenceMode}
|
||||
uploadVideo={uploadVideo}
|
||||
referenceAudios={isReferenceMode ? referenceAudios : undefined}
|
||||
setReferenceAudios={
|
||||
isReferenceMode ? setReferenceAudios : undefined
|
||||
}
|
||||
maxAudioCount={selectedModel?.maxReferenceAudios ?? 2}
|
||||
maxAudioRefDuration={selectedModel?.maxAudioRefDuration ?? 15}
|
||||
uploadAudio={uploadAudio}
|
||||
/>
|
||||
)}
|
||||
{isImageRowVisible && imagePromptRowEl}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-full rounded-2xl p-4",
|
||||
@@ -1184,31 +1208,9 @@ export const PromptBoxVideo = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{inputModeOptions && (
|
||||
<Tooltip
|
||||
content="Input Mode"
|
||||
position="top"
|
||||
className="z-50"
|
||||
closeOnClick={true}
|
||||
>
|
||||
<PopoverMenu
|
||||
items={inputModeOptions}
|
||||
onSelect={handleInputModeSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Input Mode"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{inputModeEl}
|
||||
|
||||
{selectedModel?.id === "seedance_2p0" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCharactersModalOpen(true)}
|
||||
className="flex h-9 items-center justify-center gap-1 rounded-lg border border-ui-controls-border bg-ui-controls px-3 text-sm font-medium text-base-fg transition-all duration-150 hover:bg-ui-controls/80 active:scale-95"
|
||||
>
|
||||
@Characters
|
||||
</button>
|
||||
)}
|
||||
{characterButtonEl}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{modelNeedsAnImageButNoneAreSelected && (
|
||||
@@ -1336,6 +1338,14 @@ export const PromptBoxVideo = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={
|
||||
<>
|
||||
{modelSelector}
|
||||
{inputModeEl}
|
||||
{characterButtonEl}
|
||||
</>
|
||||
}
|
||||
imagePromptRow={imagePromptRowEl}
|
||||
>
|
||||
{hasAnyMentionables ? (
|
||||
<MentionTextarea
|
||||
@@ -1349,13 +1359,12 @@ export const PromptBoxVideo = ({
|
||||
: "Describe what you want to happen in the video..."
|
||||
}
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)", resize: "none" }}
|
||||
style={{ resize: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
placeholder="Describe what you want to happen in the video..."
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
style={{ maxHeight: "calc(70vh - 7rem)" }}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { focusEditorAtEnd } from "./focusEditorAtEnd";
|
||||
|
||||
// Shared "focus mode" overlay for prompt editors. Reuses @storyteller/ui-modal
|
||||
@@ -20,6 +21,17 @@ interface PromptFullscreenModalProps {
|
||||
* MentionTextarea bound to the same value + onChange as the inline one.
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Optional controls rendered in the footer, left of the Done button — e.g.
|
||||
* the model selector + character button. The caller composes these from its
|
||||
* own toolbar pieces.
|
||||
*/
|
||||
footerControls?: ReactNode;
|
||||
/**
|
||||
* Optional reference-image row rendered above the footer, always visible in
|
||||
* focus mode regardless of the inline row's collapse state.
|
||||
*/
|
||||
imagePromptRow?: ReactNode;
|
||||
}
|
||||
|
||||
export const PromptFullscreenModal = ({
|
||||
@@ -28,6 +40,8 @@ export const PromptFullscreenModal = ({
|
||||
promptLength,
|
||||
maxLength,
|
||||
children,
|
||||
footerControls,
|
||||
imagePromptRow,
|
||||
}: PromptFullscreenModalProps) => {
|
||||
const overLimit = isFinite(maxLength) && promptLength > maxLength;
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -52,21 +66,20 @@ export const PromptFullscreenModal = ({
|
||||
{/* Explicit inline height (not a Tailwind arbitrary class) so the column
|
||||
is reliably bounded across the modal's nested wrappers — that's what
|
||||
lets the editor's textarea scroll instead of stretching the panel.
|
||||
min-h-0 on the flex children lets them shrink below content size. No
|
||||
overflow-hidden on the editor holder — the mention autocomplete
|
||||
dropdown renders outside it and must not be clipped; the editor scrolls
|
||||
via its own overflow-y-auto. */}
|
||||
<div
|
||||
className="flex min-h-0 flex-col gap-2"
|
||||
style={{ height: "70vh" }}
|
||||
>
|
||||
min-h-0 on the flex children lets them shrink below content size. */}
|
||||
<div className="flex min-h-0 flex-col gap-2" style={{ height: "70vh" }}>
|
||||
<h2 className="shrink-0 text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex-col so a flex-1 editor (MentionTextarea root) fills it; min-h-0
|
||||
so the editor area can shrink and its content scrolls. */}
|
||||
<div ref={contentRef} className="flex min-h-0 flex-1 flex-col">
|
||||
{/* flex-1 so the editor takes only the space left after the (shrink-0)
|
||||
image row + footer — that's what keeps it from overflowing when the
|
||||
window is shorter. min-h-0 + overflow-hidden bound the editor area so
|
||||
its own textarea scrolls instead of pushing the layout. */}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex shrink-0 items-center justify-end">
|
||||
<span
|
||||
className={`text-[11px] tabular-nums ${
|
||||
overLimit ? "text-red-500" : "text-base-fg/40"
|
||||
@@ -75,6 +88,11 @@ export const PromptFullscreenModal = ({
|
||||
{promptLength} / {isFinite(maxLength) ? maxLength : "∞"}
|
||||
</span>
|
||||
</div>
|
||||
{imagePromptRow && <div className="shrink-0">{imagePromptRow}</div>}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{footerControls}</div>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user