fix(app): line selection fixes
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
[data-slot="session-review-comment-popover-content"] {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: -8px;
|
||||
z-index: 6;
|
||||
min-width: 200px;
|
||||
max-width: min(320px, calc(100vw - 48px));
|
||||
border-radius: 10px;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
box-shadow: var(--shadow-lg-border-base);
|
||||
padding: 12px;
|
||||
[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"] {
|
||||
@@ -217,103 +266,7 @@
|
||||
[data-slot="session-review-diff-wrapper"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[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;
|
||||
--line-comment-z: 5;
|
||||
--line-comment-popover-z: 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +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 { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useI18n } from "../context/i18n"
|
||||
@@ -559,71 +560,74 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<div
|
||||
data-slot="session-review-comment-anchor"
|
||||
data-comment-id={comment.id}
|
||||
style={{
|
||||
top: `${positions()[comment.id] ?? 0}px`,
|
||||
opacity: positions()[comment.id] === undefined ? 0 : 1,
|
||||
"pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="session-review-comment-button"
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
<LineCommentAnchor
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
>
|
||||
<Icon name="comment" size="small" />
|
||||
</button>
|
||||
<Show when={isCommentOpen(comment)}>
|
||||
<div data-slot="session-review-comment-popover-content">
|
||||
<div data-slot="session-review-comment-popover">
|
||||
<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>
|
||||
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>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</LineCommentAnchor>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<div data-slot="session-review-comment-anchor" style={{ top: `${draftTop() ?? 0}px` }}>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="session-review-comment-button"
|
||||
onClick={() => textarea?.focus()}
|
||||
>
|
||||
<Icon name="comment" size="small" />
|
||||
</button>
|
||||
<div data-slot="session-review-comment-popover-content">
|
||||
<div data-slot="session-review-comment-popover">
|
||||
<div data-slot="session-review-comment-popover-label">
|
||||
<LineCommentAnchor
|
||||
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 !== "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())}
|
||||
</div>
|
||||
<textarea
|
||||
ref={textarea}
|
||||
data-slot="session-review-comment-textarea"
|
||||
rows={3}
|
||||
placeholder="Add a comment"
|
||||
value={draft()}
|
||||
onInput={(e) => setDraft(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return
|
||||
if (e.shiftKey) return
|
||||
e.preventDefault()
|
||||
<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?.({
|
||||
@@ -634,33 +638,12 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
})
|
||||
setCommenting(null)
|
||||
}}
|
||||
/>
|
||||
<div data-slot="session-review-comment-actions">
|
||||
<Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
|
||||
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>
|
||||
>
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LineCommentAnchor>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user