Files
opencode/packages/app/src/shell/shell.tsx
T
LukeParkerDev c3837bccdb fix(app): drop unverified iOS standalone titlebar offset
Keep the horizontal safe-area insets and the regression spec. Remove the 32px standalone offset: WebKit documents env(safe-area-inset-top) as the full status-bar height, and where iOS 26.x reports 0 the system already reserves the bar.
2026-09-20 18:53:49 +10:00

131 lines
5.3 KiB
TypeScript

import { lazy, Show, Suspense, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media"
import { ResizeHandle } from "@opencode/ui/resize-handle"
import { Titlebar, type TitlebarUpdate } from "@/shell/titlebar/titlebar"
import { usePlatform } from "@/runtime/platform/platform"
import { ToastRegion } from "@/shell/notifications/toast"
import { UploadToastHost } from "@/composer/attachments/uploads"
import { TitlebarRightProvider } from "@/shell/titlebar/right-slot"
import { useSettingsSurface } from "@/settings/surface"
import { useSettings } from "@/settings/model"
import { SshAuthentication } from "@/servers/ssh/authentication"
import { useUpdaterInstall } from "@/shell/updates/download"
import { useCommand } from "@/shell/commands/command"
import { useLanguage } from "@/runtime/i18n/language"
const DebugBar = lazy(() => import("@/shell/debug/debug-bar").then((module) => ({ default: module.DebugBar })))
export default function Layout(props: ParentProps) {
const platform = usePlatform()
const settings = useSettingsSurface()
const preferences = useSettings()
const installUpdate = useUpdaterInstall()
const command = useCommand()
const language = useLanguage()
const mobile = createMediaQuery("(max-width: 767px)")
const [state, setState] = createStore({
debugTools: false,
tabsWidth: 260,
tabsMount: undefined as HTMLElement | undefined,
})
const verticalTabs = () => preferences.appearance.tabLayout() === "vertical" && !mobile()
const bottomTitlebar = () => mobile() && preferences.general.mobileTitlebarPosition() === "bottom"
const update: TitlebarUpdate = {
get state() {
return platform.updater?.state()
},
install: installUpdate,
}
// A plain object avoids the compiler's conditional-prop memo, which leaks when read from event handlers.
const debugTools = {
get visible() {
return state.debugTools
},
toggle: () => setState("debugTools", (value) => !value),
}
command.register("debug-bar", () => [
{
id: "debugBar.toggle",
title: language.t("command.debugBar.toggle"),
category: language.t("command.category.view"),
onSelect: debugTools.toggle,
},
])
return (
<TitlebarRightProvider>
<div
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
style={{
// Native Windows chrome supplies the gap; retain paint clearance for the panels' outer outlines.
"--shell-top-inset": bottomTitlebar()
? "max(0px, calc(8px - env(safe-area-inset-top, 0px)))"
: platform.platform === "desktop" && platform.os === "windows"
? "1px"
: "8px",
"--shell-bottom-inset": bottomTitlebar()
? "8px"
: "max(0px, calc(8px - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))",
}}
>
<Titlebar
update={update}
verticalTabs={verticalTabs() ? { mount: state.tabsMount } : undefined}
debugTools={debugTools}
/>
<div class="flex flex-1 min-h-0 min-w-0 flex-row">
<Show when={verticalTabs()}>
<aside
ref={(element) => setState("tabsMount", element)}
data-slot="vertical-tabs-sidebar"
class="relative flex h-full min-h-0 shrink-0 flex-col bg-v2-background-bg-deep pe-0.5 ps-2.5 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]"
style={{
width: `${state.tabsWidth}px`,
"padding-bottom": "max(10px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)))",
}}
>
<ResizeHandle
class="-end-2"
direction="horizontal"
size={state.tabsWidth}
min={140}
max={520}
onResize={(width) => setState("tabsWidth", width)}
/>
</aside>
</Show>
{/* Size containment collapses percentage-height descendants in WebKit. */}
<main
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-content"
style={{
"padding-top": bottomTitlebar() ? "env(safe-area-inset-top, 0px)" : "0px",
"padding-bottom":
bottomTitlebar() || settings.active()
? "0px"
: "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))",
"--settings-bottom-inset": bottomTitlebar()
? "40px"
: "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))",
"--settings-top-inset": mobile() && !bottomTitlebar() ? "0px" : "var(--shell-top-inset, 8px)",
}}
>
<SshAuthentication>
<Suspense>{props.children}</Suspense>
</SshAuthentication>
</main>
</div>
<Show when={state.debugTools}>
<Suspense>
<DebugBar diagnostics={import.meta.env.DEV} inline />
</Suspense>
</Show>
<ToastRegion />
<UploadToastHost />
</div>
</TitlebarRightProvider>
)
}