fix: an embedded page keeps its name in the band

A page in a session's preview panel loads in a frame that sets `nomenubar`, so the
layout renders the band with `navHidden`. That flag hid the whole breadcrumb —
which is what carries the page's name — while the actions beside it had no such
guard, leaving a row of controls belonging to nothing.

Hiding the trail is still right: it leads to the host's workspace and offers a
picker that would take the reader out of the session. So the breadcrumb gains a
`nameOnly` mode that drops the workspace part and its picker and keeps the page's
own name, and the band asks for it instead of rendering nothing. The summary line
loses the same guard, so an item in there shows its path and summary.

Only the pages whose own title the band took over are affected. Variables,
Resources, Schedules and Triggers still draw their own `PageHeader` and render no
band when embedded, so nothing is doubled.

The bar's left padding is there to sit a button against — the sidebar handle, or
the workspace disc, both of which carry their own margin. Embedded there is
neither, and the name started as bare text 8px from the edge, so that case takes
the same 16px the bar already keeps on its right.

Workspace settings drops the copy-id button from the band: the General tab names
the workspace ID with a field of its own, which is where someone looks for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-05 07:35:58 -07:00
co-authored by Claude Opus 5
parent 64ada4e26d
commit fe94f4dc44
3 changed files with 93 additions and 85 deletions
@@ -34,7 +34,8 @@ same weight, size and icon size, so the line reads as one control rather than fo
section,
afterName,
actingWorkspaceId,
narrow = false
narrow = false,
nameOnly = false
}: {
item?: PageHeaderItem
section?: PageHeaderSection
@@ -45,6 +46,10 @@ same weight, size and icon size, so the line reads as one control rather than fo
actingWorkspaceId?: string
/** The bar is short of room: the workspace part drops its names and keeps its marks. */
narrow?: boolean
/** Only the page's own name, with no workspace part and no picker — the band inside a
* session's preview frame, where the workspace is the host's and leading the reader out of
* it is exactly what that band must not offer. The page still has to say what it is. */
nameOnly?: boolean
} = $props()
const scopeId = $derived(actingWorkspaceId ?? $workspaceStore ?? undefined)
@@ -185,79 +190,81 @@ same weight, size and icon size, so the line reads as one control rather than fo
aria-label="Breadcrumb"
class="flex items-center gap-1.5 min-w-0 text-xs font-normal text-primary"
>
<!-- One part for where the user is: the name of the workspace they are standing in — a fork
{#if !nameOnly}
<!-- One part for where the user is: the name of the workspace they are standing in — a fork
included, in its own colour — leading home, and one chevron whose picker changes it. The
picker lists the families and expands one to reach its forks, so a fork needs no part of
its own here. -->
<div class={narrow ? 'flex items-center shrink-0' : 'flex items-center min-w-0'}>
<!-- No hover fill on the name: the chevron beside it is the thing that lights up, and two
<div class={narrow ? 'flex items-center shrink-0' : 'flex items-center min-w-0'}>
<!-- No hover fill on the name: the chevron beside it is the thing that lights up, and two
boxes reacting to one pass of the pointer read as two controls fighting. -->
<!-- bind:clientWidth: the menu hangs from the chevron, so it is shifted back by the width of
<!-- bind:clientWidth: the menu hangs from the chevron, so it is shifted back by the width of
the name to line its left edge up with the disc. -->
<div class="flex items-center min-w-0" bind:clientWidth={nameWidth}>
<svelte:element
this={sessionMode ? 'span' : 'a'}
href={sessionMode ? undefined : homeHref}
class={scopeChipClass}
title={showFork ? `${familyName} / ${scopeName}` : familyName}
>
{#if narrow}
<!-- Short of room, the part keeps what it cannot be read without: the disc in the
<div class="flex items-center min-w-0" bind:clientWidth={nameWidth}>
<svelte:element
this={sessionMode ? 'span' : 'a'}
href={sessionMode ? undefined : homeHref}
class={scopeChipClass}
title={showFork ? `${familyName} / ${scopeName}` : familyName}
>
{#if narrow}
<!-- Short of room, the part keeps what it cannot be read without: the disc in the
workspace's own colour, the fork mark, and the environment. The names go — the
hover title still carries them, and the picker beside it names them all. -->
{@render workspaceDisc()}
{#if showFork}
<span class={forkChipClass} style={forkAccent}>
<GitFork size={ICON} class="flex-shrink-0" />
{@render workspaceDisc()}
{#if showFork}
<span class={forkChipClass} style={forkAccent}>
<GitFork size={ICON} class="flex-shrink-0" />
{@render envBadgeMark()}
</span>
{:else}
{@render envBadgeMark()}
</span>
{/if}
{:else}
{@render envBadgeMark()}
{/if}
{:else}
<BreadcrumbItemContent label={familyName} icon={workspaceDisc} />
{#if showFork}
<!-- Both names: the fork is where the work happens, the family is which product it
<BreadcrumbItemContent label={familyName} icon={workspaceDisc} />
{#if showFork}
<!-- Both names: the fork is where the work happens, the family is which product it
is part of, and either alone leaves the other to be guessed. -->
{@render slash()}
<span class={forkChipClass} style={forkAccent}>
<GitFork size={ICON} class="flex-shrink-0" />
<span class="truncate">{scopeName}</span>
{@render slash()}
<span class={forkChipClass} style={forkAccent}>
<GitFork size={ICON} class="flex-shrink-0" />
<span class="truncate">{scopeName}</span>
{@render envBadgeMark()}
</span>
{:else}
{@render envBadgeMark()}
</span>
{:else}
{@render envBadgeMark()}
{/if}
{/if}
{/if}
</svelte:element>
</svelte:element>
</div>
<Menubar>
{#snippet children({ createMenu })}
<Menu
bind:this={workspaceMenu}
{createMenu}
usePointerDownOutside
placement="bottom-start"
contentStyle="margin-left: {-nameWidth}px"
>
{#snippet triggr({ trigger })}
<MeltButton
meltElement={trigger}
class="flex items-center p-1.5 rounded text-tertiary hover:bg-surface-hover hover:text-primary transition-colors"
title="Switch workspace"
>
<ChevronDown size={ICON} class="flex-shrink-0" />
</MeltButton>
{/snippet}
{#snippet children({ item: menuItem })}
<WorkspacePickerBody item={menuItem} closeMenu={() => workspaceMenu?.close()} />
{/snippet}
</Menu>
{/snippet}
</Menubar>
</div>
<Menubar>
{#snippet children({ createMenu })}
<Menu
bind:this={workspaceMenu}
{createMenu}
usePointerDownOutside
placement="bottom-start"
contentStyle="margin-left: {-nameWidth}px"
>
{#snippet triggr({ trigger })}
<MeltButton
meltElement={trigger}
class="flex items-center p-1.5 rounded text-tertiary hover:bg-surface-hover hover:text-primary transition-colors"
title="Switch workspace"
>
<ChevronDown size={ICON} class="flex-shrink-0" />
</MeltButton>
{/snippet}
{#snippet children({ item: menuItem })}
<WorkspacePickerBody item={menuItem} closeMenu={() => workspaceMenu?.close()} />
{/snippet}
</Menu>
{/snippet}
</Menubar>
</div>
{/if}
{#if item || section || routePage}
{#if !nameOnly && (item || section || routePage)}
{@render slash()}
{/if}
@@ -87,6 +87,10 @@ The row's height matches the sidebar's own header row, so the two read as one ba
class={twMerge(
'flex items-center gap-1 shrink-0 min-w-0 bg-surface transition-shadow duration-150',
phone ? 'flex-wrap content-center min-h-11 py-1 pl-1 pr-2' : 'h-11 pl-2 pr-4',
// The small left padding is there to sit a button against: the sidebar handle, or the
// workspace disc, both of which carry their own visual margin. Embedded there is neither,
// and the page's name would start as bare text 8px from the edge.
navHidden && 'pl-4',
showEdge &&
'shadow-[inset_0_-1px_0_0_rgb(var(--color-border-light))] dark:shadow-[inset_0_-1px_0_0_#374151] [html.github-dark_&]:shadow-[inset_0_-1px_0_0_rgb(var(--color-border-light))]'
)}
@@ -121,28 +125,28 @@ The row's height matches the sidebar's own header row, so the two read as one ba
On a phone it stops yielding at 10rem, and that floor is what makes the bar wrap: the
page's buttons no longer fit beside a trail that wide, so they take the line below
instead of squeezing the name down to an ellipsis.
An embed (`navHidden`) gets the page's controls without the trail that would offer to
navigate the host's workspace. -->
{#if !navHidden}
<div class={twMerge('flex min-w-0', phone ? 'shrink min-w-[10rem]' : 'shrink-[0.1]')}>
<!-- Bridged like the actions below: what a page hangs off its own name renders here, out
of the tree that named it, and the pen in there asks that tree who the acting user is
before it offers to rename anything. -->
{#key content?.contexts}
<ContextBridge contexts={content?.contexts}>
<NavBreadcrumb
{item}
{section}
{narrow}
afterName={content?.afterName}
actingWorkspaceId={content?.actingWorkspaceId}
/>
</ContextBridge>
{/key}
</div>
{/if}
An embed (`navHidden`) keeps the page's own name but drops the workspace part around it:
the trail would offer to navigate the host's workspace, while the name is what says which
page the controls beside it belong to. -->
<div class={twMerge('flex min-w-0', phone ? 'shrink min-w-[10rem]' : 'shrink-[0.1]')}>
<!-- Bridged like the actions below: what a page hangs off its own name renders here, out
of the tree that named it, and the pen in there asks that tree who the acting user is
before it offers to rename anything. -->
{#key content?.contexts}
<ContextBridge contexts={content?.contexts}>
<NavBreadcrumb
{item}
{section}
{narrow}
nameOnly={navHidden}
afterName={content?.afterName}
actingWorkspaceId={content?.actingWorkspaceId}
/>
</ContextBridge>
{/key}
</div>
{#if item && !navHidden && (item.summaryContent || item.summary)}
{#if item && (item.summaryContent || item.summary)}
<!-- No kind icon: the page below is the item, and saying "this is a flow" above a flow
editor tells the reader what they can already see. -->
<div class={twMerge('flex items-center gap-1 min-w-0', phone && 'shrink')}>
@@ -3,7 +3,7 @@
import { page } from '$app/stores'
import { isCloudHosted } from '$lib/cloud'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, CopyButton, Section, Skeleton, Tab, Tabs } from '$lib/components/common'
import { Alert, Button, Section, Skeleton, Tab, Tabs } from '$lib/components/common'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
@@ -2431,9 +2431,6 @@ export async function main(
/>
{#snippet settingsActions()}
{#if $workspaceStore}
<CopyButton value={$workspaceStore} title={`Copy id: ${$workspaceStore}`} />
{/if}
{#if $superadmin}
<Button variant="default" unifiedSize="sm" on:click={() => goto('#superadmin-settings')}>
Instance settings