diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 1a468ac33a..0e2348336c 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -40,7 +40,7 @@ import { getContext, onDestroy, setContext, untrack, type Snippet } from 'svelte' import PageHeaderContent from '$lib/components/PageHeaderContent.svelte' import PathEditPopover from '$lib/components/PathEditPopover.svelte' - import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte' + import { pageHeader } from '$lib/components/pageHeaderRegistry.svelte' import { writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Button } from './common' @@ -230,19 +230,14 @@ let pathSnapshot = $state(undefined) let topbarWidth = $state(0) - // In the page header the buttons share the row with the breadcrumb, so they collapse sooner: - // the trail and the summary take ~560px before either truncates, and the full group — the - // dropdown, Diff, the preview trio and Deploy — is ~610px. - const compactBelow = $derived(ownsPageHeader ? 1200 : 720) - const compactTopbar = $derived.by(() => { - const w = ownsPageHeader ? pageHeader.barWidth : topbarWidth - return w > 0 && w < compactBelow - }) - // A phone's bar holds the trail and Deploy: the diff joins the menu and the preview buttons - // stand down, since there is no room to run a flow beside its own name. - const phoneTopbar = $derived( - ownsPageHeader && pageHeader.barWidth > 0 && pageHeader.barWidth < PHONE_BAR + // In the page header the bar folds the buttons (`actionFolds` below), once its breadcrumb has + // given all it can; anywhere else this editor's own top bar is theirs alone. + const compactTopbar = $derived( + ownsPageHeader ? pageHeader.actionFold >= 1 : topbarWidth > 0 && topbarWidth < 720 ) + // The second fold holds the trail and Deploy: the diff joins the menu and the preview buttons + // stand down, since there is no room to run a flow beside its own name. + const phoneTopbar = $derived(ownsPageHeader && pageHeader.actionFold >= 2) const diffEnabled = $derived(customUi?.topBar?.diff != false) // Nothing to compare against until a deployed version exists. @@ -1597,6 +1592,7 @@ : undefined }} actions={flowHeaderActions} + actionFolds={2} contexts={headerContexts} separator="none" /> diff --git a/frontend/src/lib/components/NavBreadcrumb.svelte b/frontend/src/lib/components/NavBreadcrumb.svelte index b053591fc0..9ae1dc399a 100644 --- a/frontend/src/lib/components/NavBreadcrumb.svelte +++ b/frontend/src/lib/components/NavBreadcrumb.svelte @@ -16,7 +16,6 @@ not this component) — so the lead can run the whole line. import WorkspaceItemKindIcon from './WorkspaceItemKindIcon.svelte' import { Menu, Menubar } from '$lib/components/meltComponents' import MeltButton from '$lib/components/meltComponents/MeltButton.svelte' - import BreadcrumbItemContent from './BreadcrumbItemContent.svelte' import WorkspacePickerBody from '$lib/components/sidebar/WorkspacePickerBody.svelte' import BreadcrumbSegment from '$lib/components/BreadcrumbSegment.svelte' import { userWorkspaces, workspaceStore, workspaceColor, superadmin } from '$lib/stores' @@ -33,14 +32,25 @@ not this component) — so the lead can run the whole line. import { KIND_LABEL_LOWER, kindKey } from '$lib/components/workspacePicker' import type { PageHeaderItem, PageHeaderSection } from './pageHeaderRegistry.svelte' import type { Snippet } from 'svelte' + import { + BAR_STAGES, + FLOOR, + shownPathLevels, + type Part, + type TrailMeasures, + type TrailStage + } from './pageHeaderStages' let { item, section, afterName, actingWorkspaceId, - narrow = false, - nameOnly = false + nameOnly = false, + stage = BAR_STAGES[0], + caps = {}, + scopesGive = false, + measures = $bindable() }: { item?: PageHeaderItem section?: PageHeaderSection @@ -49,9 +59,14 @@ not this component) — so the lead can run the whole line. afterName?: Snippet /** The workspace the page acts on, when it differs from the one the app is pointed at. */ actingWorkspaceId?: string - /** The bar is short of room: the path holds a tighter cap, so it cannot claim a third of the - * line before anything else has given way. The names truncate at every width either way. */ - narrow?: boolean + /** Which parts the bar has dropped for room (see pageHeaderStages). */ + stage?: TrailStage + /** Max widths the bar holds the names to, once dropping whole parts is not enough. */ + caps?: Partial> + /** The folders above the name truncate too: even the name at its last floor does not fit. */ + scopesGive?: boolean + /** Filled in for the bar to choose a stage with; left undefined, nothing is measured. */ + measures?: TrailMeasures /** 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. */ @@ -156,15 +171,8 @@ not this component) — so the lead can run the whole line. twMerge(SEGMENT, 'hover:bg-transparent', workspaceIsLast && !showFork ? LAST : LEAD) ) - // Capped: a draft's path is a 40-character uuid slug, and the trail yields so grudgingly that - // one would push a page's buttons off the end of the bar. const pathSegment = $derived( - twMerge( - SEGMENT, - 'gap-1.5 overflow-hidden', - itemIsLast ? LAST : LEAD, - narrow ? 'max-w-[11rem]' : 'max-w-[16rem]' - ) + twMerge(SEGMENT, 'gap-1.5 overflow-hidden', itemIsLast ? LAST : LEAD) ) // Copying is the one thing a segment does that leaves no mark on the page, so it is the one // without a hover fill: the pointer and the confirmation under it are the affordance. A segment @@ -191,6 +199,18 @@ not this component) — so the lead can run the whole line. const scopeKind = $derived(item?.path?.split('/')[0]) /** The path's levels, the `f`/`u` prefix dropped — that is what the icon says. */ const pathLevels = $derived((item?.path ?? '').split('/').slice(1).filter(Boolean)) + + // The off-screen copies stand in a fixed-width box for what a page renders into the trail: a + // second render of a page's snippet would be a second live copy of its controls. + let afterNameWidth = $state(0) + let sectionContentWidth = $state(0) + + /** A measured copy holds each name at its floor; the trail on screen at the bar's cap. */ + function capStyle(part: Part, measure: boolean): string | undefined { + if (measure) return `max-width: ${FLOOR[part]}px` + const cap = caps[part] + return cap === undefined ? undefined : `max-width: ${cap}px` + } {#snippet workspaceDisc()} @@ -241,80 +261,114 @@ not this component) — so the lead can run the whole line. {/snippet} -