mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(session-ui): keep stuck headers clear of the session title fade
This commit is contained in:
@@ -182,7 +182,8 @@ for (const scenario of scenarios) {
|
||||
.poll(() =>
|
||||
header.evaluate((element) => {
|
||||
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
|
||||
const title = viewport.querySelector("[data-session-title]")?.firstElementChild
|
||||
// Stuck headers sit below the whole title, including its fade.
|
||||
const title = viewport.querySelector("[data-session-title]")
|
||||
const toolTitle = element
|
||||
.closest('[data-component="edit-tool"]')
|
||||
?.querySelector('[data-slot="collapsible-trigger"][data-locked]')
|
||||
@@ -194,7 +195,13 @@ for (const scenario of scenarios) {
|
||||
const top = sessionTop + (group?.getBoundingClientRect().height ?? 0)
|
||||
const rect = element.getBoundingClientRect()
|
||||
const trigger = element.querySelector("button")!
|
||||
// The first stuck header covers the faded strip between it and the title.
|
||||
const cover = getComputedStyle(group ?? toolTitle ?? element, "::before")
|
||||
const fade = title
|
||||
? title.getBoundingClientRect().height - title.firstElementChild!.getBoundingClientRect().height
|
||||
: 0
|
||||
return {
|
||||
cover: cover.display === "block" && cover.height === `${fade}px`,
|
||||
gap: Math.abs(rect.top - top - (toolTitle?.getBoundingClientRect().height ?? 0)),
|
||||
groupGap: group ? Math.abs(group.getBoundingClientRect().top - sessionTop) : 0,
|
||||
titleGap: toolTitle ? Math.abs(toolTitle.getBoundingClientRect().top - top) : 0,
|
||||
@@ -204,7 +211,7 @@ for (const scenario of scenarios) {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.toEqual({ gap: 0, groupGap: 0, titleGap: 0, clickable: true })
|
||||
.toEqual({ cover: true, gap: 0, groupGap: 0, titleGap: 0, clickable: true })
|
||||
await page.screenshot({ path: info.outputPath(`${file}.png`) })
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ const pendingMarkdown = '[data-component="markdown"]:not([data-markdown-ready])'
|
||||
// exactly to the end, while a one-pixel nudge upward is a deliberate move away from it.
|
||||
const endEpsilon = 0.5
|
||||
const upwardKeys = new Set(["up", "page-up", "home"])
|
||||
// Stuck headers can clear the session title's 16px fade only where they can detect being stuck and cover it.
|
||||
const coverTitleFade = CSS.supports("container-type", "scroll-state")
|
||||
const cache = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -683,7 +685,10 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
onScroll={handleListScroll}
|
||||
onClick={input.onSelectionInteraction}
|
||||
class="relative min-w-0 w-full h-full"
|
||||
style={{ "--sticky-accordion-top": input.showHeader() ? "48px" : "0px" }}
|
||||
style={{
|
||||
"--sticky-accordion-top": !input.showHeader() ? "0px" : coverTitleFade ? "64px" : "48px",
|
||||
"--sticky-accordion-fade": input.showHeader() && coverTitleFade ? "16px" : "0px",
|
||||
}}
|
||||
>
|
||||
<Show when={input.showHeader()} fallback={<div aria-hidden="true" class="h-4 md:hidden" />}>
|
||||
{props.header}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
story("keeps the open Used header below the session title while scrolling", async ({ mount }) => {
|
||||
story("keeps the open Used header below the session title fade while scrolling", async ({ mount }) => {
|
||||
const root = await mount("current-tool-group--sticky-header", { args: { height: "720" } })
|
||||
const scroller = root.locator('[data-story="sticky-header-scroll"]')
|
||||
const header = root.getByRole("button", { name: "Used 37 Write, Shell, Grep, Edit", exact: true })
|
||||
@@ -9,10 +9,19 @@ story("keeps the open Used header below the session title while scrolling", asyn
|
||||
const scroller = node.closest('[data-story="sticky-header-scroll"]')!
|
||||
return node.getBoundingClientRect().top - scroller.getBoundingClientRect().top
|
||||
})
|
||||
// The cover fills the faded strip between the session title and a stuck header.
|
||||
const cover = (locator: typeof header) =>
|
||||
locator.evaluate((node) => {
|
||||
const style = getComputedStyle(node, "::before")
|
||||
return style.display === "none" ? "none" : style.height
|
||||
})
|
||||
|
||||
expect(await scroller.evaluate((node) => node.scrollHeight > node.clientHeight * 2)).toBe(true)
|
||||
await expect(header).toHaveAttribute("aria-expanded", "true")
|
||||
expect(await cover(header)).toBe("none")
|
||||
await scroller.evaluate((node) => (node.scrollTop = 900))
|
||||
await expect.poll(() => top(header)).toBe(48)
|
||||
await expect.poll(() => top(header)).toBe(64)
|
||||
await expect.poll(() => cover(header)).toBe("16px")
|
||||
|
||||
// Nested file headers stack below the stuck Used header instead of covering it.
|
||||
const file = root.locator('[data-component="sticky-accordion-header"]').filter({ hasText: "model.ts" })
|
||||
@@ -20,11 +29,13 @@ story("keeps the open Used header below the session title while scrolling", asyn
|
||||
const scroller = node.closest<HTMLElement>('[data-story="sticky-header-scroll"]')!
|
||||
scroller.scrollTop += node.getBoundingClientRect().top - scroller.getBoundingClientRect().top + 60
|
||||
})
|
||||
await expect.poll(() => top(header)).toBe(48)
|
||||
await expect.poll(() => top(file)).toBe(84)
|
||||
await expect.poll(() => top(header)).toBe(64)
|
||||
await expect.poll(() => top(file)).toBe(100)
|
||||
await expect.poll(() => cover(file)).toBe("0px")
|
||||
|
||||
await header.click()
|
||||
await expect(header).toHaveAttribute("aria-expanded", "false")
|
||||
await expect.poll(() => top(header)).toBe(48)
|
||||
await expect.poll(() => top(header)).toBe(64)
|
||||
await expect.poll(() => cover(header)).toBe("none")
|
||||
await expect(root.locator('[data-slot="context-tool-group-item"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
@@ -669,9 +669,41 @@
|
||||
background-color: var(--v2-background-bg-base);
|
||||
}
|
||||
|
||||
/* Nested sticky file headers stop below the open group header. */
|
||||
/* Nested sticky file headers stop below the open group header, which already covers the fade. */
|
||||
> [data-slot="collapsible-content"] {
|
||||
--sticky-accordion-top: calc(var(--context-group-sticky-top) + 28px + var(--tool-content-gap));
|
||||
--sticky-accordion-fade: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Stuck headers sit below the session title fade and cover the faded strip above them. */
|
||||
[data-component="collapsed-tool-group"] > [data-component="collapsible"] > [data-slot="collapsible-trigger"],
|
||||
:is([data-component="edit-tool"], [data-component="write-tool"], [data-component="apply-patch-tool"])
|
||||
> [data-component="collapsible"]
|
||||
> [data-slot="collapsible-trigger"] {
|
||||
&:is([aria-expanded="true"], [data-locked]) {
|
||||
container-type: scroll-state;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 100%;
|
||||
display: none;
|
||||
height: var(--sticky-accordion-fade, 0px);
|
||||
background-color: var(--v2-background-bg-base);
|
||||
}
|
||||
}
|
||||
|
||||
@container scroll-state(stuck: top) {
|
||||
[data-component="collapsed-tool-group"] > [data-component="collapsible"] > [data-slot="collapsible-trigger"],
|
||||
:is([data-component="edit-tool"], [data-component="write-tool"], [data-component="apply-patch-tool"])
|
||||
> [data-component="collapsible"]
|
||||
> [data-slot="collapsible-trigger"] {
|
||||
&::before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1292,6 +1324,8 @@
|
||||
}
|
||||
|
||||
> [data-component="collapsible"] > [data-slot="collapsible-content"] {
|
||||
/* The sticky tool title above the file headers already covers the fade. */
|
||||
--sticky-accordion-fade: 0px;
|
||||
border: none;
|
||||
border-inline-start: none;
|
||||
margin-inline-start: 0;
|
||||
|
||||
@@ -166,7 +166,7 @@ export const StickyHeader = {
|
||||
<section
|
||||
data-story="sticky-header-scroll"
|
||||
class="relative w-full max-w-[1030px] overflow-y-auto bg-v2-background-bg-base"
|
||||
style={{ height: `${args.height}px`, "--sticky-accordion-top": "48px" }}
|
||||
style={{ height: `${args.height}px`, "--sticky-accordion-top": "64px", "--sticky-accordion-fade": "16px" }}
|
||||
>
|
||||
<div class="sticky top-0 z-30 w-full bg-[linear-gradient(to_bottom,var(--v2-background-bg-base)_48px,transparent)] pb-4 pe-3 ps-2.5">
|
||||
<div class="flex h-12 items-center px-1 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base">
|
||||
|
||||
@@ -3,4 +3,22 @@
|
||||
top: calc(var(--sticky-accordion-top, 0px) + var(--sticky-accordion-offset, 0px));
|
||||
z-index: 10;
|
||||
background-color: var(--background-stronger);
|
||||
container-type: scroll-state;
|
||||
|
||||
/* When stuck, cover the faded strip that --sticky-accordion-fade reserves above the header. */
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 100%;
|
||||
display: none;
|
||||
height: var(--sticky-accordion-fade, 0px);
|
||||
background-color: var(--v2-background-bg-base);
|
||||
}
|
||||
}
|
||||
|
||||
@container scroll-state(stuck: top) {
|
||||
[data-component="sticky-accordion-header"]::before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user