Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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\]/)
})
})
32 changes: 23 additions & 9 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -122,11 +129,10 @@ html[data-sim-desktop-title-bar="inset"]
letter-spacing: 0.02em;
}

.workspace-content-shell {
--workspace-content-title-bar-inset: 0px;
}

Comment thread
waleedlatif1 marked this conversation as resolved.
.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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
)}
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
>
<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'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,26 @@ 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
* `auto`, so entering and leaving the peek snap instead of animating — otherwise the
* 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)]'
Comment thread
waleedlatif1 marked this conversation as resolved.

/**
* Peek card enter/exit — the popper idiom rather than a slide, since the card is
Expand All @@ -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. */
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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>
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
25 changes: 25 additions & 0 deletions apps/sim/components/page-header-bar.ts
Original file line number Diff line number Diff line change
@@ -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]`
5 changes: 3 additions & 2 deletions apps/sim/components/settings/settings-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand Down
Loading