fix(app): line selection fixes
This commit is contained in:
@@ -170,6 +170,36 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
|
const commentInReview = (path: string) => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return false
|
||||||
|
|
||||||
|
const diffs = sync.data.session_diff[sessionID]
|
||||||
|
if (!diffs) return false
|
||||||
|
return diffs.some((diff) => diff.file === path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => {
|
||||||
|
if (!item.commentID) return
|
||||||
|
|
||||||
|
comments.setFocus({ file: item.path, id: item.commentID })
|
||||||
|
view().reviewPanel.open()
|
||||||
|
|
||||||
|
if (item.commentOrigin === "review") {
|
||||||
|
tabs().open("review")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.commentOrigin !== "file" && commentInReview(item.path)) {
|
||||||
|
tabs().open("review")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = files.tab(item.path)
|
||||||
|
tabs().open(tab)
|
||||||
|
files.load(item.path)
|
||||||
|
}
|
||||||
|
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
const all = tabs().all()
|
const all = tabs().all()
|
||||||
const active = tabs().active()
|
const active = tabs().active()
|
||||||
@@ -1481,6 +1511,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
selection: item.selection,
|
selection: item.selection,
|
||||||
comment: item.comment,
|
comment: item.comment,
|
||||||
commentID: item.commentID,
|
commentID: item.commentID,
|
||||||
|
commentOrigin: item.commentOrigin,
|
||||||
preview: item.preview,
|
preview: item.preview,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1547,6 +1578,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
selection: item.selection,
|
selection: item.selection,
|
||||||
comment: item.comment,
|
comment: item.comment,
|
||||||
commentID: item.commentID,
|
commentID: item.commentID,
|
||||||
|
commentOrigin: item.commentOrigin,
|
||||||
preview: item.preview,
|
preview: item.preview,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1700,10 +1732,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
"cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID,
|
"cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!item.commentID) return
|
openComment(item)
|
||||||
comments.setFocus({ file: item.path, id: item.commentID })
|
|
||||||
view().reviewPanel.open()
|
|
||||||
tabs().open("review")
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type FileContextItem = {
|
|||||||
selection?: FileSelection
|
selection?: FileSelection
|
||||||
comment?: string
|
comment?: string
|
||||||
commentID?: string
|
commentID?: string
|
||||||
|
commentOrigin?: "review" | "file"
|
||||||
preview?: string
|
preview?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
|
import { LineCommentAnchor } from "@opencode-ai/ui/line-comment"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
@@ -535,6 +536,7 @@ export default function Page() {
|
|||||||
selection: SelectedLineRange
|
selection: SelectedLineRange
|
||||||
comment: string
|
comment: string
|
||||||
preview?: string
|
preview?: string
|
||||||
|
origin?: "review" | "file"
|
||||||
}) => {
|
}) => {
|
||||||
const selection = selectionFromLines(input.selection)
|
const selection = selectionFromLines(input.selection)
|
||||||
const preview = input.preview ?? selectionPreview(input.file, selection)
|
const preview = input.preview ?? selectionPreview(input.file, selection)
|
||||||
@@ -549,6 +551,7 @@ export default function Page() {
|
|||||||
selection,
|
selection,
|
||||||
comment: input.comment,
|
comment: input.comment,
|
||||||
commentID: saved.id,
|
commentID: saved.id,
|
||||||
|
commentOrigin: input.origin,
|
||||||
preview,
|
preview,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1463,7 +1466,7 @@ export default function Page() {
|
|||||||
diffs={diffs}
|
diffs={diffs}
|
||||||
view={view}
|
view={view}
|
||||||
diffStyle="unified"
|
diffStyle="unified"
|
||||||
onLineComment={addCommentToContext}
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
comments={comments.all()}
|
comments={comments.all()}
|
||||||
focusedComment={comments.focus()}
|
focusedComment={comments.focus()}
|
||||||
onFocusedCommentChange={comments.setFocus}
|
onFocusedCommentChange={comments.setFocus}
|
||||||
@@ -1782,7 +1785,7 @@ export default function Page() {
|
|||||||
view={view}
|
view={view}
|
||||||
diffStyle={layout.review.diffStyle()}
|
diffStyle={layout.review.diffStyle()}
|
||||||
onDiffStyleChange={layout.review.setDiffStyle}
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
onLineComment={addCommentToContext}
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
comments={comments.all()}
|
comments={comments.all()}
|
||||||
focusedComment={comments.focus()}
|
focusedComment={comments.focus()}
|
||||||
onFocusedCommentChange={comments.setFocus}
|
onFocusedCommentChange={comments.setFocus}
|
||||||
@@ -1974,6 +1977,22 @@ export default function Page() {
|
|||||||
requestAnimationFrame(() => textarea?.focus())
|
requestAnimationFrame(() => textarea?.focus())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const focus = comments.focus()
|
||||||
|
const p = path()
|
||||||
|
if (!focus || !p) return
|
||||||
|
if (focus.file !== p) return
|
||||||
|
if (activeTab() !== tab) return
|
||||||
|
|
||||||
|
const target = fileComments().find((comment) => comment.id === focus.id)
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
setOpenedComment(target.id)
|
||||||
|
setCommenting(null)
|
||||||
|
file.setSelectedLines(p, target.selection)
|
||||||
|
requestAnimationFrame(() => comments.clearFocus())
|
||||||
|
})
|
||||||
|
|
||||||
const renderCode = (source: string, wrapperClass: string) => (
|
const renderCode = (source: string, wrapperClass: string) => (
|
||||||
<div
|
<div
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -2016,125 +2035,109 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
<For each={fileComments()}>
|
<For each={fileComments()}>
|
||||||
{(comment) => (
|
{(comment) => (
|
||||||
<div
|
<LineCommentAnchor
|
||||||
class="absolute right-6 z-30"
|
id={comment.id}
|
||||||
style={{
|
top={positions()[comment.id]}
|
||||||
top: `${positions()[comment.id] ?? 0}px`,
|
open={openedComment() === comment.id}
|
||||||
opacity: positions()[comment.id] === undefined ? 0 : 1,
|
onMouseEnter={() => {
|
||||||
"pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
file.setSelectedLines(p, comment.selection)
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
setCommenting(null)
|
||||||
|
setOpenedComment((current) => (current === comment.id ? null : comment.id))
|
||||||
|
file.setSelectedLines(p, comment.selection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<div class="flex flex-col gap-1.5">
|
||||||
type="button"
|
<div class="text-14-regular text-text-strong whitespace-pre-wrap">
|
||||||
class="size-5 rounded-md flex items-center justify-center shadow-xs focus:outline-none focus-visible:shadow-xs-border-focus"
|
{comment.comment}
|
||||||
style={{
|
|
||||||
background: "var(--icon-interactive-base)",
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
const p = path()
|
|
||||||
if (!p) return
|
|
||||||
file.setSelectedLines(p, comment.selection)
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
const p = path()
|
|
||||||
if (!p) return
|
|
||||||
setCommenting(null)
|
|
||||||
setOpenedComment((current) => (current === comment.id ? null : comment.id))
|
|
||||||
file.setSelectedLines(p, comment.selection)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="comment" size="small" style={{ color: "var(--white)" }} />
|
|
||||||
</button>
|
|
||||||
<Show when={openedComment() === comment.id}>
|
|
||||||
<div class="absolute top-[calc(100%+4px)] right-[-8px] z-40 min-w-[200px] max-w-[320px] rounded-[14px] bg-surface-raised-stronger-non-alpha shadow-lg-border-base p-3">
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<div class="text-12-medium text-text-weak whitespace-nowrap">
|
||||||
</div>
|
Comment on {commentLabel(comment.selection)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LineCommentAnchor>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={commenting()}>
|
<Show when={commenting()}>
|
||||||
{(range) => (
|
{(range) => (
|
||||||
<Show when={draftTop() !== undefined}>
|
<Show when={draftTop() !== undefined}>
|
||||||
<div class="absolute right-6 z-30" style={{ top: `${draftTop() ?? 0}px` }}>
|
<LineCommentAnchor
|
||||||
<button
|
top={draftTop()}
|
||||||
type="button"
|
open={true}
|
||||||
class="size-5 rounded-md flex items-center justify-center shadow-xs focus:outline-none focus-visible:shadow-xs-border-focus"
|
variant="editor"
|
||||||
style={{
|
onClick={() => textarea?.focus()}
|
||||||
background: "var(--icon-interactive-base)",
|
onPopoverFocusOut={(e) => {
|
||||||
color: "var(--white)",
|
const target = e.relatedTarget as Node | null
|
||||||
}}
|
if (!target || !e.currentTarget.contains(target)) {
|
||||||
onClick={() => textarea?.focus()}
|
setCommenting(null)
|
||||||
>
|
}
|
||||||
<Icon name="comment" size="small" style={{ color: "var(--white)" }} />
|
}}
|
||||||
</button>
|
>
|
||||||
<div
|
<div class="flex flex-col gap-2">
|
||||||
class="absolute top-[calc(100%+4px)] right-[-8px] z-40 w-[380px] rounded-[14px] bg-surface-raised-stronger-non-alpha shadow-lg-border-base p-2"
|
<textarea
|
||||||
onFocusOut={(e) => {
|
ref={textarea}
|
||||||
const target = e.relatedTarget as Node | null
|
class="w-full resize-vertical p-2 rounded-[6px] bg-surface-base border border-border-base text-text-strong text-12-regular leading-5 focus:outline-none focus:shadow-xs-border-select"
|
||||||
if (!target || !e.currentTarget.contains(target)) {
|
rows={3}
|
||||||
|
placeholder="Add comment"
|
||||||
|
value={draft()}
|
||||||
|
onInput={(e) => setDraft(e.currentTarget.value)}
|
||||||
|
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) return
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
addCommentToContext({
|
||||||
|
file: p,
|
||||||
|
selection: range(),
|
||||||
|
comment: value,
|
||||||
|
origin: "file",
|
||||||
|
})
|
||||||
setCommenting(null)
|
setCommenting(null)
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
>
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="text-12-medium text-text-weak ml-1">
|
||||||
<textarea
|
Commenting on {commentLabel(range())}
|
||||||
ref={textarea}
|
</div>
|
||||||
class="w-full resize-vertical p-2 rounded-[6px] bg-surface-base border border-border-base text-text-strong text-12-regular leading-5 focus:outline-none focus:shadow-xs-border-select"
|
<div class="flex-1" />
|
||||||
rows={3}
|
<Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
|
||||||
placeholder="Add comment"
|
Cancel
|
||||||
value={draft()}
|
</Button>
|
||||||
onInput={(e) => setDraft(e.currentTarget.value)}
|
<Button
|
||||||
onKeyDown={(e) => {
|
size="small"
|
||||||
if (e.key === "Escape") {
|
variant="primary"
|
||||||
setCommenting(null)
|
disabled={draft().trim().length === 0}
|
||||||
return
|
onClick={() => {
|
||||||
}
|
|
||||||
if (e.key !== "Enter") return
|
|
||||||
if (e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
const value = draft().trim()
|
const value = draft().trim()
|
||||||
if (!value) return
|
if (!value) return
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
addCommentToContext({ file: p, selection: range(), comment: value })
|
addCommentToContext({
|
||||||
|
file: p,
|
||||||
|
selection: range(),
|
||||||
|
comment: value,
|
||||||
|
origin: "file",
|
||||||
|
})
|
||||||
setCommenting(null)
|
setCommenting(null)
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<div class="flex items-center gap-2">
|
Comment
|
||||||
<div class="text-12-medium text-text-weak ml-1">
|
</Button>
|
||||||
Commenting on {commentLabel(range())}
|
|
||||||
</div>
|
|
||||||
<div class="flex-1" />
|
|
||||||
<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
|
|
||||||
const p = path()
|
|
||||||
if (!p) return
|
|
||||||
addCommentToContext({ file: p, selection: range(), comment: value })
|
|
||||||
setCommenting(null)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Comment
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LineCommentAnchor>
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
49
packages/ui/src/components/line-comment.css
Normal file
49
packages/ui/src/components/line-comment.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
[data-component="line-comment"] {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
z-index: var(--line-comment-z, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"] [data-slot="line-comment-button"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--icon-interactive-base);
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"] [data-component="icon"] {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"] [data-slot="line-comment-button"]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"] [data-slot="line-comment-button"]:focus-visible {
|
||||||
|
box-shadow: var(--shadow-xs-border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"] [data-slot="line-comment-popover"] {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: -8px;
|
||||||
|
z-index: var(--line-comment-popover-z, 40);
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: min(320px, calc(100vw - 48px));
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface-raised-stronger-non-alpha);
|
||||||
|
box-shadow: var(--shadow-lg-border-base);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="line-comment"][data-variant="editor"] [data-slot="line-comment-popover"] {
|
||||||
|
width: 380px;
|
||||||
|
max-width: min(380px, calc(100vw - 48px));
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
53
packages/ui/src/components/line-comment.tsx
Normal file
53
packages/ui/src/components/line-comment.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Show, type JSX } from "solid-js"
|
||||||
|
import { Icon } from "./icon"
|
||||||
|
|
||||||
|
export type LineCommentVariant = "default" | "editor"
|
||||||
|
|
||||||
|
export type LineCommentAnchorProps = {
|
||||||
|
id?: string
|
||||||
|
top?: number
|
||||||
|
open: boolean
|
||||||
|
variant?: LineCommentVariant
|
||||||
|
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
||||||
|
onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
||||||
|
onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
|
||||||
|
class?: string
|
||||||
|
popoverClass?: string
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
|
||||||
|
const hidden = () => props.top === undefined
|
||||||
|
const variant = () => props.variant ?? "default"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-component="line-comment"
|
||||||
|
data-variant={variant()}
|
||||||
|
data-comment-id={props.id}
|
||||||
|
classList={{
|
||||||
|
[props.class ?? ""]: !!props.class,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
top: `${props.top ?? 0}px`,
|
||||||
|
opacity: hidden() ? 0 : 1,
|
||||||
|
"pointer-events": hidden() ? "none" : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button" data-slot="line-comment-button" onClick={props.onClick} onMouseEnter={props.onMouseEnter}>
|
||||||
|
<Icon name="comment" size="small" />
|
||||||
|
</button>
|
||||||
|
<Show when={props.open}>
|
||||||
|
<div
|
||||||
|
data-slot="line-comment-popover"
|
||||||
|
classList={{
|
||||||
|
[props.popoverClass ?? ""]: !!props.popoverClass,
|
||||||
|
}}
|
||||||
|
onFocusOut={props.onPopoverFocusOut}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -75,17 +75,66 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-review-comment-popover-content"] {
|
[data-slot="session-review-comment-content"] {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: calc(100% + 4px);
|
flex-direction: column;
|
||||||
right: -8px;
|
gap: 6px;
|
||||||
z-index: 6;
|
}
|
||||||
min-width: 200px;
|
|
||||||
max-width: min(320px, calc(100vw - 48px));
|
[data-slot="session-review-comment-text"] {
|
||||||
border-radius: 10px;
|
font-family: var(--font-family-sans);
|
||||||
background-color: var(--surface-raised-stronger-non-alpha);
|
font-size: var(--font-size-base);
|
||||||
box-shadow: var(--shadow-lg-border-base);
|
font-weight: var(--font-weight-regular);
|
||||||
padding: 12px;
|
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"] {
|
[data-slot="session-review-trigger-content"] {
|
||||||
@@ -217,103 +266,7 @@
|
|||||||
[data-slot="session-review-diff-wrapper"] {
|
[data-slot="session-review-diff-wrapper"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
--line-comment-z: 5;
|
||||||
|
--line-comment-popover-z: 6;
|
||||||
[data-slot="session-review-comment-anchor"] {
|
|
||||||
position: absolute;
|
|
||||||
right: 12px;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-button"] {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--icon-interactive-base);
|
|
||||||
box-shadow: var(--shadow-xs);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
[data-slot="icon-svg"] {
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
box-shadow: var(--shadow-xs-border-focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-hover"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-popover"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-hover-label"],
|
|
||||||
[data-slot="session-review-comment-popover-label"] {
|
|
||||||
font-family: var(--font-family-sans);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--text-weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-hover-text"],
|
|
||||||
[data-slot="session-review-comment-popover-text"] {
|
|
||||||
font-family: var(--font-family-sans);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-regular);
|
|
||||||
color: var(--text-strong);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-preview"] {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--surface-base);
|
|
||||||
border: 1px solid color-mix(in oklch, var(--border-base) 55%, transparent);
|
|
||||||
color: var(--text-base);
|
|
||||||
font-family: var(--font-family-mono);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-textarea"] {
|
|
||||||
width: 320px;
|
|
||||||
max-width: calc(100vw - 48px);
|
|
||||||
resize: vertical;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--surface-base);
|
|
||||||
border: 1px solid color-mix(in oklch, var(--border-base) 55%, transparent);
|
|
||||||
color: var(--text-strong);
|
|
||||||
font-family: var(--font-family-sans);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.4;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: var(--shadow-xs-border-focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-review-comment-actions"] {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { RadioGroup } from "./radio-group"
|
|||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { FileIcon } from "./file-icon"
|
import { FileIcon } from "./file-icon"
|
||||||
import { Icon } from "./icon"
|
import { Icon } from "./icon"
|
||||||
|
import { LineCommentAnchor } from "./line-comment"
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||||
import { useDiffComponent } from "../context/diff"
|
import { useDiffComponent } from "../context/diff"
|
||||||
import { useI18n } from "../context/i18n"
|
import { useI18n } from "../context/i18n"
|
||||||
@@ -559,71 +560,74 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
|
|
||||||
<For each={comments()}>
|
<For each={comments()}>
|
||||||
{(comment) => (
|
{(comment) => (
|
||||||
<div
|
<LineCommentAnchor
|
||||||
data-slot="session-review-comment-anchor"
|
id={comment.id}
|
||||||
data-comment-id={comment.id}
|
top={positions()[comment.id]}
|
||||||
style={{
|
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||||
top: `${positions()[comment.id] ?? 0}px`,
|
onClick={() => {
|
||||||
opacity: positions()[comment.id] === undefined ? 0 : 1,
|
if (isCommentOpen(comment)) {
|
||||||
"pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
|
setOpened(null)
|
||||||
}}
|
return
|
||||||
>
|
}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-slot="session-review-comment-button"
|
|
||||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
|
||||||
onClick={() => {
|
|
||||||
if (isCommentOpen(comment)) {
|
|
||||||
setOpened(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
openComment(comment)
|
openComment(comment)
|
||||||
}}
|
}}
|
||||||
>
|
open={isCommentOpen(comment)}
|
||||||
<Icon name="comment" size="small" />
|
>
|
||||||
</button>
|
<div data-slot="session-review-comment-content">
|
||||||
<Show when={isCommentOpen(comment)}>
|
<div data-slot="session-review-comment-text">{comment.comment}</div>
|
||||||
<div data-slot="session-review-comment-popover-content">
|
<div data-slot="session-review-comment-label">
|
||||||
<div data-slot="session-review-comment-popover">
|
Comment on {selectionLabel(comment.selection)}
|
||||||
<div data-slot="session-review-comment-popover-text">{comment.comment}</div>
|
|
||||||
<div data-slot="session-review-comment-popover-label">
|
|
||||||
Comment on {selectionLabel(comment.selection)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</LineCommentAnchor>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show when={draftRange()}>
|
<Show when={draftRange()}>
|
||||||
{(range) => (
|
{(range) => (
|
||||||
<Show when={draftTop() !== undefined}>
|
<Show when={draftTop() !== undefined}>
|
||||||
<div data-slot="session-review-comment-anchor" style={{ top: `${draftTop() ?? 0}px` }}>
|
<LineCommentAnchor
|
||||||
<button
|
top={draftTop()}
|
||||||
type="button"
|
onClick={() => textarea?.focus()}
|
||||||
data-slot="session-review-comment-button"
|
open={true}
|
||||||
onClick={() => textarea?.focus()}
|
variant="editor"
|
||||||
>
|
>
|
||||||
<Icon name="comment" size="small" />
|
<div data-slot="session-review-comment-draft">
|
||||||
</button>
|
<textarea
|
||||||
<div data-slot="session-review-comment-popover-content">
|
ref={textarea}
|
||||||
<div data-slot="session-review-comment-popover">
|
data-slot="session-review-comment-textarea"
|
||||||
<div data-slot="session-review-comment-popover-label">
|
rows={3}
|
||||||
|
placeholder="Add comment"
|
||||||
|
value={draft()}
|
||||||
|
onInput={(e) => setDraft(e.currentTarget.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
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 {getFilename(diff.file)}:{selectionLabel(range())}
|
Commenting on {getFilename(diff.file)}:{selectionLabel(range())}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
|
||||||
ref={textarea}
|
Cancel
|
||||||
data-slot="session-review-comment-textarea"
|
</Button>
|
||||||
rows={3}
|
<Button
|
||||||
placeholder="Add a comment"
|
size="small"
|
||||||
value={draft()}
|
variant="primary"
|
||||||
onInput={(e) => setDraft(e.currentTarget.value)}
|
disabled={draft().trim().length === 0}
|
||||||
onKeyDown={(e) => {
|
onClick={() => {
|
||||||
if (e.key !== "Enter") return
|
|
||||||
if (e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
const value = draft().trim()
|
const value = draft().trim()
|
||||||
if (!value) return
|
if (!value) return
|
||||||
props.onLineComment?.({
|
props.onLineComment?.({
|
||||||
@@ -634,33 +638,12 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
})
|
})
|
||||||
setCommenting(null)
|
setCommenting(null)
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<div data-slot="session-review-comment-actions">
|
Comment
|
||||||
<Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
|
</Button>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LineCommentAnchor>
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
@import "../components/icon-button.css" layer(components);
|
@import "../components/icon-button.css" layer(components);
|
||||||
@import "../components/image-preview.css" layer(components);
|
@import "../components/image-preview.css" layer(components);
|
||||||
@import "../components/keybind.css" layer(components);
|
@import "../components/keybind.css" layer(components);
|
||||||
|
@import "../components/line-comment.css" layer(components);
|
||||||
@import "../components/text-field.css" layer(components);
|
@import "../components/text-field.css" layer(components);
|
||||||
@import "../components/inline-input.css" layer(components);
|
@import "../components/inline-input.css" layer(components);
|
||||||
@import "../components/list.css" layer(components);
|
@import "../components/list.css" layer(components);
|
||||||
|
|||||||
Reference in New Issue
Block a user