From 835a7bcd6899556531fcf88047094e8cff607fe7 Mon Sep 17 00:00:00 2001 From: Towsty Date: Mon, 31 Aug 2026 21:42:29 -0500 Subject: [PATCH] Make keep-prompt snippets clickable and allow renaming the start still. Co-authored-by: Cursor --- components/KeepPromptMenu.vue | 110 ++++++++++++++++++++++++---------- components/PromptDrawer.vue | 12 ++++ pages/index.vue | 100 +++++++++++++++++++++++++++++-- 3 files changed, 184 insertions(+), 38 deletions(-) diff --git a/components/KeepPromptMenu.vue b/components/KeepPromptMenu.vue index d5de410..3318c93 100644 --- a/components/KeepPromptMenu.vue +++ b/components/KeepPromptMenu.vue @@ -2,40 +2,47 @@
- + + +
@@ -50,7 +57,7 @@ const props = withDefaults(defineProps<{ }>(), { heading: 'Saved prompts', disabled: false, - align: 'right' + align: 'left' }) const emit = defineEmits<{ @@ -58,7 +65,27 @@ const emit = defineEmits<{ }>() const root = ref(null) +const menu = ref(null) const open = ref(false) +const menuStyle = ref>({}) + +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) @@ -68,6 +95,7 @@ function pick(item: KeepPrompt) { 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 } @@ -79,13 +107,29 @@ watch(() => props.disabled, (value) => { if (value) open.value = false }) -onMounted(() => { - window.addEventListener('click', onDocClick) - window.addEventListener('keydown', onKey) +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) }) diff --git a/components/PromptDrawer.vue b/components/PromptDrawer.vue index 75b3979..af79b8d 100644 --- a/components/PromptDrawer.vue +++ b/components/PromptDrawer.vue @@ -34,6 +34,18 @@ v-else class="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-zinc-900 px-4 py-3 text-base leading-relaxed text-zinc-200 [overflow-wrap:anywhere]" >{{ text }} +
+ +
diff --git a/pages/index.vue b/pages/index.vue index c332495..34ad3d0 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -88,6 +88,30 @@ +
+ +

{{ inputStillLabel }}

+ +
+
+
+ +

No reference image. Prompt only.

Prompt only what should change in the painted area.

@@ -2361,6 +2395,9 @@ const editLightbox = ref(null) const editLightboxUrl = ref('') const file = ref(null) const inputStillId = ref('') +const renamingInputStill = ref(false) +const inputStillNameDraft = ref('') +const inputStillRenameEl = ref(null) const preview = ref('') const dragging = ref(false) const prompt = ref('') @@ -4591,6 +4628,7 @@ async function copyPrompt(text: string) { } function resetImage() { + cancelInputStillRename() if (preview.value) URL.revokeObjectURL(preview.value) file.value = null inputStillId.value = '' @@ -4623,7 +4661,59 @@ function stillIdFromLibraryUrl(url: string) { return match ? decodeURIComponent(match[1]) : '' } +const inputStill = computed(() => stills.value.find(item => item.id === inputStillId.value) || null) + +const inputStillLabel = computed(() => { + if (inputStill.value) return stillTitle(inputStill.value) + const name = file.value?.name || '' + return name.replace(/\.[a-z0-9]+$/i, '') || 'Untitled still' +}) + +function startInputStillRename() { + renamingInputStill.value = true + inputStillNameDraft.value = inputStillLabel.value + nextTick(() => { + inputStillRenameEl.value?.focus() + inputStillRenameEl.value?.select() + }) +} + +function cancelInputStillRename() { + renamingInputStill.value = false +} + +async function saveInputStillName() { + if (!renamingInputStill.value) return + const name = inputStillNameDraft.value.trim().slice(0, 80) + if (!name) { + toast('A name is required') + return + } + if (name === inputStillLabel.value) { + cancelInputStillRename() + return + } + if (file.value) { + const ext = (file.value.name.match(/\.[a-z0-9]+$/i) || ['.png'])[0] + file.value = new File([file.value], `${clipFileName(name)}${ext}`, { type: file.value.type || 'image/png' }) + } + const id = inputStillId.value + if (id) { + try { + applyLibrary(await $fetch(`/api/library/stills/${id}`, { + method: 'PATCH', + body: { name } + })) + } catch (error: any) { + toast(error?.data?.statusMessage || 'Could not rename still') + return + } + } + cancelInputStillRename() +} + function readFile(next: File, opts?: { keepThumbnailPref?: boolean; stillId?: string }) { + cancelInputStillRename() if (preview.value) URL.revokeObjectURL(preview.value) file.value = next inputStillId.value = opts?.stillId || ''