diff --git a/AGENTS.md b/AGENTS.md index 52328f8..043b6d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,10 @@ clicking an item copies it back to the clipboard. as a frame — a representable cannot resize itself through `intrinsicContentSize` once SwiftUI offers a larger size. - `Sources/Prompt/PanelView.swift` — the panel UI. Header (workspace switcher, - search, add photos), item list, composer. There is no in-header close button: - the window's own close button hides the panel. Composer `# Name` creates/ + search, add photos), a segmented Workspace/All tab (All lists every prompt + labeled with its workspace), item list, in-panel edit modal, composer. + There is no in-header close button: the window's own close button hides the + panel. Composer `# Name` creates/ switches sections. Click = select (Shift+click for a range); the selection bar offers Copy (single item), Copy as List, Copy as Lines and Delete; right-click = context menu acting on the whole selection (Copy, Copy as List, @@ -55,8 +57,9 @@ clicking an item copies it back to the clipboard. selection before it hides the panel. Drop target for images/files. - `Sources/Prompt/PanelController.swift` — owns the floating `NSPanel` (`.nonactivatingPanel`, `.floating`, right-edge docked, frame autosaved as - `PromptPanel`), the Cmd+V image-paste monitor, and `EditWindowController` - (separate edit window with a `TextEditor`). + `PromptPanel`), and the Cmd+V paste monitor. Editing a note is an in-panel + modal in `PanelView`, not a window — a separate edit window used to float + loose from the panel and its theme. - `Sources/Prompt/HotkeyManager.swift` — double-Shift detection via a CGEvent tap on `flagsChanged` (left Shift ×2 = capture, right Shift ×2 = toggle panel), plus Carbon `RegisterEventHotKey` fallbacks (Cmd+Shift+Space diff --git a/README.md b/README.md index 5e8727f..a3d989f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ to your work. Select what you need and send it back to your clipboard. the Cmd+K switcher (which opens a name modal); Cmd+K switches, and the up/down arrows in the switcher reorder them. (The code and the saved JSON still call these `sections` so existing data keeps working.) +- **An "All" tab** — a global view of every prompt across every workspace, + each labeled with the workspace it lives in. +- **Edit in place** — right-click → Edit opens an in-panel modal on the same + theme, not a separate window. - **Right-click for actions** — Copy, Copy as List, Copy as Lines, Mark as Done, Edit, Open, Merge Notes, Move To, Delete — each acting on the whole selection. - **Search** — Cmd+F searches across all workspaces. diff --git a/Sources/Prompt/PanelController.swift b/Sources/Prompt/PanelController.swift index 73c09a4..615a5d6 100644 --- a/Sources/Prompt/PanelController.swift +++ b/Sources/Prompt/PanelController.swift @@ -16,7 +16,6 @@ final class PanelController: NSObject { private var panel: KeyablePanel? private var pasteMonitor: Any? - let editController = EditWindowController() var isVisible: Bool { panel?.isVisible ?? false } @@ -102,56 +101,4 @@ final class PanelController: NSObject { } } -@MainActor -final class EditWindowController: NSObject { - private var window: NSWindow? - func edit(itemID: UUID, text: String) { - let view = EditView( - text: text, - onSave: { [weak self] newText in - Store.shared.updateText(itemID, newText: newText) - self?.window?.close() - }, - onCancel: { [weak self] in self?.window?.close() } - ) - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 320), - styleMask: [.titled, .closable, .resizable], - backing: .buffered, - defer: false - ) - window.title = "Edit Note" - window.contentView = NSHostingView(rootView: view) - window.isReleasedWhenClosed = false - window.center() - window.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) - self.window = window - } -} - -private struct EditView: View { - @State var text: String - var onSave: (String) -> Void - var onCancel: () -> Void - - var body: some View { - VStack(spacing: 12) { - TextEditor(text: $text) - .font(.body) - .padding(4) - .background(Color.primary.opacity(0.05)) - .cornerRadius(6) - HStack { - Spacer() - Button("Cancel", action: onCancel) - .keyboardShortcut(.cancelAction) - Button("Save") { onSave(text) } - .keyboardShortcut(.defaultAction) - .keyboardShortcut("s", modifiers: .command) - } - } - .padding(16) - } -} diff --git a/Sources/Prompt/PanelView.swift b/Sources/Prompt/PanelView.swift index fa8fb42..022987a 100644 --- a/Sources/Prompt/PanelView.swift +++ b/Sources/Prompt/PanelView.swift @@ -18,10 +18,15 @@ struct PanelView: View { @State private var joinCode = "" @State private var selection = Set() @State private var lastSelectedID: UUID? + @State private var editingItem: PromptItem? + @State private var editText = "" @State private var copiedID: UUID? @State private var accessibilityGranted = AXIsProcessTrusted() - private enum Field: Hashable { case search, newWorkspace } + private enum Tab { case workspace, all } + @State private var tab: Tab = .workspace + + private enum Field: Hashable { case search, newWorkspace, editor } @FocusState private var focusedField: Field? var body: some View { @@ -30,6 +35,7 @@ struct PanelView: View { if !accessibilityGranted { permissionBanner } + tabPicker if showSearch { searchField } @@ -47,6 +53,7 @@ struct PanelView: View { .overlay(sectionSwitcher) .overlay(syncOverlay) .overlay(newWorkspaceModal) + .overlay(editModal) .onDrop(of: [UTType.image.identifier, UTType.fileURL.identifier], isTargeted: nil, perform: handleDrop) .onReceive(NotificationCenter.default.publisher(for: .promptPanelDidShow)) { _ in composerFocused = true @@ -90,6 +97,20 @@ struct PanelView: View { .padding(.vertical, 10) } + // MARK: - Tabs + + /// The global view: every prompt across every workspace, each labeled. + private var tabPicker: some View { + Picker("View", selection: $tab) { + Text(store.activeSection.name).tag(Tab.workspace) + Text("All").tag(Tab.all) + } + .pickerStyle(.segmented) + .labelsHidden() + .padding(.horizontal, 12) + .padding(.vertical, 6) + } + private var permissionBanner: some View { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") @@ -135,21 +156,27 @@ struct PanelView: View { // MARK: - Content - private var displayedItems: [(section: String, item: PromptItem)] { - if search.isEmpty { - return store.activeSection.items.map { ("", $0) } + /// Every item across every workspace, each labeled with its workspace. + private var allLabeledItems: [(section: String, item: PromptItem)] { + store.sections.flatMap { section in + section.items.map { (section.name, $0) } } - let query = search.lowercased() - return store.sections.flatMap { section in - section.items.compactMap { item in - switch item.content { - case .text(let text) where text.lowercased().contains(query): - return (section.name, item) - default: - return nil + } + + private var displayedItems: [(section: String, item: PromptItem)] { + if !search.isEmpty { + let query = search.lowercased() + return allLabeledItems.filter { entry in + if case .text(let text) = entry.item.content { + return text.lowercased().contains(query) } + return false } } + if tab == .all { + return allLabeledItems + } + return store.activeSection.items.map { ("", $0) } } private var content: some View { @@ -296,7 +323,9 @@ struct PanelView: View { } if case .text(let text) = item.content, !multiple { Button("Edit…") { - PanelController.shared.editController.edit(itemID: item.id, text: text) + editText = text + editingItem = item + focusedField = .editor } } if case .image(let filename) = item.content, !multiple { @@ -376,6 +405,7 @@ struct PanelView: View { .contentShape(Rectangle()) .onTapGesture { store.activeSectionID = section.id + tab = .workspace showSectionSwitcher = false } } @@ -512,6 +542,62 @@ struct PanelView: View { } } + // MARK: - Edit + + @ViewBuilder + private var editModal: some View { + if let item = editingItem { + ZStack { + Color.black.opacity(0.25) + .onTapGesture { dismissEditor() } + VStack(alignment: .leading, spacing: 12) { + Text("Edit") + .font(.headline) + TextEditor(text: $editText) + .font(.callout) + .scrollContentBackground(.hidden) + .frame(minHeight: 120, maxHeight: 280) + .padding(8) + .background(Theme.rowSwiftUI) + .cornerRadius(8) + .focused($focusedField, equals: .editor) + HStack { + Spacer() + Button("Cancel") { dismissEditor() } + .keyboardShortcut(.cancelAction) + Button("Save") { saveEdit(item) } + .keyboardShortcut("s", modifiers: .command) + .disabled(editText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + .padding(16) + .frame(maxWidth: 320) + .background(Theme.backgroundSwiftUI) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.primary.opacity(0.12), lineWidth: 1) + ) + .shadow(radius: 18) + } + .transition(.opacity) + } + } + + private func saveEdit(_ item: PromptItem) { + let text = editText.trimmingCharacters(in: .whitespacesAndNewlines) + if !text.isEmpty { + store.updateText(item.id, newText: text) + } + dismissEditor() + } + + private func dismissEditor() { + editingItem = nil + editText = "" + composerFocused = true + } + // MARK: - New workspace @ViewBuilder