Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<SubgraphBreadcrumb />
</div>

<div class="mx-1 flex flex-col items-end gap-1">
<div ref="queueAreaRef" class="mx-1 flex flex-col items-end gap-1">
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
>
Expand Down Expand Up @@ -40,12 +40,16 @@
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
</div>
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:external-hovered="isQueueAreaHovered"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { useElementBounding, useMouse } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

Expand All @@ -68,6 +72,20 @@ const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
const isQueueOverlayExpanded = ref(false)

// Track hover over the rectangular bounding box of the queue area
// Using mouse position + element bounds instead of mouseenter/mouseleave
// because QueueProgressOverlay has pointer-events-none on its wrapper
const queueAreaRef = ref<HTMLElement | null>(null)
const { x: mouseX, y: mouseY } = useMouse()
const { left, top, right, bottom } = useElementBounding(queueAreaRef)
const isQueueAreaHovered = computed(
() =>
mouseX.value >= left.value &&
mouseX.value <= right.value &&
mouseY.value >= top.value &&
mouseY.value <= bottom.value
)
const queueStore = useQueueStore()
const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() =>
Expand Down
2 changes: 1 addition & 1 deletion src/components/queue/QueueOverlayActive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
v-tooltip.top="cancelJobTooltip"
type="secondary"
size="sm"
class="size-6 bg-secondary-background hover:bg-destructive-background"
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
@click="$emit('interruptAll')"
>
Expand Down
11 changes: 8 additions & 3 deletions src/components/queue/QueueProgressOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<div
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
:class="containerClass"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
@mouseenter="isHoveredInternal = true"
@mouseleave="isHoveredInternal = false"
>
<!-- Expanded state -->
<QueueOverlayExpanded
Expand Down Expand Up @@ -87,6 +87,8 @@ type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'

const props = defineProps<{
expanded?: boolean
/** External hover state from parent container */
externalHovered?: boolean
}>()

const emit = defineEmits<{
Expand All @@ -109,7 +111,10 @@ const {
totalProgressStyle,
currentNodeProgressStyle
} = useQueueProgress()
const isHovered = ref(false)
const isHoveredInternal = ref(false)
const isHovered = computed(
() => isHoveredInternal.value || props.externalHovered
)
const internalExpanded = ref(false)
const isExpanded = computed({
get: () =>
Expand Down
59 changes: 53 additions & 6 deletions src/components/queue/job/QueueJobItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,16 @@
:src="iconImageUrl"
class="h-full w-full object-cover"
/>
<i v-else :class="[iconClass, 'size-4']" />
<i
v-else
:class="
cn(
iconClass,
'size-4',
props.state === 'pending' && 'animate-spin'
)
"
/>
</div>
</div>
</div>
Expand All @@ -93,6 +102,23 @@
</div>
</div>

<!--
TODO: Refactor action buttons to use a declarative config system.

Instead of hardcoding button visibility logic in the template, define an array of
action button configs with properties like:
- icon, label, action, tooltip
- visibleStates: JobState[] (which job states show this button)
- alwaysVisible: boolean (show without hover)
- destructive: boolean (use destructive styling)

Then render buttons in two groups:
1. Always-visible buttons (outside Transition)
2. Hover-only buttons (inside Transition)

This would eliminate the current duplication where the cancel button exists
both outside (for running) and inside (for pending) the Transition.
-->
<div class="relative z-[1] flex items-center gap-2 text-text-secondary">
<Transition
mode="out-in"
Expand All @@ -113,18 +139,22 @@
v-tooltip.top="deleteTooltipConfig"
type="transparent"
size="sm"
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.delete')"
@click.stop="emit('delete')"
>
<i class="icon-[lucide--trash-2] size-4" />
</IconButton>
<IconButton
v-else-if="props.state !== 'completed' && computedShowClear"
v-else-if="
props.state !== 'completed' &&
props.state !== 'running' &&
computedShowClear
"
v-tooltip.top="cancelTooltipConfig"
type="transparent"
size="sm"
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.cancel')"
@click.stop="emit('cancel')"
>
Expand All @@ -143,17 +173,33 @@
v-tooltip.top="moreTooltipConfig"
type="transparent"
size="sm"
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
class="size-6 transform gap-1 rounded bg-modal-card-button-surface text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
:aria-label="t('g.more')"
@click.stop="emit('menu', $event)"
>
<i class="icon-[lucide--more-horizontal] size-4" />
</IconButton>
</div>
<div v-else key="secondary" class="pr-2">
<div
v-else-if="props.state !== 'running'"
key="secondary"
class="pr-2"
>
<slot name="secondary">{{ props.rightText }}</slot>
</div>
</Transition>
<!-- Running job cancel button - always visible -->
<IconButton
v-if="props.state === 'running' && computedShowClear"
v-tooltip.top="cancelTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.cancel')"
@click.stop="emit('cancel')"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</div>
</div>
</div>
Expand All @@ -170,6 +216,7 @@ import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'

const props = withDefaults(
defineProps<{
Expand Down
2 changes: 1 addition & 1 deletion src/utils/queueDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type JobDisplay = {
export const iconForJobState = (state: JobState): string => {
switch (state) {
case 'pending':
return 'icon-[lucide--clock]'
return 'icon-[lucide--loader-circle]'
case 'initialization':
return 'icon-[lucide--server-crash]'
case 'running':
Expand Down
Loading