feat(app): visual improvements (#36073)
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"] {
|
||||||
|
|||||||
@@ -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"] {
|
||||||
|
|||||||
Reference in New Issue
Block a user