feat(mobile): polish composer model/agent controls and selection overlay
Redesign the mobile composer model and agent buttons as borderless, full-bleed labels that hug their content, truncate with an ellipsis when space is tight, and show the provider logo inline before the model name. Tighten the footer action buttons (sessions / attach / auto-accept) so they sit close together, with a small left inset on the group. In the mobile model selection overlay, make the thinking-variant control text-only with a chevron, vertically center the variant and favorite controls in each row, and place the provider logo inline with the model name.
This commit is contained in:
@@ -4514,7 +4514,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full items-center justify-between gap-x-1.5">
|
<div className="flex w-full items-center justify-between gap-x-1.5">
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="composer-mobile-actions flex items-center gap-x-2 pl-1">
|
||||||
<MobileSessionPanelTrigger
|
<MobileSessionPanelTrigger
|
||||||
footerIconButtonClass={footerIconButtonClass}
|
footerIconButtonClass={footerIconButtonClass}
|
||||||
iconSizeClass={iconSizeClass}
|
iconSizeClass={iconSizeClass}
|
||||||
@@ -4539,7 +4539,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center min-w-0 gap-x-1 justify-end">
|
<div className="flex items-center min-w-0 gap-x-1 justify-end">
|
||||||
<div className="flex items-center gap-x-1 min-w-0 max-w-[60vw] flex-shrink">
|
<div className="flex items-center gap-x-2 min-w-0 max-w-[60vw] flex-shrink">
|
||||||
<MemoMobileModelButton onOpenModel={() => handleOpenMobilePanel('model')} className="min-w-0 flex-shrink" />
|
<MemoMobileModelButton onOpenModel={() => handleOpenMobilePanel('model')} className="min-w-0 flex-shrink" />
|
||||||
<MemoMobileAgentButton
|
<MemoMobileAgentButton
|
||||||
onOpenAgentPanel={handleOpenAgentPanel}
|
onOpenAgentPanel={handleOpenAgentPanel}
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export const MobileAgentButton: React.FC<MobileAgentButtonProps> = ({ onCycleAge
|
|||||||
onPointerLeave={handlePointerLeave}
|
onPointerLeave={handlePointerLeave}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex min-w-0 items-center select-none',
|
'inline-flex min-w-0 items-stretch select-none',
|
||||||
'rounded-lg border border-border/50 px-1.5',
|
'rounded-lg',
|
||||||
'typography-micro font-medium',
|
'typography-micro font-medium',
|
||||||
'focus:outline-none hover:bg-[var(--interactive-hover)]',
|
'focus:outline-none hover:bg-[var(--interactive-hover)]',
|
||||||
'touch-none',
|
'touch-none',
|
||||||
@@ -88,7 +88,9 @@ export const MobileAgentButton: React.FC<MobileAgentButtonProps> = ({ onCycleAge
|
|||||||
}}
|
}}
|
||||||
title={agentLabel}
|
title={agentLabel}
|
||||||
>
|
>
|
||||||
<span className="truncate">{agentLabel}</span>
|
<span className="flex h-full w-full min-w-0 items-center">
|
||||||
|
<span className="truncate">{agentLabel}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { getModelDisplayName } from './mobileControlsUtils';
|
import { getModelDisplayName } from './mobileControlsUtils';
|
||||||
|
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
interface MobileModelButtonProps {
|
interface MobileModelButtonProps {
|
||||||
@@ -12,6 +13,7 @@ interface MobileModelButtonProps {
|
|||||||
export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenModel, className }) => {
|
export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenModel, className }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const currentModelId = useConfigStore((state) => state.currentModelId);
|
const currentModelId = useConfigStore((state) => state.currentModelId);
|
||||||
|
const currentProviderId = useConfigStore((state) => state.currentProviderId);
|
||||||
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
|
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
|
||||||
const currentProvider = getCurrentProvider();
|
const currentProvider = getCurrentProvider();
|
||||||
const modelLabel = getModelDisplayName(currentProvider, currentModelId, t('chat.modelControls.selectModel'));
|
const modelLabel = getModelDisplayName(currentProvider, currentModelId, t('chat.modelControls.selectModel'));
|
||||||
@@ -21,8 +23,8 @@ export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenMode
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenModel}
|
onClick={onOpenModel}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex min-w-0 items-center justify-center',
|
'inline-flex min-w-0 items-stretch',
|
||||||
'rounded-lg border border-border/50 px-1.5',
|
'rounded-lg',
|
||||||
'typography-micro font-medium text-foreground/80',
|
'typography-micro font-medium text-foreground/80',
|
||||||
'focus:outline-none hover:bg-[var(--interactive-hover)]',
|
'focus:outline-none hover:bg-[var(--interactive-hover)]',
|
||||||
className
|
className
|
||||||
@@ -30,8 +32,11 @@ export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenMode
|
|||||||
style={{ height: '26px', maxHeight: '26px', minHeight: '26px' }}
|
style={{ height: '26px', maxHeight: '26px', minHeight: '26px' }}
|
||||||
title={modelLabel}
|
title={modelLabel}
|
||||||
>
|
>
|
||||||
<span className="min-w-0 max-w-full overflow-x-auto whitespace-nowrap scrollbar-hidden">
|
<span className="flex h-full w-full min-w-0 items-center gap-1">
|
||||||
{modelLabel}
|
{currentProviderId ? (
|
||||||
|
<ProviderLogo providerId={currentProviderId} className="size-4 flex-shrink-0" />
|
||||||
|
) : null}
|
||||||
|
<span className="truncate">{modelLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1671,7 +1671,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
isSelected && 'bg-interactive-selection/15 text-interactive-selection-foreground'
|
isSelected && 'bg-interactive-selection/15 text-interactive-selection-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 px-2 py-1.5">
|
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleMobileModelApply(providerId, modelId, resolvedVariant)}
|
onClick={() => handleMobileModelApply(providerId, modelId, resolvedVariant)}
|
||||||
@@ -1680,15 +1680,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary rounded-lg'
|
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary rounded-lg'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showProviderLogo ? (
|
|
||||||
<ProviderLogo providerId={providerId} className="mt-0.5 size-3.5 flex-shrink-0" />
|
|
||||||
) : null}
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
<div className="flex min-w-0 items-start gap-2">
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
|
{showProviderLogo ? (
|
||||||
|
<ProviderLogo providerId={providerId} className="size-3.5 flex-shrink-0" />
|
||||||
|
) : null}
|
||||||
<span className="typography-meta font-medium text-foreground truncate">
|
<span className="typography-meta font-medium text-foreground truncate">
|
||||||
{getModelDisplayName(model)}
|
{getModelDisplayName(model)}
|
||||||
</span>
|
</span>
|
||||||
{isSelected ? <Icon name="check" className="mt-0.5 size-4 flex-shrink-0 text-primary" /> : null}
|
{isSelected ? <Icon name="check" className="size-4 flex-shrink-0 text-primary" /> : null}
|
||||||
</div>
|
</div>
|
||||||
{contextText || indicatorIcons.length > 0 ? (
|
{contextText || indicatorIcons.length > 0 ? (
|
||||||
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
|
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
|
||||||
@@ -1722,7 +1722,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpandedMobileModelKey((prev) => prev === rowKey ? null : rowKey)}
|
onClick={() => setExpandedMobileModelKey((prev) => prev === rowKey ? null : rowKey)}
|
||||||
className="flex items-center gap-1 rounded-lg border border-border/40 px-2 py-1 typography-micro font-medium text-muted-foreground hover:bg-interactive-hover/50 flex-shrink-0"
|
className="flex items-center gap-0.5 typography-micro font-medium text-muted-foreground hover:text-foreground flex-shrink-0"
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
|
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
|
||||||
>
|
>
|
||||||
@@ -1730,7 +1730,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{isExpanded ? <Icon name="arrow-down-s" className="size-3.5" /> : <Icon name="arrow-right-s" className="size-3.5" />}
|
{isExpanded ? <Icon name="arrow-down-s" className="size-3.5" /> : <Icon name="arrow-right-s" className="size-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-shrink-0 items-start gap-1.5">
|
<div className="flex flex-shrink-0 items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
|||||||
@@ -85,6 +85,16 @@
|
|||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Composer footer action buttons (sessions / attach / auto-accept): hug the
|
||||||
|
icon so the group stays tight. The container only renders in the mobile
|
||||||
|
JSX, so no pointer/runtime gating is needed; !important overrides both the
|
||||||
|
w-8 utility and the global mobile touch-target min-width regardless of
|
||||||
|
cascade layer or specificity. */
|
||||||
|
.composer-mobile-actions button {
|
||||||
|
width: 1.5rem !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Improve input field touch targets */
|
/* Improve input field touch targets */
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input,
|
:root.mobile-pointer:not(.desktop-runtime) input,
|
||||||
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
||||||
|
|||||||
Reference in New Issue
Block a user