diff options
| author | Adam <[email protected]> | 2026-02-19 08:44:17 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-19 09:00:43 -0600 |
| commit | 338393c0162452777ce40f4dbc75eefe4667a3e6 (patch) | |
| tree | 85ce2af729d94c999d6964baeca30bddf80784f9 /packages/ui/src/components | |
| parent | 8ebdbe0ea2bbf4b2ca7499d59ff9549d3e291557 (diff) | |
| download | opencode-338393c0162452777ce40f4dbc75eefe4667a3e6.tar.gz opencode-338393c0162452777ce40f4dbc75eefe4667a3e6.zip | |
fix(app): accordion styles
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/accordion.css | 49 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.css | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/session-review.css | 72 | ||||
| -rw-r--r-- | packages/ui/src/components/session-review.tsx | 722 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.css | 43 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 136 | ||||
| -rw-r--r-- | packages/ui/src/components/sticky-accordion-header.css | 16 |
7 files changed, 478 insertions, 561 deletions
diff --git a/packages/ui/src/components/accordion.css b/packages/ui/src/components/accordion.css index 7bf287fe5..b4d6323d0 100644 --- a/packages/ui/src/components/accordion.css +++ b/packages/ui/src/components/accordion.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; align-items: flex-start; - gap: 8px; + gap: 0px; align-self: stretch; [data-slot="accordion-item"] { @@ -11,7 +11,11 @@ flex-direction: column; align-items: flex-start; align-self: stretch; - overflow: clip; + overflow: visible; + + & + [data-slot="accordion-item"] { + margin-top: -1px; + } [data-slot="accordion-header"] { width: 100%; @@ -31,9 +35,10 @@ cursor: default; user-select: none; - background-color: var(--surface-base); + background-color: var(--background-stronger); border: 1px solid var(--border-weak-base); - border-radius: var(--radius-md); + border-radius: 0; + box-shadow: none; overflow: clip; color: var(--text-strong); transition: background-color 0.15s ease; @@ -47,7 +52,10 @@ letter-spacing: var(--letter-spacing-normal); &:hover { - background-color: var(--surface-base); + background-color: var(--surface-base-hover); + } + &:active { + background-color: var(--surface-base-active); } &:focus-visible { outline: none; @@ -58,23 +66,40 @@ } } - &[data-expanded] { - [data-slot="accordion-trigger"] { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + &:first-child { + [data-slot="accordion-header"] [data-slot="accordion-trigger"] { + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); + } + } + + &:last-child:not([data-expanded]) { + [data-slot="accordion-header"] [data-slot="accordion-trigger"] { + border-bottom-left-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); } + } + &[data-expanded] { [data-slot="accordion-content"] { border: 1px solid var(--border-weak-base); - border-top: none; - border-bottom-left-radius: var(--radius-md); - border-bottom-right-radius: var(--radius-md); + border-top: 0; + background-color: var(--background-stronger); + } + } + + &:last-child[data-expanded] { + [data-slot="accordion-content"] { + border-bottom-left-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); } } [data-slot="accordion-content"] { overflow: hidden; width: 100%; + border: 0; + background-color: transparent; } } } diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index d123847cb..1b5694682 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -1288,7 +1288,6 @@ } [data-component="apply-patch-file-diff"] { - border-top: 1px solid var(--border-weaker-base); max-height: 420px; overflow-y: auto; scrollbar-width: none; diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index bef8f4f0e..ec1698d29 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -1,7 +1,7 @@ [data-component="session-review"] { display: flex; flex-direction: column; - gap: 8px; + gap: 0px; height: 100%; overflow-y: auto; scrollbar-width: none; @@ -19,7 +19,8 @@ top: 0; z-index: 20; background-color: var(--background-stronger); - height: 32px; + height: 40px; + padding-bottom: 8px; flex-shrink: 0; display: flex; justify-content: space-between; @@ -57,70 +58,13 @@ } [data-component="sticky-accordion-header"] { - top: 40px; + --sticky-accordion-top: 40px; } - [data-component="sticky-accordion-header"][data-expanded]::before, - [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - top: -40px; - } - - [data-slot="session-review-diffs-group"] { - background-color: var(--background-stronger); - border-radius: var(--radius-lg); - border: 1px solid var(--border-weak-base); - overflow: clip; - - [data-component="accordion"] { - gap: 0; - } - - [data-component="accordion"] [data-slot="accordion-item"] { - overflow: visible; - } - - [data-component="accordion"] - [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border: 0; - border-radius: 0; - box-shadow: none; - background-color: transparent; - - &:hover { - background-color: var(--surface-base-hover); - } - - &:active { - background-color: var(--surface-base-active); - } - } - - [data-component="accordion"] - [data-slot="accordion-item"] - + [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border-top: 1px solid var(--border-weak-base); - } - - [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { - border: 0; - border-top: 1px solid var(--border-weak-base); - border-radius: 0; - } - - [data-component="sticky-accordion-header"][data-expanded]::before, - [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - top: 0; - } - - [data-slot="session-review-accordion-item"][data-selected] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - background-color: var(--surface-base-active); - } + [data-slot="session-review-accordion-item"][data-selected] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + background-color: var(--surface-base-active); } [data-slot="accordion-item"] { diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 815d8129d..fd85fb485 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -320,395 +320,393 @@ export const SessionReview = (props: SessionReviewProps) => { </div> <div data-slot="session-review-container" class={props.classes?.container}> <Show when={hasDiffs()} fallback={props.empty}> - <div data-slot="session-review-diffs-group"> - <Accordion multiple value={open()} onChange={handleChange}> - <For each={props.diffs}> - {(diff) => { - let wrapper: HTMLDivElement | undefined - - const expanded = createMemo(() => open().includes(diff.file)) - const [force, setForce] = createSignal(false) - - const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) - const commentedLines = createMemo(() => comments().map((c) => c.selection)) - - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") - const changedLines = () => diff.additions + diff.deletions - - const tooLarge = createMemo(() => { - if (!expanded()) return false - if (force()) return false - if (isImageFile(diff.file)) return false - return changedLines() > MAX_DIFF_CHANGED_LINES - }) - - const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) - const isDeleted = () => - diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) - const isImage = () => isImageFile(diff.file) - const isAudio = () => isAudioFile(diff.file) - - const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc) - const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") - - const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc) - const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") - const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined) - - const selectedLines = createMemo(() => { - const current = selection() - if (!current || current.file !== diff.file) return null - return current.range - }) - - const draftRange = createMemo(() => { - const current = commenting() - if (!current || current.file !== diff.file) return null - return current.range - }) - - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal<Record<string, number>>({}) - const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) - - const getRoot = () => { - const el = wrapper - if (!el) return - - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - return host.shadowRoot ?? undefined + <Accordion multiple value={open()} onChange={handleChange}> + <For each={props.diffs}> + {(diff) => { + let wrapper: HTMLDivElement | undefined + + const expanded = createMemo(() => open().includes(diff.file)) + const [force, setForce] = createSignal(false) + + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) + const commentedLines = createMemo(() => comments().map((c) => c.selection)) + + const beforeText = () => (typeof diff.before === "string" ? diff.before : "") + const afterText = () => (typeof diff.after === "string" ? diff.after : "") + const changedLines = () => diff.additions + diff.deletions + + const tooLarge = createMemo(() => { + if (!expanded()) return false + if (force()) return false + if (isImageFile(diff.file)) return false + return changedLines() > MAX_DIFF_CHANGED_LINES + }) + + const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) + const isDeleted = () => + diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) + const isImage = () => isImageFile(diff.file) + const isAudio = () => isAudioFile(diff.file) + + const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc) + const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") + + const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc) + const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") + const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined) + + const selectedLines = createMemo(() => { + const current = selection() + if (!current || current.file !== diff.file) return null + return current.range + }) + + const draftRange = createMemo(() => { + const current = commenting() + if (!current || current.file !== diff.file) return null + return current.range + }) + + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal<Record<string, number>>({}) + const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) + + const getRoot = () => { + const el = wrapper + if (!el) return + + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + return host.shadowRoot ?? undefined + } + + const updateAnchors = () => { + const el = wrapper + if (!el) return + + const root = getRoot() + if (!root) return + + const next: Record<string, number> = {} + for (const item of comments()) { + const marker = findMarker(root, item.selection) + if (!marker) continue + next[item.id] = markerTop(el, marker) } + setPositions(next) - const updateAnchors = () => { - const el = wrapper - if (!el) return - - const root = getRoot() - if (!root) return - - const next: Record<string, number> = {} - for (const item of comments()) { - const marker = findMarker(root, item.selection) - if (!marker) continue - next[item.id] = markerTop(el, marker) - } - setPositions(next) - - const range = draftRange() - if (!range) { - setDraftTop(undefined) - return - } - - const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) - return - } - - setDraftTop(markerTop(el, marker)) + const range = draftRange() + if (!range) { + setDraftTop(undefined) + return } - const scheduleAnchors = () => { - requestAnimationFrame(updateAnchors) + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) + return } - createEffect(() => { - comments() - scheduleAnchors() - }) - - createEffect(() => { - const range = draftRange() - if (!range) return - setDraft("") - scheduleAnchors() - }) - - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isImage()) return - if (imageSrc()) return - if (imageStatus() !== "idle") return - if (isDeleted()) return - - const reader = props.readFile - if (!reader) return - - setImageStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { - setImageStatus("error") - return - } - setImageSrc(src) - setImageStatus("idle") - }) - .catch(() => { + setDraftTop(markerTop(el, marker)) + } + + const scheduleAnchors = () => { + requestAnimationFrame(updateAnchors) + } + + createEffect(() => { + comments() + scheduleAnchors() + }) + + createEffect(() => { + const range = draftRange() + if (!range) return + setDraft("") + scheduleAnchors() + }) + + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isImage()) return + if (imageSrc()) return + if (imageStatus() !== "idle") return + if (isDeleted()) return + + const reader = props.readFile + if (!reader) return + + setImageStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { setImageStatus("error") - }) - }) - - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isAudio()) return - if (audioSrc()) return - if (audioStatus() !== "idle") return - - const reader = props.readFile - if (!reader) return - - setAudioStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { - setAudioStatus("error") - return - } - setAudioMime(normalizeMimeType(result?.mimeType)) - setAudioSrc(src) - setAudioStatus("idle") - }) - .catch(() => { + return + } + setImageSrc(src) + setImageStatus("idle") + }) + .catch(() => { + setImageStatus("error") + }) + }) + + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isAudio()) return + if (audioSrc()) return + if (audioStatus() !== "idle") return + + const reader = props.readFile + if (!reader) return + + setAudioStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { setAudioStatus("error") - }) - }) - - const handleLineSelected = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return - - if (!range) { - setSelection(null) - return - } - - setSelection({ file: diff.file, range }) + return + } + setAudioMime(normalizeMimeType(result?.mimeType)) + setAudioSrc(src) + setAudioStatus("idle") + }) + .catch(() => { + setAudioStatus("error") + }) + }) + + const handleLineSelected = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return + + if (!range) { + setSelection(null) + return } - const handleLineSelectionEnd = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + setSelection({ file: diff.file, range }) + } - if (!range) { - setCommenting(null) - return - } - - setSelection({ file: diff.file, range }) - setCommenting({ file: diff.file, range }) - } + const handleLineSelectionEnd = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - const openComment = (comment: SessionReviewComment) => { - setOpened({ file: comment.file, id: comment.id }) - setSelection({ file: comment.file, range: comment.selection }) + if (!range) { + setCommenting(null) + return } - const isCommentOpen = (comment: SessionReviewComment) => { - const current = opened() - if (!current) return false - return current.file === comment.file && current.id === comment.id - } - - return ( - <Accordion.Item - value={diff.file} - id={diffId(diff.file)} - data-file={diff.file} - data-slot="session-review-accordion-item" - data-selected={props.focusedFile === diff.file ? "" : undefined} - > - <StickyAccordionHeader> - <Accordion.Trigger> - <div data-slot="session-review-trigger-content"> - <div data-slot="session-review-file-info"> - <FileIcon node={{ path: diff.file, type: "file" }} /> - <div data-slot="session-review-file-name-container"> - <Show when={diff.file.includes("/")}> - <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span> - </Show> - <span data-slot="session-review-filename">{getFilename(diff.file)}</span> - <Show when={props.onViewFile}> - <Tooltip value="Open file" placement="top" gutter={4}> - <button - data-slot="session-review-view-button" - type="button" - aria-label="Open file" - onClick={(e) => { - e.stopPropagation() - props.onViewFile?.(diff.file) - }} - > - <Icon name="open-file" size="small" /> - </button> - </Tooltip> - </Show> - </div> - </div> - <div data-slot="session-review-trigger-actions"> - <Switch> - <Match when={isAdded()}> - <div data-slot="session-review-change-group" data-type="added"> - <span data-slot="session-review-change" data-type="added"> - {i18n.t("ui.sessionReview.change.added")} - </span> - <DiffChanges changes={diff} /> - </div> - </Match> - <Match when={isDeleted()}> - <span data-slot="session-review-change" data-type="removed"> - {i18n.t("ui.sessionReview.change.removed")} - </span> - </Match> - <Match when={isImage()}> - <span data-slot="session-review-change" data-type="modified"> - {i18n.t("ui.sessionReview.change.modified")} - </span> - </Match> - <Match when={true}> - <DiffChanges changes={diff} /> - </Match> - </Switch> - <span data-slot="session-review-diff-chevron"> - <Icon name="chevron-down" size="small" /> - </span> + setSelection({ file: diff.file, range }) + setCommenting({ file: diff.file, range }) + } + + const openComment = (comment: SessionReviewComment) => { + setOpened({ file: comment.file, id: comment.id }) + setSelection({ file: comment.file, range: comment.selection }) + } + + const isCommentOpen = (comment: SessionReviewComment) => { + const current = opened() + if (!current) return false + return current.file === comment.file && current.id === comment.id + } + + return ( + <Accordion.Item + value={diff.file} + id={diffId(diff.file)} + data-file={diff.file} + data-slot="session-review-accordion-item" + data-selected={props.focusedFile === diff.file ? "" : undefined} + > + <StickyAccordionHeader> + <Accordion.Trigger> + <div data-slot="session-review-trigger-content"> + <div data-slot="session-review-file-info"> + <FileIcon node={{ path: diff.file, type: "file" }} /> + <div data-slot="session-review-file-name-container"> + <Show when={diff.file.includes("/")}> + <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span> + </Show> + <span data-slot="session-review-filename">{getFilename(diff.file)}</span> + <Show when={props.onViewFile}> + <Tooltip value="Open file" placement="top" gutter={4}> + <button + data-slot="session-review-view-button" + type="button" + aria-label="Open file" + onClick={(e) => { + e.stopPropagation() + props.onViewFile?.(diff.file) + }} + > + <Icon name="open-file" size="small" /> + </button> + </Tooltip> + </Show> </div> </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content data-slot="session-review-accordion-content"> - <div - data-slot="session-review-diff-wrapper" - ref={(el) => { - wrapper = el - anchors.set(diff.file, el) - scheduleAnchors() - }} - > - <Show when={expanded()}> + <div data-slot="session-review-trigger-actions"> <Switch> - <Match when={isImage() && imageSrc()}> - <div data-slot="session-review-image-container"> - <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} /> - </div> - </Match> - <Match when={isImage() && isDeleted()}> - <div data-slot="session-review-image-container" data-removed> - <span data-slot="session-review-image-placeholder"> - {i18n.t("ui.sessionReview.change.removed")} + <Match when={isAdded()}> + <div data-slot="session-review-change-group" data-type="added"> + <span data-slot="session-review-change" data-type="added"> + {i18n.t("ui.sessionReview.change.added")} </span> + <DiffChanges changes={diff} /> </div> </Match> - <Match when={isImage() && !imageSrc()}> - <div data-slot="session-review-image-container"> - <span data-slot="session-review-image-placeholder"> - {imageStatus() === "loading" - ? i18n.t("ui.sessionReview.image.loading") - : i18n.t("ui.sessionReview.image.placeholder")} - </span> - </div> + <Match when={isDeleted()}> + <span data-slot="session-review-change" data-type="removed"> + {i18n.t("ui.sessionReview.change.removed")} + </span> </Match> - <Match when={!isImage() && tooLarge()}> - <div data-slot="session-review-large-diff"> - <div data-slot="session-review-large-diff-title"> - {i18n.t("ui.sessionReview.largeDiff.title")} - </div> - <div data-slot="session-review-large-diff-meta"> - {i18n.t("ui.sessionReview.largeDiff.meta", { - limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), - current: changedLines().toLocaleString(), - })} - </div> - <div data-slot="session-review-large-diff-actions"> - <Button size="normal" variant="secondary" onClick={() => setForce(true)}> - {i18n.t("ui.sessionReview.largeDiff.renderAnyway")} - </Button> - </div> - </div> + <Match when={isImage()}> + <span data-slot="session-review-change" data-type="modified"> + {i18n.t("ui.sessionReview.change.modified")} + </span> </Match> - <Match when={!isImage()}> - <Dynamic - component={diffComponent} - preloadedDiff={diff.preloaded} - diffStyle={diffStyle()} - onRendered={() => { - props.onDiffRendered?.() - scheduleAnchors() - }} - enableLineSelection={props.onLineComment != null} - onLineSelected={handleLineSelected} - onLineSelectionEnd={handleLineSelectionEnd} - selectedLines={selectedLines()} - commentedLines={commentedLines()} - before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", - }} - /> + <Match when={true}> + <DiffChanges changes={diff} /> </Match> </Switch> - - <For each={comments()}> - {(comment) => ( - <LineComment - id={comment.id} - top={positions()[comment.id]} - onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })} - onClick={() => { - if (isCommentOpen(comment)) { - setOpened(null) - return - } - - openComment(comment) + <span data-slot="session-review-diff-chevron"> + <Icon name="chevron-down" size="small" /> + </span> + </div> + </div> + </Accordion.Trigger> + </StickyAccordionHeader> + <Accordion.Content data-slot="session-review-accordion-content"> + <div + data-slot="session-review-diff-wrapper" + ref={(el) => { + wrapper = el + anchors.set(diff.file, el) + scheduleAnchors() + }} + > + <Show when={expanded()}> + <Switch> + <Match when={isImage() && imageSrc()}> + <div data-slot="session-review-image-container"> + <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} /> + </div> + </Match> + <Match when={isImage() && isDeleted()}> + <div data-slot="session-review-image-container" data-removed> + <span data-slot="session-review-image-placeholder"> + {i18n.t("ui.sessionReview.change.removed")} + </span> + </div> + </Match> + <Match when={isImage() && !imageSrc()}> + <div data-slot="session-review-image-container"> + <span data-slot="session-review-image-placeholder"> + {imageStatus() === "loading" + ? i18n.t("ui.sessionReview.image.loading") + : i18n.t("ui.sessionReview.image.placeholder")} + </span> + </div> + </Match> + <Match when={!isImage() && tooLarge()}> + <div data-slot="session-review-large-diff"> + <div data-slot="session-review-large-diff-title"> + {i18n.t("ui.sessionReview.largeDiff.title")} + </div> + <div data-slot="session-review-large-diff-meta"> + {i18n.t("ui.sessionReview.largeDiff.meta", { + limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), + current: changedLines().toLocaleString(), + })} + </div> + <div data-slot="session-review-large-diff-actions"> + <Button size="normal" variant="secondary" onClick={() => setForce(true)}> + {i18n.t("ui.sessionReview.largeDiff.renderAnyway")} + </Button> + </div> + </div> + </Match> + <Match when={!isImage()}> + <Dynamic + component={diffComponent} + preloadedDiff={diff.preloaded} + diffStyle={diffStyle()} + onRendered={() => { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", + }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", + }} + /> + </Match> + </Switch> + + <For each={comments()}> + {(comment) => ( + <LineComment + 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)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} + /> + )} + </For> + + <Show when={draftRange()}> + {(range) => ( + <Show when={draftTop() !== undefined}> + <LineCommentEditor + top={draftTop()} + 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) }} - open={isCommentOpen(comment)} - comment={comment.comment} - selection={selectionLabel(comment.selection)} /> - )} - </For> - - <Show when={draftRange()}> - {(range) => ( - <Show when={draftTop() !== undefined}> - <LineCommentEditor - top={draftTop()} - 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> + </Show> + )} </Show> - </div> - </Accordion.Content> - </Accordion.Item> - ) - }} - </For> - </Accordion> - </div> + </Show> + </div> + </Accordion.Content> + </Accordion.Item> + ) + }} + </For> + </Accordion> </Show> </div> </div> diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index f952f6aad..902c85a8b 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -129,49 +129,6 @@ flex-direction: column; } - [data-slot="session-turn-diffs-group"] { - background-color: var(--background-stronger); - border-radius: var(--radius-lg); - border: 1px solid var(--border-weak-base); - overflow: clip; - - [data-component="accordion"] { - gap: 0; - } - - [data-component="accordion"] - [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border: 0; - border-radius: 0; - box-shadow: none; - background-color: transparent; - - &:hover { - background-color: var(--surface-base-hover); - } - - &:active { - background-color: var(--surface-base-active); - } - } - - [data-component="accordion"] - [data-slot="accordion-item"] - + [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border-top: 1px solid var(--border-weak-base); - } - - [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { - border: 0; - border-top: 1px solid var(--border-weak-base); - border-radius: 0; - } - } - [data-slot="session-turn-diff-trigger"] { display: flex; align-items: center; diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index e0f934cd5..046312738 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -315,78 +315,76 @@ export function SessionTurn( <Collapsible.Content> <Show when={open()}> <div data-component="session-turn-diffs-content"> - <div data-slot="session-turn-diffs-group"> - <Accordion - multiple - value={expanded()} - onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} - > - <For each={diffs()}> - {(diff) => { - const active = createMemo(() => expanded().includes(diff.file)) - const [visible, setVisible] = createSignal(false) - - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } - - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) - - return ( - <Accordion.Item value={diff.file}> - <Accordion.Header> - <Accordion.Trigger> - <div data-slot="session-turn-diff-trigger"> - <span data-slot="session-turn-diff-path"> - <Show when={diff.file.includes("/")}> - <span data-slot="session-turn-diff-directory"> - {`\u202A${getDirectory(diff.file)}\u202C`} - </span> - </Show> - <span data-slot="session-turn-diff-filename"> - {getFilename(diff.file)} + <Accordion + multiple + value={expanded()} + onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + <For each={diffs()}> + {(diff) => { + const active = createMemo(() => expanded().includes(diff.file)) + const [visible, setVisible] = createSignal(false) + + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) + + return ( + <Accordion.Item value={diff.file}> + <Accordion.Header> + <Accordion.Trigger> + <div data-slot="session-turn-diff-trigger"> + <span data-slot="session-turn-diff-path"> + <Show when={diff.file.includes("/")}> + <span data-slot="session-turn-diff-directory"> + {`\u202A${getDirectory(diff.file)}\u202C`} </span> + </Show> + <span data-slot="session-turn-diff-filename"> + {getFilename(diff.file)} + </span> + </span> + <div data-slot="session-turn-diff-meta"> + <span data-slot="session-turn-diff-changes"> + <DiffChanges changes={diff} /> + </span> + <span data-slot="session-turn-diff-chevron"> + <Icon name="chevron-down" size="small" /> </span> - <div data-slot="session-turn-diff-meta"> - <span data-slot="session-turn-diff-changes"> - <DiffChanges changes={diff} /> - </span> - <span data-slot="session-turn-diff-chevron"> - <Icon name="chevron-down" size="small" /> - </span> - </div> - </div> - </Accordion.Trigger> - </Accordion.Header> - <Accordion.Content> - <Show when={visible()}> - <div data-slot="session-turn-diff-view" data-scrollable> - <Dynamic - component={diffComponent} - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - /> </div> - </Show> - </Accordion.Content> - </Accordion.Item> - ) - }} - </For> - </Accordion> - </div> + </div> + </Accordion.Trigger> + </Accordion.Header> + <Accordion.Content> + <Show when={visible()}> + <div data-slot="session-turn-diff-view" data-scrollable> + <Dynamic + component={diffComponent} + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + /> + </div> + </Show> + </Accordion.Content> + </Accordion.Item> + ) + }} + </For> + </Accordion> </div> </Show> </Collapsible.Content> diff --git a/packages/ui/src/components/sticky-accordion-header.css b/packages/ui/src/components/sticky-accordion-header.css index bee8ea78f..d24c5eba6 100644 --- a/packages/ui/src/components/sticky-accordion-header.css +++ b/packages/ui/src/components/sticky-accordion-header.css @@ -1,18 +1,14 @@ [data-component="sticky-accordion-header"] { + --sticky-accordion-top: 0px; position: sticky; - top: 0px; + top: var(--sticky-accordion-top); +} + +[data-slot="accordion-item"]:first-child [data-component="sticky-accordion-header"] { + background-color: var(--background-base); } [data-component="sticky-accordion-header"][data-expanded], [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] { z-index: 10; } - -[data-component="sticky-accordion-header"][data-expanded]::before, -[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - content: ""; - z-index: -10; - position: absolute; - inset: 0; - background-color: var(--background-stronger); -} |
