feat(quickcapture): React UI with v0.2 recovery-friendly copy
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 <App /> on #root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
72
src/renderer/quickcapture/App.tsx
Normal file
72
src/renderer/quickcapture/App.tsx
Normal file
@@ -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<PastedImage[]>([]);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const ref = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<div className="card">
|
||||
<textarea
|
||||
ref={ref}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onPaste={onPaste}
|
||||
placeholder="지금 머릿속에 있는 것 한 줄. 정리는 나중입니다."
|
||||
/>
|
||||
{images.length > 0 && (
|
||||
<div className="thumbs">
|
||||
{images.map((i, idx) => (<img key={idx} src={i.url} alt="" />))}
|
||||
</div>
|
||||
)}
|
||||
<div className="hint">Ctrl+Enter 구출 · Esc 취소 · 이미지 붙여넣기</div>
|
||||
{err && <div className="err">{err}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
src/renderer/quickcapture/api.ts
Normal file
2
src/renderer/quickcapture/api.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import type { CaptureApi } from '@shared/types';
|
||||
export const captureApi: CaptureApi = window.inkling.capture;
|
||||
28
src/renderer/quickcapture/index.html
Normal file
28
src/renderer/quickcapture/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" />
|
||||
<title>Inkling Capture</title>
|
||||
<style>
|
||||
html, body, #root { margin: 0; height: 100%; background: transparent; font-family: system-ui, sans-serif; }
|
||||
body { -webkit-app-region: drag; }
|
||||
.card {
|
||||
-webkit-app-region: no-drag;
|
||||
background: #1e1e24; color: #eee;
|
||||
border-radius: 12px; box-shadow: 0 12px 48px rgba(0,0,0,0.4);
|
||||
height: calc(100% - 24px); margin: 12px;
|
||||
display: flex; flex-direction: column; padding: 12px;
|
||||
}
|
||||
textarea { flex: 1; background: transparent; color: inherit; border: none; outline: none; font-size: 14px; resize: none; }
|
||||
.thumbs { display: flex; gap: 6px; }
|
||||
.thumbs img { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; }
|
||||
.hint { color: #888; font-size: 11px; margin-top: 6px; }
|
||||
.err { color: #ff6a6a; font-size: 11px; margin-top: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/renderer/quickcapture/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4
src/renderer/quickcapture/main.tsx
Normal file
4
src/renderer/quickcapture/main.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App.js';
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
Reference in New Issue
Block a user