Bläddra i källkod

feat(app): improve titlebar tab layout (#33914)

Brendan Allan 1 månad sedan
förälder
incheckning
c097d51135

+ 2 - 2
packages/app/src/components/titlebar-tab-drag.ts

@@ -22,7 +22,7 @@ export function captureTabDragLayout(list: HTMLElement, order: string[]) {
   for (const slot of slots) {
     const id = slot.dataset.tabKey
     if (!id) continue
-    const tab = slot.querySelector<HTMLElement>("[data-titlebar-tab]")
+    const tab = slot.matches("[data-titlebar-tab]") ? slot : slot.querySelector<HTMLElement>("[data-titlebar-tab]")
     if (!tab) continue
     tabWidthById.set(id, tab.getBoundingClientRect().width)
   }
@@ -33,7 +33,7 @@ export function captureTabDragLayout(list: HTMLElement, order: string[]) {
     const secondId = order[1]
     for (const slot of slots) {
       if (slot.dataset.tabKey !== secondId) continue
-      const tab = slot.querySelector<HTMLElement>("[data-titlebar-tab]")
+      const tab = slot.matches("[data-titlebar-tab]") ? slot : slot.querySelector<HTMLElement>("[data-titlebar-tab]")
       if (!tab) break
       const style = getComputedStyle(slot)
       dividerWidth =

+ 12 - 18
packages/app/src/components/titlebar-tab-nav.css

@@ -1,20 +1,16 @@
 [data-titlebar-tab] [data-slot="tab-close"] {
   position: absolute;
-  top: 0;
-  right: 0;
+  top: 4px;
+  right: 4px;
   display: flex;
-  height: 28px;
-  width: 28px;
+  height: 20px;
+  width: 20px;
   align-items: center;
   justify-content: center;
 }
 
-[data-slot="titlebar-tabs"] {
-  container-type: inline-size;
-}
-
 [data-titlebar-tab-list] {
-  gap: calc(min(12px, 2cqw) + 1.5px);
+  gap: 13.5px;
 }
 
 [data-titlebar-tab-slot] {
@@ -25,7 +21,7 @@
   content: "";
   position: absolute;
   top: 8px;
-  left: calc(0px - min(6px, 1cqw) - 0.75px);
+  left: -6.75px;
   width: 1.5px;
   height: 12px;
   border-radius: 9999px;
@@ -54,19 +50,11 @@
   right: 100%;
 }
 
-[data-titlebar-tab] [data-slot="tab-link"] {
-  padding-right: 22px;
-}
-
 [data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"])
   [data-slot="tab-link"] {
   padding-right: 0;
 }
 
-[data-titlebar-tab][data-editing="true"] [data-slot="tab-link"] {
-  padding-right: 22px;
-}
-
 [data-titlebar-tab] [data-slot="tab-title"] {
   min-width: 0;
 }
@@ -95,4 +83,10 @@
   [data-titlebar-tab-title] {
     display: none;
   }
+
+  [data-slot="tab-close"] {
+    right: auto;
+    left: 50%;
+    transform: translateX(-50%);
+  }
 }

+ 159 - 135
packages/app/src/components/titlebar-tab-nav.tsx

@@ -29,6 +29,9 @@ export function TabNavItem(props: {
   dragging?: boolean
   pressed?: boolean
   hidden?: boolean
+  tabKey: string
+  dragActive: boolean
+  onPointerDown: (event: PointerEvent) => void
 }) {
   const language = useLanguage()
   const [editing, setEditing] = createSignal(false)
@@ -170,105 +173,112 @@ export function TabNavItem(props: {
 
   return (
     <div
-      ref={(el) => {
-        tabRoot = el
-        forwardTabRef(props.ref, el)
-      }}
-      data-titlebar-tab
-      data-slot="titlebar-tab-item"
-      data-title-overflow={titleOverflowing()}
-      data-editing={editing()}
-      class="group relative flex h-7 w-full min-w-0 max-w-56 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
-      classList={{ invisible: props.hidden }}
-      data-active={props.active}
-      data-dragging={props.dragging}
-      data-pressed={props.pressed}
-      onMouseDown={(event) => {
-        if (event.button !== 1) return
-        closeTab(event)
-      }}
+      data-titlebar-tab-slot
+      data-tab-key={props.tabKey}
+      class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
+      classList={{ invisible: props.hidden, "pointer-events-none": props.dragActive }}
+      onPointerDown={props.onPointerDown}
     >
-      <Show when={props.session()}>
-        {(session) => {
-          return (
-            <a
-              data-slot="tab-link"
-              data-titlebar-tab-link
-              href={props.href}
-              draggable={false}
-              onDragStart={(event) => {
-                event.preventDefault()
-                event.stopPropagation()
-              }}
-              onClick={(event) => {
-                event.preventDefault()
-                if (editing()) return
-                if (props.suppressNavigation?.()) return
-                props.onNavigate()
-              }}
-              class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
-            >
-              <span data-slot="project-avatar-slot">
-                <SessionTabAvatar
-                  project={project()}
-                  directory={session().directory}
-                  sessionId={session().id}
-                  activeServer={props.activeServer}
-                />
-              </span>
-              <span
-                ref={(el) => {
-                  titleEl = el
-                  titleEl.textContent = session().title
-                }}
-                data-slot="tab-title"
-                data-titlebar-tab-title
-                class="min-w-0 flex-1 outline-none leading-4"
-                classList={{
-                  "overflow-hidden text-clip whitespace-nowrap": !editing(),
-                  "select-text": editing(),
-                }}
-                contenteditable={editing() ? true : undefined}
-                onDblClick={openRename}
-                onKeyDown={(event) => {
-                  event.stopPropagation()
-                  if (event.key === "Enter") {
-                    event.preventDefault()
-                    void closeRename(true)
-                    return
-                  }
-                  if (event.key !== "Escape") return
+      <div
+        ref={(el) => {
+          tabRoot = el
+          forwardTabRef(props.ref, el)
+        }}
+        data-titlebar-tab
+        data-slot="titlebar-tab-item"
+        data-title-overflow={titleOverflowing()}
+        data-editing={editing()}
+        class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
+        data-active={props.active}
+        data-dragging={props.dragging}
+        data-pressed={props.pressed}
+        onMouseDown={(event) => {
+          if (event.button !== 1) return
+          closeTab(event)
+        }}
+      >
+        <Show when={props.session()}>
+          {(session) => {
+            return (
+              <a
+                data-slot="tab-link"
+                data-titlebar-tab-link
+                href={props.href}
+                draggable={false}
+                onDragStart={(event) => {
                   event.preventDefault()
-                  titleEl.textContent = session().title
-                  void closeRename(false)
-                }}
-                onBlur={() => void closeRename(true)}
-                onPointerDown={(event) => {
-                  if (!editing()) return
                   event.stopPropagation()
                 }}
                 onClick={(event) => {
-                  if (!editing()) return
                   event.preventDefault()
+                  if (editing()) return
+                  if (props.suppressNavigation?.()) return
+                  props.onNavigate()
                 }}
-              />
-            </a>
-          )
-        }}
-      </Show>
-
-      <div data-slot="tab-close">
-        <IconButtonV2
-          size="small"
-          variant="ghost-muted"
-          class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
-          onPointerDown={(event) => {
-            event.preventDefault()
-            event.stopPropagation()
+                class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
+              >
+                <span data-slot="project-avatar-slot">
+                  <SessionTabAvatar
+                    project={project()}
+                    directory={session().directory}
+                    sessionId={session().id}
+                    activeServer={props.activeServer}
+                  />
+                </span>
+                <span
+                  ref={(el) => {
+                    titleEl = el
+                    titleEl.textContent = session().title
+                  }}
+                  data-slot="tab-title"
+                  data-titlebar-tab-title
+                  class="min-w-0 flex-1 outline-none leading-4"
+                  classList={{
+                    "overflow-hidden text-clip whitespace-nowrap": !editing(),
+                    "select-text": editing(),
+                  }}
+                  contenteditable={editing() ? true : undefined}
+                  onDblClick={openRename}
+                  onKeyDown={(event) => {
+                    event.stopPropagation()
+                    if (event.key === "Enter") {
+                      event.preventDefault()
+                      void closeRename(true)
+                      return
+                    }
+                    if (event.key !== "Escape") return
+                    event.preventDefault()
+                    titleEl.textContent = session().title
+                    void closeRename(false)
+                  }}
+                  onBlur={() => void closeRename(true)}
+                  onPointerDown={(event) => {
+                    if (!editing()) return
+                    event.stopPropagation()
+                  }}
+                  onClick={(event) => {
+                    if (!editing()) return
+                    event.preventDefault()
+                  }}
+                />
+              </a>
+            )
           }}
-          onClick={closeTab}
-          icon={<IconV2 name="xmark-small" />}
-        />
+        </Show>
+
+        <div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
+          <IconButtonV2
+            size="small"
+            variant="ghost-muted"
+            class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
+            onPointerDown={(event) => {
+              event.preventDefault()
+              event.stopPropagation()
+            }}
+            onClick={closeTab}
+            icon={<IconV2 name="xmark-small" />}
+          />
+        </div>
       </div>
     </div>
   )
@@ -285,6 +295,9 @@ export function DraftTabItem(props: {
   dragging?: boolean
   pressed?: boolean
   hidden?: boolean
+  tabKey: string
+  dragActive: boolean
+  onPointerDown: (event: PointerEvent) => void
 }) {
   const closeTab = (event: MouseEvent) => {
     event.preventDefault()
@@ -293,58 +306,69 @@ export function DraftTabItem(props: {
   }
   return (
     <div
-      ref={(el) => forwardTabRef(props.ref, el)}
-      data-titlebar-tab
-      data-slot="titlebar-tab-item"
-      data-active={props.active}
-      data-dragging={props.dragging}
-      data-pressed={props.pressed}
-      class="group relative flex h-7 w-full min-w-0 max-w-56 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true'][data-pressed='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)]"
-      classList={{ invisible: props.hidden }}
-      onMouseDown={(event) => {
-        if (event.button !== 1) return
-        closeTab(event)
-      }}
+      data-titlebar-tab-slot
+      data-tab-key={props.tabKey}
+      class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
+      classList={{ invisible: props.hidden, "pointer-events-none": props.dragActive }}
+      onPointerDown={props.onPointerDown}
     >
-      <a
-        data-slot="tab-link"
-        data-titlebar-tab-link
-        href={props.href}
-        draggable={false}
-        onDragStart={(event) => {
-          event.preventDefault()
-          event.stopPropagation()
+      <div
+        ref={(el) => forwardTabRef(props.ref, el)}
+        data-titlebar-tab
+        data-slot="titlebar-tab-item"
+        data-active={props.active}
+        data-dragging={props.dragging}
+        data-pressed={props.pressed}
+        class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
+        onMouseDown={(event) => {
+          if (event.button !== 1) return
+          closeTab(event)
         }}
-        onClick={(event) => {
-          event.preventDefault()
-          if (props.suppressNavigation?.()) return
-          props.onNavigate()
-        }}
-        class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-[var(--v2-text-text-base)]"
       >
-        <span class="flex size-4 shrink-0 items-center justify-center">
-          <IconV2 name="edit" />
-        </span>
-        <span data-titlebar-tab-title class="truncate leading-5">
-          {props.title}
-        </span>
-      </a>
-      <div data-slot="tab-close" class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
-        <IconButtonV2
-          size="small"
-          variant="ghost-muted"
-          onPointerDown={(event) => {
+        <a
+          data-slot="tab-link"
+          data-titlebar-tab-link
+          href={props.href}
+          draggable={false}
+          onDragStart={(event) => {
             event.preventDefault()
             event.stopPropagation()
           }}
-          onMouseDown={(event) => {
+          onClick={(event) => {
             event.preventDefault()
-            event.stopPropagation()
+            if (props.suppressNavigation?.()) return
+            props.onNavigate()
           }}
-          onClick={closeTab}
-          icon={<IconV2 name="xmark-small" />}
-          aria-label="Close tab"
-        />
+          class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base [-webkit-user-drag:none]"
+        >
+          <span class="flex size-4 shrink-0 items-center justify-center">
+            <IconV2 name="edit" />
+          </span>
+          <span
+            data-titlebar-tab-title
+            class="min-w-0 flex-1 overflow-hidden text-clip whitespace-nowrap outline-none leading-4"
+          >
+            {props.title}
+          </span>
+        </a>
+        <div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
+          <IconButtonV2
+            size="small"
+            variant="ghost-muted"
+            onPointerDown={(event) => {
+              event.preventDefault()
+              event.stopPropagation()
+            }}
+            onMouseDown={(event) => {
+              event.preventDefault()
+              event.stopPropagation()
+            }}
+            class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
+            onClick={closeTab}
+            icon={<IconV2 name="xmark-small" />}
+            aria-label="Close tab"
+          />
+        </div>
       </div>
     </div>
   )

+ 45 - 57
packages/app/src/components/titlebar-tab-strip.tsx

@@ -100,41 +100,33 @@ function SessionTabSlot(props: {
   })
 
   return (
-    <div
-      data-titlebar-tab-slot
-      data-tab-key={props.id}
-      class="flex min-w-0 max-w-56 flex-1 basis-0"
-      classList={{
-        hidden: !session(),
-        "pointer-events-none": props.dragActive,
-      }}
+    <TabNavItem
+      tabKey={props.id}
+      dragActive={props.dragActive}
       onPointerDown={props.onPointerDown}
-    >
-      <TabNavItem
-        ref={ref}
-        href={tabHref(props.tab)}
-        server={props.tab.server}
-        session={session}
-        onTitleChange={(title) => {
-          const value = session()
-          const ctx = props.serverCtx()
-          if (value && ctx) ctx.sync.session.remember({ ...value, title })
-        }}
-        onTitleChangeFailed={(title) => {
-          const value = session()
-          const ctx = props.serverCtx()
-          if (value && ctx) ctx.sync.session.remember({ ...value, title })
-        }}
-        onNavigate={() => props.onNavigate(ref)}
-        onClose={props.onClose}
-        active={props.active()}
-        activeServer={props.tab.server === props.activeServerKey}
-        forceTruncate={props.forceTruncate}
-        suppressNavigation={props.suppressNavigation}
-        pressed={props.pressed()}
-        hidden={props.dragged()}
-      />
-    </div>
+      ref={ref}
+      href={tabHref(props.tab)}
+      server={props.tab.server}
+      session={session}
+      onTitleChange={(title) => {
+        const value = session()
+        const ctx = props.serverCtx()
+        if (value && ctx) ctx.sync.session.remember({ ...value, title })
+      }}
+      onTitleChangeFailed={(title) => {
+        const value = session()
+        const ctx = props.serverCtx()
+        if (value && ctx) ctx.sync.session.remember({ ...value, title })
+      }}
+      onNavigate={() => props.onNavigate(ref)}
+      onClose={props.onClose}
+      active={props.active()}
+      activeServer={props.tab.server === props.activeServerKey}
+      forceTruncate={props.forceTruncate}
+      suppressNavigation={props.suppressNavigation}
+      pressed={props.pressed()}
+      hidden={props.dragged() || !session()}
+    />
   )
 }
 
@@ -336,7 +328,10 @@ export function TitlebarTabStrip(props: {
     if (event.button !== 0 || drag.active) return
     if (!canStartTabDrag(event.pointerType)) return
     if (isTabCloseTarget(event.target)) return
-    const tabEl = (event.currentTarget as HTMLElement).querySelector<HTMLDivElement>("[data-titlebar-tab]")
+    const target = event.currentTarget as HTMLDivElement
+    const tabEl = target.matches("[data-titlebar-tab]")
+      ? target
+      : target.querySelector<HTMLDivElement>("[data-titlebar-tab]")
     if (!tabEl) return
     if (!tabEl.querySelector('[data-slot="tab-link"]')) return
     const tab = props.tabs.find((item) => tabKey(item) === id)
@@ -459,13 +454,13 @@ export function TitlebarTabStrip(props: {
 
   return (
     <>
-      <div data-slot="titlebar-tabs" class="relative min-w-0 flex-1">
+      <div data-slot="titlebar-tabs" class="relative min-w-0">
         <div
           data-slot="titlebar-tabs-scroll"
           class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
           ref={scrollRef}
         >
-          <div data-titlebar-tab-list class="flex w-full min-w-0 flex-row items-center" ref={listRef}>
+          <div data-titlebar-tab-list class="flex min-w-0 flex-row items-center" ref={listRef}>
             <For each={displayTabs()}>
               {(tab, index) => {
                 const id = tabKey(tab)
@@ -503,30 +498,23 @@ export function TitlebarTabStrip(props: {
                 }
 
                 return (
-                  <div
-                    data-titlebar-tab-slot
-                    data-tab-key={id}
-                    class="flex min-w-0 max-w-56 flex-1 basis-0"
-                    classList={{
-                      "pointer-events-none": drag.active,
-                    }}
+                  <DraftTabItem
+                    tabKey={id}
+                    dragActive={drag.active}
                     onPointerDown={(event) => {
                       if (dragged()) return
                       onPointerDown(id, event)
                     }}
-                  >
-                    <DraftTabItem
-                      ref={ref}
-                      href={tabHref(tab)}
-                      title={language.t("command.session.new")}
-                      onNavigate={() => props.onNavigate(tab, ref)}
-                      onClose={() => props.onClose(tab)}
-                      suppressNavigation={() => suppressNavigation()}
-                      active={props.currentTab() === tab}
-                      pressed={pressedId() === id}
-                      hidden={dragged()}
-                    />
-                  </div>
+                    ref={ref}
+                    href={tabHref(tab)}
+                    title={language.t("command.session.new")}
+                    onNavigate={() => props.onNavigate(tab, ref)}
+                    onClose={() => props.onClose(tab)}
+                    suppressNavigation={() => suppressNavigation()}
+                    active={props.currentTab() === tab}
+                    pressed={pressedId() === id}
+                    hidden={dragged()}
+                  />
                 )
               }}
             </For>

+ 1 - 0
packages/app/src/components/titlebar.tsx

@@ -481,6 +481,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                     />
                   </TooltipV2>
                 </Show>
+                <div class="flex-1" />
                 <TitlebarV2Right state={v2RightState()} />
                 <Show when={windows() && !electronWindows()}>
                   <div data-tauri-decorum-tb class="flex flex-row" />

+ 0 - 1
packages/ui/src/v2/components/project-avatar-v2.css

@@ -87,7 +87,6 @@
 
 [data-slot="project-avatar-surface"] [data-slot="project-avatar-image"] {
   position: relative;
-  z-index: 1;
   display: block;
   width: 100%;
   height: 100%;