Compare commits

...

3 Commits

Author SHA1 Message Date
Shoubhit Dash 01d80f37dd refactor(app): use button shortcut in shell tray 2026-04-07 19:34:13 +05:30
Shoubhit Dash f714300e9a refactor(ui): add button shortcut component 2026-04-07 19:34:05 +05:30
Shoubhit Dash 30c4ccbfee style(app): update shell mode tray 2026-04-07 18:10:35 +05:30
5 changed files with 202 additions and 31 deletions

View File

@ -19,6 +19,7 @@ import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button"
import { ButtonShortcut } from "@opencode-ai/ui/button-shortcut"
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
@ -1450,42 +1451,41 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show when={store.mode === "normal" || store.mode === "shell"}>
<DockTray attach="top">
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
<div class="flex h-7 items-center gap-1.5 min-w-0 flex-1 relative">
<div
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
class="flex items-center max-w-[160px] min-w-0 absolute inset-y-0 left-0"
style={{
padding: "0 4px 0 8px",
padding: "0 8px",
...shell(),
}}
>
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" />
<span class="truncate text-13-regular text-text-strong">{language.t("prompt.mode.shell")}</span>
</div>
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<div data-component="prompt-agent-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
<Select
size="normal"
options={agentNames()}
current={local.agent.current()?.name ?? ""}
onSelect={(value) => {
local.agent.set(value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<Show when={store.mode !== "shell"}>
<Show when={store.mode !== "shell"}>
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<div data-component="prompt-agent-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
<Select
size="normal"
options={agentNames()}
current={local.agent.current()?.name ?? ""}
onSelect={(value) => {
local.agent.set(value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<div data-component="prompt-model-control">
<Show
when={providers.paid().length > 0}
@ -1581,7 +1581,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
</TooltipKeybind>
</div>
</Show>
</div>
</Show>
<div class="absolute inset-y-0 right-0 flex items-center" style={shell()}>
<ButtonShortcut
type="button"
variant="ghost"
size="small"
shortcut="Esc"
shortcutAria="Escape"
class="h-6 gap-2 rounded-[6px] border-none px-0 py-0 pl-3 pr-0.75 text-13-medium text-text-base shadow-none"
tabIndex={store.mode === "shell" ? undefined : -1}
onClick={() => setMode("normal")}
aria-label={language.t("common.cancel")}
>
{language.t("common.cancel")}
</ButtonShortcut>
</div>
</div>
</div>

View File

@ -0,0 +1,36 @@
[data-component="button"][data-button-shortcut] {
[data-slot="button-shortcut-label"] {
min-width: 0;
}
[data-slot="button-shortcut-key"] {
display: flex;
align-items: center;
flex-shrink: 0;
}
[data-slot="button-shortcut-key"] [data-component="keybind"] {
box-shadow: none;
background: var(--surface-raised-base);
color: var(--text-weak);
font-family: var(--font-family-sans);
font-weight: var(--font-weight-regular);
}
&[data-size="small"] [data-slot="button-shortcut-key"] [data-component="keybind"],
&[data-size="normal"] [data-slot="button-shortcut-key"] [data-component="keybind"] {
height: 18px;
padding: 0 4px;
border-radius: 3px;
font-size: var(--font-size-small);
line-height: 18px;
}
&[data-size="large"] [data-slot="button-shortcut-key"] [data-component="keybind"] {
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: var(--font-size-small);
line-height: 20px;
}
}

View File

@ -0,0 +1,86 @@
// @ts-nocheck
import { ButtonShortcut } from "./button-shortcut"
const docs = `### Overview
Button with a trailing shortcut keycap.
Use this when the action label and shortcut should be taught together at the control level.
### API
- Inherits Button props.
- \`shortcut\`: visible keycap text.
- \`shortcutAria\`: semantic shortcut string for \`aria-keyshortcuts\`.
- \`shortcutClass\`: optional class override for the keycap.
### Variants and states
- Uses the same \`variant\` and \`size\` options as \`Button\`.
- Supports disabled state.
### Accessibility
- Keep the visible shortcut concise.
- Use \`shortcutAria\` for the canonical key sequence when it differs from the visible label.
### Theming/tokens
- Extends \`Button\` and composes \`Keybind\`.
`
export default {
title: "UI/ButtonShortcut",
id: "components-button-shortcut",
component: ButtonShortcut,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: docs,
},
},
},
args: {
children: "Cancel",
shortcut: "Esc",
shortcutAria: "Escape",
variant: "ghost",
size: "small",
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "ghost"],
},
size: {
control: "select",
options: ["small", "normal", "large"],
},
},
}
export const Basic = {}
export const Sizes = {
render: () => (
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
<ButtonShortcut size="small" variant="ghost" shortcut="Esc" shortcutAria="Escape">
Cancel
</ButtonShortcut>
<ButtonShortcut size="normal" variant="secondary" shortcut="Tab" shortcutAria="Tab">
Focus
</ButtonShortcut>
<ButtonShortcut size="large" variant="primary" shortcut="Enter" shortcutAria="Enter">
Submit
</ButtonShortcut>
</div>
),
}
export const Shell = {
args: {
children: "Cancel",
shortcut: "Esc",
shortcutAria: "Escape",
variant: "ghost",
size: "small",
class: "h-6 gap-2 rounded-[6px] border-none px-0 py-0 pl-3 pr-0.75 text-13-medium text-text-base shadow-none",
},
}

View File

@ -0,0 +1,33 @@
import { type ComponentProps, Show, splitProps } from "solid-js"
import { Button, type ButtonProps } from "./button"
import { Keybind } from "./keybind"
export interface ButtonShortcutProps extends ButtonProps {
shortcut?: string
shortcutAria?: string
shortcutClass?: string
shortcutClassList?: ComponentProps<"span">["classList"]
}
export function ButtonShortcut(props: ButtonShortcutProps) {
const [split, rest] = splitProps(props, [
"children",
"shortcut",
"shortcutAria",
"shortcutClass",
"shortcutClassList",
])
return (
<Button {...rest} aria-keyshortcuts={split.shortcutAria} data-button-shortcut={split.shortcut ? "true" : undefined}>
<span data-slot="button-shortcut-label">{split.children}</span>
<Show when={split.shortcut}>
<span data-slot="button-shortcut-key">
<Keybind class={split.shortcutClass} classList={split.shortcutClassList}>
{split.shortcut}
</Keybind>
</span>
</Show>
</Button>
)
}

View File

@ -12,6 +12,7 @@
@import "../components/avatar.css" layer(components);
@import "../components/basic-tool.css" layer(components);
@import "../components/button.css" layer(components);
@import "../components/button-shortcut.css" layer(components);
@import "../components/card.css" layer(components);
@import "../components/tool-error-card.css" layer(components);
@import "../components/checkbox.css" layer(components);