diff options
| author | Adam <[email protected]> | 2026-03-09 15:57:19 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-09 15:57:24 -0500 |
| commit | ee18c9976e00ebf0162fe1dcc47b209e0507b3e5 (patch) | |
| tree | 7c11e23925ffa12d3a14bed69b8f9988820cf5a8 | |
| parent | 794532928f76c197181bc80944e8be1f6e5eda9a (diff) | |
| download | opencode-ee18c9976e00ebf0162fe1dcc47b209e0507b3e5.tar.gz opencode-ee18c9976e00ebf0162fe1dcc47b209e0507b3e5.zip | |
chore(app): dev stats
| -rw-r--r-- | packages/app/src/components/debug-bar.tsx | 432 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 366 |
2 files changed, 621 insertions, 177 deletions
diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx new file mode 100644 index 000000000..93a4654f8 --- /dev/null +++ b/packages/app/src/components/debug-bar.tsx @@ -0,0 +1,432 @@ +import { useIsRouting, useLocation } from "@solidjs/router" +import { batch, createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { Tooltip } from "@opencode-ai/ui/tooltip" + +type Mem = Performance & { + memory?: { + usedJSHeapSize: number + jsHeapSizeLimit: number + } +} + +type Evt = PerformanceEntry & { + interactionId?: number + processingStart?: number +} + +type Shift = PerformanceEntry & { + hadRecentInput: boolean + value: number +} + +type Obs = PerformanceObserverInit & { + durationThreshold?: number +} + +const span = 5000 + +const ms = (n?: number, d = 0) => { + if (n === undefined || Number.isNaN(n)) return "n/a" + return `${n.toFixed(d)}ms` +} + +const time = (n?: number) => { + if (n === undefined || Number.isNaN(n)) return "n/a" + return `${Math.round(n)}` +} + +const mb = (n?: number) => { + if (n === undefined || Number.isNaN(n)) return "n/a" + const v = n / 1024 / 1024 + return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB` +} + +const bad = (n: number | undefined, limit: number, low = false) => { + if (n === undefined || Number.isNaN(n)) return false + return low ? n < limit : n > limit +} + +const session = (path: string) => path.includes("/session") + +function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string }) { + return ( + <Tooltip value={props.tip} placement="left"> + <div class="flex w-full flex-col items-center px-0.5 py-1 text-center"> + <div class="text-[7px] font-black uppercase tracking-[0.04em] opacity-70 leading-none">{props.label}</div> + <div + classList={{ + "text-[9px] font-semibold leading-none tabular-nums": true, + "text-text-on-critical-base": !!props.bad, + "opacity-70": !!props.dim, + }} + > + {props.value} + </div> + </div> + </Tooltip> + ) +} + +export function DebugBar() { + const location = useLocation() + const routing = useIsRouting() + const [state, setState] = createStore({ + cls: undefined as number | undefined, + delay: undefined as number | undefined, + fps: undefined as number | undefined, + gap: undefined as number | undefined, + heap: { + limit: undefined as number | undefined, + used: undefined as number | undefined, + }, + inp: undefined as number | undefined, + jank: undefined as number | undefined, + long: { + block: undefined as number | undefined, + count: undefined as number | undefined, + max: undefined as number | undefined, + }, + nav: { + dur: undefined as number | undefined, + pending: false, + }, + }) + + const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined) + const heapv = () => { + const value = heap() + if (value === undefined) return "n/a" + return `${Math.round(value * 100)}%` + } + const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`) + const navv = () => (state.nav.pending ? "..." : time(state.nav.dur)) + + let prev = "" + let start = 0 + let init = false + let one = 0 + let two = 0 + + createEffect(() => { + const busy = routing() + const next = `${location.pathname}${location.search}` + + if (!init) { + init = true + prev = next + return + } + + if (busy) { + if (one !== 0) cancelAnimationFrame(one) + if (two !== 0) cancelAnimationFrame(two) + one = 0 + two = 0 + if (start !== 0) return + start = performance.now() + if (session(prev)) setState("nav", { dur: undefined, pending: true }) + return + } + + if (start === 0) { + prev = next + return + } + + const at = start + const from = prev + start = 0 + prev = next + + if (!(session(from) || session(next))) return + + if (one !== 0) cancelAnimationFrame(one) + if (two !== 0) cancelAnimationFrame(two) + one = requestAnimationFrame(() => { + one = 0 + two = requestAnimationFrame(() => { + two = 0 + setState("nav", { dur: performance.now() - at, pending: false }) + }) + }) + }) + + onMount(() => { + const obs: PerformanceObserver[] = [] + const fps: Array<{ at: number; dur: number }> = [] + const long: Array<{ at: number; dur: number }> = [] + const seen = new Map<number | string, { at: number; delay: number; dur: number }>() + let hasLong = false + let poll: number | undefined + let raf = 0 + let last = 0 + let snap = 0 + + const trim = (list: Array<{ at: number; dur: number }>, span: number, at: number) => { + while (list[0] && at - list[0].at > span) list.shift() + } + + const syncFrame = (at: number) => { + trim(fps, span, at) + const total = fps.reduce((sum, entry) => sum + entry.dur, 0) + const gap = fps.reduce((max, entry) => Math.max(max, entry.dur), 0) + const jank = fps.filter((entry) => entry.dur > 32).length + batch(() => { + setState("fps", total > 0 ? (fps.length * 1000) / total : undefined) + setState("gap", gap > 0 ? gap : undefined) + setState("jank", jank) + }) + } + + const syncLong = (at = performance.now()) => { + if (!hasLong) return + trim(long, span, at) + const block = long.reduce((sum, entry) => sum + Math.max(0, entry.dur - 50), 0) + const max = long.reduce((hi, entry) => Math.max(hi, entry.dur), 0) + setState("long", { block, count: long.length, max }) + } + + const syncInp = (at = performance.now()) => { + for (const [key, entry] of seen) { + if (at - entry.at > span) seen.delete(key) + } + let delay = 0 + let inp = 0 + for (const entry of seen.values()) { + delay = Math.max(delay, entry.delay) + inp = Math.max(inp, entry.dur) + } + batch(() => { + setState("delay", delay > 0 ? delay : undefined) + setState("inp", inp > 0 ? inp : undefined) + }) + } + + const syncHeap = () => { + const mem = (performance as Mem).memory + if (!mem) return + setState("heap", { limit: mem.jsHeapSizeLimit, used: mem.usedJSHeapSize }) + } + + const reset = () => { + fps.length = 0 + long.length = 0 + seen.clear() + last = 0 + snap = 0 + batch(() => { + setState("fps", undefined) + setState("gap", undefined) + setState("jank", undefined) + setState("delay", undefined) + setState("inp", undefined) + if (hasLong) setState("long", { block: 0, count: 0, max: 0 }) + }) + } + + const watch = (type: string, init: Obs, fn: (entries: PerformanceEntry[]) => void) => { + if (typeof PerformanceObserver === "undefined") return false + if (!(PerformanceObserver.supportedEntryTypes ?? []).includes(type)) return false + const ob = new PerformanceObserver((list) => fn(list.getEntries())) + try { + ob.observe(init) + obs.push(ob) + return true + } catch { + ob.disconnect() + return false + } + } + + if ( + watch("layout-shift", { buffered: true, type: "layout-shift" }, (entries) => { + const add = entries.reduce((sum, entry) => { + const item = entry as Shift + if (item.hadRecentInput) return sum + return sum + item.value + }, 0) + if (add === 0) return + setState("cls", (value) => (value ?? 0) + add) + }) + ) { + setState("cls", 0) + } + + if ( + watch("longtask", { buffered: true, type: "longtask" }, (entries) => { + const at = performance.now() + long.push(...entries.map((entry) => ({ at: entry.startTime, dur: entry.duration }))) + syncLong(at) + }) + ) { + hasLong = true + setState("long", { block: 0, count: 0, max: 0 }) + } + + watch("event", { buffered: true, durationThreshold: 16, type: "event" }, (entries) => { + for (const raw of entries) { + const entry = raw as Evt + if (entry.duration < 16) continue + const key = + entry.interactionId && entry.interactionId > 0 + ? entry.interactionId + : `${entry.name}:${Math.round(entry.startTime)}` + const prev = seen.get(key) + const delay = Math.max(0, (entry.processingStart ?? entry.startTime) - entry.startTime) + seen.set(key, { + at: entry.startTime, + delay: Math.max(prev?.delay ?? 0, delay), + dur: Math.max(prev?.dur ?? 0, entry.duration), + }) + if (seen.size <= 200) continue + const first = seen.keys().next().value + if (first !== undefined) seen.delete(first) + } + syncInp() + }) + + const loop = (at: number) => { + if (document.visibilityState !== "visible") { + raf = 0 + return + } + + if (last === 0) { + last = at + raf = requestAnimationFrame(loop) + return + } + + fps.push({ at, dur: at - last }) + last = at + + if (at - snap >= 250) { + snap = at + syncFrame(at) + } + + raf = requestAnimationFrame(loop) + } + + const stop = () => { + if (raf !== 0) cancelAnimationFrame(raf) + raf = 0 + if (poll === undefined) return + clearInterval(poll) + poll = undefined + } + + const start = () => { + if (document.visibilityState !== "visible") return + if (poll === undefined) { + poll = window.setInterval(() => { + syncLong() + syncInp() + syncHeap() + }, 1000) + } + if (raf !== 0) return + raf = requestAnimationFrame(loop) + } + + const vis = () => { + if (document.visibilityState !== "visible") { + stop() + return + } + reset() + start() + } + + syncHeap() + start() + document.addEventListener("visibilitychange", vis) + + onCleanup(() => { + if (one !== 0) cancelAnimationFrame(one) + if (two !== 0) cancelAnimationFrame(two) + stop() + document.removeEventListener("visibilitychange", vis) + for (const ob of obs) ob.disconnect() + }) + }) + + return ( + <aside + aria-label="Development performance diagnostics" + class="pointer-events-auto h-full min-h-0 w-[36px] shrink-0 overflow-y-auto text-text-on-interactive-base no-scrollbar sm:w-[38px]" + style={{ "background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)" }} + > + <div class="flex min-h-full flex-col gap-0.5 py-2 font-mono"> + <Cell + label="NAV" + tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles." + value={navv()} + bad={bad(state.nav.dur, 400)} + dim={state.nav.dur === undefined && !state.nav.pending} + /> + <Cell + label="FPS" + tip="Rolling frames per second over the last 5 seconds." + value={state.fps === undefined ? "n/a" : `${Math.round(state.fps)}`} + bad={bad(state.fps, 50, true)} + dim={state.fps === undefined} + /> + <Cell + label="FRM" + tip="Worst frame time over the last 5 seconds." + value={time(state.gap)} + bad={bad(state.gap, 50)} + dim={state.gap === undefined} + /> + <Cell + label="JNK" + tip="Frames over 32ms in the last 5 seconds." + value={state.jank === undefined ? "n/a" : `${state.jank}`} + bad={bad(state.jank, 8)} + dim={state.jank === undefined} + /> + <Cell + label="LNG" + tip={`Blocked time and long-task count in the last 5 seconds. Max task: ${ms(state.long.max)}.`} + value={longv()} + bad={bad(state.long.block, 200)} + dim={state.long.count === undefined} + /> + <Cell + label="DLY" + tip="Worst observed input delay in the last 5 seconds." + value={time(state.delay)} + bad={bad(state.delay, 100)} + dim={state.delay === undefined} + /> + <Cell + label="INP" + tip="Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP." + value={time(state.inp)} + bad={bad(state.inp, 200)} + dim={state.inp === undefined} + /> + <Cell + label="CLS" + tip="Cumulative layout shift for the current app lifetime." + value={state.cls === undefined ? "n/a" : state.cls.toFixed(2)} + bad={bad(state.cls, 0.1)} + dim={state.cls === undefined} + /> + <Cell + label="MEM" + tip={ + state.heap.used === undefined + ? "Used JS heap vs heap limit. Chromium only." + : `Used JS heap vs heap limit. ${mb(state.heap.used)} of ${mb(state.heap.limit)}.` + } + value={heapv()} + bad={bad(heap(), 0.8)} + dim={state.heap.used === undefined} + /> + </div> + </aside> + ) +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 40015db1b..052a03c54 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -54,6 +54,7 @@ import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogEditProject } from "@/components/dialog-edit-project" +import { DebugBar } from "@/components/debug-bar" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" @@ -2135,193 +2136,204 @@ export default function Layout(props: ParentProps) { } return ( - <div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> + <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <Titlebar /> - <div class="flex-1 min-h-0 relative overflow-x-hidden"> - <nav - aria-label={language.t("sidebar.nav.projectsAndSessions")} - data-component="sidebar-nav-desktop" - classList={{ - "hidden xl:block": true, - "absolute inset-y-0 left-0": true, - "z-10": true, - }} - style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }} - ref={(el) => { - setState("nav", el) - }} - onMouseEnter={() => { - disarm() - }} - onMouseLeave={() => { - aim.reset() - if (!sidebarHovering()) return + <div class="flex-1 min-h-0 min-w-0 flex"> + <div class="flex-1 min-h-0 relative"> + <div class="size-full relative overflow-x-hidden"> + <nav + aria-label={language.t("sidebar.nav.projectsAndSessions")} + data-component="sidebar-nav-desktop" + classList={{ + "hidden xl:block": true, + "absolute inset-y-0 left-0": true, + "z-10": true, + }} + style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }} + ref={(el) => { + setState("nav", el) + }} + onMouseEnter={() => { + disarm() + }} + onMouseLeave={() => { + aim.reset() + if (!sidebarHovering()) return + + arm() + }} + > + <div class="@container w-full h-full contain-strict"> + <SidebarContent + opened={() => layout.sidebar.opened()} + aimMove={aim.move} + projects={() => layout.projects.list()} + renderProject={(project) => ( + <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} /> + )} + handleDragStart={handleDragStart} + handleDragEnd={handleDragEnd} + handleDragOver={handleDragOver} + openProjectLabel={language.t("command.project.open")} + openProjectKeybind={() => command.keybind("project.open")} + onOpenProject={chooseProject} + renderProjectOverlay={() => ( + <ProjectDragOverlay + projects={() => layout.projects.list()} + activeProject={() => store.activeProject} + /> + )} + settingsLabel={() => language.t("sidebar.settings")} + settingsKeybind={() => command.keybind("settings.open")} + onOpenSettings={openSettings} + helpLabel={() => language.t("sidebar.help")} + onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} + renderPanel={() => ( + <Show when={currentProject()} keyed> + {(project) => <SidebarPanel project={project} merged />} + </Show> + )} + /> + </div> + <Show when={layout.sidebar.opened()}> + <div onPointerDown={() => setSizing(true)}> + <ResizeHandle + direction="horizontal" + size={layout.sidebar.width()} + min={244} + max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} + collapseThreshold={244} + onResize={(w) => { + setSizing(true) + if (sizet !== undefined) clearTimeout(sizet) + sizet = window.setTimeout(() => setSizing(false), 120) + layout.sidebar.resize(w) + }} + onCollapse={layout.sidebar.close} + /> + </div> + </Show> + </nav> - arm() - }} - > - <div class="@container w-full h-full contain-strict"> - <SidebarContent - opened={() => layout.sidebar.opened()} - aimMove={aim.move} - projects={() => layout.projects.list()} - renderProject={(project) => ( - <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} /> - )} - handleDragStart={handleDragStart} - handleDragEnd={handleDragEnd} - handleDragOver={handleDragOver} - openProjectLabel={language.t("command.project.open")} - openProjectKeybind={() => command.keybind("project.open")} - onOpenProject={chooseProject} - renderProjectOverlay={() => ( - <ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} /> - )} - settingsLabel={() => language.t("sidebar.settings")} - settingsKeybind={() => command.keybind("settings.open")} - onOpenSettings={openSettings} - helpLabel={() => language.t("sidebar.help")} - onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} - renderPanel={() => ( - <Show when={currentProject()} keyed> - {(project) => <SidebarPanel project={project} merged />} - </Show> - )} + <div + class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base" + style={{ left: "calc(4rem + 12px)" }} /> - </div> - <Show when={layout.sidebar.opened()}> - <div onPointerDown={() => setSizing(true)}> - <ResizeHandle - direction="horizontal" - size={layout.sidebar.width()} - min={244} - max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} - collapseThreshold={244} - onResize={(w) => { - setSizing(true) - if (sizet !== undefined) clearTimeout(sizet) - sizet = window.setTimeout(() => setSizing(false), 120) - layout.sidebar.resize(w) + + <div class="xl:hidden"> + <div + classList={{ + "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true, + "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(), + "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(), + }} + onClick={(e) => { + if (e.target === e.currentTarget) layout.mobileSidebar.hide() }} - onCollapse={layout.sidebar.close} /> + <nav + aria-label={language.t("sidebar.nav.projectsAndSessions")} + data-component="sidebar-nav-mobile" + 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, + "translate-x-0": layout.mobileSidebar.opened(), + "-translate-x-full": !layout.mobileSidebar.opened(), + }} + onClick={(e) => e.stopPropagation()} + > + <SidebarContent + mobile + opened={() => layout.sidebar.opened()} + aimMove={aim.move} + projects={() => layout.projects.list()} + renderProject={(project) => ( + <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile /> + )} + handleDragStart={handleDragStart} + handleDragEnd={handleDragEnd} + handleDragOver={handleDragOver} + openProjectLabel={language.t("command.project.open")} + openProjectKeybind={() => command.keybind("project.open")} + onOpenProject={chooseProject} + renderProjectOverlay={() => ( + <ProjectDragOverlay + projects={() => layout.projects.list()} + activeProject={() => store.activeProject} + /> + )} + settingsLabel={() => language.t("sidebar.settings")} + settingsKeybind={() => command.keybind("settings.open")} + onOpenSettings={openSettings} + helpLabel={() => language.t("sidebar.help")} + onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} + renderPanel={() => <SidebarPanel project={currentProject()} mobile />} + /> + </nav> </div> - </Show> - </nav> - - <div - class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base" - style={{ left: "calc(4rem + 12px)" }} - /> - - <div class="xl:hidden"> - <div - classList={{ - "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true, - "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(), - "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(), - }} - onClick={(e) => { - if (e.target === e.currentTarget) layout.mobileSidebar.hide() - }} - /> - <nav - aria-label={language.t("sidebar.nav.projectsAndSessions")} - data-component="sidebar-nav-mobile" - 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, - "translate-x-0": layout.mobileSidebar.opened(), - "-translate-x-full": !layout.mobileSidebar.opened(), - }} - onClick={(e) => e.stopPropagation()} - > - <SidebarContent - mobile - opened={() => layout.sidebar.opened()} - aimMove={aim.move} - projects={() => layout.projects.list()} - renderProject={(project) => ( - <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile /> - )} - handleDragStart={handleDragStart} - handleDragEnd={handleDragEnd} - handleDragOver={handleDragOver} - openProjectLabel={language.t("command.project.open")} - openProjectKeybind={() => command.keybind("project.open")} - onOpenProject={chooseProject} - renderProjectOverlay={() => ( - <ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} /> - )} - settingsLabel={() => language.t("sidebar.settings")} - settingsKeybind={() => command.keybind("settings.open")} - onOpenSettings={openSettings} - helpLabel={() => language.t("sidebar.help")} - onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} - renderPanel={() => <SidebarPanel project={currentProject()} mobile />} - /> - </nav> - </div> - <div - classList={{ - "absolute inset-0": true, - "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true, - "z-20": true, - "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": - !sizing(), - }} - style={{ - "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem", - }} - > - <main - classList={{ - "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true, - }} - > - <Show when={!autoselecting()} fallback={<div class="size-full" />}> - {props.children} - </Show> - </main> - </div> + <div + classList={{ + "absolute inset-0": true, + "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true, + "z-20": true, + "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": + !sizing(), + }} + style={{ + "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem", + }} + > + <main + classList={{ + "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true, + }} + > + <Show when={!autoselecting()} fallback={<div class="size-full" />}> + {props.children} + </Show> + </main> + </div> - <div - classList={{ - "hidden xl:flex absolute inset-y-0 left-16 z-30": true, - "opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(), - "opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(), - "transition-[opacity,transform] motion-reduce:transition-none": true, - "duration-180 ease-out": peeked() && !layout.sidebar.opened(), - "duration-120 ease-in": !peeked() || layout.sidebar.opened(), - }} - onMouseMove={disarm} - onMouseEnter={() => { - disarm() - aim.reset() - }} - onPointerDown={disarm} - onMouseLeave={() => { - arm() - }} - > - <Show when={peek()} keyed> - {(project) => <SidebarPanel project={project} merged={false} />} - </Show> - </div> + <div + classList={{ + "hidden xl:flex absolute inset-y-0 left-16 z-30": true, + "opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(), + "opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(), + "transition-[opacity,transform] motion-reduce:transition-none": true, + "duration-180 ease-out": peeked() && !layout.sidebar.opened(), + "duration-120 ease-in": !peeked() || layout.sidebar.opened(), + }} + onMouseMove={disarm} + onMouseEnter={() => { + disarm() + aim.reset() + }} + onPointerDown={disarm} + onMouseLeave={() => { + arm() + }} + > + <Show when={peek()} keyed> + {(project) => <SidebarPanel project={project} merged={false} />} + </Show> + </div> - <div - classList={{ - "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true, - "opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(), - "opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(), - "transition-[opacity,transform] motion-reduce:transition-none": true, - "duration-180 ease-out": peeked() && !layout.sidebar.opened(), - "duration-120 ease-in": !peeked() || layout.sidebar.opened(), - }} - style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }} - > - <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} /> + <div + classList={{ + "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true, + "opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(), + "opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(), + "transition-[opacity,transform] motion-reduce:transition-none": true, + "duration-180 ease-out": peeked() && !layout.sidebar.opened(), + "duration-120 ease-in": !peeked() || layout.sidebar.opened(), + }} + style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }} + > + <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} /> + </div> + </div> </div> + {import.meta.env.DEV && <DebugBar />} </div> <Toast.Region /> </div> |
