feat(app): new tabs styling (#15284)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
@@ -46,6 +46,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
title={language.t("common.closeTab")}
|
title={language.t("common.closeTab")}
|
||||||
keybind={command.keybind("tab.close")}
|
keybind={command.keybind("tab.close")}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
gutter={10}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="close-small"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createEffect, on, onCleanup, type JSX } from "solid-js"
|
import { createEffect, on, onCleanup, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import type { FileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
import type { SelectedLineRange } from "@/context/file"
|
import type { SelectedLineRange } from "@/context/file"
|
||||||
@@ -31,38 +30,8 @@ export interface SessionReviewTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StickyAddButton(props: { children: JSX.Element }) {
|
export function StickyAddButton(props: { children: JSX.Element }) {
|
||||||
const [state, setState] = createStore({ stuck: false })
|
|
||||||
let button: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const node = button
|
|
||||||
if (!node) return
|
|
||||||
|
|
||||||
const scroll = node.parentElement
|
|
||||||
if (!scroll) return
|
|
||||||
|
|
||||||
const handler = () => {
|
|
||||||
const rect = node.getBoundingClientRect()
|
|
||||||
const scrollRect = scroll.getBoundingClientRect()
|
|
||||||
setState("stuck", rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll.addEventListener("scroll", handler, { passive: true })
|
|
||||||
const observer = new ResizeObserver(handler)
|
|
||||||
observer.observe(scroll)
|
|
||||||
handler()
|
|
||||||
onCleanup(() => {
|
|
||||||
scroll.removeEventListener("scroll", handler)
|
|
||||||
observer.disconnect()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||||
ref={button}
|
|
||||||
class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-border-weak-base px-3"
|
|
||||||
classList={{ "border-l": state.stuck }}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -219,13 +219,11 @@ export function SessionSidePanel(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={reviewTab()}>
|
<Show when={reviewTab()}>
|
||||||
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
<Tabs.Trigger value="review">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<div>{language.t("session.tab.review")}</div>
|
<div>{language.t("session.tab.review")}</div>
|
||||||
<Show when={hasReview()}>
|
<Show when={hasReview()}>
|
||||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
<div>{reviewCount()}</div>
|
||||||
{reviewCount()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
@@ -234,7 +232,7 @@ export function SessionSidePanel(props: {
|
|||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="context"
|
value="context"
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom" gutter={10}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="close-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -266,6 +264,7 @@ export function SessionSidePanel(props: {
|
|||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
|
class="!rounded-md"
|
||||||
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
|
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
|
||||||
aria-label={language.t("command.file.open")}
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
@@ -312,7 +311,7 @@ export function SessionSidePanel(props: {
|
|||||||
{(tab) => {
|
{(tab) => {
|
||||||
const path = createMemo(() => file.pathFromTab(tab))
|
const path = createMemo(() => file.pathFromTab(tab))
|
||||||
return (
|
return (
|
||||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
<div data-component="tabs-drag-preview">
|
||||||
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
[data-component="tabs"] {
|
[data-component="tabs"] {
|
||||||
|
--tabs-bar-height: 48px;
|
||||||
|
--tabs-compact-pill-height: 24px;
|
||||||
|
--tabs-compact-pill-radius: 6px;
|
||||||
|
--tabs-compact-pill-padding-x: 4px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -93,17 +98,6 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
&:has([data-hidden]) {
|
|
||||||
[data-slot="tabs-trigger-close-button"] {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
[data-slot="tabs-trigger-close-button"] {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:has([data-selected]) {
|
&:has([data-selected]) {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -112,6 +106,7 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:disabled):not([data-selected]) {
|
&:hover:not(:disabled):not([data-selected]) {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
@@ -140,6 +135,118 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#review-panel &[data-variant="normal"][data-orientation="horizontal"] {
|
||||||
|
background-color: var(--background-stronger);
|
||||||
|
|
||||||
|
[data-slot="tabs-list"] {
|
||||||
|
height: var(--tabs-bar-height);
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 0;
|
||||||
|
--tabs-review-gap: 16px;
|
||||||
|
--tabs-review-fade: 16px;
|
||||||
|
gap: var(--tabs-review-gap);
|
||||||
|
background-color: var(--background-stronger);
|
||||||
|
border-bottom: 1px solid var(--border-weak-base);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .sticky {
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: var(--background-stronger);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(var(--tabs-review-fade) * -1);
|
||||||
|
width: var(--tabs-review-fade);
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--background-stronger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger-wrapper"] {
|
||||||
|
height: var(--tabs-compact-pill-height);
|
||||||
|
margin-block: calc((var(--tabs-bar-height) - var(--tabs-compact-pill-height)) / 2);
|
||||||
|
max-width: 320px;
|
||||||
|
padding-inline: var(--tabs-compact-pill-padding-x);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--tabs-compact-pill-radius);
|
||||||
|
background-color: transparent;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-weak);
|
||||||
|
transition:
|
||||||
|
color 120ms ease,
|
||||||
|
background-color 120ms ease,
|
||||||
|
border-color 120ms ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: calc((var(--tabs-compact-pill-height) - var(--tabs-bar-height)) / 2);
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--text-strong);
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0.75);
|
||||||
|
transform-origin: center;
|
||||||
|
transition:
|
||||||
|
opacity 120ms ease,
|
||||||
|
transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-value="review"] {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger"] {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="tabs-trigger-close-button"]) {
|
||||||
|
padding-right: 5px;
|
||||||
|
[data-slot="tabs-trigger"] {
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-selected]) {
|
||||||
|
color: var(--text-strong);
|
||||||
|
background-color: var(--surface-base-active);
|
||||||
|
border-color: var(--border-weak-base);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="file-icon"] {
|
||||||
|
filter: grayscale(1) !important;
|
||||||
|
transition: filter 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-selected]) {
|
||||||
|
[data-component="file-icon"] {
|
||||||
|
filter: grayscale(0) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:has([data-selected])) {
|
||||||
|
color: var(--text-base);
|
||||||
|
background-color: var(--surface-base-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[data-variant="alt"] {
|
&[data-variant="alt"] {
|
||||||
[data-slot="tabs-list"] {
|
[data-slot="tabs-list"] {
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
@@ -282,9 +389,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="tabs-trigger-wrapper"] {
|
[data-slot="tabs-trigger-wrapper"] {
|
||||||
height: 24px;
|
height: var(--tabs-compact-pill-height);
|
||||||
border-radius: 6px;
|
border-radius: var(--tabs-compact-pill-radius);
|
||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition:
|
||||||
|
color 120ms ease,
|
||||||
|
background-color 120ms ease,
|
||||||
|
border-color 120ms ease;
|
||||||
|
|
||||||
&:not(:has([data-selected])):hover:not(:disabled) {
|
&:not(:has([data-selected])):hover:not(:disabled) {
|
||||||
color: var(--text-base);
|
color: var(--text-base);
|
||||||
@@ -292,6 +405,7 @@
|
|||||||
|
|
||||||
&:has([data-selected]) {
|
&:has([data-selected]) {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
|
border-color: var(--border-weak-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,3 +573,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="tabs-drag-preview"] {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--tabs-bar-height, 48px);
|
||||||
|
max-width: 320px;
|
||||||
|
padding-inline: var(--tabs-compact-pill-padding-x, 4px);
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-strong);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tabs-drag-preview"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc((var(--tabs-bar-height, 48px) - var(--tabs-compact-pill-height, 24px)) / 2);
|
||||||
|
height: var(--tabs-compact-pill-height, 24px);
|
||||||
|
border: 1px solid var(--border-weak-base);
|
||||||
|
border-radius: var(--tabs-compact-pill-radius, 6px);
|
||||||
|
background-color: var(--surface-base-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tabs-drag-preview"]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tabs-drag-preview"] > * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="tabs-trigger-wrapper"
|
data-slot="tabs-trigger-wrapper"
|
||||||
|
data-value={props.value}
|
||||||
classList={{
|
classList={{
|
||||||
...(split.classList ?? {}),
|
...(split.classList ?? {}),
|
||||||
[split.class ?? ""]: !!split.class,
|
[split.class ?? ""]: !!split.class,
|
||||||
@@ -80,6 +81,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
|
|||||||
<Kobalte.Trigger
|
<Kobalte.Trigger
|
||||||
{...rest}
|
{...rest}
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
|
data-value={props.value}
|
||||||
classList={{ [split.classes?.button ?? ""]: split.classes?.button }}
|
classList={{ [split.classes?.button ?? ""]: split.classes?.button }}
|
||||||
>
|
>
|
||||||
{split.children}
|
{split.children}
|
||||||
|
|||||||
Reference in New Issue
Block a user