feat: condensed top bar for session preview editors (#10011)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-07-08 16:35:57 +02:00
committed by GitHub
parent c4cb2f373b
commit b847ca2bc7
13 changed files with 183 additions and 116 deletions
@@ -6,7 +6,8 @@
const {
loading = false,
loadingSave = false,
dropdownItems = []
dropdownItems = [],
unifiedSize = 'md'
}: {
loading?: boolean
loadingSave?: boolean
@@ -14,6 +15,7 @@
label: string
onClick: () => void
}>
unifiedSize?: 'sm' | 'md' | 'lg'
} = $props()
const dispatch = createEventDispatcher()
@@ -28,7 +30,7 @@
disabled={loading}
loading={loadingSave}
variant="accent"
unifiedSize="md"
{unifiedSize}
startIcon={{ icon: Save }}
on:click={() => dispatch('save')}
{dropdownItems}
+102 -95
View File
@@ -43,6 +43,10 @@
* inline. Breadcrumb navigation still works — only the rename UI is
* gated. */
pathEditable?: boolean
/** When true, the whole path/breadcrumb row (and its pen popover) is
* dropped, leaving only the summary. Used by the condensed session-
* preview top bar to save vertical room. */
hidePath?: boolean
/** Workspace whose items the breadcrumb picker lists. Session live
* editors pass their acting workspace so the picker isn't scoped to the
* navigation workspace; falls back to $workspaceStore in the picker. */
@@ -60,6 +64,7 @@
penVisibility = 'hover',
summaryEditable = true,
pathEditable = true,
hidePath = false,
workspaceId
}: Props = $props()
@@ -135,109 +140,111 @@
<div class="inline-block max-w-full align-top group px-2 py-0.5 leading-tight">
<!-- Path row -->
<div class="flex items-center max-w-full text-2xs text-secondary font-mono">
<nav aria-label="Breadcrumb" class="contents">
<BreadcrumbSegment
label={KIND_LABEL_LOWER[kind]}
initialScope={undefined}
initialHighlight={kindKey(kind)}
isCurrent={!segments}
{currentItem}
{workspaceId}
onPick={handlePickerSelect}
/>
{#if segments}
{#each segments.dirs as dir, i (dir.fullPath)}
{@const dKey = dirKey('all', dir.fullPath)}
<BreadcrumbSegment
label={dir.name}
withChevron
extraClass={i === 0 ? 'gap-0.5 min-w-0 max-w-[40%]' : 'gap-0.5 min-w-0'}
initialScope={i === 0
? { kind: 'all' }
: { kind: 'all', dir: segments.dirs[i - 1].fullPath }}
initialHighlight={dKey}
{currentItem}
{workspaceId}
onPick={handlePickerSelect}
/>
{/each}
{@const leafKey = leafKeyFor(kind, segments.leaf.fullPath)}
{@const leafParent = segments.dirs[segments.dirs.length - 1]?.fullPath}
{#if !hidePath}
<div class="flex items-center max-w-full text-2xs text-secondary font-mono">
<nav aria-label="Breadcrumb" class="contents">
<BreadcrumbSegment
label={segments.leaf.name}
withChevron
extraClass="gap-0.5 min-w-0"
initialScope={leafParent ? { kind: 'all', dir: leafParent } : { kind: 'all' }}
initialHighlight={leafKey}
isCurrent
label={KIND_LABEL_LOWER[kind]}
initialScope={undefined}
initialHighlight={kindKey(kind)}
isCurrent={!segments}
{currentItem}
{workspaceId}
onPick={handlePickerSelect}
/>
{/if}
</nav>
<!-- Pen → path-edit popover. Skipped entirely when path editing is
disabled so the user doesn't see an inert button. -->
{#if pathEditable}
<Popover
placement="bottom-start"
contentClasses="p-4"
usePointerDownOutside
excludeSelectors=".drawer"
disableFocusTrap
closeOnOtherPopoverOpen
bind:isOpen={() => pathPopoverOpen, setPathPopoverOpen}
>
{#snippet trigger()}
<Button
variant="subtle"
unifiedSize="xs"
iconOnly
startIcon={{ icon: Pencil }}
title="Edit path"
aria-label="Edit path"
btnClasses={penVisibility === 'hover' && !pathPopoverOpen
? 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100'
: ''}
{#if segments}
{#each segments.dirs as dir, i (dir.fullPath)}
{@const dKey = dirKey('all', dir.fullPath)}
<BreadcrumbSegment
label={dir.name}
withChevron
extraClass={i === 0 ? 'gap-0.5 min-w-0 max-w-[40%]' : 'gap-0.5 min-w-0'}
initialScope={i === 0
? { kind: 'all' }
: { kind: 'all', dir: segments.dirs[i - 1].fullPath }}
initialHighlight={dKey}
{currentItem}
{workspaceId}
onPick={handlePickerSelect}
/>
{/each}
{@const leafKey = leafKeyFor(kind, segments.leaf.fullPath)}
{@const leafParent = segments.dirs[segments.dirs.length - 1]?.fullPath}
<BreadcrumbSegment
label={segments.leaf.name}
withChevron
extraClass="gap-0.5 min-w-0"
initialScope={leafParent ? { kind: 'all', dir: leafParent } : { kind: 'all' }}
initialHighlight={leafKey}
isCurrent
{currentItem}
{workspaceId}
onPick={handlePickerSelect}
/>
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-6 w-[480px]">
{#if own}
<Path
autofocus
bind:path
initialPath={snapshotPath ?? path ?? ''}
namePlaceholder={kind}
{kind}
size="sm"
drawerOffset={4000}
/>
{#if savedPath && path && path !== savedPath}
<Alert
type="info"
size="xs"
title="Deploy the {kind} to make the path change effective."
{/if}
</nav>
<!-- Pen → path-edit popover. Skipped entirely when path editing is
disabled so the user doesn't see an inert button. -->
{#if pathEditable}
<Popover
placement="bottom-start"
contentClasses="p-4"
usePointerDownOutside
excludeSelectors=".drawer"
disableFocusTrap
closeOnOtherPopoverOpen
bind:isOpen={() => pathPopoverOpen, setPathPopoverOpen}
>
{#snippet trigger()}
<Button
variant="subtle"
unifiedSize="xs"
iconOnly
startIcon={{ icon: Pencil }}
title="Edit path"
aria-label="Edit path"
btnClasses={penVisibility === 'hover' && !pathPopoverOpen
? 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100'
: ''}
/>
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-6 w-[480px]">
{#if own}
<Path
autofocus
bind:path
initialPath={snapshotPath ?? path ?? ''}
namePlaceholder={kind}
{kind}
size="sm"
drawerOffset={4000}
/>
{#if savedPath && path && path !== savedPath}
<Alert
type="info"
size="xs"
title="Deploy the {kind} to make the path change effective."
/>
{/if}
{#if onBehalfOfEmail}
<Alert type="info" title="Run on behalf of" size="xs">
This flow will be redeployed on behalf of you ({$userStore?.email}) instead of {onBehalfOfEmail}
</Alert>
{/if}
{:else}
<Label label="Path">
<span class="text-xs font-mono text-secondary">{path}</span>
<p class="text-2xs text-tertiary mt-1">Only the owner can change the path</p>
</Label>
{/if}
{#if onBehalfOfEmail}
<Alert type="info" title="Run on behalf of" size="xs">
This flow will be redeployed on behalf of you ({$userStore?.email}) instead of {onBehalfOfEmail}
</Alert>
{/if}
{:else}
<Label label="Path">
<span class="text-xs font-mono text-secondary">{path}</span>
<p class="text-2xs text-tertiary mt-1">Only the owner can change the path</p>
</Label>
{/if}
</div>
{/snippet}
</Popover>
{/if}
</div>
</div>
{/snippet}
</Popover>
{/if}
</div>
{/if}
<!-- Summary -->
<div class="max-w-full -mt-[2px]" title={summary}>
+14 -4
View File
@@ -136,9 +136,14 @@
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts,
onTestJob
onTestJob,
condensedHeader = false
}: FlowBuilderProps = $props()
// Top-bar button size + bar height. Condensed (session preview) uses the
// smallest well-supported unified size (`sm`) so the bar is thinner.
const headerBtnSize = $derived(condensedHeader ? 'sm' : 'md')
// The workspace this editor operates on: deploy, save-draft, trigger loading
// and the AutosaveIndicator all target it. Falls back to the global store, so
// the full-page editor is unchanged; the sessions preview overrides it to the
@@ -1101,7 +1106,9 @@
<!-- 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 max-h-12 h-full relative"
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">
<div class="min-w-0 overflow-hidden">
@@ -1110,6 +1117,7 @@
bind:path={$pathStore}
savedPath={initialPath}
onBehalfOfEmail={$savedOnBehalfOfEmail}
hidePath={condensedHeader}
workspaceId={autosaveWorkspace}
onNavigate={(item) => onNavigate?.(item)}
/>
@@ -1132,7 +1140,7 @@
<Awareness />
{/if}
<div class="relative">
<Dropdown items={getMoreItems} />
<Dropdown items={getMoreItems} size={headerBtnSize} fixedHeight={!condensedHeader} />
{#if $tutorialsToDo.includes(getTutorialIndex('flow-live-tutorial')) || $tutorialsToDo.includes(getTutorialIndex('troubleshoot-flow'))}
<span
class="absolute top-0.5 right-0.5 block w-2 h-2 rounded-full bg-surface-accent-primary pointer-events-none"
@@ -1152,7 +1160,7 @@
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize="md"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
@@ -1172,6 +1180,7 @@
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
unifiedSize={headerBtnSize}
{dropdownItems}
/>
</div>
@@ -1182,6 +1191,7 @@
{#snippet previewButtons()}
<FlowPreviewButtons
{suspendStatus}
unifiedSize={headerBtnSize}
on:openTriggers={(e) => {
select('Trigger')
handleSelectTriggerFromKind(triggersState, triggersCount, initialPath, e.detail.kind)
@@ -140,9 +140,14 @@
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
onOpenOthersDrafts,
condensedHeader = false
}: ScriptBuilderProps = $props()
// Top-bar button size + bar height. Condensed (session preview) uses the
// smallest well-supported unified size (`sm`) so the bar is thinner.
const headerBtnSize = $derived(condensedHeader ? 'sm' : 'md')
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
savedValue: savedScript,
@@ -1898,7 +1903,10 @@
</Drawer>
<div class="flex flex-col h-full">
<div bind:clientWidth={topbarWidth} class="flex h-12 items-center px-4">
<div
bind:clientWidth={topbarWidth}
class="flex items-center px-4 {condensedHeader ? 'h-9' : 'h-12'}"
>
<div class="flex gap-2 lg:gap-2 w-full items-center">
<div class="flex flex-row items-center gap-2 min-w-0 shrink">
<button
@@ -1908,7 +1916,7 @@
metadataOpen = true
}}
>
<LanguageIcon lang={script.language} size={24} />
<LanguageIcon lang={script.language} size={condensedHeader ? 18 : 24} />
</button>
{#if customUi?.topBar?.path != false}
<div class="min-w-0 overflow-hidden">
@@ -1919,6 +1927,7 @@
kind="script"
summaryEditable={customUi?.topBar?.editableSummary != false}
pathEditable={customUi?.topBar?.editablePath != false}
hidePath={condensedHeader}
workspaceId={autosaveWorkspace}
onNavigate={(item) => onNavigate?.(item)}
/>
@@ -1954,7 +1963,7 @@
aiId="script-builder-settings"
aiDescription="Script builder settings to configure metadata, runtime, triggers, and generated UI."
variant="default"
unifiedSize="md"
unifiedSize={headerBtnSize}
on:click={() => (metadataOpen = true)}
startIcon={{ icon: Settings }}
iconOnly={compactTopbar}
@@ -1977,7 +1986,7 @@
<div title={diffTitle} class={diffDisabled ? 'flex cursor-not-allowed' : 'flex'}>
<Button
variant="default"
unifiedSize="md"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={diffDisabled}
btnClasses={diffDisabled ? 'pointer-events-none' : undefined}
@@ -1995,7 +2004,7 @@
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
unifiedSize="md"
unifiedSize={headerBtnSize}
variant="subtle"
startIcon={{ icon: EllipsisVertical }}
iconOnly
@@ -2015,6 +2024,7 @@
nullTag={script.language}
placeholder={customUi?.tagSelectPlaceholder}
bind:tag={script.tag}
size={headerBtnSize}
workspaceId={opWorkspace}
/>
</div>
@@ -2027,6 +2037,7 @@
<DeployButton
loading={!fullyLoaded}
{loadingSave}
unifiedSize={headerBtnSize}
dropdownItems={computeDropdownItems(initialPath, savedScript)}
on:save={({ detail }) => handleEditScript(false, detail)}
/>
@@ -17,6 +17,7 @@
disabled = false,
placeholder,
inputClass,
size = 'md',
workspaceId = undefined
}: {
tag: string | undefined
@@ -27,6 +28,9 @@
language?: string
class?: string
inputClass?: string
/** Forwarded to the underlying Select — controls the input height. The
* condensed session top bar passes `sm` to match its smaller buttons. */
size?: 'sm' | 'md' | 'lg'
// Workspace to read custom tags and worker availability from. Defaults to
// $workspaceStore. Session editors act on a workspace that differs from the
// navigation one, so they pass their effective workspace to keep the tag
@@ -164,6 +168,7 @@
clearable
class="w-full"
bind:open
{size}
{inputClass}
{disabled}
placeholder={nullTag ? nullTag : (placeholder ?? 'lang default')}
@@ -62,4 +62,8 @@ export type FlowBuilderProps = {
// Fired whenever a test run is started from the flow editor, with the
// preview job id. Used by whitelabel embedders to track test jobs.
onTestJob?: (e: { jobId: string }) => void
// Condensed top bar: smaller (sm) buttons, a shorter bar, and the
// EditorHeader's path/breadcrumb row dropped (summary only). Used by the
// session preview to save vertical room.
condensedHeader?: boolean
}
@@ -1,5 +1,6 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import type { ButtonType } from '$lib/components/common/button/model'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
@@ -18,6 +19,7 @@
onJobDone?: () => void
localModuleStates?: Record<string, GraphModuleState>
suspendStatus: StateStore<Record<string, { job: Job; nb: number }>>
unifiedSize?: ButtonType.UnifiedSize
}
let {
@@ -25,7 +27,8 @@
onRunPreview,
onJobDone,
localModuleStates = $bindable({}),
suspendStatus
suspendStatus,
unifiedSize = 'md'
}: Props = $props()
const { selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -143,7 +146,7 @@
<Button
variant="accent-secondary"
wrapperClasses="whitespace-nowrap"
unifiedSize="md"
{unifiedSize}
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
@@ -132,6 +132,10 @@
// prop for the same reason as `onRestore` — `on:savedNewAppPath` forwarding
// through these runes-mode components is dropped.
onSavedNewAppPath?: (path: string) => void
// Condensed top bar: smaller (sm) buttons, a shorter bar, and the
// EditorHeader's path/breadcrumb row dropped (summary only). Used by the
// session preview to save vertical room.
condensedHeader?: boolean
}
let {
@@ -162,7 +166,8 @@
onRuntimeLogRequester = undefined,
onRunsProvider = undefined,
onRestore,
onSavedNewAppPath
onSavedNewAppPath,
condensedHeader = false
}: Props = $props()
export const version: number | undefined = undefined
@@ -1678,6 +1683,7 @@
onOpenYamlEditor={() => yamlEditorDrawer?.openDrawer()}
sidebarCollapsed={sidebarCollapsed.val}
onToggleSidebar={() => (sidebarCollapsed.val = !sidebarCollapsed.val)}
{condensedHeader}
/>
<RawAppYamlEditor
@@ -137,6 +137,10 @@
onOpenYamlEditor?: () => void
sidebarCollapsed?: boolean
onToggleSidebar?: () => void
/** Condensed top bar: smaller (sm) buttons, a shorter bar, and the
* EditorHeader's path/breadcrumb row dropped (summary only). Used by the
* session preview to save vertical room. */
condensedHeader?: boolean
onNavigate?: (item: import('$lib/components/workspacePicker').WorkspaceItem) => void
liveEditorDraftStoragePath?: string
/** Indicator-only overrides for the sessions preview: the AutosaveIndicator
@@ -194,6 +198,7 @@
onOpenYamlEditor = undefined,
sidebarCollapsed = false,
onToggleSidebar = undefined,
condensedHeader = false,
onNavigate = undefined,
liveEditorDraftStoragePath = undefined,
autosaveWorkspace = undefined,
@@ -282,6 +287,10 @@
let topbarWidth = $state(0)
const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 720)
// Top-bar button size + bar height. Condensed (session preview) uses the
// smallest well-supported unified size (`sm`) so the bar is thinner.
const headerBtnSize = $derived(condensedHeader ? 'sm' : 'md')
async function publishToHub() {
if (!app) return
publishingToHub = true
@@ -770,7 +779,9 @@
<div
bind:clientWidth={topbarWidth}
class="flex flex-row justify-between gap-2 gap-y-2 px-2 items-center overflow-y-visible overflow-x-auto max-h-12 h-12 shrink-0"
class="flex flex-row justify-between gap-2 gap-y-2 px-2 items-center overflow-y-visible overflow-x-auto shrink-0 {condensedHeader
? 'max-h-9 h-9'
: 'max-h-12 h-12'}"
>
<!-- Identity block: shrinks/truncates first so the cloud indicator and the
action buttons stay visible. Without min-w-0 the breadcrumb + summary
@@ -794,6 +805,7 @@
savedPath={appPath || newPath || undefined}
kind="app"
raw_app
hidePath={condensedHeader}
workspaceId={autosaveWorkspace}
onNavigate={(item) => (onNavigate ? onNavigate(item) : goto(editPathFor(item)))}
/>
@@ -822,7 +834,7 @@
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
unifiedSize="md"
unifiedSize={headerBtnSize}
variant="subtle"
startIcon={{ icon: EllipsisVertical }}
iconOnly
@@ -843,7 +855,7 @@
>
<Button
variant="default"
unifiedSize="md"
unifiedSize={headerBtnSize}
on:click={() => openDiffDrawer()}
disabled={!savedApp || newApp || savedApp?.no_deployed === true}
btnClasses={!savedApp || newApp || savedApp?.no_deployed === true
@@ -865,7 +877,7 @@
jobsDrawerOpen = true
}}
color="light"
unifiedSize="md"
unifiedSize={headerBtnSize}
variant="default"
btnClasses="relative"
>
@@ -900,7 +912,7 @@
>
{#snippet fallback()}
<Button
unifiedSize="md"
unifiedSize={headerBtnSize}
variant="default"
onClick={() => aiChatManager.toggleOpen()}
startIcon={{ icon: WandSparkles }}
@@ -915,7 +927,7 @@
loading={loading.save}
startIcon={{ icon: Save }}
on:click={save}
unifiedSize="md"
unifiedSize={headerBtnSize}
variant="accent"
dropdownItems={appPath != ''
? () => [
@@ -92,4 +92,8 @@ export interface ScriptBuilderProps {
othersDraftsCount?: number
// Wired by the route to flip the OtherUsersDraftsModal open.
onOpenOthersDrafts?: () => void
// Condensed top bar: smaller (sm) buttons, a shorter bar, and the
// EditorHeader's path/breadcrumb row dropped (summary only). Used by the
// session preview to save vertical room.
condensedHeader?: boolean
}
@@ -106,6 +106,7 @@
bind:savedFlow={cell.saved.val}
{diffDrawer}
{onNavigate}
condensedHeader={true}
customUi={{ topBar: { aiBuilder: false } }}
onDeploy={() => {
// FlowBuilder has no deploy toast and the session stays put, so toast
@@ -124,6 +124,7 @@
newApp={!cell.saved.val || cell.saved.val.no_deployed === true}
{diffDrawer}
{onNavigate}
condensedHeader={true}
onResetToDeployed={reloadDeployed}
onDeploy={(e) => {
// Sync the preview to deployed (raw apps deploy only from this editor).
@@ -109,6 +109,7 @@
neverShowMeta={true}
fullyLoaded={!cell.slot.loading}
disableHistoryChange={true}
condensedHeader={true}
{diffDrawer}
{onNavigate}
{initialTestPanelCollapsed}