fix(desktop): keep page headers clear of the traffic lights (#6098)

* fix(desktop): keep page headers clear of the traffic lights

In the desktop app, collapsing the sidebar hands the top-left of the screen
to the content pane — the macOS traffic lights and the sidebar expander
then sit over page content rather than over the sidebar. Every top-of-page
header bar drew straight underneath them, so back buttons and the
integrations/skills switcher were unreadable and unclickable.

The reason it hit all of them at once is that the bar's geometry was copied
verbatim into seven files, so nothing could reserve that lane in one place.
Extract it as PAGE_HEADER_BAR and fold the lane into its top padding via
`--workspace-content-title-bar-inset`, the variable the content pane
already publishes for exactly this. That variable is 0px everywhere except
the macOS desktop app with the sidebar collapsed, so this is inert on the
web and on an expanded sidebar, where the lights sit over sidebar chrome
that already reserves its own lane.

Covers the settings shell, the credential/skill detail layout, the
integrations and skills switcher, integration block detail and its
fallback, and both upgrade headers. A guard test fails with the file named
if any page re-derives the bar instead of composing it.

* fix(desktop): cover the Resource header bar too

The first pass found its surfaces by grepping one exact class string, which
turned up the minority header. `ResourceHeader` is the same bar written as
`px-4 py-[8.5px]`, and it is the one logs, files, tables, knowledge and
scheduled tasks use — plus every loading fallback. Those all still drew
under the traffic lights, and on the breadcrumb surfaces the occluded
element is an interactive popover button, not just a title.

Share the lane math as TITLE_BAR_LANE_PT and compose it from both bar
geometries, which genuinely differ (the Resource bar is bordered and has a
min height). Nothing nests one bar inside the other, so no reset is needed:
the only out-of-pane render is the landing tables preview.

Move the variable's `0px` default to `:root` alongside the other
desktop-title-bar vars, so it is defined for bars outside
`.workspace-content-shell` — the standalone settings shell and that landing
preview — where an undefined var() inside calc() would be invalid at
computed-value time and drop padding-top entirely. That replaces the
per-call fallback, leaving one default instead of two that can drift.

Fold the assertions into the existing desktop title-bar surface audit
rather than a second audit file with its own conventions. The standalone
guard is gone: it keyed on one spelling of the geometry, so it was blind to
`px-4 py-[8.5px]` — the very re-derivation that made this pass necessary.

* fix(desktop): cover fullscreen routes, spare embedded panels

Review round 2 found the two arrangements the collapsed-sidebar selector
alone gets wrong.

A fullscreen route (/upgrade) slides the sidebar to zero width without
collapsing it, so `data-sidebar-collapsed` is absent and the lane stayed
zero while the pane was in fact sitting under the traffic lights. The pane
owns the lane whenever the sidebar is not there to own it, so the selector
now matches a new `data-content-fullscreen` as well.

The mirror error: the variable is inherited, and the mothership panel is
the right half of the pane — never under the lights — yet it embeds whole
pages (KnowledgeBase and friends) whose header bars reserve the lane. Those
bars were gaining ~38px inside the panel. The panel now zeroes the variable
for its subtree.

I had checked for exactly this nesting and concluded it did not exist,
having looked only at the settings pages that import `Resource` types
without rendering `Resource.Header`. `resource-content.tsx` renders the
knowledge page itself, which does.

An assertion each, both verified to fail when the fix is reverted.

* fix(desktop): size the peek card to its content, soften the overlay shadow

The floating sidebar pinned both its top and bottom edges, so it always drew at
full window height. On a short surface — the settings list — that left a tall
empty slab of card hanging below the last entry.

The card now hugs its content and caps at the pane height less the traffic-light
lane and the bottom gutter. Dropping the bottom pin is most of it: the four
`h-full` rules down the chain resolve against an auto-height parent and collapse
to content on their own. But nothing would then bound the sidebar's own
`flex-1 overflow-y-auto` region, so a long workflow list would be clipped by the
card's `overflow-hidden` instead of scrolling. The card is therefore a capped
flex column, and the shell is allowed to shrink inside it, which restores a
definite height for the chain to resolve against. That rule is scoped to
`[data-peek]` and is inert while docked, where the shell is not a flex item.

Also eases `--shadow-overlay` in both themes (alpha ~27% lighter, bloom pulled in
from 48px to 30px), keeping it clearly above `--shadow-medium` so the scale still
reads in order. The peek card drops the shadow entirely and separates on the same
`--border` hairline the content pane beside it uses.

* fix(desktop): stop the login page scrolling, drop the pane border at the window edge

Two reports against the desktop window chrome.

The login page scrolled by exactly the traffic-light lane. `.desktop-title-bar-page`
reserved the lane with `margin-top` plus a `calc(100vh - lane)` height, which sums to
the viewport on its own — but `body` carries `min-height: 100vh`, and body is a plain
block box with no padding, border, or BFC, so that top margin had nothing to collapse
against and collapsed through, displacing body itself. The document came out one full
lane taller than the viewport while the shell's `calc` saving was re-inflated
underneath it. Reserving the lane with padding *inside* the box removes both the
collapse and the `calc`: global `box-sizing: border-box` keeps the padding within the
`100vh`. Measured in the Electron renderer over CDP — 40px of overflow before, 0 after,
with the logo still clear of the lane.

Collapsing the sidebar also left a hairline outline traced just inside the window. The
shell drops to `p-0` there, but the content pane kept its border and 8px radius, so
both drew flush against the square window frame. The pane now drops them exactly when
it is flush. Keyed off the ancestor attributes rather than React state, because the
title-bar attribute is written pre-paint and a state-driven rule would flash the border
before hydration settles.

* docs(desktop): correct the peek card chrome comment

The card's TSDoc still listed `shadow-overlay` as part of its chrome after the
shadow was deliberately dropped, so the comment contradicted the code. It now
records that the card is unshadowed on purpose, and documents the content-hugging
height and the flex-column cap that make the sidebar's scroll region bound itself.

---------

Co-authored-by: Waleed Latif <waleed@simstudio.ai>
This commit is contained in:
Waleed
2026-07-30 14:25:07 -07:00
committed by GitHub
co-authored by Waleed Latif
parent 811a39ec05
commit c9547dcfe5
12 changed files with 181 additions and 23 deletions
@@ -14,6 +14,13 @@ const workspaceChrome = read(
)
const sidebar = read('../workspace/[workspaceId]/w/components/sidebar/sidebar.tsx')
const globalStyles = read('../_styles/globals.css')
const pageHeaderBar = read('../../components/page-header-bar.ts')
const resourceHeader = read(
'../workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx'
)
const mothershipView = read(
'../workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx'
)
describe('desktop title-bar surface audit', () => {
it('applies the safe-area shell only when the auth route is login', () => {
@@ -65,4 +72,70 @@ describe('desktop title-bar surface audit', () => {
expect(sidebar).toContain('[[data-sim-desktop-title-bar=inset]_&]:pt-[var(')
expect(sidebar).not.toMatch(/\[\[data-sim-desktop-title-bar=inset\]_&\]:pt-\d/)
})
it('defines the content-pane lane once, defaulting to zero', () => {
// A `:root` default keeps the variable defined for bars that render outside
// `.workspace-content-shell` (the standalone settings shell at /account,
// /organization/[id], /selfhost; the landing tables preview). An undefined var()
// inside calc() is invalid at computed-value time and drops padding-top entirely.
expect(globalStyles).toMatch(/:root\s*\{[^}]*--workspace-content-title-bar-inset:\s*0px/s)
// The pane owns the lane in both arrangements where the sidebar is not there to
// own it: collapsed to zero width, and slid away for a fullscreen route (which
// leaves the sidebar expanded in the store, so the collapsed selector alone misses
// it and /upgrade's back chip lands under the traffic lights).
expect(globalStyles).toContain('.workspace-content-shell[data-sidebar-collapsed],')
expect(globalStyles).toContain('.workspace-content-shell[data-content-fullscreen] {')
expect(workspaceChrome).toContain('data-content-fullscreen={isFullscreen || undefined}')
})
it('sizes the peek card to its content, capped against the lane', () => {
// Pinning both edges made the card full height, so a short list (settings) left a
// tall empty slab over the content. It now hugs its content and caps at the pane
// height less the lane and the bottom gutter, so a long list still scrolls.
expect(workspaceChrome).toContain('max-h-[calc(100%-var(--desktop-title-bar-height)-8px)]')
expect(workspaceChrome).not.toMatch(/PEEK_CARD_CHROME[\s\S]{0,240}?bottom-2/)
})
it('reserves the login lane inside the box, never as a collapsing margin', () => {
// `body` carries `min-height: 100vh`, and a `margin-top` here collapses through it
// (body is a plain block box, so it opens no BFC) and displaces body itself. The
// document then measured one full lane taller than the viewport, which is what made
// the desktop login page scroll. Verified live: 40px of overflow, now 0.
// Comments are stripped first: the rule documents why `margin-top` is wrong, and a
// raw `not.toContain` would match that prose instead of a declaration.
const rule = (globalStyles.match(/\.desktop-title-bar-page \{[^}]*\}/)?.[0] ?? '').replace(
/\/\*[\s\S]*?\*\//g,
''
)
expect(rule).toContain('padding-top: var(--desktop-title-bar-height)')
expect(rule).toContain('min-height: 100vh')
expect(rule).not.toContain('margin-top')
})
it('drops the content pane border where the pane meets the window edge', () => {
// Collapsing the sidebar in the desktop shell takes the pane's padding to 0, so a
// retained border and radius drew a hairline outline inset from the square window.
const flush = '[[data-sim-desktop-title-bar=inset]_[data-sidebar-collapsed]_&]:'
expect(workspaceChrome).toContain(`${flush}rounded-none`)
expect(workspaceChrome).toContain(`${flush}border-0`)
})
it('clears the lane for panels that embed pages away from the lights', () => {
// The mothership panel is the right half of the pane and embeds whole pages
// (KnowledgeBase et al) whose header bars reserve the lane. It inherits the
// variable, so without this reset those bars gain the inset while sitting nowhere
// near the traffic lights.
expect(mothershipView).toContain('[--workspace-content-title-bar-inset:0px]')
})
it('reserves that lane in every top-of-pane header bar', () => {
// Both top-bar geometries must compose the shared lane padding. A bare
// `pt-`/`py-[8.5px]` in either is the bug: the bar then draws under the traffic
// lights and the sidebar expander whenever the sidebar is collapsed on desktop.
expect(pageHeaderBar).toContain('pt-[calc(8.5px+var(--workspace-content-title-bar-inset))]')
expect(pageHeaderBar).toContain('TITLE_BAR_LANE_PT')
expect(resourceHeader).toContain('TITLE_BAR_LANE_PT')
expect(resourceHeader).not.toMatch(/py-\[8\.5px\]/)
})
})
+23 -9
View File
@@ -14,6 +14,7 @@
--sidebar-collapsed-width: 51px; /* icon rail on web; desktop overrides to 0 before first paint */
--sidebar-expanded-width: 248px; /* SIDEBAR_WIDTH.DEFAULT; the width to restore to, held even while collapsed */
--desktop-title-bar-height: 0px; /* macOS traffic-light lane; desktop overrides before first paint */
--workspace-content-title-bar-inset: 0px; /* lane the content pane must leave clear; only non-zero when the pane, not the sidebar, sits under it */
--desktop-title-bar-inset-x: 0px; /* clearance past the traffic lights; desktop overrides */
--desktop-title-bar-control-offset: 0px; /* centres a lane control; desktop overrides */
--desktop-title-bar-control-size: 0px; /* control seated in the lane; desktop overrides */
@@ -43,7 +44,7 @@
/* Shadow scale */
--shadow-subtle: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-overlay: 0 16px 48px rgba(0, 0, 0, 0.15);
--shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.11);
--shadow-kbd: 0 4px 0 0 rgba(48, 48, 48, 1);
--shadow-kbd-sm: 0 2px 0 0 rgba(48, 48, 48, 1);
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04);
@@ -75,8 +76,14 @@ html[data-sim-desktop-title-bar="inset"] {
/** The macOS desktop login shell reserves the native traffic-light lane. */
.desktop-title-bar-page {
margin-top: var(--desktop-title-bar-height);
min-height: calc(100vh - var(--desktop-title-bar-height));
/* Reserve the lane with padding INSIDE the box, never `margin-top` + a
`calc(100vh - lane)` height. `body` carries `min-height: 100vh` of its own, and
a top margin here has nothing to collapse against (body is a block box with no
padding, border, or BFC), so it collapses through and displaces body itself —
leaving a document one full lane taller than the viewport. Global
`box-sizing: border-box` keeps this padding inside the 100vh. */
padding-top: var(--desktop-title-bar-height);
min-height: 100vh;
}
.desktop-window-drag-region {
@@ -122,11 +129,10 @@ html[data-sim-desktop-title-bar="inset"]
letter-spacing: 0.02em;
}
.workspace-content-shell {
--workspace-content-title-bar-inset: 0px;
}
.workspace-content-shell[data-sidebar-collapsed] {
/* The pane owns the lane whenever the sidebar is not there to own it: collapsed to
zero width, or slid away for a fullscreen route. */
.workspace-content-shell[data-sidebar-collapsed],
.workspace-content-shell[data-content-fullscreen] {
--workspace-content-title-bar-inset: var(--desktop-title-bar-height);
}
@@ -173,6 +179,14 @@ html[data-sim-desktop-title-bar="inset"]
transition: none;
}
/* The card is a flex column sized to its content, so the shell must be allowed to
shrink for the sidebar's own scroll region to bound itself once the card hits its
max height. Docked, this element is not a flex item and the rule is inert. */
.sidebar-shell-outer[data-peek] .sidebar-shell-inner {
min-height: 0;
flex-shrink: 1;
}
.sidebar-container span,
.sidebar-container .text-small {
transition: opacity 120ms ease;
@@ -583,7 +597,7 @@ html.sidebar-booting .sidebar-shell-inner {
--scrollbar-thumb-hover-color: #6a6a6a;
/* Shadow scale - dark */
--shadow-overlay: 0 16px 48px rgba(0, 0, 0, 0.4);
--shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.3);
}
}
@@ -1,4 +1,6 @@
import type { ReactNode } from 'react'
import { cn } from '@sim/emcn'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
interface CredentialDetailLayoutProps {
/** Back link rendered at the start of the fixed action bar. */
@@ -17,7 +19,7 @@ interface CredentialDetailLayoutProps {
export function CredentialDetailLayout({ back, actions, children }: CredentialDetailLayoutProps) {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center justify-between bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={cn(PAGE_HEADER_BAR, 'justify-between')}>
{back}
{actions ? <div className='flex items-center'>{actions}</div> : null}
</div>
@@ -31,6 +31,7 @@ import {
} from '@sim/emcn'
import { ArrowUpLeft } from 'lucide-react'
import { createPortal } from 'react-dom'
import { TITLE_BAR_LANE_PT } from '@/components/page-header-bar'
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'
import { FloatingOverflowText } from '@/app/workspace/[workspaceId]/components/resource/components/floating-overflow-text'
@@ -131,7 +132,10 @@ export const ResourceHeader = memo(function ResourceHeader({
return (
<div
ref={headerRef}
className='flex min-h-[48px] items-center border-[var(--border)] border-b px-4 py-[8.5px]'
className={cn(
'flex min-h-[48px] items-center border-[var(--border)] border-b px-4 pb-[8.5px]',
TITLE_BAR_LANE_PT
)}
>
<div className='flex min-w-0 flex-1 items-center justify-between gap-3'>
<div className='flex min-w-0 flex-1 items-center gap-2 overflow-hidden'>
@@ -22,9 +22,18 @@ const SLIDE_TRANSITION =
* The peek card's floating chrome.
*
* Every value is an existing token: `rounded-lg` is `--radius`, matching the content
* pane it floats beside; `--border` is that pane's border; `shadow-overlay` and
* `--z-modal` are what the app's other edge-anchored panels use. The card's fill is
* the sidebar's own `--surface-1`, so docked and floating are the same surface.
* pane it floats beside; `--border` is that pane's border; `--z-modal` is what the
* app's other edge-anchored panels use. The card's fill is the sidebar's own
* `--surface-1`, so docked and floating are the same surface.
*
* Deliberately unshadowed. It separates on the same `--border` hairline the content
* pane beside it uses; `--shadow-overlay` reads as too heavy at this size, where the
* card abuts the window edge rather than floating over the middle of the page.
*
* The card hugs its content and caps at the pane height less the lane and the bottom
* gutter — pinning both edges left a tall empty slab below short lists. It is a flex
* column so the shell can shrink inside that cap and the sidebar's own scroll region
* still bounds itself; see the `[data-peek]` rule in `globals.css`.
*
* `w-auto` shrink-wraps the inner shell, which `[data-peek]` has already put at the
* expanded width. It must not be a length: `width` cannot interpolate to or from
@@ -32,7 +41,7 @@ const SLIDE_TRANSITION =
* card widens as it appears and leaves a shrinking ghost on retract.
*/
const PEEK_CARD_CHROME =
'absolute top-[var(--desktop-title-bar-height)] bottom-2 left-2 z-[var(--z-modal)] w-auto origin-top-left rounded-lg border border-[var(--border)] shadow-overlay'
'absolute top-[var(--desktop-title-bar-height)] left-2 z-[var(--z-modal)] flex max-h-[calc(100%-var(--desktop-title-bar-height)-8px)] w-auto flex-col origin-top-left rounded-lg border border-[var(--border)]'
/**
* Peek card enter/exit — the popper idiom rather than a slide, since the card is
@@ -57,6 +66,20 @@ const PEEK_CARD_EXIT = cn(
/** The docked rail: in flow, width-animated by the collapse toggle. */
const SIDEBAR_SHELL_IN_FLOW = cn('transition-[width]', SLIDE_TRANSITION)
/**
* The content pane's own chrome, dropped when the pane sits flush to the window.
*
* Collapsing the sidebar in the desktop shell takes the surrounding padding to `0`,
* which puts the pane hard against the window edge — and its border and radius then
* draw a hairline outline with rounded corners inset from the square window frame.
*
* Keyed off the ancestor attributes rather than React state on purpose: the title-bar
* attribute is written pre-paint, so a state-driven rule would flash the border on
* first paint before hydration settles.
*/
const CONTENT_PANE_FLUSH =
'[[data-sim-desktop-title-bar=inset]_[data-sidebar-collapsed]_&]:rounded-none [[data-sim-desktop-title-bar=inset]_[data-sidebar-collapsed]_&]:border-0'
interface WorkspaceChromeProps {
children: React.ReactNode
/** Cookie-derived collapse state from the server layout; seeds the sidebar's first render. */
@@ -295,8 +318,16 @@ export function WorkspaceChrome({
isCollapsed && '[[data-sim-desktop-title-bar=inset]_&]:p-0'
)}
data-sidebar-collapsed={isCollapsed || undefined}
/* A fullscreen route slides the sidebar away without collapsing it, so the pane
inherits the traffic-light lane the same way a collapsed sidebar does. */
data-content-fullscreen={isFullscreen || undefined}
>
<div className='flex-1 overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
'flex-1 overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)]',
CONTENT_PANE_FLUSH
)}
>
{children}
</div>
</div>
@@ -137,6 +137,10 @@ export const MothershipView = memo(
className={cn(
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-1/2 border-l',
/* This panel is the right half of the pane, never under the traffic lights,
yet it embeds whole pages whose header bars reserve that lane. Zeroing the
inherited variable here keeps their top bars flush inside the panel. */
'[--workspace-content-title-bar-inset:0px]',
className
)}
>
@@ -2,6 +2,7 @@
import { ChipLink } from '@sim/emcn'
import { ArrowLeft } from 'lucide-react'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
interface IntegrationBlockDetailFallbackProps {
workspaceId: string
@@ -23,7 +24,7 @@ export function IntegrationBlockDetailFallback({
}: IntegrationBlockDetailFallbackProps) {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={PAGE_HEADER_BAR}>
<ChipLink href={`/workspace/${workspaceId}/integrations`} leftIcon={ArrowLeft}>
Integrations
</ChipLink>
@@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight, Plus } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useQueryState } from 'nuqs'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
import {
blockTypeToIconMap,
type Integration,
@@ -138,7 +139,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={PAGE_HEADER_BAR}>
<ChipLink href={`/workspace/${workspaceId}/integrations`} leftIcon={ArrowLeft}>
Integrations
</ChipLink>
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import { ChipLink } from '@sim/emcn'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
interface IntegrationTabsHeaderProps {
active: 'integrations' | 'skills'
@@ -18,7 +19,7 @@ export function IntegrationTabsHeader({
rightSlot,
}: IntegrationTabsHeaderProps) {
return (
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={PAGE_HEADER_BAR}>
<ChipLink href={`/workspace/${workspaceId}/integrations`} active={active === 'integrations'}>
Integrations
</ChipLink>
@@ -5,6 +5,7 @@ import { ArrowLeft, Chip, toast } from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { useRouter } from 'next/navigation'
import { useQueryState } from 'nuqs'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
import { useSession } from '@/lib/auth/auth-client'
import {
getUpgradeCardCta,
@@ -99,7 +100,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={PAGE_HEADER_BAR}>
<Chip leftIcon={ArrowLeft} onClick={handleBack}>
Back
</Chip>
@@ -192,7 +193,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={PAGE_HEADER_BAR}>
<Chip leftIcon={ArrowLeft} onClick={handleBack}>
Back
</Chip>
+25
View File
@@ -0,0 +1,25 @@
/**
* Top padding for a bar sitting at the very top of the workspace content pane.
*
* Folds in `--workspace-content-title-bar-inset`, the height that pane must leave
* clear for the desktop shell's inset title bar. That variable is `0px` everywhere
* except the macOS desktop app with the sidebar collapsed — the one arrangement where
* the pane, rather than the sidebar, sits beneath the traffic lights and the sidebar
* expander. Without it a top bar draws underneath both, which hides its controls and
* can leave them unclickable.
*
* Compose this rather than writing `pt-[8.5px]`: the app has two top-bar geometries
* ({@link PAGE_HEADER_BAR} and the `Resource` header's bordered variant), and the lane
* math has to stay identical across them.
*/
export const TITLE_BAR_LANE_PT = 'pt-[calc(8.5px+var(--workspace-content-title-bar-inset))]'
/**
* The top-of-page header bar worn by the surfaces that put a back chip, tab switcher,
* or page actions above their content. `Resource`-based pages (tables, files, logs,
* knowledge, scheduled tasks) use their own bordered bar and compose
* {@link TITLE_BAR_LANE_PT} directly.
*
* Single source of truth for this geometry — never re-derive it per page.
*/
export const PAGE_HEADER_BAR = `flex flex-shrink-0 items-center bg-[var(--bg)] px-4 ${TITLE_BAR_LANE_PT} pb-[8.5px]`
@@ -13,7 +13,8 @@ import {
useRef,
useState,
} from 'react'
import { Chip, ChipInput, ChipLink, Search, Tooltip } from '@sim/emcn'
import { Chip, ChipInput, ChipLink, cn, Search, Tooltip } from '@sim/emcn'
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
@@ -123,7 +124,7 @@ export function SettingsHeaderShell({ children }: { children: ReactNode }) {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='flex flex-shrink-0 items-center justify-between bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
<div className={cn(PAGE_HEADER_BAR, 'justify-between')}>
{back ? (
<Chip leftIcon={back.icon} onClick={() => configRef?.current.back?.onSelect()}>
{back.text}