diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..eaf9c3b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +### Summary + +- **Bullet Point**: - What Changed + +#### Added + +- What was added + +#### Changed + +- What was changed + +#### Fixed + +- What was fixed + +#### Removed + +- What was removed diff --git a/.gitignore b/.gitignore index 87553e4..d550c67 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ out .eslintcache *.log* .env -electron.vite.config.*.mjs \ No newline at end of file +electron.vite.config.*.mjs +.cursor/rules/user diff --git a/README.md b/README.md index 5299985..dff7139 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ For now, please know that the security warnings are purely administrative and th 1. **Install and Run**: Start Clipless and it begins monitoring your clipboard automatically 2. **Copy Content**: Any content you copy will appear in the Clipless window 3. **Click to Reuse**: Click on any row number to copy that item back to your clipboard -4. **Lock Important Items**: Right-click clips to lock them and prevent removal +4. **Context Menu Actions**: Right-click clips to access Copy, Scan, Lock, and Delete options ### Quick Clips Workflow diff --git a/electron-builder.yml b/electron-builder.yml index 08e4967..c2ca834 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -27,7 +27,6 @@ mac: target: - target: dmg arch: - - x64 - arm64 extendInfo: NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. @@ -62,7 +61,7 @@ dmg: type: link path: /Applications background: null - format: UDRO + format: UDZO internetEnabled: false linux: target: diff --git a/package.json b/package.json index 60c0267..d5e7091 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clipless", - "version": "1.3.1", + "version": "1.4.0", "description": "An Electron application with React and TypeScript", "main": "./out/main/index.js", "author": "Daniel Essig", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index c5cd9d7..54a6839 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1,9 +1,15 @@ -import { ipcMain } from 'electron'; +import { ipcMain, Menu, MenuItemConstructorOptions } from 'electron'; import { autoUpdater } from 'electron-updater'; import { is } from '@electron-toolkit/utils'; import { storage } from '../storage'; import { hotkeyManager } from '../hotkeys'; -import { getMainWindow, getSettingsWindow, createSettingsWindow, createToolsLauncherWindow, getToolsLauncherWindow } from '../window/creation'; +import { + getMainWindow, + getSettingsWindow, + createSettingsWindow, + createToolsLauncherWindow, + getToolsLauncherWindow, +} from '../window/creation'; import { applyWindowSettings } from '../window/settings'; import { checkForUpdatesWithRetry } from '../updater'; @@ -96,6 +102,59 @@ export function setupMainIPC(): void { return null; }); + // Context Menu IPC handler + ipcMain.handle( + 'show-clip-context-menu', + async ( + event, + options: { + index: number; + isFirstClip: boolean; + isLocked: boolean; + hasPatterns: boolean; + } + ) => { + const { index, isFirstClip, isLocked, hasPatterns } = options; + + const template: MenuItemConstructorOptions[] = [ + { + label: 'Copy to Clipboard', + click: () => { + event.sender.send('context-menu-action', { action: 'copy', index }); + }, + }, + { type: 'separator' }, + { + label: hasPatterns ? 'Scan with Quick Clips ⚡' : 'Scan with Quick Clips', + click: () => { + event.sender.send('context-menu-action', { action: 'scan', index }); + }, + }, + { type: 'separator' }, + { + label: isLocked ? 'Unlock Clip' : 'Lock Clip', + enabled: !isFirstClip, + click: () => { + event.sender.send('context-menu-action', { action: 'lock', index }); + }, + }, + { + label: 'Delete Clip', + enabled: !isFirstClip, + click: () => { + event.sender.send('context-menu-action', { action: 'delete', index }); + }, + }, + ]; + + const contextMenu = Menu.buildFromTemplate(template); + const window = getMainWindow(); + if (window) { + contextMenu.popup({ window }); + } + } + ); + ipcMain.handle('download-update', async () => { if (!is.dev) { try { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 3e7c3c7..b28d245 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -74,6 +74,15 @@ declare global { toolsLauncherReady: () => Promise; onToolsLauncherInitialize: (callback: (clipContent: string) => void) => void; removeAllListeners?: (channel: string) => void; + // Native Context Menu APIs + showClipContextMenu: (options: { + index: number; + isFirstClip: boolean; + isLocked: boolean; + hasPatterns: boolean; + }) => Promise; + onContextMenuAction: (callback: (data: { action: string; index: number }) => void) => void; + removeContextMenuListeners: () => void; }; } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 8c995c2..de12d16 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -110,6 +110,18 @@ const api = { callback(clipContent) ), removeAllListeners: (channel: string) => electronAPI.ipcRenderer.removeAllListeners(channel), + + // Native Context Menu APIs + showClipContextMenu: (options: { + index: number; + isFirstClip: boolean; + isLocked: boolean; + hasPatterns: boolean; + }) => electronAPI.ipcRenderer.invoke('show-clip-context-menu', options), + onContextMenuAction: (callback: (data: { action: string; index: number }) => void) => + electronAPI.ipcRenderer.on('context-menu-action', (_event, data) => callback(data)), + removeContextMenuListeners: () => + electronAPI.ipcRenderer.removeAllListeners('context-menu-action'), }; // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/Settings.module.css b/src/renderer/src/Settings.module.css index 49257a1..d8582c9 100644 --- a/src/renderer/src/Settings.module.css +++ b/src/renderer/src/Settings.module.css @@ -2,24 +2,24 @@ .container { width: 100%; height: 100%; - background-color: #111827; + background-color: #1a1a1a; display: flex; flex-direction: column; } .container.light { - background-color: #f9fafb; + background-color: #eaeaea; } /* Tab Navigation */ .tabsContainer { - border-bottom: 1px solid #374151; - background-color: #1f2937; + border-bottom: 1px solid #3a3a3a; + background-color: #262626; } .tabsContainer.light { - border-bottom-color: #e5e7eb; - background-color: #f9fafb; + border-bottom-color: #e5e5e5; + background-color: #fafafa; } .tabs { @@ -34,7 +34,7 @@ padding: 0.75rem 1rem; background: none; border: none; - color: #9ca3af; + color: #a0a0a0; font-size: 0.875rem; font-weight: 500; cursor: pointer; @@ -43,7 +43,7 @@ } .tab:hover { - color: #d1d5db; + color: #ffffff; } .tab.light { @@ -55,8 +55,12 @@ } .tabActive { - color: #3b82f6 !important; - border-bottom-color: #3b82f6; + color: #ffffff !important; + border-bottom-color: #10b981; +} + +.tabActive.light { + color: #1a1a1a !important; } .scrollContainer { @@ -80,7 +84,7 @@ } .section { - background-color: #1f2937; + background-color: #262626; border-radius: 0.5rem; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); padding: 1rem; @@ -95,7 +99,7 @@ margin: 0 0 0.75rem 0; font-size: 1.125rem; font-weight: 600; - color: #f9fafb; + color: #fafafa; } .container.light .sectionTitle { diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css index f42ae16..84a839d 100644 --- a/src/renderer/src/assets/base.css +++ b/src/renderer/src/assets/base.css @@ -35,8 +35,8 @@ --bg-secondary: #262626; --bg-tertiary: #333333; --bg-quaternary: #1a1a1a; - --bg-accent: rgba(74, 144, 226, 0.1); - --bg-accent-hover: rgba(74, 144, 226, 0.15); + --bg-accent: rgba(107, 114, 128, 0.1); + --bg-accent-hover: rgba(107, 114, 128, 0.15); --bg-success: rgba(16, 185, 129, 0.1); --bg-error: rgba(239, 68, 68, 0.1); --bg-disabled: #333333; @@ -51,16 +51,16 @@ --border-primary: #444444; --border-secondary: #333333; - --border-accent: #4a90e2; + --border-accent: #6b7280; --border-success: #10b981; --border-error: #ef4444; --border-warning: #f59e0b; - --accent-primary: #4a90e2; - --accent-secondary: #6366f1; - --accent-tertiary: #8b5cf6; - --accent-gradient: linear-gradient(135deg, #4a90e2, #6366f1); - --accent-gradient-hover: linear-gradient(135deg, #4f46e5, #7c3aed); + --accent-primary: #6b7280; + --accent-secondary: #9ca3af; + --accent-tertiary: #d1d5db; + --accent-gradient: linear-gradient(135deg, #6b7280, #9ca3af); + --accent-gradient-hover: linear-gradient(135deg, #4b5563, #6b7280); --success-color: #10b981; --success-hover: #059669; @@ -72,12 +72,12 @@ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1); --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15); - --shadow-accent: 0 2px 4px rgba(59, 130, 246, 0.3); - --shadow-accent-hover: 0 4px 8px rgba(59, 130, 246, 0.4); + --shadow-accent: 0 2px 4px rgba(107, 114, 128, 0.3); + --shadow-accent-hover: 0 4px 8px rgba(107, 114, 128, 0.4); --shadow-success: 0 1px 2px rgba(16, 185, 129, 0.3); --shadow-error: 0 1px 2px rgba(239, 68, 68, 0.3); - --focus-ring: 0 0 0 2px rgba(59, 130, 246, 0.1); + --focus-ring: 0 0 0 2px rgba(107, 114, 128, 0.1); --focus-ring-success: 0 0 0 2px rgba(16, 185, 129, 0.1); --focus-ring-error: 0 0 0 2px rgba(239, 68, 68, 0.1); } @@ -132,8 +132,8 @@ body.light { --bg-secondary: #f8f8f8; --bg-tertiary: #e8e8e8; --bg-quaternary: #ffffff; - --bg-accent: rgba(74, 144, 226, 0.05); - --bg-accent-hover: rgba(74, 144, 226, 0.08); + --bg-accent: rgba(107, 114, 128, 0.05); + --bg-accent-hover: rgba(107, 114, 128, 0.08); --bg-success: rgba(16, 185, 129, 0.08); --bg-error: rgba(239, 68, 68, 0.08); --bg-disabled: #e8e8e8; @@ -146,9 +146,9 @@ body.light { --text-on-accent: #ffffff; --text-disabled: #999999; - --border-primary: #d0d0d0; + --border-primary: #a0a0a0; --border-secondary: #e0e0e0; - --border-accent: #4a90e2; + --border-accent: #6b7280; --border-success: #10b981; --border-error: #ef4444; --border-warning: #f59e0b; @@ -156,8 +156,8 @@ body.light { --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.03); --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.05); --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.08); - --shadow-accent: 0 2px 4px rgba(59, 130, 246, 0.3); - --shadow-accent-hover: 0 4px 8px rgba(59, 130, 246, 0.4); + --shadow-accent: 0 2px 4px rgba(107, 114, 128, 0.3); + --shadow-accent-hover: 0 4px 8px rgba(107, 114, 128, 0.4); --shadow-success: 0 1px 2px rgba(16, 185, 129, 0.3); --shadow-error: 0 1px 2px rgba(239, 68, 68, 0.3); } diff --git a/src/renderer/src/assets/settings.css b/src/renderer/src/assets/settings.css index fff12b4..e5ceea9 100644 --- a/src/renderer/src/assets/settings.css +++ b/src/renderer/src/assets/settings.css @@ -111,7 +111,7 @@ body { } .toggle-switch.active { - background: #3b82f6; /* Blue color */ + background: #10b981; /* Green color */ } .toggle-slider { diff --git a/src/renderer/src/components/clips/clip/Clip.module.css b/src/renderer/src/components/clips/clip/Clip.module.css index 8290411..f0208cb 100644 --- a/src/renderer/src/components/clips/clip/Clip.module.css +++ b/src/renderer/src/components/clips/clip/Clip.module.css @@ -22,7 +22,24 @@ } .clipRow.light:hover { - background-color: #e8e8e8; + background-color: #e0e0e0; +} + +/* Zebra striping for even rows */ +.clip:nth-child(even) .clipRow { + background-color: #383838; +} + +.clip:nth-child(even) .clipRow.light { + background-color: #f0f0f0; +} + +.clip:nth-child(even) .clipRow:hover { + background-color: #505050; +} + +.clip:nth-child(even) .clipRow.light:hover { + background-color: #e0e0e0; } .clipRow.expanded { diff --git a/src/renderer/src/components/clips/clip/ClipContextMenu.module.css b/src/renderer/src/components/clips/clip/ClipContextMenu.module.css new file mode 100644 index 0000000..4b83b06 --- /dev/null +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.module.css @@ -0,0 +1,122 @@ +/* Context Menu */ +.contextMenu { + position: fixed; + background: #2d2d2d; + border: 1px solid #444; + border-radius: 6px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + padding: 4px 0; + min-width: 220px; + width: max-content; + max-width: 280px; + z-index: 10000; + animation: contextMenuFadeIn 0.15s ease-out; + backdrop-filter: blur(8px); +} + +.contextMenu.light { + background: #ffffff; + border-color: #d1d5db; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +@keyframes contextMenuFadeIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-4px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* Menu Items */ +.menuItem { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + color: #ffffff; + font-size: 14px; + cursor: pointer; + transition: background-color 0.15s ease; + user-select: none; + white-space: nowrap; +} + +.contextMenu.light .menuItem { + color: #1f2937; +} + +.menuItem:hover:not(.disabled) { + background-color: #404040; +} + +.contextMenu.light .menuItem:hover:not(.disabled) { + background-color: #f3f4f6; +} + +.menuItem.disabled { + color: #9ca3af; + cursor: not-allowed; + opacity: 0.6; +} + +.contextMenu.light .menuItem.disabled { + color: #9ca3af; +} + +.menuItem.warning:hover:not(.disabled) { + background-color: #f59e0b; + color: #ffffff; +} + +.contextMenu.light .menuItem.warning:hover:not(.disabled) { + background-color: #f59e0b; + color: #ffffff; +} + +.menuItem.danger:hover:not(.disabled) { + background-color: #dc3545; + color: #ffffff; +} + +.contextMenu.light .menuItem.danger:hover:not(.disabled) { + background-color: #dc3545; + color: #ffffff; +} + +.menuItem.highlighted { + background-color: rgba(59, 130, 246, 0.1); +} + +.contextMenu.light .menuItem.highlighted { + background-color: rgba(59, 130, 246, 0.12); +} + +.menuItem.highlighted:hover:not(.disabled) { + background-color: rgba(59, 130, 246, 0.2); +} + +.contextMenu.light .menuItem.highlighted:hover:not(.disabled) { + background-color: rgba(59, 130, 246, 0.18); +} + +/* Menu Icon */ +.menuIcon { + width: 14px; + height: 14px; + flex-shrink: 0; +} + +/* Separator */ +.separator { + height: 1px; + background-color: #4b5563; + margin: 4px 0; +} + +.contextMenu.light .separator { + background-color: #e5e7eb; +} diff --git a/src/renderer/src/components/clips/clip/ClipContextMenu.tsx b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx new file mode 100644 index 0000000..9f3c428 --- /dev/null +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx @@ -0,0 +1,185 @@ +import { useState, useEffect, useRef } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useClips } from '../../../providers/clips'; +import { useTheme } from '../../../providers/theme'; +import classNames from 'classnames'; +import styles from './ClipContextMenu.module.css'; + +interface ClipContextMenuProps { + index: number; + x: number; + y: number; + onClose: () => void; +} + +export function ClipContextMenu({ index, x, y, onClose }: ClipContextMenuProps) { + const { isLight } = useTheme(); + const { isClipLocked, toggleClipLock, emptyClip, getClip, copyClipToClipboard } = useClips(); + const menuRef = useRef(null); + + const clip = getClip(index); + const isFirstClip = index === 0; + + // Check for patterns + const [hasPatterns, setHasPatterns] = useState(false); + + useEffect(() => { + let isCancelled = false; + + const checkPatterns = async () => { + if (!clip.content || clip.content.trim().length === 0) { + setHasPatterns(false); + return; + } + + try { + const matches = await window.api.quickClipsScanText(clip.content); + if (!isCancelled) { + setHasPatterns(matches.length > 0); + } + } catch { + if (!isCancelled) { + setHasPatterns(false); + } + } + }; + + checkPatterns(); + + return () => { + isCancelled = true; + }; + }, [clip.content]); + + // Handle clicks outside menu + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscape); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscape); + }; + }, [onClose]); + + // Position the menu to stay within viewport + useEffect(() => { + if (menuRef.current) { + const menu = menuRef.current; + const rect = menu.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight, + }; + + let adjustedX = x; + let adjustedY = y; + + // Adjust X position if menu would go off-screen + if (x + rect.width > viewport.width) { + adjustedX = viewport.width - rect.width - 10; + } + + // Adjust Y position if menu would go off-screen + if (y + rect.height > viewport.height) { + adjustedY = viewport.height - rect.height - 10; + } + + menu.style.left = `${Math.max(10, adjustedX)}px`; + menu.style.top = `${Math.max(10, adjustedY)}px`; + } + }, [x, y]); + + const handleCopyClick = async () => { + await copyClipToClipboard(index); + onClose(); + }; + + const handleLockClick = () => { + if (!isFirstClip) { + toggleClipLock(index); + } + onClose(); + }; + + const handleDeleteClick = () => { + if (!isFirstClip) { + emptyClip(index); + } + onClose(); + }; + + const handleScanClick = async () => { + if (isFirstClip) { + onClose(); + return; + } + + try { + await window.api.openToolsLauncher(clip.content); + onClose(); + } catch (error) { + console.error('Failed to open tools launcher:', error); + } + }; + + return ( +
e.stopPropagation()} + > +
+ + Copy to Clipboard +
+ +
+ +
+ + {hasPatterns ? 'Scan with Quick Clips ⚡' : 'Scan with Quick Clips'} +
+ +
+ +
+ + {isClipLocked(index) ? 'Unlock Clip' : 'Lock Clip'} +
+ +
+ + Delete Clip +
+
+ ); +} diff --git a/src/renderer/src/components/clips/clip/ClipWrapper.tsx b/src/renderer/src/components/clips/clip/ClipWrapper.tsx index 65aedc4..554f4bc 100644 --- a/src/renderer/src/components/clips/clip/ClipWrapper.tsx +++ b/src/renderer/src/components/clips/clip/ClipWrapper.tsx @@ -3,8 +3,10 @@ import { useState } from 'react'; import { ClipItem, useClips } from '../../../providers/clips'; import { useTheme } from '../../../providers/theme'; import { usePatternDetection } from '../../../hooks/usePatternDetection'; +import { useContextMenu } from '../../../hooks/useContextMenu'; import styles from './Clip.module.css'; import { ClipOptions } from './ClipOptions'; +import { ClipContextMenu } from './ClipContextMenu'; import { TextClip } from './TextClip'; import { HtmlClip } from './HtmlClip'; import { ImageClip } from './ImageClip'; @@ -21,6 +23,7 @@ export function ClipWrapper({ clip, index }: ClipProps): React.JSX.Element { const { copyClipToClipboard, clipCopyIndex, updateClip } = useClips(); const { isLight } = useTheme(); const { hasPatterns } = usePatternDetection(clip.content); + const { contextMenu, openContextMenu, closeContextMenu } = useContextMenu(); const [isExpanded, setIsExpanded] = useState(false); const handleRowNumberClick = async () => { @@ -35,6 +38,10 @@ export function ClipWrapper({ clip, index }: ClipProps): React.JSX.Element { setIsExpanded(isEditing); }; + const handleContextMenu = (event: React.MouseEvent) => { + openContextMenu(event, index); + }; + const renderClipContent = () => { switch (clip.type) { case 'html': @@ -63,6 +70,7 @@ export function ClipWrapper({ clip, index }: ClipProps): React.JSX.Element { { [styles.light]: isLight }, { [styles.expanded]: isExpanded } )} + onContextMenu={handleContextMenu} >
+ + {/* Context Menu */} + {contextMenu.isOpen && contextMenu.targetIndex === index && ( + + )} ); } diff --git a/src/renderer/src/components/settings/HotkeyManager.module.css b/src/renderer/src/components/settings/HotkeyManager.module.css index 3b35f5f..f869e67 100644 --- a/src/renderer/src/components/settings/HotkeyManager.module.css +++ b/src/renderer/src/components/settings/HotkeyManager.module.css @@ -159,7 +159,11 @@ } .toggle input:checked + .slider { - background-color: var(--accent-primary); + background-color: #10b981; +} + +.toggle.light input:checked + .slider { + background-color: #10b981; } .toggle input:checked + .slider:before { @@ -295,20 +299,30 @@ .instructions { margin-top: 20px; padding: 16px; - background-color: var(--bg-accent); + background-color: rgba(59, 130, 246, 0.15); border-radius: 6px; - border: 1px solid var(--border-accent); + border-left: 4px solid #3b82f6; + border-top: 1px solid rgba(59, 130, 246, 0.3); + border-right: 1px solid rgba(59, 130, 246, 0.3); + border-bottom: 1px solid rgba(59, 130, 246, 0.3); } .instructions.light { - background-color: var(--bg-accent); - border-color: var(--border-accent); + background-color: rgba(59, 130, 246, 0.08); + border-left-color: #3b82f6; + border-top-color: rgba(59, 130, 246, 0.2); + border-right-color: rgba(59, 130, 246, 0.2); + border-bottom-color: rgba(59, 130, 246, 0.2); } .instructions h3 { margin: 0 0 8px 0; font-size: 1.1rem; - color: var(--accent-primary); + color: #60a5fa; +} + +.instructions.light h3 { + color: #2563eb; } .instructions ul { diff --git a/src/renderer/src/components/settings/QuickClipsManager.module.css b/src/renderer/src/components/settings/QuickClipsManager.module.css index 8609825..b518c84 100644 --- a/src/renderer/src/components/settings/QuickClipsManager.module.css +++ b/src/renderer/src/components/settings/QuickClipsManager.module.css @@ -2,10 +2,7 @@ .container { display: flex; flex-direction: column; - height: 100vh; - min-height: 600px; padding: 0.75rem; - overflow: hidden; background-color: var(--bg-primary); border-radius: 0.5rem; gap: 0.75rem; @@ -512,7 +509,7 @@ display: inline-block; width: 1rem; height: 1rem; - border: 2px solid rgba(59, 130, 246, 0.3); + border: 2px solid rgba(107, 114, 128, 0.3); border-radius: 50%; border-top-color: var(--accent-primary); animation: spin 1s ease-in-out infinite; @@ -538,7 +535,7 @@ .progressBar { width: 100%; height: 4px; - background-color: rgba(59, 130, 246, 0.2); + background-color: rgba(107, 114, 128, 0.2); border-radius: 2px; overflow: hidden; margin: 1rem 0; @@ -928,9 +925,9 @@ .builtinSelect { background-color: var(--bg-quaternary); border: 1px solid var(--border-primary); - border-radius: 0.25rem; - padding: 0.25rem 0.5rem; - font-size: 0.75rem; + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; cursor: pointer; min-width: 150px; color: var(--text-primary); @@ -991,7 +988,7 @@ } .cancelButton:hover { - background: linear-gradient(135deg, #6b7280, #4b5563); + background: linear-gradient(135deg, #6b7280, #4a4a4a); color: var(--text-inverse); transform: translateY(-1px); box-shadow: 0 2px 4px rgba(107, 114, 128, 0.3); diff --git a/src/renderer/src/components/settings/StorageSettings.module.css b/src/renderer/src/components/settings/StorageSettings.module.css index 35b2478..94e30a4 100644 --- a/src/renderer/src/components/settings/StorageSettings.module.css +++ b/src/renderer/src/components/settings/StorageSettings.module.css @@ -22,7 +22,7 @@ animation: spin 1s linear infinite; width: 1.25rem; height: 1.25rem; - color: #2563eb; + color: #6b7280; } @keyframes spin { @@ -36,11 +36,11 @@ /* Default to dark theme */ .loadingText { - color: #9ca3af; + color: #a0a0a0; } .loadingText.light { - color: #4b5563; + color: #666666; } .errorContainer { @@ -70,17 +70,17 @@ } .errorTitle.light { - color: #111827; + color: #1a1a1a; } .errorDescription { - color: #9ca3af; + color: #a0a0a0; font-size: 0.875rem; margin-top: 0.25rem; } .errorDescription.light { - color: #6b7280; + color: #666666; } .section { @@ -97,7 +97,7 @@ } .sectionTitle.light { - color: #111827; + color: #1a1a1a; } /* Statistics - Default to dark theme */ @@ -115,13 +115,13 @@ /* Default to dark theme */ .statCardBlue { - background-color: rgba(37, 99, 235, 0.2); - border-color: rgba(37, 99, 235, 0.8); + background-color: rgba(107, 114, 128, 0.2); + border-color: rgba(107, 114, 128, 0.8); } .statCardBlue.light { - background-color: #eff6ff; - border-color: #bfdbfe; + background-color: #fafafa; + border-color: #d4d4d4; } .statCardGreen { @@ -158,11 +158,11 @@ /* Default to dark theme */ .statIconBlue { - color: #60a5fa; + color: #a0a0a0; } .statIconBlue.light { - color: #2563eb; + color: #666666; } .statIconGreen { @@ -192,11 +192,11 @@ /* Default to dark theme */ .statValueBlue { - color: #bfdbfe; + color: #d4d4d4; } .statValueBlue.light { - color: #1e3a8a; + color: #333333; } .statValueGreen { @@ -222,11 +222,11 @@ /* Default to dark theme */ .statLabelBlue { - color: #93c5fd; + color: #a0a0a0; } .statLabelBlue.light { - color: #1e40af; + color: #666666; } .statLabelGreen { @@ -257,14 +257,14 @@ align-items: center; justify-content: space-between; padding: 1rem; - background-color: #374151; + background-color: #333333; border-radius: 0.5rem; - border: 1px solid #4b5563; + border: 1px solid #4a4a4a; } .settingItem.light { - background-color: #f9fafb; - border-color: #e5e7eb; + background-color: #fafafa; + border-color: #e5e5e5; } .settingItemDisabled { @@ -278,37 +278,37 @@ .settingLabel { font-size: 0.875rem; font-weight: 500; - color: #d1d5db; + color: #d4d4d4; } .settingLabel.light { - color: #374151; + color: #333333; } .settingDescription { font-size: 0.75rem; - color: #9ca3af; + color: #a0a0a0; margin-top: 0.25rem; } .settingDescription.light { - color: #6b7280; + color: #666666; } .input { width: 5rem; padding: 0.5rem 0.75rem; font-size: 0.875rem; - border: 1px solid #4b5563; + border: 1px solid #4a4a4a; border-radius: 0.375rem; - background-color: #1f2937; + background-color: #262626; color: white; } .input:focus { outline: none; - border-color: #2563eb; - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.5); + border-color: #9ca3af; + box-shadow: 0 0 0 2px rgba(156, 163, 175, 0.5); } .input:disabled { @@ -317,28 +317,28 @@ } .input.light { - border-color: #d1d5db; + border-color: #d4d4d4; background-color: white; - color: #111827; + color: #1a1a1a; } .input.light:focus { - border-color: #2563eb; + border-color: #9ca3af; } .select { padding: 0.5rem 0.75rem; font-size: 0.875rem; - border: 1px solid #4b5563; + border: 1px solid #4a4a4a; border-radius: 0.375rem; - background-color: #1f2937; + background-color: #262626; color: white; } .select:focus { outline: none; - border-color: #2563eb; - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.5); + border-color: #9ca3af; + box-shadow: 0 0 0 2px rgba(156, 163, 175, 0.5); } .select:disabled { @@ -347,13 +347,13 @@ } .select.light { - border-color: #d1d5db; + border-color: #d4d4d4; background-color: white; - color: #111827; + color: #1a1a1a; } .select.light:focus { - border-color: #2563eb; + border-color: #9ca3af; } /* Toggle Switch - Default to dark theme */ @@ -373,18 +373,18 @@ .toggleSwitch { width: 2.75rem; height: 1.5rem; - background-color: #4b5563; + background-color: #4a4a4a; border-radius: 0.75rem; position: relative; transition: background-color 0.2s; } .toggleSwitch:focus-within { - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.3); + box-shadow: 0 0 0 2px rgba(156, 163, 175, 0.3); } .toggleSwitch.light { - background-color: #e5e7eb; + background-color: #c0c0c0; } .toggleSwitchDisabled { @@ -393,7 +393,11 @@ } .toggleSwitchChecked { - background-color: #2563eb; + background-color: #10b981; +} + +.toggleSwitchChecked.light { + background-color: #10b981; } .toggleSlider { @@ -438,13 +442,13 @@ } .buttonBlue { - background-color: #2563eb; + background-color: #6b7280; color: white; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .buttonBlue:hover { - background-color: #1d4ed8; + background-color: #4a4a4a; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } @@ -538,7 +542,7 @@ align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; - background-color: #2563eb; + background-color: #6b7280; color: white; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); @@ -552,18 +556,18 @@ /* Close Button - Default to dark theme */ .closeButtonContainer { padding-top: 1rem; - border-top: 1px solid #374151; + border-top: 1px solid #3a3a3a; } .closeButtonContainer.light { - border-top-color: #e5e7eb; + border-top-color: #e5e5e5; } /* Range Slider */ .rangeSlider { width: 100px; height: 6px; - background: #4b5563; + background: #4a4a4a; border-radius: 3px; outline: none; opacity: 0.7; @@ -592,7 +596,7 @@ appearance: none; width: 16px; height: 16px; - background: #2563eb; + background: #6b7280; border-radius: 50%; cursor: pointer; } @@ -600,7 +604,7 @@ .rangeSlider::-moz-range-thumb { width: 16px; height: 16px; - background: #2563eb; + background: #6b7280; border-radius: 50%; cursor: pointer; border: none; @@ -611,17 +615,17 @@ } .rangeSlider.light::-webkit-slider-thumb { - background: #2563eb; + background: #6b7280; } .rangeSlider.light::-moz-range-thumb { - background: #2563eb; + background: #6b7280; } .closeButton { width: 100%; padding: 0.5rem 1rem; - background-color: #4b5563; + background-color: #4a4a4a; color: white; border-radius: 0.5rem; font-weight: 500; @@ -633,7 +637,7 @@ } .closeButton:hover { - background-color: #374151; + background-color: #3a3a3a; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } diff --git a/src/renderer/src/components/settings/TemplateManager.module.css b/src/renderer/src/components/settings/TemplateManager.module.css index 0457c4f..9ed89ec 100644 --- a/src/renderer/src/components/settings/TemplateManager.module.css +++ b/src/renderer/src/components/settings/TemplateManager.module.css @@ -16,16 +16,16 @@ margin: 0; font-size: 1.125rem; font-weight: 600; - color: #f9fafb; + color: #fafafa; } .title.light { - color: #111827; + color: #1a1a1a; } .createButton { padding: 0.5rem 1rem; - background-color: #3b82f6; + background-color: #6b7280; color: white; border: none; border-radius: 0.375rem; @@ -36,15 +36,15 @@ } .createButton:hover { - background-color: #2563eb; + background-color: #4a4a4a; } .createButton.light { - background-color: #3b82f6; + background-color: #6b7280; } .createButton.light:hover { - background-color: #2563eb; + background-color: #4a4a4a; } .description { @@ -54,23 +54,23 @@ .descriptionText { margin: 0; font-size: 0.875rem; - color: #9ca3af; + color: #a0a0a0; line-height: 1.5; } .descriptionText.light { - color: #6b7280; + color: #666666; } .emptyState { text-align: center; padding: 2rem; - color: #9ca3af; + color: #a0a0a0; font-style: italic; } .emptyState.light { - color: #6b7280; + color: #666666; } .templatesList { @@ -80,10 +80,10 @@ } .templateCard { - background-color: #374151; + background-color: #333333; border-radius: 0.5rem; padding: 1rem; - border: 1px solid #4b5563; + border: 1px solid #4a4a4a; transition: all 0.2s ease-in-out; cursor: grab; } @@ -93,17 +93,17 @@ } .templateCard.light { - background-color: #f9fafb; - border-color: #e5e7eb; + background-color: #fafafa; + border-color: #e5e5e5; } .templateCard.editing { - border-color: #3b82f6; + border-color: #9ca3af; cursor: default; } .templateCard.editing.light { - border-color: #3b82f6; + border-color: #9ca3af; } .templateCard.dragging { @@ -132,10 +132,14 @@ flex-direction: column; gap: 0.75rem; padding-top: 0.75rem; - border-top: 1px solid #4b5563; + border-top: 1px solid #4a4a4a; margin-top: 0.25rem; } +.templateDetails.light { + border-top-color: #e5e5e5; +} + .templateTitleSection { display: flex; align-items: center; @@ -149,7 +153,7 @@ align-items: center; cursor: grab; padding: 0.25rem; - color: #9ca3af; + color: #a0a0a0; user-select: none; } @@ -170,7 +174,7 @@ margin: 0; font-size: 1rem; font-weight: 600; - color: #f9fafb; + color: #fafafa; cursor: pointer; flex: 1; min-width: 0; @@ -180,21 +184,21 @@ } .templateName:hover { - color: #3b82f6; + color: #a0a0a0; } .templateName.light { - color: #111827; + color: #1a1a1a; } .templateName.light:hover { - color: #3b82f6; + color: #666666; } .expandButton { background: none; border: none; - color: #9ca3af; + color: #a0a0a0; font-size: 0.875rem; cursor: pointer; padding: 0.25rem; @@ -203,20 +207,21 @@ } .expandButton:hover { - color: #d1d5db; + color: #d4d4d4; } .expandButton.light { - color: #6b7280; + color: #666666; } .expandButton.light:hover { - color: #374151; + color: #4a4a4a; } .templateActions { display: flex; gap: 0.5rem; + flex-shrink: 0; } .editButton, @@ -235,22 +240,24 @@ .editButton { background-color: transparent; border-color: #6b7280; - color: #d1d5db; + color: #d4d4d4; } .editButton:hover { - border-color: #9ca3af; - color: #f9fafb; + background-color: #6b7280; + border-color: #6b7280; + color: white; } .editButton.light { border-color: #9ca3af; - color: #374151; + color: #333333; } .editButton.light:hover { + background-color: #6b7280; border-color: #6b7280; - color: #111827; + color: white; } .deleteButton { @@ -288,17 +295,17 @@ .cancelButton { background-color: transparent; border-color: #6b7280; - color: #d1d5db; + color: #d4d4d4; } .cancelButton:hover { border-color: #9ca3af; - background-color: #374151; + background-color: #333333; } .cancelButton.light { border-color: #9ca3af; - color: #374151; + color: #333333; } .cancelButton.light:hover { @@ -307,23 +314,23 @@ } .templateContent { - background-color: #1f2937; + background-color: #262626; border-radius: 0.375rem; padding: 0.75rem; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; font-size: 0.875rem; line-height: 1.5; - color: #d1d5db; + color: #d4d4d4; white-space: pre-wrap; word-wrap: break-word; margin: 0; - border: 1px solid #374151; + border: 1px solid #3a3a3a; } .templateContent.light { - background-color: #f8fafc; - color: #374151; - border-color: #e2e8f0; + background-color: #e8e8e8; + color: #333333; + border-color: #d4d4d4; } .editForm { @@ -334,31 +341,31 @@ .nameInput { padding: 0.5rem; - background-color: #1f2937; - border: 1px solid #374151; + background-color: #262626; + border: 1px solid #3a3a3a; border-radius: 0.375rem; - color: #f9fafb; + color: #fafafa; font-size: 0.875rem; } .nameInput.light { background-color: white; - border-color: #d1d5db; - color: #111827; + border-color: #d4d4d4; + color: #1a1a1a; } .nameInput:focus { outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 1px #3b82f6; + border-color: #9ca3af; + box-shadow: 0 0 0 1px #9ca3af; } .contentTextarea { padding: 0.75rem; - background-color: #1f2937; - border: 1px solid #374151; + background-color: #262626; + border: 1px solid #3a3a3a; border-radius: 0.375rem; - color: #f9fafb; + color: #fafafa; font-size: 0.875rem; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; line-height: 1.5; @@ -368,14 +375,27 @@ .contentTextarea.light { background-color: white; - border-color: #d1d5db; - color: #111827; + border-color: #d4d4d4; + color: #1a1a1a; } .contentTextarea:focus { outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 1px #3b82f6; + border-color: #9ca3af; + box-shadow: 0 0 0 1px #9ca3af; +} + +.templateFooter { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding-top: 0.75rem; + border-top: 1px solid #4a4a4a; +} + +.templateFooter.light { + border-top-color: #e5e5e5; } .tokenPreview { @@ -383,20 +403,22 @@ align-items: center; gap: 0.5rem; flex-wrap: wrap; + flex: 1; + min-width: 0; } .tokenLabel { font-size: 0.75rem; - color: #9ca3af; + color: #a0a0a0; font-weight: 500; } .tokenLabel.light { - color: #6b7280; + color: #666666; } .token { - background-color: #3b82f6; + background-color: #6b7280; color: white; padding: 0.125rem 0.375rem; border-radius: 0.25rem; diff --git a/src/renderer/src/components/settings/TemplateManager.tsx b/src/renderer/src/components/settings/TemplateManager.tsx index 1cbfd1a..202a247 100644 --- a/src/renderer/src/components/settings/TemplateManager.tsx +++ b/src/renderer/src/components/settings/TemplateManager.tsx @@ -292,58 +292,58 @@ export function TemplateManager(): React.JSX.Element {
) : (
-
-
-
- ⋮⋮ -
-

handleToggleExpand(template.id)} - > - {template.name} -

- -
-
- - +
+
+ ⋮⋮
+

handleToggleExpand(template.id)} + > + {template.name} +

+
{expandedId === template.id && ( -
+
                         {template.content}
                       
-
- - Tokens: - - {extractTokens(template.content).map((token) => ( - - {token} +
+
+ + Tokens: - ))} + {extractTokens(template.content).map((token) => ( + + {token} + + ))} +
+
+ + +
)} diff --git a/src/renderer/src/components/settings/UpdaterControl.module.css b/src/renderer/src/components/settings/UpdaterControl.module.css index 169f280..f7be70b 100644 --- a/src/renderer/src/components/settings/UpdaterControl.module.css +++ b/src/renderer/src/components/settings/UpdaterControl.module.css @@ -7,14 +7,14 @@ .statusCard { padding: 1rem; - background-color: #374151; + background-color: #333333; border-radius: 0.5rem; - border: 1px solid #4b5563; + border: 1px solid #4a4a4a; } .statusCard.light { - background-color: #f9fafb; - border-color: #e5e7eb; + background-color: #fafafa; + border-color: #e5e5e5; } .statusContent { @@ -59,11 +59,11 @@ .statusText { font-size: 0.875rem; font-weight: 500; - color: #d1d5db; + color: #d4d4d4; } .statusText.light { - color: #374151; + color: #333333; } .statusValue { @@ -71,7 +71,7 @@ } .statusValue.light { - color: #111827; + color: #1a1a1a; } .buttonContainer { @@ -98,25 +98,25 @@ } .buttonPrimary { - background-color: #2563eb; + background-color: #6b7280; color: white; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .buttonPrimary:hover:not(:disabled) { - background-color: #1d4ed8; + background-color: #4a4a4a; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .buttonPrimary:disabled { background-color: #f3f4f6; - color: #9ca3af; + color: #a0a0a0; cursor: not-allowed; } .buttonPrimary:disabled.dark { - background-color: #374151; - color: #6b7280; + background-color: #333333; + color: #666666; } .buttonSuccess { @@ -147,9 +147,9 @@ .helperText { font-size: 0.75rem; - color: #9ca3af; + color: #a0a0a0; } .helperText.light { - color: #6b7280; + color: #666666; } diff --git a/src/renderer/src/components/settings/Versions.module.css b/src/renderer/src/components/settings/Versions.module.css index 966b081..22bfec5 100644 --- a/src/renderer/src/components/settings/Versions.module.css +++ b/src/renderer/src/components/settings/Versions.module.css @@ -5,22 +5,15 @@ } .versionFooter { - padding: 0.75rem 0; text-align: center; - border-top: 1px solid #4b5563; - background-color: transparent; -} - -.versionFooter.light { - border-top-color: #e5e7eb; } .versionText { font-size: 0.875rem; font-weight: 500; - color: #9ca3af; + color: #a0a0a0; } .versionText.light { - color: #6b7280; + color: #666666; } diff --git a/src/renderer/src/hooks/useContextMenu.ts b/src/renderer/src/hooks/useContextMenu.ts new file mode 100644 index 0000000..0b3497b --- /dev/null +++ b/src/renderer/src/hooks/useContextMenu.ts @@ -0,0 +1,42 @@ +import { useState, useCallback } from 'react'; + +interface ContextMenuState { + isOpen: boolean; + x: number; + y: number; + targetIndex?: number; +} + +export function useContextMenu() { + const [contextMenu, setContextMenu] = useState({ + isOpen: false, + x: 0, + y: 0, + }); + + const openContextMenu = useCallback((event: React.MouseEvent, targetIndex?: number) => { + event.preventDefault(); + event.stopPropagation(); + + setContextMenu({ + isOpen: true, + x: event.clientX, + y: event.clientY, + targetIndex, + }); + }, []); + + const closeContextMenu = useCallback(() => { + setContextMenu({ + isOpen: false, + x: 0, + y: 0, + }); + }, []); + + return { + contextMenu, + openContextMenu, + closeContextMenu, + }; +} diff --git a/src/renderer/src/hooks/useNativeContextMenu.ts b/src/renderer/src/hooks/useNativeContextMenu.ts new file mode 100644 index 0000000..e416227 --- /dev/null +++ b/src/renderer/src/hooks/useNativeContextMenu.ts @@ -0,0 +1,68 @@ +import { useEffect } from 'react'; +import { useClips } from '../providers/clips'; + +interface NativeContextMenuProps { + index: number; +} + +export function useNativeContextMenu({ index }: NativeContextMenuProps) { + const { isClipLocked, toggleClipLock, emptyClip, getClip, copyClipToClipboard } = useClips(); + + const clip = getClip(index); + const isFirstClip = index === 0; + + useEffect(() => { + const handleContextMenuAction = (data: { action: string; index: number }) => { + if (data.index !== index) return; + + switch (data.action) { + case 'copy': + copyClipToClipboard(index); + break; + case 'lock': + if (!isFirstClip) { + toggleClipLock(index); + } + break; + case 'delete': + if (!isFirstClip) { + emptyClip(index); + } + break; + case 'scan': + if (!isFirstClip) { + window.api.openToolsLauncher(clip.content).catch(console.error); + } + break; + } + }; + + window.api.onContextMenuAction(handleContextMenuAction); + + return () => { + window.api.removeContextMenuListeners(); + }; + }, [index, isFirstClip, clip.content, copyClipToClipboard, toggleClipLock, emptyClip]); + + const showContextMenu = async (event: React.MouseEvent) => { + event.preventDefault(); + + // Check for patterns + let hasPatterns = false; + try { + const matches = await window.api.quickClipsScanText(clip.content); + hasPatterns = matches.length > 0; + } catch { + hasPatterns = false; + } + + await window.api.showClipContextMenu({ + index, + isFirstClip, + isLocked: isClipLocked(index), + hasPatterns, + }); + }; + + return { showContextMenu }; +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index d02ba5b..7113f4b 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,12 +1,10 @@ import './assets/main.css'; +import './fontawesome'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; -// Load FontAwesome icons asynchronously to avoid blocking initial render -import('./fontawesome'); - createRoot(document.getElementById('root')!).render(