mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): peek the closed sidebar on hover (#58156)
This commit is contained in:
committed by
GitHub
Unverified
parent
010283ed0d
commit
110faa19ae
@@ -62,6 +62,7 @@ export const dict = {
|
||||
"command.input.focus": "Focus input",
|
||||
"command.review.toggle": "Toggle review",
|
||||
"command.sidebar.toggle": "Toggle sidebar",
|
||||
"command.sidebar.toggle.unread": "Toggle sidebar, unread sessions",
|
||||
"command.bottom.toggle": "Toggle bottom panel",
|
||||
"command.message.previous": "Previous message",
|
||||
"command.message.previous.description": "Go to the previous user message",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useServers } from "@/runtime/server/registry"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { CollapsedSidebarToggle, measureSidebarToggles, paintCollapsedSlots } from "@/shell/sidebar-toggle"
|
||||
import { createPanelMotion, guardClosingFocus, panelOpacity, replayPanelToggle } from "@/shell/panel-motion"
|
||||
import { createSidebarPeek } from "@/shell/sidebar-peek"
|
||||
import { windowDragEnabled } from "@/session/header/drag-area"
|
||||
|
||||
export const sidebarToggleCommand = { id: "sidebar.toggle", keybind: "mod+shift+b" }
|
||||
@@ -68,6 +69,14 @@ export default function Layout(props: ParentProps) {
|
||||
offset: () => -10,
|
||||
})
|
||||
|
||||
const peek = createSidebarPeek({
|
||||
available: () => verticalTabs() && !preferences.appearance.sidebarOpen() && !sidebarMotion.present(),
|
||||
open: preferences.appearance.sidebarOpen,
|
||||
aside: () => state.tabsAside,
|
||||
panel: () => state.tabsPanel,
|
||||
content: () => state.tabsMount,
|
||||
})
|
||||
|
||||
replayPanelToggle({ id: "sidebar", label: "Toggle the left sidebar", toggle: toggleSidebar })
|
||||
|
||||
guardClosingFocus({
|
||||
@@ -158,6 +167,8 @@ export default function Layout(props: ParentProps) {
|
||||
id="vertical-tabs-sidebar"
|
||||
data-slot="vertical-tabs-sidebar"
|
||||
data-state={preferences.appearance.sidebarOpen() ? "open" : "closed"}
|
||||
data-peek={peek.shown() ? "" : undefined}
|
||||
style={{ "--sidebar-peek-width": `${state.tabsWidth}px` }}
|
||||
ref={(element) => setState("tabsAside", element)}
|
||||
class="relative z-10 h-full min-h-0 shrink-0"
|
||||
>
|
||||
@@ -165,13 +176,15 @@ export default function Layout(props: ParentProps) {
|
||||
a reopen reuses their layout. */}
|
||||
<div
|
||||
ref={(element) => setState("tabsPanel", element)}
|
||||
data-slot="vertical-tabs-panel"
|
||||
class="absolute inset-0 overflow-hidden [contain:layout_style_paint]"
|
||||
classList={{ "[content-visibility:hidden]": !sidebarMotion.present() }}
|
||||
classList={{ "[content-visibility:hidden]": !sidebarMotion.present() && !peek.shown() }}
|
||||
// Under James the sidebar keeps its landing width (start edge held) while the layout moves.
|
||||
style={{ width: sidebarMotion.pinned() ? `${state.tabsWidth}px` : undefined }}
|
||||
>
|
||||
<div
|
||||
ref={(element) => setState("tabsMount", element)}
|
||||
data-slot="vertical-tabs-mount"
|
||||
class="absolute inset-y-0 end-0 contain-strict will-change-[opacity] flex min-h-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`,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/* The closed sidebar floats as a card below the session header, as tall as its rows up to the window's bottom gap,
|
||||
without resizing its zero-width aside. */
|
||||
[data-slot="vertical-tabs-sidebar"][data-peek] > [data-slot="vertical-tabs-panel"] {
|
||||
/* Past the shell's 8px frame inset, so the card's edges sit inside the frame's. */
|
||||
--sidebar-peek-gap: 12px;
|
||||
--sidebar-peek-start: 16px;
|
||||
/* Below the session header, which ends 48px past the shell's top inset. */
|
||||
--sidebar-peek-top: calc(var(--shell-top-inset, 8px) + 48px + var(--sidebar-peek-gap));
|
||||
inset: var(--sidebar-peek-top) auto auto;
|
||||
inset-inline: var(--sidebar-peek-start) auto;
|
||||
width: var(--sidebar-peek-width);
|
||||
max-height: calc(100% - var(--sidebar-peek-top) - var(--sidebar-peek-gap));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
contain: none;
|
||||
}
|
||||
|
||||
[data-slot="vertical-tabs-sidebar"][data-peek] [data-slot="vertical-tabs-mount"] {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: 100% !important;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
padding-block: 8px !important;
|
||||
padding-inline: 8px 4px !important;
|
||||
contain: layout style;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--v2-elevation-floating);
|
||||
}
|
||||
|
||||
/* The header only spaces the traffic lights and holds the docked toggle; Home, where it leads, stays as a row. */
|
||||
[data-slot="vertical-tabs-sidebar"][data-peek] [data-slot="sidebar-header"]:not([data-home]),
|
||||
[data-slot="vertical-tabs-sidebar"][data-peek] [data-slot="sidebar-header"] > :last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="vertical-tabs-sidebar"][data-peek] [data-slot="sidebar-header"] {
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { prefersReducedMotion, Spring } from "@opencode/ui/motion"
|
||||
import { motionTuning } from "@opencode/ui/motion-tuning"
|
||||
import "./sidebar-peek.css"
|
||||
|
||||
export const sidebarPeekEnabled = motionTuning.toggle("sidebar.peek", {
|
||||
label: "Peekable sidebar",
|
||||
group: "Experiments",
|
||||
value: true,
|
||||
})
|
||||
|
||||
/** Whether any session in the vertical sidebar draws its unread dot, published by the sidebar's tab strip. */
|
||||
export const [sidebarUnread, setSidebarUnread] = createSignal(false)
|
||||
|
||||
const toggles = 'button[aria-controls="vertical-tabs-sidebar"]'
|
||||
|
||||
const menus = '[data-component="menu-v2-content"], [role="menu"]'
|
||||
|
||||
// How long the pointer rests on the toggle before the sidebar peeks, so passing over it doesn't flash it.
|
||||
const intent = 500
|
||||
|
||||
// How far past the sidebar and toggle the pointer may stray, and for how long, before the peek closes.
|
||||
const margin = 15
|
||||
|
||||
const grace = 100
|
||||
|
||||
/**
|
||||
* Experiment: while the left sidebar is closed, hovering its toggle floats the sidebar over the app without moving the
|
||||
* layout. The peek holds while the pointer stays within the box spanning the toggle and the sidebar, plus a margin, so
|
||||
* the way from the toggle into the sidebar never closes it. Closing reads the pointer's last known position on every
|
||||
* move, scroll and wheel, and on a slow interval, rather than trusting enter and leave events on elements.
|
||||
*/
|
||||
export function createSidebarPeek(input: {
|
||||
/** Whether a peek may start: the sidebar is fully closed. */
|
||||
available: Accessor<boolean>
|
||||
open: Accessor<boolean>
|
||||
aside: Accessor<HTMLElement | undefined>
|
||||
/** The floating box, which doesn't move while the content inside it slides. */
|
||||
panel: Accessor<HTMLElement | undefined>
|
||||
content: Accessor<HTMLElement | undefined>
|
||||
}) {
|
||||
const [shown, setShown] = createSignal(false)
|
||||
let toggle: HTMLElement | undefined
|
||||
let animation: Animation | undefined
|
||||
let opening: ReturnType<typeof setTimeout> | undefined
|
||||
let closing: ReturnType<typeof setTimeout> | undefined
|
||||
let watch: ReturnType<typeof setInterval> | undefined
|
||||
// The pointer's last position in the window, or undefined once it has left the window.
|
||||
let pointer: { x: number; y: number } | undefined
|
||||
let pressed = false
|
||||
let keyboard = false
|
||||
|
||||
const cancelOpen = () => {
|
||||
clearTimeout(opening)
|
||||
opening = undefined
|
||||
}
|
||||
|
||||
const cancelClose = () => {
|
||||
clearTimeout(closing)
|
||||
closing = undefined
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
cancelOpen()
|
||||
cancelClose()
|
||||
const content = input.content()
|
||||
|
||||
if (!content) return
|
||||
setShown(true)
|
||||
watch ??= setInterval(check, 150)
|
||||
|
||||
if (animation) {
|
||||
if (animation.playbackRate < 0) animation.reverse()
|
||||
animation.onfinish = null
|
||||
return
|
||||
}
|
||||
|
||||
const direction = document.documentElement.dir === "rtl" ? 1 : -1
|
||||
// Filled forward, so it also lifts the closed sidebar's dimmed opacity while the peek lasts.
|
||||
animation = content.animate(
|
||||
[
|
||||
{ opacity: 0, transform: `translateX(calc(${direction} * (100% + 16px)))` },
|
||||
{ opacity: 1, transform: "none" },
|
||||
],
|
||||
{ ...(prefersReducedMotion() ? { duration: 0 } : timing()), fill: "both" },
|
||||
)
|
||||
}
|
||||
|
||||
const end = () => {
|
||||
cancelOpen()
|
||||
cancelClose()
|
||||
clearInterval(watch)
|
||||
watch = undefined
|
||||
animation?.cancel()
|
||||
animation = undefined
|
||||
toggle = undefined
|
||||
setShown(false)
|
||||
}
|
||||
|
||||
const hide = (focus?: boolean) => {
|
||||
cancelClose()
|
||||
|
||||
if (!shown()) return
|
||||
clearInterval(watch)
|
||||
watch = undefined
|
||||
const aside = input.aside()
|
||||
|
||||
if (aside?.contains(document.activeElement)) {
|
||||
if (focus && toggle?.isConnected) toggle.focus({ preventScroll: true })
|
||||
if (!focus && document.activeElement instanceof HTMLElement) document.activeElement.blur()
|
||||
}
|
||||
|
||||
if (!animation || prefersReducedMotion()) return end()
|
||||
const current = animation
|
||||
current.reverse()
|
||||
current.onfinish = () => {
|
||||
if (current.playbackRate < 0) end()
|
||||
}
|
||||
}
|
||||
|
||||
// A visible menu, a drag, or typing in one of the sidebar's fields keeps it open wherever the pointer goes.
|
||||
const held = () => {
|
||||
const active = document.activeElement
|
||||
const typing =
|
||||
active instanceof HTMLElement &&
|
||||
input.aside()?.contains(active) &&
|
||||
(active.matches("input, textarea") || active.isContentEditable) &&
|
||||
active.getClientRects().length > 0
|
||||
const menu = [...document.querySelectorAll(menus)].some((element) => element.getClientRects().length > 0)
|
||||
|
||||
return pressed || typing || menu || !!document.querySelector('[data-dragging="true"]')
|
||||
}
|
||||
|
||||
const inside = (point: { x: number; y: number }) => {
|
||||
const panel = input.panel()
|
||||
|
||||
if (!panel) return false
|
||||
const sheet = panel.getBoundingClientRect()
|
||||
const button = toggle?.isConnected ? toggle.getBoundingClientRect() : sheet
|
||||
|
||||
// The box spanning both is the corridor from the toggle down to the sheet's top.
|
||||
return (
|
||||
point.x >= Math.min(sheet.left, button.left) - margin &&
|
||||
point.x <= Math.max(sheet.right, button.right) + margin &&
|
||||
point.y >= Math.min(sheet.top, button.top) - margin &&
|
||||
point.y <= Math.max(sheet.bottom, button.bottom) + margin
|
||||
)
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (!shown() || input.open() || !watch) return
|
||||
|
||||
if ((pointer && inside(pointer)) || held()) return cancelClose()
|
||||
closing ??= setTimeout(() => {
|
||||
closing = undefined
|
||||
|
||||
if (!(pointer && inside(pointer)) && !held()) hide()
|
||||
}, grace)
|
||||
}
|
||||
|
||||
const track = (event: PointerEvent | WheelEvent) => {
|
||||
if (event instanceof PointerEvent && event.pointerType === "touch") return
|
||||
|
||||
if (event instanceof PointerEvent) pressed = event.buttons !== 0
|
||||
pointer = { x: event.clientX, y: event.clientY }
|
||||
check()
|
||||
}
|
||||
|
||||
makeEventListener(
|
||||
document,
|
||||
"pointerover",
|
||||
(event) => {
|
||||
if (event.pointerType === "touch" || !(event.target instanceof Element)) return
|
||||
const button = event.target.closest<HTMLElement>(toggles)
|
||||
|
||||
if (!button || input.aside()?.contains(button)) return
|
||||
toggle = button
|
||||
|
||||
if (shown()) return show()
|
||||
|
||||
if (!sidebarPeekEnabled() || !input.available() || opening) return
|
||||
opening = setTimeout(() => {
|
||||
opening = undefined
|
||||
|
||||
if (sidebarPeekEnabled() && input.available() && toggle?.matches(":hover")) show()
|
||||
}, intent)
|
||||
},
|
||||
{ passive: true },
|
||||
)
|
||||
makeEventListener(
|
||||
document,
|
||||
"pointerout",
|
||||
(event) => {
|
||||
// Leaving the window, or into an iframe or native view, reports no related target.
|
||||
if (!event.relatedTarget) {
|
||||
pointer = undefined
|
||||
check()
|
||||
}
|
||||
|
||||
if (!opening || !(event.target instanceof Element)) return
|
||||
const button = event.target.closest(toggles)
|
||||
|
||||
if (button && !(event.relatedTarget instanceof Node && button.contains(event.relatedTarget))) cancelOpen()
|
||||
},
|
||||
{ passive: true },
|
||||
)
|
||||
makeEventListener(document, "pointermove", track, { passive: true, capture: true })
|
||||
makeEventListener(document, "wheel", track, { passive: true, capture: true })
|
||||
makeEventListener(document, "scroll", () => check(), { passive: true, capture: true })
|
||||
makeEventListener(
|
||||
document,
|
||||
"pointerdown",
|
||||
(event) => {
|
||||
keyboard = false
|
||||
pressed = event.pointerType !== "touch"
|
||||
},
|
||||
{ passive: true, capture: true },
|
||||
)
|
||||
makeEventListener(window, "pointerup", () => (pressed = false), { passive: true, capture: true })
|
||||
makeEventListener(window, "pointercancel", () => (pressed = false), { passive: true, capture: true })
|
||||
makeEventListener(document.documentElement, "pointerleave", () => {
|
||||
pointer = undefined
|
||||
check()
|
||||
})
|
||||
makeEventListener(document, "keydown", (event) => {
|
||||
keyboard = true
|
||||
|
||||
if (event.key !== "Escape" || !shown() || input.open() || event.defaultPrevented || held()) return
|
||||
hide(true)
|
||||
})
|
||||
makeEventListener(window, "blur", () => {
|
||||
pressed = false
|
||||
hide()
|
||||
})
|
||||
|
||||
// Keyboard focus leaving the peek closes it; pointer clicks elsewhere already close it by leaving.
|
||||
createEffect(() => {
|
||||
const aside = input.aside()
|
||||
|
||||
if (!aside) return
|
||||
const leave = (event: FocusEvent) => {
|
||||
const next = event.relatedTarget
|
||||
|
||||
if (!keyboard || !shown() || input.open() || !(next instanceof Element)) return
|
||||
|
||||
if (aside.contains(next) || toggle?.contains(next) || next.closest(menus)) return
|
||||
hide()
|
||||
}
|
||||
aside.addEventListener("focusout", leave)
|
||||
onCleanup(() => aside.removeEventListener("focusout", leave))
|
||||
})
|
||||
|
||||
// Docking hands over to the docked sidebar's own motion.
|
||||
createEffect(on(input.open, (open) => open && end(), { defer: true }))
|
||||
|
||||
createEffect(() => {
|
||||
if (!sidebarPeekEnabled()) end()
|
||||
})
|
||||
|
||||
onCleanup(end)
|
||||
|
||||
return { shown }
|
||||
}
|
||||
|
||||
// The peek's spring, 0.15s without bounce, sampled per 60 Hz frame into a CSS `linear()` easing.
|
||||
function timing() {
|
||||
const motion = Spring.fromDuration(0, { duration: 0.15, bounce: 0 }).set(1)
|
||||
const points = [0]
|
||||
|
||||
while (points.length < 240) {
|
||||
points.push(Math.round(motion.step(1 / 60) * 1000) / 1000)
|
||||
|
||||
if (Math.abs(1 - motion.value) < 0.005 && Math.abs(motion.velocity) < 0.05) break
|
||||
}
|
||||
|
||||
points.push(1)
|
||||
|
||||
return { duration: ((points.length - 1) * 1000) / 60, easing: `linear(${points.join(", ")})` }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, onCleanup, Show } from "solid-js"
|
||||
import { createEffect, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
@@ -8,6 +8,32 @@ import { useCommand } from "@/shell/commands/command"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { motionTuning } from "@opencode/ui/motion-tuning"
|
||||
import { sidebarPeekEnabled, sidebarUnread } from "@/shell/sidebar-peek"
|
||||
|
||||
// The Sidebar peek experiment shows the closed sidebar's toggle as a menu.
|
||||
const [peek, setPeek] = createSignal(sidebarPeekEnabled())
|
||||
|
||||
motionTuning.subscribe(() => setPeek(sidebarPeekEnabled()))
|
||||
|
||||
const closedIcon = () => (peek() ? "menu" : "layout-left")
|
||||
|
||||
// The menu icon's dot for sidebar sessions with unread activity, shown while the sidebar is closed.
|
||||
function UnreadBadge(props: { show: boolean; ring: "base" | "deep" }) {
|
||||
return (
|
||||
<Show when={props.show}>
|
||||
<span
|
||||
data-slot="sidebar-toggle-unread"
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -top-0.5 -end-0.5 size-[7px] rounded-full bg-v2-background-bg-accent"
|
||||
classList={{
|
||||
"shadow-[0_0_0_1.5px_var(--v2-background-bg-base)]": props.ring === "base",
|
||||
"shadow-[0_0_0_1.5px_var(--v2-background-bg-deep)]": props.ring === "deep",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// The desktop window sets `trafficLightPosition: { x: 14, y: 14 }`, and the titlebar moves the buttons down to share the
|
||||
// header's center line in vertical tabs. macOS 26 and later draw 14px buttons on a 23px pitch, so the zoom button ends
|
||||
@@ -44,7 +70,8 @@ export function SidebarToggle(props: { handoff?: "out" | "in" }) {
|
||||
const keybind = () => command.keybindParts("sidebar.toggle")
|
||||
const side = props.handoff
|
||||
let full: SVGSVGElement | undefined
|
||||
let outline: SVGSVGElement | undefined
|
||||
let outline: HTMLSpanElement | undefined
|
||||
const unread = () => peek() && !settings.appearance.sidebarOpen() && sidebarUnread()
|
||||
const ref =
|
||||
side === "out"
|
||||
? collapsedRef((button, closed) => {
|
||||
@@ -70,6 +97,8 @@ export function SidebarToggle(props: { handoff?: "out" | "in" }) {
|
||||
<Tooltip
|
||||
class="shrink-0 [app-region:no-drag]"
|
||||
placement="bottom"
|
||||
// Held shut while the closed sidebar peeks on hover, where it would cover the peek.
|
||||
forceOpen={peek() && !settings.appearance.sidebarOpen() ? false : undefined}
|
||||
value={
|
||||
<>
|
||||
{language.t("command.sidebar.toggle")}
|
||||
@@ -87,19 +116,25 @@ export function SidebarToggle(props: { handoff?: "out" | "in" }) {
|
||||
class="shrink-0"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => settings.appearance.setSidebarOpen(!settings.appearance.sidebarOpen())}
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-label={language.t(unread() ? "command.sidebar.toggle.unread" : "command.sidebar.toggle")}
|
||||
aria-expanded={settings.appearance.sidebarOpen()}
|
||||
aria-controls="vertical-tabs-sidebar"
|
||||
icon={
|
||||
side === "in" ? (
|
||||
<span class="relative flex">
|
||||
<Icon ref={full} name="layout-left-full" />
|
||||
<Icon ref={outline} name="layout-left" class="absolute inset-0" />
|
||||
<span ref={outline} class="absolute inset-0 flex">
|
||||
<Icon name={closedIcon()} />
|
||||
<UnreadBadge show={unread()} ring="base" />
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<Icon
|
||||
name={side === "out" || (!side && settings.appearance.sidebarOpen()) ? "layout-left-full" : "layout-left"}
|
||||
/>
|
||||
<span class="relative flex">
|
||||
<Icon
|
||||
name={side === "out" || (!side && settings.appearance.sidebarOpen()) ? "layout-left-full" : closedIcon()}
|
||||
/>
|
||||
<UnreadBadge show={!side && unread()} ring="deep" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { sessionTreeIDs } from "@/session/requests/session-request-tree"
|
||||
import { sessionTitle } from "@/session/title"
|
||||
import { errorMessage } from "@/shell/layout/helpers"
|
||||
import { notifySessionTabsRemoved } from "./session-events"
|
||||
import { setSidebarUnread } from "@/shell/sidebar-peek"
|
||||
import type { SessionInfo } from "@opencode/client/promise"
|
||||
|
||||
export const previousSessionCommand = { id: "session.previous", keybind: "alt+arrowup" }
|
||||
@@ -396,6 +397,13 @@ export function TitlebarTabStrip(props: {
|
||||
const visibleTabs = createMemo(() => props.tabs.filter((tab) => tab.type === "draft" || visibility[tabKey(tab)]))
|
||||
const visibleTabIds = () => visibleTabs().map(tabKey)
|
||||
|
||||
// Tells the closed sidebar's toggle whether any row here shows its unread dot, which the current tab never does.
|
||||
createEffect(() => {
|
||||
if (!vertical()) return
|
||||
setSidebarUnread(props.tabs.some((tab) => tab !== props.currentTab && !!unread[tabKey(tab)]))
|
||||
onCleanup(() => setSidebarUnread(false))
|
||||
})
|
||||
|
||||
command.register("titlebar-tab-cycle", () => [
|
||||
{
|
||||
id: `tab.prev`,
|
||||
|
||||
@@ -796,6 +796,8 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
|
||||
{/* Shares the session header's 48px center line. Without traffic lights, Home fills the
|
||||
start so the row doesn't leave an empty band. */}
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-home={homeInSidebarHeader() ? "" : undefined}
|
||||
class="flex w-full shrink-0 gap-2"
|
||||
classList={{
|
||||
"items-start pt-2.5 pb-1": homeInSidebarHeader(),
|
||||
|
||||
Reference in New Issue
Block a user