mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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\]/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+5
-1
@@ -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'>
|
||||
|
||||
+36
-5
@@ -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>
|
||||
|
||||
+4
@@ -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
-1
@@ -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>
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user