ui: move session review bottom padding

Remove bottom padding from the scroll wrapper and apply it to the accordion content instead.
This commit is contained in:
David Hill
2026-03-02 16:19:35 +00:00
parent 633a3ba03a
commit 0a3a3216db
2 changed files with 253 additions and 251 deletions

View File

@@ -176,7 +176,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
open={props.view().review.open()} open={props.view().review.open()}
onOpenChange={props.view().review.setOpen} onOpenChange={props.view().review.setOpen}
classes={{ classes={{
root: props.classes?.root ?? "pb-6 pr-3", root: props.classes?.root ?? "pr-3",
header: props.classes?.header ?? "px-3", header: props.classes?.header ?? "px-3",
container: props.classes?.container ?? "pl-3", container: props.classes?.container ?? "pl-3",
}} }}

View File

@@ -621,279 +621,281 @@ export const SessionReview = (props: SessionReviewProps) => {
<div data-slot="session-review-container" class={props.classes?.container}> <div data-slot="session-review-container" class={props.classes?.container}>
<Show when={hasDiffs()} fallback={props.empty}> <Show when={hasDiffs()} fallback={props.empty}>
<Accordion multiple value={open()} onChange={handleChange}> <div class="pb-6">
<For each={files()}> <Accordion multiple value={open()} onChange={handleChange}>
{(file) => { <For each={files()}>
let wrapper: HTMLDivElement | undefined {(file) => {
let wrapper: HTMLDivElement | undefined
const diff = createMemo(() => diffs().get(file)) const diff = createMemo(() => diffs().get(file))
const item = () => diff()! const item = () => diff()!
const expanded = createMemo(() => open().includes(file)) const expanded = createMemo(() => open().includes(file))
const force = () => !!store.force[file] const force = () => !!store.force[file]
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file)) const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file))
const commentedLines = createMemo(() => comments().map((c) => c.selection)) const commentedLines = createMemo(() => comments().map((c) => c.selection))
const beforeText = () => (typeof item().before === "string" ? item().before : "") const beforeText = () => (typeof item().before === "string" ? item().before : "")
const afterText = () => (typeof item().after === "string" ? item().after : "") const afterText = () => (typeof item().after === "string" ? item().after : "")
const changedLines = () => item().additions + item().deletions const changedLines = () => item().additions + item().deletions
const mediaKind = createMemo(() => mediaKindFromPath(file)) const mediaKind = createMemo(() => mediaKindFromPath(file))
const tooLarge = createMemo(() => { const tooLarge = createMemo(() => {
if (!expanded()) return false if (!expanded()) return false
if (force()) return false if (force()) return false
if (mediaKind()) return false if (mediaKind()) return false
return changedLines() > MAX_DIFF_CHANGED_LINES return changedLines() > MAX_DIFF_CHANGED_LINES
}) })
const isAdded = () => const isAdded = () =>
item().status === "added" || (beforeText().length === 0 && afterText().length > 0) item().status === "added" || (beforeText().length === 0 && afterText().length > 0)
const isDeleted = () => const isDeleted = () =>
item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0) item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
const selectedLines = createMemo(() => { const selectedLines = createMemo(() => {
const current = selection() const current = selection()
if (!current || current.file !== file) return null if (!current || current.file !== file) return null
return current.range return current.range
}) })
const draftRange = createMemo(() => { const draftRange = createMemo(() => {
const current = commenting() const current = commenting()
if (!current || current.file !== file) return null if (!current || current.file !== file) return null
return current.range return current.range
}) })
const commentsUi = createLineCommentController<SessionReviewComment>({ const commentsUi = createLineCommentController<SessionReviewComment>({
comments, comments,
label: i18n.t("ui.lineComment.submit"), label: i18n.t("ui.lineComment.submit"),
draftKey: () => file, draftKey: () => file,
state: { state: {
opened: () => { opened: () => {
const current = opened() const current = opened()
if (!current || current.file !== file) return null if (!current || current.file !== file) return null
return current.id return current.id
},
setOpened: (id) => setOpened(id ? { file, id } : null),
selected: selectedLines,
setSelected: (range) => setSelection(range ? { file, range } : null),
commenting: draftRange,
setCommenting: (range) => setCommenting(range ? { file, range } : null),
}, },
setOpened: (id) => setOpened(id ? { file, id } : null), getSide: selectionSide,
selected: selectedLines, clearSelectionOnSelectionEndNull: false,
setSelected: (range) => setSelection(range ? { file, range } : null), onSubmit: ({ comment, selection }) => {
commenting: draftRange, props.onLineComment?.({
setCommenting: (range) => setCommenting(range ? { file, range } : null), file,
}, selection,
getSide: selectionSide, comment,
clearSelectionOnSelectionEndNull: false, preview: selectionPreview(item(), selection),
onSubmit: ({ comment, selection }) => { })
props.onLineComment?.({ },
file, onUpdate: ({ id, comment, selection }) => {
selection, props.onLineCommentUpdate?.({
comment, id,
preview: selectionPreview(item(), selection), file,
}) selection,
}, comment,
onUpdate: ({ id, comment, selection }) => { preview: selectionPreview(item(), selection),
props.onLineCommentUpdate?.({ })
id, },
file, onDelete: (comment) => {
selection, props.onLineCommentDelete?.({
comment, id: comment.id,
preview: selectionPreview(item(), selection), file,
}) })
}, },
onDelete: (comment) => { editSubmitLabel: props.lineCommentActions?.saveLabel,
props.onLineCommentDelete?.({ renderCommentActions: props.lineCommentActions
id: comment.id, ? (comment, controls) => (
file, <ReviewCommentMenu
}) labels={props.lineCommentActions!}
}, onEdit={controls.edit}
editSubmitLabel: props.lineCommentActions?.saveLabel, onDelete={controls.remove}
renderCommentActions: props.lineCommentActions />
? (comment, controls) => ( )
<ReviewCommentMenu : undefined,
labels={props.lineCommentActions!} })
onEdit={controls.edit}
onDelete={controls.remove}
/>
)
: undefined,
})
onCleanup(() => { onCleanup(() => {
anchors.delete(file) anchors.delete(file)
readyFiles.delete(file) readyFiles.delete(file)
searchHandles.delete(file) searchHandles.delete(file)
if (highlightedFile === file) highlightedFile = undefined if (highlightedFile === file) highlightedFile = undefined
}) })
const handleLineSelected = (range: SelectedLineRange | null) => { const handleLineSelected = (range: SelectedLineRange | null) => {
if (!props.onLineComment) return if (!props.onLineComment) return
commentsUi.onLineSelected(range) commentsUi.onLineSelected(range)
} }
const handleLineSelectionEnd = (range: SelectedLineRange | null) => { const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
if (!props.onLineComment) return if (!props.onLineComment) return
commentsUi.onLineSelectionEnd(range) commentsUi.onLineSelectionEnd(range)
} }
return ( return (
<Accordion.Item <Accordion.Item
value={file} value={file}
id={diffId(file)} id={diffId(file)}
data-file={file} data-file={file}
data-slot="session-review-accordion-item" data-slot="session-review-accordion-item"
data-selected={props.focusedFile === file ? "" : undefined} data-selected={props.focusedFile === file ? "" : undefined}
> >
<StickyAccordionHeader> <StickyAccordionHeader>
<Accordion.Trigger> <Accordion.Trigger>
<div data-slot="session-review-trigger-content"> <div data-slot="session-review-trigger-content">
<div data-slot="session-review-file-info"> <div data-slot="session-review-file-info">
<FileIcon node={{ path: file, type: "file" }} /> <FileIcon node={{ path: file, type: "file" }} />
<div data-slot="session-review-file-name-container"> <div data-slot="session-review-file-name-container">
<Show when={file.includes("/")}> <Show when={file.includes("/")}>
<span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span> <span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span>
</Show> </Show>
<span data-slot="session-review-filename">{getFilename(file)}</span> <span data-slot="session-review-filename">{getFilename(file)}</span>
<Show when={props.onViewFile}> <Show when={props.onViewFile}>
<Tooltip value={openFileLabel()} placement="top" gutter={4}> <Tooltip value={openFileLabel()} placement="top" gutter={4}>
<button <button
data-slot="session-review-view-button" data-slot="session-review-view-button"
type="button" type="button"
aria-label={openFileLabel()} aria-label={openFileLabel()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
props.onViewFile?.(file) props.onViewFile?.(file)
}} }}
> >
<Icon name="open-file" size="small" /> <Icon name="open-file" size="small" />
</button> </button>
</Tooltip> </Tooltip>
</Show> </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={item()} />
</div>
</Match>
<Match when={isDeleted()}>
<span data-slot="session-review-change" data-type="removed">
{i18n.t("ui.sessionReview.change.removed")}
</span>
</Match>
<Match when={!!mediaKind()}>
<span data-slot="session-review-change" data-type="modified">
{i18n.t("ui.sessionReview.change.modified")}
</span>
</Match>
<Match when={true}>
<DiffChanges changes={item()} />
</Match>
</Switch>
<span data-slot="session-review-diff-chevron">
<Icon name="chevron-down" size="small" />
</span>
</div> </div>
</div> </div>
<div data-slot="session-review-trigger-actions"> </Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content data-slot="session-review-accordion-content">
<div
data-slot="session-review-diff-wrapper"
ref={(el) => {
wrapper = el
anchors.set(file, el)
}}
>
<Show when={expanded()}>
<Switch> <Switch>
<Match when={isAdded()}> <Match when={tooLarge()}>
<div data-slot="session-review-change-group" data-type="added"> <div data-slot="session-review-large-diff">
<span data-slot="session-review-change" data-type="added"> <div data-slot="session-review-large-diff-title">
{i18n.t("ui.sessionReview.change.added")} {i18n.t("ui.sessionReview.largeDiff.title")}
</span> </div>
<DiffChanges changes={item()} /> <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={() => setStore("force", file, true)}
>
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
</Button>
</div>
</div> </div>
</Match> </Match>
<Match when={isDeleted()}>
<span data-slot="session-review-change" data-type="removed">
{i18n.t("ui.sessionReview.change.removed")}
</span>
</Match>
<Match when={!!mediaKind()}>
<span data-slot="session-review-change" data-type="modified">
{i18n.t("ui.sessionReview.change.modified")}
</span>
</Match>
<Match when={true}> <Match when={true}>
<DiffChanges changes={item()} /> <Dynamic
component={fileComponent}
mode="diff"
preloadedDiff={item().preloaded}
diffStyle={diffStyle()}
expansionLineCount={searchExpanded() ? Number.MAX_SAFE_INTEGER : 20}
onRendered={() => {
readyFiles.add(file)
props.onDiffRendered?.()
}}
enableLineSelection={props.onLineComment != null}
enableHoverUtility={props.onLineComment != null}
onLineSelected={handleLineSelected}
onLineSelectionEnd={handleLineSelectionEnd}
onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
annotations={commentsUi.annotations()}
renderAnnotation={commentsUi.renderAnnotation}
renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined}
selectedLines={selectedLines()}
commentedLines={commentedLines()}
search={{
shortcuts: "disabled",
showBar: false,
disableVirtualization: searchExpanded(),
register: (handle: FileSearchHandle | null) => {
if (!handle) {
searchHandles.delete(file)
readyFiles.delete(file)
if (highlightedFile === file) highlightedFile = undefined
return
}
searchHandles.set(file, handle)
},
}}
before={{
name: file,
contents: typeof item().before === "string" ? item().before : "",
}}
after={{
name: file,
contents: typeof item().after === "string" ? item().after : "",
}}
media={{
mode: "auto",
path: file,
before: item().before,
after: item().after,
readFile: props.readFile,
}}
/>
</Match> </Match>
</Switch> </Switch>
<span data-slot="session-review-diff-chevron"> </Show>
<Icon name="chevron-down" size="small" />
</span>
</div>
</div> </div>
</Accordion.Trigger> </Accordion.Content>
</StickyAccordionHeader> </Accordion.Item>
<Accordion.Content data-slot="session-review-accordion-content"> )
<div }}
data-slot="session-review-diff-wrapper" </For>
ref={(el) => { </Accordion>
wrapper = el </div>
anchors.set(file, el)
}}
>
<Show when={expanded()}>
<Switch>
<Match when={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={() => setStore("force", file, true)}
>
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
</Button>
</div>
</div>
</Match>
<Match when={true}>
<Dynamic
component={fileComponent}
mode="diff"
preloadedDiff={item().preloaded}
diffStyle={diffStyle()}
expansionLineCount={searchExpanded() ? Number.MAX_SAFE_INTEGER : 20}
onRendered={() => {
readyFiles.add(file)
props.onDiffRendered?.()
}}
enableLineSelection={props.onLineComment != null}
enableHoverUtility={props.onLineComment != null}
onLineSelected={handleLineSelected}
onLineSelectionEnd={handleLineSelectionEnd}
onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
annotations={commentsUi.annotations()}
renderAnnotation={commentsUi.renderAnnotation}
renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined}
selectedLines={selectedLines()}
commentedLines={commentedLines()}
search={{
shortcuts: "disabled",
showBar: false,
disableVirtualization: searchExpanded(),
register: (handle: FileSearchHandle | null) => {
if (!handle) {
searchHandles.delete(file)
readyFiles.delete(file)
if (highlightedFile === file) highlightedFile = undefined
return
}
searchHandles.set(file, handle)
},
}}
before={{
name: file,
contents: typeof item().before === "string" ? item().before : "",
}}
after={{
name: file,
contents: typeof item().after === "string" ? item().after : "",
}}
media={{
mode: "auto",
path: file,
before: item().before,
after: item().after,
readFile: props.readFile,
}}
/>
</Match>
</Switch>
</Show>
</div>
</Accordion.Content>
</Accordion.Item>
)
}}
</For>
</Accordion>
</Show> </Show>
</div> </div>
</ScrollView> </ScrollView>