wip: polish to shortcuts

desktop-shortcuts-panel
David Hill 2025-12-22 15:06:00 +00:00 committed by Adam
parent 0f398e612f
commit 3ff6ce5967
No known key found for this signature in database
GPG Key ID: 9CB48779AF150E75
6 changed files with 64 additions and 22 deletions

View File

@ -44,7 +44,7 @@ import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command"
import { formatKeybind, useCommand } from "@/context/command"
import { Persist, persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id"
import { SessionContextUsage } from "@/components/session-context-usage"
@ -1400,8 +1400,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
custom
</span>
</Show>
<Show when={command.keybind(cmd.id)}>
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
<Show when={cmd.keybind}>
<span class="text-12-regular text-text-subtle">{formatKeybind(cmd.keybind!)}</span>
</Show>
</div>
</button>

View File

@ -3,7 +3,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { parseKeybind } from "@/context/command"
import { parseKeybind, formatKeybind } from "@/context/command"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
@ -163,10 +163,14 @@ function ShortcutItem(props: { shortcut: Shortcut }) {
<For each={getKeyChars(props.shortcut.keybind)}>
{(char) => {
const tooltip = SPECIAL_CHAR_NAMES[char]
const isSpecial = tooltip && !isLetter(char)
const isShift = char === "⇧"
return (
<Show when={tooltip && !isLetter(char)} fallback={<kbd class="shortcut-key">{char}</kbd>}>
<Show when={isSpecial} fallback={<kbd class="shortcut-key">{char}</kbd>}>
<Tooltip value={tooltip} placement="top">
<kbd class="shortcut-key">{char}</kbd>
<kbd class="shortcut-key shortcut-key-special">
<span classList={{ "shortcut-key-shift": isShift }}>{char}</span>
</kbd>
</Tooltip>
</Show>
)
@ -197,7 +201,16 @@ export function ShortcutsPanel(props: { onClose: () => void }) {
{(category) => <Tabs.Trigger value={category.name}>{category.name}</Tabs.Trigger>}
</For>
</Tabs.List>
<IconButton icon="close" variant="ghost" onClick={props.onClose} />
<Tooltip
placement="top"
value={
<span>
Close shortcuts <span class="text-text-weak">{formatKeybind("ctrl+/")}</span>
</span>
}
>
<IconButton icon="close" variant="ghost" onClick={props.onClose} />
</Tooltip>
</div>
<For each={SHORTCUT_CATEGORIES}>
{(category) => (

View File

@ -1,5 +1,5 @@
import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { batch, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
@ -93,6 +93,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}),
)
const [shortcutsOpened, setShortcutsOpened] = createSignal(false)
const MAX_SESSION_KEYS = 50
const meta = { active: undefined as string | undefined, pruned: false }
const used = new Map<string, number>()
@ -353,6 +355,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("review", "diffStyle", diffStyle)
},
},
shortcuts: {
opened: shortcutsOpened,
open() {
setShortcutsOpened(true)
},
close() {
setShortcutsOpened(false)
},
toggle() {
setShortcutsOpened((x) => !x)
},
},
session: {
width: createMemo(() => store.session?.width ?? 600),
resize(width: number) {

View File

@ -97,7 +97,7 @@
.shortcuts-panel [data-slot="tabs-trigger-wrapper"] {
background: transparent;
border: 0.5px solid transparent;
border: none;
font-weight: var(--font-weight-regular);
border-radius: 6px;
color: var(--color-text-weak);
@ -108,8 +108,7 @@
}
.shortcuts-panel [data-slot="tabs-trigger-wrapper"]:has([data-selected]) {
border: 1px solid var(--color-border-weak-base);
background: var(--color-surface-raised-base);
background: var(--color-surface-raised-base-active);
color: var(--color-text-strong);
}
@ -202,7 +201,20 @@
white-space: nowrap;
}
/* Adjust main content when shortcuts panel is open */
.shortcut-key-special {
font-size: 18px;
}
.shortcut-key-shift {
position: relative;
top: -2px;
}
/* Adjust main content and sidebar when shortcuts panel is open */
main.shortcuts-open {
padding-bottom: 280px;
}
.sidebar-shortcuts-open {
padding-bottom: 280px;
}

View File

@ -59,7 +59,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useCommand, type CommandOption } from "@/context/command"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { navStart } from "@/utils/perf"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
@ -81,7 +81,6 @@ export default function Layout(props: ParentProps) {
workspaceExpanded: {} as Record<string, boolean>,
}),
)
const [shortcutsOpen, setShortcutsOpen] = createSignal(false)
const pageReady = createMemo(() => ready())
@ -757,7 +756,7 @@ export default function Layout(props: ParentProps) {
title: "Toggle shortcuts panel",
category: "View",
keybind: "ctrl+/",
onSelect: () => setShortcutsOpen(!shortcutsOpen()),
onSelect: () => layout.shortcuts.toggle(),
},
{
id: "project.open",
@ -1897,7 +1896,7 @@ export default function Layout(props: ParentProps) {
const homedir = createMemo(() => sync.data.path.home)
return (
<div class="flex h-full w-full overflow-hidden">
<div class="flex h-full w-full overflow-hidden" classList={{ "sidebar-shortcuts-open": layout.shortcuts.opened() }}>
<div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden">
<div class="flex-1 min-h-0 w-full">
<DragDropProvider
@ -1937,7 +1936,7 @@ export default function Layout(props: ParentProps) {
<Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Settings" class="hidden">
<IconButton disabled icon="settings-gear" variant="ghost" size="large" />
</Tooltip>
<DropdownMenu>
<DropdownMenu placement={layout.shortcuts.opened() ? "top-start" : "bottom-start"}>
<Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help">
<DropdownMenu.Trigger as={IconButton} icon="question-mark" variant="ghost" size="large" />
</Tooltip>
@ -1946,7 +1945,9 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Item onSelect={() => platform.openLink("https://opencode.ai/desktop-feedback")}>
Submit feedback
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => setShortcutsOpen(true)}>Keyboard shortcuts</DropdownMenu.Item>
<DropdownMenu.Item class="flex justify-between gap-6" onSelect={() => layout.shortcuts.open()}>
Keyboard shortcuts <span class="text-text-weaker">{formatKeybind("ctrl+/")}</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
@ -2160,14 +2161,14 @@ export default function Layout(props: ParentProps) {
classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true,
"xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
"shortcuts-open": shortcutsOpen(),
"shortcuts-open": layout.shortcuts.opened(),
}}
>
{props.children}
</main>
</div>
<Show when={shortcutsOpen()}>
<ShortcutsPanel onClose={() => setShortcutsOpen(false)} />
<Show when={layout.shortcuts.opened()}>
<ShortcutsPanel onClose={() => layout.shortcuts.close()} />
</Show>
<Toast.Region />
<ReleaseNotesHandler />

View File

@ -15,6 +15,7 @@ export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
children?: (item: T | undefined) => JSX.Element
placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end"
}
export function Select<T>(props: SelectProps<T> & ButtonProps) {
@ -29,6 +30,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
"groupBy",
"onSelect",
"children",
"placement",
])
const grouped = createMemo(() => {
const result = pipe(
@ -46,7 +48,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
<Kobalte<T, { category: string; options: T[] }>
{...others}
data-component="select"
placement="bottom-start"
placement={local.placement ?? "bottom-start"}
value={local.current}
options={grouped()}
optionValue={(x) => (local.value ? local.value(x) : (x as string))}