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

fix(app): cleanup comment component usage

adamelmore 4 недель назад
Родитель
Сommit
ddc4e89359

+ 22 - 92
packages/app/src/pages/session.tsx

@@ -15,7 +15,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { useCodeComponent } from "@opencode-ai/ui/context/code"
-import { LineCommentAnchor } from "@opencode-ai/ui/line-comment"
+import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { SessionReview } from "@opencode-ai/ui/session-review"
@@ -1885,7 +1885,6 @@ export default function Page() {
                     })
 
                     let wrap: HTMLDivElement | undefined
-                    let textarea: HTMLTextAreaElement | undefined
 
                     const fileComments = createMemo(() => {
                       const p = path()
@@ -1898,7 +1897,6 @@ export default function Page() {
                     const [openedComment, setOpenedComment] = createSignal<string | null>(null)
                     const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null)
                     const [draft, setDraft] = createSignal("")
-                    const [draftError, setDraftError] = createSignal(false)
                     const [positions, setPositions] = createSignal<Record<string, number>>({})
                     const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
 
@@ -1986,7 +1984,6 @@ export default function Page() {
                       const range = commenting()
                       if (!range) return
                       setDraft("")
-                      requestAnimationFrame(() => textarea?.focus())
                     })
 
                     createEffect(() => {
@@ -2047,7 +2044,7 @@ export default function Page() {
                         />
                         <For each={fileComments()}>
                           {(comment) => (
-                            <LineCommentAnchor
+                            <LineCommentView
                               id={comment.id}
                               top={positions()[comment.id]}
                               open={openedComment() === comment.id}
@@ -2063,26 +2060,31 @@ export default function Page() {
                                 setOpenedComment((current) => (current === comment.id ? null : comment.id))
                                 file.setSelectedLines(p, comment.selection)
                               }}
-                            >
-                              <div class="flex flex-col gap-1.5">
-                                <div class="text-14-regular text-text-strong whitespace-pre-wrap">
-                                  {comment.comment}
-                                </div>
-                                <div class="text-12-medium text-text-weak whitespace-nowrap">
-                                  Comment on {commentLabel(comment.selection)}
-                                </div>
-                              </div>
-                            </LineCommentAnchor>
+                              comment={comment.comment}
+                              selection={commentLabel(comment.selection)}
+                            />
                           )}
                         </For>
                         <Show when={commenting()}>
                           {(range) => (
                             <Show when={draftTop() !== undefined}>
-                              <LineCommentAnchor
+                              <LineCommentEditor
                                 top={draftTop()}
-                                open={true}
-                                variant="editor"
-                                onClick={() => textarea?.focus()}
+                                value={draft()}
+                                selection={commentLabel(range())}
+                                onInput={setDraft}
+                                onCancel={() => setCommenting(null)}
+                                onSubmit={(comment) => {
+                                  const p = path()
+                                  if (!p) return
+                                  addCommentToContext({
+                                    file: p,
+                                    selection: range(),
+                                    comment,
+                                    origin: "file",
+                                  })
+                                  setCommenting(null)
+                                }}
                                 onPopoverFocusOut={(e) => {
                                   const target = e.relatedTarget as Node | null
                                   if (target && e.currentTarget.contains(target)) return
@@ -2093,79 +2095,7 @@ export default function Page() {
                                     }
                                   }, 0)
                                 }}
-                              >
-                                <div class="flex flex-col gap-2">
-                                  <textarea
-                                    ref={textarea}
-                                    classList={{
-                                      "w-full resize-vertical p-2 rounded-[6px] bg-surface-base text-text-strong text-12-regular leading-5 focus:outline-none": true,
-                                      "focus:shadow-xs-border-select": !draftError(),
-                                      "shadow-xs-border-critical-base": draftError(),
-                                    }}
-                                    rows={3}
-                                    placeholder="Add comment"
-                                    value={draft()}
-                                    onInput={(e) => {
-                                      setDraft(e.currentTarget.value)
-                                      setDraftError(false)
-                                    }}
-                                    onKeyDown={(e) => {
-                                      if (e.key === "Escape") {
-                                        setCommenting(null)
-                                        return
-                                      }
-                                      if (e.key !== "Enter") return
-                                      if (e.shiftKey) return
-                                      e.preventDefault()
-                                      const value = draft().trim()
-                                      if (!value) {
-                                        setDraftError(true)
-                                        return
-                                      }
-                                      const p = path()
-                                      if (!p) return
-                                      addCommentToContext({
-                                        file: p,
-                                        selection: range(),
-                                        comment: value,
-                                        origin: "file",
-                                      })
-                                      setCommenting(null)
-                                    }}
-                                  />
-                                  <div class="flex items-center gap-2">
-                                    <div class="text-12-medium text-text-weak ml-1">
-                                      Commenting on {commentLabel(range())}
-                                    </div>
-                                    <div class="flex-1" />
-                                    <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
-                                      Cancel
-                                    </Button>
-                                    <Button
-                                      size="small"
-                                      variant="primary"
-                                      onClick={() => {
-                                        const value = draft().trim()
-                                        if (!value) {
-                                          setDraftError(true)
-                                          return
-                                        }
-                                        const p = path()
-                                        if (!p) return
-                                        addCommentToContext({
-                                          file: p,
-                                          selection: range(),
-                                          comment: value,
-                                          origin: "file",
-                                        })
-                                        setCommenting(null)
-                                      }}
-                                    >
-                                      Comment
-                                    </Button>
-                                  </div>
-                                </div>
-                              </LineCommentAnchor>
+                              />
                             </Show>
                           )}
                         </Show>

+ 61 - 0
packages/ui/src/components/line-comment.css

@@ -52,3 +52,64 @@
   padding: 8px;
   border-radius: 14px;
 }
+
+[data-component="line-comment"] [data-slot="line-comment-content"] {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-text"] {
+  font-family: var(--font-family-sans);
+  font-size: var(--font-size-base);
+  font-weight: var(--font-weight-regular);
+  line-height: var(--line-height-x-large);
+  letter-spacing: var(--letter-spacing-normal);
+  color: var(--text-strong);
+  white-space: pre-wrap;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-label"],
+[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
+  font-family: var(--font-family-sans);
+  font-size: var(--font-size-small);
+  font-weight: var(--font-weight-medium);
+  line-height: var(--line-height-large);
+  letter-spacing: var(--letter-spacing-normal);
+  color: var(--text-weak);
+  white-space: nowrap;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-editor"] {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-textarea"] {
+  width: 100%;
+  resize: vertical;
+  padding: 8px;
+  border-radius: var(--radius-md);
+  background: var(--surface-base);
+  border: 1px solid var(--border-base);
+  color: var(--text-strong);
+  font-family: var(--font-family-sans);
+  font-size: var(--font-size-small);
+  line-height: var(--line-height-large);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-textarea"]:focus {
+  outline: none;
+  box-shadow: var(--shadow-xs-border-select);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-actions"] {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
+  margin-right: auto;
+}

+ 104 - 1
packages/ui/src/components/line-comment.tsx

@@ -1,4 +1,5 @@
-import { Show, type JSX } from "solid-js"
+import { onMount, Show, splitProps, type JSX } from "solid-js"
+import { Button } from "./button"
 import { Icon } from "./icon"
 
 export type LineCommentVariant = "default" | "editor"
@@ -52,3 +53,105 @@ export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
     </div>
   )
 }
+
+export type LineCommentProps = Omit<LineCommentAnchorProps, "children" | "variant"> & {
+  comment: JSX.Element
+  selection: JSX.Element
+}
+
+export const LineComment = (props: LineCommentProps) => {
+  const [split, rest] = splitProps(props, ["comment", "selection"])
+
+  return (
+    <LineCommentAnchor {...rest} variant="default">
+      <div data-slot="line-comment-content">
+        <div data-slot="line-comment-text">{split.comment}</div>
+        <div data-slot="line-comment-label">Comment on {split.selection}</div>
+      </div>
+    </LineCommentAnchor>
+  )
+}
+
+export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & {
+  value: string
+  selection: JSX.Element
+  onInput: (value: string) => void
+  onCancel: VoidFunction
+  onSubmit: (value: string) => void
+  placeholder?: string
+  rows?: number
+  autofocus?: boolean
+  cancelLabel?: string
+  submitLabel?: string
+}
+
+export const LineCommentEditor = (props: LineCommentEditorProps) => {
+  const [split, rest] = splitProps(props, [
+    "value",
+    "selection",
+    "onInput",
+    "onCancel",
+    "onSubmit",
+    "placeholder",
+    "rows",
+    "autofocus",
+    "cancelLabel",
+    "submitLabel",
+  ])
+
+  const refs = {
+    textarea: undefined as HTMLTextAreaElement | undefined,
+  }
+
+  const focus = () => refs.textarea?.focus()
+
+  const submit = () => {
+    const value = split.value.trim()
+    if (!value) return
+    split.onSubmit(value)
+  }
+
+  onMount(() => {
+    if (split.autofocus === false) return
+    requestAnimationFrame(focus)
+  })
+
+  return (
+    <LineCommentAnchor {...rest} open={true} variant="editor" onClick={() => focus()}>
+      <div data-slot="line-comment-editor">
+        <textarea
+          ref={(el) => {
+            refs.textarea = el
+          }}
+          data-slot="line-comment-textarea"
+          rows={split.rows ?? 3}
+          placeholder={split.placeholder ?? "Add comment"}
+          value={split.value}
+          onInput={(e) => split.onInput(e.currentTarget.value)}
+          onKeyDown={(e) => {
+            if (e.key === "Escape") {
+              e.preventDefault()
+              e.stopPropagation()
+              split.onCancel()
+              return
+            }
+            if (e.key !== "Enter") return
+            if (e.shiftKey) return
+            e.preventDefault()
+            e.stopPropagation()
+            submit()
+          }}
+        />
+        <div data-slot="line-comment-actions">
+          <div data-slot="line-comment-editor-label">Commenting on {split.selection}</div>
+          <Button size="small" variant="ghost" onClick={split.onCancel}>
+            {split.cancelLabel ?? "Cancel"}
+          </Button>
+          <Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
+            {split.submitLabel ?? "Comment"}
+          </Button>
+        </div>
+      </div>
+    </LineCommentAnchor>
+  )
+}

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

@@ -75,68 +75,6 @@
     overflow: hidden;
   }
 
-  [data-slot="session-review-comment-content"] {
-    display: flex;
-    flex-direction: column;
-    gap: 6px;
-  }
-
-  [data-slot="session-review-comment-text"] {
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-base);
-    font-weight: var(--font-weight-regular);
-    line-height: var(--line-height-x-large);
-    letter-spacing: var(--letter-spacing-normal);
-    color: var(--text-strong);
-    white-space: pre-wrap;
-  }
-
-  [data-slot="session-review-comment-label"],
-  [data-slot="session-review-comment-draft-label"] {
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-small);
-    font-weight: var(--font-weight-medium);
-    line-height: var(--line-height-large);
-    letter-spacing: var(--letter-spacing-normal);
-    color: var(--text-weak);
-    white-space: nowrap;
-  }
-
-  [data-slot="session-review-comment-draft"] {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-  }
-
-  [data-slot="session-review-comment-textarea"] {
-    width: 100%;
-    max-width: min(380px, calc(100vw - 48px));
-    resize: vertical;
-    padding: 8px;
-    border-radius: var(--radius-md);
-    background: var(--surface-base);
-    border: 1px solid var(--border-base);
-    color: var(--text-strong);
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-small);
-    line-height: var(--line-height-large);
-
-    &:focus {
-      outline: none;
-      box-shadow: var(--shadow-xs-border-select);
-    }
-  }
-
-  [data-slot="session-review-comment-actions"] {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-  }
-
-  [data-slot="session-review-comment-draft-label"] {
-    margin-right: auto;
-  }
-
   [data-slot="session-review-trigger-content"] {
     display: flex;
     align-items: center;

+ 20 - 74
packages/ui/src/components/session-review.tsx

@@ -4,7 +4,7 @@ import { RadioGroup } from "./radio-group"
 import { DiffChanges } from "./diff-changes"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
-import { LineCommentAnchor } from "./line-comment"
+import { LineComment, LineCommentEditor } from "./line-comment"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { useDiffComponent } from "../context/diff"
 import { useI18n } from "../context/i18n"
@@ -305,7 +305,6 @@ export const SessionReview = (props: SessionReviewProps) => {
           <For each={props.diffs}>
             {(diff) => {
               let wrapper: HTMLDivElement | undefined
-              let textarea: HTMLTextAreaElement | undefined
 
               const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
               const commentedLines = createMemo(() => comments().map((c) => c.selection))
@@ -396,7 +395,6 @@ export const SessionReview = (props: SessionReviewProps) => {
                 if (!range) return
                 setDraft("")
                 scheduleAnchors()
-                requestAnimationFrame(() => textarea?.focus())
               })
 
               createEffect(() => {
@@ -565,7 +563,7 @@ export const SessionReview = (props: SessionReviewProps) => {
 
                       <For each={comments()}>
                         {(comment) => (
-                          <LineCommentAnchor
+                          <LineComment
                             id={comment.id}
                             top={positions()[comment.id]}
                             onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
@@ -578,83 +576,31 @@ export const SessionReview = (props: SessionReviewProps) => {
                               openComment(comment)
                             }}
                             open={isCommentOpen(comment)}
-                          >
-                            <div data-slot="session-review-comment-content">
-                              <div data-slot="session-review-comment-text">{comment.comment}</div>
-                              <div data-slot="session-review-comment-label">
-                                Comment on {selectionLabel(comment.selection)}
-                              </div>
-                            </div>
-                          </LineCommentAnchor>
+                            comment={comment.comment}
+                            selection={selectionLabel(comment.selection)}
+                          />
                         )}
                       </For>
 
                       <Show when={draftRange()}>
                         {(range) => (
                           <Show when={draftTop() !== undefined}>
-                            <LineCommentAnchor
+                            <LineCommentEditor
                               top={draftTop()}
-                              onClick={() => textarea?.focus()}
-                              open={true}
-                              variant="editor"
-                            >
-                              <div data-slot="session-review-comment-draft">
-                                <textarea
-                                  ref={textarea}
-                                  data-slot="session-review-comment-textarea"
-                                  rows={3}
-                                  placeholder="Add comment"
-                                  value={draft()}
-                                  onInput={(e) => setDraft(e.currentTarget.value)}
-                                  onKeyDown={(e) => {
-                                    if (e.key === "Escape") {
-                                      e.preventDefault()
-                                      e.stopPropagation()
-                                      setCommenting(null)
-                                      return
-                                    }
-                                    if (e.key !== "Enter") return
-                                    if (e.shiftKey) return
-                                    e.preventDefault()
-                                    const value = draft().trim()
-                                    if (!value) return
-                                    props.onLineComment?.({
-                                      file: diff.file,
-                                      selection: range(),
-                                      comment: value,
-                                      preview: selectionPreview(diff, range()),
-                                    })
-                                    setCommenting(null)
-                                  }}
-                                />
-                                <div data-slot="session-review-comment-actions">
-                                  <div data-slot="session-review-comment-draft-label">
-                                    Commenting on {selectionLabel(range())}
-                                  </div>
-                                  <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
-                                    Cancel
-                                  </Button>
-                                  <Button
-                                    size="small"
-                                    variant="primary"
-                                    disabled={draft().trim().length === 0}
-                                    onClick={() => {
-                                      const value = draft().trim()
-                                      if (!value) return
-                                      props.onLineComment?.({
-                                        file: diff.file,
-                                        selection: range(),
-                                        comment: value,
-                                        preview: selectionPreview(diff, range()),
-                                      })
-                                      setCommenting(null)
-                                    }}
-                                  >
-                                    Comment
-                                  </Button>
-                                </div>
-                              </div>
-                            </LineCommentAnchor>
+                              value={draft()}
+                              selection={selectionLabel(range())}
+                              onInput={setDraft}
+                              onCancel={() => setCommenting(null)}
+                              onSubmit={(comment) => {
+                                props.onLineComment?.({
+                                  file: diff.file,
+                                  selection: range(),
+                                  comment,
+                                  preview: selectionPreview(diff, range()),
+                                })
+                                setCommenting(null)
+                              }}
+                            />
                           </Show>
                         )}
                       </Show>