diff --git a/apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts b/apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts
index 50287ceaa35..4b0c822220d 100644
--- a/apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts
+++ b/apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts
@@ -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\]/)
+ })
})
diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css
index 87c1a884809..1ba253d00b5 100644
--- a/apps/sim/app/_styles/globals.css
+++ b/apps/sim/app/_styles/globals.css
@@ -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);
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-detail-layout.tsx b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-detail-layout.tsx
index 5753feabdcb..d4099a6a6f7 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-detail-layout.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-detail-layout.tsx
@@ -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 (
-
+
{back}
{actions ?
{actions}
: null}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
index f6b464404cd..a28d735a1f8 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
@@ -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 (
diff --git a/apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx b/apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx
index 7f206c26da2..6609facab82 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx
@@ -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}
>
-
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
index 01a1ddd3d69..48957e8fcd4 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
@@ -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
)}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx
index e634e7806e7..df7af0e6c8a 100644
--- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx
@@ -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 (
-
+
Integrations
diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx
index 0bbf99b3630..6ad76f5996e 100644
--- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx
@@ -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 (
-
+
Integrations
diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx
index 66fc7fb2f22..b4d71e20bab 100644
--- a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx
@@ -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 (
-
+
Integrations
diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx
index 446469a2dc6..20e698d4a2c 100644
--- a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx
@@ -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 (
-
+
Back
@@ -192,7 +193,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
return (
-
+
Back
diff --git a/apps/sim/components/page-header-bar.ts b/apps/sim/components/page-header-bar.ts
new file mode 100644
index 00000000000..1c1fb8e5ce9
--- /dev/null
+++ b/apps/sim/components/page-header-bar.ts
@@ -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]`
diff --git a/apps/sim/components/settings/settings-header.tsx b/apps/sim/components/settings/settings-header.tsx
index 8e5f92bc49f..98d507b3d01 100644
--- a/apps/sim/components/settings/settings-header.tsx
+++ b/apps/sim/components/settings/settings-header.tsx
@@ -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 (
-
+
{back ? (
configRef?.current.back?.onSelect()}>
{back.text}