mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
feat: one corner control for the chrome an app page hides
Hovering the button overlaid on a deployed app brings back whatever that page is hiding: the band always, and the sidebar card too when the sidebar is detached. The card hangs from this button rather than from the header's own handle, which is off-screen with the band. The handle in the band no longer attaches the sidebar — it reveals it and nothing more. A control that hides the thing it sits on cannot also be the way to bring it back, so attaching stays with the toggle in the sidebar's own footer, where detaching happens. The band's two buttons go in the order the things nest: the sidebar's handle, then the header's pin. The overlaid button stands where the pin will be once pinned — same 28px box, and a slot further along when the sidebar's handle takes the first — so pinning moves nothing. It wears the panel pair the sidebar's toggle uses: solid while the band is away, dashed while it is shown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
63c1f7b6e8
commit
49035cfd08
@@ -7,7 +7,7 @@ section name — and the route's own buttons at the far end.
|
||||
The row's height matches the sidebar's own header row, so the two read as one band.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PanelLeft, PanelTop } from 'lucide-svelte'
|
||||
import { PanelLeft, PanelTopDashed } from 'lucide-svelte'
|
||||
import { navDetached } from './sidebar/navDetached.svelte'
|
||||
import { navHandleSlot } from './sidebar/navHandlePlacement.svelte'
|
||||
import NavBreadcrumb from './NavBreadcrumb.svelte'
|
||||
@@ -16,15 +16,11 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
|
||||
let {
|
||||
navHidden = false,
|
||||
onDock,
|
||||
onUnpin
|
||||
}: {
|
||||
navHidden?: boolean
|
||||
/** Sends the band back behind its handle, on a page that owns the viewport. */
|
||||
onUnpin?: () => void
|
||||
/** Docks the sidebar. The layout owns the two other things that must happen with it — the
|
||||
* flag that suppresses the rail's entry animation, and closing the card. */
|
||||
onDock?: () => void
|
||||
} = $props()
|
||||
|
||||
const content = $derived(pageHeader.content)
|
||||
@@ -35,22 +31,11 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
</script>
|
||||
|
||||
<div data-page-header class="flex items-center gap-1 h-11 pl-2 pr-4 shrink-0 min-w-0 bg-surface">
|
||||
{#if onUnpin}
|
||||
<!-- First, before the sidebar's handle: this one is about the band the user is looking at,
|
||||
the other about the sidebar beside it. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Unpin header"
|
||||
title="Unpin header"
|
||||
onclick={() => onUnpin?.()}
|
||||
>
|
||||
<PanelTop size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if navDetached.val && !navHidden}
|
||||
<!-- The only way back to a hidden sidebar: hovering slides the card in for a look, a click
|
||||
puts it back for good. Docked, the sidebar speaks for itself and nothing leads the bar. -->
|
||||
<!-- Reveals the hidden sidebar, and only that: hovering slides the card in, clicking holds
|
||||
it there. Attaching it for good belongs to the toggle in the sidebar's own footer, where
|
||||
detaching it happened — a control that hides the thing it sits on cannot also be the
|
||||
way to bring it back. Docked, the sidebar speaks for itself and nothing leads the bar. -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
data-nav-handle
|
||||
@@ -62,13 +47,27 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Show sidebar"
|
||||
onclick={() => onDock?.()}
|
||||
onclick={() => navHandleSlot.open()}
|
||||
>
|
||||
<PanelLeft size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if onUnpin}
|
||||
<!-- After the sidebar's handle: the sidebar is the outer thing, the band sits inside it. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Unpin the header from deployed apps"
|
||||
title="Unpin the header from deployed apps"
|
||||
onclick={() => onUnpin?.()}
|
||||
>
|
||||
<!-- Dashed while the band is shown, solid while it is away: the pair the sidebar's own
|
||||
toggle uses, so the two controls read as one idea. -->
|
||||
<PanelTopDashed size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- The breadcrumb yields width grudgingly (shrink-[0.1]): when a page fills the bar with
|
||||
controls, they are what should narrow, not the name of where the user is. It still gives
|
||||
way rather than pushing them off the bar once there is nothing left to take.
|
||||
|
||||
@@ -25,12 +25,17 @@ export const navHandleSlot = {
|
||||
scheduleClose() {
|
||||
hoverClose?.schedule()
|
||||
},
|
||||
open() {
|
||||
/**
|
||||
* Opens the card under `anchor`, or under the handle in the page header when none is given.
|
||||
* A page that hides the header reveals the sidebar from its own control, and the card has to
|
||||
* hang from the control the pointer is actually on — the header's handle is off-screen there.
|
||||
*/
|
||||
open(anchor?: Element | null) {
|
||||
hoverClose?.cancel()
|
||||
const handle = document.querySelector('[data-nav-handle]')
|
||||
cardTop = handle
|
||||
? Math.round(handle.getBoundingClientRect().bottom) + GAP_UNDER_HANDLE
|
||||
: FALLBACK_CARD_TOP
|
||||
const handle = anchor ?? document.querySelector('[data-nav-handle]')
|
||||
const bottom = handle?.getBoundingClientRect().bottom
|
||||
cardTop =
|
||||
bottom != null && bottom > 0 ? Math.round(bottom) + GAP_UNDER_HANDLE : FALLBACK_CARD_TOP
|
||||
opener?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +526,7 @@
|
||||
const bandPeek = $derived(fullBleed && !appHeaderPinned.val)
|
||||
let bandPeeked = $state(false)
|
||||
let bandHandleHot = $state(false)
|
||||
let bandHandleEl: HTMLDivElement | undefined = $state(undefined)
|
||||
const bandShown = $derived(!bandPeek || bandPeeked)
|
||||
const floatBand = $derived(pageHeader.content?.barRightInset != null || bandPeek)
|
||||
// Same delay as the sidebar's peek, and for the same reason: the handle and the band do not
|
||||
@@ -533,6 +534,9 @@
|
||||
const { debounced: scheduleBandHide, clearDebounce: cancelBandHide } = debounce(() => {
|
||||
bandPeeked = false
|
||||
}, PEEK_CLOSE_DELAY_MS)
|
||||
// Where the band's pin button will be once pinned, so the click moves nothing: the band's own
|
||||
// pl-2, plus a slot (28px button + the row's gap-1) when the sidebar's handle takes the first.
|
||||
const bandHandleOffset = $derived(navDetached.val && !menuHidden ? '2.5rem' : '0.5rem')
|
||||
|
||||
// The handle and the edge band both open the card this layout owns.
|
||||
navHandleSlot.setOpener(() => (menuOpen = true), {
|
||||
@@ -1587,7 +1591,6 @@
|
||||
>
|
||||
<PageHeaderBar
|
||||
navHidden={menuHidden}
|
||||
onDock={() => setDetached(false)}
|
||||
onUnpin={fullBleed && appHeaderPinned.val
|
||||
? () => (appHeaderPinned.val = false)
|
||||
: undefined}
|
||||
@@ -1600,24 +1603,33 @@
|
||||
costs an app's own corner as little as an always-present control can. -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="absolute top-1 z-40 transition-opacity duration-300 {bandShown || bandHandleHot
|
||||
class="absolute top-2 z-40 transition-opacity duration-300 {bandShown || bandHandleHot
|
||||
? 'opacity-100'
|
||||
: 'opacity-40'}"
|
||||
style:left="calc({leftInset}rem + 0.25rem)"
|
||||
style:left="calc({leftInset}rem + {bandHandleOffset})"
|
||||
bind:this={bandHandleEl}
|
||||
onmouseenter={() => {
|
||||
bandHandleHot = true
|
||||
cancelBandHide()
|
||||
bandPeeked = true
|
||||
// The one control for the chrome this page is hiding: the band always, and
|
||||
// the sidebar too when it is detached — hung from this button, since the
|
||||
// header's own handle is off-screen with the band.
|
||||
if (navDetached.val) navHandleSlot.open(bandHandleEl)
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
bandHandleHot = false
|
||||
scheduleBandHide()
|
||||
navHandleSlot.scheduleClose()
|
||||
}}
|
||||
>
|
||||
<!-- Standing where the band's own first button will be — same 28px box, same offsets
|
||||
as its pl-2 and 44px row — so pinning moves nothing. A border would make it
|
||||
30 and the icon would jump on the click. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded bg-surface/80 backdrop-blur-sm shadow-sm border hover:bg-surface"
|
||||
aria-label="Pin header"
|
||||
title="Pin header"
|
||||
class="flex items-center p-1.5 rounded bg-surface/80 backdrop-blur-sm shadow-sm hover:bg-surface"
|
||||
aria-label="Pin the header on deployed apps"
|
||||
title="Pin the header on deployed apps"
|
||||
onclick={() => {
|
||||
appHeaderPinned.val = true
|
||||
bandPeeked = false
|
||||
|
||||
Reference in New Issue
Block a user