Skip to content

Commit 9312867

Browse files
feat(app): new tabs styling (#15284)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
1 parent 7e6a007 commit 9312867

5 files changed

Lines changed: 174 additions & 51 deletions

File tree

packages/app/src/components/session/session-sortable-tab.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
4646
title={language.t("common.closeTab")}
4747
keybind={command.keybind("tab.close")}
4848
placement="bottom"
49+
gutter={10}
4950
>
5051
<IconButton
5152
icon="close-small"

packages/app/src/pages/session/review-tab.tsx

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createEffect, on, onCleanup, type JSX } from "solid-js"
2-
import { createStore } from "solid-js/store"
32
import type { FileDiff } from "@opencode-ai/sdk/v2"
43
import { SessionReview } from "@opencode-ai/ui/session-review"
54
import type { SelectedLineRange } from "@/context/file"
@@ -31,38 +30,8 @@ export interface SessionReviewTabProps {
3130
}
3231

3332
export function StickyAddButton(props: { children: JSX.Element }) {
34-
const [state, setState] = createStore({ stuck: false })
35-
let button: HTMLDivElement | undefined
36-
37-
createEffect(() => {
38-
const node = button
39-
if (!node) return
40-
41-
const scroll = node.parentElement
42-
if (!scroll) return
43-
44-
const handler = () => {
45-
const rect = node.getBoundingClientRect()
46-
const scrollRect = scroll.getBoundingClientRect()
47-
setState("stuck", rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth)
48-
}
49-
50-
scroll.addEventListener("scroll", handler, { passive: true })
51-
const observer = new ResizeObserver(handler)
52-
observer.observe(scroll)
53-
handler()
54-
onCleanup(() => {
55-
scroll.removeEventListener("scroll", handler)
56-
observer.disconnect()
57-
})
58-
})
59-
6033
return (
61-
<div
62-
ref={button}
63-
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"
64-
classList={{ "border-l": state.stuck }}
65-
>
34+
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
6635
{props.children}
6736
</div>
6837
)

packages/app/src/pages/session/session-side-panel.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,11 @@ export function SessionSidePanel(props: {
219219
}}
220220
>
221221
<Show when={reviewTab()}>
222-
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
222+
<Tabs.Trigger value="review">
223223
<div class="flex items-center gap-1.5">
224224
<div>{language.t("session.tab.review")}</div>
225225
<Show when={hasReview()}>
226-
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
227-
{reviewCount()}
228-
</div>
226+
<div>{reviewCount()}</div>
229227
</Show>
230228
</div>
231229
</Tabs.Trigger>
@@ -234,7 +232,7 @@ export function SessionSidePanel(props: {
234232
<Tabs.Trigger
235233
value="context"
236234
closeButton={
237-
<Tooltip value={language.t("common.closeTab")} placement="bottom">
235+
<Tooltip value={language.t("common.closeTab")} placement="bottom" gutter={10}>
238236
<IconButton
239237
icon="close-small"
240238
variant="ghost"
@@ -266,6 +264,7 @@ export function SessionSidePanel(props: {
266264
icon="plus-small"
267265
variant="ghost"
268266
iconSize="large"
267+
class="!rounded-md"
269268
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
270269
aria-label={language.t("command.file.open")}
271270
/>
@@ -312,7 +311,7 @@ export function SessionSidePanel(props: {
312311
{(tab) => {
313312
const path = createMemo(() => file.pathFromTab(tab))
314313
return (
315-
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
314+
<div data-component="tabs-drag-preview">
316315
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
317316
</div>
318317
)

packages/ui/src/components/tabs.css

Lines changed: 165 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[data-component="tabs"] {
2+
--tabs-bar-height: 48px;
3+
--tabs-compact-pill-height: 24px;
4+
--tabs-compact-pill-radius: 6px;
5+
--tabs-compact-pill-padding-x: 4px;
6+
27
width: 100%;
38
height: 100%;
49
display: flex;
@@ -93,17 +98,6 @@
9398
outline: none;
9499
box-shadow: none;
95100
}
96-
&:has([data-hidden]) {
97-
[data-slot="tabs-trigger-close-button"] {
98-
opacity: 0;
99-
}
100-
101-
&:hover {
102-
[data-slot="tabs-trigger-close-button"] {
103-
opacity: 1;
104-
}
105-
}
106-
}
107101
&:has([data-selected]) {
108102
color: var(--text-strong);
109103
background-color: transparent;
@@ -112,6 +106,7 @@
112106
opacity: 1;
113107
}
114108
}
109+
115110
&:hover:not(:disabled):not([data-selected]) {
116111
color: var(--text-strong);
117112
}
@@ -140,6 +135,118 @@
140135
}
141136
}
142137

138+
#review-panel &[data-variant="normal"][data-orientation="horizontal"] {
139+
background-color: var(--background-stronger);
140+
141+
[data-slot="tabs-list"] {
142+
height: var(--tabs-bar-height);
143+
padding-left: 12px;
144+
padding-right: 0;
145+
--tabs-review-gap: 16px;
146+
--tabs-review-fade: 16px;
147+
gap: var(--tabs-review-gap);
148+
background-color: var(--background-stronger);
149+
border-bottom: 1px solid var(--border-weak-base);
150+
151+
&::after {
152+
display: none;
153+
}
154+
155+
> .sticky {
156+
border-bottom: none;
157+
background-color: var(--background-stronger);
158+
159+
&::before {
160+
content: "";
161+
position: absolute;
162+
top: 0;
163+
bottom: 0;
164+
left: calc(var(--tabs-review-fade) * -1);
165+
width: var(--tabs-review-fade);
166+
pointer-events: none;
167+
background: linear-gradient(90deg, transparent, var(--background-stronger));
168+
}
169+
}
170+
}
171+
172+
[data-slot="tabs-trigger-wrapper"] {
173+
height: var(--tabs-compact-pill-height);
174+
margin-block: calc((var(--tabs-bar-height) - var(--tabs-compact-pill-height)) / 2);
175+
max-width: 320px;
176+
padding-inline: var(--tabs-compact-pill-padding-x);
177+
box-sizing: border-box;
178+
border: 1px solid transparent;
179+
border-radius: var(--tabs-compact-pill-radius);
180+
background-color: transparent;
181+
gap: 8px;
182+
color: var(--text-weak);
183+
transition:
184+
color 120ms ease,
185+
background-color 120ms ease,
186+
border-color 120ms ease;
187+
188+
&::after {
189+
content: "";
190+
position: absolute;
191+
left: 0;
192+
right: 0;
193+
bottom: calc((var(--tabs-compact-pill-height) - var(--tabs-bar-height)) / 2);
194+
height: 1px;
195+
background-color: var(--text-strong);
196+
opacity: 0;
197+
transform: scaleX(0.75);
198+
transform-origin: center;
199+
transition:
200+
opacity 120ms ease,
201+
transform 120ms ease;
202+
}
203+
204+
&[data-value="review"] {
205+
padding-left: 8px;
206+
padding-right: 8px;
207+
}
208+
209+
[data-slot="tabs-trigger"] {
210+
height: 100%;
211+
padding: 0 !important;
212+
}
213+
214+
&:has([data-slot="tabs-trigger-close-button"]) {
215+
padding-right: 5px;
216+
[data-slot="tabs-trigger"] {
217+
padding-right: 0 !important;
218+
}
219+
}
220+
221+
&:has([data-selected]) {
222+
color: var(--text-strong);
223+
background-color: var(--surface-base-active);
224+
border-color: var(--border-weak-base);
225+
226+
&::after {
227+
opacity: 1;
228+
transform: scaleX(1);
229+
}
230+
}
231+
232+
[data-component="file-icon"] {
233+
filter: grayscale(1) !important;
234+
transition: filter 120ms ease;
235+
}
236+
237+
&:has([data-selected]) {
238+
[data-component="file-icon"] {
239+
filter: grayscale(0) !important;
240+
}
241+
}
242+
243+
&:hover:not(:disabled):not(:has([data-selected])) {
244+
color: var(--text-base);
245+
background-color: var(--surface-base-hover);
246+
}
247+
}
248+
}
249+
143250
&[data-variant="alt"] {
144251
[data-slot="tabs-list"] {
145252
padding-left: 24px;
@@ -282,16 +389,23 @@
282389
}
283390

284391
[data-slot="tabs-trigger-wrapper"] {
285-
height: 24px;
286-
border-radius: 6px;
392+
height: var(--tabs-compact-pill-height);
393+
border-radius: var(--tabs-compact-pill-radius);
287394
color: var(--text-weak);
395+
box-sizing: border-box;
396+
border: 1px solid transparent;
397+
transition:
398+
color 120ms ease,
399+
background-color 120ms ease,
400+
border-color 120ms ease;
288401

289402
&:not(:has([data-selected])):hover:not(:disabled) {
290403
color: var(--text-base);
291404
}
292405

293406
&:has([data-selected]) {
294407
color: var(--text-strong);
408+
border-color: var(--border-weak-base);
295409
}
296410
}
297411
}
@@ -459,3 +573,41 @@
459573
}
460574
}
461575
}
576+
577+
[data-component="tabs-drag-preview"] {
578+
position: relative;
579+
display: flex;
580+
align-items: center;
581+
height: var(--tabs-bar-height, 48px);
582+
max-width: 320px;
583+
padding-inline: var(--tabs-compact-pill-padding-x, 4px);
584+
overflow: hidden;
585+
color: var(--text-strong);
586+
opacity: 0.6;
587+
}
588+
589+
[data-component="tabs-drag-preview"]::before {
590+
content: "";
591+
position: absolute;
592+
left: 0;
593+
right: 0;
594+
top: calc((var(--tabs-bar-height, 48px) - var(--tabs-compact-pill-height, 24px)) / 2);
595+
height: var(--tabs-compact-pill-height, 24px);
596+
border: 1px solid var(--border-weak-base);
597+
border-radius: var(--tabs-compact-pill-radius, 6px);
598+
background-color: var(--surface-base-active);
599+
}
600+
601+
[data-component="tabs-drag-preview"]::after {
602+
content: "";
603+
position: absolute;
604+
left: 0;
605+
right: 0;
606+
bottom: 0;
607+
height: 1px;
608+
background-color: var(--text-strong);
609+
}
610+
611+
[data-component="tabs-drag-preview"] > * {
612+
position: relative;
613+
}

packages/ui/src/components/tabs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
6161
return (
6262
<div
6363
data-slot="tabs-trigger-wrapper"
64+
data-value={props.value}
6465
classList={{
6566
...(split.classList ?? {}),
6667
[split.class ?? ""]: !!split.class,
@@ -80,6 +81,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
8081
<Kobalte.Trigger
8182
{...rest}
8283
data-slot="tabs-trigger"
84+
data-value={props.value}
8385
classList={{ [split.classes?.button ?? ""]: split.classes?.button }}
8486
>
8587
{split.children}

0 commit comments

Comments
 (0)