mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
feat(app): add settings to vertical tabs (#47119)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
c76e602ba4
commit
6af46cc8a9
@@ -232,6 +232,92 @@ test("vertical tabs show project details, resize, and navigate", async ({ page }
|
||||
await expect(tabB).toBeVisible()
|
||||
})
|
||||
|
||||
for (const direction of ["ltr", "rtl"]) {
|
||||
test(`vertical tabs keep Settings pinned while scrolling in ${direction}`, async ({ page }, testInfo) => {
|
||||
await mockServer(page)
|
||||
await page.addInitScript(
|
||||
({ server, sessionA, sessionB, directory }) => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ appearance: { tabLayout: "vertical" } }))
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify([
|
||||
{ type: "session", server, sessionId: sessionA },
|
||||
...Array.from({ length: 24 }, (_, index) => ({
|
||||
type: "draft",
|
||||
server,
|
||||
directory,
|
||||
draftID: `draft_scroll_${index}`,
|
||||
})),
|
||||
{ type: "session", server, sessionId: sessionB },
|
||||
]),
|
||||
)
|
||||
},
|
||||
{ server, sessionA: sessionA.id, sessionB: sessionB.id, directory: sessionA.directory },
|
||||
)
|
||||
await page.goto("/")
|
||||
|
||||
const sidebar = page.locator('[data-slot="vertical-tabs-sidebar"]')
|
||||
const settings = sidebar.getByRole("button", { name: "Settings", exact: true })
|
||||
const scroll = sidebar.locator('[data-slot="vertical-tabs-scroll"]')
|
||||
const hrefB = `/server/${base64Encode(server)}/session/${sessionB.id}`
|
||||
const tabB = sidebar.locator(`[data-titlebar-tab-link][href="${hrefB}"]`)
|
||||
await expect(sidebar.locator("[data-titlebar-tab-slot]")).toHaveCount(26)
|
||||
await expect(settings).toHaveText("Settings")
|
||||
await page.evaluate((direction) => document.documentElement.setAttribute("dir", direction), direction)
|
||||
|
||||
for (const width of [1280, 800]) {
|
||||
await page.setViewportSize({ width, height: 360 })
|
||||
await expect(settings).toBeInViewport({ ratio: 1 })
|
||||
await expect(sidebar).toHaveCSS("padding-inline-start", "10px")
|
||||
await expect(sidebar).toHaveCSS("padding-bottom", "10px")
|
||||
await expect(settings).toHaveCSS("margin-top", "8px")
|
||||
await expect
|
||||
.poll(() =>
|
||||
sidebar.locator('[data-slot="vertical-tabs-footer"]').evaluate((element) => {
|
||||
const content = Math.max(
|
||||
0,
|
||||
...Array.from(element.children, (child) => child.getBoundingClientRect().height),
|
||||
)
|
||||
return element.getBoundingClientRect().height - content
|
||||
}),
|
||||
)
|
||||
.toBe(0)
|
||||
await expect(scroll).toHaveCSS("mask-image", /linear-gradient/)
|
||||
await scroll.evaluate((element) => element.scrollTo(0, 0))
|
||||
await expect(scroll).toHaveJSProperty("scrollTop", 0)
|
||||
const pinned = await settings.boundingBox()
|
||||
await scroll.hover()
|
||||
await page.mouse.wheel(0, 200)
|
||||
await expect.poll(() => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(0)
|
||||
await expect.poll(() => settings.boundingBox()).toEqual(pinned)
|
||||
await testInfo.attach(`vertical-tabs-settings-${width}`, {
|
||||
body: await sidebar.screenshot(),
|
||||
contentType: "image/png",
|
||||
})
|
||||
|
||||
await scroll.evaluate((element) => element.scrollTo(0, element.scrollHeight))
|
||||
await expect(tabB).toBeInViewport({ ratio: 1 })
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const tab = await tabB.boundingBox()
|
||||
const viewport = await scroll.boundingBox()
|
||||
return !!tab && !!viewport && tab.y + tab.height <= viewport.y + viewport.height - 16
|
||||
})
|
||||
.toBe(true)
|
||||
await expect.poll(() => settings.boundingBox()).toEqual(pinned)
|
||||
}
|
||||
|
||||
await settings.click()
|
||||
await expect(page.getByTestId("settings-screen")).toBeVisible()
|
||||
await expect(settings).toHaveAttribute("aria-pressed", "true")
|
||||
await sidebar.getByRole("button", { name: "Home", exact: true }).click()
|
||||
await expect(page.getByTestId("settings-screen")).toBeHidden()
|
||||
await settings.focus()
|
||||
await settings.press("Enter")
|
||||
await expect(page.getByTestId("settings-screen")).toBeVisible()
|
||||
})
|
||||
}
|
||||
|
||||
for (const profile of [
|
||||
{ locale: "en", direction: "ltr" },
|
||||
{ locale: "en", direction: "rtl" },
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function Layout(props: ParentProps) {
|
||||
class="relative flex h-full min-h-0 shrink-0 flex-col bg-v2-background-bg-deep px-2.5 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]"
|
||||
style={{
|
||||
width: `${state.tabsWidth}px`,
|
||||
"padding-bottom": "max(8px, env(safe-area-inset-bottom, 0px))",
|
||||
"padding-bottom": "max(10px, env(safe-area-inset-bottom, 0px))",
|
||||
}}
|
||||
>
|
||||
<ResizeHandle
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-slot="vertical-tabs-sidebar"] [data-slot="vertical-tabs"] {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-slot="vertical-tabs-sidebar"] [data-slot="vertical-tabs-scroll"] {
|
||||
min-height: 0;
|
||||
/* Leave enough space to scroll the final tab clear of the fade above Settings. */
|
||||
padding-block-end: 16px;
|
||||
scroll-padding-block-end: 16px;
|
||||
-webkit-mask-image: linear-gradient(to bottom, black calc(100% - 16px), transparent);
|
||||
mask-image: linear-gradient(to bottom, black calc(100% - 16px), transparent);
|
||||
}
|
||||
|
||||
[data-slot="mobile-tabs-trigger"][aria-expanded="true"] {
|
||||
background:
|
||||
linear-gradient(var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||
|
||||
@@ -676,10 +676,19 @@ export function Titlebar(props: {
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-slot="vertical-tabs-footer"
|
||||
class="mt-auto flex h-9 w-full shrink-0 items-center gap-1.5"
|
||||
<button
|
||||
type="button"
|
||||
data-action="vertical-tabs-settings"
|
||||
data-state={layout.route().type === "settings" ? "pressed" : undefined}
|
||||
class="mt-2 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] px-1.5 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base data-[state=pressed]:bg-v2-background-bg-layer-02 data-[state=pressed]:text-v2-text-text-base focus-visible:outline-none focus-visible:bg-v2-background-bg-layer-02 [app-region:no-drag]"
|
||||
onClick={openSettings}
|
||||
aria-label={language.t("sidebar.settings")}
|
||||
aria-pressed={layout.route().type === "settings"}
|
||||
>
|
||||
<Icon name="settings-gear" />
|
||||
{language.t("sidebar.settings")}
|
||||
</button>
|
||||
<div data-slot="vertical-tabs-footer" class="flex w-full shrink-0 items-center gap-1.5">
|
||||
<TitlebarRightMount />
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
Reference in New Issue
Block a user