mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
feat: fold the runs header through the bar and wrap its filter to a second row
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23f6e69bf2
commit
78485aa23b
@@ -152,173 +152,199 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
data-page-header
|
||||
bind:clientWidth={() => pageHeader.barWidth, (w) => pageHeader.setBarWidth(w)}
|
||||
class={twMerge(
|
||||
'flex items-center gap-1 shrink-0 min-w-0 bg-surface transition-shadow duration-150',
|
||||
// One row at every width. A wrapping row never squeezes anything — flex moves an item to the
|
||||
// next line instead of taking it below its content width — so wrapping here would hand the
|
||||
// trail a line of its own and leave it whole, which is the opposite of what a narrow bar
|
||||
// wants: the trail truncating from its start.
|
||||
phone ? `${BAR_HEIGHT} pl-1 pr-2` : `${BAR_HEIGHT} pl-2 pr-4`,
|
||||
// The filling surface is what meets the right edge, so the bar keeps no gutter of its own
|
||||
// there.
|
||||
content?.actionsFill && 'pr-0',
|
||||
// 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',
|
||||
'flex flex-col shrink-0 min-w-0 bg-surface transition-shadow duration-150',
|
||||
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))]'
|
||||
)}
|
||||
>
|
||||
{#if (navDetached.val || panelled) && !navHidden}
|
||||
<!-- Reveals the hidden sidebar, and only that: the card follows the pointer, holding while it
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex items-center gap-1 min-w-0',
|
||||
// No flex-wrap. A wrapping row never squeezes anything — flex moves an item to the next line
|
||||
// instead of taking it below its content width — so wrapping here would hand the trail a
|
||||
// line of its own and leave it whole. What needs a line of its own asks for one through
|
||||
// `belowBar`, at a fold the bar picks.
|
||||
phone ? `${BAR_HEIGHT} pl-1 pr-2` : `${BAR_HEIGHT} pl-2 pr-4`,
|
||||
// The filling surface is what meets the right edge, so the bar keeps no gutter of its own
|
||||
// there.
|
||||
content?.actionsFill && 'pr-0',
|
||||
// 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'
|
||||
)}
|
||||
>
|
||||
{#if (navDetached.val || panelled) && !navHidden}
|
||||
<!-- Reveals the hidden sidebar, and only that: the card follows the pointer, holding while it
|
||||
is on the handle, on the card, or in a menu either opened, and sliding back out when it
|
||||
leaves. Where no pointer hovers, a tap opens it and it stays until dismissed. Attaching
|
||||
it for good belongs to the toggle in the sidebar's own footer, where detaching it
|
||||
happened. Docked, the sidebar speaks for itself and nothing leads the bar. -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
data-nav-handle
|
||||
bind:clientWidth={handleWidth}
|
||||
onmouseenter={() => navHandleSlot.open()}
|
||||
onmouseleave={() => navHandleSlot.scheduleClose()}
|
||||
>
|
||||
<!-- No tooltip: hovering here already slides the sidebar in, which says what the button
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
data-nav-handle
|
||||
bind:clientWidth={handleWidth}
|
||||
onmouseenter={() => navHandleSlot.open()}
|
||||
onmouseleave={() => navHandleSlot.scheduleClose()}
|
||||
>
|
||||
<!-- No tooltip: hovering here already slides the sidebar in, which says what the button
|
||||
does better than a label popping up over it. -->
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
iconOnly
|
||||
startIcon={{ icon: PanelLeft }}
|
||||
aria-label="Show sidebar"
|
||||
onclick={() => navHandleSlot.open()}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
iconOnly
|
||||
startIcon={{ icon: PanelLeft }}
|
||||
aria-label="Show sidebar"
|
||||
onclick={() => navHandleSlot.open()}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- The breadcrumb's parts are rigid and the bar sizes them (see `fit`): it drops whole parts
|
||||
<!-- The breadcrumb's parts are rigid and the bar sizes them (see `fit`): it drops whole parts
|
||||
and caps the names left, so this box only shrinks past what they add up to on a bar too
|
||||
narrow for even the last stage.
|
||||
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="flex min-w-0">
|
||||
<!-- Bridged like the actions below: what a page hangs off its own name renders here, out of
|
||||
<div class="flex min-w-0">
|
||||
<!-- Bridged like the actions below: what a page hangs off its own name renders here, out of
|
||||
the tree that named it, and the rename editor in there asks that tree who the acting user
|
||||
is before it offers to rename anything. -->
|
||||
<!-- Each fragment the band renders comes from another tree and reads that tree's state. The
|
||||
<!-- Each fragment the band renders comes from another tree and reads that tree's state. The
|
||||
registry already tolerates a registration that cannot answer while its owner's data is
|
||||
being cleared (see `read`); the same window reaches rendering, and an error here would
|
||||
otherwise travel to the root and blank the app rather than the one fragment. A
|
||||
boundary per fragment keeps that to the fragment. -->
|
||||
<svelte:boundary onerror={(e) => console.error('page header: breadcrumb failed to render', e)}>
|
||||
{#key content?.contexts}
|
||||
<ContextBridge contexts={content?.contexts}>
|
||||
<NavBreadcrumb
|
||||
{item}
|
||||
{section}
|
||||
{stage}
|
||||
caps={fit.caps}
|
||||
scopesGive={fit.scopesGive}
|
||||
bind:measures={trailMeasures}
|
||||
nameOnly={navHidden}
|
||||
afterName={content?.afterName}
|
||||
actingWorkspaceId={content?.actingWorkspaceId}
|
||||
/>
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
<svelte:boundary
|
||||
onerror={(e) => console.error('page header: breadcrumb failed to render', e)}
|
||||
>
|
||||
{#key content?.contexts}
|
||||
<ContextBridge contexts={content?.contexts}>
|
||||
<NavBreadcrumb
|
||||
{item}
|
||||
{section}
|
||||
{stage}
|
||||
caps={fit.caps}
|
||||
scopesGive={fit.scopesGive}
|
||||
bind:measures={trailMeasures}
|
||||
nameOnly={navHidden}
|
||||
afterName={content?.afterName}
|
||||
actingWorkspaceId={content?.actingWorkspaceId}
|
||||
/>
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
|
||||
{#if item && hasSummary && stage.summary}
|
||||
<!-- No kind icon: the page below is the item, and saying "this is a flow" above a flow
|
||||
{#if item && hasSummary && stage.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="flex items-center gap-1 shrink-0">
|
||||
<!-- A dot rather than a slash: the summary names the same item the path just located, it
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<!-- A dot rather than a slash: the summary names the same item the path just located, it
|
||||
is not another level of it. -->
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5" aria-hidden="true">·</span>
|
||||
{#if item.summaryContent}
|
||||
<!-- Read only while uncapped: capped, the box says how wide the bar let it be, not how
|
||||
wide the page's control wants to be. -->
|
||||
<div
|
||||
class="flex min-w-0 overflow-hidden"
|
||||
style={fit.caps.summary === undefined ? undefined : `max-width: ${fit.caps.summary}px`}
|
||||
bind:clientWidth={
|
||||
() => summaryContentWidth,
|
||||
(w) => fit.caps.summary === undefined && (summaryContentWidth = w)
|
||||
}
|
||||
>
|
||||
<svelte:boundary
|
||||
onerror={(e) => console.error('page header: summary failed to render', e)}
|
||||
>
|
||||
{#key content?.contexts}
|
||||
<ContextBridge contexts={content?.contexts}>
|
||||
{@render item.summaryContent()}
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
{:else}
|
||||
<span
|
||||
class="truncate text-xs font-medium text-emphasis"
|
||||
style={fit.caps.summary === undefined ? undefined : `max-width: ${fit.caps.summary}px`}
|
||||
>{item.summary}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if actions.length > 0}
|
||||
<!-- A plain row of buttons holds its width (shrink-0): squeezing it only pushes buttons out
|
||||
of the bar, so the bar makes room by dropping breadcrumb parts and then by asking the
|
||||
page to fold (`actionFolds`). Flexible actions (the Runs filter row) are a control that
|
||||
can give width back, which it can only do if this box may shrink. -->
|
||||
<!-- `actionsFill`: the actions are a surface rather than a row, so they take the rest of the
|
||||
bar and run to its right edge, and they stretch its full height because that surface
|
||||
sits on the bottom edge. -->
|
||||
<div
|
||||
{@attach measureActions(fit.fold, actionsEpoch)}
|
||||
class={twMerge(
|
||||
'flex',
|
||||
// pl-4: the gap between the breadcrumb and the filling surface is bare bar, so it
|
||||
// belongs here rather than inside that surface, where it would be filled.
|
||||
content?.actionsFill
|
||||
? 'flex-1 min-w-0 self-stretch items-stretch pl-4'
|
||||
: 'ml-auto items-center',
|
||||
!content?.actionsFill && (actionsRigid ? 'shrink-0' : 'min-w-0'),
|
||||
!content?.actionsFill && (phone ? 'gap-1 pl-1' : 'gap-2 pl-4'),
|
||||
actionsUnmeasured && 'invisible'
|
||||
)}
|
||||
>
|
||||
{#each actions as entry, i (i)}
|
||||
<svelte:boundary onerror={(e) => console.error('page header: actions failed to render', e)}>
|
||||
{#key entry.contexts}
|
||||
<ContextBridge contexts={entry.contexts}>
|
||||
{@render entry.render()}
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if item && hasSummary}
|
||||
<!-- The summary at its floor, which is what a stage that keeps it needs; and whole, which is
|
||||
what the bar caps it from. A page's own control stands in at the width last seen. -->
|
||||
<div class="absolute -left-[99999px] top-0 invisible" aria-hidden="true" inert>
|
||||
<div class="flex items-center gap-1 w-max" bind:clientWidth={summaryFloorWidth}>
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5">·</span>
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5" aria-hidden="true">·</span>
|
||||
{#if item.summaryContent}
|
||||
<span style="width: {Math.min(summaryContentWidth, FLOOR.summary)}px"></span>
|
||||
<!-- Read only while uncapped: capped, the box says how wide the bar let it be, not how
|
||||
wide the page's control wants to be. -->
|
||||
<div
|
||||
class="flex min-w-0 overflow-hidden"
|
||||
style={fit.caps.summary === undefined ? undefined : `max-width: ${fit.caps.summary}px`}
|
||||
bind:clientWidth={
|
||||
() => summaryContentWidth,
|
||||
(w) => fit.caps.summary === undefined && (summaryContentWidth = w)
|
||||
}
|
||||
>
|
||||
<svelte:boundary
|
||||
onerror={(e) => console.error('page header: summary failed to render', e)}
|
||||
>
|
||||
{#key content?.contexts}
|
||||
<ContextBridge contexts={content?.contexts}>
|
||||
{@render item.summaryContent()}
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="truncate text-xs font-medium" style="max-width: {FLOOR.summary}px"
|
||||
<span
|
||||
class="truncate text-xs font-medium text-emphasis"
|
||||
style={fit.caps.summary === undefined ? undefined : `max-width: ${fit.caps.summary}px`}
|
||||
>{item.summary}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<span
|
||||
class="inline-block whitespace-nowrap text-xs font-medium"
|
||||
bind:clientWidth={summaryTextWidth}>{item.summary ?? ''}</span
|
||||
{/if}
|
||||
|
||||
{#if actions.length > 0}
|
||||
<!-- A plain row of buttons holds its width (shrink-0): squeezing it only pushes buttons out
|
||||
of the bar, so the bar makes room by dropping breadcrumb parts and then by asking the
|
||||
page to fold (`actionFolds`). Flexible actions (the Runs filter row) are a control that
|
||||
can give width back, which it can only do if this box may shrink. -->
|
||||
<!-- `actionsFill`: the actions are a surface rather than a row, so they take the rest of the
|
||||
bar and run to its right edge, and they stretch its full height because that surface
|
||||
sits on the bottom edge. -->
|
||||
<div
|
||||
{@attach measureActions(fit.fold, actionsEpoch)}
|
||||
class={twMerge(
|
||||
'flex',
|
||||
// pl-4: the gap between the breadcrumb and the filling surface is bare bar, so it
|
||||
// belongs here rather than inside that surface, where it would be filled.
|
||||
content?.actionsFill
|
||||
? 'flex-1 min-w-0 self-stretch items-stretch pl-4'
|
||||
: 'ml-auto items-center',
|
||||
!content?.actionsFill && (actionsRigid ? 'shrink-0' : 'min-w-0'),
|
||||
!content?.actionsFill && (phone ? 'gap-1 pl-1' : 'gap-2 pl-4'),
|
||||
actionsUnmeasured && 'invisible'
|
||||
)}
|
||||
>
|
||||
{#each actions as entry, i (i)}
|
||||
<svelte:boundary
|
||||
onerror={(e) => console.error('page header: actions failed to render', e)}
|
||||
>
|
||||
{#key entry.contexts}
|
||||
<ContextBridge contexts={entry.contexts}>
|
||||
{@render entry.render()}
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if item && hasSummary}
|
||||
<!-- The summary at its floor, which is what a stage that keeps it needs; and whole, which is
|
||||
what the bar caps it from. A page's own control stands in at the width last seen. -->
|
||||
<div class="absolute -left-[99999px] top-0 invisible" aria-hidden="true" inert>
|
||||
<div class="flex items-center gap-1 w-max" bind:clientWidth={summaryFloorWidth}>
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5">·</span>
|
||||
{#if item.summaryContent}
|
||||
<span style="width: {Math.min(summaryContentWidth, FLOOR.summary)}px"></span>
|
||||
{:else}
|
||||
<span class="truncate text-xs font-medium" style="max-width: {FLOOR.summary}px"
|
||||
>{item.summary}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<span
|
||||
class="inline-block whitespace-nowrap text-xs font-medium"
|
||||
bind:clientWidth={summaryTextWidth}>{item.summary ?? ''}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if content?.belowBar && fit.fold >= (content.belowBarFold ?? 0)}
|
||||
<!-- Same gutter as the row above, so what moved down lines up with the breadcrumb's start. -->
|
||||
<div
|
||||
class={twMerge('flex items-center pb-2', phone ? 'px-2' : 'pl-3 pr-4', navHidden && 'pl-4')}
|
||||
>
|
||||
<svelte:boundary
|
||||
onerror={(e) => console.error('page header: second row failed to render', e)}
|
||||
>
|
||||
{#key content?.contexts}
|
||||
<ContextBridge contexts={content?.contexts}>
|
||||
{@render content.belowBar()}
|
||||
</ContextBridge>
|
||||
{/key}
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFlexible,
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
contexts,
|
||||
actingWorkspaceId,
|
||||
@@ -35,6 +37,8 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFlexible,
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
contexts,
|
||||
actingWorkspaceId,
|
||||
|
||||
@@ -37,9 +37,8 @@
|
||||
import RunsQueue from '$lib/components/runs/RunsQueue.svelte'
|
||||
import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import { pageHeader } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import { RUNS_PATH } from '$lib/components/sessions/previewPaths'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { computeJobKinds, useJobsLoader } from '$lib/components/runs/useJobsLoader.svelte'
|
||||
import ConcurrentJobsChart from '$lib/components/ConcurrentJobsChart.svelte'
|
||||
import BatchLoadProgress from '$lib/components/BatchLoadProgress.svelte'
|
||||
@@ -603,7 +602,11 @@
|
||||
openedRunId = undefined
|
||||
}
|
||||
let resolutionNote = $state('')
|
||||
const phone = $derived(pageHeader.barWidth > 0 && pageHeader.barWidth < PHONE_BAR)
|
||||
// The bar folds this row (`actionFolds` below) once its breadcrumb has given all it can: first
|
||||
// the counters, which are a glance and repeat the chart below; then the session button; then
|
||||
// the filter field moves to a row of its own rather than shrinking past usable.
|
||||
const fold = $derived(pageHeader.actionFold)
|
||||
const filterBelow = $derived(fold >= 3)
|
||||
</script>
|
||||
|
||||
{#snippet runsHint()}
|
||||
@@ -615,18 +618,34 @@
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
{#snippet runsFilter(cls: string)}
|
||||
<FilterSearchbar
|
||||
class={cls}
|
||||
schema={runsFilterSearchbarSchema}
|
||||
presets={buildRunsFilterPresets({
|
||||
isSuperAdminOrDevops: !!$superadmin || !!$devopsRole,
|
||||
isAdminsWorkspace: $workspaceStore === 'admins'
|
||||
})}
|
||||
bind:value={filters.val}
|
||||
placeholder="Filter runs..."
|
||||
size="sm"
|
||||
autofocus
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet filterRow()}
|
||||
{@render runsFilter('relative w-full min-w-0')}
|
||||
{/snippet}
|
||||
|
||||
{#snippet headerActions()}
|
||||
<!-- The page's own row inside the band's actions box: these controls are denser than a page's
|
||||
two or three buttons, so they take a wider gap than the band's own. `flex-1 min-w-0` passes
|
||||
the band's flexibility down to the search field, the one control here that gives width
|
||||
back; the rest keep theirs or drop out of the row entirely at a width. -->
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
two or three buttons, so they take a wider gap than the band's own. Every control keeps its
|
||||
width: the bar measures the row and folds it rather than squeezing it. -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Always the icon form: the bar has room for two counters, never for the two labelled
|
||||
blocks. pr-2 on top of the row's own gap because the counts hang outside their icons,
|
||||
so without it the badge butts against the first filter.
|
||||
A phone's bar has room for the filter field and the timeframe and nothing else: the
|
||||
counters are a glance, and the same numbers are on the chart below. -->
|
||||
<div class="flex items-center pr-2 {phone ? 'hidden' : ''}">
|
||||
so without it the badge butts against the first filter. -->
|
||||
<div class="flex items-center pr-2 {fold >= 1 ? 'hidden' : ''}">
|
||||
<RunsQueue
|
||||
success={filters.val.status ?? null}
|
||||
{queue_count}
|
||||
@@ -797,32 +816,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The one control in the band that gives way: everything else keeps the width it asked
|
||||
for, or drops out of the row at a width, and this takes what is left. -->
|
||||
<FilterSearchbar
|
||||
class={twMerge(
|
||||
// A width of its own, and only ever less. As `flex-1` the field was whatever the row
|
||||
// had left over, so a chip appearing or a filter committing resized it under the
|
||||
// cursor and the last pixels came off the breadcrumb. 18rem is its width whenever the
|
||||
// row fits, and it still shrinks when the row runs short, which is what keeps the row
|
||||
// inside a box the bar lets shrink (`actionsFlexible`). `min-w-0` is what makes that
|
||||
// shrink unconditional: a flex item's floor is its min-content width, which the chips
|
||||
// inside the field raise, so without it a typed filter would push the field wider
|
||||
// again exactly when the row has no room to give.
|
||||
'relative w-[18rem] min-w-0 max-w-full',
|
||||
// A phone's bar cannot spare that much before the timeframe beside it.
|
||||
phone && 'w-[10rem]'
|
||||
)}
|
||||
schema={runsFilterSearchbarSchema}
|
||||
presets={buildRunsFilterPresets({
|
||||
isSuperAdminOrDevops: !!$superadmin || !!$devopsRole,
|
||||
isAdminsWorkspace: $workspaceStore === 'admins'
|
||||
})}
|
||||
bind:value={filters.val}
|
||||
placeholder="Filter runs..."
|
||||
size="sm"
|
||||
autofocus
|
||||
/>
|
||||
<!-- A width of its own, never what the row has left over: as `flex-1` a chip appearing or a
|
||||
filter committing resized it under the cursor. `shrink-0` keeps it at that width, which
|
||||
is the row's width the bar measures and folds. -->
|
||||
{#if !filterBelow}{@render runsFilter('relative w-[18rem] shrink-0')}{/if}
|
||||
<TimeframeSelect
|
||||
unifiedSize="sm"
|
||||
onClick={() => jobsLoader?.loadJobs(true)}
|
||||
@@ -833,7 +830,7 @@
|
||||
<!-- The filters are shallow-routed, so the search has to come off `window.location` at click
|
||||
time — `page.url` never sees them. Always the canonical `/runs`: only that is a recognized
|
||||
preview page, and the `/runs/<path>` route mirrors its path into `?path=` anyway. -->
|
||||
<div class={phone ? 'hidden' : 'contents'}>
|
||||
<div class={fold >= 2 ? 'hidden' : 'contents'}>
|
||||
<OpenInSessionButton
|
||||
source={{
|
||||
page: () => RUNS_PATH + window.location.search,
|
||||
@@ -913,9 +910,13 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full flex flex-col" bind:clientWidth={innerWidth}>
|
||||
<!-- `actionsFlexible`: the filter row holds a search field that can give width back, so the
|
||||
bar lets this box shrink on a phone instead of holding its content width. -->
|
||||
<PageHeaderContent afterName={runsHint} actions={headerActions} actionsFlexible />
|
||||
<PageHeaderContent
|
||||
afterName={runsHint}
|
||||
actions={headerActions}
|
||||
actionFolds={3}
|
||||
belowBar={filterRow}
|
||||
belowBarFold={3}
|
||||
/>
|
||||
|
||||
<!-- Graph -->
|
||||
<div id="runs-chart" class="p-2 px-4 bg-surface-tertiary mx-4 border rounded-md">
|
||||
|
||||
@@ -57,6 +57,14 @@ export type PageHeaderContent = {
|
||||
* all it can, and the page reads it back as `pageHeader.actionFold`. Without it the actions
|
||||
* never fold and the breadcrumb gives up width around them. */
|
||||
actionFolds?: number
|
||||
/** A second row of the band, under the breadcrumb and the actions: where a control too wide
|
||||
* for one row goes at a fold that has no room for it beside them, rather than being squeezed
|
||||
* until it is useless. The band is taller while it is there. */
|
||||
belowBar?: Snippet
|
||||
/** The fold from which the bar shows `belowBar`. The bar decides rather than the page passing
|
||||
* the row in only at that fold: the fold is derived from this registration, so a registration
|
||||
* that read it would be a cycle. */
|
||||
belowBarFold?: number
|
||||
/** Rendered right after the page's name in the breadcrumb: a mark that belongs to the name,
|
||||
* like a documentation tooltip, or a control that acts on the thing named — an app's Edit,
|
||||
* which at the far end of the bar would be a journey away from what it edits. */
|
||||
@@ -159,6 +167,8 @@ export const pageHeader = {
|
||||
if (c.actionsFlexible !== undefined) merged.actionsFlexible = c.actionsFlexible
|
||||
if (c.actionsFill !== undefined) merged.actionsFill = c.actionsFill
|
||||
if (c.actionFolds !== undefined) merged.actionFolds = c.actionFolds
|
||||
if (c.belowBar !== undefined) merged.belowBar = c.belowBar
|
||||
if (c.belowBarFold !== undefined) merged.belowBarFold = c.belowBarFold
|
||||
if (c.separator !== undefined) merged.separator = c.separator
|
||||
}
|
||||
return merged
|
||||
|
||||
Reference in New Issue
Block a user