diff options
| author | adamelmore <[email protected]> | 2026-01-25 06:20:44 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-25 06:20:50 -0600 |
| commit | ddc4e893598b7aeb54d8476e97332ab97c02002f (patch) | |
| tree | f546f07d05dfbfab92e76975e2c44a312dd1eefa /packages | |
| parent | a5c058e584dce24c47af5a88f6c83b69d79211d2 (diff) | |
| download | opencode-ddc4e893598b7aeb54d8476e97332ab97c02002f.tar.gz opencode-ddc4e893598b7aeb54d8476e97332ab97c02002f.zip | |
fix(app): cleanup comment component usage
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/app/src/pages/session.tsx | 114 | ||||
| -rw-r--r-- | packages/ui/src/components/line-comment.css | 61 | ||||
| -rw-r--r-- | packages/ui/src/components/line-comment.tsx | 105 | ||||
| -rw-r--r-- | packages/ui/src/components/session-review.css | 62 | ||||
| -rw-r--r-- | packages/ui/src/components/session-review.tsx | 94 |
5 files changed, 207 insertions, 229 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 38717317d..a14b6bcf6 100644 --- a/packages/app/src/pages/session.tsx +++ b/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> diff --git a/packages/ui/src/components/line-comment.css b/packages/ui/src/components/line-comment.css index 36fb14c64..9dc8eb74f 100644 --- a/packages/ui/src/components/line-comment.css +++ b/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; +} diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index 41462fa8e..f8869748c 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/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> + ) +} diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 7c77c0e35..dd75b1905 100644 --- a/packages/ui/src/components/session-review.css +++ b/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; diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 9bc7d82c1..1ae0b1a13 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/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> |
