From ac7c6a573c6e7b5ef07ce45123bca5a779de762e Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 29 Jul 2025 12:57:51 -0700 Subject: [PATCH 01/11] Claude --- CONTEXT_MENU_GUIDE.md | 154 +++++++++++++++ src/main/ipc/index.ts | 63 +++++- src/preload/index.d.ts | 9 + src/preload/index.ts | 12 ++ .../clips/clip/ClipContextMenu.module.css | 105 ++++++++++ .../components/clips/clip/ClipContextMenu.tsx | 180 ++++++++++++++++++ .../src/components/clips/clip/ClipWrapper.tsx | 18 ++ src/renderer/src/hooks/useContextMenu.ts | 42 ++++ .../src/hooks/useNativeContextMenu.ts | 69 +++++++ 9 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 CONTEXT_MENU_GUIDE.md create mode 100644 src/renderer/src/components/clips/clip/ClipContextMenu.module.css create mode 100644 src/renderer/src/components/clips/clip/ClipContextMenu.tsx create mode 100644 src/renderer/src/hooks/useContextMenu.ts create mode 100644 src/renderer/src/hooks/useNativeContextMenu.ts diff --git a/CONTEXT_MENU_GUIDE.md b/CONTEXT_MENU_GUIDE.md new file mode 100644 index 0000000..6eca632 --- /dev/null +++ b/CONTEXT_MENU_GUIDE.md @@ -0,0 +1,154 @@ +# Context Menu Implementation Guide + +I've implemented two different approaches for adding right-click context menus to your clip items. Here's how to use them: + +## Option 1: CSS-based Context Menu (Recommended) + +This approach uses pure React and CSS, similar to your existing `ClipOptions` component but triggered by right-click instead of button click. + +### Files Created: +- `src/renderer/src/components/clips/clip/ClipContextMenu.tsx` - The context menu component +- `src/renderer/src/components/clips/clip/ClipContextMenu.module.css` - The styles +- `src/renderer/src/hooks/useContextMenu.ts` - Custom hook for managing context menu state + +### How it works: +1. The `ClipWrapper` component has been updated to include `onContextMenu={handleContextMenu}` +2. When right-clicked, it opens a custom context menu at the cursor position +3. The menu includes all the same actions as the gear menu: Copy, Scan, Lock, Delete +4. The menu automatically positions itself to stay within the viewport +5. Clicking outside or pressing Escape closes the menu + +### Features: +- **Smart Positioning**: Menu repositions if it would go off-screen +- **Visual Feedback**: Items highlight on hover, disabled items are grayed out +- **Pattern Detection**: Shows ⚡ icon when Quick Clips patterns are detected +- **Theme Support**: Works with both light and dark themes +- **Animations**: Smooth fade-in animation + +## Option 2: Native Electron Context Menu + +This approach uses Electron's native context menu system for a more platform-native experience. + +### Files Created: +- `src/main/ipc/index.ts` - Updated with context menu IPC handlers +- `src/preload/index.ts` - Updated with context menu API +- `src/preload/index.d.ts` - Updated with context menu types +- `src/renderer/src/hooks/useNativeContextMenu.ts` - Hook for native context menus + +### How it works: +1. Right-click triggers an IPC call to the main process +2. Main process creates a native OS context menu +3. Menu selections send IPC messages back to the renderer +4. Hook listens for these messages and executes the appropriate actions + +### Features: +- **Native Look**: Uses the OS's native context menu styling +- **Keyboard Navigation**: Supports arrow keys and keyboard shortcuts +- **Accessibility**: Better screen reader support +- **Platform Consistency**: Matches other native app context menus + +## Usage Examples + +### Using the CSS Context Menu (Current Implementation) + +The CSS context menu is already integrated into your `ClipWrapper` component. Just right-click any clip item to see it in action. + +```tsx +// Already implemented in ClipWrapper.tsx +return ( +
  • +
    + {/* Your existing clip content */} +
    + + {/* Context menu is conditionally rendered */} + {contextMenu.isOpen && contextMenu.targetIndex === index && ( + + )} +
  • +); +``` + +### Using the Native Context Menu + +To switch to the native context menu, replace the current implementation in `ClipWrapper.tsx`: + +```tsx +import { useNativeContextMenu } from '../../../hooks/useNativeContextMenu'; + +export function ClipWrapper({ clip, index }: ClipProps) { + // Replace useContextMenu with useNativeContextMenu + const { showContextMenu } = useNativeContextMenu({ index }); + + // ... other code ... + + return ( +
  • +
    + {/* Your existing clip content */} +
    + {/* No need to render custom context menu component */} +
  • + ); +} +``` + +## Customization + +### Adding New Menu Items + +**For CSS Context Menu:** +Edit `ClipContextMenu.tsx` to add new menu items: + +```tsx +
    + + Your New Action +
    +``` + +**For Native Context Menu:** +Edit the template array in `src/main/ipc/index.ts`: + +```typescript +const template = [ + // ... existing items ... + { + label: 'Your New Action', + click: () => { + event.sender.send('context-menu-action', { action: 'your-action', index }); + }, + }, +]; +``` + +### Styling the CSS Context Menu + +Edit `ClipContextMenu.module.css` to customize: +- Colors: Change `background`, `color`, and hover states +- Animations: Modify the `@keyframes contextMenuFadeIn` +- Spacing: Adjust `padding`, `gap`, and `margin` values +- Shadows: Update `box-shadow` for different depth effects + +## Recommendation + +I recommend using **Option 1 (CSS-based Context Menu)** because: + +1. **Consistency**: Matches your existing UI design and theme system +2. **Flexibility**: Easier to customize and style to match your app +3. **Control**: You have full control over behavior and appearance +4. **No IPC Overhead**: Doesn't require communication between processes +5. **Debugging**: Easier to debug since it's all in the renderer process + +The native context menu is better if you want maximum platform integration, but the CSS approach gives you more design control and consistency with your existing UI. 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/components/clips/clip/ClipContextMenu.module.css b/src/renderer/src/components/clips/clip/ClipContextMenu.module.css new file mode 100644 index 0000000..2d38f47 --- /dev/null +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.module.css @@ -0,0 +1,105 @@ +/* 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: 180px; + z-index: 10000; + animation: contextMenuFadeIn 0.15s ease-out; + backdrop-filter: blur(8px); +} + +.contextMenu.light { + background: #ffffff; + border-color: #dddddd; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +@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; +} + +.menuItem.light { + color: #1a1a1a; +} + +.menuItem:hover:not(.disabled) { + background-color: #404040; +} + +.menuItem.light:hover:not(.disabled) { + background-color: #f5f5f5; +} + +.menuItem.disabled { + color: #888888; + cursor: not-allowed; + opacity: 0.6; +} + +.menuItem.light.disabled { + color: #999999; +} + +.menuItem.danger:hover:not(.disabled) { + background-color: #dc3545; + color: #ffffff; +} + +.menuItem.light.danger:hover:not(.disabled) { + background-color: #dc3545; + color: #ffffff; +} + +.menuItem.highlighted { + background-color: rgba(59, 130, 246, 0.1); +} + +.menuItem.light.highlighted { + background-color: rgba(59, 130, 246, 0.1); +} + +.menuItem.highlighted:hover:not(.disabled) { + background-color: rgba(59, 130, 246, 0.2); +} + +/* Menu Icon */ +.menuIcon { + width: 14px; + height: 14px; + flex-shrink: 0; +} + +/* Separator */ +.separator { + height: 1px; + background-color: #444; + margin: 4px 0; +} + +.light .separator { + background-color: #e0e0e0; +} 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..4113e2a --- /dev/null +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx @@ -0,0 +1,180 @@ +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 () => { + 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/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..1355120 --- /dev/null +++ b/src/renderer/src/hooks/useNativeContextMenu.ts @@ -0,0 +1,69 @@ +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 = ( + _event: Electron.IpcRendererEvent, + 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': + window.api.openToolsLauncher(clip.content).catch(console.error); + break; + } + }; + + window.api.onContextMenuAction(handleContextMenuAction); + + return () => { + // Clean up listener if needed + }; + }, [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 }; +} From 9308edb4e2dd25da032c261db9141f2b24e9729c Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:44:43 -0700 Subject: [PATCH 02/11] build: optimize mac build for arm64 only and add dmg cleanup script --- .gitignore | 3 ++- electron-builder.yml | 3 +-- package.json | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) 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/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..70ab3db 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win", - "build:mac": "electron-vite build && electron-builder --mac", + "cleanup:dmg": "osascript -e 'tell application \"Finder\" to close every window whose name contains \"Clipless\"' 2>/dev/null || true; sleep 1; for vol in \"/Volumes/Clipless\"*; do [ -d \"$vol\" ] && hdiutil detach -force \"$vol\" 2>/dev/null || true; done", + "build:mac": "npm run cleanup:dmg && electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux", "release": "electron-builder" }, From 3d5f4038043140821d600f1454e1ab9e0c54c54a Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:44:50 -0700 Subject: [PATCH 03/11] style: update theme accent colors from blue to gray and toggle switch to green --- src/renderer/src/assets/base.css | 34 ++++++++++++++-------------- src/renderer/src/assets/settings.css | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) 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 { From 5e1580d24051bf404acaba03c20ee4f5fd1ad7c4 Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:44:56 -0700 Subject: [PATCH 04/11] enhance context menu styling with improved sizing and light mode support --- .../src/components/clips/clip/Clip.module.css | 19 ++++++- .../clips/clip/ClipContextMenu.module.css | 49 +++++++++++++------ .../components/clips/clip/ClipContextMenu.tsx | 2 +- 3 files changed, 52 insertions(+), 18 deletions(-) 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 index 2d38f47..4b83b06 100644 --- a/src/renderer/src/components/clips/clip/ClipContextMenu.module.css +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.module.css @@ -6,7 +6,9 @@ border-radius: 6px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); padding: 4px 0; - min-width: 180px; + min-width: 220px; + width: max-content; + max-width: 280px; z-index: 10000; animation: contextMenuFadeIn 0.15s ease-out; backdrop-filter: blur(8px); @@ -14,8 +16,8 @@ .contextMenu.light { background: #ffffff; - border-color: #dddddd; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + border-color: #d1d5db; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); } @keyframes contextMenuFadeIn { @@ -40,28 +42,39 @@ cursor: pointer; transition: background-color 0.15s ease; user-select: none; + white-space: nowrap; } -.menuItem.light { - color: #1a1a1a; +.contextMenu.light .menuItem { + color: #1f2937; } .menuItem:hover:not(.disabled) { background-color: #404040; } -.menuItem.light:hover:not(.disabled) { - background-color: #f5f5f5; +.contextMenu.light .menuItem:hover:not(.disabled) { + background-color: #f3f4f6; } .menuItem.disabled { - color: #888888; + color: #9ca3af; cursor: not-allowed; opacity: 0.6; } -.menuItem.light.disabled { - color: #999999; +.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) { @@ -69,7 +82,7 @@ color: #ffffff; } -.menuItem.light.danger:hover:not(.disabled) { +.contextMenu.light .menuItem.danger:hover:not(.disabled) { background-color: #dc3545; color: #ffffff; } @@ -78,14 +91,18 @@ background-color: rgba(59, 130, 246, 0.1); } -.menuItem.light.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; @@ -96,10 +113,10 @@ /* Separator */ .separator { height: 1px; - background-color: #444; + background-color: #4b5563; margin: 4px 0; } -.light .separator { - background-color: #e0e0e0; +.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 index 4113e2a..1586c51 100644 --- a/src/renderer/src/components/clips/clip/ClipContextMenu.tsx +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx @@ -158,7 +158,7 @@ export function ClipContextMenu({ index, x, y, onClose }: ClipContextMenuProps)
    Date: Tue, 16 Dec 2025 15:45:03 -0700 Subject: [PATCH 05/11] refactor settings UI layout and improve template manager design --- src/renderer/src/Settings.module.css | 28 ++-- .../settings/HotkeyManager.module.css | 26 +++- .../settings/QuickClipsManager.module.css | 15 +- .../settings/StorageSettings.module.css | 114 ++++++++------- .../settings/TemplateManager.module.css | 136 ++++++++++-------- .../components/settings/TemplateManager.tsx | 84 +++++------ .../settings/UpdaterControl.module.css | 28 ++-- .../components/settings/Versions.module.css | 11 +- 8 files changed, 238 insertions(+), 204 deletions(-) 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/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; } From 7526120bf442095a21d2aac8842c6e1a8fef19a9 Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:45:08 -0700 Subject: [PATCH 06/11] refactor: remove unused IpcRendererEvent parameter in context menu hook --- src/renderer/src/hooks/useNativeContextMenu.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/renderer/src/hooks/useNativeContextMenu.ts b/src/renderer/src/hooks/useNativeContextMenu.ts index 1355120..23bbac1 100644 --- a/src/renderer/src/hooks/useNativeContextMenu.ts +++ b/src/renderer/src/hooks/useNativeContextMenu.ts @@ -12,10 +12,7 @@ export function useNativeContextMenu({ index }: NativeContextMenuProps) { const isFirstClip = index === 0; useEffect(() => { - const handleContextMenuAction = ( - _event: Electron.IpcRendererEvent, - data: { action: string; index: number } - ) => { + const handleContextMenuAction = (data: { action: string; index: number }) => { if (data.index !== index) return; switch (data.action) { From 5b16f696fb74898e67455d8e2b5cc48164fa786f Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:52:26 -0700 Subject: [PATCH 07/11] fix: resolve context menu issues and Font Awesome loading - Add guard check to prevent scan action on first clip in ClipContextMenu - Add guard check to prevent scan action on first clip in useNativeContextMenu - Fix event listener cleanup in useNativeContextMenu to prevent memory leaks - Change Font Awesome import from async to sync to fix missing icons on load --- src/renderer/src/components/clips/clip/ClipContextMenu.tsx | 7 ++++++- src/renderer/src/hooks/useNativeContextMenu.ts | 6 ++++-- src/renderer/src/main.tsx | 4 +--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/clips/clip/ClipContextMenu.tsx b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx index 1586c51..9f3c428 100644 --- a/src/renderer/src/components/clips/clip/ClipContextMenu.tsx +++ b/src/renderer/src/components/clips/clip/ClipContextMenu.tsx @@ -67,7 +67,7 @@ export function ClipContextMenu({ index, x, y, onClose }: ClipContextMenuProps) document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); - + return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); @@ -122,6 +122,11 @@ export function ClipContextMenu({ index, x, y, onClose }: ClipContextMenuProps) }; const handleScanClick = async () => { + if (isFirstClip) { + onClose(); + return; + } + try { await window.api.openToolsLauncher(clip.content); onClose(); diff --git a/src/renderer/src/hooks/useNativeContextMenu.ts b/src/renderer/src/hooks/useNativeContextMenu.ts index 23bbac1..e416227 100644 --- a/src/renderer/src/hooks/useNativeContextMenu.ts +++ b/src/renderer/src/hooks/useNativeContextMenu.ts @@ -30,7 +30,9 @@ export function useNativeContextMenu({ index }: NativeContextMenuProps) { } break; case 'scan': - window.api.openToolsLauncher(clip.content).catch(console.error); + if (!isFirstClip) { + window.api.openToolsLauncher(clip.content).catch(console.error); + } break; } }; @@ -38,7 +40,7 @@ export function useNativeContextMenu({ index }: NativeContextMenuProps) { window.api.onContextMenuAction(handleContextMenuAction); return () => { - // Clean up listener if needed + window.api.removeContextMenuListeners(); }; }, [index, isFirstClip, clip.content, copyClipToClipboard, toggleClipLock, emptyClip]); 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( From c4ecbeabfdb6709e42a4f0d9ccf3bf522e06986a Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:55:38 -0700 Subject: [PATCH 08/11] chore: PR template --- .github/pull_request_template.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/pull_request_template.md 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 From ea4128b335821de28d474c1335d9806db58d59ff Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 15:58:11 -0700 Subject: [PATCH 09/11] chore: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70ab3db..638bcbb 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", From 77c0893de87950dba09c94db0ef30e21b08d52f8 Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 16:01:40 -0700 Subject: [PATCH 10/11] docs: removed artifact and updated README --- CONTEXT_MENU_GUIDE.md | 154 ------------------------------------------ README.md | 2 +- 2 files changed, 1 insertion(+), 155 deletions(-) delete mode 100644 CONTEXT_MENU_GUIDE.md diff --git a/CONTEXT_MENU_GUIDE.md b/CONTEXT_MENU_GUIDE.md deleted file mode 100644 index 6eca632..0000000 --- a/CONTEXT_MENU_GUIDE.md +++ /dev/null @@ -1,154 +0,0 @@ -# Context Menu Implementation Guide - -I've implemented two different approaches for adding right-click context menus to your clip items. Here's how to use them: - -## Option 1: CSS-based Context Menu (Recommended) - -This approach uses pure React and CSS, similar to your existing `ClipOptions` component but triggered by right-click instead of button click. - -### Files Created: -- `src/renderer/src/components/clips/clip/ClipContextMenu.tsx` - The context menu component -- `src/renderer/src/components/clips/clip/ClipContextMenu.module.css` - The styles -- `src/renderer/src/hooks/useContextMenu.ts` - Custom hook for managing context menu state - -### How it works: -1. The `ClipWrapper` component has been updated to include `onContextMenu={handleContextMenu}` -2. When right-clicked, it opens a custom context menu at the cursor position -3. The menu includes all the same actions as the gear menu: Copy, Scan, Lock, Delete -4. The menu automatically positions itself to stay within the viewport -5. Clicking outside or pressing Escape closes the menu - -### Features: -- **Smart Positioning**: Menu repositions if it would go off-screen -- **Visual Feedback**: Items highlight on hover, disabled items are grayed out -- **Pattern Detection**: Shows ⚡ icon when Quick Clips patterns are detected -- **Theme Support**: Works with both light and dark themes -- **Animations**: Smooth fade-in animation - -## Option 2: Native Electron Context Menu - -This approach uses Electron's native context menu system for a more platform-native experience. - -### Files Created: -- `src/main/ipc/index.ts` - Updated with context menu IPC handlers -- `src/preload/index.ts` - Updated with context menu API -- `src/preload/index.d.ts` - Updated with context menu types -- `src/renderer/src/hooks/useNativeContextMenu.ts` - Hook for native context menus - -### How it works: -1. Right-click triggers an IPC call to the main process -2. Main process creates a native OS context menu -3. Menu selections send IPC messages back to the renderer -4. Hook listens for these messages and executes the appropriate actions - -### Features: -- **Native Look**: Uses the OS's native context menu styling -- **Keyboard Navigation**: Supports arrow keys and keyboard shortcuts -- **Accessibility**: Better screen reader support -- **Platform Consistency**: Matches other native app context menus - -## Usage Examples - -### Using the CSS Context Menu (Current Implementation) - -The CSS context menu is already integrated into your `ClipWrapper` component. Just right-click any clip item to see it in action. - -```tsx -// Already implemented in ClipWrapper.tsx -return ( -
  • -
    - {/* Your existing clip content */} -
    - - {/* Context menu is conditionally rendered */} - {contextMenu.isOpen && contextMenu.targetIndex === index && ( - - )} -
  • -); -``` - -### Using the Native Context Menu - -To switch to the native context menu, replace the current implementation in `ClipWrapper.tsx`: - -```tsx -import { useNativeContextMenu } from '../../../hooks/useNativeContextMenu'; - -export function ClipWrapper({ clip, index }: ClipProps) { - // Replace useContextMenu with useNativeContextMenu - const { showContextMenu } = useNativeContextMenu({ index }); - - // ... other code ... - - return ( -
  • -
    - {/* Your existing clip content */} -
    - {/* No need to render custom context menu component */} -
  • - ); -} -``` - -## Customization - -### Adding New Menu Items - -**For CSS Context Menu:** -Edit `ClipContextMenu.tsx` to add new menu items: - -```tsx -
    - - Your New Action -
    -``` - -**For Native Context Menu:** -Edit the template array in `src/main/ipc/index.ts`: - -```typescript -const template = [ - // ... existing items ... - { - label: 'Your New Action', - click: () => { - event.sender.send('context-menu-action', { action: 'your-action', index }); - }, - }, -]; -``` - -### Styling the CSS Context Menu - -Edit `ClipContextMenu.module.css` to customize: -- Colors: Change `background`, `color`, and hover states -- Animations: Modify the `@keyframes contextMenuFadeIn` -- Spacing: Adjust `padding`, `gap`, and `margin` values -- Shadows: Update `box-shadow` for different depth effects - -## Recommendation - -I recommend using **Option 1 (CSS-based Context Menu)** because: - -1. **Consistency**: Matches your existing UI design and theme system -2. **Flexibility**: Easier to customize and style to match your app -3. **Control**: You have full control over behavior and appearance -4. **No IPC Overhead**: Doesn't require communication between processes -5. **Debugging**: Easier to debug since it's all in the renderer process - -The native context menu is better if you want maximum platform integration, but the CSS approach gives you more design control and consistency with your existing UI. 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 From af80f1b7ff0beaaa11bcfd84b6ff48169220ec4a Mon Sep 17 00:00:00 2001 From: Brandon Kidd Date: Tue, 16 Dec 2025 16:37:34 -0700 Subject: [PATCH 11/11] fix: remove unnecessary osascript --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 638bcbb..d5e7091 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win", - "cleanup:dmg": "osascript -e 'tell application \"Finder\" to close every window whose name contains \"Clipless\"' 2>/dev/null || true; sleep 1; for vol in \"/Volumes/Clipless\"*; do [ -d \"$vol\" ] && hdiutil detach -force \"$vol\" 2>/dev/null || true; done", - "build:mac": "npm run cleanup:dmg && electron-vite build && electron-builder --mac", + "build:mac": "electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux", "release": "electron-builder" },