wip: polish to shortcuts

This commit is contained in:
David Hill 2025-12-22 15:06:00 +00:00 committed by Adam
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 { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command" import { formatKeybind, useCommand } from "@/context/command"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id" import { Identifier } from "@/utils/id"
import { SessionContextUsage } from "@/components/session-context-usage" import { SessionContextUsage } from "@/components/session-context-usage"
@ -1400,8 +1400,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
custom custom
</span> </span>
</Show> </Show>
<Show when={command.keybind(cmd.id)}> <Show when={cmd.keybind}>
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span> <span class="text-12-regular text-text-subtle">{formatKeybind(cmd.keybind!)}</span>
</Show> </Show>
</div> </div>
</button> </button>

View file

@ -3,7 +3,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip" 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) 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)}> <For each={getKeyChars(props.shortcut.keybind)}>
{(char) => { {(char) => {
const tooltip = SPECIAL_CHAR_NAMES[char] const tooltip = SPECIAL_CHAR_NAMES[char]
const isSpecial = tooltip && !isLetter(char)
const isShift = char === "⇧"
return ( 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"> <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> </Tooltip>
</Show> </Show>
) )
@ -197,7 +201,16 @@ export function ShortcutsPanel(props: { onClose: () => void }) {
{(category) => <Tabs.Trigger value={category.name}>{category.name}</Tabs.Trigger>} {(category) => <Tabs.Trigger value={category.name}>{category.name}</Tabs.Trigger>}
</For> </For>
</Tabs.List> </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> </div>
<For each={SHORTCUT_CATEGORIES}> <For each={SHORTCUT_CATEGORIES}>
{(category) => ( {(category) => (

View file

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

View file

@ -97,7 +97,7 @@
.shortcuts-panel [data-slot="tabs-trigger-wrapper"] { .shortcuts-panel [data-slot="tabs-trigger-wrapper"] {
background: transparent; background: transparent;
border: 0.5px solid transparent; border: none;
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
border-radius: 6px; border-radius: 6px;
color: var(--color-text-weak); color: var(--color-text-weak);
@ -108,8 +108,7 @@
} }
.shortcuts-panel [data-slot="tabs-trigger-wrapper"]:has([data-selected]) { .shortcuts-panel [data-slot="tabs-trigger-wrapper"]:has([data-selected]) {
border: 1px solid var(--color-border-weak-base); background: var(--color-surface-raised-base-active);
background: var(--color-surface-raised-base);
color: var(--color-text-strong); color: var(--color-text-strong);
} }
@ -202,7 +201,20 @@
white-space: nowrap; 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 { main.shortcuts-open {
padding-bottom: 280px; 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 { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server" 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 { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { navStart } from "@/utils/perf" import { navStart } from "@/utils/perf"
import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectDirectory } from "@/components/dialog-select-directory"
@ -81,7 +81,6 @@ export default function Layout(props: ParentProps) {
workspaceExpanded: {} as Record<string, boolean>, workspaceExpanded: {} as Record<string, boolean>,
}), }),
) )
const [shortcutsOpen, setShortcutsOpen] = createSignal(false)
const pageReady = createMemo(() => ready()) const pageReady = createMemo(() => ready())
@ -757,7 +756,7 @@ export default function Layout(props: ParentProps) {
title: "Toggle shortcuts panel", title: "Toggle shortcuts panel",
category: "View", category: "View",
keybind: "ctrl+/", keybind: "ctrl+/",
onSelect: () => setShortcutsOpen(!shortcutsOpen()), onSelect: () => layout.shortcuts.toggle(),
}, },
{ {
id: "project.open", id: "project.open",
@ -1897,7 +1896,7 @@ export default function Layout(props: ParentProps) {
const homedir = createMemo(() => sync.data.path.home) const homedir = createMemo(() => sync.data.path.home)
return ( 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="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden">
<div class="flex-1 min-h-0 w-full"> <div class="flex-1 min-h-0 w-full">
<DragDropProvider <DragDropProvider
@ -1937,7 +1936,7 @@ export default function Layout(props: ParentProps) {
<Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Settings" class="hidden"> <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Settings" class="hidden">
<IconButton disabled icon="settings-gear" variant="ghost" size="large" /> <IconButton disabled icon="settings-gear" variant="ghost" size="large" />
</Tooltip> </Tooltip>
<DropdownMenu> <DropdownMenu placement={layout.shortcuts.opened() ? "top-start" : "bottom-start"}>
<Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help"> <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help">
<DropdownMenu.Trigger as={IconButton} icon="question-mark" variant="ghost" size="large" /> <DropdownMenu.Trigger as={IconButton} icon="question-mark" variant="ghost" size="large" />
</Tooltip> </Tooltip>
@ -1946,7 +1945,9 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Item onSelect={() => platform.openLink("https://opencode.ai/desktop-feedback")}> <DropdownMenu.Item onSelect={() => platform.openLink("https://opencode.ai/desktop-feedback")}>
Submit feedback Submit feedback
</DropdownMenu.Item> </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.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu> </DropdownMenu>
@ -2160,14 +2161,14 @@ export default function Layout(props: ParentProps) {
classList={{ classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true, "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(), "xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
"shortcuts-open": shortcutsOpen(), "shortcuts-open": layout.shortcuts.opened(),
}} }}
> >
{props.children} {props.children}
</main> </main>
</div> </div>
<Show when={shortcutsOpen()}> <Show when={layout.shortcuts.opened()}>
<ShortcutsPanel onClose={() => setShortcutsOpen(false)} /> <ShortcutsPanel onClose={() => layout.shortcuts.close()} />
</Show> </Show>
<Toast.Region /> <Toast.Region />
<ReleaseNotesHandler /> <ReleaseNotesHandler />

View file

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