fix(ui): separate prompt composer action columns
Keep the textarea width independent from the prompt controls so wrapping matches the visible layout. Split secondary controls from the primary stop/send rail to preserve the original action column width and add a matching divider.
This commit is contained in:
@@ -581,113 +581,6 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<div class="prompt-nav-buttons">
|
|
||||||
<div class="prompt-nav-column prompt-nav-column-left">
|
|
||||||
<Show when={showVoiceInput()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`prompt-voice-button prompt-nav-voice-button ${voiceInput.isRecording() ? "is-recording" : ""}`}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
beginVoicePress(event)
|
|
||||||
}}
|
|
||||||
onPointerUp={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
endVoicePress()
|
|
||||||
}}
|
|
||||||
onPointerCancel={() => endVoicePress()}
|
|
||||||
onLostPointerCapture={() => endVoicePress()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.repeat) return
|
|
||||||
if (event.key !== " " && event.key !== "Enter") return
|
|
||||||
event.preventDefault()
|
|
||||||
beginVoicePress(event)
|
|
||||||
}}
|
|
||||||
onKeyUp={(event) => {
|
|
||||||
if (event.key !== " " && event.key !== "Enter") return
|
|
||||||
event.preventDefault()
|
|
||||||
endVoicePress()
|
|
||||||
}}
|
|
||||||
onBlur={() => endVoicePress()}
|
|
||||||
disabled={!voiceInput.isRecording() && (props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput())}
|
|
||||||
aria-label={voiceInput.buttonTitle()}
|
|
||||||
title={voiceInput.buttonTitle()}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={voiceInput.isRecording()}
|
|
||||||
fallback={
|
|
||||||
<Show when={voiceInput.isTranscribing()} fallback={<Mic class="h-4 w-4" aria-hidden="true" />}>
|
|
||||||
<Loader2 class="h-4 w-4 animate-spin" aria-hidden="true" />
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Mic class="h-4 w-4" aria-hidden="true" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={showConversationToggle()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`prompt-voice-button prompt-nav-voice-button prompt-conversation-button ${conversationModeEnabled() ? "is-active" : ""}`}
|
|
||||||
onClick={() => toggleConversationMode(props.instanceId)}
|
|
||||||
disabled={!conversationModeEnabled() && !canToggleConversationMode()}
|
|
||||||
aria-pressed={conversationModeEnabled()}
|
|
||||||
aria-label={conversationModeButtonTitle()}
|
|
||||||
title={conversationModeButtonTitle()}
|
|
||||||
>
|
|
||||||
<Volume2 class="h-4 w-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-clear-button"
|
|
||||||
onClick={handleClearPrompt}
|
|
||||||
disabled={!canClearPrompt()}
|
|
||||||
aria-label={t("promptInput.clear.ariaLabel")}
|
|
||||||
title={t("promptInput.clear.title")}
|
|
||||||
>
|
|
||||||
<X class="h-4 w-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="prompt-nav-column prompt-nav-column-right">
|
|
||||||
<ExpandButton
|
|
||||||
expandState={expandState}
|
|
||||||
onToggleExpand={handleExpandToggle}
|
|
||||||
/>
|
|
||||||
<Show when={hasHistory()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-history-button"
|
|
||||||
onClick={() =>
|
|
||||||
selectPreviousHistory({
|
|
||||||
force: true,
|
|
||||||
isPickerOpen: showPicker(),
|
|
||||||
getTextarea: () => textareaRef,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!canHistoryGoPrevious()}
|
|
||||||
aria-label={t("promptInput.history.previousAriaLabel")}
|
|
||||||
>
|
|
||||||
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-history-button"
|
|
||||||
onClick={() =>
|
|
||||||
selectNextHistory({
|
|
||||||
force: true,
|
|
||||||
isPickerOpen: showPicker(),
|
|
||||||
getTextarea: () => textareaRef,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!canHistoryGoNext()}
|
|
||||||
aria-label={t("promptInput.history.nextAriaLabel")}
|
|
||||||
>
|
|
||||||
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={shouldShowOverlay()}>
|
<Show when={shouldShowOverlay()}>
|
||||||
<div class={`prompt-input-overlay keyboard-hints ${mode() === "shell" ? "shell-mode" : ""}`}>
|
<div class={`prompt-input-overlay keyboard-hints ${mode() === "shell" ? "shell-mode" : ""}`}>
|
||||||
<Show
|
<Show
|
||||||
@@ -742,6 +635,116 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prompt-input-actions">
|
<div class="prompt-input-actions">
|
||||||
|
<div class="prompt-nav-buttons">
|
||||||
|
<div class="prompt-nav-column prompt-nav-column-left">
|
||||||
|
<Show when={showVoiceInput()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`prompt-voice-button prompt-nav-voice-button ${voiceInput.isRecording() ? "is-recording" : ""}`}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
beginVoicePress(event)
|
||||||
|
}}
|
||||||
|
onPointerUp={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
endVoicePress()
|
||||||
|
}}
|
||||||
|
onPointerCancel={() => endVoicePress()}
|
||||||
|
onLostPointerCapture={() => endVoicePress()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.repeat) return
|
||||||
|
if (event.key !== " " && event.key !== "Enter") return
|
||||||
|
event.preventDefault()
|
||||||
|
beginVoicePress(event)
|
||||||
|
}}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
if (event.key !== " " && event.key !== "Enter") return
|
||||||
|
event.preventDefault()
|
||||||
|
endVoicePress()
|
||||||
|
}}
|
||||||
|
onBlur={() => endVoicePress()}
|
||||||
|
disabled={!voiceInput.isRecording() && (props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput())}
|
||||||
|
aria-label={voiceInput.buttonTitle()}
|
||||||
|
title={voiceInput.buttonTitle()}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={voiceInput.isRecording()}
|
||||||
|
fallback={
|
||||||
|
<Show when={voiceInput.isTranscribing()} fallback={<Mic class="h-4 w-4" aria-hidden="true" />}>
|
||||||
|
<Loader2 class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Mic class="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={showConversationToggle()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`prompt-voice-button prompt-nav-voice-button prompt-conversation-button ${conversationModeEnabled() ? "is-active" : ""}`}
|
||||||
|
onClick={() => toggleConversationMode(props.instanceId)}
|
||||||
|
disabled={!conversationModeEnabled() && !canToggleConversationMode()}
|
||||||
|
aria-pressed={conversationModeEnabled()}
|
||||||
|
aria-label={conversationModeButtonTitle()}
|
||||||
|
title={conversationModeButtonTitle()}
|
||||||
|
>
|
||||||
|
<Volume2 class="h-4 w-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="prompt-clear-button"
|
||||||
|
onClick={handleClearPrompt}
|
||||||
|
disabled={!canClearPrompt()}
|
||||||
|
aria-label={t("promptInput.clear.ariaLabel")}
|
||||||
|
title={t("promptInput.clear.title")}
|
||||||
|
>
|
||||||
|
<X class="h-4 w-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-nav-column prompt-nav-column-right">
|
||||||
|
<ExpandButton
|
||||||
|
expandState={expandState}
|
||||||
|
onToggleExpand={handleExpandToggle}
|
||||||
|
/>
|
||||||
|
<Show when={hasHistory()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="prompt-history-button"
|
||||||
|
onClick={() =>
|
||||||
|
selectPreviousHistory({
|
||||||
|
force: true,
|
||||||
|
isPickerOpen: showPicker(),
|
||||||
|
getTextarea: () => textareaRef,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!canHistoryGoPrevious()}
|
||||||
|
aria-label={t("promptInput.history.previousAriaLabel")}
|
||||||
|
>
|
||||||
|
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="prompt-history-button"
|
||||||
|
onClick={() =>
|
||||||
|
selectNextHistory({
|
||||||
|
force: true,
|
||||||
|
isPickerOpen: showPicker(),
|
||||||
|
getTextarea: () => textareaRef,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!canHistoryGoNext()}
|
||||||
|
aria-label={t("promptInput.history.nextAriaLabel")}
|
||||||
|
>
|
||||||
|
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prompt-input-primary-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="stop-button"
|
class="stop-button"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
.prompt-input-wrapper {
|
.prompt-input-wrapper {
|
||||||
@apply grid items-stretch;
|
@apply grid items-stretch;
|
||||||
grid-template-columns: minmax(0, 1fr) 64px;
|
grid-template-columns: minmax(0, 1fr) 72px 64px;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,16 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-input-primary-actions {
|
||||||
|
@apply flex flex-col items-center;
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
border-inline-start: 1px solid var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-input-field-container {
|
.prompt-input-field-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -37,7 +47,7 @@
|
|||||||
.prompt-input {
|
.prompt-input {
|
||||||
@apply w-full pt-2.5 border text-sm resize-none outline-none transition-colors;
|
@apply w-full pt-2.5 border text-sm resize-none outline-none transition-colors;
|
||||||
padding-inline-start: 0.75rem;
|
padding-inline-start: 0.75rem;
|
||||||
padding-inline-end: 7.5rem;
|
padding-inline-end: 0.75rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -85,16 +95,12 @@
|
|||||||
|
|
||||||
/* Navigation buttons container (expand, prev, next). */
|
/* Navigation buttons container (expand, prev, next). */
|
||||||
.prompt-nav-buttons {
|
.prompt-nav-buttons {
|
||||||
position: absolute;
|
|
||||||
top: 0.25rem;
|
|
||||||
inset-inline-end: 0.25rem;
|
|
||||||
bottom: 0.25rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
z-index: 2;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-nav-column {
|
.prompt-nav-column {
|
||||||
@@ -287,7 +293,6 @@
|
|||||||
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
||||||
background-color: var(--accent-primary);
|
background-color: var(--accent-primary);
|
||||||
color: var(--text-inverted);
|
color: var(--text-inverted);
|
||||||
margin-top: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-button.shell-mode {
|
.send-button.shell-mode {
|
||||||
@@ -421,7 +426,7 @@
|
|||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.prompt-input-wrapper {
|
.prompt-input-wrapper {
|
||||||
grid-template-columns: minmax(0, 1fr) 40px;
|
grid-template-columns: minmax(0, 1fr) 64px 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +434,6 @@
|
|||||||
.prompt-input {
|
.prompt-input {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
padding-inline-end: 7.5rem;
|
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user