fix(app): accordion styles

This commit is contained in:
Adam
2026-02-19 08:44:17 -06:00
parent 8ebdbe0ea2
commit 338393c016
7 changed files with 455 additions and 538 deletions

View File

@@ -2,7 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 0px;
align-self: stretch; align-self: stretch;
[data-slot="accordion-item"] { [data-slot="accordion-item"] {
@@ -11,7 +11,11 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
align-self: stretch; align-self: stretch;
overflow: clip; overflow: visible;
& + [data-slot="accordion-item"] {
margin-top: -1px;
}
[data-slot="accordion-header"] { [data-slot="accordion-header"] {
width: 100%; width: 100%;
@@ -31,9 +35,10 @@
cursor: default; cursor: default;
user-select: none; user-select: none;
background-color: var(--surface-base); background-color: var(--background-stronger);
border: 1px solid var(--border-weak-base); border: 1px solid var(--border-weak-base);
border-radius: var(--radius-md); border-radius: 0;
box-shadow: none;
overflow: clip; overflow: clip;
color: var(--text-strong); color: var(--text-strong);
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
@@ -47,7 +52,10 @@
letter-spacing: var(--letter-spacing-normal); letter-spacing: var(--letter-spacing-normal);
&:hover { &:hover {
background-color: var(--surface-base); background-color: var(--surface-base-hover);
}
&:active {
background-color: var(--surface-base-active);
} }
&:focus-visible { &:focus-visible {
outline: none; outline: none;
@@ -58,23 +66,40 @@
} }
} }
&[data-expanded] { &:first-child {
[data-slot="accordion-trigger"] { [data-slot="accordion-header"] [data-slot="accordion-trigger"] {
border-bottom-left-radius: 0; border-top-left-radius: var(--radius-lg);
border-bottom-right-radius: 0; 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"] { [data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base); border: 1px solid var(--border-weak-base);
border-top: none; border-top: 0;
border-bottom-left-radius: var(--radius-md); background-color: var(--background-stronger);
border-bottom-right-radius: var(--radius-md); }
}
&: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"] { [data-slot="accordion-content"] {
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
border: 0;
background-color: transparent;
} }
} }
} }

View File

@@ -1288,7 +1288,6 @@
} }
[data-component="apply-patch-file-diff"] { [data-component="apply-patch-file-diff"] {
border-top: 1px solid var(--border-weaker-base);
max-height: 420px; max-height: 420px;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;

View File

@@ -1,7 +1,7 @@
[data-component="session-review"] { [data-component="session-review"] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 0px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
@@ -19,7 +19,8 @@
top: 0; top: 0;
z-index: 20; z-index: 20;
background-color: var(--background-stronger); background-color: var(--background-stronger);
height: 32px; height: 40px;
padding-bottom: 8px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -57,70 +58,13 @@
} }
[data-component="sticky-accordion-header"] { [data-component="sticky-accordion-header"] {
top: 40px; --sticky-accordion-top: 40px;
} }
[data-component="sticky-accordion-header"][data-expanded]::before, [data-slot="session-review-accordion-item"][data-selected]
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { [data-slot="accordion-header"]
top: -40px; [data-slot="accordion-trigger"] {
} background-color: var(--surface-base-active);
[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="accordion-item"] { [data-slot="accordion-item"] {

View File

@@ -320,395 +320,393 @@ export const SessionReview = (props: SessionReviewProps) => {
</div> </div>
<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}>
<div data-slot="session-review-diffs-group"> <Accordion multiple value={open()} onChange={handleChange}>
<Accordion multiple value={open()} onChange={handleChange}> <For each={props.diffs}>
<For each={props.diffs}> {(diff) => {
{(diff) => { let wrapper: HTMLDivElement | undefined
let wrapper: HTMLDivElement | undefined
const expanded = createMemo(() => open().includes(diff.file)) const expanded = createMemo(() => open().includes(diff.file))
const [force, setForce] = createSignal(false) const [force, setForce] = createSignal(false)
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
const commentedLines = createMemo(() => comments().map((c) => c.selection)) const commentedLines = createMemo(() => comments().map((c) => c.selection))
const beforeText = () => (typeof diff.before === "string" ? diff.before : "") const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
const afterText = () => (typeof diff.after === "string" ? diff.after : "") const afterText = () => (typeof diff.after === "string" ? diff.after : "")
const changedLines = () => diff.additions + diff.deletions const changedLines = () => diff.additions + diff.deletions
const tooLarge = createMemo(() => { const tooLarge = createMemo(() => {
if (!expanded()) return false if (!expanded()) return false
if (force()) return false if (force()) return false
if (isImageFile(diff.file)) return false if (isImageFile(diff.file)) return false
return changedLines() > MAX_DIFF_CHANGED_LINES return changedLines() > MAX_DIFF_CHANGED_LINES
}) })
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
const isDeleted = () => const isDeleted = () =>
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
const isImage = () => isImageFile(diff.file) const isImage = () => isImageFile(diff.file)
const isAudio = () => isAudioFile(diff.file) const isAudio = () => isAudioFile(diff.file)
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc) const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc) const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined) const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
const selectedLines = createMemo(() => { const selectedLines = createMemo(() => {
const current = selection() const current = selection()
if (!current || current.file !== diff.file) return null if (!current || current.file !== diff.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 !== diff.file) return null if (!current || current.file !== diff.file) return null
return current.range return current.range
}) })
const [draft, setDraft] = createSignal("") const [draft, setDraft] = createSignal("")
const [positions, setPositions] = createSignal<Record<string, number>>({}) const [positions, setPositions] = createSignal<Record<string, number>>({})
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
const getRoot = () => { const getRoot = () => {
const el = wrapper const el = wrapper
if (!el) return if (!el) return
const host = el.querySelector("diffs-container") const host = el.querySelector("diffs-container")
if (!(host instanceof HTMLElement)) return if (!(host instanceof HTMLElement)) return
return host.shadowRoot ?? undefined 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 range = draftRange()
if (!range) {
setDraftTop(undefined)
return
} }
const updateAnchors = () => { const marker = findMarker(root, range)
const el = wrapper if (!marker) {
if (!el) return setDraftTop(undefined)
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 scheduleAnchors = () => { setDraftTop(markerTop(el, marker))
requestAnimationFrame(updateAnchors) }
}
createEffect(() => { const scheduleAnchors = () => {
comments() requestAnimationFrame(updateAnchors)
scheduleAnchors() }
})
createEffect(() => { createEffect(() => {
const range = draftRange() comments()
if (!range) return scheduleAnchors()
setDraft("") })
scheduleAnchors()
})
createEffect(() => { createEffect(() => {
if (!open().includes(diff.file)) return const range = draftRange()
if (!isImage()) return if (!range) return
if (imageSrc()) return setDraft("")
if (imageStatus() !== "idle") return scheduleAnchors()
if (isDeleted()) return })
const reader = props.readFile createEffect(() => {
if (!reader) return if (!open().includes(diff.file)) return
if (!isImage()) return
if (imageSrc()) return
if (imageStatus() !== "idle") return
if (isDeleted()) return
setImageStatus("loading") const reader = props.readFile
reader(diff.file) if (!reader) return
.then((result) => {
const src = dataUrl(result) setImageStatus("loading")
if (!src) { reader(diff.file)
setImageStatus("error") .then((result) => {
return const src = dataUrl(result)
} if (!src) {
setImageSrc(src)
setImageStatus("idle")
})
.catch(() => {
setImageStatus("error") setImageStatus("error")
}) return
}) }
setImageSrc(src)
setImageStatus("idle")
})
.catch(() => {
setImageStatus("error")
})
})
createEffect(() => { createEffect(() => {
if (!open().includes(diff.file)) return if (!open().includes(diff.file)) return
if (!isAudio()) return if (!isAudio()) return
if (audioSrc()) return if (audioSrc()) return
if (audioStatus() !== "idle") return if (audioStatus() !== "idle") return
const reader = props.readFile const reader = props.readFile
if (!reader) return if (!reader) return
setAudioStatus("loading") setAudioStatus("loading")
reader(diff.file) reader(diff.file)
.then((result) => { .then((result) => {
const src = dataUrl(result) const src = dataUrl(result)
if (!src) { if (!src) {
setAudioStatus("error")
return
}
setAudioMime(normalizeMimeType(result?.mimeType))
setAudioSrc(src)
setAudioStatus("idle")
})
.catch(() => {
setAudioStatus("error") setAudioStatus("error")
}) return
}) }
setAudioMime(normalizeMimeType(result?.mimeType))
setAudioSrc(src)
setAudioStatus("idle")
})
.catch(() => {
setAudioStatus("error")
})
})
const handleLineSelected = (range: SelectedLineRange | null) => { const handleLineSelected = (range: SelectedLineRange | null) => {
if (!props.onLineComment) return if (!props.onLineComment) return
if (!range) { if (!range) {
setSelection(null) setSelection(null)
return return
}
setSelection({ file: diff.file, range })
} }
const handleLineSelectionEnd = (range: SelectedLineRange | null) => { setSelection({ file: diff.file, range })
if (!props.onLineComment) return }
if (!range) { const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
setCommenting(null) if (!props.onLineComment) return
return
}
setSelection({ file: diff.file, range }) if (!range) {
setCommenting({ file: diff.file, range }) setCommenting(null)
return
} }
const openComment = (comment: SessionReviewComment) => { setSelection({ file: diff.file, range })
setOpened({ file: comment.file, id: comment.id }) setCommenting({ file: diff.file, range })
setSelection({ file: comment.file, range: comment.selection }) }
}
const isCommentOpen = (comment: SessionReviewComment) => { const openComment = (comment: SessionReviewComment) => {
const current = opened() setOpened({ file: comment.file, id: comment.id })
if (!current) return false setSelection({ file: comment.file, range: comment.selection })
return current.file === comment.file && current.id === comment.id }
}
return ( const isCommentOpen = (comment: SessionReviewComment) => {
<Accordion.Item const current = opened()
value={diff.file} if (!current) return false
id={diffId(diff.file)} return current.file === comment.file && current.id === comment.id
data-file={diff.file} }
data-slot="session-review-accordion-item"
data-selected={props.focusedFile === diff.file ? "" : undefined} return (
> <Accordion.Item
<StickyAccordionHeader> value={diff.file}
<Accordion.Trigger> id={diffId(diff.file)}
<div data-slot="session-review-trigger-content"> data-file={diff.file}
<div data-slot="session-review-file-info"> data-slot="session-review-accordion-item"
<FileIcon node={{ path: diff.file, type: "file" }} /> data-selected={props.focusedFile === diff.file ? "" : undefined}
<div data-slot="session-review-file-name-container"> >
<Show when={diff.file.includes("/")}> <StickyAccordionHeader>
<span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span> <Accordion.Trigger>
</Show> <div data-slot="session-review-trigger-content">
<span data-slot="session-review-filename">{getFilename(diff.file)}</span> <div data-slot="session-review-file-info">
<Show when={props.onViewFile}> <FileIcon node={{ path: diff.file, type: "file" }} />
<Tooltip value="Open file" placement="top" gutter={4}> <div data-slot="session-review-file-name-container">
<button <Show when={diff.file.includes("/")}>
data-slot="session-review-view-button" <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
type="button" </Show>
aria-label="Open file" <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
onClick={(e) => { <Show when={props.onViewFile}>
e.stopPropagation() <Tooltip value="Open file" placement="top" gutter={4}>
props.onViewFile?.(diff.file) <button
}} data-slot="session-review-view-button"
> type="button"
<Icon name="open-file" size="small" /> aria-label="Open file"
</button> onClick={(e) => {
</Tooltip> e.stopPropagation()
</Show> props.onViewFile?.(diff.file)
</div> }}
</div> >
<div data-slot="session-review-trigger-actions"> <Icon name="open-file" size="small" />
<Switch> </button>
<Match when={isAdded()}> </Tooltip>
<div data-slot="session-review-change-group" data-type="added"> </Show>
<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>
</div> </div>
</div> </div>
</Accordion.Trigger> <div data-slot="session-review-trigger-actions">
</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> <Switch>
<Match when={isImage() && imageSrc()}> <Match when={isAdded()}>
<div data-slot="session-review-image-container"> <div data-slot="session-review-change-group" data-type="added">
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} /> <span data-slot="session-review-change" data-type="added">
</div> {i18n.t("ui.sessionReview.change.added")}
</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> </span>
<DiffChanges changes={diff} />
</div> </div>
</Match> </Match>
<Match when={isImage() && !imageSrc()}> <Match when={isDeleted()}>
<div data-slot="session-review-image-container"> <span data-slot="session-review-change" data-type="removed">
<span data-slot="session-review-image-placeholder"> {i18n.t("ui.sessionReview.change.removed")}
{imageStatus() === "loading" </span>
? i18n.t("ui.sessionReview.image.loading")
: i18n.t("ui.sessionReview.image.placeholder")}
</span>
</div>
</Match> </Match>
<Match when={!isImage() && tooLarge()}> <Match when={isImage()}>
<div data-slot="session-review-large-diff"> <span data-slot="session-review-change" data-type="modified">
<div data-slot="session-review-large-diff-title"> {i18n.t("ui.sessionReview.change.modified")}
{i18n.t("ui.sessionReview.largeDiff.title")} </span>
</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>
<Match when={!isImage()}> <Match when={true}>
<Dynamic <DiffChanges changes={diff} />
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> </Match>
</Switch> </Switch>
<span data-slot="session-review-diff-chevron">
<For each={comments()}> <Icon name="chevron-down" size="small" />
{(comment) => ( </span>
<LineComment </div>
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)
}}
/>
</Show>
)}
</Show>
</Show>
</div> </div>
</Accordion.Content> </Accordion.Trigger>
</Accordion.Item> </StickyAccordionHeader>
) <Accordion.Content data-slot="session-review-accordion-content">
}} <div
</For> data-slot="session-review-diff-wrapper"
</Accordion> ref={(el) => {
</div> 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)
}}
/>
</Show>
)}
</Show>
</Show>
</div>
</Accordion.Content>
</Accordion.Item>
)
}}
</For>
</Accordion>
</Show> </Show>
</div> </div>
</div> </div>

View File

@@ -129,49 +129,6 @@
flex-direction: column; 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"] { [data-slot="session-turn-diff-trigger"] {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -315,78 +315,76 @@ export function SessionTurn(
<Collapsible.Content> <Collapsible.Content>
<Show when={open()}> <Show when={open()}>
<div data-component="session-turn-diffs-content"> <div data-component="session-turn-diffs-content">
<div data-slot="session-turn-diffs-group"> <Accordion
<Accordion multiple
multiple value={expanded()}
value={expanded()} onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} >
> <For each={diffs()}>
<For each={diffs()}> {(diff) => {
{(diff) => { const active = createMemo(() => expanded().includes(diff.file))
const active = createMemo(() => expanded().includes(diff.file)) const [visible, setVisible] = createSignal(false)
const [visible, setVisible] = createSignal(false)
createEffect( createEffect(
on( on(
active, active,
(value) => { (value) => {
if (!value) { if (!value) {
setVisible(false) setVisible(false)
return return
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!active()) return if (!active()) return
setVisible(true) setVisible(true)
}) })
}, },
{ defer: true }, { defer: true },
), ),
) )
return ( return (
<Accordion.Item value={diff.file}> <Accordion.Item value={diff.file}>
<Accordion.Header> <Accordion.Header>
<Accordion.Trigger> <Accordion.Trigger>
<div data-slot="session-turn-diff-trigger"> <div data-slot="session-turn-diff-trigger">
<span data-slot="session-turn-diff-path"> <span data-slot="session-turn-diff-path">
<Show when={diff.file.includes("/")}> <Show when={diff.file.includes("/")}>
<span data-slot="session-turn-diff-directory"> <span data-slot="session-turn-diff-directory">
{`\u202A${getDirectory(diff.file)}\u202C`} {`\u202A${getDirectory(diff.file)}\u202C`}
</span>
</Show>
<span data-slot="session-turn-diff-filename">
{getFilename(diff.file)}
</span> </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> </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> </div>
</Accordion.Trigger> </div>
</Accordion.Header> </Accordion.Trigger>
<Accordion.Content> </Accordion.Header>
<Show when={visible()}> <Accordion.Content>
<div data-slot="session-turn-diff-view" data-scrollable> <Show when={visible()}>
<Dynamic <div data-slot="session-turn-diff-view" data-scrollable>
component={diffComponent} <Dynamic
before={{ name: diff.file, contents: diff.before }} component={diffComponent}
after={{ name: diff.file, contents: diff.after }} before={{ name: diff.file, contents: diff.before }}
/> after={{ name: diff.file, contents: diff.after }}
</div> />
</Show> </div>
</Accordion.Content> </Show>
</Accordion.Item> </Accordion.Content>
) </Accordion.Item>
}} )
</For> }}
</Accordion> </For>
</div> </Accordion>
</div> </div>
</Show> </Show>
</Collapsible.Content> </Collapsible.Content>

View File

@@ -1,18 +1,14 @@
[data-component="sticky-accordion-header"] { [data-component="sticky-accordion-header"] {
--sticky-accordion-top: 0px;
position: sticky; 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-component="sticky-accordion-header"][data-expanded],
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] { [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] {
z-index: 10; 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);
}