diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 8f91f08c7093..76f17801d5af 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -53,7 +53,6 @@ import { folderDropTarget, resolveDroppedFolderPath } from "./folderDrop"; import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model"; import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits"; import { - Fragment, memo, type ComponentProps, type ReactNode, @@ -185,17 +184,14 @@ import { import { useComposerPathSearch } from "../../lib/composerPathSearchState"; import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; import { - COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX, - COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, getRestingComposerImagePreviewCounts, resolveRestingComposerControlsLayout, shouldAnimateComposerRestingTransition, shouldUseCompactComposerPrimaryActions, - shouldUseCompactComposerFooter, + resolveRestingComposerControlsNaturalWidth, shouldUseRestingComposerLayout, } from "../composerFooterLayout"; import { measureRestingComposerControls } from "./restingComposerControlsMeasurement"; -import { observeResponsiveBreakpointFade, usePanelAnimationSettings } from "../../panelAnimations"; import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor"; import { ComposerContextActionsContext, @@ -2096,8 +2092,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) key: 0, active: false, }); - const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = - usePanelAnimationSettings(); const isComposerCollapsedMobile = isMobileViewport && !forceExpandedOnMobile && !isComposerFocused && !hasMultilinePrompt; @@ -3133,71 +3127,48 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) // ------------------------------------------------------------------ // Footer compact layout observation // ------------------------------------------------------------------ - useLayoutEffect(() => { + const measureFooterCompactness = useCallback(() => { const composerForm = composerFormRef.current; if (!composerForm) return; - const measureComposerFormWidth = () => composerForm.clientWidth; - const measureFooterCompactness = () => { - const composerFormWidth = measureComposerFormWidth(); - const footerCompact = shouldUseCompactComposerFooter(composerFormWidth, { + setIsComposerPrimaryActionsCompact( + shouldUseCompactComposerPrimaryActions(composerForm.clientWidth, { hasWideActions: composerFooterHasWideActions, - }); - const primaryActionsCompact = - footerCompact && - shouldUseCompactComposerPrimaryActions(composerFormWidth, { - hasWideActions: composerFooterHasWideActions, - }); - return { - primaryActionsCompact, - footerCompact, - }; - }; - - const initialCompactness = measureFooterCompactness(); - setIsComposerPrimaryActionsCompact(initialCompactness.primaryActionsCompact); - setIsComposerFooterCompact(initialCompactness.footerCompact); - if (typeof ResizeObserver === "undefined") return; - const footerControls = composerFooterControlsRef.current; - const stopFooterControlsFade = footerControls - ? observeResponsiveBreakpointFade({ - target: footerControls, - container: composerForm, - active: panelAnimationsActive, - durationMs: panelAnimationDurationMs, - breakpoint: { - value: composerFooterHasWideActions - ? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX - : COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX, - unit: "px", - }, - }) - : undefined; - - const observer = new ResizeObserver(() => { - const nextCompactness = measureFooterCompactness(); - setIsComposerPrimaryActionsCompact((previous) => - previous === nextCompactness.primaryActionsCompact - ? previous - : nextCompactness.primaryActionsCompact, - ); - setIsComposerFooterCompact((previous) => - previous === nextCompactness.footerCompact ? previous : nextCompactness.footerCompact, - ); - }); - + }), + ); + const controls = composerFooterControlsRef.current; + if (!controls || controls.clientWidth === 0) return; + const measurement = measureRestingComposerControls(controls); + if (!measurement) return; + const style = getComputedStyle(controls); + const availableWidth = + controls.clientWidth - + (Number.parseFloat(style.paddingInlineStart) || 0) - + (Number.parseFloat(style.paddingInlineEnd) || 0); + const naturalWidth = resolveRestingComposerControlsNaturalWidth(measurement); + // Hidden controls keep their natural width, so reopening the footer does + // not depend on the smaller overflow menu. Leave a pixel for rounding. + setIsComposerFooterCompact((previous) => naturalWidth > availableWidth - (previous ? 1 : 0)); + }, [composerFooterHasWideActions]); + + useLayoutEffect(measureFooterCompactness); + useEffect(() => { + const composerForm = composerFormRef.current; + if (!composerForm || typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver(measureFooterCompactness); observer.observe(composerForm); + const controls = composerFooterControlsRef.current; + if (controls) observer.observe(controls); + document.fonts.addEventListener("loadingdone", measureFooterCompactness); return () => { observer.disconnect(); - stopFooterControlsFade?.(); + document.fonts.removeEventListener("loadingdone", measureFooterCompactness); }; }, [ activeThreadId, composerFooterActionLayoutKey, - composerFooterHasWideActions, isComposerApprovalState, isComposerCollapsedMobile, - panelAnimationDurationMs, - panelAnimationsActive, + measureFooterCompactness, ]); // ------------------------------------------------------------------ @@ -4909,7 +4880,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) content: ( <> - {composerControlsInStrip ? restingProviderTraitsPicker : providerTraitsPicker} + {composerControlsInStrip + ? restingProviderTraitsPicker + : renderProviderTraitsPicker({ + ...providerTraitsPickerInput, + hidden: composerControlsHidden || composerControlsCompact, + })} ), }, @@ -4923,7 +4899,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) interactionMode={interactionMode} runtimeMode={runtimeMode} size={composerControlsInStrip ? "xs" : "sm"} - hidden={composerControlsHidden || restingHiddenBlockCount > 0} + hidden={composerControlsHidden || composerControlsCompact || restingHiddenBlockCount > 0} onToggleInteractionMode={toggleInteractionMode} onRuntimeModeChange={handleRuntimeModeChange} /> @@ -5052,56 +5028,49 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onToggleInteractionMode={toggleInteractionMode} onRuntimeModeChange={handleRuntimeModeChange} /> - ) : ( - <> - {restingBlockDefs.map((def, index) => { - if (!composerControlsInStrip) { - return {def.content}; + ) : null} + {restingBlockDefs.map((def, index) => { + const hidden = + composerControlsCompact || index >= restingBlockDefs.length - restingHiddenBlockCount; + return ( + + ); + })} + {composerControlsInStrip ? ( +
+
+ ) : null} ); const showTasksTab = @@ -6901,9 +6870,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) data-chat-composer-footer="true" data-chat-composer-footer-compact={isComposerFooterCompact ? "true" : "false"} className={cn( - "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:px-4 sm:pb-4", + "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:gap-0 sm:px-4 sm:pb-4", pendingUserInputs.length > 0 && "pt-2", - isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0", showMobilePendingAnswerActions && "hidden sm:flex", isComposerResting && "absolute bottom-px right-px z-10 h-12 w-auto gap-0 py-0 sm:gap-0 sm:py-0",