summaryrefslogtreecommitdiffhomepage
path: root/packages/ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui')
-rw-r--r--packages/ui/src/components/line-comment.css49
-rw-r--r--packages/ui/src/components/line-comment.tsx53
-rw-r--r--packages/ui/src/components/session-review.css171
-rw-r--r--packages/ui/src/components/session-review.tsx141
-rw-r--r--packages/ui/src/styles/index.css1
5 files changed, 227 insertions, 188 deletions
diff --git a/packages/ui/src/components/line-comment.css b/packages/ui/src/components/line-comment.css
new file mode 100644
index 000000000..1cdf78189
--- /dev/null
+++ b/packages/ui/src/components/line-comment.css
@@ -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;
+}
diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx
new file mode 100644
index 000000000..a9e22036b
--- /dev/null
+++ b/packages/ui/src/components/line-comment.tsx
@@ -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>
+ )
+}
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 746e6a139..b61248fec 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -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;
}
}
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 6891836ff..2b6d93e2d 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -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",
+ <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)
}}
+ open={isCommentOpen(comment)}
>
- <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)
- }}
- >
- <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>
+ <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>
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index b4b0883ae..3ed0310ef 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -25,6 +25,7 @@
@import "../components/icon-button.css" layer(components);
@import "../components/image-preview.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/inline-input.css" layer(components);
@import "../components/list.css" layer(components);