mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 08:07:15 +00:00
fix(frontend): graceful small-screen timeframe picker on the runs page (#10073)
* fix(frontend): prevent runs timeframe calendar popover overflow on small screens The Runs page timeframe picker rendered its popover as a wide 3-column row (preset list + two side-by-side calendars). With the right-aligned trigger and a center-anchored `bottom` placement, the popup ran off the right edge on narrow viewports. Anchor the popover to the right edge (`placement="bottom-end"`) and make its content reflow to a vertical stack below the `sm` breakpoint, capped at `max-w-[calc(100vw-2rem)] max-h-[80vh] overflow-auto` so it can never exceed the viewport. The desktop side-by-side layout is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): compact runs timeframe picker with a Start/End toggle on small screens The two-calendar desktop popover needs ~780px (two min-w-9 grids + presets + popover padding); below that it overflows. Under 800px, show a single calendar with a Start/End toggle picking which bound it edits, using set-start/set-end so each bound keeps its date and HH:MM time inputs — the same precision the desktop start/end pair offers. On short/landscape viewports the compact panel is scroll-contained within the popover's fitViewport height (contentClasses overflow-y-auto, scoped to the small layout) so its lower controls stay reachable. The desktop two-calendar layout is unchanged. Presets are shared between both layouts via a snippet, and the active range is preserved across the breakpoint since both branches drive the same value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): let InlineCalendarInput month/year selects portal, use in compact timeframe picker Add an opt-in `portalSelects` prop to InlineCalendarInput that portals the month/year dropdowns to the body (default keeps them in-flow, so existing consumers are unchanged). The compact runs timeframe picker enables it so the dropdowns escape its scroll-contained (overflow-y-auto) popover instead of being clipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2fe999f66c
commit
af177cefe0
@@ -63,9 +63,18 @@
|
||||
|
||||
type Props = (DateProps | RangeProps) & {
|
||||
class?: string
|
||||
// Portal the month/year dropdowns to the body so they escape an `overflow`
|
||||
// ancestor (e.g. a scroll-contained popover). Off by default: in-flow.
|
||||
portalSelects?: boolean
|
||||
}
|
||||
|
||||
let { mode = 'date', value = $bindable(), class: className, ...rest }: Props = $props()
|
||||
let {
|
||||
mode = 'date',
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
portalSelects = false,
|
||||
...rest
|
||||
}: Props = $props()
|
||||
|
||||
const onClickBehavior = $derived(
|
||||
mode === 'range' ? ((rest as RangeProps).onClickBehavior ?? 'set-range') : 'set-range'
|
||||
@@ -425,14 +434,14 @@
|
||||
<Select
|
||||
class="basis-1/2"
|
||||
inputClass="text-center !rounded-r-none !border-r-0"
|
||||
disablePortal
|
||||
disablePortal={!portalSelects}
|
||||
bind:value={viewMonth}
|
||||
items={MONTH_NAMES.map((name, i) => ({ label: name, value: i + 1 }))}
|
||||
/>
|
||||
<Select
|
||||
class="basis-1/2"
|
||||
inputClass="text-center !rounded-l-none !border-l-0"
|
||||
disablePortal
|
||||
disablePortal={!portalSelects}
|
||||
bind:value={viewYear}
|
||||
onCreateItem={(val) => (viewYear = parseInt(val) || viewYear)}
|
||||
items={YEAR_LIST.map((year) => ({ label: year.toString(), value: year }))}
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
<script lang="ts">
|
||||
import { CalendarIcon, RefreshCw } from 'lucide-svelte'
|
||||
import { Button } from '../common'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Popover from '../meltComponents/Popover.svelte'
|
||||
import { watch } from 'runed'
|
||||
import { page } from '$app/state'
|
||||
@@ -98,6 +100,14 @@
|
||||
|
||||
let isOpen = $state(false)
|
||||
|
||||
// The two-calendar desktop popover needs ~780px (two min-w-9 grids + presets +
|
||||
// popover padding); below that it overflows. Under 800px use a single calendar
|
||||
// with a Start/End toggle, which keeps each bound's date + time inputs.
|
||||
const TWO_CALENDAR_MIN_WIDTH = 800
|
||||
let innerWidth = $state<number | undefined>(undefined)
|
||||
const isSmall = $derived(innerWidth != undefined && innerWidth < TWO_CALENDAR_MIN_WIDTH)
|
||||
let smallBound = $state<'start' | 'end'>('start')
|
||||
|
||||
function onManualInput(input: { minTs?: string | null; maxTs?: string | null }) {
|
||||
if (value.type !== 'manual')
|
||||
value = buildManualTimeframe(input.minTs ?? null, input.maxTs ?? null)
|
||||
@@ -113,6 +123,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
|
||||
{#snippet presetButtons()}
|
||||
{#each items as item (item.label)}
|
||||
<Button
|
||||
onClick={() => (value = { ...item })}
|
||||
variant="subtle"
|
||||
unifiedSize="md"
|
||||
selected={value.label === item.label}
|
||||
btnClasses="justify-start text-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<div class="relative flex {wrapperClasses}">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
@@ -134,7 +160,12 @@
|
||||
Reset
|
||||
</Button>
|
||||
{/if}
|
||||
<Popover enableFlyTransition bind:isOpen>
|
||||
<Popover
|
||||
enableFlyTransition
|
||||
placement="bottom-end"
|
||||
contentClasses={isSmall ? 'overflow-y-auto' : ''}
|
||||
bind:isOpen
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
@@ -152,41 +183,63 @@
|
||||
value.type === 'manual' && value.minTs ? new Date(value.minTs) : undefined
|
||||
)
|
||||
}}
|
||||
<div class="flex divide-x">
|
||||
<div class="flex flex-col p-2">
|
||||
{#each items as item}
|
||||
<Button
|
||||
onClick={() => (value = { ...item })}
|
||||
variant="subtle"
|
||||
unifiedSize="md"
|
||||
selected={value.label === item.label}
|
||||
btnClasses="justify-start text-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
{/each}
|
||||
{#if isSmall}
|
||||
<div class="flex flex-col divide-y max-w-[calc(100vw-2rem)]">
|
||||
<div class="flex flex-row flex-wrap gap-1 p-2">
|
||||
{@render presetButtons()}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-2">
|
||||
<ToggleButtonGroup bind:selected={smallBound} class="w-full">
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="start" label="Start" {item} />
|
||||
<ToggleButton value="end" label="End" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<InlineCalendarInput
|
||||
class="w-full"
|
||||
infiniteRange
|
||||
portalSelects
|
||||
mode="range"
|
||||
onClickBehavior={smallBound === 'end' ? 'set-end' : 'set-start'}
|
||||
bind:value={
|
||||
() => range,
|
||||
(v) =>
|
||||
onManualInput(
|
||||
smallBound === 'end'
|
||||
? { maxTs: fromCalendarDate(v.end)?.toISOString() ?? null }
|
||||
: { minTs: fromCalendarDate(v.start)?.toISOString() ?? null }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InlineCalendarInput
|
||||
class="p-4 max-w-[18rem]"
|
||||
infiniteRange
|
||||
mode="range"
|
||||
onClickBehavior="set-start"
|
||||
bind:value={
|
||||
() => range,
|
||||
(v) => onManualInput({ minTs: fromCalendarDate(v.start)?.toISOString() ?? null })
|
||||
}
|
||||
/>
|
||||
<InlineCalendarInput
|
||||
class="p-4 max-w-[18rem]"
|
||||
infiniteRange
|
||||
mode="range"
|
||||
onClickBehavior="set-end"
|
||||
bind:value={
|
||||
() => range,
|
||||
(v) => onManualInput({ maxTs: fromCalendarDate(v.end)?.toISOString() ?? null })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex divide-x">
|
||||
<div class="flex flex-col p-2">
|
||||
{@render presetButtons()}
|
||||
</div>
|
||||
<InlineCalendarInput
|
||||
class="p-4 max-w-[18rem]"
|
||||
infiniteRange
|
||||
mode="range"
|
||||
onClickBehavior="set-start"
|
||||
bind:value={
|
||||
() => range,
|
||||
(v) => onManualInput({ minTs: fromCalendarDate(v.start)?.toISOString() ?? null })
|
||||
}
|
||||
/>
|
||||
<InlineCalendarInput
|
||||
class="p-4 max-w-[18rem]"
|
||||
infiniteRange
|
||||
mode="range"
|
||||
onClickBehavior="set-end"
|
||||
bind:value={
|
||||
() => range,
|
||||
(v) => onManualInput({ maxTs: fromCalendarDate(v.end)?.toISOString() ?? null })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user