Skip to content

Commit 4fdffed

Browse files
author
Waleed Latif
committed
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.
1 parent 6647808 commit 4fdffed

8 files changed

Lines changed: 84 additions & 8 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-detail-layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { ReactNode } from 'react'
2+
import { cn } from '@sim/emcn'
3+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
24

35
interface CredentialDetailLayoutProps {
46
/** Back link rendered at the start of the fixed action bar. */
@@ -17,7 +19,7 @@ interface CredentialDetailLayoutProps {
1719
export function CredentialDetailLayout({ back, actions, children }: CredentialDetailLayoutProps) {
1820
return (
1921
<div className='flex h-full flex-col bg-[var(--bg)]'>
20-
<div className='flex flex-shrink-0 items-center justify-between bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
22+
<div className={cn(PAGE_HEADER_BAR, 'justify-between')}>
2123
{back}
2224
{actions ? <div className='flex items-center'>{actions}</div> : null}
2325
</div>

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { ChipLink } from '@sim/emcn'
44
import { ArrowLeft } from 'lucide-react'
5+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
56

67
interface IntegrationBlockDetailFallbackProps {
78
workspaceId: string
@@ -23,7 +24,7 @@ export function IntegrationBlockDetailFallback({
2324
}: IntegrationBlockDetailFallbackProps) {
2425
return (
2526
<div className='flex h-full flex-col bg-[var(--bg)]'>
26-
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
27+
<div className={PAGE_HEADER_BAR}>
2728
<ChipLink href={`/workspace/${workspaceId}/integrations`} leftIcon={ArrowLeft}>
2829
Integrations
2930
</ChipLink>

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight, Plus } from 'lucide-react'
66
import Link from 'next/link'
77
import { useRouter } from 'next/navigation'
88
import { useQueryState } from 'nuqs'
9+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
910
import {
1011
blockTypeToIconMap,
1112
type Integration,
@@ -129,7 +130,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
129130

130131
return (
131132
<div className='flex h-full flex-col bg-[var(--bg)]'>
132-
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
133+
<div className={PAGE_HEADER_BAR}>
133134
<ChipLink href={`/workspace/${workspaceId}/integrations`} leftIcon={ArrowLeft}>
134135
Integrations
135136
</ChipLink>

apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ReactNode } from 'react'
22
import { ChipLink } from '@sim/emcn'
3+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
34

45
interface IntegrationTabsHeaderProps {
56
active: 'integrations' | 'skills'
@@ -18,7 +19,7 @@ export function IntegrationTabsHeader({
1819
rightSlot,
1920
}: IntegrationTabsHeaderProps) {
2021
return (
21-
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
22+
<div className={PAGE_HEADER_BAR}>
2223
<ChipLink href={`/workspace/${workspaceId}/integrations`} active={active === 'integrations'}>
2324
Integrations
2425
</ChipLink>

apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ArrowLeft, Chip, toast } from '@sim/emcn'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { useRouter } from 'next/navigation'
77
import { useQueryState } from 'nuqs'
8+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
89
import { useSession } from '@/lib/auth/auth-client'
910
import {
1011
getUpgradeCardCta,
@@ -99,7 +100,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
99100

100101
return (
101102
<div className='flex h-full flex-col bg-[var(--bg)]'>
102-
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
103+
<div className={PAGE_HEADER_BAR}>
103104
<Chip leftIcon={ArrowLeft} onClick={handleBack}>
104105
Back
105106
</Chip>
@@ -192,7 +193,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
192193

193194
return (
194195
<div className='flex h-full flex-col bg-[var(--bg)]'>
195-
<div className='flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
196+
<div className={PAGE_HEADER_BAR}>
196197
<Chip leftIcon={ArrowLeft} onClick={handleBack}>
197198
Back
198199
</Chip>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { readdirSync, readFileSync, statSync } from 'node:fs'
5+
import { join } from 'node:path'
6+
import { describe, expect, it } from 'vitest'
7+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
8+
9+
/** The geometry every page header bar shares, minus the top padding. */
10+
const BAR_SIGNATURE = 'px-[16px]'
11+
const LANE_VAR = 'var(--workspace-content-title-bar-inset)'
12+
13+
const ROOTS = ['app', 'components'].map((dir) => join(process.cwd(), dir))
14+
15+
function* sourceFiles(dir: string): Generator<string> {
16+
for (const entry of readdirSync(dir)) {
17+
if (entry === 'node_modules' || entry.startsWith('.')) continue
18+
const full = join(dir, entry)
19+
if (statSync(full).isDirectory()) {
20+
yield* sourceFiles(full)
21+
continue
22+
}
23+
if (/\.tsx?$/.test(entry)) yield full
24+
}
25+
}
26+
27+
describe('PAGE_HEADER_BAR', () => {
28+
it('reserves the desktop title-bar lane in its top padding', () => {
29+
// Without this the bar sits under the macOS traffic lights and the sidebar
30+
// expander whenever the sidebar is collapsed in the desktop app.
31+
expect(PAGE_HEADER_BAR).toContain(LANE_VAR)
32+
expect(PAGE_HEADER_BAR).toContain(BAR_SIGNATURE)
33+
})
34+
35+
it('is the only definition of the page header bar geometry', () => {
36+
const offenders: string[] = []
37+
for (const root of ROOTS) {
38+
for (const file of sourceFiles(root)) {
39+
if (file.endsWith(join('components', 'page-header-bar.ts'))) continue
40+
const source = readFileSync(file, 'utf8')
41+
// A header bar re-derived inline: the shared gutter plus the old fixed top
42+
// padding, rather than composing PAGE_HEADER_BAR.
43+
if (source.includes(`${BAR_SIGNATURE} pt-[8.5px]`)) {
44+
offenders.push(file.replace(process.cwd(), '.'))
45+
}
46+
}
47+
}
48+
49+
expect(offenders).toEqual([])
50+
})
51+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* The canonical top-of-page header bar worn by every workspace surface that puts a
3+
* back chip, tab switcher, or page actions above its content (settings, integrations,
4+
* skills, credential and block detail, upgrade).
5+
*
6+
* Single source of truth for that chrome — never re-derive the geometry per page. It
7+
* was previously copied verbatim into seven files, which is how every one of them came
8+
* to overlap the macOS traffic lights at once.
9+
*
10+
* The top padding folds in `--workspace-content-title-bar-inset`, the height the
11+
* workspace content pane must leave clear for the desktop shell's inset title bar.
12+
* That variable is `0px` everywhere except the macOS desktop app with the sidebar
13+
* collapsed — the only arrangement where the content pane, rather than the sidebar,
14+
* sits beneath the traffic lights and the sidebar expander — so this is inert on the
15+
* web and costs consumers nothing.
16+
*/
17+
export const PAGE_HEADER_BAR =
18+
'flex flex-shrink-0 items-center bg-[var(--bg)] px-[16px] pt-[calc(8.5px+var(--workspace-content-title-bar-inset))] pb-[8.5px]'

apps/sim/components/settings/settings-header.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
useRef,
1414
useState,
1515
} from 'react'
16-
import { Chip, ChipInput, ChipLink, Search, Tooltip } from '@sim/emcn'
16+
import { Chip, ChipInput, ChipLink, cn, Search, Tooltip } from '@sim/emcn'
17+
import { PAGE_HEADER_BAR } from '@/components/page-header-bar'
1718

1819
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
1920

@@ -123,7 +124,7 @@ export function SettingsHeaderShell({ children }: { children: ReactNode }) {
123124

124125
return (
125126
<div className='flex h-full flex-col bg-[var(--bg)]'>
126-
<div className='flex flex-shrink-0 items-center justify-between bg-[var(--bg)] px-[16px] pt-[8.5px] pb-[8.5px]'>
127+
<div className={cn(PAGE_HEADER_BAR, 'justify-between')}>
127128
{back ? (
128129
<Chip leftIcon={back.icon} onClick={() => configRef?.current.back?.onSelect()}>
129130
{back.text}

0 commit comments

Comments
 (0)