fix: the header carries what it promised, and nothing it does not

The registry's item described pickers the breadcrumb no longer has: nine fields — the
editability flags, the change callbacks, the navigation hook — that nothing read, while
the flow editor passed eight of them and quietly lost its inline rename. The type now
says what the bar does: a path, and either a summary or the page's own control for one.
The flow editor renames through that control, as the detail page already did.

`barPlacement` went the same way: no page registered 'inline', so the layout's guard
never fired.

The sessions page registered an inset of 0 whenever its preview pane never mounted —
the "no session selected" and opted-out arms — and 0 reads as "a page owns the right
edge", floating the band over the page's first line. It registers an inset only once
the pane has a width.

The band's sidebar handle docks through `setDetached` like the card's own toggle, so
docking still suppresses the rail's entry animation and closes the card; a summary
without its breadcrumb no longer leads with a stray separator in an embed; a page's
hint sits with its name whichever way the page named itself; and `Menu.closeInstantly`
and `EditorHeader`'s `inline`/`hideSummary` go, their callers having been replaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-23 09:26:31 +02:00
co-authored by Claude Opus 5
parent 3f55533558
commit 0f0aaac876
11 changed files with 53 additions and 83 deletions
+14 -27
View File
@@ -56,14 +56,9 @@
* dropped, leaving only the summary. Used by the condensed session-
* preview top bar to save vertical room. */
hidePath?: boolean
/** Drop the summary, for a host that renders its own after the path. */
hideSummary?: boolean
/** Workspace whose items the breadcrumb picker lists; defaults to the operating
* workspace (see `useOperatingWorkspace`). */
workspaceId?: string
/** Lay the summary out after the path on one line, for the page header bar, rather than
* stacked under it. */
inline?: boolean
}
let {
@@ -78,9 +73,7 @@
summaryEditable = true,
pathEditable = true,
hidePath = false,
hideSummary = false,
workspaceId,
inline = false
workspaceId
}: Props = $props()
let pathPopoverOpen = $state(false)
@@ -133,11 +126,7 @@
}
</script>
<div
class="max-w-full group px-2 py-0.5 leading-tight {inline
? 'flex items-center gap-2 min-w-0'
: 'inline-block align-top'}"
>
<div class="max-w-full group px-2 py-0.5 leading-tight inline-block align-top">
<!-- Path row -->
{#if !hidePath}
<div class="flex items-center max-w-full text-2xs text-secondary font-mono">
@@ -247,18 +236,16 @@
{/if}
<!-- Summary -->
{#if !hideSummary}
<div class="max-w-full {inline ? 'min-w-0' : '-mt-[2px]'}" title={summary}>
<EditableInput
value={summary ?? ''}
placeholder="Add a summary..."
editable={summaryEditable}
commitOnInput
size="sm"
onSave={handleSummarySave}
textClass="text-xs font-semibold text-emphasis leading-tight"
class="max-w-full"
/>
</div>
{/if}
<div class="max-w-full -mt-[2px]" title={summary}>
<EditableInput
value={summary ?? ''}
placeholder="Add a summary..."
editable={summaryEditable}
commitOnInput
size="sm"
onSave={handleSummarySave}
textClass="text-xs font-semibold text-emphasis leading-tight"
class="max-w-full"
/>
</div>
</div>
+17 -13
View File
@@ -69,6 +69,7 @@
import FlowHistory from './flows/FlowHistory.svelte'
import EditorHeader from './EditorHeader.svelte'
import AutosaveIndicator from './AutosaveIndicator.svelte'
import EditableInput from './common/EditableInput.svelte'
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
import { type TriggerContext, type ScheduleTrigger } from './triggers'
@@ -1544,19 +1545,7 @@
<!-- The editor's own top bar is the page header on this route: its breadcrumb and
summary are the header's, and its buttons are the header's actions. -->
<PageHeaderContent
item={{
kind: 'flow',
path: $pathStore,
savedPath: initialPath,
summary: flowStore.val.summary,
workspaceId: autosaveWorkspace,
onBehalfOfEmail: $savedOnBehalfOfEmail,
pathEditable: customUi?.topBar?.editablePath != false,
summaryEditable: customUi?.topBar?.editableSummary != false,
onPathChange: (v) => ($pathStore = v),
onSummaryChange: (v) => (flowStore.val.summary = v),
onNavigate: (item) => onNavigate?.(item)
}}
item={{ kind: 'flow', path: $pathStore, summaryContent: flowSummary }}
actions={flowHeaderActions}
contexts={headerContexts}
/>
@@ -1635,6 +1624,21 @@
</div>
{/if}
{#snippet flowSummary()}
<!-- The summary stays editable where the editor's own bar had it: the breadcrumb
beside it is the path, which this editor renames from its settings tab. -->
<EditableInput
value={flowStore.val.summary ?? ''}
placeholder="Add a summary..."
editable={customUi?.topBar?.editableSummary != false}
commitOnInput
size="sm"
onSave={(v) => (flowStore.val.summary = v)}
textClass="text-xs font-medium text-emphasis leading-tight"
class="max-w-full min-w-0"
/>
{/snippet}
{#snippet flowHeaderActions()}
<!-- 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
@@ -262,6 +262,7 @@ same weight, size and icon size, so the line reads as one control rather than fo
<span class="truncate">{level}</span>
{/each}
</button>
{#if hint}{@render hint()}{/if}
{#if pathCopied}
<span
class="absolute left-0 top-full mt-1 z-[6000] rounded-md border bg-surface px-2 py-1 text-2xs text-secondary shadow-md whitespace-nowrap"
@@ -14,7 +14,15 @@ 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()
let {
navHidden = false,
onDock
}: {
navHidden?: boolean
/** Docks the sidebar. The layout owns the two other things that must happen with it — the
* flag that suppresses the rail's entry animation, and closing the card. */
onDock?: () => void
} = $props()
const content = $derived(pageHeader.content)
const actions = $derived(pageHeader.actions)
@@ -38,7 +46,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
<button
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
aria-label="Show sidebar"
onclick={() => (navDetached.val = false)}
onclick={() => onDock?.()}
>
<PanelLeft size={16} class="flex-shrink-0 text-hint" />
</button>
@@ -61,7 +69,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
</div>
{/if}
{#if item && (item.summaryContent || item.summary)}
{#if item && !navHidden && (item.summaryContent || item.summary)}
<!-- A dot rather than a slash: the summary names the same item the path just located, it is
not another level of it. -->
<span class="shrink-0 text-hint/40 text-xs px-0.5" aria-hidden="true">·</span>
@@ -17,7 +17,6 @@ the header, so nesting is decided by the caller rather than guessed from context
actions,
actionsOrder,
contexts,
barPlacement,
actingWorkspaceId,
barRightInset
}: PageHeaderContent = $props()
@@ -30,7 +29,6 @@ the header, so nesting is decided by the caller rather than guessed from context
actions,
actionsOrder,
contexts,
barPlacement,
actingWorkspaceId,
barRightInset
}))
@@ -156,10 +156,7 @@
{#if canWriteApp && !hideEditBtn}
<!-- The page header carries it, with the app's name: a floating button over the app's own
canvas lands on whatever the app draws there. -->
<PageHeaderContent
item={{ kind: 'app', path, raw_app: true, summaryEditable: false, pathEditable: false }}
actions={editAction}
/>
<PageHeaderContent item={{ kind: 'app', path }} actions={editAction} />
{/if}
{#snippet editAction()}
@@ -202,13 +202,7 @@
breadcrumb, the summary keeps its rename-and-labels popover, and everything else rides along
as the bar's actions. -->
<PageHeaderContent
item={{
kind: errorHandlerKind,
path,
summaryContent,
pathEditable: false,
summaryEditable: false
}}
item={{ kind: errorHandlerKind, path, summaryContent }}
{actions}
contexts={headerContexts}
/>
@@ -16,10 +16,6 @@
interface Props {
placement?: Placement
/** Drops the fly-out, so the panel is out of the DOM the moment it closes. For a menu whose
* act tears its own trigger down — picking a workspace — where floating-ui spends the
* outro re-placing the panel against a reference on its way out, and it bounces. */
closeInstantly?: boolean
/** Inline style on the panel. A margin here shifts it after floating-ui has placed it,
* which is the only way to anchor it to a box wider than the trigger: melt's menubar
* menu ignores `positioning.offset`. */
@@ -54,7 +50,6 @@
let {
placement = 'right-start',
contentStyle = '',
closeInstantly = false,
justifyEnd = false,
lightMode = false,
maxHeight = 900,
@@ -230,7 +225,7 @@
data-menu
onmouseenter={cancelPendingClose}
onmouseleave={scheduleClose}
transition:placementFly={{ duration: closeInstantly ? 0 : 100, placement }}
transition:placementFly={{ duration: 100, placement }}
style={contentStyle}
class={twMerge(
'z-[6000] border w-56 origin-top-right rounded-md shadow-md focus:outline-none',
@@ -1,25 +1,15 @@
import type { Snippet } from 'svelte'
import type { WorkspaceItem, WorkspaceItemKind } from './workspacePicker'
import type { WorkspaceItemKind } from './workspacePicker'
/** An item the page is working on: the breadcrumb walks its path and ends with its summary. */
export type PageHeaderItem = {
kind: WorkspaceItemKind
/** Shown whole, and copied on click. */
path: string | undefined
/** The item's saved path, so a mid-rename breadcrumb and picker stay coherent. */
savedPath?: string
summary?: string
/** Rendered after the path instead of the editable summary — for a page whose summary is
* itself a control, such as a detail page's rename-and-labels popover. */
/** Rendered in place of the plain summary, for a page whose summary is itself a control —
* a detail page's rename-and-labels popover, an editor's editable title. */
summaryContent?: Snippet
raw_app?: boolean
/** Which workspace the breadcrumb's pickers read (a session editor acts on its fork). */
workspaceId?: string
pathEditable?: boolean
summaryEditable?: boolean
onBehalfOfEmail?: string
onSummaryChange?: (summary: string) => void
onPathChange?: (path: string) => void
onNavigate?: (item: WorkspaceItem) => void
}
/** A page with no item of its own, such as a list: the breadcrumb is the section alone. */
@@ -50,9 +40,6 @@ export type PageHeaderContent = {
* session's side panel. The band stops there instead of running over it, and the band floats
* above the page rather than pushing it down, so that column starts at the top. */
barRightInset?: number
/** 'inline' when the route renders the bar itself — for a layout whose side panel must run to
* the top of the viewport beside it, rather than starting under a full-width bar. */
barPlacement?: 'layout' | 'inline'
/** Contexts those buttons look up. They render under the header, not under the page that
* wrote them, so anything the page's tree provides has to travel with them. */
contexts?: Map<any, any>
@@ -86,7 +73,6 @@ export const pageHeader = {
if (c.section !== undefined) merged.section = c.section
if (c.hint !== undefined) merged.hint = c.hint
if (c.contexts !== undefined) merged.contexts = c.contexts
if (c.barPlacement !== undefined) merged.barPlacement = c.barPlacement
if (c.actingWorkspaceId !== undefined) merged.actingWorkspaceId = c.actingWorkspaceId
if (c.barRightInset !== undefined) merged.barRightInset = c.barRightInset
}
@@ -1541,7 +1541,7 @@
<!-- `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)}
{#if !devOnly && (!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
@@ -1560,7 +1560,7 @@
style:right={rightInset != null ? `${rightInset}px` : undefined}
style:padding-left={rightInset != null ? undefined : `${leftInset}rem`}
>
<PageHeaderBar navHidden={menuHidden} />
<PageHeaderBar navHidden={menuHidden} onDock={() => setDetached(false)} />
</div>
{/if}
{#if $enterpriseLicense && !menuHidden}
@@ -874,7 +874,7 @@
<!-- The session showing fills the band's breadcrumb. The band is the layout's, but it stops
where the preview panel starts so that panel can run from the top of the viewport. -->
<PageHeaderContent
barRightInset={previewCollapsed || fullscreen ? undefined : previewWidth}
barRightInset={previewCollapsed || fullscreen || previewWidth === 0 ? undefined : previewWidth}
actions={sessionPageActions}
/>