Files
aigen/components/KeepPromptMenu.vue

136 lines
4.1 KiB
Vue

<template>
<div ref="root" class="relative shrink-0">
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="open || items.length
? 'border-amber-300/40 text-amber-100 hover:bg-amber-400/10'
: 'border-white/15 text-zinc-400 hover:border-amber-300/40 hover:text-amber-100'"
:disabled="disabled"
:title="items.length ? 'Add a saved snippet to the prompt' : 'Add snippets in Settings → Keep prompts'"
:aria-expanded="open"
aria-haspopup="menu"
aria-label="Add saved prompt snippet"
@click.stop="toggle"
>
Snippets
</button>
<Teleport to="body">
<div
v-if="open"
ref="menu"
class="fixed z-[80] w-72 max-w-[calc(100vw-1rem)] overflow-hidden rounded-2xl border border-white/10 bg-zinc-950 shadow-2xl"
:style="menuStyle"
role="menu"
@click.stop
>
<p class="px-3 pt-2 text-[10px] uppercase tracking-wide text-zinc-500">{{ heading }}</p>
<p v-if="!items.length" class="px-3 py-3 text-xs text-zinc-500">
No snippets in this list yet. Add them in Settings → Keep prompts.
</p>
<ul v-else class="max-h-64 overflow-y-auto p-1">
<li v-for="item in items" :key="item.id">
<button
type="button"
class="flex w-full flex-col items-start rounded-xl px-3 py-2 text-left hover:bg-white/5"
role="menuitem"
@click="pick(item)"
>
<span class="text-xs font-medium text-amber-100">{{ item.label }}</span>
<span class="mt-0.5 line-clamp-2 whitespace-pre-wrap text-[11px] text-zinc-500">{{ item.text }}</span>
</button>
</li>
</ul>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import type { KeepPrompt } from '~/utils/keepPrompts'
const props = withDefaults(defineProps<{
items: KeepPrompt[]
heading?: string
disabled?: boolean
align?: 'left' | 'right'
}>(), {
heading: 'Saved prompts',
disabled: false,
align: 'left'
})
const emit = defineEmits<{
insert: [item: KeepPrompt]
}>()
const root = ref<HTMLElement | null>(null)
const menu = ref<HTMLElement | null>(null)
const open = ref(false)
const menuStyle = ref<Record<string, string>>({})
function place() {
const el = root.value
if (!el) return
const rect = el.getBoundingClientRect()
const width = Math.min(288, window.innerWidth - 16)
let left = props.align === 'right' ? rect.right - width : rect.left
left = Math.min(Math.max(8, left), window.innerWidth - width - 8)
menuStyle.value = {
top: `${Math.round(rect.bottom + 4)}px`,
left: `${Math.round(left)}px`,
width: `${width}px`
}
}
function toggle() {
open.value = !open.value
}
function pick(item: KeepPrompt) {
emit('insert', item)
open.value = false
}
function onDocClick(event: MouseEvent) {
const target = event.target as Node | null
if (root.value && target && root.value.contains(target)) return
if (menu.value && target && menu.value.contains(target)) return
open.value = false
}
function onKey(event: KeyboardEvent) {
if (event.key === 'Escape') open.value = false
}
watch(() => props.disabled, (value) => {
if (value) open.value = false
})
watch(open, (value) => {
if (typeof window === 'undefined') return
if (value) {
nextTick(() => {
place()
window.addEventListener('click', onDocClick)
window.addEventListener('keydown', onKey)
window.addEventListener('resize', place)
window.addEventListener('scroll', place, true)
})
return
}
window.removeEventListener('click', onDocClick)
window.removeEventListener('keydown', onKey)
window.removeEventListener('resize', place)
window.removeEventListener('scroll', place, true)
})
onBeforeUnmount(() => {
if (typeof window === 'undefined') return
window.removeEventListener('click', onDocClick)
window.removeEventListener('keydown', onKey)
window.removeEventListener('resize', place)
window.removeEventListener('scroll', place, true)
})
</script>