Просмотр исходного кода

fix(app): highlight selected change

Track clicked file in the Changes tree and apply selection styling to the matching review diff.
David Hill 3 недель назад
Родитель
Сommit
f2bf620206

+ 4 - 1
packages/app/src/components/file-tree.tsx

@@ -29,6 +29,7 @@ export default function FileTree(props: {
   path: string
   path: string
   class?: string
   class?: string
   nodeClass?: string
   nodeClass?: string
+  active?: string
   level?: number
   level?: number
   allowed?: readonly string[]
   allowed?: readonly string[]
   modified?: readonly string[]
   modified?: readonly string[]
@@ -149,6 +150,7 @@ export default function FileTree(props: {
         component={local.as ?? "div"}
         component={local.as ?? "div"}
         classList={{
         classList={{
           "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
           "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
+          "bg-surface-base-active": local.node.path === props.active,
           ...(local.classList ?? {}),
           ...(local.classList ?? {}),
           [local.class ?? ""]: !!local.class,
           [local.class ?? ""]: !!local.class,
           [props.nodeClass ?? ""]: !!props.nodeClass,
           [props.nodeClass ?? ""]: !!props.nodeClass,
@@ -297,7 +299,7 @@ export default function FileTree(props: {
                     <Show when={ignored()}>
                     <Show when={ignored()}>
                       <>
                       <>
                         <span class="mx-1 font-bold text-text-invert-strong">•</span>
                         <span class="mx-1 font-bold text-text-invert-strong">•</span>
-                        <span class="shrink-0 text-text-invert-weak">Ignored</span>
+                        <span class="shrink-0 text-text-invert-strong">Ignored</span>
                       </>
                       </>
                     </Show>
                     </Show>
                   </div>
                   </div>
@@ -343,6 +345,7 @@ export default function FileTree(props: {
                       allowed={props.allowed}
                       allowed={props.allowed}
                       modified={props.modified}
                       modified={props.modified}
                       kinds={props.kinds}
                       kinds={props.kinds}
+                      active={props.active}
                       draggable={props.draggable}
                       draggable={props.draggable}
                       tooltip={props.tooltip}
                       tooltip={props.tooltip}
                       onFileClick={props.onFileClick}
                       onFileClick={props.onFileClick}

+ 28 - 1
packages/app/src/pages/session.tsx

@@ -89,6 +89,7 @@ interface SessionReviewTabProps {
   comments?: LineComment[]
   comments?: LineComment[]
   focusedComment?: { file: string; id: string } | null
   focusedComment?: { file: string; id: string } | null
   onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
   onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
+  focusedFile?: string
   onScrollRef?: (el: HTMLDivElement) => void
   onScrollRef?: (el: HTMLDivElement) => void
   classes?: {
   classes?: {
     root?: string
     root?: string
@@ -213,6 +214,7 @@ function SessionReviewTab(props: SessionReviewTabProps) {
       diffStyle={props.diffStyle}
       diffStyle={props.diffStyle}
       onDiffStyleChange={props.onDiffStyleChange}
       onDiffStyleChange={props.onDiffStyleChange}
       onViewFile={props.onViewFile}
       onViewFile={props.onViewFile}
+      focusedFile={props.focusedFile}
       readFile={readFile}
       readFile={readFile}
       onLineComment={props.onLineComment}
       onLineComment={props.onLineComment}
       comments={props.comments}
       comments={props.comments}
@@ -480,12 +482,29 @@ export default function Page() {
   }
   }
 
 
   const kinds = createMemo(() => {
   const kinds = createMemo(() => {
+    const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
+      if (!a) return b
+      if (a === b) return a
+      return "mix" as const
+    }
+
+    const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
+
     const out = new Map<string, "add" | "del" | "mix">()
     const out = new Map<string, "add" | "del" | "mix">()
     for (const diff of diffs()) {
     for (const diff of diffs()) {
+      const file = normalize(diff.file)
       const add = diff.additions > 0
       const add = diff.additions > 0
       const del = diff.deletions > 0
       const del = diff.deletions > 0
       const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
       const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
-      out.set(diff.file, kind)
+
+      out.set(file, kind)
+
+      const parts = file.split("/")
+      for (const [idx] of parts.slice(0, -1).entries()) {
+        const dir = parts.slice(0, idx + 1).join("/")
+        if (!dir) continue
+        out.set(dir, merge(out.get(dir), kind))
+      }
     }
     }
     return out
     return out
   })
   })
@@ -1084,12 +1103,15 @@ export default function Page() {
   const [tree, setTree] = createStore({
   const [tree, setTree] = createStore({
     reviewScroll: undefined as HTMLDivElement | undefined,
     reviewScroll: undefined as HTMLDivElement | undefined,
     pendingDiff: undefined as string | undefined,
     pendingDiff: undefined as string | undefined,
+    activeDiff: undefined as string | undefined,
   })
   })
 
 
   const reviewScroll = () => tree.reviewScroll
   const reviewScroll = () => tree.reviewScroll
   const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
   const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
   const pendingDiff = () => tree.pendingDiff
   const pendingDiff = () => tree.pendingDiff
   const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
   const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
+  const activeDiff = () => tree.activeDiff
+  const setActiveDiff = (value: string | undefined) => setTree("activeDiff", value)
 
 
   const showAllFiles = () => {
   const showAllFiles = () => {
     if (fileTreeTab() !== "changes") return
     if (fileTreeTab() !== "changes") return
@@ -1151,6 +1173,7 @@ export default function Page() {
   const focusReviewDiff = (path: string) => {
   const focusReviewDiff = (path: string) => {
     const current = view().review.open() ?? []
     const current = view().review.open() ?? []
     if (!current.includes(path)) view().review.setOpen([...current, path])
     if (!current.includes(path)) view().review.setOpen([...current, path])
+    setActiveDiff(path)
     setPendingDiff(path)
     setPendingDiff(path)
   }
   }
 
 
@@ -1697,6 +1720,7 @@ export default function Page() {
                                 diffs={diffs}
                                 diffs={diffs}
                                 view={view}
                                 view={view}
                                 diffStyle="unified"
                                 diffStyle="unified"
+                                focusedFile={activeDiff()}
                                 onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                                 onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                                 comments={comments.all()}
                                 comments={comments.all()}
                                 focusedComment={comments.focus()}
                                 focusedComment={comments.focus()}
@@ -2046,6 +2070,7 @@ export default function Page() {
                           </StickyAddButton>
                           </StickyAddButton>
                         </Tabs.List>
                         </Tabs.List>
                       </div>
                       </div>
+
                       <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
                       <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
                         <Show when={activeTab() === "empty"}>
                         <Show when={activeTab() === "empty"}>
                           <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
                           <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
@@ -2597,6 +2622,7 @@ export default function Page() {
                             diffStyle={layout.review.diffStyle()}
                             diffStyle={layout.review.diffStyle()}
                             onDiffStyleChange={layout.review.setDiffStyle}
                             onDiffStyleChange={layout.review.setDiffStyle}
                             onScrollRef={setReviewScroll}
                             onScrollRef={setReviewScroll}
+                            focusedFile={activeDiff()}
                             onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                             onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                             comments={comments.all()}
                             comments={comments.all()}
                             focusedComment={comments.focus()}
                             focusedComment={comments.focus()}
@@ -2664,6 +2690,7 @@ export default function Page() {
                               allowed={diffFiles()}
                               allowed={diffFiles()}
                               kinds={kinds()}
                               kinds={kinds()}
                               draggable={false}
                               draggable={false}
+                              active={activeDiff()}
                               onFileClick={(node) => focusReviewDiff(node.path)}
                               onFileClick={(node) => focusReviewDiff(node.path)}
                             />
                             />
                           </Show>
                           </Show>

+ 8 - 1
packages/ui/src/components/button.css

@@ -43,6 +43,10 @@
     background-color: transparent;
     background-color: transparent;
     color: var(--text-strong);
     color: var(--text-strong);
 
 
+    [data-slot="icon-svg"] {
+      color: var(--icon-base);
+    }
+
     &:hover:not(:disabled) {
     &:hover:not(:disabled) {
       background-color: var(--surface-raised-base-hover);
       background-color: var(--surface-raised-base-hover);
     }
     }
@@ -54,8 +58,11 @@
     }
     }
     &:disabled {
     &:disabled {
       color: var(--text-weak);
       color: var(--text-weak);
-      opacity: 0.7;
       cursor: not-allowed;
       cursor: not-allowed;
+
+      [data-slot="icon-svg"] {
+        color: var(--icon-disabled);
+      }
     }
     }
     &[data-selected="true"]:not(:disabled) {
     &[data-selected="true"]:not(:disabled) {
       background-color: var(--surface-raised-base-hover);
       background-color: var(--surface-raised-base-hover);

+ 7 - 0
packages/ui/src/components/session-review.css

@@ -54,6 +54,13 @@
     background-color: var(--background-stronger) !important;
     background-color: var(--background-stronger) !important;
   }
   }
 
 
+  [data-slot="session-review-accordion-item"][data-selected] {
+    [data-slot="session-review-accordion-content"] {
+      box-shadow: var(--shadow-xs-border-select);
+      border-radius: var(--radius-lg);
+    }
+  }
+
   [data-slot="accordion-item"] {
   [data-slot="accordion-item"] {
     [data-slot="accordion-content"] {
     [data-slot="accordion-content"] {
       display: none;
       display: none;

+ 2 - 0
packages/ui/src/components/session-review.tsx

@@ -44,6 +44,7 @@ export interface SessionReviewProps {
   comments?: SessionReviewComment[]
   comments?: SessionReviewComment[]
   focusedComment?: SessionReviewFocus | null
   focusedComment?: SessionReviewFocus | null
   onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
   onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
+  focusedFile?: string
   open?: string[]
   open?: string[]
   onOpenChange?: (open: string[]) => void
   onOpenChange?: (open: string[]) => void
   scrollRef?: (el: HTMLDivElement) => void
   scrollRef?: (el: HTMLDivElement) => void
@@ -501,6 +502,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                   id={diffId(diff.file)}
                   id={diffId(diff.file)}
                   data-file={diff.file}
                   data-file={diff.file}
                   data-slot="session-review-accordion-item"
                   data-slot="session-review-accordion-item"
+                  data-selected={props.focusedFile === diff.file ? "" : undefined}
                 >
                 >
                   <StickyAccordionHeader>
                   <StickyAccordionHeader>
                     <Accordion.Trigger>
                     <Accordion.Trigger>

+ 2 - 0
packages/ui/src/styles/theme.css

@@ -286,6 +286,7 @@
   --icon-diff-add-active: var(--mint-light-12);
   --icon-diff-add-active: var(--mint-light-12);
   --icon-diff-delete-base: var(--ember-light-10);
   --icon-diff-delete-base: var(--ember-light-10);
   --icon-diff-delete-hover: var(--ember-light-11);
   --icon-diff-delete-hover: var(--ember-light-11);
+  --icon-diff-modified-base: var(--icon-warning-base);
   --syntax-comment: var(--text-weak);
   --syntax-comment: var(--text-weak);
   --syntax-regexp: var(--text-base);
   --syntax-regexp: var(--text-base);
   --syntax-string: #006656;
   --syntax-string: #006656;
@@ -543,6 +544,7 @@
     --icon-diff-add-active: var(--mint-dark-11);
     --icon-diff-add-active: var(--mint-dark-11);
     --icon-diff-delete-base: var(--ember-dark-9);
     --icon-diff-delete-base: var(--ember-dark-9);
     --icon-diff-delete-hover: var(--ember-dark-10);
     --icon-diff-delete-hover: var(--ember-dark-10);
+    --icon-diff-modified-base: var(--icon-warning-base);
     --syntax-comment: var(--text-weak);
     --syntax-comment: var(--text-weak);
     --syntax-regexp: var(--text-base);
     --syntax-regexp: var(--text-base);
     --syntax-string: #00ceb9;
     --syntax-string: #00ceb9;

+ 1 - 0
packages/ui/src/theme/resolve.ts

@@ -240,6 +240,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
   tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
   tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
   tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
   tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
   tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
+  tokens["icon-diff-modified-base"] = tokens["icon-warning-base"]
 
 
   tokens["syntax-comment"] = "var(--text-weak)"
   tokens["syntax-comment"] = "var(--text-weak)"
   tokens["syntax-regexp"] = "var(--text-base)"
   tokens["syntax-regexp"] = "var(--text-base)"