mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2451ea7303 |
@@ -19,3 +19,42 @@ test("server picker dialog opens from home", async ({ page }) => {
|
|||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("home hides desktop history and sidebar controls", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 900 })
|
||||||
|
await page.goto("/")
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Toggle sidebar" })).toHaveCount(0)
|
||||||
|
await expect(page.getByRole("button", { name: "Go back" })).toHaveCount(0)
|
||||||
|
await expect(page.getByRole("button", { name: "Go forward" })).toHaveCount(0)
|
||||||
|
await expect(page.getByRole("button", { name: "Toggle menu" })).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("home keeps the mobile menu available", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 430, height: 900 })
|
||||||
|
await page.goto("/")
|
||||||
|
|
||||||
|
const toggle = page.getByRole("button", { name: "Toggle menu" }).first()
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await toggle.click()
|
||||||
|
|
||||||
|
const nav = page.locator('[data-component="sidebar-nav-mobile"]')
|
||||||
|
await expect(nav).toBeVisible()
|
||||||
|
await expect.poll(async () => (await nav.boundingBox())?.width ?? 0).toBeLessThan(120)
|
||||||
|
await expect(nav.getByRole("button", { name: "Settings" })).toBeVisible()
|
||||||
|
await expect(nav.getByRole("button", { name: "Help" })).toBeVisible()
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1400, height: 900 })
|
||||||
|
await expect(nav).toBeHidden()
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 430, height: 900 })
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await expect(toggle).toHaveAttribute("aria-expanded", "false")
|
||||||
|
await expect(nav).toHaveClass(/-translate-x-full/)
|
||||||
|
|
||||||
|
await toggle.click()
|
||||||
|
await expect(nav).toBeVisible()
|
||||||
|
|
||||||
|
await nav.getByRole("button", { name: "Settings" }).click()
|
||||||
|
await expect(page.getByRole("dialog")).toBeVisible()
|
||||||
|
})
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function Titlebar() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
const path = () => `${location.pathname}${location.search}${location.hash}`
|
||||||
|
const home = createMemo(() => !params.dir)
|
||||||
const creating = createMemo(() => {
|
const creating = createMemo(() => {
|
||||||
if (!params.dir) return false
|
if (!params.dir) return false
|
||||||
if (params.id) return false
|
if (params.id) return false
|
||||||
@@ -198,91 +199,93 @@ export function Titlebar() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<Show when={!home()}>
|
||||||
<TooltipKeybind
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
<TooltipKeybind
|
||||||
placement="bottom"
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||||
title={language.t("command.sidebar.toggle")}
|
placement="bottom"
|
||||||
keybind={command.keybind("sidebar.toggle")}
|
title={language.t("command.sidebar.toggle")}
|
||||||
>
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
|
||||||
onClick={layout.sidebar.toggle}
|
|
||||||
aria-label={language.t("command.sidebar.toggle")}
|
|
||||||
aria-expanded={layout.sidebar.opened()}
|
|
||||||
>
|
>
|
||||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
<div class="hidden xl:flex items-center shrink-0">
|
onClick={layout.sidebar.toggle}
|
||||||
<Show when={params.dir}>
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
<div
|
aria-expanded={layout.sidebar.opened()}
|
||||||
class="flex items-center shrink-0 w-8 mr-1"
|
|
||||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
|
||||||
>
|
>
|
||||||
|
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
|
<Show when={params.dir}>
|
||||||
<div
|
<div
|
||||||
class="transition-opacity"
|
class="flex items-center shrink-0 w-8 mr-1"
|
||||||
classList={{
|
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
|
||||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<div
|
||||||
placement="bottom"
|
class="transition-opacity"
|
||||||
title={language.t("command.session.new")}
|
classList={{
|
||||||
keybind={command.keybind("session.new")}
|
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||||
openDelay={2000}
|
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<TooltipKeybind
|
||||||
variant="ghost"
|
placement="bottom"
|
||||||
icon={creating() ? "new-session-active" : "new-session"}
|
title={language.t("command.session.new")}
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
keybind={command.keybind("session.new")}
|
||||||
disabled={layout.sidebar.opened()}
|
openDelay={2000}
|
||||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
>
|
||||||
onClick={() => {
|
<Button
|
||||||
if (!params.dir) return
|
variant="ghost"
|
||||||
navigate(`/${params.dir}/session`)
|
icon={creating() ? "new-session-active" : "new-session"}
|
||||||
}}
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
aria-label={language.t("command.session.new")}
|
disabled={layout.sidebar.opened()}
|
||||||
aria-current={creating() ? "page" : undefined}
|
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||||
/>
|
onClick={() => {
|
||||||
</TooltipKeybind>
|
if (!params.dir) return
|
||||||
|
navigate(`/${params.dir}/session`)
|
||||||
|
}}
|
||||||
|
aria-label={language.t("command.session.new")}
|
||||||
|
aria-current={creating() ? "page" : undefined}
|
||||||
|
/>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-0 transition-transform"
|
||||||
|
classList={{
|
||||||
|
"translate-x-0": !layout.sidebar.opened(),
|
||||||
|
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-left"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canBack()}
|
||||||
|
onClick={back}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-right"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canForward()}
|
||||||
|
onClick={forward}
|
||||||
|
aria-label={language.t("common.goForward")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-0 transition-transform"
|
|
||||||
classList={{
|
|
||||||
"translate-x-0": !layout.sidebar.opened(),
|
|
||||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-left"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canBack()}
|
|
||||||
onClick={back}
|
|
||||||
aria-label={language.t("common.goBack")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-right"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canForward()}
|
|
||||||
onClick={forward}
|
|
||||||
aria-label={language.t("common.goForward")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -200,13 +200,20 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const stop = () => setState("sizing", false)
|
const stop = () => setState("sizing", false)
|
||||||
|
const sync = () => {
|
||||||
|
if (!window.matchMedia("(min-width: 1280px)").matches) return
|
||||||
|
layout.mobileSidebar.hide()
|
||||||
|
}
|
||||||
window.addEventListener("pointerup", stop)
|
window.addEventListener("pointerup", stop)
|
||||||
window.addEventListener("pointercancel", stop)
|
window.addEventListener("pointercancel", stop)
|
||||||
window.addEventListener("blur", stop)
|
window.addEventListener("blur", stop)
|
||||||
|
window.addEventListener("resize", sync)
|
||||||
|
sync()
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener("pointerup", stop)
|
window.removeEventListener("pointerup", stop)
|
||||||
window.removeEventListener("pointercancel", stop)
|
window.removeEventListener("pointercancel", stop)
|
||||||
window.removeEventListener("blur", stop)
|
window.removeEventListener("blur", stop)
|
||||||
|
window.removeEventListener("resize", sync)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2242,6 +2249,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<SidebarContent
|
<SidebarContent
|
||||||
mobile={mobile}
|
mobile={mobile}
|
||||||
opened={() => layout.sidebar.opened()}
|
opened={() => layout.sidebar.opened()}
|
||||||
|
hasPanel={() => !!currentProject()}
|
||||||
aimMove={aim.move}
|
aimMove={aim.move}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
renderProject={(project) => (
|
renderProject={(project) => (
|
||||||
@@ -2340,7 +2348,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||||
data-component="sidebar-nav-mobile"
|
data-component="sidebar-nav-mobile"
|
||||||
classList={{
|
classList={{
|
||||||
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
"@container fixed top-10 bottom-0 left-0 z-50 overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||||
|
"w-16": !currentProject(),
|
||||||
|
"w-full max-w-[400px]": !!currentProject(),
|
||||||
"translate-x-0": layout.mobileSidebar.opened(),
|
"translate-x-0": layout.mobileSidebar.opened(),
|
||||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { type LocalProject } from "@/context/layout"
|
|||||||
export const SidebarContent = (props: {
|
export const SidebarContent = (props: {
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
opened: Accessor<boolean>
|
opened: Accessor<boolean>
|
||||||
|
hasPanel?: Accessor<boolean>
|
||||||
aimMove: (event: MouseEvent) => void
|
aimMove: (event: MouseEvent) => void
|
||||||
projects: Accessor<LocalProject[]>
|
projects: Accessor<LocalProject[]>
|
||||||
renderProject: (project: LocalProject) => JSX.Element
|
renderProject: (project: LocalProject) => JSX.Element
|
||||||
@@ -33,6 +34,7 @@ export const SidebarContent = (props: {
|
|||||||
renderPanel: () => JSX.Element
|
renderPanel: () => JSX.Element
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const expanded = createMemo(() => !!props.mobile || props.opened())
|
const expanded = createMemo(() => !!props.mobile || props.opened())
|
||||||
|
const hasPanel = createMemo(() => props.hasPanel?.() ?? true)
|
||||||
const placement = () => (props.mobile ? "bottom" : "right")
|
const placement = () => (props.mobile ? "bottom" : "right")
|
||||||
let panel: HTMLDivElement | undefined
|
let panel: HTMLDivElement | undefined
|
||||||
|
|
||||||
@@ -111,15 +113,17 @@ export const SidebarContent = (props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<Show when={hasPanel()}>
|
||||||
ref={(el) => {
|
<div
|
||||||
panel = el
|
ref={(el) => {
|
||||||
}}
|
panel = el
|
||||||
classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
|
}}
|
||||||
aria-hidden={!expanded()}
|
classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
|
||||||
>
|
aria-hidden={!expanded()}
|
||||||
{props.renderPanel()}
|
>
|
||||||
</div>
|
{props.renderPanel()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user