feat(app): 활성 창 캡쳐·캡쳐 목적지 선택·단축키 Ctrl+Alt 재배치 + UX 손질
- Ctrl+Alt+A/Z 맨 앞 창 통째 캡쳐(DWM 확장 프레임 경계), S/X 드래그 캡쳐. A/S 새 대화, Z/X 마지막 대화 - 캡쳐 후 창을 확실히 앞으로(AttachThreadInput), capture.image 에 목적지 path 실어 그 Composer 만 소비 - 드래그 선택영역 원본 밝기·더블버퍼(번쩍임) — code-assistant-v2 복사본에서 이식 - 단축키 Ctrl+Alt+Q/W: Eclipse·ADT·SAP GUI 바인딩 목록 대조해 비어 있는 조합으로 - 버튼 cursor:pointer(Tailwind v4), 가운데 스피너, 스니펫→챗 버튼, 스니펫 검색창 높이 84→88 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
2869985139
commit
47d31dbe06
@@ -1,3 +1,4 @@
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
@@ -67,7 +68,11 @@ afterEach(() => {
|
||||
|
||||
describe("새 스니펫 단축키", () => {
|
||||
it("Ctrl+N으로 생성창을 열고 기존 Ctrl+2는 무시함", () => {
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
fireEvent.keyDown(window, { key: "2", ctrlKey: true })
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
|
||||
@@ -79,7 +84,11 @@ describe("새 스니펫 단축키", () => {
|
||||
it("생성 성공 뒤 입력창을 1초 유지한 다음 기본 검색 화면으로 돌아감", () => {
|
||||
vi.useFakeTimers()
|
||||
createSnippet.mockImplementation((_input, options) => options.onSuccess())
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
const search = screen.getByRole("textbox")
|
||||
fireEvent.change(search, { target: { value: "기존 검색" } })
|
||||
fireEvent.keyDown(window, { key: "n", ctrlKey: true })
|
||||
@@ -104,7 +113,11 @@ describe("스니펫 Enter 복사", () => {
|
||||
})
|
||||
)
|
||||
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(writeText)
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "B" } })
|
||||
fireEvent.click(screen.getByText("B"))
|
||||
fireEvent.keyDown(window, { key: "Enter" })
|
||||
@@ -122,7 +135,11 @@ describe("스니펫 Enter 복사", () => {
|
||||
it("복사 실패 시 창과 사용 기록을 유지하고 오류 표시", async () => {
|
||||
const writeText = vi.fn().mockRejectedValue(new Error("denied"))
|
||||
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(writeText)
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "A" } })
|
||||
fireEvent.keyDown(window, { key: "ArrowDown" })
|
||||
fireEvent.keyDown(window, { key: "Enter", isComposing: true })
|
||||
@@ -136,7 +153,11 @@ describe("스니펫 Enter 복사", () => {
|
||||
it("하단 복사는 클릭과 Enter 모두 원문을 복사하고 팔레트를 열린 채로 둠", async () => {
|
||||
const user = userEvent.setup()
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue()
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "B" } })
|
||||
await user.click(screen.getByRole("button", { name: "B" }))
|
||||
const copy = screen.getByRole("button", { name: "복사" })
|
||||
@@ -154,7 +175,11 @@ describe("Ctrl Enter 앱에 붙여넣기", () => {
|
||||
it("선택한 원문을 붙여넣고 일반 복사를 실행하지 않으며 키 반복을 무시함", async () => {
|
||||
const user = userEvent.setup()
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue()
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
await user.type(screen.getByRole("textbox"), "B")
|
||||
await user.click(screen.getByRole("button", { name: "B" }))
|
||||
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter", ctrlKey: true })
|
||||
@@ -169,7 +194,11 @@ describe("Ctrl Enter 앱에 붙여넣기", () => {
|
||||
it("복사 버튼에 포커스가 있어도 Ctrl Enter는 붙여넣고 일반 Enter는 복사함", async () => {
|
||||
const user = userEvent.setup()
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue()
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
await user.type(screen.getByRole("textbox"), "B")
|
||||
await user.click(screen.getByRole("button", { name: "B" }))
|
||||
screen.getByRole("button", { name: "복사" }).focus()
|
||||
@@ -188,7 +217,11 @@ describe("Ctrl Enter 앱에 붙여넣기", () => {
|
||||
|
||||
it("미선택·조합 중·다른 modifier·편집 중에는 붙여넣지 않음", () => {
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue()
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
const input = screen.getByRole("textbox")
|
||||
fireEvent.change(input, { target: { value: "B" } })
|
||||
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true })
|
||||
@@ -208,7 +241,11 @@ describe("Ctrl Enter 앱에 붙여넣기", () => {
|
||||
it("브라우저의 Ctrl Enter는 복사나 붙여넣기로 바뀌지 않음", () => {
|
||||
vi.mocked(isWebView).mockReturnValue(false)
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText")
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
const input = screen.getByRole("textbox")
|
||||
fireEvent.change(input, { target: { value: "B" } })
|
||||
fireEvent.keyDown(input, { key: "ArrowDown" })
|
||||
@@ -220,7 +257,11 @@ describe("Ctrl Enter 앱에 붙여넣기", () => {
|
||||
|
||||
describe("검색 후 선택할 때만 미리보기", () => {
|
||||
it("빈 검색 → 결과 → 직접 선택 → 검색 변경과 지우기 순서로 화면이 접힘", () => {
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
const input = screen.getByRole("textbox")
|
||||
expect(screen.queryByText("A")).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText("코드 미리보기")).not.toBeInTheDocument()
|
||||
@@ -244,7 +285,11 @@ describe("검색 후 선택할 때만 미리보기", () => {
|
||||
|
||||
it("검색 결과만 보일 때 Enter는 복사하지 않고 재소환은 검색창으로 돌아감", () => {
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText")
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "A" } })
|
||||
fireEvent.keyDown(window, { key: "Enter" })
|
||||
expect(writeText).not.toHaveBeenCalled()
|
||||
@@ -259,7 +304,11 @@ describe("검색 후 선택할 때만 미리보기", () => {
|
||||
|
||||
it("마우스로 결과를 선택한 뒤 Esc를 누르면 검색창에서 바로 다시 입력할 수 있음", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetPalettePage />)
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SnippetPalettePage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
const input = screen.getByRole("textbox")
|
||||
await waitFor(() => expect(input).toHaveFocus())
|
||||
await user.type(input, "A")
|
||||
|
||||
@@ -2,7 +2,10 @@ import { hostKind, send } from "@/lib/bridge/transport"
|
||||
import { LogoMark } from "@/shared/components/LogoMark"
|
||||
// 검색 → 결과 → 직접 선택한 코드 순서로 화면과 데스크톱 창을 펼침.
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Check, Search } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Check, MessageSquare, Search } from "lucide-react"
|
||||
import { PATHS } from "@/config/routes"
|
||||
import { Kbd } from "@/shared/components/Kbd"
|
||||
import { hideWindow, isWebView, pasteToApp, startWindowDrag } from "@/lib/bridge/webviewBridge"
|
||||
import { toast } from "sonner"
|
||||
import { useEscapeKey } from "@/features/snap/hooks/useEscapeKey"
|
||||
@@ -25,6 +28,7 @@ import type { Snippet, SnippetInput } from "../types"
|
||||
const ALL_CATEGORY = "전체"
|
||||
|
||||
export default function SnippetPalettePage() {
|
||||
const navigate = useNavigate()
|
||||
const [q, setQ] = useState("")
|
||||
const { data: snippets = [], isLoading, error } = useSnippets()
|
||||
const recordUse = useRecordUse()
|
||||
@@ -307,6 +311,14 @@ export default function SnippetPalettePage() {
|
||||
placeholder="스니펫 검색…"
|
||||
className="placeholder:text-muted-foreground w-full bg-transparent text-base outline-none"
|
||||
/>
|
||||
{/* 챗으로 전환 — 같은 창에서 route 만 바뀜(Ctrl+Alt+W 와 동일) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(PATHS.SNAP)}
|
||||
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring inline-flex h-7 flex-none items-center gap-1 rounded-md px-2 text-[11.5px] whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<MessageSquare className="size-3.5" aria-hidden="true" />챗<Kbd>Ctrl+Alt+W</Kbd>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* category 칩 바 — 2개 이상(전체 포함)일 때만 보여줌 */}
|
||||
|
||||
Reference in New Issue
Block a user