From 7148ad0f1757bcac60b55116bb405ac59d7bfedc Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 25 Apr 2026 12:12:53 +0900 Subject: [PATCH] feat(quickcapture): React UI with v0.2 recovery-friendly copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 19 of the slice plan. Frameless dark card with: - placeholder "지금 머릿속에 있는 것 한 줄. 정리는 나중입니다." - hint "Ctrl+Enter 구출 · Esc 취소 · 이미지 붙여넣기" - Ctrl/Cmd+Enter to submit (window.inkling.capture.submit then hide), Esc to cancel (with "이 한 줄을 흘려보낼까요?" confirm when text > 5 chars) - clipboard image paste -> thumbnail strip with ArrayBuffer retained for submit - fallback "저장에 실패했습니다. 다시 시도해주세요." in-card on IPC error (window stays open with content preserved) api.ts wraps window.inkling.capture as the typed CaptureApi. main.tsx mounts on #root. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer/quickcapture/App.tsx | 72 ++++++++++++++++++++++++++++ src/renderer/quickcapture/api.ts | 2 + src/renderer/quickcapture/index.html | 28 +++++++++++ src/renderer/quickcapture/main.tsx | 4 ++ 4 files changed, 106 insertions(+) create mode 100644 src/renderer/quickcapture/App.tsx create mode 100644 src/renderer/quickcapture/api.ts create mode 100644 src/renderer/quickcapture/index.html create mode 100644 src/renderer/quickcapture/main.tsx diff --git a/src/renderer/quickcapture/App.tsx b/src/renderer/quickcapture/App.tsx new file mode 100644 index 0000000..8d28bd5 --- /dev/null +++ b/src/renderer/quickcapture/App.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { captureApi } from './api.js'; + +interface PastedImage { url: string; buffer: ArrayBuffer; } + +export function App(): React.ReactElement { + const [text, setText] = useState(''); + const [images, setImages] = useState([]); + const [err, setErr] = useState(null); + const ref = useRef(null); + + useEffect(() => { ref.current?.focus(); }, []); + + const submit = useCallback(async () => { + setErr(null); + if (text.trim().length === 0 && images.length === 0) return; + try { + await captureApi.submit({ text, images: images.map((i) => i.buffer) }); + setText(''); setImages([]); captureApi.hide(); + } catch (e) { setErr('저장에 실패했습니다. 다시 시도해주세요.'); } + }, [text, images]); + + const cancel = useCallback(() => { + if (text.trim().length > 5) { + const ok = window.confirm('이 한 줄을 흘려보낼까요?'); + if (!ok) return; + } + setText(''); setImages([]); captureApi.hide(); + }, [text]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { e.preventDefault(); cancel(); } + else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); void submit(); } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [cancel, submit]); + + const onPaste = useCallback(async (e: React.ClipboardEvent) => { + const items = Array.from(e.clipboardData.items); + const imgs = items.filter((i) => i.type.startsWith('image/')); + if (imgs.length === 0) return; + e.preventDefault(); + for (const it of imgs) { + const blob = it.getAsFile(); + if (!blob) continue; + const buffer = await blob.arrayBuffer(); + const url = URL.createObjectURL(blob); + setImages((prev) => [...prev, { url, buffer }]); + } + }, []); + + return ( +
+