fix: keep the composer's + and Integrations buttons visible at larger UI Scale on mobile (#30374)

On a phone with UI Scale at 1.2x or higher, the model selector in the chat composer covers the Integrations button, so it cannot be seen or tapped. From 1.3x it covers the + button too, which leaves no way to attach files or open integrations.

The model selector's width cap is rem based, so it grows with UI Scale, and the left button group could shrink to nothing, so the selector took the space first. The + and Integrations buttons now sit in a group that keeps its width, and only the toggled chips stay in the scrolling strip. When space runs out the model name truncates, and chips scroll as before.

Measured in the real app at a 360px mobile viewport: before, Integrations is covered at 1.2x and both buttons at 1.3x and 1.5x; after, both are tappable at every scale, with the model name truncated (114px at 1.3x). Button positions at 1x and on desktop are unchanged to the pixel, with and without chips.

Fixes #29989
This commit is contained in:
Classic298 2026-09-24 06:08:48 +02:00 • committed by GitHub
parent a046e07044
commit e6e9fc9721
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2223,7 +2223,7 @@
</div>
<div class=" flex justify-between mt-0.5 mb-2 mx-0.5 max-w-full" dir="ltr">
<div class="ml-1 self-end flex items-center flex-1 min-w-0">
<div class="ml-1 self-end flex items-center">
<InputMenu
bind:files
selectedModels={selectedModelIds}
@ -2298,279 +2298,277 @@
<div
class="flex self-center w-[0.0625rem] h-4 mx-1 bg-gray-200/50 dark:bg-gray-800/50 shrink-0"
/>
{/if}
<div class="flex flex-1 items-center min-w-0 overflow-x-auto scrollbar-none">
{#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || showSkillsButton || (toggleFilters && toggleFilters.length > 0)}
<IntegrationsMenu
selectedModels={selectedModelIds}
{toggleFilters}
{showWebSearchButton}
{showImageGenerationButton}
{showCodeInterpreterButton}
bind:selectedToolIds
bind:selectedSkillIds
bind:selectedFilterIds
bind:webSearchEnabled
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
oauthRedirectHandler={(tool: {
id: string;
serverId: string;
authType?: string | null;
}) => oauthRedirectHandler(tool, chatInputDraft)}
{onWebSearchToggle}
closeOnOutsideClick={integrationsMenuCloseOnOutsideClick}
onShowValves={(e) => {
const { type, id } = e;
selectedValvesType = type;
selectedValvesItemId = id;
showValvesModal = true;
integrationsMenuCloseOnOutsideClick = false;
}}
onClose={async () => {
await tick();
<IntegrationsMenu
selectedModels={selectedModelIds}
{toggleFilters}
{showWebSearchButton}
{showImageGenerationButton}
{showCodeInterpreterButton}
bind:selectedToolIds
bind:selectedSkillIds
bind:selectedFilterIds
bind:webSearchEnabled
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
oauthRedirectHandler={(tool: {
id: string;
serverId: string;
authType?: string | null;
}) => oauthRedirectHandler(tool, chatInputDraft)}
{onWebSearchToggle}
closeOnOutsideClick={integrationsMenuCloseOnOutsideClick}
onShowValves={(e) => {
const { type, id } = e;
selectedValvesType = type;
selectedValvesItemId = id;
showValvesModal = true;
integrationsMenuCloseOnOutsideClick = false;
}}
onClose={async () => {
await tick();
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
}}
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
}}
>
<button
type="button"
id="integration-menu-button"
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-[1.875rem] flex justify-center items-center outline-hidden focus:outline-hidden shrink-0"
aria-label={$i18n.t('Integrations')}
>
<Component className="size-4.5" strokeWidth="1.5" />
</button>
</IntegrationsMenu>
{/if}
</div>
<div class="flex flex-1 items-center min-w-0 overflow-x-auto scrollbar-none">
{#if selectedModelIds.length === 1 && $models.find((m) => m.id === selectedModelIds[0])?.has_user_valves}
<div class="ml-1 flex gap-1.5 shrink-0">
<Tooltip content={$i18n.t('Valves')} placement="top">
<button
type="button"
id="integration-menu-button"
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-[1.875rem] flex justify-center items-center outline-hidden focus:outline-hidden shrink-0"
aria-label={$i18n.t('Integrations')}
id="model-valves-button"
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-[1.875rem] flex justify-center items-center outline-hidden focus:outline-hidden"
on:click={() => {
selectedValvesType = 'function';
selectedValvesItemId = selectedModelIds[0]?.split('.')[0];
showValvesModal = true;
}}
>
<Component className="size-4.5" strokeWidth="1.5" />
<Knobs className="size-4" strokeWidth="1.5" />
</button>
</IntegrationsMenu>
</Tooltip>
</div>
{/if}
<div class="ml-1 flex gap-1.5 shrink-0">
{#if (selectedToolIds ?? []).length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Tools', {
COUNT: (selectedToolIds ?? []).length
})}
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label={$i18n.t('Available Tools')}
type="button"
on:click={() => {
showTools = !showTools;
}}
>
<Wrench className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedToolIds ?? []).length}
</span>
</button>
</Tooltip>
{/if}
{#if selectedModelIds.length === 1 && $models.find((m) => m.id === selectedModelIds[0])?.has_user_valves}
<div class="ml-1 flex gap-1.5 shrink-0">
<Tooltip content={$i18n.t('Valves')} placement="top">
<button
type="button"
id="model-valves-button"
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-[1.875rem] flex justify-center items-center outline-hidden focus:outline-hidden"
on:click={() => {
selectedValvesType = 'function';
selectedValvesItemId = selectedModelIds[0]?.split('.')[0];
showValvesModal = true;
}}
>
<Knobs className="size-4" strokeWidth="1.5" />
</button>
</Tooltip>
</div>
{#if (selectedSkillIds ?? []).length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Skills', {
COUNT: (selectedSkillIds ?? []).length
})}
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label={$i18n.t('Available Skills')}
type="button"
on:click={() => {
showSkills = !showSkills;
}}
>
<Cube className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedSkillIds ?? []).length}
</span>
</button>
</Tooltip>
{/if}
<div class="ml-1 flex gap-1.5 shrink-0">
{#if (selectedToolIds ?? []).length > 0}
{#each selectedFilterIds as filterId (filterId)}
{@const filter = toggleFilters.find((f) => f.id === filterId)}
{#if filter}
<Tooltip
content={$i18n.t('{{COUNT}} Available Tools', {
COUNT: (selectedToolIds ?? []).length
})}
content={resolveLocalizedFunction(
filter,
$localizedFunctions,
$i18n.language
)}
placement="top"
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label={$i18n.t('Available Tools')}
type="button"
on:click={() => {
showTools = !showTools;
}}
>
<Wrench className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedToolIds ?? []).length}
</span>
</button>
</Tooltip>
{/if}
{#if (selectedSkillIds ?? []).length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Skills', {
COUNT: (selectedSkillIds ?? []).length
})}
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label={$i18n.t('Available Skills')}
type="button"
on:click={() => {
showSkills = !showSkills;
}}
>
<Cube className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedSkillIds ?? []).length}
</span>
</button>
</Tooltip>
{/if}
{#each selectedFilterIds as filterId (filterId)}
{@const filter = toggleFilters.find((f) => f.id === filterId)}
{#if filter}
<Tooltip
content={resolveLocalizedFunction(
filter,
$localizedFunctions,
$i18n.language
)}
placement="top"
>
<button
on:click|preventDefault={() => {
if (
filter?.has_user_valves &&
($_user?.role === 'admin' ||
($_user?.permissions?.chat?.valves ?? true))
) {
selectedValvesType = 'function';
selectedValvesItemId = filterId;
showValvesModal = true;
} else {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
);
}
}}
type="button"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
filterId
)
? 'text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
>
{#if filter?.icon}
<div class="size-4 items-center flex justify-center">
<img
src={filter.icon}
class="size-3.5 {filter.icon.includes('data:image/svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={resolveLocalizedFunction(
filter,
$localizedFunctions,
$i18n.language
)}
/>
</div>
{:else}
<Sparkles className="size-4" strokeWidth="1.75" />
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hidden group-hover:block"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
);
}}
>
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{/each}
{#if webSearchEnabled && showWebSearchButton}
<Tooltip content={$i18n.t('Web Search')} placement="top">
<button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
type="button"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always'
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
>
<GlobeAlt className="size-4" strokeWidth="1.75" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#if imageGenerationEnabled && showImageGenerationButton}
<Tooltip content={$i18n.t('Image')} placement="top">
<button
on:click|preventDefault={() =>
(imageGenerationEnabled = !imageGenerationEnabled)}
type="button"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
>
<Photo className="size-4" strokeWidth="1.75" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#if codeInterpreterEnabled && showCodeInterpreterButton}
<Tooltip content={$i18n.t('Code Interpreter')} placement="top">
<button
aria-label={codeInterpreterEnabled
? $i18n.t('Disable Code Interpreter')
: $i18n.t('Enable Code Interpreter')}
aria-pressed={codeInterpreterEnabled}
on:click|preventDefault={() =>
(codeInterpreterEnabled = !codeInterpreterEnabled)}
type="button"
class=" group p-[0.375rem] flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden {codeInterpreterEnabled
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} {($settings?.highContrastMode ??
false)
? 'm-1'
: 'focus:outline-hidden rounded-full'}"
>
<Terminal className="size-3.5" strokeWidth="2" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#each pendingOAuthTools as pendingTool (pendingTool.id)}
<Tooltip content={$i18n.t('Click to connect')} placement="top">
<button
on:click|preventDefault={() => {
oauthRedirectHandler(pendingTool, chatInputDraft);
if (
filter?.has_user_valves &&
($_user?.role === 'admin' ||
($_user?.permissions?.chat?.valves ?? true))
) {
selectedValvesType = 'function';
selectedValvesItemId = filterId;
showValvesModal = true;
} else {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
);
}
}}
type="button"
class="group px-2 py-[0.3125rem] flex gap-1.5 items-center text-xs rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden
text-amber-600 dark:text-amber-400 bg-amber-50 hover:bg-amber-100 dark:bg-amber-400/10 dark:hover:bg-amber-600/10 border border-amber-200/40 dark:border-amber-500/20"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
filterId
)
? 'text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
>
<Wrench className="size-3.5" strokeWidth="1.75" />
<span class="truncate">{pendingTool.name}</span>
{#if filter?.icon}
<div class="size-4 items-center flex justify-center">
<img
src={filter.icon}
class="size-3.5 {filter.icon.includes('data:image/svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={resolveLocalizedFunction(
filter,
$localizedFunctions,
$i18n.language
)}
/>
</div>
{:else}
<Sparkles className="size-4" strokeWidth="1.75" />
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hidden group-hover:block"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
);
}}
>
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/each}
<!-- Terminal Server Selector -->
{#if showTerminalSelector}
<TerminalMenu
bind:show={showTerminalMenu}
disabled={generating ||
(!!history?.currentId &&
history.messages[history.currentId]?.done != true)}
/>
{/if}
</div>
{/each}
{#if webSearchEnabled && showWebSearchButton}
<Tooltip content={$i18n.t('Web Search')} placement="top">
<button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
type="button"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always'
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
>
<GlobeAlt className="size-4" strokeWidth="1.75" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#if imageGenerationEnabled && showImageGenerationButton}
<Tooltip content={$i18n.t('Image')} placement="top">
<button
on:click|preventDefault={() =>
(imageGenerationEnabled = !imageGenerationEnabled)}
type="button"
class="group p-[0.375rem] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
>
<Photo className="size-4" strokeWidth="1.75" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#if codeInterpreterEnabled && showCodeInterpreterButton}
<Tooltip content={$i18n.t('Code Interpreter')} placement="top">
<button
aria-label={codeInterpreterEnabled
? $i18n.t('Disable Code Interpreter')
: $i18n.t('Enable Code Interpreter')}
aria-pressed={codeInterpreterEnabled}
on:click|preventDefault={() =>
(codeInterpreterEnabled = !codeInterpreterEnabled)}
type="button"
class=" group p-[0.375rem] flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden {codeInterpreterEnabled
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} {($settings?.highContrastMode ??
false)
? 'm-1'
: 'focus:outline-hidden rounded-full'}"
>
<Terminal className="size-3.5" strokeWidth="2" />
<div class="hidden group-hover:block">
<XMark className="size-4" strokeWidth="1.75" />
</div>
</button>
</Tooltip>
{/if}
{#each pendingOAuthTools as pendingTool (pendingTool.id)}
<Tooltip content={$i18n.t('Click to connect')} placement="top">
<button
on:click|preventDefault={() => {
oauthRedirectHandler(pendingTool, chatInputDraft);
}}
type="button"
class="group px-2 py-[0.3125rem] flex gap-1.5 items-center text-xs rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden
text-amber-600 dark:text-amber-400 bg-amber-50 hover:bg-amber-100 dark:bg-amber-400/10 dark:hover:bg-amber-600/10 border border-amber-200/40 dark:border-amber-500/20"
>
<Wrench className="size-3.5" strokeWidth="1.75" />
<span class="truncate">{pendingTool.name}</span>
</button>
</Tooltip>
{/each}
<!-- Terminal Server Selector -->
{#if showTerminalSelector}
<TerminalMenu
bind:show={showTerminalMenu}
disabled={generating ||
(!!history?.currentId &&
history.messages[history.currentId]?.done != true)}
/>
{/if}
</div>
</div>