adamelmore 3 недель назад
Родитель
Сommit
801eb5d2cb

+ 58 - 27
packages/app/src/components/file-tree.tsx

@@ -9,6 +9,7 @@ import {
   Match,
   splitProps,
   Switch,
+  untrack,
   type ComponentProps,
   type ParentProps,
 } from "solid-js"
@@ -21,10 +22,14 @@ export default function FileTree(props: {
   nodeClass?: string
   level?: number
   allowed?: readonly string[]
+  draggable?: boolean
+  tooltip?: boolean
   onFileClick?: (file: FileNode) => void
 }) {
   const file = useFile()
   const level = props.level ?? 0
+  const draggable = () => props.draggable ?? true
+  const tooltip = () => props.tooltip ?? true
 
   const filter = createMemo(() => {
     const allowed = props.allowed
@@ -45,6 +50,18 @@ export default function FileTree(props: {
     return { files, dirs }
   })
 
+  createEffect(() => {
+    const current = filter()
+    if (!current) return
+    if (level !== 0) return
+
+    for (const dir of current.dirs) {
+      const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
+      if (expanded) continue
+      file.tree.expand(dir)
+    }
+  })
+
   createEffect(() => {
     void file.tree.list(props.path)
   })
@@ -78,8 +95,9 @@ export default function FileTree(props: {
           [props.nodeClass ?? ""]: !!props.nodeClass,
         }}
         style={`padding-left: ${8 + level * 12}px`}
-        draggable={true}
+        draggable={draggable()}
         onDragStart={(e: DragEvent) => {
+          if (!draggable()) return
           e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
           e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
           if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
@@ -123,41 +141,54 @@ export default function FileTree(props: {
       <For each={nodes()}>
         {(node) => {
           const expanded = () => file.tree.state(node.path)?.expanded ?? false
+          const Wrapper = (p: ParentProps) => {
+            if (!tooltip()) return p.children
+            return (
+              <Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right" class="w-full">
+                {p.children}
+              </Tooltip>
+            )
+          }
+
           return (
-            <Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
-              <Switch>
-                <Match when={node.type === "directory"}>
-                  <Collapsible
-                    variant="ghost"
-                    class="w-full"
-                    forceMount={false}
-                    open={expanded()}
-                    onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
-                  >
-                    <Collapsible.Trigger>
+            <Switch>
+              <Match when={node.type === "directory"}>
+                <Collapsible
+                  variant="ghost"
+                  class="w-full"
+                  forceMount={false}
+                  open={expanded()}
+                  onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
+                >
+                  <Collapsible.Trigger>
+                    <Wrapper>
                       <Node node={node}>
                         <Collapsible.Arrow class="text-icon-weak ml-1" />
                         <FileIcon node={node} expanded={expanded()} class="text-icon-weak -ml-1 size-4" />
                       </Node>
-                    </Collapsible.Trigger>
-                    <Collapsible.Content>
-                      <FileTree
-                        path={node.path}
-                        level={level + 1}
-                        allowed={props.allowed}
-                        onFileClick={props.onFileClick}
-                      />
-                    </Collapsible.Content>
-                  </Collapsible>
-                </Match>
-                <Match when={node.type === "file"}>
+                    </Wrapper>
+                  </Collapsible.Trigger>
+                  <Collapsible.Content>
+                    <FileTree
+                      path={node.path}
+                      level={level + 1}
+                      allowed={props.allowed}
+                      draggable={props.draggable}
+                      tooltip={props.tooltip}
+                      onFileClick={props.onFileClick}
+                    />
+                  </Collapsible.Content>
+                </Collapsible>
+              </Match>
+              <Match when={node.type === "file"}>
+                <Wrapper>
                   <Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
                     <div class="w-4 shrink-0" />
                     <FileIcon node={node} class="text-icon-weak size-4" />
                   </Node>
-                </Match>
-              </Switch>
-            </Tooltip>
+                </Wrapper>
+              </Match>
+            </Switch>
           )
         }}
       </For>

+ 53 - 12
packages/app/src/pages/session.tsx

@@ -1037,7 +1037,7 @@ export default function Page() {
     return `session-review-diff-${sum}`
   }
 
-  const scrollToReviewDiff = (path: string, behavior: ScrollBehavior) => {
+  const reviewDiffTop = (path: string) => {
     const root = reviewScroll()
     if (!root) return
 
@@ -1050,15 +1050,25 @@ export default function Page() {
 
     const a = el.getBoundingClientRect()
     const b = root.getBoundingClientRect()
-    const top = a.top - b.top + root.scrollTop
-    root.scrollTo({ top, behavior })
+    return a.top - b.top + root.scrollTop
+  }
+
+  const scrollToReviewDiff = (path: string) => {
+    const root = reviewScroll()
+    if (!root) return false
+
+    const top = reviewDiffTop(path)
+    if (top === undefined) return false
+
+    view().setScroll("review", { x: root.scrollLeft, y: top })
+    root.scrollTo({ top, behavior: "auto" })
+    return true
   }
 
   const focusReviewDiff = (path: string) => {
     const current = view().review.open() ?? []
     if (!current.includes(path)) view().review.setOpen([...current, path])
     setPendingDiff(path)
-    requestAnimationFrame(() => scrollToReviewDiff(path, "smooth"))
   }
 
   createEffect(() => {
@@ -1067,10 +1077,39 @@ export default function Page() {
     if (!reviewScroll()) return
     if (!diffsReady()) return
 
-    requestAnimationFrame(() => {
-      scrollToReviewDiff(pending, "smooth")
-      setPendingDiff(undefined)
-    })
+    const attempt = (count: number) => {
+      if (pendingDiff() !== pending) return
+      if (count > 60) {
+        setPendingDiff(undefined)
+        return
+      }
+
+      const root = reviewScroll()
+      if (!root) {
+        requestAnimationFrame(() => attempt(count + 1))
+        return
+      }
+
+      if (!scrollToReviewDiff(pending)) {
+        requestAnimationFrame(() => attempt(count + 1))
+        return
+      }
+
+      const top = reviewDiffTop(pending)
+      if (top === undefined) {
+        requestAnimationFrame(() => attempt(count + 1))
+        return
+      }
+
+      if (Math.abs(root.scrollTop - top) <= 1) {
+        setPendingDiff(undefined)
+        return
+      }
+
+      requestAnimationFrame(() => attempt(count + 1))
+    }
+
+    requestAnimationFrame(() => attempt(0))
   })
 
   const activeTab = createMemo(() => {
@@ -2605,12 +2644,12 @@ export default function Page() {
             <Show when={layout.fileTree.opened()}>
               <div class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
                 <div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden">
-                  <Tabs value={fileTreeTab()} onChange={setFileTreeTabValue} class="h-full">
-                    <Tabs.List class="h-auto">
-                      <Tabs.Trigger value="changes" class="w-1/2" classes={{ button: "w-full" }}>
+                  <Tabs variant="pill" value={fileTreeTab()} onChange={setFileTreeTabValue} class="h-full">
+                    <Tabs.List>
+                      <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
                         Changes
                       </Tabs.Trigger>
-                      <Tabs.Trigger value="all" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
+                      <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
                         All files
                       </Tabs.Trigger>
                     </Tabs.List>
@@ -2624,6 +2663,8 @@ export default function Page() {
                             <FileTree
                               path=""
                               allowed={diffs().map((d) => d.file)}
+                              draggable={false}
+                              tooltip={false}
                               onFileClick={(node) => focusReviewDiff(node.path)}
                             />
                           </Show>

+ 53 - 0
packages/ui/src/components/tabs.css

@@ -212,6 +212,59 @@
     /* } */
   }
 
+  &[data-variant="pill"][data-orientation="horizontal"] {
+    background-color: transparent;
+
+    [data-slot="tabs-list"] {
+      height: auto;
+      padding: 6px;
+      gap: 4px;
+      border-bottom: 1px solid var(--border-weak-base);
+      background-color: var(--background-base);
+
+      &::after {
+        display: none;
+      }
+    }
+
+    [data-slot="tabs-trigger-wrapper"] {
+      height: 32px;
+      border: none;
+      border-radius: 999px;
+      background-color: transparent;
+      gap: 0;
+
+      /* text-13-medium */
+      font-family: var(--font-family-sans);
+      font-size: var(--font-size-small);
+      font-style: normal;
+      font-weight: var(--font-weight-medium);
+      line-height: var(--line-height-large);
+      letter-spacing: var(--letter-spacing-normal);
+
+      [data-slot="tabs-trigger"] {
+        height: 100%;
+        width: 100%;
+        padding: 0 12px;
+        background-color: transparent;
+      }
+
+      &:hover:not(:disabled) {
+        background-color: var(--surface-raised-base-hover);
+        color: var(--text-strong);
+      }
+
+      &:has([data-selected]) {
+        background-color: var(--surface-raised-base-active);
+        color: var(--text-strong);
+
+        &:hover:not(:disabled) {
+          background-color: var(--surface-raised-base-active);
+        }
+      }
+    }
+  }
+
   &[data-orientation="vertical"] {
     flex-direction: row;
 

+ 1 - 1
packages/ui/src/components/tabs.tsx

@@ -3,7 +3,7 @@ import { Show, splitProps, type JSX } from "solid-js"
 import type { ComponentProps, ParentProps, Component } from "solid-js"
 
 export interface TabsProps extends ComponentProps<typeof Kobalte> {
-  variant?: "normal" | "alt" | "settings"
+  variant?: "normal" | "alt" | "pill" | "settings"
   orientation?: "horizontal" | "vertical"
 }
 export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}