fix: keep a page's own controls when the workspace navigation is hidden

`menuHidden` means "this is an embed, don't show the workspace's navigation" — but
Runs, the detail pages and the flow editor now keep their controls in the band and
nowhere else, so hiding it took the filters, the timeframe and the page's buttons with
it. A session's preview iframe opens exactly that way. The band now renders for an
embed too, without its breadcrumb, so the page keeps its controls and the embed still
gains no way to navigate the host's workspace.

The flow editor's autosave indicator moves into the header's actions with the rest of
its bar: saving state, the autosave toggle and other people's drafts were left behind
in the branch the full-page editor no longer takes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-23 09:18:47 +02:00
co-authored by Claude Opus 5
parent 283cf48ccf
commit 3f55533558
3 changed files with 133 additions and 110 deletions
+112 -98
View File
@@ -1561,115 +1561,129 @@
contexts={headerContexts}
/>
{:else}
<!-- Nav between steps-->
<div
bind:clientWidth={topbarWidth}
class="justify-between flex flex-row items-center pl-2 pr-4 space-x-4 scrollbar-hidden overflow-x-auto h-full relative {condensedHeader
? 'max-h-9'
: 'max-h-12'}"
>
<div class="flex flex-row items-center gap-2 min-w-0">
{#if customUi?.topBar?.path != false}
<div class="min-w-0 overflow-hidden">
<EditorHeader
bind:summary={flowStore.val.summary}
bind:path={$pathStore}
savedPath={initialPath}
onBehalfOfEmail={$savedOnBehalfOfEmail}
summaryEditable={customUi?.topBar?.editableSummary != false}
pathEditable={customUi?.topBar?.editablePath != false}
hidePath={condensedHeader}
workspaceId={autosaveWorkspace}
onNavigate={(item) => onNavigate?.(item)}
<!-- Nav between steps-->
<div
bind:clientWidth={topbarWidth}
class="justify-between flex flex-row items-center pl-2 pr-4 space-x-4 scrollbar-hidden overflow-x-auto h-full relative {condensedHeader
? 'max-h-9'
: 'max-h-12'}"
>
<div class="flex flex-row items-center gap-2 min-w-0">
{#if customUi?.topBar?.path != false}
<div class="min-w-0 overflow-hidden">
<EditorHeader
bind:summary={flowStore.val.summary}
bind:path={$pathStore}
savedPath={initialPath}
onBehalfOfEmail={$savedOnBehalfOfEmail}
summaryEditable={customUi?.topBar?.editableSummary != false}
pathEditable={customUi?.topBar?.editablePath != false}
hidePath={condensedHeader}
workspaceId={autosaveWorkspace}
onNavigate={(item) => onNavigate?.(item)}
/>
</div>
{/if}
{#if opWorkspace && indicatorPath !== undefined}
<AutosaveIndicator
workspace={opWorkspace}
itemKind="flow"
path={indicatorPath}
draftOnly={newFlow}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
</div>
{/if}
{#if opWorkspace && indicatorPath !== undefined}
<AutosaveIndicator
workspace={opWorkspace}
itemKind="flow"
path={indicatorPath}
draftOnly={newFlow}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
{/if}
</div>
<div class="flex flex-row gap-2 items-center shrink-0">
{#if $enterpriseLicense && !newFlow && !inSessionPane}
<Awareness />
{/if}
<Dropdown items={getMoreItems} size={headerBtnSize} fixedHeight={!condensedHeader} />
{#if diffEnabled && !diffInMenu}
<!-- A disabled <button> fires no pointer events, so a title/tooltip on
{/if}
</div>
<div class="flex flex-row gap-2 items-center shrink-0">
{#if $enterpriseLicense && !newFlow && !inSessionPane}
<Awareness />
{/if}
<Dropdown items={getMoreItems} size={headerBtnSize} fixedHeight={!condensedHeader} />
{#if diffEnabled && !diffInMenu}
<!-- A disabled <button> fires no pointer events, so a title/tooltip on
it never shows on hover. pointer-events-none on the button lets the
hover reach this titled wrapper instead. -->
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
title={diffTitle}
startIcon={{ icon: DiffIcon }}
>
Diff
</Button>
</div>
{/if}
{#if !compactTopbar}
{@render previewButtons()}
{/if}
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
title={diffTitle}
startIcon={{ icon: DiffIcon }}
>
Diff
</Button>
</div>
{/if}
{#if !compactTopbar}
{@render previewButtons()}
{/if}
<DeployButton
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
unifiedSize={headerBtnSize}
{dropdownItems}
/>
<DeployButton
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
unifiedSize={headerBtnSize}
{dropdownItems}
/>
</div>
</div>
</div>
{/if}
{#snippet flowHeaderActions()}
{#if $enterpriseLicense && !newFlow && !inSessionPane}
<Awareness />
{/if}
<Dropdown items={getMoreItems} size={headerBtnSize} fixedHeight={!condensedHeader} />
{#if diffEnabled && !diffInMenu}
<!-- A disabled <button> fires no pointer events, so a title/tooltip on
<!-- Saving state, the autosave toggle and other people's drafts: the editor's own
bar carries them beside the path, so the header has to carry them when it owns
the bar — without them a full-page editor saves with no word either way. -->
{#if opWorkspace && indicatorPath !== undefined}
<AutosaveIndicator
workspace={opWorkspace}
itemKind="flow"
path={indicatorPath}
draftOnly={newFlow}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
{/if}
{#if $enterpriseLicense && !newFlow && !inSessionPane}
<Awareness />
{/if}
<Dropdown items={getMoreItems} size={headerBtnSize} fixedHeight={!condensedHeader} />
{#if diffEnabled && !diffInMenu}
<!-- A disabled <button> fires no pointer events, so a title/tooltip on
it never shows on hover. pointer-events-none on the button lets the
hover reach this titled wrapper instead. -->
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
title={diffTitle}
startIcon={{ icon: DiffIcon }}
>
Diff
</Button>
</div>
{/if}
{#if !compactTopbar}
{@render previewButtons()}
{/if}
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
title={diffTitle}
startIcon={{ icon: DiffIcon }}
>
Diff
</Button>
</div>
{/if}
{#if !compactTopbar}
{@render previewButtons()}
{/if}
<DeployButton
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
unifiedSize={headerBtnSize}
{dropdownItems}
/>
<DeployButton
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
unifiedSize={headerBtnSize}
{dropdownItems}
/>
{/snippet}
<!-- Rendered either inline in the top bar (wide) or as a graph overlay
@@ -14,6 +14,8 @@ The row's height matches the sidebar's own header row, so the two read as one ba
import { pageHeader } from './pageHeaderRegistry.svelte'
import ContextBridge from './ContextBridge.svelte'
let { navHidden = false }: { navHidden?: boolean } = $props()
const content = $derived(pageHeader.content)
const actions = $derived(pageHeader.actions)
@@ -22,7 +24,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
</script>
<div data-page-header class="flex items-center gap-1 h-11 pl-2 pr-4 shrink-0 min-w-0 bg-surface">
{#if navDetached.val}
{#if navDetached.val && !navHidden}
<!-- The only way back to a hidden sidebar: hovering slides the card in for a look, a click
puts it back for good. Docked, the sidebar speaks for itself and nothing leads the bar. -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -45,15 +47,19 @@ The row's height matches the sidebar's own header row, so the two read as one ba
<!-- The breadcrumb yields width grudgingly (shrink-[0.1]): when a page fills the bar with
controls, they are what should narrow, not the name of where the user is. It still gives
way rather than pushing them off the bar once there is nothing left to take. -->
<div class="flex min-w-0 shrink-[0.1]">
<NavBreadcrumb
{item}
{section}
hint={content?.hint}
actingWorkspaceId={content?.actingWorkspaceId}
/>
</div>
way rather than pushing them off the bar once there is nothing left to take.
An embed (`navHidden`) gets the page's controls without the trail that would offer to
navigate the host's workspace. -->
{#if !navHidden}
<div class="flex min-w-0 shrink-[0.1]">
<NavBreadcrumb
{item}
{section}
hint={content?.hint}
actingWorkspaceId={content?.actingWorkspaceId}
/>
</div>
{/if}
{#if item && (item.summaryContent || item.summary)}
<!-- A dot rather than a slash: the summary names the same item the path just located, it is
@@ -1538,7 +1538,10 @@
</div>
{/if}
<div class="flex flex-col h-full w-full relative">
{#if !menuHidden && !devOnly && pageHeader.content?.barPlacement !== 'inline'}
<!-- `menuHidden` hides the workspace navigation, not the page: an embedded Runs or detail
page keeps its own controls, which now live in this band and nowhere else. The band
renders without its breadcrumb there, so the embed gains no workspace nav. -->
{#if !devOnly && pageHeader.content?.barPlacement !== 'inline' && (!menuHidden || pageHeader.actions.length > 0)}
{@const rightInset = pageHeader.content?.barRightInset}
{@const leftInset = useDrawer ? 0 : railWidth}
<!-- One element for both placements, styled rather than branched: a page registers its
@@ -1557,7 +1560,7 @@
style:right={rightInset != null ? `${rightInset}px` : undefined}
style:padding-left={rightInset != null ? undefined : `${leftInset}rem`}
>
<PageHeaderBar />
<PageHeaderBar navHidden={menuHidden} />
</div>
{/if}
{#if $enterpriseLicense && !menuHidden}