Files
aigen/components/PromptPreviewTip.vue
TowstyandCursor 03687b0bd6 Let library prompt previews escape the video tile.
The info popover was clipped to the card, so the prompt sat inside the thumbnail frame instead of over the page.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 21:51:34 -05:00

121 lines
3.2 KiB
Vue

<template>
<div
ref="root"
class="relative shrink-0"
@mouseenter="open"
@mouseleave="scheduleClose"
@focusin="open"
@focusout="onFocusOut"
@wheel.prevent="onIconWheel"
>
<button
type="button"
class="relative z-10 flex h-6 w-6 items-center justify-center rounded-full text-zinc-500 hover:text-amber-200"
:aria-label="ariaLabel"
title="Copy prompt"
@click.stop="emit('copy', text)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-4 w-4">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" stroke-linecap="round" />
</svg>
</button>
<Teleport to="body">
<div
v-if="show"
ref="panel"
class="fixed z-[80] max-h-[min(20rem,50vh)] w-[min(28rem,calc(100vw-1.5rem))] overflow-y-auto overscroll-contain whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-zinc-950 p-3 text-xs leading-relaxed text-zinc-300 shadow-2xl"
:style="panelStyle"
@mouseenter="open"
@mouseleave="scheduleClose"
@click.stop
>
{{ text }}
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
defineProps<{
text: string
ariaLabel: string
}>()
const emit = defineEmits<{
copy: [text: string]
}>()
const root = ref<HTMLElement | null>(null)
const panel = ref<HTMLElement | null>(null)
const show = ref(false)
const panelStyle = ref<Record<string, string>>({})
let closeTimer: ReturnType<typeof setTimeout> | null = null
function place() {
const el = root.value
if (!el) return
const rect = el.getBoundingClientRect()
const width = Math.min(448, window.innerWidth - 24)
const maxHeight = Math.min(320, window.innerHeight * 0.5)
let left = rect.right - width
left = Math.min(Math.max(12, left), window.innerWidth - width - 12)
const below = rect.bottom + 8
const above = rect.top - 8 - maxHeight
const top = below + maxHeight <= window.innerHeight - 12 ? below : Math.max(12, above)
panelStyle.value = {
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`,
width: `${Math.round(width)}px`
}
}
function open() {
if (closeTimer) {
clearTimeout(closeTimer)
closeTimer = null
}
show.value = true
nextTick(place)
}
function close() {
show.value = false
}
function scheduleClose() {
if (closeTimer) clearTimeout(closeTimer)
closeTimer = setTimeout(close, 120)
}
function onFocusOut(event: FocusEvent) {
const next = event.relatedTarget as Node | null
if (root.value?.contains(next) || panel.value?.contains(next)) return
scheduleClose()
}
function onIconWheel(event: WheelEvent) {
const el = panel.value
if (!el) return
el.scrollTop += event.deltaY
}
watch(show, (value) => {
if (typeof window === 'undefined') return
if (value) {
window.addEventListener('resize', place)
window.addEventListener('scroll', place, true)
return
}
window.removeEventListener('resize', place)
window.removeEventListener('scroll', place, true)
})
onBeforeUnmount(() => {
if (closeTimer) clearTimeout(closeTimer)
if (typeof window === 'undefined') return
window.removeEventListener('resize', place)
window.removeEventListener('scroll', place, true)
})
</script>