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")}
|
||||
keybind={command.keybind("tab.close")}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createEffect, on, onCleanup, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { FileDiff } from "@opencode-ai/sdk/v2"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import type { SelectedLineRange } from "@/context/file"
|
||||
@@ -31,38 +30,8 @@ export interface SessionReviewTabProps {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
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 }}
|
||||
>
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -219,13 +219,11 @@ export function SessionSidePanel(props: {
|
||||
}}
|
||||
>
|
||||
<Show when={reviewTab()}>
|
||||
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
||||
<Tabs.Trigger value="review">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{language.t("session.tab.review")}</div>
|
||||
<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">
|
||||
{reviewCount()}
|
||||
</div>
|
||||
<div>{reviewCount()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
@@ -234,7 +232,7 @@ export function SessionSidePanel(props: {
|
||||
<Tabs.Trigger
|
||||
value="context"
|
||||
closeButton={
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom" gutter={10}>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
@@ -266,6 +264,7 @@ export function SessionSidePanel(props: {
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
class="!rounded-md"
|
||||
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
@@ -312,7 +311,7 @@ export function SessionSidePanel(props: {
|
||||
{(tab) => {
|
||||
const path = createMemo(() => file.pathFromTab(tab))
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[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%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -93,17 +98,6 @@
|
||||
outline: 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]) {
|
||||
color: var(--text-strong);
|
||||
background-color: transparent;
|
||||
@@ -112,6 +106,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not([data-selected]) {
|
||||
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-slot="tabs-list"] {
|
||||
padding-left: 24px;
|
||||
@@ -282,9 +389,15 @@
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
height: var(--tabs-compact-pill-height);
|
||||
border-radius: var(--tabs-compact-pill-radius);
|
||||
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) {
|
||||
color: var(--text-base);
|
||||
@@ -292,6 +405,7 @@
|
||||
|
||||
&:has([data-selected]) {
|
||||
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 (
|
||||
<div
|
||||
data-slot="tabs-trigger-wrapper"
|
||||
data-value={props.value}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
@@ -80,6 +81,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
|
||||
<Kobalte.Trigger
|
||||
{...rest}
|
||||
data-slot="tabs-trigger"
|
||||
data-value={props.value}
|
||||
classList={{ [split.classes?.button ?? ""]: split.classes?.button }}
|
||||
>
|
||||
{split.children}
|
||||
|
||||
Reference in New Issue
Block a user