feat(app): align slash popover to v2 tokens (#34286)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
Aarav Sareen
2026-06-29 18:44:18 +08:00
committed by GitHub
co-authored by Brendan Allan Brendan Allan
parent 0a5e617da8
commit 7fac84319d
3 changed files with 175 additions and 49 deletions
+7 -3
View File
@@ -791,8 +791,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
} }
// Auto-scroll active command into view when navigating with keyboard const scrollSlashActiveIntoView = () => {
createEffect(() => {
const activeId = slashActive() const activeId = slashActive()
if (!activeId || !slashPopoverRef) return if (!activeId || !slashPopoverRef) return
@@ -800,7 +799,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`) const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
}) })
}) }
const selectPopoverActive = () => { const selectPopoverActive = () => {
if (store.popover === "at") { if (store.popover === "at") {
const items = atFlat() const items = atFlat()
@@ -1287,6 +1286,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
if (store.popover === "slash") { if (store.popover === "slash") {
slashOnKeyDown(event) slashOnKeyDown(event)
if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) {
scrollSlashActiveIntoView()
}
} }
event.preventDefault() event.preventDefault()
return return
@@ -1406,6 +1408,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setSlashActive={setSlashActive} setSlashActive={setSlashActive}
onSlashSelect={handleSlashSelect} onSlashSelect={handleSlashSelect}
commandKeybind={command.keybind} commandKeybind={command.keybind}
commandKeybindParts={command.keybindParts}
newLayoutDesigns={props.controls.newLayoutDesigns}
t={(key) => language.t(key as Parameters<typeof language.t>[0])} t={(key) => language.t(key as Parameters<typeof language.t>[0])}
/> />
<Switch> <Switch>
@@ -1,6 +1,8 @@
import { Component, For, Match, Show, Switch } from "solid-js" import { Component, For, Match, Show, Switch } from "solid-js"
import { FileIcon } from "@opencode-ai/ui/file-icon" import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
export type AtOption = export type AtOption =
@@ -30,6 +32,8 @@ type PromptPopoverProps = {
setSlashActive: (id: string) => void setSlashActive: (id: string) => void
onSlashSelect: (item: SlashCommand) => void onSlashSelect: (item: SlashCommand) => void
commandKeybind: (id: string) => string | undefined commandKeybind: (id: string) => string | undefined
commandKeybindParts: (id: string) => string[]
newLayoutDesigns: boolean
t: (key: string) => string t: (key: string) => string
} }
@@ -41,15 +45,30 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
if (props.popover === "slash") props.setSlashPopoverRef(el) if (props.popover === "slash") props.setSlashPopoverRef(el)
}} }}
class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10 class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10
overflow-auto no-scrollbar flex flex-col p-2 rounded-[12px] overflow-auto no-scrollbar flex flex-col p-2"
bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]" classList={{
"z-[70] rounded-[10px] bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]":
props.newLayoutDesigns,
"rounded-[12px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]":
!props.newLayoutDesigns,
}}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
> >
<Switch> <Switch>
<Match when={props.popover === "at"}> <Match when={props.popover === "at"}>
<Show <Show
when={props.atFlat.length > 0} when={props.atFlat.length > 0}
fallback={<div class="text-text-weak px-2 py-1">{props.t("prompt.popover.emptyResults")}</div>} fallback={
<div
class="px-2 py-1"
classList={{
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{props.t("prompt.popover.emptyResults")}
</div>
}
> >
<For each={props.atFlat.slice(0, 10)}> <For each={props.atFlat.slice(0, 10)}>
{(item) => { {(item) => {
@@ -58,13 +77,29 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
if (item.type === "agent") { if (item.type === "agent") {
return ( return (
<button <button
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5" class="w-full flex items-center gap-x-2 px-2 py-0.5"
classList={{ "bg-surface-raised-base-hover": props.atActive === key }} classList={{
"rounded-[4px]": props.newLayoutDesigns,
"rounded-md": !props.newLayoutDesigns,
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
}}
onClick={() => props.onAtSelect(item)} onClick={() => props.onAtSelect(item)}
onMouseEnter={() => props.setAtActive(key)} onPointerMove={() => props.setAtActive(key)}
> >
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" /> <Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
<span class="text-14-regular text-text-strong whitespace-nowrap">@{item.name}</span> <span
class="whitespace-nowrap"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-v2-text-text-base": props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
"text-text-strong": !props.newLayoutDesigns,
}}
>
@{item.name}
</span>
</button> </button>
) )
} }
@@ -75,16 +110,44 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
return ( return (
<button <button
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5" class="w-full flex items-center gap-x-2 px-2 py-0.5"
classList={{ "bg-surface-raised-base-hover": props.atActive === key }} classList={{
"rounded-[4px]": props.newLayoutDesigns,
"rounded-md": !props.newLayoutDesigns,
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
}}
onClick={() => props.onAtSelect(item)} onClick={() => props.onAtSelect(item)}
onMouseEnter={() => props.setAtActive(key)} onPointerMove={() => props.setAtActive(key)}
> >
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" /> <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular min-w-0"> <div
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{directory}</span> class="flex items-center min-w-0"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
}}
>
<span
class="whitespace-nowrap truncate min-w-0"
classList={{
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{directory}
</span>
<Show when={!isDirectory}> <Show when={!isDirectory}>
<span class="text-text-strong whitespace-nowrap">{filename}</span> <span
class="whitespace-nowrap"
classList={{
"text-v2-text-text-base": props.newLayoutDesigns,
"text-text-strong": !props.newLayoutDesigns,
}}
>
{filename}
</span>
</Show> </Show>
</div> </div>
</button> </button>
@@ -96,41 +159,100 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
<Match when={props.popover === "slash"}> <Match when={props.popover === "slash"}>
<Show <Show
when={props.slashFlat.length > 0} when={props.slashFlat.length > 0}
fallback={<div class="text-text-weak px-2 py-1">{props.t("prompt.popover.emptyCommands")}</div>} fallback={
<div
class="px-2 py-1"
classList={{
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{props.t("prompt.popover.emptyCommands")}
</div>
}
> >
<For each={props.slashFlat}> <For each={props.slashFlat}>
{(cmd) => ( {(cmd) => {
const keybind = () => props.commandKeybind(cmd.id)
const keybindParts = () => props.commandKeybindParts(cmd.id)
return (
<button <button
data-slash-id={cmd.id} data-slash-id={cmd.id}
classList={{ classList={{
"w-full flex items-center justify-between gap-4 rounded-md px-2 py-1": true, "w-full flex items-center justify-between gap-4 px-2 py-1": true,
"bg-surface-raised-base-hover": props.slashActive === cmd.id, "rounded-[4px] scroll-my-2": props.newLayoutDesigns,
"rounded-md": !props.newLayoutDesigns,
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.slashActive === cmd.id,
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.slashActive === cmd.id,
}} }}
onClick={() => props.onSlashSelect(cmd)} onClick={() => props.onSlashSelect(cmd)}
onMouseEnter={() => props.setSlashActive(cmd.id)} onPointerMove={() => props.setSlashActive(cmd.id)}
> >
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">/{cmd.trigger}</span> <span
class="whitespace-nowrap"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-v2-text-text-base": props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
"text-text-strong": !props.newLayoutDesigns,
}}
>
/{cmd.trigger}
</span>
<Show when={cmd.description}> <Show when={cmd.description}>
<span class="text-14-regular text-text-weak truncate">{cmd.description}</span> <span
class="truncate"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{cmd.description}
</span>
</Show> </Show>
</div> </div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<Show when={cmd.type === "custom" && cmd.source !== "command"}> <Show when={cmd.type === "custom" && cmd.source !== "command"}>
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded"> <Show
when={props.newLayoutDesigns}
fallback={
<span class="text-11-regular px-1.5 py-0.5 rounded bg-surface-base text-text-subtle">
{cmd.source === "skill" {cmd.source === "skill"
? props.t("prompt.slash.badge.skill") ? props.t("prompt.slash.badge.skill")
: cmd.source === "mcp" : cmd.source === "mcp"
? props.t("prompt.slash.badge.mcp") ? props.t("prompt.slash.badge.mcp")
: props.t("prompt.slash.badge.custom")} : props.t("prompt.slash.badge.custom")}
</span> </span>
}
>
<Tag>
{cmd.source === "skill"
? props.t("prompt.slash.badge.skill")
: cmd.source === "mcp"
? props.t("prompt.slash.badge.mcp")
: props.t("prompt.slash.badge.custom")}
</Tag>
</Show>
</Show>
<Show
when={props.newLayoutDesigns ? keybindParts().length > 0 : keybind()}
>
<Show
when={props.newLayoutDesigns}
fallback={<span class="text-12-regular text-text-subtle">{keybind()}</span>}
>
<KeybindV2 keys={keybindParts()} variant="neutral" />
</Show> </Show>
<Show when={props.commandKeybind(cmd.id)}>
<span class="text-12-regular text-text-subtle">{props.commandKeybind(cmd.id)}</span>
</Show> </Show>
</div> </div>
</button> </button>
)} )
}}
</For> </For>
</Show> </Show>
</Match> </Match>
@@ -125,7 +125,7 @@ export function SessionComposerRegion(props: {
</Show> </Show>
<div <div
classList={{ classList={{
"relative z-30": true, "relative z-[70]": true,
}} }}
style={{ style={{
"margin-top": `${-controller.lift()}px`, "margin-top": `${-controller.lift()}px`,