Explorar el Código

feat(app): new tabs styling (#15284)

Co-authored-by: David Hill <[email protected]>
Adam hace 1 mes
padre
commit
9312867565

+ 1 - 0
packages/app/src/components/session/session-sortable-tab.tsx

@@ -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 - 32
packages/app/src/pages/session/review-tab.tsx

@@ -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>
   )

+ 5 - 6
packages/app/src/pages/session/session-side-panel.tsx

@@ -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>
                     )

+ 165 - 13
packages/ui/src/components/tabs.css

@@ -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;
+}

+ 2 - 0
packages/ui/src/components/tabs.tsx

@@ -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}