feat(app): visual improvements (#36073)

This commit is contained in:
Aarav Sareen
2026-07-10 08:22:05 +05:30
committed by GitHub
parent 01131c75d8
commit c769380776
14 changed files with 103 additions and 25 deletions
+7 -8
View File
@@ -36,6 +36,8 @@ import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
@@ -1704,22 +1706,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
> >
<MenuV2 gutter={6} modal={false} placement="top-start"> <MenuV2 gutter={6} modal={false} placement="top-start">
<MenuV2.Trigger <MenuV2.Trigger
as={IconButton} as={IconButtonV2}
data-action="prompt-attach" data-action="prompt-attach"
type="button" type="button"
icon="plus" icon={<IconV2 name="plus" />}
variant="ghost" variant="ghost-muted"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted" size="large"
style={buttons()} style={buttons()}
disabled={store.mode !== "normal"} disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1} tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.menu.addImagesAndFiles")} aria-label={language.t("prompt.menu.addImagesAndFiles")}
/> />
<MenuV2.Portal> <MenuV2.Portal>
<MenuV2.Content <MenuV2.Content style={{ "min-width": "180px" }}>
class="[&_[data-slot=menu-v2-item-shortcut]]:w-5 [&_[data-slot=menu-v2-item-shortcut]]:justify-center"
style={{ "min-width": "180px" }}
>
<MenuV2.Item onSelect={pick} shortcut={command.keybind("file.attach")}> <MenuV2.Item onSelect={pick} shortcut={command.keybind("file.attach")}>
{language.t("prompt.menu.imagesAndFiles")} {language.t("prompt.menu.imagesAndFiles")}
</MenuV2.Item> </MenuV2.Item>
@@ -211,7 +211,17 @@ export function SortableTerminalTabV2(props: {
<MenuV2.Context.Trigger class="relative" as="div"> <MenuV2.Context.Trigger class="relative" as="div">
<Tabs.Trigger <Tabs.Trigger
value={props.terminal.id} value={props.terminal.id}
onClick={focus} onMouseDown={(e) => {
// Switch on mousedown to shave the press-release delay off tab switches.
if (e.button !== 0) return
if (store.editing) return
focus()
}}
onClick={(e) => {
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
if (e.detail > 0) return
focus()
}}
closeButton={ closeButton={
<IconButton <IconButton
icon="close-small" icon="close-small"
@@ -20,6 +20,11 @@
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
user-select: none;
}
.settings-v2-panel :is(input, textarea, [contenteditable="true"]) {
user-select: text;
} }
.settings-v2-panel::-webkit-scrollbar { .settings-v2-panel::-webkit-scrollbar {
@@ -181,6 +186,7 @@
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding: 4px 0 4px 4px; padding: 4px 0 4px 4px;
user-select: none;
} }
.settings-v2-nav-footer > span { .settings-v2-nav-footer > span {
+19 -2
View File
@@ -1,7 +1,8 @@
import { withAlpha } from "@opencode-ai/ui/theme/color" import { withAlpha } from "@opencode-ai/ui/theme/color"
import { useTheme } from "@opencode-ai/ui/theme/context" import { useTheme } from "@opencode-ai/ui/theme/context"
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve" import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
import type { HexColor } from "@opencode-ai/ui/theme/types" import { resolveThemeVariantV2 } from "@opencode-ai/ui/theme/v2/resolve"
import type { HexColor, ResolvedV2Theme } from "@opencode-ai/ui/theme/types"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web" import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js" import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
@@ -68,6 +69,19 @@ const debugTerminal = (...values: unknown[]) => {
console.debug("[terminal]", ...values) console.debug("[terminal]", ...values)
} }
const resolveV2Token = (tokens: ResolvedV2Theme, key: string) => {
let current = tokens[key]
for (let i = 0; i < 8 && current; i++) {
const match = /^var\(--([^)]+)\)$/.exec(current.trim())
if (!match) {
const hex = current.trim()
if (/^#[0-9a-fA-F]{8}$/.test(hex)) return hex.slice(0, 7)
return hex
}
current = tokens[match[1]]
}
}
const useTerminalUiBindings = (input: { const useTerminalUiBindings = (input: {
container: HTMLDivElement container: HTMLDivElement
term: Term term: Term
@@ -238,7 +252,10 @@ export const Terminal = (props: TerminalProps) => {
if (!variant?.seeds && !variant?.palette) return fallback if (!variant?.seeds && !variant?.palette) return fallback
const resolved = resolveThemeVariant(variant, mode === "dark") const resolved = resolveThemeVariant(variant, mode === "dark")
const text = resolved["text-stronger"] ?? fallback.foreground const text = resolved["text-stronger"] ?? fallback.foreground
const background = resolved["background-stronger"] ?? fallback.background const background = settings.general.newLayoutDesigns()
? (resolveV2Token(resolveThemeVariantV2(variant, mode === "dark"), "v2-background-bg-base") ??
fallback.background)
: (resolved["background-stronger"] ?? fallback.background)
const alpha = mode === "dark" ? 0.25 : 0.2 const alpha = mode === "dark" ? 0.25 : 0.2
const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor) const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor)
const selectionBackground = withAlpha(base, alpha) const selectionBackground = withAlpha(base, alpha)
+1 -1
View File
@@ -1304,7 +1304,7 @@ export default function Page() {
const reviewPanelV2Rendered = createMemo<boolean>((prev) => prev || !store.deferRender, false) const reviewPanelV2Rendered = createMemo<boolean>((prev) => prev || !store.deferRender, false)
const reviewPanelV2 = () => ( const reviewPanelV2 = () => (
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict"> <div class="flex flex-col h-full overflow-hidden bg-v2-background-bg-base contain-strict">
<Show when={reviewPanelV2Rendered()}> <Show when={reviewPanelV2Rendered()}>
<ReviewPanelV2 {...reviewPanelV2Props()} /> <ReviewPanelV2 {...reviewPanelV2Props()} />
</Show> </Show>
@@ -255,8 +255,10 @@ export function SessionSidePanel(props: {
aria-label={language.t("session.panel.reviewAndFiles")} aria-label={language.t("session.panel.reviewAndFiles")}
aria-hidden={!open()} aria-hidden={!open()}
inert={!open()} inert={!open()}
class="relative min-w-0 flex overflow-hidden bg-background-base" class="relative min-w-0 flex overflow-hidden"
classList={{ classList={{
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
"bg-background-base": !settings.general.newLayoutDesigns(),
"h-full shrink-0": !props.stacked, "h-full shrink-0": !props.stacked,
"h-full min-h-0": props.stacked, "h-full min-h-0": props.stacked,
"pointer-events-none": !open(), "pointer-events-none": !open(),
@@ -275,8 +277,20 @@ export function SessionSidePanel(props: {
}} }}
> >
<Show when={reviewOpen()}> <Show when={reviewOpen()}>
<div class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"> <div
<div class="size-full min-w-0 h-full bg-background-base"> class="relative min-w-0 h-full flex-1 overflow-hidden"
classList={{
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
"bg-background-base": !settings.general.newLayoutDesigns(),
}}
>
<div
class="size-full min-w-0 h-full"
classList={{
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
"bg-background-base": !settings.general.newLayoutDesigns(),
}}
>
<DragDropProvider <DragDropProvider
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
@@ -379,7 +393,13 @@ export function SessionSidePanel(props: {
)} )}
</For> </For>
</SortableProvider> </SortableProvider>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"> <div
class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"
classList={{
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
"bg-background-stronger": !settings.general.newLayoutDesigns(),
}}
>
<TooltipKeybind <TooltipKeybind
title={language.t("command.file.open")} title={language.t("command.file.open")}
keybind={command.keybind("file.open")} keybind={command.keybind("file.open")}
@@ -195,7 +195,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
aria-label={language.t("terminal.title")} aria-label={language.t("terminal.title")}
aria-hidden={!opened()} aria-hidden={!opened()}
inert={!opened()} inert={!opened()}
class="relative shrink-0 overflow-hidden bg-background-stronger" class="relative shrink-0 overflow-hidden bg-v2-background-bg-base"
classList={{ classList={{
"w-full": !isDesktop() || stacked(), "w-full": !isDesktop() || stacked(),
"min-w-0 h-full flex-1": isDesktop() && opened() && !stacked(), "min-w-0 h-full flex-1": isDesktop() && opened() && !stacked(),
@@ -237,7 +237,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
when={terminal.ready()} when={terminal.ready()}
fallback={ fallback={
<div class="flex flex-col h-full pointer-events-none"> <div class="flex flex-col h-full pointer-events-none">
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden"> <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-v2-background-bg-base overflow-hidden">
<For each={handoff()}> <For each={handoff()}>
{(title) => ( {(title) => (
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40"> <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
@@ -1386,7 +1386,7 @@ export function MessageTimeline(props: {
"w-full": true, "w-full": true,
"pb-4": true, "pb-4": true,
"pr-3": true, "pr-3": true,
"pl-2": settings.general.newLayoutDesigns(), "pl-2.5": settings.general.newLayoutDesigns(),
"pl-2 md:pl-4": !settings.general.newLayoutDesigns(), "pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(), "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
}} }}
@@ -4,7 +4,7 @@
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
background: var(--background-stronger, var(--v2-background-bg-base)); background: var(--v2-background-bg-base);
} }
[data-component="session-review-v2"] [data-slot="session-review-v2-body"] { [data-component="session-review-v2"] [data-slot="session-review-v2-body"] {
@@ -31,7 +31,7 @@
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
border-right: 1px solid var(--border-weaker-base, var(--v2-border-border-weak)); border-right: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
background: var(--background-stronger, var(--v2-background-bg-base)); background: var(--v2-background-bg-base);
} }
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"][aria-hidden="true"] { [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"][aria-hidden="true"] {
+19
View File
@@ -679,3 +679,22 @@
[data-component="tabs-drag-preview"] > * { [data-component="tabs-drag-preview"] > * {
position: relative; position: relative;
} }
body[data-new-layout] #review-panel [data-component="tabs"],
body[data-new-layout] #terminal-panel [data-component="tabs"],
body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"],
body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"] {
background-color: var(--v2-background-bg-base);
[data-slot="tabs-list"] {
background-color: var(--v2-background-bg-base);
> .sticky {
background-color: var(--v2-background-bg-base);
&::before {
background: linear-gradient(90deg, transparent, var(--v2-background-bg-base));
}
}
}
}
@@ -112,8 +112,8 @@
color: var(--v2-text-text-base); color: var(--v2-text-text-base);
} }
[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"], [data-expanded]):not( [data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not(
:disabled [data-expanded]
) { ) {
background-color: var(--v2-overlay-simple-overlay-hover); background-color: var(--v2-overlay-simple-overlay-hover);
} }
@@ -122,6 +122,10 @@
background-color: var(--v2-overlay-simple-overlay-pressed); background-color: var(--v2-overlay-simple-overlay-pressed);
} }
[data-component="icon-button-v2"][data-variant="ghost"]:where([data-expanded]):not(:disabled) {
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="icon-button-v2"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) { [data-component="icon-button-v2"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
@@ -133,8 +137,8 @@
color: var(--v2-icon-icon-muted); color: var(--v2-icon-icon-muted);
} }
[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"], [data-expanded]):not( [data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not(
:disabled [data-expanded]
) { ) {
background-color: var(--v2-overlay-simple-overlay-hover); background-color: var(--v2-overlay-simple-overlay-hover);
} }
@@ -6,6 +6,7 @@
[data-component="line-comment-v2"] { [data-component="line-comment-v2"] {
box-sizing: border-box; box-sizing: border-box;
font-family: var(--v2-font-family-sans);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
min-width: 0; min-width: 0;
width: 100%; width: 100%;
@@ -65,6 +65,7 @@
box-shadow: var(--v2-elevation-button-neutral); box-shadow: var(--v2-elevation-button-neutral);
flex: none; flex: none;
align-self: stretch; align-self: stretch;
user-select: none;
transition: transition:
background 85ms ease-out, background 85ms ease-out,
outline-color 85ms ease-out, outline-color 85ms ease-out,
@@ -192,6 +192,7 @@
color: var(--v2-text-text-muted); color: var(--v2-text-text-muted);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
user-select: none;
} }
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger-wrapper"] { [data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger-wrapper"] {