fix: the app canvas controls stay reachable, and an unnamed item says so

Codex found the app editor's canvas controls unreachable on an ordinary laptop:
the owning route collapsed at 1350px, and a 1440 window with the rail out leaves
1232, so the width, theme and breakpoint toggles were hidden with nothing in the
menu to replace them — and the breakpoint is the only way onto the mobile layout.
Measured on the band, the toggles are 240px beside 334 of other actions and a
~430 trail, so they fit to about 1050: the threshold is 1100 now, and below it
each of the three keeps a menu entry rather than disappearing.

An item with no summary read as a bare `·` with an invisible pen after it, since
the plain text replaced a field that had a placeholder. It now reads "Add a
summary..." in the italic SummaryPathDisplay uses for the same state, and the pen
stops hiding when there is nothing to hover.

Three more from the review:
- `actionsFlexible` says "these actions can give width back", which is true of the
  Runs filter row and not of the pipeline's row of fixed buttons.
- `pathSnapshot ?? (newEditedPath || …)` keeps the `||` chain the five rename
  sites read and the declaration documents; `??` had made the rest unreachable,
  since `newEditedPath` is a string that is never undefined.
- The band bridged `contexts` onto the actions alone, while the pen that decides
  whether a path is the caller's to change renders in `summaryContent`. The
  breadcrumb's `afterName` and the summary are bridged the same way now, so that
  permission check reads the tree that registered it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-01 03:00:12 -07:00
co-authored by Claude Opus 5
parent 24223ef9cd
commit 72e678a923
7 changed files with 85 additions and 25 deletions
@@ -28,7 +28,8 @@
userPathPrefix,
type Item,
type StateStore,
type Value
type Value,
emptyString
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
@@ -1658,11 +1659,15 @@
it renames. -->
<div class="group flex items-center gap-1 min-w-0">
<span
class="min-w-0 truncate text-xs font-medium text-emphasis"
title={flowStore.val.summary}>{flowStore.val.summary}</span
class="min-w-0 truncate text-xs {emptyString(flowStore.val.summary)
? 'text-tertiary italic font-normal'
: 'font-medium text-emphasis'}"
title={flowStore.val.summary}
>{emptyString(flowStore.val.summary) ? 'Add a summary...' : flowStore.val.summary}</span
>
{#if customUi?.topBar?.editablePath != false}
<PathEditPopover
penVisibility={emptyString(flowStore.val.summary) ? 'always' : 'hover'}
bind:summary={flowStore.val.summary}
summaryEditable={customUi?.topBar?.editableSummary != false}
bind:path={$pathStore}
@@ -162,13 +162,20 @@ The row's height matches the sidebar's own header row, so the two read as one ba
navigate the host's workspace. -->
{#if !navHidden}
<div class={twMerge('flex min-w-0', phone ? 'shrink min-w-[10rem]' : 'shrink-[0.1]')}>
<NavBreadcrumb
{item}
{section}
{narrow}
afterName={content?.afterName}
actingWorkspaceId={content?.actingWorkspaceId}
/>
<!-- 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}
@@ -181,7 +188,11 @@ The row's height matches the sidebar's own header row, so the two read as one ba
carries it down with the summary instead of stranding it on the line above. -->
<span class="shrink-0 text-hint/40 text-xs px-0.5" aria-hidden="true">·</span>
{#if item.summaryContent}
{@render item.summaryContent()}
{#key content?.contexts}
<ContextBridge contexts={content?.contexts}>
{@render item.summaryContent()}
</ContextBridge>
{/key}
{:else}
<span class="min-w-0 truncate text-xs font-medium text-emphasis">{item.summary}</span>
{/if}
@@ -2342,11 +2342,16 @@
named rather than where it is edited. The pen sits after it, at the end of what it
renames. -->
<div class="group flex items-center gap-1 min-w-0">
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={script.summary}
>{script.summary}</span
<span
class="min-w-0 truncate text-xs {emptyString(script.summary)
? 'text-tertiary italic font-normal'
: 'font-medium text-emphasis'}"
title={script.summary}
>{emptyString(script.summary) ? 'Add a summary...' : script.summary}</span
>
{#if customUi?.topBar?.editablePath != false}
<PathEditPopover
penVisibility={emptyString(script.summary) ? 'always' : 'hover'}
bind:summary={script.summary}
summaryEditable={customUi?.topBar?.editableSummary != false}
bind:path={script.path}
@@ -8,7 +8,7 @@
import { discardDraftAfterDeploy } from '$lib/userDraftToast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { enterpriseLicense, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
import { isMac, type Item, userPathPrefix } from '$lib/utils'
import { isMac, type Item, userPathPrefix, emptyString } from '$lib/utils'
import { random_adj } from '$lib/components/random_positive_adjetive'
import {
AlignHorizontalSpaceAround,
@@ -264,7 +264,11 @@
// In the page header the buttons share the row with the breadcrumb, so they shed the canvas
// toggles and fold Debug runs into the menu sooner: the whole group is ~840px and the trail
// with the summary takes ~430px of the same row.
const compactBelow = $derived(ownsPageHeader ? 1350 : 720)
// Measured on the band: the canvas toggles are 240px, the rest of the actions 334, and the trail
// with the summary about 430 — so the whole row fits down to ~1050. 1350 hid the toggles on an
// ordinary laptop (a 1440 window with the rail out leaves 1232), and the menu below is what
// keeps them reachable under this.
const compactBelow = $derived(ownsPageHeader ? 1100 : 720)
const compactTopbar = $derived.by(() => {
const w = ownsPageHeader ? pageHeader.barWidth : topbarWidth
return w > 0 && w < compactBelow
@@ -620,7 +624,33 @@
selectedJobId = $jobs[$jobs.length - 1]
}
$jobsDrawerOpen = true
},
}
},
// The canvas toggles stand down with the bar, and the breakpoint among them is the
// only way onto the mobile layout — so each one keeps an entry here.
{
displayName: $breakpoint === 'sm' ? 'Computer view' : 'Mobile view',
icon: $breakpoint === 'sm' ? Laptop2 : Smartphone,
action: () => ($breakpoint = $breakpoint === 'sm' ? 'lg' : 'sm')
},
{
displayName: $app?.fullscreen ? 'Centered canvas' : 'Full-width canvas',
icon: $app?.fullscreen ? AlignHorizontalSpaceAround : Expand,
action: () => {
if ($app) $app.fullscreen = !$app.fullscreen
}
},
{
displayName:
$app?.darkMode === undefined
? 'Theme: automatic'
: $app.darkMode
? 'Theme: dark'
: 'Theme: light',
icon: $app?.darkMode === undefined ? SunMoon : $app.darkMode ? Moon : Sun,
// Cycles auto → light → dark, the order the toggle group reads in.
action: () =>
setTheme($app?.darkMode === undefined ? false : $app.darkMode ? undefined : true),
separatorBottom: true
}
]
@@ -953,7 +983,7 @@
// will create. Same fallback chain the rename sites read.
// Frozen while the pen's popover is open so the trail holds still as the user types.
kind: 'app',
path: pathSnapshot ?? newEditedPath ?? $appPath ?? newPath ?? undefined,
path: pathSnapshot ?? (newEditedPath || $appPath || newPath || undefined),
summaryContent: appSummary
}}
actions={appHeaderActions}
@@ -994,10 +1024,14 @@
named rather than where it is edited. `title` so one the band truncates can still be read.
The pen sits after it, at the end of what it renames. -->
<div class="group flex items-center gap-1 min-w-0">
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={$summary}
>{$summary}</span
<span
class="min-w-0 truncate text-xs {emptyString($summary)
? 'text-tertiary italic font-normal'
: 'font-medium text-emphasis'}"
title={$summary}>{emptyString($summary) ? 'Add a summary...' : $summary}</span
>
<PathEditPopover
penVisibility={emptyString($summary) ? 'always' : 'hover'}
bind:summary={$summary}
bind:path={newEditedPath}
bind:snapshotPath={pathSnapshot}
@@ -50,8 +50,9 @@ 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
/** 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 this registration's snippets look up — its actions, what it hangs after the name,
* and its summary. They all render under the header rather than under the page that wrote
* them, so anything the page's tree provides has to travel with them. */
contexts?: Map<any, any>
}
@@ -3,7 +3,7 @@
import { base } from '$lib/base'
import RawAppRecordSession from '$lib/components/workspaceSettings/RawAppRecordSession.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { isMac, userPathPrefix } from '$lib/utils'
import { isMac, userPathPrefix, emptyString } from '$lib/utils'
import { editPathFor } from '$lib/components/workspacePicker'
import { invalidateWorkspacePaths } from '$lib/components/PathNameAutocomplete.svelte'
@@ -943,7 +943,7 @@
// will create — which is why `newEditedPath` refuses the placeholder. Frozen while the
// pen's popover is open so the trail holds still as the user types.
kind: 'app',
path: pathSnapshot ?? newEditedPath ?? appPath ?? newPath ?? undefined,
path: pathSnapshot ?? (newEditedPath || appPath || newPath || undefined),
summaryContent: appSummary
}}
actions={rawAppHeaderActions}
@@ -1025,9 +1025,14 @@
named rather than where it is edited. `title` so one the band truncates can still be read.
The pen sits after it, at the end of what it renames. -->
<div class="group flex items-center gap-1 min-w-0">
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={summary}>{summary}</span
<span
class="min-w-0 truncate text-xs {emptyString(summary)
? 'text-tertiary italic font-normal'
: 'font-medium text-emphasis'}"
title={summary}>{emptyString(summary) ? 'Add a summary...' : summary}</span
>
<PathEditPopover
penVisibility={emptyString(summary) ? 'always' : 'hover'}
bind:summary
bind:path={newEditedPath}
bind:snapshotPath={pathSnapshot}
@@ -2392,7 +2392,6 @@
section={{ label: mode === 'edit' ? 'Pipeline editor' : 'Pipeline' }}
afterName={pipelineFolder}
actions={pipelineActions}
actionsFlexible
/>
{#snippet pipelineFolder()}