From 666a2feb52f057056d5cc4fbfa231ce9018a8f37 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 5 Oct 2026 01:42:59 -0700 Subject: [PATCH] feat: the band owns its bottom edge, and one pen renames everywhere The page header decides whether a separator is drawn, so a page cannot draw a second line under it. A registration asks for `onScroll` (the default, drawn only while something passes under the bar), `always` for a page whose content starts right beneath it, or `none` where the page genuinely draws its own first line. Detail pages, the agent editor, workspace settings and the pipeline list take `always`; the raw app editor takes `none`, since its splitpanes already draw that line. The pen that opens the rename popover becomes PathEditPen, shared by the editors and the deployed pages so the two cannot drift, and the deployed pages gain it. SummaryPathDisplay makes the pen the only trigger rather than the whole name block, so the name is selectable text again. Renaming an agent goes through ResourceService with only the fields the popover edits: the backend sets a column per field it is given, so writing the config back would clobber a concurrent edit. The popover also stops clearing a real collision verdict by telling Path which path is its own. Two Button fixes, both reaching call sites beyond this work: - `dropdownItems` given as a function was asked for `.length`, which is its arity and therefore 0, so a split button built that way rendered no caret. Both app editors' Deploy dropdowns were unreachable. - the anchor branch dropped `title`, so a link button had no tooltip. The audit log filters move into the header actions at the design system's sizes and trade absolutely positioned labels for the form layout in brand-guidelines.md: a label above its input, 4px apart, fields 8px apart. The deployed app viewer floors itself at `min-h-full` instead of claiming the viewport, which would overhang the page box by the height of the band, and carries Edit and a public-url link that fold into a menu on a narrow bar. The root layout drops the legacy menu block, which rendered a second hidden sidebar and 54 invisible tab stops, and marks the closed detached sidebar `inert`. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/FlowBuilder.svelte | 3 + .../src/lib/components/PageHeaderBar.svelte | 13 +- .../lib/components/PageHeaderContent.svelte | 2 + .../src/lib/components/PathEditPen.svelte | 37 +++ .../src/lib/components/PathEditPopover.svelte | 28 +- .../src/lib/components/ScriptBuilder.svelte | 3 + .../lib/components/SummaryPathDisplay.svelte | 300 ++++++++++-------- .../apps/editor/AppEditorHeader.svelte | 17 + .../apps/editor/InWorkspaceAppViewer.svelte | 100 +++++- .../components/apps/editor/PublicApp.svelte | 10 +- .../auditLogs/AuditLogMobileFilters.svelte | 2 +- .../auditLogs/AuditLogsFilters.svelte | 111 ++++--- .../components/common/button/Button.svelte | 21 +- .../details/DetailPageHeader.svelte | 19 +- .../flows/content/AgentEditorModal.svelte | 1 + .../src/lib/components/moveRenameManager.ts | 23 +- .../components/pageHeaderRegistry.svelte.ts | 8 + .../components/raw_apps/RawAppEditor.svelte | 4 + .../raw_apps/RawAppEditorHeader.svelte | 19 +- .../components/sessions/PreviewTabHost.svelte | 1 + .../sessions/RawAppEditorView.svelte | 5 + .../lib/components/sidebar/operatorRoutes.ts | 8 +- .../src/routes/(root)/(logged)/+layout.svelte | 100 +----- .../agents/get/[...path]/+page.svelte | 13 +- .../apps_raw/edit/[...path]/+page.svelte | 1 + .../(root)/(logged)/audit_logs/+page.svelte | 7 +- .../(root)/(logged)/pipeline/+page.svelte | 29 +- .../(logged)/workspace_settings/+page.svelte | 36 ++- 28 files changed, 540 insertions(+), 381 deletions(-) create mode 100644 frontend/src/lib/components/PathEditPen.svelte diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 9874495110..a347cd4003 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -1570,6 +1570,8 @@ {#if ownsPageHeader} + {:else} diff --git a/frontend/src/lib/components/PageHeaderBar.svelte b/frontend/src/lib/components/PageHeaderBar.svelte index 4b541c1592..ca2927712a 100644 --- a/frontend/src/lib/components/PageHeaderBar.svelte +++ b/frontend/src/lib/components/PageHeaderBar.svelte @@ -70,21 +70,24 @@ The row's height matches the sidebar's own header row, so the two read as one ba // A page left while scrolled would otherwise hand its edge to the next one, which may have // nothing to scroll at all. afterNavigate(() => (scrolledUnder = false)) + + const separator = $derived(pageHeader.content?.separator ?? 'onScroll') + const showEdge = $derived(separator === 'always' || (separator === 'onScroll' && scrolledUnder)) - +
pageHeader.barWidth, (w) => pageHeader.setBarWidth(w)} 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', - scrolledUnder && + 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))]' )} > diff --git a/frontend/src/lib/components/PageHeaderContent.svelte b/frontend/src/lib/components/PageHeaderContent.svelte index 462984e6e7..5e023a68b3 100644 --- a/frontend/src/lib/components/PageHeaderContent.svelte +++ b/frontend/src/lib/components/PageHeaderContent.svelte @@ -17,6 +17,7 @@ the header, so nesting is decided by the caller rather than guessed from context actions, actionsOrder, actionsFlexible, + separator, contexts, actingWorkspaceId, barRightInset @@ -30,6 +31,7 @@ the header, so nesting is decided by the caller rather than guessed from context actions, actionsOrder, actionsFlexible, + separator, contexts, actingWorkspaceId, barRightInset diff --git a/frontend/src/lib/components/PathEditPen.svelte b/frontend/src/lib/components/PathEditPen.svelte new file mode 100644 index 0000000000..c74ea8dded --- /dev/null +++ b/frontend/src/lib/components/PathEditPen.svelte @@ -0,0 +1,37 @@ + + + + {:else} - {path} -

Only the owner can change the path

+ +
+
Path
+ +
{/if} - - {#if onBehalfOfEmail} - - This flow will be redeployed on behalf of you ({$userStore?.email}) instead of {onBehalfOfEmail} - - {/if} - - {:else} - -
-
Path
-
- {/if} -
- {/snippet} - + {/snippet} + + + {:else}
{#if !emptyString(summary) && !compact} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 6e83eb824e..2ee69a7380 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -634,6 +634,22 @@ icon: $breakpoint === 'sm' ? Laptop2 : Smartphone, action: () => ($breakpoint = $breakpoint === 'sm' ? 'lg' : 'sm') }, + // The setting the mobile view is edited for, which otherwise lives beside the + // toggle group that stood down: without it here a deployed app could be put on + // the mobile layout at this width and never taken off it. + ...($breakpoint === 'sm' + ? [ + { + displayName: $app?.mobileViewOnSmallerScreens + ? 'Mobile view on small screens: on' + : 'Mobile view on small screens: off', + icon: Smartphone, + action: () => { + if ($app) $app.mobileViewOnSmallerScreens = !$app.mobileViewOnSmallerScreens + } + } + ] + : []), { displayName: $app?.fullscreen ? 'Centered canvas' : 'Full-width canvas', icon: $app?.fullscreen ? AlignHorizontalSpaceAround : Expand, @@ -989,6 +1005,7 @@ }} actions={appHeaderActions} contexts={headerContexts} + separator="always" /> {:else}
(undefined) + async function loadPublicUrl() { + try { + const secret = await AppService.getPublicSecretOfApp({ workspace, path }) + // Built from this viewer's workspace rather than the navigation one, like everything + // else here: the two differ inside a session's preview panel. + publicUrl = secret + ? `${window.location.origin}${base}/public/${workspace}/${secret}` + : undefined + } catch (_) { + publicUrl = undefined + } + } + $effect(() => { if (workspace && path) loadPerms() }) + $effect(() => { + publicUrl = undefined + if (ownsPageHeader && !menuHidden && workspace && path) loadPublicUrl() + }) + + // Both of this page's buttons carry a label, and a phone's bar has room for neither beside the + // app's path — so below that width they become one menu. Unmeasured (0) counts as wide: the bar + // measures itself on mount, and starting compact would pop them out a frame later. + const compact = $derived(pageHeader.barWidth > 0 && pageHeader.barWidth < PHONE_BAR) + const compactItems: Item[] = $derived([ + ...(publicUrl + ? [ + { + displayName: 'Public url', + icon: ExternalLink, + href: publicUrl, + hrefTarget: '_blank' as const + } + ] + : []), + { + displayName: 'Edit', + icon: Pen, + href: onEdit ? undefined : editHref, + action: onEdit ? () => onEdit() : undefined + } + ])
@@ -208,14 +255,15 @@ {#if ownsPageHeader} {/if} - + {#if showEdit && (menuHidden || !ownsPageHeader)}
{@render editAction()} @@ -223,14 +271,34 @@ {/if} {#snippet editAction()} - - + {#if compact} + + + {:else} + {#if publicUrl} + + + {/if} + + + {/if} {/snippet} diff --git a/frontend/src/lib/components/apps/editor/PublicApp.svelte b/frontend/src/lib/components/apps/editor/PublicApp.svelte index bb73bc171f..6d073ee711 100644 --- a/frontend/src/lib/components/apps/editor/PublicApp.svelte +++ b/frontend/src/lib/components/apps/editor/PublicApp.svelte @@ -187,10 +187,12 @@
{#snippet trigger()} - {/snippet} diff --git a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte index 4090a39dbb..ae6ac5eba6 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte @@ -266,12 +266,20 @@ ;[username, perPage, before, after, operation, resource, actionKind, scope, pageIndex] untrack(() => updateQueryParams()) }) + + // `leading-none`: the label's own line box is what decides whether a stacked field fits the + // header band. At the default leading the field is 52px against a 50px bar and the label is + // clipped off the top; at the font's own height it is 44px. + const fieldLabel = 'text-xs font-semibold text-emphasis shrink-0 leading-none' -
+ +
{#if $workspaceStore == 'admins'} -
- Scope +
+ Scope { @@ -301,43 +309,48 @@
{/if} -
- From - - { - after = new Date(detail).toISOString() - }} - on:clear={() => { - after = undefined - }} - /> +
+ From +
+ + { + after = new Date(detail).toISOString() + }} + on:clear={() => { + after = undefined + }} + /> +
-
- To - - { - before = new Date(detail).toISOString() - }} - on:clear={() => { - before = undefined - }} - /> +
+ To +
+ + { + before = new Date(detail).toISOString() + }} + on:clear={() => { + before = undefined + }} + /> +
-
- Username +
+ Username (resources.value?.push(r), (resource = r))} createText="Press enter to use this value" bind:value={resource} @@ -368,11 +381,11 @@ />
-
- Operation - +
+ Operation
-
+ +
-
-
-
- - {#if !isCollapsed}{#if $whitelabelNameStore}{capitalize( - $whitelabelNameStore - )}{:else}Windmill{/if}{/if} -
- -
- - {#snippet children({ createMenu })} - - - {/snippet} - - openSearchModal()} - {isCollapsed} - icon={Search} - label="Search" - class="!text-xs" - shortcut={`${getModifierKey()}k`} - /> - {#if !$copilotInfo.workspaceDisabled} - aiChatManager.toggleOpen()} - {isCollapsed} - icon={WandSparkles} - iconProps={{ - forceDarkMode: true - }} - label="Ask AI" - class="!text-xs" - iconClasses="!text-ai" - shortcut={`${getModifierKey()}L`} - /> - {/if} -
- - -
-
-
-
{/if}
- + {#snippet auditHint()} @@ -121,7 +121,10 @@ {/snippet} {#snippet auditActions()} -